import bleach import markdown from boxes.decorators import conditional_login_required as login_required from django.db.models import Q, Prefetch from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from .forms import ( BoxForm, BoxTypeForm, ThingFileForm, ThingForm, ThingFormSet, ThingLinkForm, ThingPictureForm, ) from .models import Box, BoxType, Facet, Tag, Thing, ThingFile, ThingLink def health_check(request): """Health check endpoint for Kubernetes liveness/readiness probes.""" return HttpResponse("OK", status=200) def _strip_markdown(text, max_length=100): """Convert Markdown to plain text and truncate.""" if not text: return "" html = markdown.markdown(text) plain_text = bleach.clean(html, tags=[], strip=True) plain_text = " ".join(plain_text.split()) if len(plain_text) > max_length: return plain_text[:max_length].rsplit(" ", 1)[0] + "..." return plain_text @login_required def index(request): """Home page with search and tags.""" facets = Facet.objects.all().prefetch_related("tags") facet_tag_counts = {} for facet in facets: for tag in facet.tags.all(): count = tag.things.count() if count > 0: if facet not in facet_tag_counts: facet_tag_counts[facet] = [] facet_tag_counts[facet].append((tag, count)) return render( request, "boxes/index.html", { "facets": facets, "facet_tag_counts": facet_tag_counts, }, ) @login_required def box_detail(request, box_id): """Display contents of a box.""" box = get_object_or_404(Box, pk=box_id) things = box.things.prefetch_related("tags").all() return render( request, "boxes/box_detail.html", { "box": box, "things": things, }, ) @login_required def thing_detail(request, thing_id): """Display details of a thing (read-only).""" thing = get_object_or_404( Thing.objects.select_related("box", "box__box_type").prefetch_related( "files", "links", "tags" ), pk=thing_id, ) return render(request, "boxes/thing_detail.html", {"thing": thing}) @login_required def edit_thing(request, thing_id): """Edit a thing's details.""" thing = get_object_or_404( Thing.objects.select_related("box", "box__box_type").prefetch_related( "files", "links", "tags" ), pk=thing_id, ) boxes = Box.objects.select_related("box_type").all() facets = Facet.objects.all().prefetch_related("tags") picture_form = ThingPictureForm(instance=thing) file_form = ThingFileForm() link_form = ThingLinkForm() if request.method == "POST": action = request.POST.get("action") if action == "save_details": form = ThingForm(request.POST, request.FILES, instance=thing) if form.is_valid(): form.save() return redirect("thing_detail", thing_id=thing.id) elif action == "move": new_box_id = request.POST.get("new_box") if new_box_id: new_box = get_object_or_404(Box, pk=new_box_id) thing.box = new_box thing.save() return redirect("edit_thing", thing_id=thing.id) elif action == "upload_picture": picture_form = ThingPictureForm(request.POST, request.FILES, instance=thing) if picture_form.is_valid(): picture_form.save() return redirect("edit_thing", thing_id=thing.id) elif action == "delete_picture": if thing.picture: thing.picture.delete() thing.picture = None thing.save() return redirect("edit_thing", thing_id=thing.id) elif action == "add_file": file_form = ThingFileForm(request.POST, request.FILES) if file_form.is_valid(): thing_file = file_form.save(commit=False) thing_file.thing = thing thing_file.save() return redirect("edit_thing", thing_id=thing.id) elif action == "add_link": link_form = ThingLinkForm(request.POST) if link_form.is_valid(): thing_link = link_form.save(commit=False) thing_link.thing = thing thing_link.save() return redirect("edit_thing", thing_id=thing.id) elif action == "delete_file": file_id = request.POST.get("file_id") if file_id: try: thing_file = ThingFile.objects.get(pk=file_id, thing=thing) thing_file.file.delete() thing_file.delete() except ThingFile.DoesNotExist: pass return redirect("edit_thing", thing_id=thing.id) elif action == "delete_link": link_id = request.POST.get("link_id") if link_id: try: thing_link = ThingLink.objects.get(pk=link_id, thing=thing) thing_link.delete() except ThingLink.DoesNotExist: pass return redirect("edit_thing", thing_id=thing.id) elif action == "add_tag": tag_id = request.POST.get("tag_id") if tag_id: try: tag = Tag.objects.get(pk=tag_id) if tag.facet.cardinality == Facet.Cardinality.SINGLE: existing_tags = list(thing.tags.filter(facet=tag.facet)) for existing_tag in existing_tags: thing.tags.remove(existing_tag) thing.tags.add(tag) except Tag.DoesNotExist: pass return redirect("edit_thing", thing_id=thing.id) elif action == "remove_tag": tag_id = request.POST.get("tag_id") if tag_id: try: tag = Tag.objects.get(pk=tag_id) thing.tags.remove(tag) except Tag.DoesNotExist: pass return redirect("edit_thing", thing_id=thing.id) thing_form = ThingForm(instance=thing) return render( request, "boxes/edit_thing.html", { "thing": thing, "boxes": boxes, "facets": facets, "picture_form": picture_form, "file_form": file_form, "link_form": link_form, "thing_form": thing_form, }, ) @login_required def boxes_list(request): """Boxes list page showing all boxes with contents.""" boxes = Box.objects.select_related("box_type").prefetch_related("things").all() return render( request, "boxes/boxes_list.html", { "boxes": boxes, }, ) @login_required def search_api(request): """AJAX endpoint for searching things.""" query = request.GET.get("q", "").strip() if len(query) < 2: return JsonResponse({"results": []}) # Check for "Facet:Word" format if ":" in query: parts = query.split(":", 1) facet_name = parts[0].strip() tag_name = parts[1].strip() # Search for things with specific facet and tag things = ( Thing.objects.filter( Q(tags__facet__name__icontains=facet_name) & Q(tags__name__icontains=tag_name) ) .prefetch_related("files", "links", "tags") .select_related("box") .distinct()[:50] ) else: # Normal search things = ( Thing.objects.filter( Q(name__icontains=query) | Q(description__icontains=query) | Q(files__title__icontains=query) | Q(files__file__icontains=query) | Q(links__title__icontains=query) | Q(links__url__icontains=query) | Q(tags__name__icontains=query) | Q(tags__facet__name__icontains=query) ) .prefetch_related("files", "links", "tags") .select_related("box") .distinct()[:50] ) results = [ { "id": thing.id, "name": thing.name, "box": thing.box.id, "description": _strip_markdown(thing.description), "tags": [ { "name": tag.name, "color": tag.facet.color, } for tag in thing.tags.all() ], "files": [ { "title": f.title, "filename": f.filename(), } for f in thing.files.all() ], "links": [ { "title": l.title, "url": l.url, } for l in thing.links.all() ], } for thing in things ] return JsonResponse({"results": results}) @login_required def add_things(request, box_id): """Add multiple things to a box at once.""" box = get_object_or_404(Box, pk=box_id) success_message = None if request.method == "POST": formset = ThingFormSet( request.POST, request.FILES, queryset=Thing.objects.filter(box=box) ) if formset.is_valid(): things = formset.save(commit=False) created_count = 0 for thing in things: if thing.name or thing.description or thing.picture: thing.box = box thing.save() created_count += 1 if created_count > 0: success_message = f"Added {created_count} thing{'s' if created_count > 1 else ''} successfully." formset = ThingFormSet(queryset=Thing.objects.filter(box=box)) else: formset = ThingFormSet(queryset=Thing.objects.filter(box=box)) return render( request, "boxes/add_things.html", { "box": box, "formset": formset, "success_message": success_message, }, ) @login_required def box_management(request): """Main page for managing boxes and box types.""" box_types = BoxType.objects.all().prefetch_related("boxes") boxes = Box.objects.select_related("box_type").all().prefetch_related("things") box_type_form = BoxTypeForm() box_form = BoxForm() return render( request, "boxes/box_management.html", { "box_types": box_types, "boxes": boxes, "box_type_form": box_type_form, "box_form": box_form, }, ) @login_required def add_box_type(request): """Add a new box type.""" if request.method == "POST": form = BoxTypeForm(request.POST) if form.is_valid(): form.save() return redirect("box_management") @login_required def edit_box_type(request, type_id): """Edit an existing box type.""" box_type = get_object_or_404(BoxType, pk=type_id) if request.method == "POST": form = BoxTypeForm(request.POST, instance=box_type) if form.is_valid(): form.save() return redirect("box_management") @login_required def delete_box_type(request, type_id): """Delete a box type.""" box_type = get_object_or_404(BoxType, pk=type_id) if request.method == "POST": if box_type.boxes.exists(): return redirect("box_management") box_type.delete() return redirect("box_management") @login_required def add_box(request): """Add a new box.""" if request.method == "POST": form = BoxForm(request.POST) if form.is_valid(): form.save() return redirect("box_management") @login_required def edit_box(request, box_id): """Edit an existing box.""" box = get_object_or_404(Box, pk=box_id) if request.method == "POST": form = BoxForm(request.POST, instance=box) if form.is_valid(): form.save() return redirect("box_management") @login_required def delete_box(request, box_id): """Delete a box.""" box = get_object_or_404(Box, pk=box_id) if request.method == "POST": if box.things.exists(): return redirect("box_management") box.delete() return redirect("box_management") @login_required def delete_thing(request, thing_id): """Delete a thing and its associated files.""" thing = get_object_or_404(Thing, pk=thing_id) if request.method == "POST": box_id = thing.box.id if thing.picture: thing.picture.delete(save=False) for thing_file in thing.files.all(): thing_file.file.delete(save=False) thing.delete() return redirect("box_detail", box_id=box_id) return redirect("edit_thing", thing_id=thing_id) @login_required def resources_list(request): """List all links and files from things that have them.""" things_with_files = ( Thing.objects.filter(files__isnull=False).prefetch_related("files").distinct() ) things_with_links = ( Thing.objects.filter(links__isnull=False).prefetch_related("links").distinct() ) all_things = (things_with_files | things_with_links).distinct().order_by("name") resources = [] for thing in all_things.prefetch_related("files", "links"): for file in thing.files.all(): resources.append( { "type": "file", "thing_name": thing.name, "thing_id": thing.id, "title": file.title, "url": file.file.url, } ) for link in thing.links.all(): resources.append( { "type": "link", "thing_name": thing.name, "thing_id": thing.id, "title": link.title, "url": link.url, } ) return render( request, "boxes/resources_list.html", { "resources": resources, }, ) @login_required def fixme(request): """Page to find and fix things missing tags for specific facets.""" facets = Facet.objects.all().prefetch_related("tags") selected_facet = None missing_things = [] if request.method == "GET" and "facet_id" in request.GET: try: selected_facet = Facet.objects.get(pk=request.GET["facet_id"]) # Find things that don't have any tag from this facet missing_things = ( Thing.objects.exclude(tags__facet=selected_facet) .select_related("box", "box__box_type") .prefetch_related("tags") ) except Facet.DoesNotExist: selected_facet = None elif request.method == "POST": facet_id = request.POST.get("facet_id") tag_ids = request.POST.getlist("tag_ids") thing_ids = request.POST.getlist("thing_ids") if facet_id and tag_ids and thing_ids: facet = get_object_or_404(Facet, pk=facet_id) tags = Tag.objects.filter(id__in=tag_ids, facet=facet) things = Thing.objects.filter(id__in=thing_ids) for thing in things: if facet.cardinality == Facet.Cardinality.SINGLE: # Remove existing tags from this facet thing.tags.remove(*thing.tags.filter(facet=facet)) # Add new tags for tag in tags: if tag.facet == facet: thing.tags.add(tag) return redirect("fixme") return render( request, "boxes/fixme.html", { "facets": facets, "selected_facet": selected_facet, "missing_things": missing_things, }, )