Compare View

switch
from
...
to
 
Commits (10)
config/pyros_observatory/general/schemas/schema_device-2.0.yml
... ... @@ -123,9 +123,9 @@ schema;schema_device:
123 123 required: False
124 124 mapping:
125 125 voltage:
126   - type: int
  126 + type: float
127 127 intensity:
128   - type: number
  128 + type: float
129 129 socket:
130 130 type: str
131 131 hostname:
... ...
config/pyros_observatory/general/schemas/schema_observatory-2.0.yml
... ... @@ -317,6 +317,20 @@ schema;schema_FN_CONTEXTS:
317 317 type: str
318 318 pathnaming:
319 319 type: str
  320 + pyros_seq_tmp:
  321 + type: map
  322 + required: False
  323 + mapping:
  324 + root_dir:
  325 + type: str
  326 + description:
  327 + type: str
  328 + extension:
  329 + type: str
  330 + naming:
  331 + type: str
  332 + pathnaming:
  333 + type: str
320 334 pyros_eph:
321 335 type: map
322 336 required: False
... ...
src/core/pyros_django/dashboard/templates/dashboard/observation_index.html
... ... @@ -102,6 +102,24 @@
102 102 </a>
103 103 </div>
104 104 </li>
  105 + <li>
  106 + {% comment %}
  107 + Old version to be remastered :
  108 + <a href="{% url "proposal" %}">
  109 + <div class="all-info">
  110 +
  111 + <h3>Proposal</h3>
  112 + {% load static %} <img src="{% static "media/proposal.png" %}" alt="html5" height="180" width="180" />
  113 + </div></a>
  114 + {% endcomment %}
  115 + <div class="all-info">
  116 + <a href="{% url "quota_sp" %}">
  117 +
  118 + <h3>Quota Scientific programs</h3>
  119 + {% load static %} <img src="{% static "media/proposal.png" %}" alt="html5" />
  120 + </a>
  121 + </div>
  122 + </li>
