##from __future__ import unicode_literals # (EP 21/9/22) To allow autoreferencing (ex: AgentCmd.create() returns a AgentCmd) from __future__ import annotations # Stdlib imports from src.device_controller.abstract_component.device_controller import DeviceCmd from datetime import datetime, timedelta, date from dateutil.relativedelta import relativedelta import os import sys from typing import Any, List, Tuple, Optional import re # Django imports from django.core.validators import MaxValueValidator, MinValueValidator # DJANGO imports from django.contrib.auth.models import AbstractUser, UserManager from django.db import models from django.db.models import Q, Max from django.core.validators import MaxValueValidator, MinValueValidator #from django.db.models.deletion import DO_NOTHING from django.db.models.expressions import F from django.db.models.query import QuerySet from model_utils import Choices from django.utils import timezone from asgiref.sync import async_to_sync from channels.layers import get_channel_layer from django.db.models.signals import post_save from django.dispatch import receiver # Project imports from user_mgmt.models import PyrosUser # DeviceCommand is used by class Command sys.path.append("../../..") """ STYLE RULES =========== https://simpleisbetterthancomplex.com/tips/2018/02/10/django-tip-22-designing-better-models.html https://steelkiwi.com/blog/best-practices-working-django-models-python/ - Model name => singular Call it Company instead of Companies. A model definition is the representation of a single object (the object in this example is a company), and not a collection of companies The model definition is a class, so always use CapWords convention (no underscores) E.g. User, Permission, ContentType, etc. - For the model’s attributes use snake_case. E.g. first_name, last_name, etc - Blank and Null Fields (https://simpleisbetterthancomplex.com/tips/2016/07/25/django-tip-8-blank-or-null.html) - Null: It is database-related. Defines if a given database column will accept null values or not. - Blank: It is validation-related. It will be used during forms validation, when calling form.is_valid(). Do not use null=True for text-based fields that are optional. Otherwise, you will end up having two possible values for “no data”, that is: None and an empty string. Having two possible values for “no data” is redundant. The Django convention is to use the empty string, not NULL. Example: # The default values of `null` and `blank` are `False`. class Person(models.Model): name = models.CharField(max_length=255) # Mandatory bio = models.TextField(max_length=500, blank=True) # Optional (don't put null=True) birth_date = models.DateField(null=True, blank=True) # Optional (here you may add null=True) The default values of null and blank are False. Special case, when you need to accept NULL values for a BooleanField, use NullBooleanField instead. - Choices : you can use Choices from the model_utils library. Take model Article, for instance: from model_utils import Choices class Article(models.Model): STATUSES = Choices( (0, 'draft', _('draft')), (1, 'published', _('published')) ) status = models.IntegerField(choices=STATUSES, default=STATUSES.draft) - Reverse Relationships - related_name : Rule of thumb: if you are not sure what would be the related_name, use the plural of the model holding the ForeignKey. ex: class Company: name = models.CharField(max_length=30) class Employee: first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='employees') usage: google = Company.objects.get(name='Google') google.employees.all() You can also use the reverse relationship to modify the company field on the Employee instances: vitor = Employee.objects.get(first_name='Vitor') google = Company.objects.get(name='Google') google.employees.add(vitor) - related_query_name : This kind of relationship also applies to query filters. For example, if I wanted to list all companies that employs people named ‘Vitor’, I could do the following: companies = Company.objects.filter(employee__first_name='Vitor') If you want to customize the name of this relationship, here is how we do it: class Employee: first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) company = models.ForeignKey( Company, on_delete=models.CASCADE, related_name='employees', related_query_name='person' ) Then the usage would be: companies = Company.objects.filter(person__first_name='Vitor') To use it consistently, related_name goes as plural and related_query_name goes as singular. GENERAL EXAMPLE ======= from django.db import models from django.urls import reverse class Company(models.Model): # CHOICES PUBLIC_LIMITED_COMPANY = 'PLC' PRIVATE_COMPANY_LIMITED = 'LTD' LIMITED_LIABILITY_PARTNERSHIP = 'LLP' COMPANY_TYPE_CHOICES = ( (PUBLIC_LIMITED_COMPANY, 'Public limited company'), (PRIVATE_COMPANY_LIMITED, 'Private company limited by shares'), (LIMITED_LIABILITY_PARTNERSHIP, 'Limited liability partnership'), ) # DATABASE FIELDS name = models.CharField('name', max_length=30) vat_identification_number = models.CharField('VAT', max_length=20) company_type = models.CharField('type', max_length=3, choices=COMPANY_TYPE_CHOICES) # MANAGERS objects = models.Manager() limited_companies = LimitedCompanyManager() # META CLASS class Meta: verbose_name = 'company' verbose_name_plural = 'companies' # TO STRING METHOD def __str__(self): return self.name # SAVE METHOD def save(self, *args, **kwargs): do_something() super().save(*args, **kwargs) # Call the "real" save() method. do_something_else() # ABSOLUTE URL METHOD def get_absolute_url(self): return reverse('company_details', kwargs={'pk': self.id}) # OTHER METHODS def process_invoices(self): do_something() """ # --- # --- Utility functions # --- def printd(*args, **kwargs): if os.environ.get('PYROS_DEBUG', '0') == '1': print('(MODEL)', *args, **kwargs) ''' def get_or_create_unique_row_from_model(model: models.Model): # return model.objects.get(id=1) if model.objects.exists() else model.objects.create(id=1) return model.objects.first() if model.objects.exists() else model.objects.create(id=1) ''' """ ------------------------ BASE MODEL CLASSES ------------------------ """ """ ------------------------ OTHER MODEL CLASSES ------------------------ """ # TODO: OLD Config : à virer (mais utilisé dans dashboard/templatetags/tags.py) class Config(models.Model): PYROS_STATE = ["Starting", "Passive", "Standby", "Remote", "Startup", "Scheduler", "Closing"] id = models.IntegerField(default='1', primary_key=True) #latitude = models.FloatField(default=1) latitude = models.DecimalField( max_digits=4, decimal_places=2, default=1, validators=[ MaxValueValidator(90), MinValueValidator(-90) ] ) local_time_zone = models.FloatField(default=1) #longitude = models.FloatField(default=1) longitude = models.DecimalField( max_digits=5, decimal_places=2, default=1, validators=[ MaxValueValidator(360), MinValueValidator(-360) ] ) altitude = models.FloatField(default=1) horizon_line = models.FloatField(default=1) row_data_save_frequency = models.IntegerField(default='300') request_frequency = models.IntegerField(default='300') analysed_data_save = models.IntegerField(default='300') telescope_ip_address = models.CharField(max_length=45, default="127.0.0.1") camera_ip_address = models.CharField(max_length=45, default="127.0.0.1") plc_ip_address = models.CharField(max_length=45, default="127.0.0.1") # TODO: changer ça, c'est pas clair du tout... # True = mode Scheduler-standby, False = mode Remote !!!! global_mode = models.BooleanField(default='True') ack = models.BooleanField(default='False') bypass = models.BooleanField(default='True') lock = models.BooleanField(default='False') pyros_state = models.CharField(max_length=25, default=PYROS_STATE[0]) force_passive_mode = models.BooleanField(default='False') plc_timeout_seconds = models.PositiveIntegerField(default=60) majordome_state = models.CharField(max_length=25, default="") ntc = models.BooleanField(default='False') majordome_restarted = models.BooleanField(default='False') class Meta: managed = True db_table = 'config' verbose_name_plural = "Config" def __str__(self): return (str(self.__dict__)) class Log(models.Model): agent = models.CharField(max_length=45, blank=True, null=True) created = models.DateTimeField(blank=True, null=True, auto_now_add=True) message = models.TextField(blank=True, null=True) class Meta: managed = True db_table = 'log' def __str__(self): return (str(self.agent)) # TODO: à virer car utilisé pour Celery (ou bien à utiliser pour les agents) ''' class TaskId(models.Model): task = models.CharField(max_length=45, blank=True, null=True) created = models.DateTimeField(blank=True, null=True, auto_now_add=True) task_id = models.CharField(max_length=45, blank=True, null=True) class Meta: managed = True db_table = 'task_id' def __str__(self): return (str(self.task) + " - " + str(self.task_id)) ''' class Version(models.Model): module_name = models.CharField(max_length=45, blank=True, null=True) version = models.CharField(max_length=15, blank=True, null=True) created = models.DateTimeField(blank=True, null=True, auto_now_add=True) updated = models.DateTimeField(blank=True, null=True, auto_now=True) class Meta: managed = True db_table = 'version' def __str__(self): return (str(self.module_name) + " - " + str(self.version)) class Tickets(models.Model): created = models.DateTimeField(blank=True, null=True, auto_now_add=True) updated = models.DateTimeField(blank=True, null=True, auto_now=True) end = models.DateTimeField(blank=True, null=True) title = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True) resolution = models.TextField(blank=True, null=True) pyros_user = models.ForeignKey(PyrosUser, on_delete=models.DO_NOTHING, related_name="tickets", blank=True, null=True) last_modified_by = models.ForeignKey(PyrosUser, on_delete=models.DO_NOTHING, related_name="tickets_modified_by", blank=True, null=True) LEVEL_ONE = "ONE" LEVEL_TWO = "TWO" LEVEL_THREE = "THREE" LEVEL_FOUR = "FOUR" LEVEL_FIVE = "FIVE" SECURITY_LEVEL_CHOICES = ( (LEVEL_ONE,"Warning non compromising for the operation of the system"), (LEVEL_TWO,"Known issue which can be solved by operating the software remotely"), (LEVEL_THREE,"Known issue which can be solved by an human remotely"), (LEVEL_FOUR,"Known issue without immediate solution"), (LEVEL_FIVE,"Issue not categorized until it happened") ) security_level = models.TextField(choices=SECURITY_LEVEL_CHOICES, default=LEVEL_ONE) class Meta: managed = True db_table = 'tickets' verbose_name_plural = "tickets"