Building profile pages for Djangobin

Creating Settings View

The settings view will allow a logged in user to change preferences like default language, expiration and exposure. It also allows a user to change the profile visibility option.

The profile view (which we will create next) shows a paginated list of snippets created by the user in reverse chronological order (i.e latest first). By default, the profile page is public.

All these settings are defined by the Author model. For your reference, here is how it looks like:

djangobin/django_project/djangobin/models.py

Let’s start by adding a class named SettingForm in forms.py as follows:

djangobin/django_project/djangobin/forms.py

In the views.py, add settings() view just below the activate_account() view as follows:

djangobin/django_project/djangobin/views.py

The code for settings.html template is as follows:

djangobin/django_project/djangobin/templates/djangobin/settings.html

Add a new URL pattern named settings to urls.py as follows:

djangobin/django_project/djangobin/urls.py

At last, add a link to the settings page to base.html as follows:

djangobin/django_project/djangobin/templates/djangobin/base.html

Login to Djangobin and visit http://localhost:8000/settings/. You should see Account Preferences page like this:

Select the desired preferences and hit submit. You should see a "Settings Saved." message as follows:

Creating Profile View

The profile view shows a paginated list of snippets created by a user in reverse chronological order (i.e latest first). If the user’s profile is private then its username will not appear in the trending snippet page, and tag list page.

Open views.py and update profile() view function as follows:

djangobin/django_project/djangobin/views.py

This is how the view function works:

  1. The view takes an additional argument named username which will come from the URL.
  2. In line 7, we are using get_object_or_404() to fetch the relevant User object. If no matching object found, get_object_or_404() will return an HTTP 404 error.
  3. In line 11, we are checking if the profile is private and logged in user is not same as the user being viewed. If the condition comes out to be true we show an HTTP 404 error; otherwise, the program control is transferred to the elif statement in line 16.
  4. In line 16, we are checking if the profile is not private and logged in user is not same as the user being viewed. If the condition is true, we retrieve the public snippets of the user and increment the profile view count by 1; otherwise, the program control is transferred to the else statement in line 23.
  5. If the program control comes to the else statement then it means that the logged in user is same as the user being viewed. As a result, we fetch all the snippets of the user.
  6. In line 26, we are calling paginate_result() to get the paginated result. Finally, in line 28, we render the template with the context data by calling render() function.

The code for the profile.html is as follows:

djangobin/django_project/djangobin/templates/djangobin/profile.html

Next, add a link to the profile view in base.html template as follows:

djangobin/django_project/djangobin/templates/djangobin/base.html

To view the profile page login to Djangobin, click the dropdown at the top right corner of the screen and select My Pastes.

You should see profile page of the logged in user as follows:

To view, the profile page of other users visit trending snippets page or tag list page and click on the usernames listed in the User column.

Note that the profile link in trending snippets and tag list page will only appear when the user profile is not private. If the profile is private then you will see a dash (-) in place of the username.

Creating Decorators for Views

Everything appears to be working great, but in reality, our application suffers from a major privacy concern regarding the private snippets.

We want private snippets to be accessible only to the users who created them (after they are logged in). However, as things stand, if you know the URL of the private snippet then you can access them no matter whether you have created it or not. In other words, private snippets are treated just like unlisted snippets (remember that unlisted snippets are the snippets that do not appear on any pages our the site. Only the users that have a link to the snippet can access it).

You can verify this by logging in to Djangobin and creating a private snippet. Copy the snippet URL, open a new browser window in “private” or “incognito” mode and paste the URL and you should be able to view the snippet.

The issue is not just limited to viewing snippets. The users can also view raw snippets and download them.

At first you might think, doing something like this would resolve issue:

Certainly, this is one of the possible ways to fix the problem. But if you go by this route you would have to copy and paste same code over and over again in all the other views. We can do much better – decorators to the rescue.

Decorators are special functions that takes other another function as an argument and extends the original function without explicitly modifying it. You can think of decorators as a guard for your function, before calling the original function the decorator is run, which makes them suitable for performing tasks (or tests) that needs to be done before the original function gets called.

We have been using login_required decorator for a while without paying much attention to it.

Here is what happens when you visit the URL tied to the my_view function.

The login_required() decorator is called to check whether the user is logged in or not. If the user is not logged then it will redirect the user to /account/login/ (or the URL specified by LOGIN_URL setting); otherwise, the my_view() function would be called as usual.

With the basic concept of decorators under out belt. We will now create our own custom decorator.

Create a new file named decorators.py next to models.py and add the following code to it:

djangobin/django_project/djangobin/decorators.py