105 123 {% endif %}
106 124 {# TBD #}
107 125 {% if USER_LEVEL|ifinlist:"Admin,Operator,Unit-PI,Management board member,Observer,TAC" %}
... ...
src/core/pyros_django/misc/fixtures/initial_fixture_dev_TZ.json
... ... @@ -204,6 +204,49 @@
204 204 "description_long": "",
205 205 "sp_pi": 2,
206 206 "science_theme": 1,
  207 + "quota_f":0.9,
  208 + "is_auto_validated": true
  209 + }
  210 + },
  211 + {
  212 + "model": "user_mgmt.scientificprogram",
  213 + "pk": 3,
  214 + "fields": {
  215 + "name": "debris-test",
  216 + "institute": 1,
  217 + "description_short": "",
  218 + "description_long": "",
  219 + "sp_pi": 2,
  220 + "science_theme": 1,
  221 + "quota_f":0.1,
  222 + "is_auto_validated": true
  223 + }
  224 + },
  225 + {
  226 + "model": "user_mgmt.scientificprogram",
  227 + "pk": 2,
  228 + "fields": {
  229 + "name": "irap",
  230 + "institute": 2,
  231 + "description_short": "",
  232 + "description_long": "",
  233 + "sp_pi": 5,
  234 + "science_theme": 1,
  235 + "quota_f": "0.7",
  236 + "is_auto_validated": true
  237 + }
  238 + },
  239 + {
  240 + "model": "user_mgmt.scientificprogram",
  241 + "pk": 4,
  242 + "fields": {
  243 + "name": "irap-test",
  244 + "institute": 2,
  245 + "description_short": "",
  246 + "description_long": "",
  247 + "sp_pi": 5,
  248 + "science_theme": 1,
  249 + "quota_f": "0.3",
207 250 "is_auto_validated": true
208 251 }
209 252 },
... ... @@ -219,6 +262,39 @@
219 262 }
220 263 },
221 264 {
  265 + "model": "user_mgmt.SP_Period",
  266 + "pk": 3,
  267 + "fields": {
  268 + "period": 1,
  269 + "scientific_program": 3,
  270 + "status": "Accepted",
  271 + "public_visibility": "Name",
  272 + "is_valid": "Accepted"
  273 + }
  274 + },
  275 + {
  276 + "model": "user_mgmt.SP_Period",
  277 + "pk": 4,
  278 + "fields": {
  279 + "period": 1,
  280 + "scientific_program": 4,
  281 + "status": "Accepted",
  282 + "public_visibility": "Name",
  283 + "is_valid": "Accepted"
  284 + }
  285 + },
  286 + {
  287 + "model": "user_mgmt.SP_Period",
  288 + "pk": 2,
  289 + "fields": {
  290 + "period": 1,
  291 + "scientific_program": 2,
  292 + "status": "Accepted",
  293 + "public_visibility": "Name",
  294 + "is_valid": "Accepted"
  295 + }
  296 + },
  297 + {
222 298 "model": "user_mgmt.SP_Period_User",
223 299 "pk": 2,
224 300 "fields": {
... ... @@ -243,6 +319,78 @@
243 319 }
244 320 },
245 321 {
  322 + "model": "user_mgmt.SP_Period_User",
  323 + "pk": 5,
  324 + "fields": {
  325 + "SP_Period": 2,
  326 + "user": 5
  327 + }
  328 + },
  329 + {
  330 + "model": "user_mgmt.SP_Period_User",
  331 + "pk": 6,
  332 + "fields": {
  333 + "SP_Period": 2,
  334 + "user": 8
  335 + }
  336 + },
  337 + {
  338 + "model": "user_mgmt.SP_Period_User",
  339 + "pk": 6,
  340 + "fields": {
  341 + "SP_Period": 2,
  342 + "user": 7
  343 + }
  344 + },
  345 + {
  346 + "model": "user_mgmt.SP_Period_User",
  347 + "pk": 7,
  348 + "fields": {
  349 + "SP_Period": 4,
  350 + "user": 5
  351 + }
  352 + },
  353 + {
  354 + "model": "user_mgmt.SP_Period_User",
  355 + "pk": 8,
  356 + "fields": {
  357 + "SP_Period": 4,
  358 + "user": 8
  359 + }
  360 + },
  361 + {
  362 + "model": "user_mgmt.SP_Period_User",
  363 + "pk": 9,
  364 + "fields": {
  365 + "SP_Period": 4,
  366 + "user": 7
  367 + }
  368 + },
  369 + {
  370 + "model": "user_mgmt.SP_Period_User",
  371 + "pk": 10,
  372 + "fields": {
  373 + "SP_Period": 3,
  374 + "user": 3
  375 + }
  376 + },
  377 + {
  378 + "model": "user_mgmt.SP_Period_User",
  379 + "pk": 11,
  380 + "fields": {
  381 + "SP_Period": 3,
  382 + "user": 4
  383 + }
  384 + },
  385 + {
  386 + "model": "user_mgmt.SP_Period_User",
  387 + "pk": 12,
  388 + "fields": {
  389 + "SP_Period": 3,
  390 + "user": 17
  391 + }
  392 + },
  393 + {
246 394 "model": "devices.telescope",
247 395 "pk": 1,
248 396 "fields": {
... ...
src/core/pyros_django/scheduling/A_Scheduler.py
... ... @@ -5,6 +5,9 @@
5 5 # Linux console:
6 6 # cd /srv/develop/pyros/docker
7 7 # ./PYROS_DOCKER_START.sh
  8 +# ./PYROS_RUN_WEBSERVER_ONLY -o tnc -fg
  9 +# cd ..
  10 +# ./PYROS start -o tnc -fg -a A_Scheduler
8 11 #
9 12 # Launch from Power Shell:
10 13 # To go from docker to Powershell: pyros_user@ORION:~/app$ exit (or Ctrl+d)
... ... @@ -29,6 +32,7 @@ import argparse
29 32 import os
30 33 import pickle
31 34 import socket
  35 +import yaml
32 36  
33 37 pwd = os.environ['PROJECT_ROOT_PATH']
34 38 if pwd not in sys.path:
... ... @@ -44,6 +48,7 @@ for short_path in short_paths:
44 48 from majordome.agent.Agent import Agent, build_agent, log, parse_args
45 49 from seq_submit.models import Sequence
46 50 from user_mgmt.models import Period, ScientificProgram, SP_Period
  51 +from scp_mgmt.models import Quota
47 52 from scheduling.models import PredictiveSchedule, EffectiveSchedule
48 53 # = Specials
49 54 import glob
... ... @@ -54,6 +59,8 @@ from decimal import Decimal
54 59 import zoneinfo
55 60 import numpy as np
56 61  
  62 +from pyros_api import PyrosAPI
  63 +
57 64 class A_Scheduler(Agent):
58 65  
59 66 DPRINT = True
... ... @@ -223,10 +230,12 @@ class A_Scheduler(Agent):
223 230 """
224 231  
225 232  
226   - def update_db_quota_sequence(sequence, quota_attributes, id_period, night_id, d_total=sequence_info['duration']):
227   - sequence_quota = sequence.quota
228   - sp_quota = sequence.scientific_program
229   - institute_quota =
  233 + def update_db_quota_sequence(self, sequence_id, quota_attributes):
  234 + sequence = Sequence.objects.get(id=sequence_id)
  235 + new_quota = Quota()
  236 + new_quota.set_attributes_and_save(quota_attributes)
  237 + sequence.quota = new_quota
  238 + sequence.save()
230 239  
231 240 def _compute_schedule_1(self):
232 241 """Simple scheduler based on selection-insertion one state algorithm.
... ... @@ -247,13 +256,14 @@ class A_Scheduler(Agent):
247 256 # --- Get the night
248 257 night = info['night']
249 258 # --- Get ephemeris informations of the night and initialize quotas
250   - night_info = self.update_sun_moon_ephems()
251   - quota_total_period = night_info['total'][1]
252   - quota_total_night_start = night_info[night][0]
253   - quota_total_night_end = night_info[night][1]
254   - self.dprint(f"{quota_total_period=}")
255   - self.dprint(f"{quota_total_night_start=}")
256   - self.dprint(f"{quota_total_night_end=}")
  259 + self.update_sun_moon_ephems()
  260 +
  261 + # Get quota for night
  262 +
  263 + oquota = Quota.objects.get(id_period=info["operiod"].id, night_id=info["night"])
  264 +
  265 +
  266 +
257 267 # --- Build the wildcard to list the sequences
258 268 wildcard = os.path.join(rootdir, subdir, "*.p")
259 269 self.dprint(f"{wildcard=}")
... ... @@ -327,6 +337,7 @@ class A_Scheduler(Agent):
327 337 eph_info = pickle.load(open(ephfile,"rb"))
328 338 #print("="*20 + "\n" + f"{eph_info=}")
329 339 # ---
  340 + self._fn.fcontext = "pyros_seq"
330 341 param = self._fn.naming_get(seqfile)
331 342 sequence_info['id'] = int(param['id_seq'])
332 343 # --- scientific_program_id is an integer
... ... @@ -349,11 +360,18 @@ class A_Scheduler(Agent):
349 360 sequence_info['visibility_duration'] = visibility_duration # total slots - duration
350 361 sequence_info['duration'] = seq_info['sequence']['duration']
351 362 sequence_info['scientific_program_id'] = scientific_program_id
  363 + sequence_info["period"] = seq_info["sequence"]["period"]
  364 + sequence_info["night_id"] = seq_info["sequence"]["night_id"]
352 365 self.dprint(f" {scientific_program_id=} range to start={len(kobss)}")
353 366 if scientific_program_id not in scientific_program_ids:
354 367 scientific_program_ids.append(scientific_program_id)
355 368 # --- TODO
356   - # update_db_quota_sequence( id_period, night_id, d_total=sequence_info['duration'] )
  369 + quota_attributes = {}
  370 + quota_attributes["d_total"] = int(np.ceil(sequence_info["duration"]))
  371 + quota_attributes["d_schedule"] = int(np.ceil(sequence_info["duration"]))
  372 + quota_attributes["night_id"] = sequence_info["night_id"]
  373 + quota_attributes["id_period"] = sequence_info["period"]
  374 + self.update_db_quota_sequence(seq_info["sequence"]["id"], quota_attributes)
357 375 else:
358 376 sequence_info['error'] = f"File {ephfile} not exists"
359 377 sequence_infos.append(sequence_info)
... ... @@ -551,9 +569,12 @@ class A_Scheduler(Agent):
551 569 log.info(f"_compute_schedule_1 finished in {time.time() - t0:.2f} seconds")
552 570  
553 571 def _create_seq_1(self, nb_seq: int):
  572 + # delete all previous test seq
  573 + Sequence.objects.filter(id__gte=9990000000).delete()
554 574 t0 = time.time()
555 575 self.dprint("Debut _create_seq_1")
556   - seq_template = {'sequence': {'id': 4, 'start_expo_pref': 'IMMEDIATE', 'pyros_user': 2, 'scientific_program': 1, 'name': 'seq_20230628T102140', 'desc': None, 'last_modified_by': 2, 'is_alert': False, 'status': 'TBP', 'with_drift': False, 'priority': None, 'analysis_method': None, 'moon_min': None, 'alt_min': None, 'type': None, 'img_current': None, 'img_total': None, 'not_obs': False, 'obsolete': False, 'processing': False, 'flag': None, 'period': 1, 'start_date': datetime.datetime(2023, 6, 28, 10, 21, 40, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'end_date': datetime.datetime(2023, 6, 28, 10, 21, 40, 999640, tzinfo=datetime.timezone.utc), 'jd1': Decimal('0E-8'), 'jd2': Decimal('0E-8'), 'tolerance_before': '1s', 'tolerance_after': '1min', 'duration': -1.0, 'overhead': Decimal('0E-8'), 'submitted': False, 'config_attributes': {'tolerance_before': '1s', 'tolerance_after': '1min', 'target': 'RADEC 0H10M -15D', 'conformation': 'WIDE', 'layout': 'Altogether'}, 'ra': None, 'dec': None, 'complete': True, 'night_id': '20230627'}, 'albums': {'Altogether': {'plans': [{'id': 4, 'album': 4, 'duration': 0.0, 'nb_fnges': 1, 'config_attributes': {'binnings': {'binxy': [1, 1], 'readouttime': 6}, 'exposuretime': 1.0}, 'complete': True}]}}}
  576 + seq_template = yaml.safe_load(open("scheduler_seq_template.yml","r"))
  577 + #{"simplified":True, 'sequence': {'id': 4, 'start_expo_pref': 'IMMEDIATE', 'pyros_user': 2, 'scientific_program': 1, 'name': 'seq_20230628T102140', 'desc': None, 'last_modified_by': 2, 'is_alert': False, 'status': 'TBP', 'with_drift': False, 'priority': None, 'analysis_method': None, 'moon_min': None, 'alt_min': None, 'type': None, 'img_current': None, 'img_total': None, 'not_obs': False, 'obsolete': False, 'processing': False, 'flag': None, 'period': 1, 'start_date': datetime.datetime(2023, 6, 28, 10, 21, 40, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'end_date': datetime.datetime(2023, 6, 28, 10, 21, 40, 999640, tzinfo=datetime.timezone.utc), 'tolerance_before': '1s', 'tolerance_after': '1min', 'duration': -1.0, 'submitted': False, 'config_attributes': {'tolerance_before': '1s', 'tolerance_after': '1min', 'target': 'RADEC 0H10M -15D', 'conformation': 'WIDE', 'layout': 'Altogether'}, 'ra': None, 'dec': None, 'complete': True, 'night_id': '20230627'}, 'albums': {'Altogether': {'plans': [{'id': 4, 'album': 4, 'duration': 0.0, 'nb_fnges': 1, 'config_attributes': {'binnings': {'binxy': [1, 1], 'readouttime': 6}, 'exposuretime': 1.0}, 'complete': True}]}}}
557 578 # decode general variables info a dict info
558 579 info = self.get_infos()
559 580 rootdir = info['rootdir']
... ... @@ -598,35 +619,54 @@ class A_Scheduler(Agent):
598 619 # --- Create new sequences
599 620 for k in range(nb_seq):
600 621 #print("B"*20 + f" {info['operiod'].id} {info['night']} {k}")
  622 + if k < nb_seq/2:
  623 + scientific_program = 0
  624 + else:
  625 + scientific_program = 1
  626 +
601 627 time.sleep(1)
602 628 seq = seq_template.copy()
603   - seq['sequence']['period'] = info['operiod'].id # int
604   - seq['sequence']['night_id'] = info['night'] # str
605   - seq['sequence']['config_attributes']['target'] = k # int
  629 + print(f"{seq}")
  630 + #seq['sequence']['config_attributes']['target'] = k # int
606 631 # ---
607   - start_expo_pref = "BESTELEV" #"IMMEDIATE"
608   - scientific_program = int(k/2)
609   - start_date = datetime.datetime(2023, 6, 28, 10, 21, 40)
610   - end_date = datetime.datetime(2023, 6, 28, 10, 21, 40, 999640, tzinfo=datetime.timezone.utc)
  632 + #start_expo_pref = "BESTELEV" #"IMMEDIATE"
  633 + start_expo_pref = 0 # for bestelev 1, for immediate 0
  634 + start_date,_ = self._fn.night2date(info["night"])
  635 + start_date = start_date + 0.25 + (0.5*k)
  636 + start_date_to_datetime = guitastro.Date(start_date).iso()
  637 + #start_date = datetime.datetime(2023, 6, 28, 10, 21, 40)
  638 + #end_date = datetime.datetime(2023, 6, 28, 10, 21, 40, 999640, tzinfo=datetime.timezone.utc)
611 639 jd1 = Decimal('0E-8')
612 640 jd2 = Decimal('0E-8')
613 641 tolerance_before = '1s'
614   - tolerance_after = '1min'
  642 + tolerance_after = '5min'
615 643 duration = 3000.0
616 644 target = f"RADEC {k}h {10+2*k}d"
617 645 # ---
618 646 seq['sequence']['start_expo_pref'] = start_expo_pref
619 647 seq['sequence']['scientific_program'] = scientific_program
620   - seq['sequence']['start_date'] = start_date
621   - seq['sequence']['end_date'] = end_date
622   - seq['sequence']['jd1'] = jd1
623   - seq['sequence']['jd2'] = jd2
  648 + seq['sequence']['start_date'] = start_date_to_datetime
  649 + # seq['sequence']['jd1'] = jd1
  650 + # seq['sequence']['jd2'] = jd2
624 651 seq['sequence']['tolerance_before'] = tolerance_before
625 652 seq['sequence']['tolerance_after'] = tolerance_after
626   - seq['sequence']['duration'] = duration
627   - seq['sequence']['config_attributes']['target'] = target
  653 + seq['sequence']['target'] = target
628 654 # --- Build the path and file name of the sequence file
629   - fn_param["id_seq"] = int("999" + f"{k:07d}")
  655 + seq["sequence"]["id"] = int("999" + f"{k:07d}")
  656 + seq["sequence"]["name"] = "seq_"+str(seq["sequence"]["id"])
  657 + # Add sequence to db
  658 + # with pyros_api script
  659 + pyros_api = PyrosAPI(None)
  660 + self._fn.fcontext = "pyros_seq_tmp"
  661 + self._fn.extension = ".json"
  662 + seq_fname = self._fn.join(str(seq["sequence"]["id"]))
  663 + seq_file = open(seq_fname,"w")
  664 + seq_file.write(yaml.dump(seq, sort_keys=False))
  665 + seq_file.close()
  666 + response = pyros_api.submit_sequence_file(seq_fname)
  667 + log.info(f"{response}")
  668 +
  669 + """
630 670 self.dprint(f"{k} : {self._fn.fcontext=}")
631 671 self._fn.fname = self._fn.naming_set(fn_param)
632 672 self.dprint(f"{k} : {self._fn.fname=}")
... ... @@ -653,6 +693,7 @@ class A_Scheduler(Agent):
653 693 pickle.dump(seq, open(seq_file,"wb"))
654 694 #dprint(f"{errors=}")
655 695 #dprint("C"*20)
  696 + """
656 697 log.info(f"_create_seq_1 finished in {time.time() - t0:.2f} seconds")
657 698  
658 699 def load_sequence(self):
... ...
src/core/pyros_django/scheduling/scheduler_seq_template.yml 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +simplified: True
  2 +sequence:
  3 + scientific_program: 0
  4 + name: seq_20231019T153204
  5 + start_date: '2023-10-19T15:32:04.000000'
  6 + tolerance_before: 1s
  7 + tolerance_after: 1min
  8 + start_expo_pref: 0
  9 + target: RADEC 0H10M -15D
  10 + conformation: 0
  11 + layout: 0
  12 + ALBUMS:
  13 + - Album:
  14 + name: Altogether
  15 + Plans:
  16 + - Plan:
  17 + nb_images: 100
  18 + exposuretime: 10
  19 + binnings: 0
... ...
src/core/pyros_django/scp_mgmt/A_SCP_Manager.py
... ... @@ -19,6 +19,7 @@ for short_path in short_paths:
19 19 from majordome.agent.Agent import Agent, build_agent
20 20 from user_mgmt.models import PyrosUser, Institute, SP_Period, Period, SP_Period, SP_Period_Guest, SP_PeriodWorkflow
21 21 import vendor.guitastro.src.guitastro as guitastro
  22 +from scp_mgmt.models import Quota
22 23  
23 24 # Django imports
24 25 from django.shortcuts import reverse
... ... @@ -47,6 +48,9 @@ class A_SCP_Manager(Agent):
47 48 # Format : “cmd_name” : (timeout, exec_mode, tooltip)
48 49  
49 50 "do_generate_ephem_moon_and_sun_for_period": (3, Agent.EXEC_MODE.SEQUENTIAL, 'generate ephem of moon & sun for a period'),
  51 + "do_set_quota_for_SP": (60, Agent.EXEC_MODE.SEQUENTIAL, 'set quota for scientific programs for id period'),
  52 + "do_set_quota_for_institutes": (60, Agent.EXEC_MODE.SEQUENTIAL, 'set quota for institutes for id period'),
  53 + "do_run_quota_workflow": (60, Agent.EXEC_MODE.SEQUENTIAL, 'set quota for a period'),
50 54 }
51 55  
52 56 # new init with obsconfig
... ... @@ -229,8 +233,8 @@ class A_SCP_Manager(Agent):
229 233 self.send_mail_to_observers_for_notification(next_sp_to_be_notified)
230 234 SP_PeriodWorkflow.objects.create(period=self.period,action=SP_PeriodWorkflow.NOTIFICATION)
231 235 self.update_sun_moon_ephems()
232   - self.set_quota_for_institutes(self.period.id)
233   - self.set_quota_for_sp(self.period.id)
  236 + self.do_set_quota_for_institutes(self.period.id)
  237 + self.do_set_quota_for_SP(self.period.id)
234 238  
235 239 def routine_process_body(self):
236 240 print("routine automatic period workflow")
... ... @@ -245,27 +249,49 @@ class A_SCP_Manager(Agent):
245 249 for n in range(int((end_date - start_date).days)):
246 250 yield start_date + timedelta(n)
247 251  
248   - def set_quota_for_institutes(self, id_period):
249   - for institute in Institute.objects.all():
250   - quota_f = institute.quota_f
251   - # the lowest id of quota table for this period should be the first night of the period
252   - period_quota = Period.objects.get(id=id_period).quota
253   - institute_quota = period_quota.convert_to_quota(quota_f)
254   - new_quota = Quota()
255   - new_quota.set_attributes_and_save(institute_quota)
256   - institute.quota = new_quota
257   - institute.save()
  252 + def do_run_quota_workflow(self, id_period:int):
  253 + try:
  254 + self.update_sun_moon_ephems()
  255 + except Exception as e:
  256 + print(e)
  257 + self.do_set_quota_for_institutes(id_period)
  258 + self.do_set_quota_for_SP(id_period)
  259 +
  260 + def do_set_quota_for_institutes(self, id_period:int):
  261 + try:
  262 + for institute in Institute.objects.all():
  263 + quota_f = institute.quota_f
  264 + # the lowest id of quota table for this period should be the first night of the period
  265 + period_quota = Period.objects.get(id=id_period).quota
  266 + institute_quota = period_quota.convert_to_quota(quota_f)
  267 + institute_quota["night_id"] = 0
  268 + institute_quota["id_period"] = id_period
  269 + new_quota = Quota()
  270 + new_quota.set_attributes_and_save(institute_quota)
  271 + institute.quota = new_quota
  272 + institute.save()
  273 + except Exception as e:
  274 + print(e)
  275 + raise e
258 276  
259   - def set_quota_for_SP(self, id_period):
260   - period = Period.objects.get(id=id_period)
261   - for sp_period in SP_Period.objects.filter(period=period):
262   - sp = sp_period.scientific_program
263   - institute = sp.institute
264   - institute_quota = institute.quota
265   - new_quota = Quota()
266   - quota_attributes = institute_quota.convert_to_quota(sp.quota_f)
267   - new_quota.set_attributes_and_save(quota_attributes)
268   -
  277 + def do_set_quota_for_SP(self, id_period:int):
  278 + try:
  279 + period = Period.objects.get(id=id_period)
  280 + for sp_period in SP_Period.objects.filter(period=period):
  281 + sp = sp_period.scientific_program
  282 + institute = sp.institute
  283 + institute_quota = institute.quota
  284 + new_quota = Quota()
  285 + quota_attributes = institute_quota.convert_to_quota(sp.quota_f)
  286 + quota_attributes["night_id"] = 1
  287 + quota_attributes["id_period"] = id_period
  288 + new_quota.set_attributes_and_save(quota_attributes)
  289 + sp.quota = new_quota
  290 + sp.save()
  291 +
  292 + except Exception as e:
  293 + print(e)
  294 + raise e
269 295  
270 296 if __name__ == "__main__":
271 297  
... ...
src/core/pyros_django/scp_mgmt/models.py
... ... @@ -4,90 +4,114 @@ from django.db import models
4 4 class Quota(models.Model):
5 5 id_period = models.BigIntegerField(blank=True, null=True)
6 6 night_id = models.CharField(max_length=8, null=True, blank=True, db_index=True)
  7 + d_total = models.BigIntegerField(default=0, blank=True, null=True)
7 8 d_totalq = models.BigIntegerField(default=0, blank=True, null=True)
8 9 d_totalx = models.BigIntegerField(default=0, blank=True, null=True)
9 10  
  11 + d_previous = models.BigIntegerField(default=0, blank=True, null=True)
10 12 d_previousq = models.BigIntegerField(default=0, blank=True, null=True)
11 13 d_previousx = models.BigIntegerField(default=0, blank=True, null=True)
12 14  
  15 + d_current = models.BigIntegerField(default=0, blank=True, null=True)
13 16 d_currentq = models.BigIntegerField(default=0, blank=True, null=True)
14 17 d_currentx = models.BigIntegerField(default=0, blank=True, null=True)
15 18  
  19 + d_passed = models.BigIntegerField(default=0, blank=True, null=True)
16 20 d_passedq = models.BigIntegerField(default=0, blank=True, null=True)
17 21 d_passedx = models.BigIntegerField(default=0, blank=True, null=True)
18 22  
  23 + d_schedule = models.BigIntegerField(default=0, blank=True, null=True)
19 24 d_scheduleq = models.BigIntegerField(default=0, blank=True, null=True)
20 25 d_schedulex = models.BigIntegerField(default=0, blank=True, null=True)
21 26  
  27 + d_next = models.BigIntegerField(default=0, blank=True, null=True)
22 28 d_nextq = models.BigIntegerField(default=0, blank=True, null=True)
23 29 d_nextx = models.BigIntegerField(default=0, blank=True, null=True)
24 30  
25   - @property
26   - def d_total(self):
27   - return self.d_totalq + self.d_totalx
  31 + # @property
  32 + # def d_total(self):
  33 + # return self.d_totalq + self.d_totalx
28 34  
29   - @property
30   - def d_previous(self):
31   - return self.d_previousq + self.d_previousx
  35 + # @property
  36 + # def d_previous(self):
  37 + # return self.d_previousq + self.d_previousx
32 38  
33   - @property
34   - def d_current(self):
35   - return self.d_currentq + self.d_currentx
  39 + # @property
  40 + # def d_current(self):
  41 + # return self.d_currentq + self.d_currentx
36 42  
37   - @property
38   - def d_passed(self):
39   - return self.d_passedq + self.d_passedx
  43 + # @property
  44 + # def d_passed(self):
  45 + # return self.d_passedq + self.d_passedx
40 46  
41   - @property
42   - def d_schedule(self):
43   - return self.d_scheduleq + self.d_schedulex
  47 + # @property
  48 + # def d_schedule(self):
  49 + # return self.d_scheduleq + self.d_schedulex
44 50  
45   - @property
46   - def d_next(self):
47   - return self.d_nextq + self.d_nextx
  51 + # @property
  52 + # def d_next(self):
  53 + # return self.d_nextq + self.d_nextx
48 54  
49 55 def set_attributes_and_save(self, quota_attributes:dict):
50   -
51 56 if quota_attributes.get("id_period") != None:
52 57 self.id_period = quota_attributes["id_period"]
53 58 if quota_attributes.get("night_id") != None:
54 59 self.night_id = quota_attributes["night_id"]
  60 +
55 61 if quota_attributes.get("d_totalq") != None:
56 62 self.d_totalq = quota_attributes["d_totalq"]
57 63 if quota_attributes.get("d_totalx") != None:
58 64 self.d_totalx = quota_attributes["d_totalx"]
  65 +
59 66 if quota_attributes.get("d_previousq") != None:
60 67 self.d_previousq = quota_attributes["d_previousq"]
61 68 if quota_attributes.get("d_previousx") != None:
62 69 self.d_previousx = quota_attributes["d_previousx"]
  70 +
63 71 if quota_attributes.get("d_currentq") != None:
64 72 self.d_currentq = quota_attributes["d_currentq"]
65 73 if quota_attributes.get("d_currentx") != None:
66 74 self.d_currentx = quota_attributes["d_currentx"]
  75 +
  76 + if quota_attributes.get("d_passedq") != None:
  77 + self.d_passedq = quota_attributes["d_passedq"]
  78 + if quota_attributes.get("d_passedx") != None:
  79 + self.d_passedx = quota_attributes["d_passedx"]
  80 +
  81 +
67 82 if quota_attributes.get("d_scheduleq") != None:
68 83 self.d_scheduleq = quota_attributes["d_scheduleq"]
69 84 if quota_attributes.get("d_schedulex") != None:
70   - self.d_schedule = quota_attributes["d_schedulex"]
  85 + self.d_schedulex = quota_attributes["d_schedulex"]
  86 +
  87 + if quota_attributes.get("d_nextx") != None:
  88 + self.d_nextq = quota_attributes["d_nextx"]
71 89 if quota_attributes.get("d_nextq") != None:
72 90 self.d_nextq = quota_attributes["d_nextq"]
73   - if quota_attributes.get("d_nextx") != None:
74   - self.d_nextx = quota_attributes["d_nextx"]
  91 +
  92 +
  93 + if quota_attributes.get("d_total") != None:
  94 + self.d_total = quota_attributes["d_total"]
  95 + if quota_attributes.get("d_previous") != None:
  96 + self.d_previous = quota_attributes["d_previous"]
  97 + if quota_attributes.get("d_current") != None:
  98 + self.d_current = quota_attributes["d_current"]
  99 + if quota_attributes.get("d_passed") != None:
  100 + self.d_passed = quota_attributes["d_passed"]
  101 + if quota_attributes.get("d_schedule") != None:
  102 + self.d_schedule = quota_attributes["d_schedule"]
  103 + if quota_attributes.get("d_next") != None:
  104 + self.d_next = quota_attributes["d_next"]
75 105 self.save()
76 106  
77 107 def convert_to_quota(self, quota_f):
78 108 quota_institute = {}
79 109  
80   - quota_institute["d_totalq"] = self.d_totalq * quota_f
81   - quota_institute["d_totalx"] = self.d_totalx * quota_f
82   - quota_institute["d_previousq"] = self.d_previousq * quota_f
83   - quota_institute["d_previousx"] = self.d_previousx * quota_f
84   - quota_institute["d_currentq"] = self.d_currentq * quota_f
85   - quota_institute["d_currentx"] = self.d_currentx * quota_f
86   - quota_institute["d_passedq"] = self.d_passedq * quota_f
87   - quota_institute["d_passedx"] = self.d_passedx * quota_f
88   - quota_institute["d_scheduleq"] = self.d_scheduleq * quota_f
89   - quota_institute["d_schedulex"] = self.d_schedulex * quota_f
90   - quota_institute["d_nextq"] = self.d_nextq * quota_f
91   - quota_institute["d_nextx"] = self.d_nextx * quota_f
  110 + quota_institute["d_total"] = self.d_total * quota_f
  111 + quota_institute["d_previous"] = self.d_previous * quota_f
  112 + quota_institute["d_current"] = self.d_current * quota_f
  113 + quota_institute["d_passed"] = self.d_passed * quota_f
  114 + quota_institute["d_schedule"] = self.d_schedule * quota_f
  115 + quota_institute["d_next"] = self.d_next * quota_f
92 116  
93 117 return quota_institute
... ...
src/core/pyros_django/scp_mgmt/templates/scp_mgmt/quota_sp.html 0 → 100644
... ... @@ -0,0 +1,128 @@
  1 +{% extends "base.html" %}
  2 +
  3 +{% load tags %}
  4 +{% block content %}
  5 +
  6 +<style>
  7 + .institute{
  8 + background-color: aqua;
  9 + }
  10 + .sp{
  11 + background-color: aquamarine;
  12 + }
  13 +</style>
  14 +<table class="table table-sm table-bordered tablesorter">
  15 + <thead>
  16 + <tr>
  17 + <td> name </td>
  18 + <td> quota_f </td>
  19 + <td> <b> d_total </b> </td>
  20 + <td> d_totalq </td>
  21 + <td> d_totalx </td>
  22 + <td> <b> d_previous </b> </td>
  23 + <td> d_previousq </td>
  24 + <td> d_previousx </td>
  25 + <td> <b> d_current </b> </td>
  26 + <td> d_currentq </td>
  27 + <td> d_currentx </td>
  28 + <td> <b> d_passed </b> </td>
  29 + <td> d_passedq </td>
  30 + <td> d_passedx </td>
  31 + <td> <b> d_schedule </b> </td>
  32 + <td> d_scheduleq </td>
  33 + <td> d_schedulex </td>
  34 + <td> <b> d_next </b> </td>
  35 + <td> d_nextq </td>
  36 + <td> d_nextx </td>
  37 + </tr>
  38 + </thead>
  39 + <tr class="current_night">
  40 + <td> Current night : {{ current_night }} </td>
  41 + <td> </td>
  42 + <td> <b>{{quota_current_night.d_total}}</b> </td>
  43 + <td>{{quota_current_night.d_totalq}}</td>
  44 + <td>{{quota_current_night.d_totalx}}</td>
  45 +
  46 + <td> <b>{{quota_current_night.d_previous}}</b> </td>
  47 + <td>{{quota_current_night.d_previousq}}</td>
  48 + <td>{{quota_current_night.d_previousx}}</td>
  49 +
  50 + <td> <b>{{quota_current_night.d_current}}</b> </td>
  51 + <td>{{quota_current_night.d_currentq}}</td>
  52 + <td>{{quota_current_night.d_currentx}}</td>
  53 +
  54 + <td> <b>{{quota_current_night.d_passed}}</b> </td>
  55 + <td>{{quota_current_night.d_passedq}}</td>
  56 + <td>{{quota_current_night.d_passedx}}</td>
  57 +
  58 + <td> <b>{{quota_current_night.d_schedule}}</b> </td>
  59 + <td>{{quota_current_night.d_scheduleq}}</td>
  60 + <td>{{quota_current_night.d_schedulex}} </td>
  61 +
  62 + <td> <b>{{quota_current_night.d_next}}</b> </td>
  63 + <td>{{quota_current_night.d_nextq}}</td>
  64 + <td>{{quota_current_night.d_nextx}}</td>
  65 + </tr>
  66 + {% for institute in institutes %}
  67 + <tr class="institute">
  68 + <td> Institute :{{institute}} </td>
  69 + <td>{{institute.quota_f}} </td>
  70 + <td> <b>{{institute.quota.d_total}}</b> </td>
  71 + <td>{{institute.quota.d_totalq}}</td>
  72 + <td>{{institute.quota.d_totalx}}</td>
  73 +
  74 + <td> <b>{{institute.quota.d_previous}}</b> </td>
  75 + <td>{{institute.quota.d_previousq}}</td>
  76 + <td>{{institute.quota.d_previousx}}</td>
  77 +
  78 + <td> <b>{{institute.quota.d_current}}</b> </td>
  79 + <td>{{institute.quota.d_currentq}}</td>
  80 + <td>{{institute.quota.d_currentx}}</td>
  81 +
  82 + <td> <b>{{institute.quota.d_passed}}</b> </td>
  83 + <td>{{institute.quota.d_passedq}}</td>
  84 + <td>{{institute.quota.d_passedx}}</td>
  85 +
  86 + <td> <b>{{institute.quota.d_schedule}}</b> </td>
  87 + <td>{{institute.quota.d_scheduleq}}</td>
  88 + <td>{{institute.quota.d_schedulex}} </td>
  89 +
  90 + <td> <b>{{institute.quota.d_next}}</b> </td>
  91 + <td>{{institute.quota.d_nextq}}</td>
  92 + <td>{{institute.quota.d_nextx}}</td>
  93 + </tr>
  94 + {% for sp in institute.scientific_programs.all %}
  95 + <tr class="sp">
  96 + <td>Scientific program: {{sp.name}} </td>
  97 + <td>{{sp.quota_f}} </td>
  98 + <td> <b>{{sp.quota.d_total}}</b> </td>
  99 + <td>{{sp.quota.d_totalq}}</td>
  100 + <td>{{sp.quota.d_totalx}}</td>
  101 +
  102 + <td> <b>{{sp.quota.d_previous}}</b> </td>
  103 + <td>{{sp.quota.d_previousq}}</td>
  104 + <td>{{sp.quota.d_previousx}}</td>
  105 +
  106 + <td> <b>{{sp.quota.d_current}}</b> </td>
  107 + <td>{{sp.quota.d_currentq}}</td>
  108 + <td>{{sp.quota.d_currentx}}</td>
  109 +
  110 + <td> <b>{{sp.quota.d_passed}}</b> </td>
  111 + <td>{{sp.quota.d_passedq}}</td>
  112 + <td>{{sp.quota.d_passedx}}</td>
  113 +
  114 + <td> <b>{{sp.quota.d_schedule}}</b> </td>
  115 + <td>{{sp.quota.d_scheduleq}}</td>
  116 + <td>{{sp.quota.d_schedulex}} </td>
  117 +
  118 + <td> <b>{{sp.quota.d_next}}</b> </td>
  119 + <td>{{sp.quota.d_nextq}}</td>
  120 + <td>{{sp.quota.d_nextx}}</td>
  121 + </tr>
  122 + {% endfor %}
  123 +
  124 + {% endfor %}
  125 +
  126 +
  127 +
  128 +{% endblock %}
... ...
src/core/pyros_django/scp_mgmt/urls.py
... ... @@ -43,5 +43,7 @@ urlpatterns = [
43 43 path("detail_science_theme/<int:id>/",views.detail_science_theme,name="detail_science_theme"),
44 44 path("edit_science_theme/<int:id>/",views.edit_science_theme,name="edit_science_theme"),
45 45 path("delete_science_theme/<int:id>/",views.delete_science_theme,name="delete_science_theme"),
46   - path("test_tac_auto",views.test_tac_auto,name="test_tac_auto")
  46 + path("test_tac_auto",views.test_tac_auto,name="test_tac_auto"),
  47 + # quota
  48 + path("quota_sp",views.quota_sp,name="quota_sp")
47 49 ]
... ...
src/core/pyros_django/scp_mgmt/views.py
... ... @@ -2,7 +2,7 @@
2 2 from datetime import date, datetime
3 3 from dateutil.relativedelta import relativedelta
4 4 import matplotlib.pyplot as plt
5   -import re,io,urllib,base64
  5 +import re,io,urllib,base64, os
6 6  
7 7 # Django imports
8 8 from django.http.response import HttpResponse
... ... @@ -22,12 +22,12 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
22 22 from dashboard.config_pyros import ConfigPyros
23 23 from .functions import get_global_svg_timeline, get_svg_timeline, get_proposal_svg_timeline
24 24 from user_mgmt.models import ScientificProgram, Institute, Period, SP_Period_User, SP_Period, PyrosUser, SP_Period_Guest, ScienceTheme #, UserLevel
25   -from seq_submit.models import Sequence
  25 +from seq_submit.models import Sequence, Quota
26 26 #from src.core.pyros_django import scientific_program
27 27 from .forms import PeriodForm, ScienceThemeForm, ScientificProgramForm, InstituteForm, SP_PeriodForm,TACAssociationForm
28 28 from src.core.pyros_django.dashboard.decorator import level_required
29 29  
30   -
  30 +from src.core.pyros_django.obs_config.obsconfig_class import OBSConfig
31 31  
32 32  
33 33  
... ... @@ -807,6 +807,19 @@ def institute_list(request):
807 807 })
808 808  
809 809  
  810 +@login_required
  811 +@level_required("Admin", "Unit-PI", "Observer", "Operator", "TAC", "Management board member")
  812 +def quota_sp(request):
  813 + institutes = Institute.objects.all()
  814 + scientific_programs = ScientificProgram.objects.all()
  815 + config = OBSConfig(os.environ["PATH_TO_OBSCONF_FILE"],os.environ["unit_name"])
  816 + current_night = config.fn.date2night("now")
  817 + current_period = Period.objects.exploitation_period()
  818 + # lowest id is period line
  819 + quota_current_night = Quota.objects.filter(id_period=current_period.id, night_id=current_night).order_by("id").first()
  820 + return render(request, 'scp_mgmt/quota_sp.html', locals())
  821 +
  822 +
