diff options
Diffstat (limited to 'alphabetlearning/resources/views.py')
-rw-r--r-- | alphabetlearning/resources/views.py | 431 |
1 files changed, 431 insertions, 0 deletions
diff --git a/alphabetlearning/resources/views.py b/alphabetlearning/resources/views.py new file mode 100644 index 0000000..3bf3d22 --- /dev/null +++ b/alphabetlearning/resources/views.py @@ -0,0 +1,431 @@ +import logging +import os +import tempfile +from collections.abc import Generator +from dataclasses import dataclass +from typing import Optional + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.core.paginator import Paginator +from django.db import IntegrityError +from django.db import transaction +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render + +from . import services +from .forms import ResourceCreateForm +from .forms import ResourceUpdateMetadataForm +from .forms import ResourceUpdatePDFsForm +from .forms import ResourceUpdateThumbnailsForm +from .models import PDFPageSnapshot +from .models import PDFResource +from .models import Resource +from .models import ResourceCategory +from .models import ResourceSubcategory +from .s3 import get_presigned_obj_url +from .s3 import upload_files_to_s3 +from .s3 import upload_snapshotted_pages_to_s3 +from .s3 import upload_to_s3 + +logger = logging.getLogger(__name__) + + +# I want to create a dataclass here to hold the resource information to pass to the view +@dataclass +class ResourceInfo: + id: int + name: str + description: str + card_description: str + main_resource_category_name: str + main_resource_category_colour_css_class: str + main_resource_badge_foreground_colour: str + subcategories: str | None + age_range: str | None + pdf_filenames: list[str] + pdf_urls: list[Optional[str]] + snapshot_urls: dict[str, list[str]] + thumbnail_filenames: list[str] + thumbnail_urls: list[Optional[str]] + feature_slot: int + created: str + updated: str + + +@login_required +def create_featured(request): + return render(request, "resources/create_featured_resource.html") + + +def _extract_metadata_from_resource(resource_obj) -> ResourceInfo | None: + """ + This function extracts the resource information from the model object and returns it as a + ResourceInfo object + :param resource_obj: + :return: + """ + pdf_resource_filenames = [ + x.file_name for x in PDFResource.objects.filter(resource=resource_obj).all() + ] + pdf_resources = PDFResource.objects.filter(resource=resource_obj).all() + snapshot_dict = {} + for p in pdf_resources: + snapshot_dict[p.file_name] = [ + x.file_name for x in PDFPageSnapshot.objects.filter(pdf_file=p).all() + ] + snapshot_url_dict = {} + # Iterate through the snapshot dict and generate the URLs + for k, v in snapshot_dict.items(): + snapshot_url_dict[k] = [ + get_presigned_obj_url( + settings.AWS_STORAGE_BUCKET_NAME, + f"snapshotted_pages/{f}", + ) + for f in v + ] + pdf_urls = [ + get_presigned_obj_url(settings.AWS_STORAGE_BUCKET_NAME, f"pdfuploads/{f}") + for f in pdf_resource_filenames + ] + thumbnail_urls = [ + get_presigned_obj_url(settings.AWS_STORAGE_BUCKET_NAME, f"thumbnails/{f}") + for f in resource_obj.thumbnail_filenames + ] + try: + arc_name = resource_obj.subcategories.name if resource_obj.subcategories else None + return ResourceInfo( + id=resource_obj.id, + name=resource_obj.name, + description=resource_obj.description, + card_description=resource_obj.card_description, + main_resource_category_name=resource_obj.main_resource_category.name, + main_resource_category_colour_css_class=resource_obj.main_resource_category.colour_css_class, + main_resource_badge_foreground_colour=resource_obj.main_resource_category.badge_foreground_colour, + subcategories=arc_name, + age_range=resource_obj.age_range, + pdf_filenames=pdf_resource_filenames, + pdf_urls=pdf_urls, + snapshot_urls=snapshot_url_dict, + thumbnail_filenames=resource_obj.thumbnail_filenames, + thumbnail_urls=thumbnail_urls, + feature_slot=resource_obj.feature_slot, + created=resource_obj.created_at, + updated=resource_obj.updated_at, + ) + except Exception: + logging.exception("Error extracting resource information: ") + return None + + +@login_required +def index(request): + resource_objs = Resource.objects.all() + categories = ResourceCategory.objects.all() + category = request.GET.get("category", "all") + + resource_list = [_extract_metadata_from_resource(r) for r in resource_objs] + + # Create a separate queryset for Featured resources + featured_resources = [r for r in resource_list if r.feature_slot] + featured_resources = sorted(featured_resources, key=lambda resource: resource.feature_slot) + + if category != "all": + resource_list = [r for r in resource_list if r.main_resource_category_name == category] + + paginator = Paginator(resource_list, 20) + page_number = request.GET.get("page") + page_obj = paginator.get_page(page_number) + + context = { + "page_obj": page_obj, + "categories": categories, + "featured_resources": featured_resources, + "selected_category": category, + } + return render(request, "resources/resource_list.html", context) + + +def create_metadata(pdf_files) -> Generator[tuple[services.PDFMetadata, str], None, None]: + """ + Generates PDF metadata and snapshot images for a list of PDF files. + + This function takes a list of PDF file objects, creates temporary files for each one, + and then uses the `services.get_pdf_metadata_from_path` and `services.export_pages_as_images` functions to extract metadata and snapshot images for each PDF. + + The function yields a tuple containing the PDF metadata and a list of snapshot image paths for each PDF file. + + Args: + pdf_files (list[django.core.files.uploadedfile.InMemoryUploadedFile]): A list of PDF file objects. + + Yields: + tuple[servies.PDFMetadata, list[str]]: A tuple containing the PDF metadata and a list of snapshot image paths for each PDF file. + """ + with tempfile.TemporaryDirectory() as temp_dir: + for pdf_file in pdf_files: + file_path = os.path.join(temp_dir, pdf_file.name) + + with open(file_path, "wb") as temp_file: + for chunk in pdf_file.chunks(): + temp_file.write(chunk) + + metadata = services.get_pdf_metadata_from_path(file_path) + snapshot_images = services.export_pages_as_images(file_path) + + yield metadata, snapshot_images + + +@login_required +def create_resource(request): + if request.method == "POST": + form = ResourceCreateForm(request.POST, request.FILES) + + if form.is_valid(): + pdf_files = form.cleaned_data["pdf_files"] + thumbnail_files = form.cleaned_data["thumbnail_files"] + name = form.cleaned_data["name"] + description = form.cleaned_data["description"] + card_description = form.cleaned_data["card_description"] + resource_type = form.cleaned_data["resource_type"] + age_range = form.cleaned_data["age_range"] + curriculum = form.cleaned_data["curriculum"] + main_resource_category = form.cleaned_data["main_resource_category"] + subcategories = form.cleaned_data["subcategories"] + feature_slot = form.cleaned_data["feature_slot"] + + # We use get here because we know these categories exist + subcategories_objs = [ResourceSubcategory.objects.get(name=x) for x in subcategories] + + try: + with transaction.atomic(): + resource = Resource( + name=name, + description=description, + card_description=card_description, + resource_type=resource_type, + age_range=age_range, + curriculum=curriculum, + main_resource_category=main_resource_category, + feature_slot=feature_slot, + ) + resource.save() + resource.subcategories.set(subcategories_objs) + + metadata_generator = create_metadata(pdf_files) + snapshotted_pages = [] + + for metadata, snapshot_images in metadata_generator: + pdf_resource = PDFResource.objects.create( + resource=resource, + file_name=os.path.basename(metadata.file_name), + file_size=metadata.file_size, + ) + + for snapshot_image in snapshot_images: + PDFPageSnapshot.objects.create( + name="test", + file_name=os.path.basename(snapshot_image), + pdf_file=pdf_resource, + ) + + snapshotted_pages.append(snapshot_images) + + resource.thumbnail_filenames = [f.name for f in thumbnail_files] + resource.save() + + # Reset the file pointers for pdf_files + for pdf_file in pdf_files: + pdf_file.seek(0) + + if not upload_to_s3(pdf_files, thumbnail_files, snapshotted_pages): + raise Exception("Error uploading files to S3") + + return redirect("resources:resource_detail", resource_id=resource.id) + except IntegrityError: + slot = form.cleaned_data["feature_slot"] + messages.add_message( + request, + messages.ERROR, + f"Feature slot {slot} is already " + "in use. Quit this form and remove from existing " + "resource.", + ) + except Exception: + logger.exception("Error creating resource") + form.add_error(None, "An error occurred while creating the resource.") + else: + # extract form errors + errors = {} + for field in form: + if field.errors: + errors[field.name] = field.errors + + # add non-field errors + if form.non_field_errors(): + errors["non_field_errors"] = form.non_field_errors() + + # render form with errors + return render( + request, + "resources/resource_create.html", + {"form": form, "errors": errors}, + ) + else: + form = ResourceCreateForm() + + return render(request, "resources/resource_create.html", {"form": form}) + + +@login_required +def resource_detail(request, resource_id): + """ + This function returns the resource detail page. + """ + resource_obj = get_object_or_404(Resource, pk=resource_id) + resource_metadata = _extract_metadata_from_resource(resource_obj) + resource = { + "id": resource_obj.id, + "name": resource_obj.name, + "description": resource_obj.description, + "card_description": resource_obj.card_description, + "resource_type": resource_obj.resource_type.name, + "main_resource_category": resource_obj.main_resource_category.name, + "main_resource_category_colour_css_class": resource_obj.main_resource_category.colour_css_class, + "additional_resource_category": ( + resource_obj.subcategories.name if resource_obj.subcategories else None + ), + "age_range": resource_obj.age_range, + "curriculum": resource_obj.curriculum, + "pdf_filenames": resource_metadata.pdf_filenames, + "pdf_urls": resource_metadata.pdf_urls, + "thumbnails": list( + zip( + resource_metadata.thumbnail_urls, + resource_metadata.thumbnail_filenames, + strict=False, + ), + ), + "thumbnail_filenames": resource_metadata.thumbnail_filenames, + "thumbnail_urls": resource_metadata.thumbnail_urls, + "snapshot_urls": resource_metadata.snapshot_urls, + "created": resource_metadata.created, + "updated": resource_metadata.updated, + } + return render(request, "resources/resource_detail.html", {"resource": resource}) + + +@login_required() +def update_resource_thumbnails(request, pk): + resource = get_object_or_404(Resource, pk=pk) + if request.method == "POST": + form = ResourceUpdateThumbnailsForm(request.POST, request.FILES) + if form.is_valid(): + thumbnail_files = form.cleaned_data["thumbnail_files"] + resource.thumbnail_filenames = [f.name for f in thumbnail_files] + upload_files_to_s3(thumbnail_files, "thumbnails") + resource.save() + return redirect("resources:resource_detail", resource_id=resource.id) + + else: + form = ResourceUpdateThumbnailsForm(resource=pk) + + return render(request, "resources/update_thumbnails.html", {"form": form, "resource": resource}) + + +@login_required +def hx_download_button(request): + """ + This is an HTMX view that is called when the user clicks the download button. + :param: + :return: + """ + pdf = request.GET.get("rn") + res = Resource.objects.get(pdf_filename=pdf) + return render( + request, + "resources/hx_download_button.html", + {"pdf_url": _extract_metadata_from_resource(res).pdf_url}, + ) + + +@login_required +def update_resource_metadata(request, pk): # Change resource_id to pk + resource = get_object_or_404(Resource, pk=pk) + + if request.method == "POST": + form = ResourceUpdateMetadataForm(request.POST, instance=resource) + if form.is_valid(): + form.save() + return redirect( + "resources:resource_detail", + resource_id=resource.pk, + ) # Use pk instead of resource_id + else: + form = ResourceUpdateMetadataForm(instance=resource) + + return render( + request, + "resources/resource_metadata_update.html", + {"form": form, "resource": resource}, + ) + + +@login_required() +def add_resource_pdfs(request, pk): + """ + Adds PDF files to a resource in the system. + + This view handles the process of adding PDF files to an existing resource. + It allows the user to upload one or more PDF files, which are then processed and associated with the resource. + The view creates PDFResource and PDFPageSnapshot objects to represent the uploaded PDFs and their page snapshots, and uploads the files to S3 storage. + + Args: + request (django.http.request.HttpRequest): The HTTP request object. + pk (int): The primary key of the resource to which the PDFs will be added. + + Returns: + django.http.response.HttpResponse: A redirect to the resource detail page upon successful PDF upload. + """ + resource = get_object_or_404(Resource, pk=pk) + if request.method == "POST": + form = ResourceUpdatePDFsForm(resource.get_absolute_url(), request.POST, request.FILES) + if form.is_valid(): + pdf_files = form.cleaned_data["pdf_files"] + + metadata_generator = create_metadata(pdf_files) + + snapshotted_pages = [] + + for metadata, snapshot_images in metadata_generator: + # TODO replace or add? This needs to be decided here + pdf_resource = PDFResource.objects.create( + resource=resource, + file_name=os.path.basename(metadata.file_name), + file_size=metadata.file_size, + ) + + for snapshot_image in snapshot_images: + PDFPageSnapshot.objects.create( + name="test", + file_name=os.path.basename(snapshot_image), + pdf_file=pdf_resource, + ) + + snapshotted_pages.append(snapshot_images) + + # Reset the file pointers for pdf_files + for pdf_file in pdf_files: + pdf_file.seek(0) + + upload_files_to_s3(pdf_files, "pdfuploads") + if not upload_snapshotted_pages_to_s3(snapshotted_pages): + raise Exception("Error uploading snapshotted pages to S3") + + return redirect("resources:resource_detail", resource_id=resource.id) + + else: + form = ResourceUpdatePDFsForm(resource.get_absolute_url(), resource=pk) + + return render(request, "resources/update_pdfs.html", {"form": form, "resource": resource}) |