📜 ⬆️ ⬇️

Django tips & tricks

Greetings

In this post - small tips on working with Django, which can be useful for novice developers. How I would like to know this at the beginning of my journey of mastering Django! ..
These tips should be viewed with criticism. I would be glad if you find inaccuracies / the best solution, or offer your "chips" for django, which are not in the documentation.

So, let's start from afar, and then move on to the details.
')


venv

Virtual environment


If you are not using virtualenv for your django application, be sure to try.

If you're already using virtualenv, then answer if you need --no-site-packages. This flag is enabled by default and is used when creating a virtual environment. When the “inside” program flag is turned on, the environment will not see the “outside” program. If you put any package globally with your package manager, for example, python2-django, then you will have to do pip install django “inside” the environment.
Why would you need globally installed packages?
I ran into this when I set up a search engine on xapian. Xapian comes in the delivery of xapian-core (written in C ++) and xapian-bindings (binding for various PLs, including python). It is logical to update them at the same time - if the engine has changed, then the binding must be updated. Therefore, to install xapian-core globally as a package manager, and the pip binding does not fix it (besides, they are not in the pip). Output 2:
  1. Create a trash bin inside virtualenv: ./configure --prefix = / path / to / virtualenv && make && make install
  2. Make global packages visible from the outside and update them with the package distribution manager, which I chose

Generally, when the module is written on pure python, there are no problems - install via pip in virtualenv. If a module is a mixture of, say, c ++ and python, magic begins.

The visibility / invisibility of global programs from virtualenv is set to be absent / by the presence of the [virtualenv] / lib / python *. * / No-global-site-packages.txt file. Just like that.

By the way, I recommend everyone to the article about the “isolation” of virtualenv: Why I hate virtualenv and pip (the site slows down, could only be opened via web.archive.org ). It examines how virtualenv is truly isolated from the “external” environment - in short, it is only partial isolation.


ipython


Pip install ipython will replace the standard Python shell with an advanced one, with coloring, autocompletion, introspection, convenient multi-line input, copy-tagging, etc. Django automatically picks up ipython if it is installed.
By the way, all the listed advantages can be used not only in the ./manage.py shell, but also in debugging, causing debugging with the help of import i pdb; i pdb.set_trace ().

Project structure


Django, by default, creates the necessary directories when creating a project or application. But you yourself need to think.

As the project you name, and you will import

Call your project project (django-admin.py startproject project) - well, or by a different, but the same name for all projects. Previously, I called projects according to the domain, but when re-using applications in other projects, I had to change the import paths - then from supersite import utils, then from newsite import utils. It confuses and distracts. If you expand this advice - fix (unify) for yourself the directory structure of all your projects and strictly adhere to it.

Living example:
--site.ru |--static |--media |--project (  ) |--manage.py |--project (   ) | |--settings.py | |--urls.py | |-- ... |--app1 |--app2 |--... 


Where to save html-templates

Never, never throw templates (.html) into the templates folder of your application. Always create an additional directory with the same name as the application.
This is bad, because creates a template collision, for example, with {% include 'main.html'%}:
 /gallery/templates/main.html /reviews/templates/main.html 

This is good, you can use {% include 'reviews / main.html'%}:
 /gallery/templates/gallery/main.html /reviews/templates/reviews/main.html 


{% include%}

By the way, if you use {% include 'some_template.html'%}, then it is likely that something is wrong. Why?
Example:
 def view(request): return render( request, 'master.html', {'var': 'Some text'} } 

 <!-- master.html --> Value of variable var: {{ var }}. {% include 'slave.html' %} <!-- slave.html --> Again, value of variable var: {{ var }}. 


1) KISS rides a forest. On the one hand, the page code is divided into several - master.html and the connected slave.html, and it is convenient for splitting large html-pages into parts. But in this case, the var variable is passed to the slave.html template implicitly - var is transferred to master.html, and slave.html simply “clings” to the master context. Thus, we see that the pattern inside {% include%} depends on the context of the main pattern. We have to keep track of the context of the parent template, otherwise something wrong could get into the child.
2) According to my observations, {% include%} expensive in terms of rendering. Better to avoid it.

