django_basic

Basic Page with Comments:

Tutorial

Django-comments-xtd is a reusable app that extends the built-in Django Comments Framework.

Installation

Check out the code and add it to your project or PYTHONPATH. Use git, pip or easy_install to check out Django-comments-xtd from Github or get a release from PyPI:

    1. Use git to clone the repository, and then install the package (read more about git):

    • git clone git://github.com/danirus/django-comments-xtd.git and

  • python setup.py install

    1. Or use pip (read more about pip):

    • Do pip install django-comments-xtd, or

    • Edit your project’s requirements file and append either the Github URL or the package name django-comments-xtd, and then do pip install -r requirements.

    • Do easy_install django-comments-xtd

    1. Optionally, if you want to allow comments written in markup languages like Markdown or reStructuredText, install django-markup.

Configuration

Configuring Django-comments-xtd comprehends the following steps:

    1. Add COMMENTS_APP = "django_comments_xtd" to your settings module.

    2. Add 'django.contrib.comments' and 'django_comments_xtd' to your INSTALLED_APPS setting.

    • If you allow comments written in markup languages add django_markup to INSTALLED_APPS too.

    1. Add COMMENTS_XTD_CONFIRM_EMAIL = True to the settings file in order to require comment confirmation by email.

    2. Add url(r'^comments/', include('django_comments_xtd.urls')) to your urls.py.

    3. Create a comments directory in your templates directory and copy the default templates from the Django Comments Framework that you want to customise. Maybe the following:

    • comments/list.html used by templatetag render_comments_list

    • comments/form.html used by templatetag render_comment_form

    • comments/preview.html used to preview the comment or when there are form errors

    • comments/posted.html rendered after comment is sent

    1. To customise how templatetag render_last_xtdcomments renders comments, copy the template file django_comment_xtd/comment.html to any of the following targets in your templates directory:

    • django_comment_xtd/<app>/<model>/comment.html to customise them for the given <app>.<model>

    • django_comment_xtd/<app>/comment.html to customise them for all <app> models

    • django_comment_xtd/comment.html to customise all your site comments at once

    1. Run python manage.py syncdb to create the django_comments_xtd_xtdcomment table.

Optionally you can add an extra setting to control Django-comments-xtd behaviour (see Settings), but it has a sane default.

Workflow

Workflow described in 4 actions:

    1. The user visits a page that accepts comments. Your app or a 3rd. party app handles the request:

    2. Your template shows content that accepts comments. It loads the comments templatetag and using tags as render_comment_list and render_comment_form the template shows the current list of comments and the post your comment form.

    3. The user clicks on preview. The Django Comments Framework post_comment view handles the request:

    4. Renders comments/preview.html either with the comment preview or with form errors if any.

    5. The user clicks on post. The Django Comments Framework post_comment view handles the request:

    6. If there were form errors it does the same as in point 2.

    7. Otherwise creates an instance of TmpXtdComment model: an in-memory representation of the comment.

    8. Send signal comment_will_be_posted and comment_was_posted. The django-comments-xtd receiver on_comment_was_posted receives the second signal with the TmpXtdComment instance and does as follows:

    9. If the user is authenticated or confirmation by email is not required (see Settings):

    10. An instance of XtdComment hits the database.

    11. An email notification is sent to previous comments followers telling them about the new comment following up theirs. Comment followers are those who ticked the box Notify me of follow up comments via email.

    12. Otherwise a confirmation email is sent to the user with a link to confirm the comment. The link contains a secured token with the TmpXtdComment. See below Creating the secure token for the confirmation URL.

    13. Pass control to the next parameter handler if any, or render the comments/posted.html template:

    14. If the instance of XtdComment has already been created, redirect to the the comments’s absolute URL.

    15. Otherwise the template content should inform the user about the confirmation request sent by email (see the multiple models demo site templates directory for an example).

    16. The user clicks on the confirmation link, in the email message. Django-comments-xtd confirm view handles the request:

    17. Checks the secured token in the URL. If it’s wrong returns a 404 code.

    18. Otherwise checks whether the comment was already confirmed, in such a case returns a 404 code.

    19. Otherwise sends a confirmation_received signal. You can register a receiver to this signal to do some extra process before approving the comment. See Signal and receiver. If any receiver returns False the comment will be rejected and the template django_comments_xtd/discarded.html will be rendered.

    20. Otherwise an instance of XtdComment finally hits the database, and

    21. An email notification is sent to previous comments followers telling them about the new comment following up theirs.