810 823 # Exploitation Periods CRUD
811 824  
812 825 @login_required
... ...
src/core/pyros_django/seq_submit/functions.py
... ... @@ -163,6 +163,17 @@ def check_sequence_file_validity_and_save(yaml_content: dict, request: HttpReque
163 163 if Period.objects.next_period() != None and Period.objects.next_period().start_date < seq.start_date.date():
164 164 period = Period.objects.next_period()
165 165 seq.period = period
  166 + # Sum seq duration
  167 + duration = 0
  168 + max_duration = 0
  169 + for album in seq.albums.all():
  170 + for plan in album.plans.all():
  171 + duration = plan.nb_images * (plan.config_attributes.get("exposuretime",0) + plan.config_attributes.get("readouttime",0))
  172 + plan.duration = duration
  173 + plan.save()
  174 + if duration >= max_duration:
  175 + max_duration = duration
  176 + seq.duration = max_duration
166 177 fn = guitastro.FileNames()
167 178 home = config.getHome()
168 179 guitastro_home = guitastro.Home(home)
... ... @@ -315,6 +326,9 @@ def process_sequence(yaml_content, seq, config, is_simplified, result, user_sp):
315 326  
316 327 if is_simplified:
317 328 seq.scientific_program = sp_list[yaml_content["sequence"]["scientific_program"]]
  329 + if yaml_content["sequence"].get("id"):
  330 + seq.id = yaml_content["sequence"].get("id")
  331 + seq.save()
318 332 else:
319 333 # get scientific program field's attributes
320 334 yaml_seq_sp = yaml_content["sequence"]["scientific_program"]
... ... @@ -415,7 +429,6 @@ def process_sequence(yaml_content, seq, config, is_simplified, result, user_sp):
415 429 # else associate field & value in config_attributes sequence's field (JsonField) = variable fields of an sequence
416 430 seq.config_attributes[field] = value
417 431  
418   -
419 432 def create_sequence_pickle(sequence):
420 433 seq_dict = model_to_dict(sequence)
421 434 fullseq_dict = {
... ... @@ -442,8 +455,6 @@ def create_sequence_pickle(sequence):
442 455 config = OBSConfig(os.environ["PATH_TO_OBSCONF_FILE"], unit_name)
443 456 pyros_config = ConfigPyros(os.environ["pyros_config_file"])
444 457 config.fn.fcontext = "pyros_seq"
445   - home = guitastro.Home(config.getHome())
446   - config.fn.longitude(home.longitude)
447 458 period_id = str(period.id)
448 459 if len(str(period.id)) < 3:
449 460 while len(period_id) < 3:
... ... @@ -455,6 +466,12 @@ def create_sequence_pickle(sequence):
455 466 "date": sequence.night_id,
456 467 "id_seq": sequence.id
457 468 }
  469 + test_mode = False
  470 + if sequence.id >= 9990000000:
  471 + # in test mode
  472 + config.fn.rootdir = os.path.abspath(config.fn.rootdir.replace("PRODUCTS/","PRODUCTS/TESTS/", 1))
  473 + test_mode = True
  474 +
458 475 config.fn.fname = config.fn.naming_set(fn_param)
459 476 fpath_name = config.fn.join(config.fn.fname)
460 477 # create dirs if they don't exist
... ... @@ -466,9 +483,22 @@ def create_sequence_pickle(sequence):
466 483 duskelev = -7
467 484 errors = []
468 485 try:
469   - # TODO remplacer les none par les fichiers pickle de ephem_sun & ephem_moon
470 486 #fullseq_dict["ephem"] = eph.target2night(fullseq_dict["sequence"]["config_attributes"]["target"], sequence.night_id, None, None, preferance=sequence.start_expo_pref, duskelev=duskelev)
471   - ephem = eph.target2night(fullseq_dict["sequence"]["config_attributes"]["target"], sequence.night_id, None, None, preference=sequence.start_expo_pref, duskelev=duskelev)
  487 + # change fcontext to eph context
  488 + config.fn.fcontext = "pyros_eph"
  489 + if test_mode:
  490 + config.fn.rootdir = os.path.abspath(config.fn.rootdir.replace("PRODUCTS/","PRODUCTS/TESTS/", 1))
  491 + eph_root_dir = config.fn.rootdir
  492 + fn_param["target"] = "sun"
  493 + config.fn.fname = config.fn.naming_set(fn_param)
  494 + sun_eph_fpath = config.fn.join(config.fn.fname)
  495 + fn_param["target"] = "moon"
  496 + config.fn.fname = config.fn.naming_set(fn_param)
  497 + moon_eph_fpath = config.fn.join(config.fn.fname)
  498 + # open eph files
  499 + sun_eph = pickle.load(open(sun_eph_fpath,"rb"))
  500 + moon_eph = pickle.load(open(moon_eph_fpath,"rb"))
  501 + ephem = eph.target2night(fullseq_dict["sequence"]["config_attributes"]["target"], sequence.night_id, sun_eph, moon_eph, preference=sequence.start_expo_pref, duskelev=duskelev)
472 502 except ValueError:
473 503 errors.append("Target value is not valid")
474 504 except guitastro.ephemeris.EphemerisException as ephemException:
... ...
src/core/pyros_django/seq_submit/models.py
... ... @@ -352,7 +352,7 @@ class Sequence(models.Model):
352 352 flag = models.CharField(max_length=45, blank=True, null=True)
353 353 period = models.ForeignKey(Period, on_delete=models.DO_NOTHING, related_name="sequence_period", blank=True, null=True)
354 354 #period = models.ForeignKey("Period", on_delete=models.DO_NOTHING, related_name="sequences", blank=True, null=True)
355   - quota = models.ForeignKey(Quota, on_delete=models.DO_NOTHING,related_name="sequence_quotas", blank=True, null=True)
  355 + quota = models.ForeignKey(Quota, on_delete=models.SET_NULL,related_name="sequence_quotas", blank=True, null=True)
356 356  
357 357 start_date = models.DateTimeField(
358 358 blank=True, null=True, default=timezone.now, editable=True)
... ...
src/core/pyros_django/user_mgmt/models.py
... ... @@ -87,7 +87,7 @@ class Institute(models.Model):
87 87 quota_f = models.FloatField(
88 88 validators=[MinValueValidator(0), MaxValueValidator(1)], blank=True, null=True)
89 89  
90   - quota = models.ForeignKey(Quota, on_delete=models.DO_NOTHING,related_name="institute_quotas", blank=True, null=True)
  90 + quota = models.ForeignKey(Quota, on_delete=models.SET_NULL,related_name="institute_quotas", blank=True, null=True)
91 91 #representative_user = models.ForeignKey("PyrosUser", on_delete=models.DO_NOTHING,related_name="institutes",default=1)
92 92  
93 93 def __str__(self) -> str:
... ... @@ -375,7 +375,7 @@ class Period(models.Model):
375 375 data_accessibility_duration = models.PositiveIntegerField(
376 376 blank=True, null=True, default=365*10, editable=True)
377 377  
378   - quota = models.ForeignKey(Quota, on_delete=models.DO_NOTHING,related_name="period_quotas", blank=True, null=True)
  378 + quota = models.ForeignKey(Quota, on_delete=models.SET_NULL,related_name="period_quotas", blank=True, null=True)
379 379  
380 380 @property
381 381 def end_date(self):
... ... @@ -475,7 +475,7 @@ class ScientificProgram(models.Model):
475 475 science_theme = models.ForeignKey(ScienceTheme, on_delete=models.DO_NOTHING, related_name="scientific_program_theme", default=1)
476 476 is_auto_validated = models.BooleanField(default=False)
477 477 objects = ScientificProgramManager()
478   - quota = models.ForeignKey(Quota, on_delete=models.DO_NOTHING, related_name="scientific_program_quotas", blank=True, null=True)
  478 + quota = models.ForeignKey(Quota, on_delete=models.SET_NULL, related_name="scientific_program_quotas", blank=True, null=True)
479 479 quota_f = models.FloatField(
480 480 validators=[MinValueValidator(0), MaxValueValidator(1)], blank=True, null=True)
481 481  
... ...