Here we are defining a decorator named private_snippet. It checks whether the snippet exposure is private and logged in user is same as the snippet creator. If it is, it calls the view function; otherwise, it shows an HTTP 404 error.

Now modify, views.py to use private_snippet decorator as follows:

djangobin/django_project/djangobin/views.py

If you now try to view or download the private snippet of another user you will get an HTTP 404 error.

Initializing SnippetForm with Preferences

With the settings view in place, users can now set their preferences. But as things stand, everytime you create a new snippet you have to select values from language, expiration and exposure fields. If most of the time you create public snippets which never expires, then it makes sense to automatically initialize expiration and exposure fields to Never and Public respectively.

Another point I would like to draw your attention to are the options of the Exposure field. Currently, the Exposure dropdown shows three options: Public, Unlisted and Private. These options are always displayed no matter whether the user is logged in or not. A user can’t create private snippets unless he is logged in, hence displaying Private option to a guest user is completely pointless. Further, creating private snippets as a guest doesn’t make sense because you won’t be able to access them. A better way would be to display Private option only when the user is logged in. That way we would be able to associate the private snippet to the logged in user.

To make these changes we have to change the way the form is initialized. Open form.py file and override the __init__() method of the SnippetForm as follows:

djangobin/django_project/djangobin/forms.py

In addition to request argument, we specify that the __init__() accepts *args and **kwargs. This is just a Pythonic way of saying that __init__() can take an arbitrary number of positional and keyword arguments.

In line 14, we are calling the parent class’s __init__() method, this is necessary to preserve the functionalities provided by the base __init__() method.

In line 16, we check whether the user is logged in or not using the is_authenticated attribute of the User object. If the user is logged in, we assign all exposure choices to the exposure field.

In the next line, we use get_preferences method of the Author model to provide initial values to language, expiration and exposure fields. The get_preferences() method is defined in Author model as follows:

djangobin/django_project/djangobin/models.py

On the other hand, if the user is not logged in. Then we only provide two options to the exposure field: Public and Unlisted. At last, in line 24, we set the initial values for the guest user.

Now, modify the index() view to use updated SnippetForm as follows:

djangobin/django_project/djangobin/views.py

To view the fruits of our labor, open your browser and navigate to http://localhost:8000/. You will see that the Language, Expiration and Exposure fields are pre-populated with data as follows:

If you are logged in then the fields will get populated based on your preferences.

Deleting Snippets

In this section, we are going to add a view function to delete snippets.

Open views.py and add a view named delete_snippet() just below the profile() view as follows:

djangobin/django_project/djangobin/views.py

The view function work as follows:

  1. The delete_snippet() accepts two parameters request and snippet_slug.
  2. In line 8, we try to retrieve the Snippet object using get_object_or_404() function. If no matching snippet found, get_object_or_404() will return HTTP 404 error.
  3. In line 9, we test whether the logged in user is the snippet author or not. If not, we show an HTTP 404 error, otherwise, we delete the snippet using the delete() method (line 11).
  4. Finally, in line 12, we redirect the logged in user to the profile page.

Next, add a new URL pattern to delete snippets in urls.py as follows:

djangobin/django_project/djangobin/urls.py

Now, let’s add a link to delete snippets in the snippet detail page:

djangobin/django_project/djangobin/templates/djangobin/snippet_detail.html

We want to show delete link only to the user who created the snippet. In line 5, we use {% if %} tag to check we check whether the logged in user is the snippet author. If so, we display the link to delete the snippet.

Revisiting Contact Form

Our Contact form consists of four fields (name, email, purpose and message) and all of them are required.

However, asking a logged in user for name and email is undesirable. We better hide these fields if the user is logged in and populate them automatically while sending the email.

Note that just hiding the fields from the form will not make them optional. We will have to set the required attribute of the field to False dynamically, to make it optional.

To accomplish this we will override the __init__() method of the Form class.

Open forms.py file and modify ContactForm class as follows:

djangobin/django_project/djangobin/forms.py

In line 10, we test whether the user is authenticated. If so, we make the name and email field optional by setting their required attribute to False.

Next, modify the contact.html as follows:

djangobin/django_project/djangobin/templates/djangobin/contact.html

In lines 6-24, we use {% if %} tag to display name and email field only when the user is not logged in.

Finally, modify the contact() view as follows:

djangobin/django_project/djangobin/views.py

In line 10, we test whether the user is authenticated. If so, we set name and email using request.name object. Otherwise, we set name and email using cleaned_data attribute of the form object.

If you now visit Contact form after logging in. You should see a form like this:

Select purpose, enter your message and hit submit. As usual in the shell running the server you should get output like this:

Leave a Comment