Creating the secure token for the confirmation URL

The Confirmation URL sent by email to the user has a secured token with the comment. To create the token Django-comments-xtd uses the module signed.py authored by Simon Willison and provided in Django-OpenID.

django_openid.signed offers two high level functions:

    • dumps: Returns URL-safe, sha1 signed base64 compressed pickle of a given object.

    • loads: Reverse of dumps(), raises ValueError if signature fails.

A brief example:

>>> signed.dumps("hello")'UydoZWxsbycKcDAKLg.QLtjWHYe7udYuZeQyLlafPqAx1E'>>> signed.loads('UydoZWxsbycKcDAKLg.QLtjWHYe7udYuZeQyLlafPqAx1E')'hello'>>> signed.loads('UydoZWxsbycKcDAKLg.QLtjWHYe7udYuZeQyLlafPqAx1E-modified')BadSignature: Signature failed: QLtjWHYe7udYuZeQyLlafPqAx1E-modified

There are two components in dump’s output UydoZWxsbycKcDAKLg.QLtjWHYe7udYuZeQyLlafPqAx1E, separatad by a ‘.’. The first component is a URLsafe base64 encoded pickle of the object passed to dumps(). The second component is a base64 encoded hmac/SHA1 hash of “$first_component.$secret”.

Calling signed.loads(s) checks the signature BEFORE unpickling the object -this protects against malformed pickle attacks. If the signature fails, a ValueError subclass is raised (actually a BadSignature).

Signal and receiver

In addition to the signals sent by the Django Comments Framework, django-comments-xtd sends the following signal:

    • confirmation_received: Sent when the user clicks on the confirmation link and before the XtdComment instance is created in the database.

You might want to register a receiver for this signal. An example function receiver might check the datetime a user submitted a comment and the datetime the confirmation URL has been clicked. Say that if the difference between them is over 7 days the message should be discarded with a graceful “sorry, too old comment” template.

Extending the demo site with the following code would do the job:

#----------------------------------------# append the code below to demo/views.py:from datetime import datetime, timedeltafrom django_comments_xtd import signalsdef check_submit_date_is_within_last_7days(sender, data, request, **kwargs): plus7days = timedelta(days=7) if data["submit_date"] + plus7days < datetime.now(): return Falsesignals.confirmation_received.connect(check_submit_date_is_within_last_7days)#-----------------------------------------------------# change get_comment_create_data in django_comments_xtd/forms.py to cheat a# bit and make Django believe that the comment was submitted 7 days ago:def get_comment_create_data(self): from datetime import timedelta # ADD THIS data = super(CommentForm, self).get_comment_create_data() data['followup'] = self.cleaned_data['followup'] if settings.COMMENTS_XTD_CONFIRM_EMAIL: # comment must be verified before getting approved data['is_public'] = False data['submit_date'] = datetime.datetime.now() - timedelta(days=8) # ADD THIS return data

Try the demo site again and see that the django_comments_xtd/discarded.html template is rendered after clicking on the confirmation URL.

Maximum Thread Level

Nested comments are disabled by default, to enable them use the following settings:

    • COMMENTS_XTD_MAX_THREAD_LEVEL: an integer value

    • COMMENTS_XTD_MAX_THREAD_LEVEL_BY_APP_MODEL: a dictionary

Django-comments-xtd inherits the flexibility of the built-in Django Comments Framework, so that developers can plug it to support comments on as many models as they want in their projects. It is as suitable for one model based project, like comments posted to stories in a simple blog, as for a project with multiple applications and models.