What to do? If you really want to include some templates in others - use inclusion tags (read about them below). But easier - just write everything in one file:
 <!-- master.html --> Value of variable var: {{ var }}. Again, value of variable var: {{ var }}. 


settings.py

You do not have two different settings.py on the test and deployment servers, right?
Create additional local_settings.py and deployment_settings.py, where throw off everything that applies only to the corresponding server.
Here, for example, that it is logical to set in local_settings.py
 DEBUG = True DOMAIN = '127.0.0.1:8000' ALLOWED_HOSTS = ['127.0.0.1', DOMAIN] SERVER_EMAIL = 'mail@test.ru' EMAIL_HOST = 'localhost' EMAIL_PORT = 1025 EMAIL_HOST_USER = '' EMAIL_HOST_PASSWORD = '' EMAIL_USE_TLS = False EMAIL_SUBJECT_PREFIX = '[' + DOMAIN + '] ' DEFAULT_FROM_EMAIL = 'mail@test.ru' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'test', 'USER': 'test', 'PASSWORD': 'test', 'HOST': 'localhost', 'PORT': '', 'ATOMIC_REQUESTS': True, } } CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', } } 



In settings.py we write at the beginning:
 # Load local settings if available try: from local_settings import * except ImportError: from deployment_settings import * 

Accordingly, delete the local_settings.py. So that it does not interfere, you can add it to .gitignore.

Project root

Set the project root in settings.py - this will make life easier later:
 from os import path BASE = path.dirname(path.dirname(path.dirname(path.abspath(__file__)))) MEDIA_ROOT = BASE + '/media/' STATIC_ROOT = BASE + '/static/' 


Contextual processors (context_processors.py), {% include%} and inclusion tags


Use contextual processors only if you need to add variables to the context of each page of the site - because contextual processors will be invoked for any page, even if you do not use their results. Personally, I use them to transfer the phone number to the template context - this number is actually displayed on every page, and not once. Another example is the site menu. I registered the headers and links in the context processor, and if I need to add a new section in the menu - I just add it to the context processor, and it will automatically be added everywhere on the site.

There is one mistake - the use of contextual processors for widgets. For example, you have a news column on your site that is always displayed, i.e. on every page. It would seem to create news / context_processors.py and add the news variable with news to the context, and in the {% include 'news / news_widget.html'%} template, or even {% load news_widget%} {% news_widget news}} ...

It works, but it is littering the context and, besides, who knows if you will always have this column. There is a way - use the inclusion tag . You simply write in the template {% news%}, and already this templatetag searches for news and inserts a news column. And it works only when you actually launch it - i.e. write {% news%} in the template.


Batteries


django-debug-toolbar-template-timings

Everyone knows him and probably uses him. But there is a django-debug-toolbar-template-timings plugin for debug toolbar that measures the rendering time of templates. And given that the django templates are quite “expensive” (they are rendered for a long time), then to speed up the site this plugin is what the doctor ordered.

adv_cache_tag

django-adv-cache-tag allows you to manage caching in templates in a very flexible way - versioning, compression, partial caching. Just rate:
 {% load adv_cache %} {% cache 0 object_cache_name object.pk obj.date_last_updated %} <!--   ,     obj.date_last_updated --> {{ obj }} {% nocache %} {{ now }} <!--      --> {% endnocache %} {{ obj.date_last_updated }} {% endcache %} 


django-mail-templated

Email templates are what django lacks. django-mail-templated

django-ipware

django-ipware will determine the user's ip for you, and make it better.
You know where to get the ip user?
 'HTTP_X_FORWARDED_FOR', # client, proxy1, proxy2 'HTTP_CLIENT_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'HTTP_VIA', 'REMOTE_ADDR', 



Beautiful soup

Do not write your html parser. Do not parse html yourself. Everything is already there.

Templatetags that may come in handy


add_class

If you create a form and want to set a style, class or placeholder for each input, then django will force you to violate the principles and write all styles directly in forms.py:
 class SomeForm(ModelForm): class Meta: model = SomeModel fields = ('field1', 'field2') widgets = { 'field1': Textarea(attrs={'rows': '2', 'class': 'field1_class'}), } 

Every time I get messed up when seeing html text not in .html files. This violates the MVT architecture. Therefore, I created a filter for myself:
 {% load add_class %} {{ form.field1|add_class:'field1_class' }} 

This filter adds a class to the tags, but you can rewrite and add any property.
Code add_class.py
 from django import template from django.utils.safestring import mark_safe from bs4 import BeautifulSoup register = template.Library() @register.filter def add_class(html, css_class): soup = BeautifulSoup(unicode(html), 'html.parser') for tag in soup.children: if tag.name != 'script': if 'class' in tag: tag['class'].append(css_class) else: tag['class'] = [css_class] return mark_safe(soup.renderContents()) 



is_current_page

Sometimes you need to display something in a template if a particular page is open. For example, highlight the "store" button in the menu if the user is now in the store section. I suggest the following option:
 from django import template from django.core.urlresolvers import resolve from project.utils import parse_args register = template.Library() @register.filter def is_current_page(request, param): return resolve(request.path).view_name == param 

This is a filter, not a tag, and the reason is the same: you can build absolutely wildest constructions with {% if%}. For example, if the current page is a product card, and the user is authorized:
 {% if request|is_current_page:'shop/product' and user.is_authenticated %} 

There is an alternative, more accurate, implementation in which arguments (args or kwargs) are used to determine the exact page (i.e., not just “the page of a product”, but “the page of the product with id = 36”):
 {% if request|is_current_page:'shop/product,id=36' %} 

 @register.filter def is_current_page(request, param): params = param.split(',') name = params[0] args, kwargs = parse_args(params[1:]) # Do not mix args and kwargs in reverse() - it is forbidden! if args: return request.path == reverse(name, args=args) elif kwargs: return request.path == reverse(name, kwargs=kwargs) else: return request.path == reverse(name) 



Models


Empty

Models may be empty. Like this:
 class Phrase(models.Model): pass class PhraseRu(models.Model): phrase = models.ForeignKey(Phrase, verbose_name='', related_name='ru') class PhraseEn(models.Model): phrase = models.ForeignKey(Phrase, verbose_name='', related_name='en') 

In this case, Phrase is the link between PhraseEn and PhraseRu, although it contains nothing in itself. It is useful when the two models are equivalent, and they need to be connected into a single whole.

Generic relation mixin

GenericRelation objects are always returned by the QuerySet, even if we have, we know for sure that there is only one object:
 class Token(models.Model): content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey() class Registration(models.Model): tokens = generic.GenericRelation(Token) 

If you need to access the token, we write registration.tokens.first (). But we know that the token is one, and we want to write just registration.token and get the coveted token right away. This is possible using mixin:
 class Token(models.Model): content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey() class TokenMixin(object): @property def token(self): content_type = ContentType.objects.get_for_model(self.__class__) try: return Token.objects.get(content_type__pk=content_type.pk, object_id=self.id) except Token.DoesNotExist: return None class Registration(models.Model, TokenMixin): tokens = generic.GenericRelation(Token) 


Now registration.token works!

get_absolute_url


Try not to write {% url 'shop / product' id = product.id%}.
Better for each model, set the get_absolute_url () method, and use {{object.get_absolute_url}}. At the same time, the “watch on site” link will appear in the admin panel.

pre_save

In pre_save, you can see if the model changes after saving or not. Price - request to the database to get the old record from the database.
 @receiver(pre_save, sender=SomeModel) def process_signal(sender, instance, **kwargs): old_model = get_object_or_None(SomeModel, pk=instance.pk) if not old_model: # Created old_value = None ... else: old_value = old_model.field new_value = instance.field if new_value != old_value: # field changed! 


Forms


This pattern was already on Habré, but it is too good not to mention it.
 form = SomeForm(request.POST or None) if form.is_valid(): # ... actions ... return HttpResponseRedirect(...) return render( request, {'form': form} ) 


That's all. Thanks for attention.

UPD. As usual on Habré, in the comments, habrazhiteli expressed their opinions and offered a bunch of great ideas, additions and comments to the article. I did not include them in the article, but instead I strongly recommend that you read the comments on the article.

Source: https://habr.com/ru/post/224011/


All Articles