THUX and DJANGO

1. Django Project

Il nome del progetto è composto da: <tipology><year>.<domain>.

All’interno del progetto abbiamo le seguenti cartelle:

  • apps

  • docs

  • web

Nella cartella apps vengono inserite le APP del progetto.

La cartella docs contiene la documentazione del progetto.

Mentre la cartella web contiene i file e le cartelle proprie del progetto.

All’interno della cartella principale abbiamo i seguenti file:

  • .env

  • .hgignore

  • .sync

  • manage.py

  • requirements.txt

  • setup.py

2. Cartella web

Nella cartella web sono presenti le seguenti cartelle:

  • locale

  • management

  • settings

  • static

  • templates

  • tests

Inoltre sono presenti i seguenti file:

  • dashboard.py

  • exceptions.py

  • jmenu.py

  • middleware.py

  • remote_test.py

  • urls.py

  • utils.py

  • views.py

2.1 Cartella locale

La cartella locale viene popolata con i file .po e .mo per le traduzioni del progetto.

2.2 Cartella management

Nella cartella management sono inseriti i comandi di progetto.

2.3 Cartella settings

Nella cartella settings sono presenti i seguenti file:

  • __init__.py

  • base.py

  • dev.py

  • staging.py

  • production.py

  • local.py

  • loggings.py

  • settings.py

  • routers.py

Il file __init__.py è stato personalizzato per importare i file di settaggio presenti nella cartella secondo uno specifico ordine. Il file settings.py è un link simbolico che punta a dev, production o staging in funzione dell’environment.

2.3.1 file base.py

Nel file base.py sono inseriti tutti i settaggi di base del progetto che sono comuni in tutti gli environment.

ADMIN_LANGUAGE_CODE = 'it'
ADMINS = [('THUX Team', 'code@thux.it')]

AUTH_USER_MODEL = 'account.User'
AUTH_USER_USERNAME_FIELD = 'email'
AUTH_USER_EMAIL_UNIQUE = True
AUTH_USER_REQUIRED_FIELDS = []

AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

Importante

  • AUTH_USER_MODEL: da inserire solo se si vuole cambiare il comportamento di default

  • AUTH_USER_EMAIL_UNIQUE: se True email unique se False username unique

  • AUTH_USER_REQUIRED_FIELDS: da inserire solo se si vuole cambiare il comportamento di default

  • AUTH_USER_REQUIRED_FIELDS: se nel AUTH_USER_USERNAME_FIELD = “username” occorre impostare nel setting la stringa “email”

AUTH_USER_MODEL = 'account.User'
AUTH_USER_USERNAME_FIELD = 'username'
AUTH_USER_EMAIL_UNIQUE = False
AUTH_USER_REQUIRED_FIELDS = ['email']

2.3.2 file dev.py

Nel file dev.py sono inseriti i settaggi che vengono utilizzati sui PC personali ma che sono comuni a tutti gli sviluppatori.

DEBUG = True
CORS_ORIGIN_ALLOW_ALL = True
EMAIL_SUBJECT_PREFIX = '[DEV:{{ ORGANIZATION }}-{{ PROJECT }}-{{ YEAR }}]'
EMAIL_BACKEND = 'web.core.dev_mail_backend.DevEmailBackend'

2.3.3 file staging.py

Il file staging.py contiene i settaggi per l’ambiente di staging.

EMAIL_SUBJECT_PREFIX = '[STAG:{{ ORGANIZATION }}-{{ PROJECT }}-{{ YEAR }}]'
EMAIL_BACKEND = 'web.core.dev_mail_backend.DevEmailBackend'

2.3.4 file production.py

Il file production.py contiene i settaggi dell’ambiente di produzione.

EMAIL_SUBJECT_PREFIX = '[PROD:{{ ORGANIZATION }}-{{ PROJECT }}-{{ YEAR }}]'
EMAIL_HOST = '< to set >'
EMAIL_PORT = '< to set >'
EMAIL_HOST_USER = '< to set >'
EMAIL_USE_TLS = '< to set >'

BASE_SITE_DOMAIN = ''< to set >''
BASE_PROTOCOL = 'https://'
BASE_URL = ''< to set >''

EMAIL_BACKEND = 'web.core.prod_mail_backend.ProdEmailBackend'

2.3.5 file local.py

Il file local.py è contiene i settaggi specifici di ogni macchina o le chiavi segrete e le password che non devono essere pubblicate su un sistema di versioning.

