================= THUX and DJANGO ================= 1. Django Project ================= Il nome del progetto è composto da: ``.``. 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. .. code-block:: python 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'}, ] .. important:: - *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' .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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 = [] .. important:: - 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: .. code-block:: python 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: .. code-block:: python '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``: .. code-block:: python '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: .. code-block:: python '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: .. code-block:: python '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. .. important:: Occorre definire il settaggio anche nel file **production.py** con valori impostati a ""? 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` .. code-block:: python # -*- 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: .. code-block:: python 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): .. important:: *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: .. code-block:: python 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: .. code-block:: python urlpatterns = router.urls router.register( path({{ Model }}), views.{{ Url }}ViewSet, ) urlpatterns = router.urls Nel file **views.py** sono presenti le classi ViewSet: .. code-block:: python 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) .. important:: - *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. .. important:: - per facilitare la ricerca e comprendere a quali applicazioni il settaggio si riferisce il nome dello segue questa convenzione **___** - 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: .. code-block:: python class Ticket(CleanModel, StatusModel, UserModel, DateModel, OrderedModel): areas = models.ManyToManyField( Area, blank=True, related_name='tickets', verbose_name=_('areas), ) .. important:: - 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** .. code-block:: python category = models.ForeignKey( Category, on_delete=models.PROTECT, related_name='tickets', blank=True, null=True, verbose_name=_('category'), ) .. important:: - 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 .. code-block:: python 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'), ) .. important:: - 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 .. code-block:: python 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'), ) .. important:: - l'attributo **choices** viene sempre definito tramite l'utilizzo di un **setting** - l'attributo **upload_to** viene sempre valorizzato con una **funzione** .. code-block:: python 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' .. important:: - 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 .. code-block:: python 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_() def clean(self): self.check_() self.check_() super().clean() def set_(self): self. = def check_(self): if : raise ValidationError({'': _('')}) def get_other_method(self): pass @property def (self): return .. important:: - sopra vengono riportati **in ordine** la classe meta e i metodi come da nostra convenzione - nel **clean_fields** vengono inseriti tutti i metodi **set_** 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_** e poi i **check_** che vengono usati sia dai **form** sia dal **save** .. code-block:: python class TicketProject(CleanModel, ProfileModel, UserModel, DateModel, StatusModel, OrderedModel): @classmethod def class_method(cls, ): pass .. important:: - 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: .. code-block:: python 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) .. important:: - **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**: .. code-block:: python 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 .. important:: - 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: .. code-block:: admin __init__.py actions.py admin.py forms.py inlines.py Oppure in presenza di molti modelli: .. code-block:: 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: .. code-block:: python # -*- 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**: .. code-block:: python 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: .. code-block:: python 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**: .. code-block:: python 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 .. important:: - 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**: .. code-block:: python 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**: .. code-block:: python 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: .. code-block:: python 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**: .. code-block:: python 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