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