SECRET_KEY = '< to set >'
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': '< to set >',
        'USER': '< to set >',
        'PASSWORD': '< to set >',
        'HOST': '< to set >',
        'PORT': '< to set >',
    },
EMAIL_HOST_PASSWORD = '< to set >
AUTH_PASSWORD_VALIDATORS = []

Importante

  • le password, le secret-key ecc. vengono inserite in local.py per non essere committate

  • nel local.py in dev il setting AUTH_PASSWORD_VALIDATORS viene disabilitato

2.3.6 file di loggings

Il file loggings.py contiene i settaggi per definire le modalità di log. Di seguito la struttura del file settings.py di log:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {},
    'filters': {},
    'handlers': {},
    'loggers': {},
}

L’attributo formatter serve per definire la struttura del messaggio di log.

Abbiamo previsto due livelli: dettagliato e sintetico:

'formatters': {
    'verbose': {'format': '%(levelname)s %(asctime)s %(message)s'},
    'simple': {'format': '%(levelname)s %(message)s'},
}

L’attributo filters viene utilizzato per definire il comportamento con debug=True o debug=False:

'filters': {
    'require_debug_true': {'()': 'django.utils.log.RequireDebugTrue',},
    'require_debug_false': {'()': 'django.utils.log.RequireDebugFalse',},
}

L’attributo handlers serve per definire su quale tipo di “output” scrivere il log:

'handlers': {
    'console': {
        'class': 'logging.StreamHandler',
        'filters': ['require_debug_true'],
        'formatter': 'verbose',
    },
    'file': {
        'class': 'logging.FileHandler',
        'formatter': 'verbose',
    },
    'mail_admins': {
        'class': 'django.utils.log.AdminEmailHandler',
        'level': 'ERROR',
    },
}

L’attributo loggers serve per definire le proprietà di ogni singolo logger:

'loggers': {
    # tail -f /var/log/django/thux-exceptions.log
    'exceptions': {
        'handlers': ['file'],
        'filename': '/var/log/django/thux-exceptions.log',
        'level': 'DEBUG',
    },
    # tail -f /var/log/django/thux-api_exceptions.log
    'api_exceptions': {
        'handlers': ['file'],
        'filename': '/var/log/django/thux-api_exceptions.log',
        'propagate': True,
        'level': 'DEBUG',
    },
    'console': {
        'handlers': ['console'],
        'level': 'DEBUG',
        'propagate': True,
    },
    # tail -f /var/log/django/thux_errors.log
    'error': {
        'handlers': ['file', 'mail_admins'],
        'level': 'ERROR',
    },
    'import': {
        'handlers': ['file'],
        'filename': '/var/log/django/thux_imports.log',
        'level': 'WARNING',
    },
    'email': {
        'handlers': ['mail_admins'],
        'level': 'INFO',
    },
    'cron': {
        'handlers': ['file'],
        'filename': '/var/log/django/thux_crons.log',
        'level': 'INFO',
    },
    'tests': {
        'handlers': ['tests',],
        'filename': '/var/log/django/thux_tests.log',
        'level': 'INFO',
    },
}

2.3.7 routers.py

Il file routers.py non sempre è presente, viene utilizzato in presenza di più DB. Di seguito un esempio con un database la cui chiave nel settings è «dealer». «default» è la chiave del database principale.

Importante

Occorre definire il settaggio anche nel file production.py con valori impostati a «<set in local>»? Potrebbe essere utile per evidenziare la presenza di più DB. Analogamente devono essere riportate nel file «cookiecutter.readme» le operazioni necessarie all’installazione e alla migrazione dei DB. Per migrare il database: dj migrate –database=dealer

# -*- coding: utf-8 -*-

