learning-website-django1-10 Exercise 3: Reading Progress ========================================================== A logged-in visitor can mark a chapter finished (and undo it) and see, per course, how many chapters they have finished. Save as apps/progress/models.py: from django.conf import settings from django.db import models class PageProgress(models.Model): """A user has finished a page. The page is identified by its PATH, not by a foreign key: the importer may delete and recreate a page, and a foreign key would silently delete the user's progress with it. (If a file is RENAMED, its progress is orphaned: the path no longer exists. That is the price of this choice.)""" user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="progress") path = models.CharField(max_length=500) completed_at = models.DateTimeField(auto_now_add=True) class Meta: constraints = [models.UniqueConstraint(fields=["user", "path"], name="one_progress_row_per_page")] verbose_name_plural = "page progress" def __str__(self): return f"{self.user} finished {self.path}" Why the page is stored as a PATH and not a foreign key: the importer may delete and recreate a page, and a foreign key would delete the user's progress with it. A test deletes a page, recreates it and checks the progress is still there. The cost: if a FILE is renamed, its progress points at a path that no longer exists (the row stays, but it counts for nothing). Save as apps/progress/views.py: from django.contrib.auth.decorators import login_required from django.db.models import Count from django.http import Http404 from django.shortcuts import redirect, render from django.views.decorators.http import require_POST from apps.content.models import Course, Page from .models import PageProgress @login_required @require_POST def mark(request, site): """Mark a page finished (or not finished). The page is looked up on THIS site, and the redirect is built from the page we found, never from anything the visitor sent, so there is nothing to redirect elsewhere.""" page = Page.objects.light().filter(path=request.POST.get("path", ""), site=site).first() if page is None: raise Http404("No such page on this site") if request.POST.get("done") == "1": PageProgress.objects.get_or_create(user=request.user, path=page.path) else: PageProgress.objects.filter(user=request.user, path=page.path).delete() return redirect(page.url_path) @login_required def overview(request, site): """For each course on this site: how many chapters, how many finished.""" courses = list(Course.objects.filter(site=site).annotate(total=Count("pages")).order_by("folder")) paths = list(PageProgress.objects.filter(user=request.user).values_list("path", flat=True)) done = dict(Page.objects.light().filter(site=site, path__in=paths, course__isnull=False) .values_list("course_id").annotate(n=Count("id"))) rows = [{"course": c, "total": c.total, "done": done.get(c.id, 0), "percent": round(100 * done.get(c.id, 0) / c.total) if c.total else 0} for c in courses if c.total] return render(request, "progress/overview.html", { "started": [r for r in rows if r["done"]], "not_started": [r for r in rows if not r["done"]], "finished_total": sum(r["done"] for r in rows), "breadcrumbs": [], "active_folder": None, }) Save as apps/progress/templates/progress/overview.html: {% extends "theme/base.html" %} {% block title %}My progress{% endblock %} {% block content %}
{{ finished_total }} chapter{{ finished_total|pluralize }} finished on {{ site_title }}.
{% if started %}Nothing finished yet. Use “Mark as finished” at the bottom of a chapter.
{% endif %} {% if not_started %}