Django tips & tricks

    Greetings!

    In this post - some small tips on working with Django that may be useful to novice developers. As I would like to know this at the beginning of my journey of mastering Django! ..
    Consider these tips with some criticism. I will be glad if you find inaccuracies / the best solution, or suggest your own "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, then be sure to give it a try.

    If you are 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 flag for the program “inside” is on, the environment will not see the program “outside”. If you package your package manager globally, for example, python2-django, then you will still have to install pip install django "inside" the environment.
    Why might you need globally installed packages?
    I came across this when setting up a search engine on xapian. Xapian comes in the supply of xapian-core (written in C ++) and xapian-bindings (binding for different languages, including python). It is logical to update them at the same time - if the engine has changed, then the bindings must be updated. Therefore, putting xapian-core globally as a package manager, and piping does not fix it (besides, they are not in pip). Output 2:
    1. Create a garbage can inside virtualenv: ./configure --prefix = / path / to / virtualenv && make && make install
    2. Make global packages visible from the outside and update them with the package manager of the distribution, which I chose

    In general, when the module is written in 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 by the absence / presence of the file [virtualenv] / lib / python *. * / No-global-site-packages.txt. So simple.

    By the way, I recommend everyone an article about the "isolation" of virtualenv: Why I hate virtualenv and pip (the site slows down, I could only open through web.archive.org ). It examines how truly virtualenv is 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, auto-completion, introspection, convenient multi-line input, copy-paste, etc. Django automatically picks up ipython if it is installed.
    By the way, all these advantages can be used not only in ./manage.py shell, but also in debug, causing debugging using import i pdb; i pdb.set_trace ().

    Project structure


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

    As you name the project, you will import

    Name your project project (django-admin.py startproject project) - well, or another, but the same name for all projects. I used to name projects according to the domain, but when reusing applications in other projects, I had to change the import paths - from supersite import utils, then from newsite import utils. This is confusing and distracting. If you expand this advice, fix (unify) the directory structure of all your projects for yourself and strictly adhere to it.

    Live example:
    --site.ru
      |--static
      |--media
      |--project (папка с проектом)
         |--manage.py
         |--project (папка с основным приложением)
         |  |--settings.py
         |  |--urls.py
         |  |-- ...
         |--app1
         |--app2
         |--...
    


    Where to save html templates

    Never, ever drop templates (.html) into the templates folder of your application. Always create an additional directory with a name that matches the name of 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 chances are that something is wrong. Why?
    Example:
    def view(request):
        return render(
            request,
            'master.html',
            {'var': 'Some text'}
        }
    

    
    Value of variable var: {{ var }}.
    {% include 'slave.html' %}
    
    Again, value of variable var: {{ var }}.
    


    1) KISS rides the forest. On the one hand, the page code is divided into several - master.html and plugin slave.html, and this 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 passed to master.html, and slave.html just “hooks” the master context. Thus, we see that the pattern inside {% include%} depends on the context of the main pattern. We are forced to monitor the context of the parent template, otherwise something may go wrong in the child template.
    2) According to my observations, {% include%} is 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:
    
    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 deploy servers, right?
    Create additional local_settings.py and deployment_settings.py, where drop everything that applies only to the corresponding server.
    Here, for example, what 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, on deploy we delete local_settings.py. So that it does not interfere, it can be added to .gitignore.

    Project root

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


    Context 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 called for any page, even if you do not use their results. Personally, I use them to transfer the phone number to the context of the template - this number is actually displayed on every page, and more than 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 to the menu, I just add it to the context processor and it will automatically be added everywhere on the site.

    There is one mistake - using context 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 template {% include 'news / news_widget.html'%}, or even {% load news_widget%} {% news_widget news%} ...

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


    Batteries


    django-debug-toolbar-template-timings

    Everyone knows him and probably uses him. But there is django-debug-toolbar-template-timings - a debug toolbar plugin that measures the time it takes to render templates. And considering that django templates are quite “expensive” (render 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 very flexible caching control in templates - versioning, compression, partial caching. Just rate:
    {% load adv_cache %}
    {% cache 0 object_cache_name object.pk 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.
    Do you know where to get the user's ip?
    '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 the 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 jarred when I see 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 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 certain 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 completely 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, which uses arguments (args or kwargs) to determine the exact page (that is, not just “the page of any product”, but “the product page 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 two models are equivalent, and they need to be connected into a single whole.

    Generic relation mixin

    GenericRelation objects are always returned by QuerySet, even if 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 there is only one token, and we want to write just registration.token and get the treasured token right away. This is possible with 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 link “look at the site” will appear in the admin panel.

    pre_save

    In pre_save, you can find out whether the model will change after saving or not. Price - a 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 the hub, 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 the hawkers 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.

    Also popular now: