15.2.164. camcops_server.cc_modules.cc_view_classes

camcops_server/cc_modules/cc_view_classes.py


Copyright (C) 2012, University of Cambridge, Department of Psychiatry. Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

This file is part of CamCOPS.

CamCOPS is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

CamCOPS is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.


Django-style class-based views for Pyramid. Adapted from Django’s views/generic/base.py and views/generic/edit.py.

Django has the following licence:

Copyright (c) Django Software Foundation and individual contributors.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in the
       documentation and/or other materials provided with the distribution.

    3. Neither the name of Django nor the names of its contributors may be
       used to endorse or promote products derived from this software
       without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

Custom views typically inherit from CreateView, DeleteView or UpdateView.

A Pyramid view function with a named route should create a view of the custom class, passing in the request, and return the results of its dispatch() method. For example:

@view_config(route_name="edit_server_created_patient")
def edit_server_created_patient(req: Request) -> Response:
    return EditServerCreatedPatientView(req).dispatch()

To provide a custom view class to create a new object in the database:

  • Inherit from CreateView.

  • Set the object_class property.

  • Set the form_class property.

  • Set the template_name property or implement get_template_name().

  • Override get_extra_context() for any extra parameters to pass to the template.

  • Set success_url or override get_success_url() to be the redirect on successful creation.

  • Override get_form_kwargs() for any extra parameters to pass to the form constructor.

  • For simple views, set the model_form_dict property to be a mapping of object properties to form parameters.

  • Override get_form_values() with any values additional to model_form_dict to populate the form.

  • Override save_object() to do anything more than a simple record save (saving related objects, for example).

To provide a custom view class to delete an object from the database:

  • Inherit from DeleteView.

  • Set the object_class property.

  • Set the form_class property.

  • Set the template_name property or implement get_template_name().

  • Override get_extra_context(). for any extra parameters to pass to the template.

  • Set success_url or override get_success_url() to be the redirect on successful creation.

  • Override get_form_kwargs() for any extra parameters to pass to the form constructor.

  • Set the pk_param property to be the name of the parameter in the request that holds the unique/primary key of the object to be deleted.

  • Set the server_pk_name property to be the name of the property on the object that is the unique/primary key.

  • Override get_object() if the object cannot be retrieved with the above.

  • Override delete() to do anything more than a simple record delete; for example, to delete dependant objects

To provide a custom view class to update an object in the database:

  • Inherit from UpdateView.

  • Set the object_class property.

  • Set the form_class property.

  • Set the template_name property or implement get_template_name().

  • Override get_extra_context() for any extra parameters to pass to the template.

  • Set success_url or override get_success_url() to be the redirect on successful creation.

  • Override get_form_kwargs() for any extra parameters to pass to the form constructor.

  • Set the pk_param property to be the name of the parameter in the request that holds the unique/primary key of the object to be updated.

  • Set the server_pk_name property to be the name of the property on the object that is the unique/primary key.

  • Override get_object() if the object cannot be retrieved with the above.

  • For simple views, set the model_form_dict property to be a mapping of object properties to form parameters.

  • Override save_object() to do anything more than a simple record save (saving related objects, for example).

You can use mixins for settings common to multiple views.

Note

When we move to Python 3.8, there is typing.Protocol, which allows mixins to be type-checked properly. Currently we suppress warnings.

Some examples are in webview.py.

class camcops_server.cc_modules.cc_view_classes.BaseCreateView(request: CamcopsRequest)[source]

Base view for creating a new object instance.

Using this base class requires subclassing to provide a response mixin.

get() Any[source]

Handle GET requests: instantiate a blank version of the form and render it.

post() Any[source]

Handle POST requests:

  • if the user has cancelled, redirect to the cancellation URL;

  • instantiate a form instance with the passed POST variables and then check if it’s valid;

  • if it’s invalid, call form_invalid(), which typically renders the form to show the errors and allow resubmission;

  • if it’s valid, call form_valid(), which in the default handler

    1. processes data via form_valid_process_data(), and

    2. returns a response (either another form or redirection to another URL) via form_valid_response().

class camcops_server.cc_modules.cc_view_classes.BaseDeleteView(request: CamcopsRequest)[source]

Base view for deleting an object.

Using this base class requires subclassing to provide a response mixin.

delete() None[source]

Delete the fetched object

form_valid_process_data(form: Form, appstruct: Dict[str, Any]) None[source]

Called when the form is valid. Deletes the associated model.

get() pyramid.response.Response[source]

Handle GET requests: fetch the object from the database, and renders a form with its data.

post() pyramid.response.Response[source]

Handle POST requests: instantiate a form instance with the passed POST variables and then check if it’s valid.

class camcops_server.cc_modules.cc_view_classes.BaseFormView(request: CamcopsRequest)[source]

