aboutsummaryrefslogtreecommitdiffstats
path: root/alphabetlearning/resources/views.py
diff options
context:
space:
mode:
Diffstat (limited to 'alphabetlearning/resources/views.py')
-rw-r--r--alphabetlearning/resources/views.py431
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})