The configuration of the maximum thread level on a simple project is done by declaring the COMMENTS_XTD_MAX_THREAD_LEVEL in the settings.py file:

COMMENTS_XTD_MAX_THREAD_LEVEL = 2

Comments then could be nested up to level 2:

<In an instance detail page that allows comments> First comment (level 0) |-- Comment to First comment (level 1) |-- Comment to Comment to First comment (level 2)

Comments posted to instances of every model in the project will allow up to level 2 of threading.

On a project that allows users posting comments to instances of different models, the developer may want to declare a maximum thread level per app.model basis. For example, on an imaginary blog project with stories, quotes, diary entries and book/movie reviews, the developer might want to define a default project wide maximum thread level of 1 for any model and an specific maximum level of 5 for stories and quotes:

COMMENTS_XTD_MAX_THREAD_LEVEL = 1COMMENTS_XTD_MAX_THREAD_LEVEL_BY_APP_MODEL = { 'blog.story': 5, 'blog.quote': 5,}

So that blog.review and blog.diaryentry instances would support comments nested up to level 1, while blog.story and blog.quote instances would allow comments nested up to level 5.

-------------------------------------------------

Demo projects

Django-comments-xtd comes with three demo projects:

    1. simple: Single model with non-threaded comments

    2. simple_threads: Single model with threaded comments up to level 2

    3. multiple: Several models with comments, and a maximum thread level defined on per app.model basis.

You may want to have a look at the example sites dir in the repository.

Demo sites setup

The recommended way to run the demo sites is in its own virtualenv. Once in a new virtualenv, clone the code and cd into any of the 3 demo sites. Then run the install script and launch the dev server:

$ git clone git://github.com/danirus/django-comments-xtd.git $ cd django-comments-xtd/django_comments_xtd/demos/[simple|simple_thread|multiple] $ sh ./install.sh (to syncdb, migrate and loaddata) $ python manage.py runserver

By default:

    • There’s an admin user, with password admin

    • Emails are sent to the console.EmailBackend

Simple demo site

The simple demo site is a project with just one application called articles with an Article model whose instances accept comments. The example features:

    • Comments have to be confirmed by email before they hit the database.

    • Commenters may request follow up notifications.

    • Logged out, to receive confirmation requests by email

    • Logged in, to get your comments accepted without requiring confirmation

    1. When adding new articles in the admin interface be sure to tick the box allow comments, otherwise comments won’t be allowed.

Simple with threads

The simple_threads demo site extends the simple demo functionality featuring:

    • Thread support up to level 2

    1. Visit http://localhost:8000/ and look at the first article page with 9 comments.

    2. See the comments in the admin interface too:

    • The first field represents the thread level.

    • When in a thread it mentions the parent comment.

Multiple demo site

The multiple demo allows users post comments to three different type of instances: stories, quotes, and project releases. Stories and quotes belong to the blog app while project releases belong to the projects app. The demo shows the blog homepage with the last 5 comments posted to either stories or quotes and a link to the complete paginated list of comments posted to the blog. It features:

    • Definition of maximum thread level on a per app.model basis.

    • Use of comments_xtd template tags (get_xtdcomment_count, render_last_xtdcomments, get_last_xtdcomments) and filter (render_markup_comment).

    • The Blog contains Stories and Quotes. Instances of both models have comments. The blog index page shows the last 5 comments posted to either stories or quotes. It also gives access to the complete paginated list of comments.

    • Project releases have comments as well but are not included in the complete paginated list of comments shown in the blog.

    1. To render the last 5 comments the site uses:

    • The templatetag {% render_last_xtdcomments 5 for blog.story blog.quote %}

    • And the following template files from the demos/multiple/templates directory:

    • django_comments_xtd/blog/story/comment.html to render comments posted to stories

    • django_comments_xtd/blog/quote/comment.html to render comments posted to quotes

    • You may rather use a common template to render comments:

    • For all blog app models: django_comments_xtd/blog/comment.html

    • For all the website models: django_comments_xtd/comment.html

    1. To render the complete paginated list of comments the site uses:

    • An instance of a generic ListView class declared in blog/urls.py that uses the following queryset:

  • XtdComment.objects.for_app_models("blog.story", "blog.quote")

    1. The comment posted to the story Net Neutrality in Jeopardy starts with a specific line to get the content rendered as reStructuredText. Go to the admin site and see the source of the comment; it’s the one sent by Alice to the story 2.

    • To format and render a comment in a markup language, make sure the first line of the comment looks like: #!<markup-language> being <markup-language> any of the following options:

    • markdown

    • restructuredtext

    • linebreaks

    • Then use the filter render_markup_comment with the comment field in your template to interpret the content (see demos/multiple/templates/comments/list.html).