A base view for displaying a form.

class camcops_server.cc_modules.cc_view_classes.BaseUpdateView(request: CamcopsRequest)[source]

Base view for updating an existing object.

Using this base class requires subclassing to provide a response mixin.

get() Any[source]

Handle GET requests: instantiate a blank version of the form and render it.

post() Any[source]

Handle POST requests:

  • if the user has cancelled, redirect to the cancellation URL;

  • instantiate a form instance with the passed POST variables and then check if it’s valid;

  • if it’s invalid, call form_invalid(), which typically renders the form to show the errors and allow resubmission;

  • if it’s valid, call form_valid(), which in the default handler

    1. processes data via form_valid_process_data(), and

    2. returns a response (either another form or redirection to another URL) via form_valid_response().

class camcops_server.cc_modules.cc_view_classes.ContextMixin[source]

A default context mixin that passes the keyword arguments received by get_context_data() as the template context.

get_context_data(**kwargs: Any) Dict[str, Any][source]

Provides context for a template, including the view argument and any additional context provided by get_extra_context().

get_extra_context() Dict[str, Any][source]

Override to provide extra context, merged in by get_context_data().

class camcops_server.cc_modules.cc_view_classes.CreateView(request: CamcopsRequest)[source]

View for creating a new object, with a response rendered by a template.

class camcops_server.cc_modules.cc_view_classes.DeleteView(request: CamcopsRequest)[source]

View for deleting an object retrieved with self.get_object(), with a response rendered by a template.

class camcops_server.cc_modules.cc_view_classes.FormMixin[source]

Provide a way to show and handle a single form in a request.

fail(message: str) NoReturn[source]

Raises a failure exception, redirecting to a failure URL.

form_invalid(validation_error: deform.exception.ValidationFailure) pyramid.response.Response[source]

Called when the form is submitted via POST and is invalid. Returns a response with a rendering of the invalid form.

form_valid_response(form: Form, appstruct: Dict[str, Any]) pyramid.response.Response[source]

Called when the form is submitted via POST and is valid. Redirects to the supplied “success” URL.

get_cancel_url() str[source]

Return the URL to redirect to when cancelling a form.

get_context_data(**kwargs: Any) Dict[str, Any][source]

Insert the rendered form (as HTML) into the context dict.

get_failure_url() str[source]

Return the URL to redirect to on error after processing a valid form. e.g. when a password is of the correct form but is invalid.

get_form() Form[source]

Return an instance of the form to be used in this view.

get_form_class() Optional[Type[Form]][source]

Return the form class to use.

get_form_kwargs() Dict[str, Any][source]

Return the keyword arguments for instantiating the form.

get_rendered_form(form: Form) str[source]

Returns the form, rendered as HTML.

get_success_url() str[source]

Return the URL to redirect to after processing a valid form.

class camcops_server.cc_modules.cc_view_classes.FormView(request: CamcopsRequest)[source]

A view for displaying a form and rendering a template response.

class camcops_server.cc_modules.cc_view_classes.FormWizardMixin(*args, **kwargs)[source]

Basic support for multi-step form entry. For more complexity we could do something like https://github.com/jazzband/django-formtools/tree/master/formtools/wizard

We store temporary state in the form_state dictionary on the CamcopsSession object on the request. Arbitrary values can be stored in form_state. The following are used by this mixin:

  • “step” stores the name of the current form entry step.

  • “route_name” stores the name of the current route, so we can detect if the form state is stale from a previous incomplete operation.

Views using this Mixin should implement:

wizard_first_step: The name of the first form entry step wizard_forms: step name -> :class:Form dict wizard_templates: step name -> template filename dict wizard_extra_contexts: step name -> context dict dict

Alternatively, subclasses can override get_first_step() etc.

The logic of changing steps is left to the subclass.

__init__(*args, **kwargs) None[source]

We prevent stale state from messing things up by clearing state when a form sequence starts. Form sequences start with HTTP GET and proceed via HTTP POST. So, if this is a GET request, we clear the state. We do so in the __init__ sequence, as others may wish to write state before the view is dispatched.

An example of stale state: the user sets an MFA method but then that is disallowed on the server whilst they are halfway through login. (That leaves users totally stuffed as they are not properly “logged in” and therefore can’t easily log out.)

There are other examples seen in testing. This method gets round all those. (For example, the worst-case situation is then advising the user to log in again, or start whatever form-based process it was again).

We also reset the state if the stored route name doesn’t match the current route name.

fail(message: str) NoReturn[source]

Raises a failure.

finish() None[source]

Ends, by marking the state as finished, and clearing any other state except the current route/step (the step in particular may be useful for subsequent functions).

finished() bool[source]

Have we finished?

form_valid_response(form: Form, appstruct: Dict[str, Any]) pyramid.response.Response[source]

