========= The Views ========= .. module:: django_resume.views .. admonition:: About this document This section covers the core views in django-resume for displaying, creating, and deleting resumes. It also includes a redirect view for convenience. Overview ======== The ``resume_list`` and ``resume_delete`` views are defined in ``django_resume.views``. The cover/detail, CV, and permission-denied pages are no longer plain views; they are registered ``ResumePage`` classes (``CoverLetterPage``, ``CvPage``, ``PermissionDeniedPage``) in ``django_resume.pages``, and their routes are generated by ``page_registry.get_urls()``. The bare ``/`` catch-all (the ``detail`` page) is emitted last by construction, so the more specific ``cv/`` and ``403/`` routes still resolve unambiguously. The URL names (``detail``, ``cv``, ``403``) are preserved for ``reverse()`` and external links. The page templates below resolve as ``django_resume/pages/{resume.current_theme}/{template_name}`` and fall back to the ``plain`` theme (frame and section fragments together) when the active theme ships no such template, so a missing theme template does not raise ``TemplateDoesNotExist``. The page registry itself -- the :class:`~django_resume.pages.ResumePage` base class, :class:`~django_resume.pages.PageRegistry`, capability-based section selection, page discovery (installed apps and entry points), and the navigation template tags -- is documented in :doc:`pages`. See :doc:`../guide/creating_page_plugins` to add your own pages and :doc:`../dev/page_plugins` for the design rationale. The URL patterns are found in ``django_resume.urls`` (shown below for reference): .. code-block:: python from django.urls import path, reverse from django.views.generic import RedirectView from . import views from .pages import page_registry class CvRedirectView(RedirectView): permanent = True def get_redirect_url(self, *args, **kwargs) -> str: slug = kwargs["slug"] return reverse("django_resume:cv", kwargs={"slug": slug}) app_name = "django_resume" urlpatterns = [ # resumes (non-page routes) path("", views.resume_list, name="list"), path("/delete/", views.resume_delete, name="delete"), path("cv//", CvRedirectView.as_view(), name="cv-redirect"), # cover, cv and 403 pages (generated; bare "/" catch-all is last) *page_registry.get_urls(), ] ``resume_list(request)`` ------------------------ .. function:: resume_list(request: HttpRequest) -> HttpResponse Displays a list of resumes belonging to the currently authenticated user. It also provides a form for creating new resumes. **URL name**: ``django_resume:list`` **Methods**: ``GET``, ``POST`` **Requires**: Authentication (via :func:`django.contrib.auth.decorators.login_required`) **Templates**: - On ``GET`` requests: ``django_resume/pages/plain/resume_list.html`` - On ``POST`` requests: ``django_resume/pages/plain/resume_list_main.html`` **Behavior**: - When requesting via ``GET``, displays a list of the user’s resumes and an empty :class:`~django_resume.views.ResumeForm` for creating a new resume. - When requesting via ``POST``, processes the submitted form data. If valid, a new :class:`~django_resume.models.Resume` is created, owned by the current user. The template re-renders with the newly created resume’s info. **Example**:: from django.urls import reverse from django.test import Client client = Client() client.login(username="alice", password="password") # GET usage response = client.get(reverse("django_resume:list")) assert response.status_code == 200 # POST usage to create a new resume response = client.post( reverse("django_resume:list"), data={"name": "My New Resume", "slug": "my-new-resume"} ) assert response.status_code == 200 ``resume_delete(request, slug)`` -------------------------------- .. function:: resume_delete(request: HttpRequest, slug: str) -> HttpResponse Deletes the specified resume if it belongs to the currently authenticated user. **URL name**: ``django_resume:delete`` **Methods**: ``DELETE`` **Requires**: Authentication (via :func:`django.contrib.auth.decorators.login_required`) **Template**: None (returns an HTTP status code) **Behavior**: - Fetches the :class:`~django_resume.models.Resume` by ``slug``. - If the resume’s owner matches the current user, it deletes the resume. - Responds with a 403 (Forbidden) if ownership does not match. - Returns HTTP 200 on success (rather than 204) for HTMX compatibility. **Example**:: from django.urls import reverse from django.test import Client client = Client() client.login(username="alice", password="password") response = client.delete(reverse("django_resume:delete", args=["my-resume"])) assert response.status_code == 200 ``CoverLetterPage`` (``django_resume:detail``) ---------------------------------------------- .. class:: CoverLetterPage A registered :class:`~django_resume.pages.ResumePage` in ``django_resume.pages``. Displays the "detail" page of a resume, often used for a cover letter or high-level resume info, along with select plugin data. It is the default/root page (``path = ""``), so its route is the bare ``/`` catch-all. **URL name**: ``django_resume:detail`` **Methods**: ``GET`` **Template**: ``django_resume/pages/{resume.current_theme}/resume_detail.html`` **Behavior**: - Resolves the :class:`~django_resume.models.Resume` by ``slug``. - If ``?edit=true`` is present in the query, shows “edit” modes if the user is authenticated and is the owner. - Builds context for an explicit subset of section plugins (``["about", "identity", "cover", "theme"]``). If these plugins are registered, their data is added to the template context. **Example**:: from django.urls import reverse from django.test import Client client = Client() response = client.get("/django-resume/my-resume/") assert response.status_code == 200 # The response includes the "cover" plugin's data if installed. ``CvPage`` (``django_resume:cv``) --------------------------------- .. class:: CvPage A registered :class:`~django_resume.pages.ResumePage` in ``django_resume.pages``. Renders a specialized “CV” (Curriculum Vitae) page of the resume (``path = "cv/"``), with plugin data injected for each registered plugin. **URL name**: ``django_resume:cv`` **Methods**: ``GET`` (non-``GET`` requests return HTTP 405) **Template**: ``django_resume/pages/{resume.current_theme}/resume_cv.html`` **Behavior**: - Resolves the :class:`~django_resume.models.Resume` by ``slug``. - If ``?edit=true`` is present, toggles an edit mode for owners. - Builds context from **all** registered section plugins. - Each plugin’s context is merged into the template context under the plugin’s name key (``context[plugin.name]``). - ``check_access`` enforces token permissions explicitly (via :class:`~django_resume.plugins.TokenPlugin`) before rendering. If the token plugin is registered and the check fails, it short-circuits with a 403 rendered from ``cv_403.html``. - ``finalize_response`` sets ``Referrer-Policy: no-referrer`` when the resume requires a token, on both the rendered page and the 403 response. **Example**:: client = Client() response = client.get("/django-resume/my-resume/cv/") assert response.status_code == 200 # Renders the user’s resume in CV format, including all plugin contexts. ``PermissionDeniedPage`` (``django_resume:403``) ------------------------------------------------ .. class:: PermissionDeniedPage A registered :class:`~django_resume.pages.ResumePage` in ``django_resume.pages`` (``path = "403/"``). A special owner-only page for inline editing of the 403 (Forbidden) page, useful if a user wants to customize the message shown when someone lacks permission to view their CV. **URL name**: ``django_resume:403`` **Methods**: ``GET`` **Requires**: Authentication (only resume owners can edit their 403 page) **Template**: ``django_resume/pages/{resume.current_theme}/cv_403.html`` **Behavior**: - Resolves the specified :class:`~django_resume.models.Resume`. - ``check_access`` redirects anonymous users to the login page, and returns a 403 response if the current user is not the owner. - Otherwise renders ``cv_403.html`` through the same shared renderer the CV denial path uses, building the ``permission_denied`` section context, so the two paths cannot drift. **Example**:: client = Client() client.login(username="alice", password="password") response = client.get("/django-resume/my-resume/403/") assert response.status_code == 200 ``CvRedirectView`` ------------------ .. class:: CvRedirectView Inherits from :class:`django.views.generic.RedirectView`. Redirects from a URL structure of ``/cv//`` to ``//cv/``. **URL name**: ``django_resume:cv-redirect`` **Methods**: ``GET`` **Template**: None (redirect response) **Behavior**: - Resolves the final CV URL via :func:`~django.urls.reverse`, pointing to ``django_resume:cv`` with the same ``slug``. - Returns a permanent redirect (HTTP 301) by default. **Example**:: client = Client() response = client.get("/django-resume/cv/my-resume/") assert response.status_code == 301 assert response["Location"] == "/django-resume/my-resume/cv/"