-----------------------------------------------------------------------------

Django’s admin framework includes the basic functionalities for logging in and out of the admin site, but if you’re building a so-called ‘social’ application and want people to be able to sign-up, log in and thus benefit from additional site functionalities then you need to use a more generic ‘registration’ application. The good news is: most of the work has already been done, so it’s just a matter of putting things together correctly.

I found a nicely written blog post on this topic, so in what follows I’m just doing a re-cap of the main steps involved and adding some other useful info.

Before we start: what follows has been tested on Django 1.1 Django 1.3, Python 2.6 and MySQL (note: on 13/8/12 I revised this post so that the code would work on django 1.3 – many thanks to the folks who left comments and pointed out what needed to be updated!)

1) Install

Download the package from bitbucket and add it to your application directory (or put it somewhere else and then modify your python path as needed). Then add the ‘registration’ app to the installed_apps tuple in your settings.py:

INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.humanize', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.admin', # etc.. 'registration',)

2) Syncdb

Do a quick syncdb from the console, which will result in one new table being created in the DB, ‘RegistrationProfile’. All it contains is its primary key, the ‘user’ key, and the ‘activation code’ fields. Now we’ve got the data structures needed for setting up the registration application!

> python manage.py syncdb

3) Setting.py

Create some registration settings in settings.py:

ACCOUNT_ACTIVATION_DAYS=7 EMAIL_HOST='localhost' EMAIL_PORT=1023 EMAIL_HOST_USER='username' EMAIL_HOST_PASSWORD='password'

Obviously you’ve got to change the settings above depending on your specific server setup (more information is available here or here). Also, see point 7 below if you’re just testing things out without a SMTP server available.

4) Urls.py

Include the required registration urls to your urls.py:

urlpatterns = patterns(", (r'^admin/', include('django.contrib.admin.urls')), (r'^accounts/', include('registration.urls')),)

5) Templates

Add the registration templates to your django-templates directory. The django-registration package we downloaded earlier on doesn’t include any templates, but you can easily find some of them online (for example here).

Anyways, no need to do that: I took those templates and added some other ones too so to create the minimal working package that’ll get you going (that doesn’t include any fancy css styling but all the basic html stuff is there). You can download the templates here, expand the zip file and put it in templates/registration.

6) Done! Let’s recap..

We’re now ready to go. Let’s pause for a moment and recap what we achieved: we installed and activated django-registration, which sets up a whole bunch of new urls. These are divided into two groups:

a) /login, /logout, the two-step password change at password/change/ and password/change/done/; the four-step password reset at password/reset/, password/reset/confirm/, password/reset/complete/ and password/reset/done/.

This is the original URL specification source code (you can see it on bitbucket too) :

# from django-registration / registration / auth_urls.py urlpatterns = patterns('', url(r'^login/$', auth_views.login, {'template_name': 'registration/login.html'}, name='auth_login'), url(r'^logout/$', auth_views.logout, {'template_name': 'registration/logout.html'}, name='auth_logout'), url(r'^password/change/$', auth_views.password_change, {'template_name': 'registration/password_change_form.html'}, name='auth_password_change'), url(r'^password/change/done/$', auth_views.password_change_done, {'template_name': 'registration/password_change_done.html'}, name='auth_password_change_done'), url(r'^password/reset/$', auth_views.password_reset, {'template_name': 'registration/password_reset_form.html'}, name='auth_password_reset'), url(r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', auth_views.password_reset_confirm, {'template_name': 'registration/password_reset_confirm.html'}, name='auth_password_reset_confirm'), url(r'^password/reset/complete/$', auth_views.password_reset_complete, {'template_name': 'registration/password_reset_complete.html'}, name='auth_password_reset_complete'), url(r'^password/reset/done/$', auth_views.password_reset_done, {'template_name': 'registration/password_reset_done.html'}, name='auth_password_reset_done'),)