Called when the form is submitted via POST and is valid. Redirects to the supplied “success” URL.

get_extra_context() Dict[str, Any][source]

Returns any extra context information (as a dictionary) for the current step.

get_first_step() str[source]

Returns the first step to be used when the form is first loaded.

get_form_class() Optional[Type[Form]][source]

Returns the class of Form to be used for the current step (not a form instance).

get_template_name() str[source]

Returns the Mako template filename to be used for the current step.

property route_name: Optional[str]

Get the name of the current route. See class help.

property state: Dict[str, Any]

Returns the (arbitrary) state dictionary. See class help.

property step: str

Returns the current step.

class camcops_server.cc_modules.cc_view_classes.ModelFormMixin[source]

Represents an ORM object (the model) and an associated form.

form_valid_process_data(form: Form, appstruct: Dict[str, Any]) None[source]

Called when the form is valid. Saves the associated model.

get_form_values() Dict[str, Any][source]

Reads form values from the object (or provides an empty dictionary if there is no object yet). Returns a form dictionary.

get_model_form_dict() Dict[str, str][source]

Returns the dictionary mapping model attribute names to form parameter names.

save_object(appstruct: Dict[str, Any]) None[source]

Saves the object in the database, from data provided via the form.

set_object_properties(appstruct: Dict[str, Any]) None[source]

Sets properties of the object, from form data.

class camcops_server.cc_modules.cc_view_classes.ProcessFormView(request: CamcopsRequest)[source]

Render a form on GET and processes it on POST.

Requires ContextMixin.

form_invalid(validation_error: deform.exception.ValidationFailure) pyramid.response.Response[source]

Called when the form is submitted via POST and is invalid. Returns a response with a rendering of the invalid form.

form_valid(form: Form, appstruct: Dict[str, Any]) pyramid.response.Response[source]

2021-10-05: separate data handling and the response to return. Why? Because:

  1. returning a response can involve “return response” or “raise HTTPFound”, making flow harder to track;

  2. the Python method resolution order (MRO) makes it harder to be clear on the flow through the combination function.

form_valid_process_data(form: Form, appstruct: Dict[str, Any]) None[source]

Perform any handling of data from the form.

Override in subclasses or mixins if necessary. Be sure to call the superclass method to ensure all actions are performed.

form_valid_response(form: Form, appstruct: Dict[str, Any]) pyramid.response.Response[source]

Return the response (or raise a redirection exception) following valid form submission.

get() pyramid.response.Response[source]

Handle GET requests: instantiate a blank version of the form and render it.

get_cancel_url() str[source]

Return the URL to redirect to when cancelling a form.

post() pyramid.response.Response[source]

Handle POST requests:

  • if the user has cancelled, redirect to the cancellation URL;

  • instantiate a form instance with the passed POST variables and then check if it’s valid;

  • if it’s invalid, call form_invalid(), which typically renders the form to show the errors and allow resubmission;

  • if it’s valid, call form_valid(), which in the default handler

    1. processes data via form_valid_process_data(), and

    2. returns a response (either another form or redirection to another URL) via form_valid_response().

class camcops_server.cc_modules.cc_view_classes.SingleObjectMixin[source]

Represents a single ORM object, for use as a mixin.

get_context_data(**kwargs: Any) Dict[str, Any][source]

Insert the single object into the context dict.

get_object() Any[source]

Returns the ORM object being manipulated.

get_pk_value() int[source]

Returns the integer primary key of the object.

class camcops_server.cc_modules.cc_view_classes.TemplateResponseMixin[source]

A mixin that can be used to render a Mako template.

get_template_name() str[source]

Returns the template filename.

render_to_response(context: Dict) pyramid.response.Response[source]

Takes the supplied context, renders it through our specified template (set by template_name), and returns a pyramid.response.Response.

class camcops_server.cc_modules.cc_view_classes.UpdateView(request: CamcopsRequest)[source]

View for updating an object, with a response rendered by a template.

class camcops_server.cc_modules.cc_view_classes.View(request: CamcopsRequest)[source]

Simple parent class for all views. Owns the request object and provides a dispatcher for HTTP requests.

Derived classes typically implement get() and post().

__init__(request: CamcopsRequest) None[source]
Parameters

request – a camcops_server.cc_modules.cc_request.CamcopsRequest

dispatch() pyramid.response.Response[source]

Try to dispatch to the right HTTP method (e.g. GET, POST). If a method doesn’t exist, defer to the error handler. Also defer to the error handler if the request method isn’t on the approved list.

Specifically, this ends up calling self.get() or self.post() or self.http_method_not_allowed().

http_method_not_allowed() NoReturn[source]

Raise a pyramid.httpexceptions.HTTPMethodNotAllowed (error 405) indicating that the selected HTTP method is not allowed.