class DealerRouter:
    """
    A router to control all database operations on models in the 'delaer' application.
    """

    def db_for_read(self, model, **hints):
        """
        Attempts to read dealer models go to 'dealer' db.
        """
        if model._meta.app_label == 'dealer':
            return 'dealer'
        return 'default'

    def db_for_write(self, model, **hints):
        """
        Attempts to write dealer models go to 'dealer' db.
        """
        if model._meta.app_label == 'dealer':
            return 'dealer'
        return 'default'

    def allow_relation(self, obj1, obj2, **hints):
        """
        Allow relations if objs is in same db
        """
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        Make sure the 'dealer' app only appear in the 'dealer' database.
        """
        if app_label == 'dealer':
            return db == 'dealer'
        return db == 'default'

Nel file base.py per completare l’operazione occorre aggiungere il seguente settings:

# -- coding: utf-8 --

DATABASE_ROUTERS = [“web.settings.routers.DealerRouter”]

Non occore fare alcuna modifica ai managers.py dell’applicazione «dealer» in quanto si utilizzano i routers.

2.4 Cartella static

Per adesso la cartella contiene la cartella admin con i file statici html, css e js per l’interfaccia di Admin.

2.5 Cartella templates

2.6 Cartella tests

2.7 File dashboard.py

2.8 File exceptions.py

3 Django app

Nella cartella apps vengono inserite tutte le applicazioni, divise in cartelle, che compongono il progetto.

La struttura di ogni singola app si compone delle seguenti cartelle:

  • api

  • docs

  • management

  • migrations

  • static

  • templates

  • tests

e dei seguenti file nella cartella principale:

  • admin.py

  • admin_actions.py

  • admin_forms.py

  • admin_views.py

  • apps.py

  • cookiecutter.yaml

  • dashboard.py

  • forms.py

  • imports.py

  • jmenu.py

  • managers.py

  • models.py

  • resources.py

  • settings.py

  • urls.py

  • utils.py

  • views.py

3.1 Cartella api

La cartella api si compone delle seguenti cartelle:

  • versionX: dove X è la versione dell’API REST.

In questa cartella sono presenti i file:

  • remote_test.py

  • serializers.py

  • urls.py

  • views.py

Inoltre nella cartella principale sono presenti i seguenti file:

  • remote_test.py

  • urls.py

  • views.py

3.1.1 cartella verdionX

Nella cartella è presente il file remote_test.py con all’interno un Mixin:

class RemoteTestMixin:
    def {{ test }}_list(self, filters=None):
    def {{ test }}_retrieve(self, pk):
    def {{ test }}_create(self, post_dict):
    def {{ test }}_partial_update(self, pk, post_dict):
    def {{ test }}_enable(self, pk):
    def {{ test }}_disable(self, pk):
    def {{ test }}_destroy(self, pk):

Importante

test: v{{ X }}_{{ app_name }}_{{ model_name }} X: numero di versione app_name: nome dell’APP model_name: nome del modello esempio: v1_ticket_ticket_retrieve

il file serializers.py contiene le classi dei serializer:

class {{ Ser }}ListSeializer(ThuxListSerializer, serializers.ModelSerializer):
    class Meta:
        model = models.{{ Model }}
        fields = (...)

class {{ Ser }}RetrieveSeializer(ThuxRetrieveSerializer, serializers.ModelSerializer):
    class Meta:
        model = models.{{ Model }}
        fields = (...)

class {{ Ser }}CreateSeializer(ThuxCreateSerializer, serializers.ModelSerializer):
    class Meta:
        model = models.{{ Model }}
        fields = (...)

class {{ Ser }}PartialUpdateSeializer(
    ThuxPartialUpdateSerializer, serializers.ModelSerializer):
    class Meta:
        model = models.{{ Model }}
        fields = (...)

class {{ Ser }}SetStatusSerializer(ThuxSetStatusSerializer, serializers.ModelSerializer):
    class Meta:
        model = models.{{ Model }}
        fields = ('status',)

    def update(self, instance, validated_data):
        instance.status = validated_data['status']
        instance.save()
        return instance

    def to_representation(self, instance):
        return {{ Ser }}RetrieveSerializer(instance).data

Nel file urls.py sono presenti i router dell’API:

urlpatterns = router.urls

router.register(
    path({{ Model }}),
    views.{{ Url }}ViewSet,
)
urlpatterns = router.urls

Nel file views.py sono presenti le classi ViewSet:

class {{ View }}ViewSet(TicketAbstractViewsSet):
    queryset = models.{{ Model }}.objects.all()
    serializer_class = serializers.{{ Ser }}Serializer

    def get_queryset(self):
        qs = super().get_queryset()
        if self.user.is_superuser:
            return qs
        elif self.user.< user_role >:
            return qs.filter(status=1)
        return qs.none()

    def check_permissions(self, requests):
        # GESTIRE I PERMESSI SECONDO LE SPECIFICHE DI DJANGO
        super().check_permissions(request)
        if self.user.is_superuser:
            return
        elif self.user.< user_role >:
            if self.action in < action_list>:
            pass
        raise exceptions.PermissionDenied()

    def get_serializer_class(self):
        action = self.action.replace('-', ' ').replace('_', ' ').title().replace(' ', '')
        serializer = getattr(serializers, f'{{ Ser }}{action}Serializer', None)
        if serializer:
            return serializer
        return super().get_serializer_class()

    @action(methods=['patch'], detail=True)
    def enable(self, request, **filters):
        """
        enable {{ Model Name }}
        """
        return super().partial_update(request)

    @action(methods=['patch'], detail=True)
    def disable(self, request, **filters):
        """
        enable {{ Model Name }}
        """
        return super().partial_update(request)

Importante

  • user_role: condizione per riconoscere ruolo dello user. Utilizziamo il modello ContactSetting dell’applicazione.

  • action_list: le azioni definite sono: “list”, “retrieve”, “create”, “partial_update”, “destroy”, “enable”, “disable”

Utilizziamo il modello ContactRole per determinare i permessi sulle azioni

role

app_name

model_name

action

conditions

Backoffice

ticket

assistance

list

own, active

Backoffice

ticket

assistance

retrieve

own, active

3.2 Cartella principale APP

3.2.1 File settings.py

Nel file settings.py sono riportati i settaggi di default dell’applicazione. L’applicazione non va in in errore quando mancano i settaggi del progetto perché sono impostati i setting di default nell’applicazione. I settaggi dell’app devono essere generici e non specifici del progetto.

Importante

  • per facilitare la ricerca e comprendere a quali applicazioni il settaggio si riferisce il nome dello segue questa convenzione <APPLICATION>_<MODEL>_<FIELD>_<NAME>

  • i settaggi si importano con l’istruzione from django.conf import settings

3.2.2 File models.py

Nel file models.py sono presenti i modelli:

class Ticket(CleanModel, StatusModel, UserModel, DateModel, OrderedModel):
    areas = models.ManyToManyField(
        Area,
        blank=True,
        related_name='tickets',
        verbose_name=_('areas),
    )

Importante

  • il nome del campo M2M è scritto al plurale

  • per un campo M2M non si usa l’attributo null

  • l’attributo related_name riporta il nome del modello a cui si riferisce al plurale

  • il verbose_name è sempre riportato in fondo

  • gli attributi del campo per una più facile lettura vengono elencati in verticale

  • tutti i modelli ereditano i modelli astratti:

    CleanModel, StatusModel, UserModel, DateModel, OrderedModel

category = models.ForeignKey(
    Category,
    on_delete=models.PROTECT,
    related_name='tickets',
    blank=True, null=True,
    verbose_name=_('category'),
)

Importante

  • il nome del campo FK è scritto al singolare

  • l’attributo related_name riporta il nome del modello a cui si riferisce al plurale

  • on_delete sempre impostato in PROTECT tranne alcuni casi particolari

class TicketProject(CleanModel, ProfileModel, UserModel, DateModel, StatusModel, OrderedModel):
    project = models.OneToOneField(
        Project,
        primary_key=True,
        on_delete=models.PROTECT,
        related_name='ticket_project',
        verbose_name=_('project'),
    )

Importante

  • il nome del campo O2O è scritto al singolare

  • l’attributo related_name riporta il nome del modello a cui si riferisce al singolare

  • on_delete sempre impostato in PROTECT tranne alcuni casi particolari

class TicketProject(CleanModel, ProfileModel, UserModel, DateModel, StatusModel, OrderedModel):
    is_unique = models.IntegerField(
        # Possible values are None and 1
        choices=settings.TICKET_TICKETPROJECT_IS_UNIQUE_CHOICHES,
        blank=True, null=True,
        verbose_name=_('is unique')
    )
    icon = models.FileField(
        upload_to=upload_ticket_project_icon,
        max_length=100,
        blank=True,
        verbose_name=_('icon'),
    )

Importante

  • l’attributo choices viene sempre definito tramite l’utilizzo di un setting

  • l’attributo upload_to viene sempre valorizzato con una funzione

def upload_ticket_project_icon(instance, filename):
    date_dir = timezone.now()
    return f'ticket/ticket_project/icon/{date_dir:%Y/%m/%d}/{filename}'

def upload_ticket_document(instance, filename):
    date_dir = timezone.now()
    new_filename = slugify(instance.title)
    return f'ticket/ticket_document/{date_dir:%Y/%m/%d}/{new_filename}.pdf'

Importante

  • la funzione per l’upload dei file è sviluppata in modo da ridurre il numero dei file in una singola cartella per velocizzare la lettura e da standardizzare i nomi

class TicketProject(CleanModel, ProfileModel, UserModel, DateModel, StatusModel, OrderedModel):
    ...
    class Meta:
        verbose_name = _('ticket project')
        verbose_name_plural = _('ticket projects')
        ordering = ('ordering', 'default_for_organization', 'assistance_email',)
        permissions = (
            ("list_ticketproject", "Can list ticket project"),
            ("detail_ticketproject", "Can detail ticket project"),
            ("enable_ticketproject", "Can enable ticket project"),
            ("disable_ticketproject", "Can disable ticket project"),
        )
        unique_together = (('is_unique', 'assistance_email'),)

    def __str__(self):
        return f"{self.project}"

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

    def update(self, *args, **kwargs):
        super().update(*args, **kwargs)

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)

    def clean_fields(self):
        self.set_<method_field>()

    def clean(self):
        self.check_<method>()
        self.check_<method_field>()
        super().clean()

    def set_<method_field>(self):
        self.<field> = <value>

    def check_<method_field>(self):
    if <condition>:
        raise ValidationError({'<field>': _('<error message>')})

    def get_other_method(self):
        pass

    @property
    def <property>(self):
        return <property>

Importante

  • sopra vengono riportati in ordine la classe meta e i metodi come da nostra convenzione

  • nel clean_fields vengono inseriti tutti i metodi set_<method_field> che settano il campo p i campi per non fare andare in errore l’applicazione prima del save

  • nel metodo clean sono riportati prima i metodi set_<method_field> e poi i check_<method_field> che vengono usati sia dai form sia dal save

class TicketProject(CleanModel, ProfileModel, UserModel, DateModel, StatusModel, OrderedModel):
    @classmethod
    def class_method(cls, <attibutes>):
        pass

Importante

  • i classmethod vengono inseriti in testa prima della definizione dei campi del modello

  • i method vengono inseriti dopo i check

  • infine vengono create le property

3.2.3 File admin.py

Il file admin.py contiene le classi per creare le interfacce di amministrazione con Django. Di seguito la struttura di un’interfaccia per la gestione del «ContactSetting» model:

class ContactSettingAdmin(UserAdminMixin, AdvancedSearchModelAdmin, admin.ModelAdmin):
    search_fields = ('contact__first_name__icontains', 'contact__last_name__icontains')
    advanced_search_fields = (
        ('status', 'main_areas__name__icontains',),
        ('contact__first_name__icontains', 'contact__last_name__icontains',),
        ('catch_all', 'is_manager', 'is_staff', 'is_customer'),
    )
    list_filter = ('status', 'is_manager', 'is_staff', 'is_customer', 'catch_all', 'main_areas',)
    fieldsets = (
        (_('general information'), {
            'fields': (
                ('contact', 'main_areas'),
                ('is_manager', 'catch_all',),
                ('is_staff',),
                ('is_customer',)
            )
        }),
        (_('visualization admin'), {
            'classes': ('collapse',),
            'fields': ('ordering', 'status')
        }),
        (_('logs admin'), {
            'classes': ('collapse',),
            'fields': ('creator', 'date_create', 'last_modifier', 'date_last_modify')
        }),
    )
    list_display = (
        'id', 'status', 'contact', 'organization', 'areas_display',
        'is_manager', 'is_staff', 'is_customer', 'catch_all', 'last_modifier', 'date_last_modify',
        'ordering',
    )
    list_display_links = ('contact',)
    autocomplete_fields = ('contact',)
    filter_horizontal = ('main_areas',)
    readonly_fields = (
        'creator', 'date_create', 'last_modifier', 'date_last_modify',
    )

    def get_queryset(self, request):
        return super().get_queryset(request).select_related(...).prefetch_related(...)

admin.site.register(models.ContactSetting, ContactSettingAdmin)

Importante

  • search_field: utilizzato anche dagli autocomplete_fields

  • advanced_search_fields: modulo THUX per la ricerca avanzata

  • list_display_links: link sul campo più significativo della ListSeializer

  • id e status: sempre primi della lista

  • ordering: sempre ultimo

Esempio di struttura di un’interfaccia Inline:

class TicketCommentAdminForm(forms.ModelForm):
    message = forms.CharField(widget=CKEditorWidget())
    info = forms.CharField(widget=JSONEditorWidget())

    class Meta:
        fields = '__all__'
        model = models.TicketComment

class TicketCommentInline(admin.TabularInline):
    form = TicketCommentAdminForm
    model = models.TicketComment
    show_change_link = True
    fields = ('title', 'parent', 'message', )
    autocomplete_fields = ('parent', )
    extra = 0

Importante

  • per applicazioni con pochi modelli il codice viene scritto all’interno di un unico file admin.py

  • per applicazioni con molti modelli è preferibile una struttura più articolata

Esempio di struttura articolata:

admin
    __init__.py
    actions.py
    admin.py
    forms.py
    inlines.py

Oppure in presenza di molti modelli:

admin
    __init__.py
    dealer
        __init__.py
        actions.py
        admin.py
        forms.py
        inlines.py
    customer
        __init__.py
        actions.py
        admin.py
        forms.py
        inlines.py
    supplier
        __init__.py
        actions.py
        admin.py
        forms.py
        inlines.py

3.2.4 File managers.py

Di seguito alcuni esempi:

** managers.py .. code-block:: python

class TournamentQuerySet(models.QuerySet):
def game_status(self, value):
if value == “open”:

return self.filter(Q(deadline__gt=timezone.now()) & ~Q(status=-1))

if value == “live”:

return self.filter(Q(deadline__lte=timezone.now(), end_date__gte=timezone.now()) & ~Q(status=-1))

if value == “over”:

return self.filter(Q(end_date__lt=timezone.now()) | Q(status=-1))

class TournamentManager(models.Manager):
def get_queryset(self):

return TournamentQuerySet(self.model, using=self._db)

def game_status(self, value):

return self.get_queryset().filter(~Q(status=0)).game_status(value)

** models.py .. code-block:: python

class Tournament(cleans.TournamentCleanModel, UserModel, DateModel, StatusModel, OrderedModel):

objects = managers.TournamentManager()

3.2.5 Cartelle management/commands

Di seguito la struttura di un comand e alcuni esempi:

# -*- coding: utf-8 -*-
import logging

from django.conf import settings
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.utils import timezone
from logging import StreamHandler


def valid_datetime(s):
    try:
        datetime = timezone.datetime.strptime(s, "%Y-%m-%dT%H:%M")
        tz = timezone.get_current_timezone()
        return timezone.make_aware(date, tz, True)
    except ValueError:
        raise ValueError


def valid_date(s):
    try:
        date = timezone.datetime.strptime(s, "%Y-%m-%d")
        tz = timezone.get_current_timezone()
        return timezone.make_aware(date, tz, True)
    except ValueError:
        raise ValueError


def valid_month(month):
    if not isinstance(int(month), int) and str(int(month)) == month:
        raise ValidationError("Insert positive or negative integer")
    month = int(month)
    if not (-11 <= month <= 12):
        raise ValidationError("Insert valid month: from -11 to 12")
    return month


def valid_year(year):
    if not isinstance(year, int) and str(int(year)) == year:
        raise ValidationError("Insert positive or negative integer")
    return int(year)


class Command(BaseCommand):
    help = "..."
    logger = logging.getLogger('file')

    def add_arguments(self, parser):
        parser.add_argument(
            '-c', '--console', action='store_true', default=False,
            help='Debug - write logging to console',
        )
        parser.add_argument(
            '-l', '--debug-level', default='info',
            help='Set debug level (debug, info, warnings) for console',
        )
        parser.add_argument(
            '-e', '--to_emails',
            nargs='+', type=str,
            help="Insert list of emails"
        )
        parser.add_argument(
            '-d', '--date', type=valid_date,
            help="Insert date - format YYYY-MM-DD"
        )
        parser.add_argument(
            '--datetime', required=False,
            type=valid_datetime,
            help="Insert date time - format YYYY-MM-DD HH:MM"
        )
        parser.add_argument(
            '-D', '--delete', action='store_true', default=False,
            help='Delete data',
        )
        parser.add_argument(
            '-m', '--month',
            type=valid_month, help='Insert positive or negative integer'
        )
        parser.add_argument(
            '-y', '--year',
            type=int, help='Insert positive or negative integer'
        )
        parser.add_argument(
            '-s', '--status', type=int, required=False,
            choices=[value[0] for value in settings.STATUS_CHOICES]
        )
        parser.add_argument(
            '-L', '--language',
            type=str, choices=['it', 'en']
        )

    def handle(self, *args, **options):
        try:
            debug_level = getattr(logging,  options.get('debug_level').upper())
            self.logger.setLevel(debug_level)
            self.logger.handlers[0].setLevel(debug_level)

            if options.get('console'):
                console_handler = StreamHandler()
                console_handler.setLevel(debug_level)
                self.logger.addHandler(console_handler)
                self.logger.setLevel(debug_level)

            to_emails = options['to_emails']
            date = options['date']
            language = options['language']

            # ...
            self.logger.debug("...")

        except Exception as e:
            self.stdout.write(self.style.ERROR(
                f'Execution failed: {e}')
            )

Comando per la disabilitare i TicketProject scaduti:

class Command(BaseCommand):
    help = __doc__

    def add_arguments(self, parser):
        # -c enables logging using store_true
        parser.add_argument(
            '-c', '--console', action='store_true', default=False,
            help='Debug - write logging to console',
        )
        parser.add_argument(
            '-s', '--stop', action='store_true', default=False,
            help='If find an error stop command',
        )
        parser.add_argument(
            '-l', '--debug-level', default='info',
            help='Set debug level (debug, info, warnings) for console',
        )

    output_transaction = True

    def handle(self, *args, **options):
        try:
            debug_level = getattr(logging, options.get('debug_level').upper())
            logger.setLevel(debug_level)
            logger.handlers[0].setLevel(debug_level)

            if options.get('console'):
                console_handler = StreamHandler()
                console_handler.setLevel(debug_level)
                logger.addHandler(console_handler)
                logger.setLevel(debug_level)

            disable_ticket_projects_expired(options.get('stop'))
        except Exception as e:
            self.stdout.write(self.style.ERROR(
                f'Execution failed: {e}')
            )

Comando per parsare le email da riga di comando:

class Command(BaseCommand):
    logger = logging.getLogger('file')

    help = __doc__

    def add_arguments(self, parser):
        parser.add_argument(
            '-c', '--console', action='store_true', default=False,
            help='Debug - write logging to console',
        )
        parser.add_argument(
            '-l', '--debug-level', default='info',
            help='Set debug level (debug, info, warnings) for console',
        )
        parser.add_argument(
            '-f', '--file-name',
            help='Read from file (not from stdin)'
        )

    output_transaction = True

    def handle(self, *args, **options):
        try:
            debug_level = getattr(logging,  options.get('debug_level').upper())
            self.logger.setLevel(debug_level)
            self.logger.handlers[0].setLevel(debug_level)
            recorder = User.objects.get(email=settings.TICKET_COMMANDS_PARSE_EMAIL_RECORDER)
            filename = options.get('file_name')

            if options.get('console'):
                console_handler = StreamHandler()
                console_handler.setLevel(debug_level)
                self.logger.addHandler(console_handler)
                self.logger.setLevel(debug_level)

            email_creator = CreateTicketFromEmail(filename=filename, recorder=recorder)

            self.logger.debug(f"Start parsing email")
            email_creator.do()
            self.logger.info(f"Successfully executed command: parse_email")
            self.stdout.write(self.style.SUCCESS(
                f'Successfully executed command: parse_email')
            )
        except Exception as e:
            try:
                if not e.filename:
                    timestamp = timezone.now().strftime('%Y.%m.%d-%H:%M')
                    filename = f'/var/tmp/error-email/{timestamp}_ticket.mbox'
                    email_object = parser.ParseEmail(filename)
                    with open(filename, 'wb') as f:
                        f.write(email_object.msg.as_bytes())
                    e.filename = filename
                self.logger.error(f"Failed to parse email: {e}")
                self.stdout.write(self.style.ERROR(
                    f'Execution failed: {e}')
                )
                raise e
            except Exception as ex2:
                self.logger.error(
                f"Parser failed to parse email and we can not write email in /var/tmp/error-email/: {ex2}"
                    f"ORIGINAL ERROR: {e}"
                )
                self.stdout.write(self.style.ERROR(
                    f'Execution failed: {ex2} - ORIGINAL ERROR: {ex2}'
                ))

3.2.6 File jobs.py

Il file jobs.py viene creato per implementare job asincroni tramite l’applicazione thux.cron.

Un esempio di job potrebbe essere unp script di importazione:

class ImportTicketApp(JobHandler):
    def import_tables(self):
        idb = ImportDB()
        idb.start(**self.parameters)

    def do(self):
        start = timezone.now()
        try:
            self.import_tables()
            self.elaboration_results = json.dumps({"success": "Ticket APP successfully updated"})
            self.is_valid = True
        except Exception as e:
            self.elaboration_results = json.dumps({"error": str(e)})
            self.is_valid = False
        end = timezone.now()
        self.elaboration_time = end - start
        return self

Importante

  • solitamente i job chiamano dei classmethod o dei method di modelli evitando di duplicare il codice

  • vengono utilizzati i job per poter controllare l’esecuzione tramite interfaccia o per operazioni che richiedono del tempo e devono essere eseguite in maniera asincrona

Un altro esempio potrebbe essere quello per disabilitare i TicketProject scaduti:

class DisableTicketProjectExpiredJob(JobHandler):
    def do(self):
        start = timezone.now()
        try:
            disable_ticket_projects_expired(**self.parameters)
            self.elaboration_results = json.dumps(
            {"success": "Ticket Project expired successfully updated"}
            )
            self.is_valid = True
        except Exception as e:
            self.elaboration_results = json.dumps({"error": str(e)})
            self.is_valid = False
        end = timezone.now()
        self.elaboration_time = end - start
        return self

Un altro esempio potrebbe essere l’invio di email programmate:

class SendEmailErrorTransits(JobHandler):
    def do(self):
        start = timezone.now()
        try:
            msg = Employee.send_email_error_transits(**self.parameters)
            self.elaboration_results = json.dumps(msg)
            self.is_valid = True
        except Exception as e:
            self.elaboration_results = json.dumps({"error": str(e)})
            self.is_valid = False
        end = timezone.now()
        self.elaboration_time = end - start
        return self

Operazione che richiede del tempo per essere eseguita:

class GenerateDayTransitData(JobHandler):
    def do(self):
        start = timezone.now()
        try:
            user = User.objects.get(id=self.parameters.pop('user_id'))
            date_start = timezone.datetime.strptime(self.parameters.pop('date_start'), '%Y-%m-%d')
            date_end = timezone.datetime.strptime(self.parameters.pop('date_end'), '%Y-%m-%d')
            msg = Transit.set_employees_data(creator=user, date_start=date_start, date_end=date_end)
            self.elaboration_results = json.dumps(msg)
            self.is_valid = True
        except Exception as e:
            self.elaboration_results = json.dumps({"error": str(e)})
            self.is_valid = False
        end = timezone.now()
        self.elaboration_time = end - start
        return self

Operazione giornaliera per la generazione degli aggregati e delle statistiche:

class SetAdkAllData(JobHandler):
    def set_adk_all_data(self):
        user = get_user_model().objects.get(username=settings.STATISTIC_DATA_ADMIN_USER)
        AdkAllData.set_data(user)

    def do(self):
        start = timezone.now()
        try:
            self.set_adk_all_data()
            self.elaboration_results = json.dumps({"success": "Set Adk all data Job successfully end"})
            self.is_valid = True
        except Exception as e:
            self.elaboration_results = json.dumps({"error": str(e)})
            self.is_valid = False
        end = timezone.now()
        self.elaboration_time = end - start
        return self

4.1 tricks

4.1.1 Group by

4.1.1.2 Esempio Estratto Conto

Di seguito riporto un esempio per calcolare i saldi dei clienti moltiplicando di volta in volta l’importo «amount» per la «typology» (valore: -1 o 1).

class Balance(CleanModel, UserModel, DateModel, OrderedModel):
    ...
    customer = models.ForeignKey(
        Customer,
        related_name='balance_rows',
        on_delete=models.PROTECT,
        verbose_name=_('customer'),
    )
    area = models.ForeignKey(
        Area,
        related_name='balance_rows',
        on_delete=models.PROTECT,
        verbose_name = _('area'),
    )
    office = models.ForeignKey(
        Office,
        related_name='balance_rows',
        on_delete=models.PROTECT,
        verbose_name = _('office'),
    )
    date_value = models.DateField(verbose_name='value date')
    amount = models.DecimalField(
        max_digits=7,
        decimal_places=2,
        verbose_name=_('amount),
    )
    typology = models.IntegerField(
        choices=settings.ACCOUNTING_BALANCE_AMOUNT_CHOICES,
        verbose_name=_('typology'),
    )
    ...
from django.db.models import Sum, Count, F, DecimalField


Balance.objects.all().select_related(
    'customer', 'customer__area', 'customer__office'
).values(
    'customer', 'customer__name', 'customer__area__name', 'customer__office__name'
).annotate(
    balance=Sum(F('amount') * F('typology'), output_field=DecimalField()),
    last_date_value=Max('date_value')
).order_by('-last_date_value', 'customer__name')