b) the second group of urls are /activate and /register:

This is the original URL specification source (see it on bitbucket) :

# django-registration / registration / backends / default / urls.py urlpatterns = patterns('', url(r'^activate/complete/$', direct_to_template, { 'template': 'registration/activation_complete.html' }, name='registration_activation_complete'), # Activation keys get matched by w+ instead of the more specific # [a-fA-F0-9]{40} because a bad activation key should still get to the view; # that way it can return a sensible "invalid key" message instead of a # confusing 404. url(r'^activate/(?P<activation_key>w+)/$', activate, { 'backend': 'registration.backends.default.DefaultBackend' }, name='registration_activate'), url(r'^register/$', register, { 'backend': 'registration.backends.default.DefaultBackend' }, name='registration_register'), url(r'^register/complete/$', direct_to_template, { 'template': 'registration/registration_complete.html' }, name='registration_complete'), url(r'^register/closed/$', # UNUSED direct_to_template, { 'template': 'registration/registration_closed.html' }, name='registration_disallowed'), # this is the default django login module (r'', include('registration.auth_urls')), )

7) Testing

If you’re testing things on a development server you might not have access to an SMTP server (needed to test the email-based registration process). In such a case you can still try out your application using a workaround. In your settings.py file change the registration settings with the following ones:

EMAIL_HOST = 'localhost' EMAIL_PORT = 1025 EMAIL_HOST_USER = " EMAIL_HOST_PASSWORD = " EMAIL_USE_TLS = False DEFAULT_FROM_EMAIL = 'testing@example.com'

Then open up another console window and run a temporary ‘dummy’ SMTP server with python:

bash-3.2$ python -m smtpd -n -c DebuggingServer localhost:1025

This local SMTP server remains there waiting for incoming messages. If now you go to /accounts/register/, fill out the form and hit ‘send’ you’ll see the registration email printed out in the console. Basically what happened is this: the ‘dummy’ python SMTP server we’ve just set up picked up django’s email-sending request and consequently printed out the email contents. If this is indeed what happened, it means that your code is working properly… and that you can use the url provided in the email to test the activation functionality too.

For example, here is what you would see in the console after registering:

---------- MESSAGE FOLLOWS ---------- Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Subject: Activate your djangoproject.com account - you have 2 days! From: testing@example.com To: michele.pasin@hotmail.com Date: Wed, 12 Jan 2011 16:49:59 -0000 Message-ID: <20110112164959.3366.35638@mymac.local> X-Peer: 127.0.0.1 Someone, hopefully you, signed up for a new account at djangoproject.com using this email address. If it was you, and you'd like to activate and use your account, click the link below or copy and paste it into your web browser's address bar: http://127.0.0.1:8000/accounts/activate/6342fca5ffd430a820be6d98acde6e59a4c2d29c/ If you didn't request this, you don't need to do anything; you won't receive any more email from us, and the account will expire automatically in two days. ------------ END MESSAGE ------------

Pasting the ‘activation’ url in your browser should allow you to complete the registration process and check the rest of your code.

Finally, keep in mind also that the django-registration application sends out emails that contain your site’s URL for the activation link, and that URL is dynamically determined using the ‘sites’ application (normally added via settings.py). By default, your domain name is listed as ‘example.com’, and the easiest way to change this is to log into the admin application and click on the ‘Sites’ link on the admin home page to get to the relevant entry.

FINAL STEPS:

add templates to project_root/templates/registration

go to: http://127.0.0.1:8000/accounts/login/ for login interface and

go to: http://127.0.0.1:8000/accounts/register/ to register