# User related models # (EP 21/9/22) To allow autoreferencing (ex: AgentCmd.create() returns a AgentCmd) #from __future__ import annotations # USER - DJANGO general imports ''' from django.db import models from django.core.validators import MaxValueValidator, MinValueValidator from django.contrib.auth.models import AbstractUser, UserManager from django.db.models.query import QuerySet from django.db.models import Q, Max ''' # (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 enum import Enum #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.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 # Pyros import from scp_mgmt.models import Quota # Project imports #scp_mgmt.models import ScientificProgram, SP_Period, SP_Period_User #, ScienceTheme # ########### # # USER MODELS # # ########### # class UserLevel(models.Model): name = models.CharField(max_length=45, blank=True, null=True) desc = models.TextField(blank=True, null=True) priority = models.IntegerField(blank=True, null=True) class Meta: managed = True db_table = 'user_level' def __str__(self): return (str(self.name)) class Country(models.Model): name = models.CharField(max_length=45, blank=True, null=True) desc = models.TextField(blank=True, null=True) quota = models.FloatField(blank=True, null=True) class Meta: managed = True db_table = 'country' verbose_name_plural = "Countries" def __str__(self): return (str(self.name)) class Institute(models.Model): name = models.CharField(max_length=100, blank=False, null=False, unique=True) # fraction quota quota_f = models.FloatField( validators=[MinValueValidator(0), MaxValueValidator(1)], blank=True, null=True) quota = models.ForeignKey(Quota, on_delete=models.DO_NOTHING,related_name="institute_quotas", blank=True, null=True) #representative_user = models.ForeignKey("PyrosUser", on_delete=models.DO_NOTHING,related_name="institutes",default=1) def __str__(self) -> str: return str(self.name) class Meta: db_table = "institute" class ScienceTheme(models.Model): name = models.CharField(max_length=120, blank=False, null=False, default="", unique=True) def __str__(self) -> str: return str(self.name) class Meta: db_table = "science_theme" class PyrosUserManager(UserManager): def tac_users(self): return PyrosUser.objects.filter(user_level__name="TAC") def unit_users(self): return PyrosUser.objects.filter(Q(user_level__name="Unit-PI") | Q(user_level__name="Unit-board")) class PyrosUser(AbstractUser): username = models.CharField( max_length=255, blank=False, null=False, unique=True) is_active = models.BooleanField(default='False') first_time = models.BooleanField(default='False') country = models.ForeignKey( Country, on_delete=models.DO_NOTHING, related_name="pyros_users") user_level = models.ManyToManyField(UserLevel, related_name="pyros_users") desc = models.TextField(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) tel = models.CharField(max_length=45, blank=True, null=True) address = models.TextField(max_length=100, blank=True, null=True) laboratory = models.CharField(max_length=45, blank=True, null=True) institute = models.ForeignKey( Institute, on_delete=models.DO_NOTHING, related_name="pyros_users") is_institute_representative = models.BooleanField(default=False) motive_of_registration = models.TextField( max_length=300, blank=False, default="") # last_connect = models.DateTimeField(blank=True, null=True) # cur_connect = models.DateTimeField(blank=True, null=True) # putvalid_beg = models.DateTimeField(blank=True, null=True) # putvalid_end = models.DateTimeField(blank=True, null=True) # acqvalid_beg = models.CharField(max_length=45, blank=True, null=True) # acqvalid_end = models.CharField(max_length=45, blank=True, null=True) # quota = models.FloatField(blank=True, null=True) # quota_rea = models.FloatField(blank=True, null=True) # u_priority = models.IntegerField(blank=True, null=True) # p_priority = models.IntegerField(blank=True, null=True) # dir_level = models.IntegerField(blank=True, null=True) # can_del_void_req = models.BooleanField(default=False) validator = models.ForeignKey("PyrosUser", on_delete=models.DO_NOTHING, null=True, related_name="pyros_users") referee_themes = models.ManyToManyField(ScienceTheme, related_name="referee_themes", blank=True) #referee_themes = models.ManyToManyField('scientific_program.ScienceTheme', related_name="referee_themes", blank=True) #referee_themes = models.ManyToManyField('ScienceTheme', related_name="referee_themes", blank=True) objects = PyrosUserManager() class Meta: managed = True db_table = 'pyros_user' def __str__(self): return (str(self.get_username())) def user_username(self): return self.__str__() user_username.short_description = "Username" def get_priority(self) -> int: """ return maximum priority of all roles of the user Returns: int: maximum priority """ return PyrosUser.objects.get(id=self.id).user_level.all().aggregate(Max("priority"))["priority__max"] def get_roles_str(self) -> str: """ return string that represent all roles assigned to the user Returns: str: string of all roles assigned to user """ roles_str = "" # loop on all roles assigned to user for role in PyrosUser.objects.get(id=self.id).user_level.all(): roles_str += role.name + ", " return roles_str[:-2] def get_list_of_roles(self) -> list: """ return list of all roles assigned to the user Returns: list: list of all roles assigned to user """ return PyrosUser.objects.get(id=self.id).user_level.all().values_list("name", flat=True) # Note for the next functions : We take the first result of the query because it should be only one result with the filter applied since each UserLevel has a unique priority. def get_max_priority_desc(self) -> str: """ return the desc of the maximum priority of all roles assigned to user Returns: str: desc of the maximum priority of all roles assigned to user """ return PyrosUser.objects.get(id=self.id).user_level.filter(priority=self.get_priority())[0].desc def get_max_priority_quota(self) -> int: """ return the quota of the maximum priority of all roles assigned to user Returns: int: quota of the maximum priority of all roles assigned to user """ return PyrosUser.objects.get(id=self.id).user_level.filter(priority=self.get_priority())[0].quota def get_referee_themes_as_str(self) -> str: """ Return the list of science themes associated to that user if user is a TAC. Return empty string otherwise Returns: str: list of science themes associated to that user """ str = "" if self.referee_themes != None: for science_theme in PyrosUser.objects.get(id=self.id).referee_themes.all(): str += science_theme.name + ", " return str[:-2] else: return str def get_scientific_programs(self) -> QuerySet: sp_where_user_is_sp_pi = ScientificProgram.objects.filter( sp_pi=self.id) # Get all SP of user where he's not an SP PI user_other_sp = ScientificProgram.objects.filter(id__in=SP_Period.objects.filter(id__in=SP_Period_User.objects.filter( user=PyrosUser.objects.get(username=self.username)).values("SP_Period")).values_list("scientific_program", flat=True)) sp_of_user = sp_where_user_is_sp_pi | user_other_sp return sp_of_user def get_scientific_program_where_user_is_sp_pi(self) -> QuerySet: sp_where_user_is_sp_pi = ScientificProgram.objects.filter( sp_pi=self.id) return sp_where_user_is_sp_pi # ######################### # # SCIENTIFIC PROGRAM MODELS # # ######################### # ''' moved BEFORE PyrosUser which uses it class ScienceTheme(models.Model): name = models.CharField(max_length=120, blank=False, null=False, default="", unique=True) def __str__(self) -> str: return str(self.name) class Meta: db_table = "science_theme" ''' class PeriodManager(models.Manager): # to get the currently active period, use exploitation_period() # def currently_active_period(self) -> QuerySet: # today = timezone.now().date() # return Period.objects.get(start_date__lte=today, end_date__gt=today) def submission_periods(self) -> QuerySet: today = timezone.now().date() submission_periods_id = [] periods = Period.objects.filter(start_date__gte=today) for period in periods: if period.submission_start_date <= today and period.submission_end_date > today: submission_periods_id.append(period.id) periods = periods.filter(id__in=submission_periods_id) return periods def evaluation_periods(self) -> QuerySet: today = timezone.now().date() evaluation_periods_id = [] periods = Period.objects.filter(start_date__gte=today) for period in periods: if period.submission_end_date <= today and period.unit_pi_validation_start_date > today: evaluation_periods_id.append(period.id) periods = periods.filter(id__in=evaluation_periods_id) return periods def validation_periods(self) -> QuerySet: today = timezone.now().date() validation_periods_id = [] periods = Period.objects.filter(start_date__gte=today) for period in periods: if period.unit_pi_validation_start_date <= today and period.notification_start_date > today: validation_periods_id.append(period.id) periods = periods.filter(id__in=validation_periods_id) return periods def notification_periods(self) -> QuerySet: today = timezone.now().date() notification_periods_id = [] periods = Period.objects.filter(start_date__gte=today) for period in periods: if period.notification_start_date <= today and period.start_date > today: notification_periods_id.append(period.id) periods = periods.filter(id__in=notification_periods_id) return periods def exploitation_period(self) -> Any: today = timezone.now().date() periods = Period.objects.filter(start_date__lte=today) for period in periods: if period.start_date <= today and period.end_date > today: return period return None def latest_period(self) -> Any: today = timezone.now().date() future_period = Period.objects.filter( start_date__gt=today).order_by("-start_date").first() if future_period is None: # no future period defined so we return the current period if self.exploitation_period() != None: return self.exploitation_period() else: return self.previous_periods().first() return future_period def previous_periods(self) -> Any: today = timezone.now().date() previous_periods = Period.objects.filter(start_date__lt=today).order_by( "-start_date") if self.exploitation_period(): previous_periods.exclude(id=self.exploitation_period().id) return previous_periods def next_period(self) -> Any: current_period = self.exploitation_period() next_period = Period.objects.filter( start_date__gt=current_period.start_date).order_by("start_date").first() return next_period class Period(models.Model): # if change of default value, those values need to be changed also in create_period and edit_period.html (Javascript ) today = timezone.now().date() today_plus_six_months = today + relativedelta(months=+6) today_minus_six_months = today + relativedelta(months=-6) today_minus_one_month_and_half = today + \ relativedelta(months=-1) + relativedelta(days=-15) today_minus_fifteen_days = today + relativedelta(days=-15) end_date_plus_one_year = today_plus_six_months + relativedelta(years=+1) end_date_plus_eleven_year = today_plus_six_months + \ relativedelta(years=+11) start_date = models.DateField( blank=True, null=True, default=timezone.now, editable=True) exploitation_duration = models.PositiveIntegerField( blank=True, null=True, default=182, editable=True) submission_duration = models.PositiveIntegerField( blank=True, null=True, default=136, editable=True) evaluation_duration = models.PositiveIntegerField( blank=True, null=True, default=31, editable=True) validation_duration = models.PositiveIntegerField( blank=True, null=True, default=5, editable=True) notification_duration = models.PositiveIntegerField( blank=True, null=True, default=10, editable=True) property_of_data_duration = models.PositiveIntegerField( blank=True, null=True, default=365, editable=True) data_accessibility_duration = models.PositiveIntegerField( blank=True, null=True, default=365*10, editable=True) quota = models.ForeignKey(Quota, on_delete=models.DO_NOTHING,related_name="period_quotas", blank=True, null=True) @property def end_date(self): return self.start_date + relativedelta(days=self.exploitation_duration) @property def submission_start_date(self): return self.start_date + relativedelta(days=-(self.submission_duration+self.evaluation_duration+self.validation_duration+self.notification_duration)) @property def submission_end_date(self): return self.submission_start_date + relativedelta(days=self.submission_duration) @property def unit_pi_validation_start_date(self): return self.submission_end_date + relativedelta(days=self.evaluation_duration) @property def notification_start_date(self): return self.unit_pi_validation_start_date + relativedelta(days=self.validation_duration) @property def property_of_data_end_date(self): return self.end_date + relativedelta(days=self.property_of_data_duration) @property def data_accessibility_end_date(self): return self.end_date + relativedelta(days=self.data_accessibility_duration) """ end_date = models.DateField(blank=True, null=True, default=start_date_plus_six_months,editable=True) submission_start_date = models.DateField(blank=True, null=True, default=today_minus_six_months,editable=True) submission_end_date = models.DateField(blank=True, null=True, default=today_minus_one_month_and_half,editable=True) unit_pi_validation_start_date = models.DateField(blank=True, null=True, default=today_minus_fifteen_days,editable=True) property_of_data_end_date = models.DateField(blank=True, null=True, default=end_date_plus_one_year,editable=True) data_accessibility_end_date = models.DateField(blank=True, null=True, default=end_date_plus_eleven_year,editable=True) #class Meta: # constrains start_date and duration to be unique (We don't want to have multiple records in db that represent the same period) # constraints = [ # models.UniqueConstraint(fields=["start_date","duration"], name="unique_period") # ] """ objects = PeriodManager() def __str__(self) -> str: return "P"+str(self.id)+" ("+self.start_date.strftime("%d/%m/%Y") + " to " + self.end_date.strftime("%d/%m/%Y")+")" def is_currently_active(self) -> bool: today = timezone.now().date() return self.start_date <= today < self.end_date def can_submit_sequence(self) -> bool: today = timezone.now().date() return today >= self.notification_start_date def get_id_as_str(self) -> str: """ Return period_id as P+period_id Returns: str: P+period_id """ period_id = str(self.id) if len(period_id) < 3: while len(period_id) < 3: period_id = "0" + period_id period_id = "P" + period_id return period_id class Meta: db_table = "period" class ScientificProgramManager(models.Manager): def observable_programs(self): exploitable_sp = [] for sp_period in SP_Period.objects.all(): if sp_period.can_submit_sequence(): exploitable_sp.append(sp_period.scientific_program.id) return ScientificProgram.objects.filter(id__in=exploitable_sp) class ScientificProgram(models.Model): created = models.DateTimeField(blank=True, null=True, auto_now_add=True) updated = models.DateTimeField(blank=True, null=True, auto_now=True) name = models.CharField(max_length=30, blank=False, null=False, default="", unique=True) description_short = models.TextField(default="", max_length=320) description_long = models.TextField(default="") institute = models.ForeignKey('Institute', on_delete=models.DO_NOTHING, related_name="scientific_programs") sp_pi = models.ForeignKey('PyrosUser', on_delete=models.DO_NOTHING, related_name="scientific_Program_Users") science_theme = models.ForeignKey(ScienceTheme, on_delete=models.DO_NOTHING, related_name="scientific_program_theme", default=1) is_auto_validated = models.BooleanField(default=False) objects = ScientificProgramManager() quota = models.ForeignKey(Quota, on_delete=models.DO_NOTHING, related_name="scientific_program_quotas", blank=True, null=True) quota_f = models.FloatField( validators=[MinValueValidator(0), MaxValueValidator(1)], blank=True, null=True) class Meta: managed = True db_table = 'scientific_program' def __str__(self): return (str(self.name)) class SP_Period(models.Model): STATUSES_DRAFT = "Draft" STATUSES_SUBMITTED = "Submitted" STATUSES_EVALUATED = "Evaluated" STATUSES_ACCEPTED = "Accepted" STATUSES_REJECTED = "Rejected" STATUSES = [ (STATUSES_DRAFT, "Draft"), (STATUSES_SUBMITTED, "Submitted"), (STATUSES_EVALUATED, "Evaluated"), (STATUSES_ACCEPTED, "Accepted"), (STATUSES_REJECTED, "Rejected") ] VOTES_YES = "A: Accepted" VOTES_NO = "C: Refused" VOTES_TO_DISCUSS = "B: To be discussed" VOTES = [ (VOTES_YES, "A: Accepted"), (VOTES_TO_DISCUSS, "B: To be discussed"), (VOTES_NO, "C: Refused") ] IS_VALID_ACCEPTED = "Accepted" IS_VALID_REJECTED = "Rejected" IS_VALID = [ (IS_VALID_ACCEPTED, "Accepted"), (IS_VALID_REJECTED, "Rejected") ] VISIBILITY_YES = "Yes" VISIBILITY_NO = "No" VISIBILITY_CHOICES = [ (VISIBILITY_YES, "Yes"), (VISIBILITY_NO, "No"), ] period = models.ForeignKey( Period, on_delete=models.DO_NOTHING, related_name="SP_Periods") scientific_program = models.ForeignKey( ScientificProgram, on_delete=models.DO_NOTHING, related_name="SP_Periods") public_visibility = models.TextField( choices=VISIBILITY_CHOICES, default=VISIBILITY_YES) referee1 = models.ForeignKey('PyrosUser', on_delete=models.DO_NOTHING, related_name="TAC1_judgement", null=True) vote_referee1 = models.TextField( choices=VOTES, default=VOTES_YES, blank=True) reason_referee1 = models.TextField(blank=True) referee2 = models.ForeignKey('PyrosUser', on_delete=models.DO_NOTHING, related_name="TAC2_judgement", null=True) vote_referee2 = models.TextField( choices=VOTES, default=VOTES_YES, blank=True) reason_referee2 = models.TextField(blank=True) is_valid = models.TextField( choices=IS_VALID, default=IS_VALID_REJECTED, blank=True) status = models.TextField(choices=STATUSES, default=STATUSES_DRAFT) token = models.PositiveIntegerField(default=0) token_allocated = models.PositiveIntegerField(default=0, blank=True) token_remaining = models.PositiveIntegerField(default=0, blank=True) # Unit PI donne un nombre de priorité (100 = alert) priority = models.IntegerField( validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) def is_currently_active(self): return self.period.is_currently_active() def can_submit_sequence(self) -> bool: return self.is_currently_active() or self.period.can_submit_sequence() and self.status == self.STATUSES_ACCEPTED class Meta: db_table = "sp_period" class SP_Period_User(models.Model): SP_Period = models.ForeignKey(SP_Period, on_delete=models.DO_NOTHING, related_name="SP_Period_Users") user = models.ForeignKey("PyrosUser", on_delete=models.CASCADE, related_name="SP_Period_Users") #is_SP_PI = models.BooleanField(default=False) class Meta: unique_together = ('SP_Period', 'user') db_table = "sp_period_user" class SP_Period_Guest(models.Model): SP_Period = models.ForeignKey( SP_Period, on_delete=models.DO_NOTHING, related_name="SP_Period_Guests") email = models.EmailField(max_length=254) class Meta: db_table = "sp_period_guest" class SP_PeriodWorkflow(models.Model): SUBMISSION = "SUB" EVALUATION = "EVAL" VALIDATION = "VALI" NOTIFICATION = "NOTI" ACTIONS_CHOICES = ( (SUBMISSION, "Submission"), (EVALUATION, "Evaluation"), (VALIDATION, "Validation"), (NOTIFICATION, "Notification") ) action = models.CharField(max_length=120, choices=ACTIONS_CHOICES) period = models.ForeignKey( "Period", on_delete=models.DO_NOTHING, related_name="SP_Period_Workflows") created = models.DateTimeField(blank=True, null=True, auto_now_add=True) class Meta: db_table = "sp_period_workflow"