NEMO-CE 7.3.6__py3-none-any.whl → 7.3.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- NEMO/admin.py +11 -1
- NEMO/migrations/0141_toolqualificationexpiration.py +103 -0
- NEMO/models.py +81 -15
- NEMO/templates/customizations/customizations_tool.html +5 -88
- NEMO/templates/tool_control/tool_status.html +2 -2
- NEMO/templates/tool_control/usage_data.html +46 -6
- NEMO/tests/test_tools/test_tool_qualification_expiration.py +40 -30
- NEMO/views/api.py +6 -1
- NEMO/views/calendar.py +2 -3
- NEMO/views/customization.py +1 -9
- NEMO/views/timed_services.py +37 -28
- NEMO/views/tool_control.py +52 -24
- {nemo_ce-7.3.6.dist-info → nemo_ce-7.3.8.dist-info}/METADATA +1 -1
- {nemo_ce-7.3.6.dist-info → nemo_ce-7.3.8.dist-info}/RECORD +18 -17
- {nemo_ce-7.3.6.dist-info → nemo_ce-7.3.8.dist-info}/WHEEL +0 -0
- {nemo_ce-7.3.6.dist-info → nemo_ce-7.3.8.dist-info}/entry_points.txt +0 -0
- {nemo_ce-7.3.6.dist-info → nemo_ce-7.3.8.dist-info}/licenses/LICENSE +0 -0
- {nemo_ce-7.3.6.dist-info → nemo_ce-7.3.8.dist-info}/top_level.txt +0 -0
NEMO/admin.py
CHANGED
|
@@ -287,7 +287,6 @@ class ToolAdmin(admin.ModelAdmin):
|
|
|
287
287
|
"parent_tool",
|
|
288
288
|
"_category",
|
|
289
289
|
"_operation_mode",
|
|
290
|
-
"_qualifications_never_expire",
|
|
291
290
|
"_problem_shutdown_enabled",
|
|
292
291
|
"_reservation_required",
|
|
293
292
|
"_logout_grace_period",
|
|
@@ -342,6 +341,17 @@ class ToolAdmin(admin.ModelAdmin):
|
|
|
342
341
|
)
|
|
343
342
|
},
|
|
344
343
|
),
|
|
344
|
+
(
|
|
345
|
+
"Qualification expiration",
|
|
346
|
+
{
|
|
347
|
+
"fields": (
|
|
348
|
+
"_qualification_reminder_days",
|
|
349
|
+
"_qualification_expiration_days",
|
|
350
|
+
"_qualification_expiration_never_used_days",
|
|
351
|
+
"_qualification_notification_email",
|
|
352
|
+
)
|
|
353
|
+
},
|
|
354
|
+
),
|
|
345
355
|
(
|
|
346
356
|
"Area Access",
|
|
347
357
|
{
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Generated by Django 4.2.27 on 2026-01-26 20:42
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import django.core.validators
|
|
6
|
+
import django.db.models.deletion
|
|
7
|
+
from django.db import migrations, models
|
|
8
|
+
|
|
9
|
+
import NEMO.fields
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def migrate_tool_qualification_expiration_forward(apps, schema_editor):
|
|
13
|
+
Tool = apps.get_model("NEMO", "Tool")
|
|
14
|
+
Customization = apps.get_model("NEMO", "Customization")
|
|
15
|
+
tool_qualification_reminder_days = Customization.objects.filter(name="tool_qualification_reminder_days").first()
|
|
16
|
+
tool_qualification_expiration_days = Customization.objects.filter(name="tool_qualification_expiration_days").first()
|
|
17
|
+
tool_qualification_expiration_never_used_days = Customization.objects.filter(
|
|
18
|
+
name="tool_qualification_expiration_never_used_days"
|
|
19
|
+
).first()
|
|
20
|
+
tool_qualification_notification = Customization.objects.filter(name="tool_qualification_cc").first()
|
|
21
|
+
for tool in Tool.objects.filter(_qualifications_never_expire=False, parent_tool__isnull=True):
|
|
22
|
+
if tool_qualification_expiration_days or tool_qualification_expiration_never_used_days:
|
|
23
|
+
if tool_qualification_reminder_days and tool_qualification_reminder_days.value:
|
|
24
|
+
tool._qualification_reminder_days = tool_qualification_reminder_days.value
|
|
25
|
+
if tool_qualification_expiration_days and tool_qualification_expiration_days.value:
|
|
26
|
+
tool._qualification_expiration_days = tool_qualification_expiration_days.value
|
|
27
|
+
if tool_qualification_expiration_never_used_days and tool_qualification_expiration_never_used_days.value:
|
|
28
|
+
tool._qualification_expiration_never_used_days = tool_qualification_expiration_never_used_days.value
|
|
29
|
+
if tool_qualification_notification and tool_qualification_notification.value:
|
|
30
|
+
tool._qualification_notification_email = tool_qualification_notification.value
|
|
31
|
+
tool.save()
|
|
32
|
+
if tool_qualification_reminder_days:
|
|
33
|
+
tool_qualification_reminder_days.delete()
|
|
34
|
+
if tool_qualification_expiration_days:
|
|
35
|
+
tool_qualification_expiration_days.delete()
|
|
36
|
+
if tool_qualification_expiration_never_used_days:
|
|
37
|
+
tool_qualification_expiration_never_used_days.delete()
|
|
38
|
+
if tool_qualification_notification:
|
|
39
|
+
tool_qualification_notification.delete()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Migration(migrations.Migration):
|
|
43
|
+
|
|
44
|
+
dependencies = [
|
|
45
|
+
("NEMO", "0140_alter_user_options"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
operations = [
|
|
49
|
+
migrations.AddField(
|
|
50
|
+
model_name="tool",
|
|
51
|
+
name="_qualification_expiration_days",
|
|
52
|
+
field=models.PositiveIntegerField(
|
|
53
|
+
db_column="qualification_expiration_days",
|
|
54
|
+
blank=True,
|
|
55
|
+
help_text="The number of days from the user’s last tool use until the qualification expires.",
|
|
56
|
+
null=True,
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
migrations.AddField(
|
|
60
|
+
model_name="tool",
|
|
61
|
+
name="_qualification_expiration_never_used_days",
|
|
62
|
+
field=models.PositiveIntegerField(
|
|
63
|
+
db_column="qualification_expiration_never_used_days",
|
|
64
|
+
blank=True,
|
|
65
|
+
help_text="Number of days from the user's first qualification until the qualification expires (if the user never used the tool).",
|
|
66
|
+
null=True,
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
migrations.AddField(
|
|
70
|
+
model_name="tool",
|
|
71
|
+
name="_qualification_notification_email",
|
|
72
|
+
field=NEMO.fields.MultiEmailField(
|
|
73
|
+
db_column="qualification_notification_email",
|
|
74
|
+
blank=True,
|
|
75
|
+
help_text="The email addresses to cc on tool qualification expiration and on reminders. Separate multiple emails with commas.",
|
|
76
|
+
max_length=2000,
|
|
77
|
+
null=True,
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
migrations.AddField(
|
|
81
|
+
model_name="tool",
|
|
82
|
+
name="_qualification_reminder_days",
|
|
83
|
+
field=models.CharField(
|
|
84
|
+
db_column="qualification_reminder_days",
|
|
85
|
+
blank=True,
|
|
86
|
+
help_text="The (optional) number of days to send a reminder prior to the user's tool qualification expiration (below). A comma-separated list can be used for multiple reminders. This applies to both expiration cases.",
|
|
87
|
+
max_length=255,
|
|
88
|
+
null=True,
|
|
89
|
+
validators=[
|
|
90
|
+
django.core.validators.RegexValidator(
|
|
91
|
+
re.compile("^\\d+(?:,\\d+)*\\Z"),
|
|
92
|
+
code="invalid",
|
|
93
|
+
message="Enter only digits separated by commas.",
|
|
94
|
+
)
|
|
95
|
+
],
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
migrations.RunPython(migrate_tool_qualification_expiration_forward, migrations.RunPython.noop),
|
|
99
|
+
migrations.RemoveField(
|
|
100
|
+
model_name="tool",
|
|
101
|
+
name="_qualifications_never_expire",
|
|
102
|
+
),
|
|
103
|
+
]
|
NEMO/models.py
CHANGED
|
@@ -1349,10 +1349,32 @@ class Tool(SerializationByNameModel):
|
|
|
1349
1349
|
_interlock = models.OneToOneField(
|
|
1350
1350
|
"Interlock", db_column="interlock_id", blank=True, null=True, on_delete=models.SET_NULL
|
|
1351
1351
|
)
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
db_column="
|
|
1355
|
-
|
|
1352
|
+
# Qualification expiration fields
|
|
1353
|
+
_qualification_reminder_days = models.CharField(
|
|
1354
|
+
db_column="qualification_reminder_days",
|
|
1355
|
+
null=True,
|
|
1356
|
+
blank=True,
|
|
1357
|
+
max_length=CHAR_FIELD_MEDIUM_LENGTH,
|
|
1358
|
+
validators=[validate_comma_separated_integer_list],
|
|
1359
|
+
help_text="The (optional) number of days to send a reminder prior to the user's tool qualification expiration (below). A comma-separated list can be used for multiple reminders. This applies to both expiration cases.",
|
|
1360
|
+
)
|
|
1361
|
+
_qualification_expiration_days = models.PositiveIntegerField(
|
|
1362
|
+
db_column="qualification_expiration_days",
|
|
1363
|
+
null=True,
|
|
1364
|
+
blank=True,
|
|
1365
|
+
help_text="The number of days from the user’s last tool use until the qualification expires.",
|
|
1366
|
+
)
|
|
1367
|
+
_qualification_expiration_never_used_days = models.PositiveIntegerField(
|
|
1368
|
+
db_column="qualification_expiration_never_used_days",
|
|
1369
|
+
null=True,
|
|
1370
|
+
blank=True,
|
|
1371
|
+
help_text="Number of days from the user's first qualification until the qualification expires (if the user never used the tool).",
|
|
1372
|
+
)
|
|
1373
|
+
_qualification_notification_email = fields.MultiEmailField(
|
|
1374
|
+
db_column="qualification_notification_email",
|
|
1375
|
+
null=True,
|
|
1376
|
+
blank=True,
|
|
1377
|
+
help_text="The email addresses to cc on tool qualification expiration and on reminders. Separate multiple emails with commas.",
|
|
1356
1378
|
)
|
|
1357
1379
|
# Policy fields:
|
|
1358
1380
|
_requires_area_access = TreeForeignKey(
|
|
@@ -1530,17 +1552,6 @@ class Tool(SerializationByNameModel):
|
|
|
1530
1552
|
self.raise_setter_error_if_child_tool("category")
|
|
1531
1553
|
self._category = value
|
|
1532
1554
|
|
|
1533
|
-
@property
|
|
1534
|
-
def qualifications_never_expire(self):
|
|
1535
|
-
return (
|
|
1536
|
-
self.parent_tool.qualifications_never_expire if self.is_child_tool() else self._qualifications_never_expire
|
|
1537
|
-
)
|
|
1538
|
-
|
|
1539
|
-
@qualifications_never_expire.setter
|
|
1540
|
-
def qualifications_never_expire(self, value):
|
|
1541
|
-
self.raise_setter_error_if_child_tool("qualifications_never_expire")
|
|
1542
|
-
self._qualifications_never_expire = value
|
|
1543
|
-
|
|
1544
1555
|
@property
|
|
1545
1556
|
def description(self):
|
|
1546
1557
|
return self.parent_tool.description if self.is_child_tool() else self._description
|
|
@@ -1689,6 +1700,56 @@ class Tool(SerializationByNameModel):
|
|
|
1689
1700
|
self.raise_setter_error_if_child_tool("interlock")
|
|
1690
1701
|
self._interlock = value
|
|
1691
1702
|
|
|
1703
|
+
@property
|
|
1704
|
+
def qualification_reminder_days(self):
|
|
1705
|
+
return (
|
|
1706
|
+
self.parent_tool.qualification_reminder_days if self.is_child_tool() else self._qualification_reminder_days
|
|
1707
|
+
)
|
|
1708
|
+
|
|
1709
|
+
@qualification_reminder_days.setter
|
|
1710
|
+
def qualification_reminder_days(self, value):
|
|
1711
|
+
self.raise_setter_error_if_child_tool("qualification_reminder_days")
|
|
1712
|
+
self._qualification_reminder_days = value
|
|
1713
|
+
|
|
1714
|
+
@property
|
|
1715
|
+
def qualification_expiration_days(self):
|
|
1716
|
+
return (
|
|
1717
|
+
self.parent_tool.qualification_expiration_days
|
|
1718
|
+
if self.is_child_tool()
|
|
1719
|
+
else self._qualification_expiration_days
|
|
1720
|
+
)
|
|
1721
|
+
|
|
1722
|
+
@qualification_expiration_days.setter
|
|
1723
|
+
def qualification_expiration_days(self, value):
|
|
1724
|
+
self.raise_setter_error_if_child_tool("qualification_expiration_days")
|
|
1725
|
+
self._qualification_expiration_days = value
|
|
1726
|
+
|
|
1727
|
+
@property
|
|
1728
|
+
def qualification_expiration_never_used_days(self):
|
|
1729
|
+
return (
|
|
1730
|
+
self.parent_tool.qualification_expiration_never_used_days
|
|
1731
|
+
if self.is_child_tool()
|
|
1732
|
+
else self._qualification_expiration_never_used_days
|
|
1733
|
+
)
|
|
1734
|
+
|
|
1735
|
+
@qualification_expiration_never_used_days.setter
|
|
1736
|
+
def qualification_expiration_never_used_days(self, value):
|
|
1737
|
+
self.raise_setter_error_if_child_tool("qualification_expiration_never_used_days")
|
|
1738
|
+
self._qualification_expiration_never_used_days = value
|
|
1739
|
+
|
|
1740
|
+
@property
|
|
1741
|
+
def qualification_notification_email(self):
|
|
1742
|
+
return (
|
|
1743
|
+
self.parent_tool.qualification_notification_email
|
|
1744
|
+
if self.is_child_tool()
|
|
1745
|
+
else self._qualification_notification_email
|
|
1746
|
+
)
|
|
1747
|
+
|
|
1748
|
+
@qualification_notification_email.setter
|
|
1749
|
+
def qualification_notification_email(self, value):
|
|
1750
|
+
self.raise_setter_error_if_child_tool("qualification_notification_email")
|
|
1751
|
+
self._qualification_notification_email = value
|
|
1752
|
+
|
|
1692
1753
|
@property
|
|
1693
1754
|
def requires_area_access(self):
|
|
1694
1755
|
return self.parent_tool.requires_area_access if self.is_child_tool() else self._requires_area_access
|
|
@@ -2379,6 +2440,11 @@ class Tool(SerializationByNameModel):
|
|
|
2379
2440
|
else:
|
|
2380
2441
|
raise ValueError(f"A {'project' if user else 'user'} must be provided for usage questions")
|
|
2381
2442
|
|
|
2443
|
+
def get_qualification_reminder_days(self) -> List[int]:
|
|
2444
|
+
if not self.qualification_reminder_days:
|
|
2445
|
+
return []
|
|
2446
|
+
return [int(days) for days in self.qualification_reminder_days.split(",") if days]
|
|
2447
|
+
|
|
2382
2448
|
def clean(self):
|
|
2383
2449
|
errors = {}
|
|
2384
2450
|
if self.parent_tool_id:
|
|
@@ -623,97 +623,14 @@
|
|
|
623
623
|
</div>
|
|
624
624
|
</div>
|
|
625
625
|
<div class="customization-separation" style="margin-bottom: 15px"></div>
|
|
626
|
-
<
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
<p>
|
|
630
|
-
The <a href="{% url 'customization' 'templates' %}?#tool_qualification_expiration_email_id">user tool qualification expiration email</a> need to be set to enable this feature.
|
|
631
|
-
</p>
|
|
632
|
-
<br />
|
|
633
|
-
<div class="form-group {% if errors.tool_qualification_reminder_days %}has-error{% endif %}">
|
|
634
|
-
<label class="control-label col-md-3" for="tool_qualification_reminder_days">Reminder days</label>
|
|
635
|
-
<div class="col-md-5">
|
|
636
|
-
<input type="text"
|
|
637
|
-
id="tool_qualification_reminder_days"
|
|
638
|
-
name="tool_qualification_reminder_days"
|
|
639
|
-
class="form-control"
|
|
640
|
-
value="{% if errors.tool_qualification_reminder_days %}{{ errors.tool_qualification_reminder_days.value }}{% else %}{{ tool_qualification_reminder_days }}{% endif %}" />
|
|
641
|
-
</div>
|
|
642
|
-
<div class="col-md-offset-3 col-md-9 help-block light-grey">
|
|
643
|
-
{% if errors.tool_qualification_reminder_days %}
|
|
644
|
-
{{ errors.tool_qualification_reminder_days.error }}
|
|
645
|
-
{% else %}
|
|
646
|
-
The (optional) number of days to send a reminder prior to the user's tool qualification expiration (below). A comma-separated list can be used for multiple reminders. This applies to both expiration cases.
|
|
647
|
-
{% endif %}
|
|
648
|
-
</div>
|
|
649
|
-
</div>
|
|
650
|
-
<div class="form-group {% if errors.tool_qualification_expiration_days %}has-error{% endif %}">
|
|
651
|
-
<label class="control-label col-md-3" for="tool_qualification_expiration_days">
|
|
652
|
-
Expiration days (previous tool usage)
|
|
653
|
-
</label>
|
|
654
|
-
<div class="col-md-5">
|
|
655
|
-
<input type="number"
|
|
656
|
-
step="1"
|
|
657
|
-
id="tool_qualification_expiration_days"
|
|
658
|
-
name="tool_qualification_expiration_days"
|
|
659
|
-
class="form-control"
|
|
660
|
-
value="{% if errors.tool_qualification_expiration_days %}{{ errors.tool_qualification_expiration_days.value }}{% else %}{{ tool_qualification_expiration_days }}{% endif %}" />
|
|
661
|
-
</div>
|
|
662
|
-
<div class="col-md-offset-3 col-md-9 help-block light-grey">
|
|
663
|
-
{% if errors.tool_qualification_expiration_days %}
|
|
664
|
-
{{ errors.tool_qualification_expiration_days.error }}
|
|
665
|
-
{% else %}
|
|
666
|
-
The number of days before the user's tool qualification expires since the user last used the tool.
|
|
667
|
-
{% endif %}
|
|
668
|
-
</div>
|
|
669
|
-
</div>
|
|
670
|
-
<div class="form-group {% if errors.tool_qualification_expiration_never_used_days %}has-error{% endif %}">
|
|
671
|
-
<label class="control-label col-md-3" for="tool_qualification_expiration_never_used_days">
|
|
672
|
-
Expiration days (no tool usage)
|
|
673
|
-
</label>
|
|
674
|
-
<div class="col-md-5">
|
|
675
|
-
<input type="number"
|
|
676
|
-
step="1"
|
|
677
|
-
id="tool_qualification_expiration_never_used_days"
|
|
678
|
-
name="tool_qualification_expiration_never_used_days"
|
|
679
|
-
class="form-control"
|
|
680
|
-
value="{% if errors.tool_qualification_expiration_never_used_days %}{{ errors.tool_qualification_expiration_never_used_days.value }}{% else %}{{ tool_qualification_expiration_never_used_days }}{% endif %}" />
|
|
681
|
-
</div>
|
|
682
|
-
<div class="col-md-offset-3 col-md-9 help-block light-grey">
|
|
683
|
-
{% if errors.tool_qualification_expiration_never_used_days %}
|
|
684
|
-
{{ errors.tool_qualification_expiration_never_used_days.error }}
|
|
685
|
-
{% else %}
|
|
686
|
-
The number of days before the user's tool qualification expires since the user qualified for the first time.
|
|
687
|
-
{% endif %}
|
|
688
|
-
</div>
|
|
689
|
-
</div>
|
|
690
|
-
<div class="form-group {% if errors.tool_qualification_cc %}has-error{% endif %}">
|
|
691
|
-
<label class="control-label col-md-3" for="tool_qualification_cc">Reminder/expiration CC</label>
|
|
692
|
-
<div class="col-md-5">
|
|
693
|
-
<input type="text"
|
|
694
|
-
id="tool_qualification_cc"
|
|
695
|
-
name="tool_qualification_cc"
|
|
696
|
-
class="form-control"
|
|
697
|
-
value="{% if errors.tool_qualification_cc %}{{ errors.tool_qualification_cc.value }}{% else %}{{ tool_qualification_cc }}{% endif %}"
|
|
698
|
-
placeholder="information@example.org" />
|
|
699
|
-
</div>
|
|
700
|
-
<div class="col-md-offset-3 col-md-9 help-block light-grey">
|
|
701
|
-
{% if errors.tool_qualification_cc %}
|
|
702
|
-
{{ errors.tool_qualification_cc.error }}
|
|
703
|
-
{% else %}
|
|
704
|
-
Extra email address to copy when a user's tool qualification reminder/expiration email is sent. A comma-separated list can be used.
|
|
705
|
-
{% endif %}
|
|
706
|
-
</div>
|
|
707
|
-
</div>
|
|
708
|
-
<div class="customization-separation" style="margin-bottom: 15px"></div>
|
|
709
|
-
<div class="text-center">{% button type="save" value="Save settings" %}</div>
|
|
710
|
-
</div>
|
|
711
|
-
<script type="text/javascript">
|
|
626
|
+
<div class="text-center">{% button type="save" value="Save settings" %}</div>
|
|
627
|
+
</div>
|
|
628
|
+
<script type="text/javascript">
|
|
712
629
|
$("#tool-tab-link").click(function() {setTimeout(on_tool_tab_show, 50)});
|
|
713
630
|
function on_tool_tab_show()
|
|
714
631
|
{
|
|
715
632
|
auto_size_textarea(document.getElementById('tool_control_configuration_setting_template'))
|
|
716
633
|
}
|
|
717
634
|
on_tool_tab_show();
|
|
718
|
-
|
|
719
|
-
|
|
635
|
+
</script>
|
|
636
|
+
</form>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
History</a>
|
|
18
18
|
</li>
|
|
19
19
|
{% endif %}
|
|
20
|
-
{% if
|
|
20
|
+
{% if not hide_data_history or user.is_any_part_of_staff %}
|
|
21
21
|
<li style="width: 48%; text-align: center">
|
|
22
22
|
<a href="#usage_data" style="padding: 10px 5px;" onclick="load_usage_data('{{ tool.id }}');">Usage Data History</a>
|
|
23
23
|
</li>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
<a href="#config" onclick="load_config_history('{{ tool.id }}');">Config History</a>
|
|
51
51
|
</li>
|
|
52
52
|
{% endif %}
|
|
53
|
-
{% if
|
|
53
|
+
{% if not hide_data_history or user.is_any_part_of_staff %}
|
|
54
54
|
<li>
|
|
55
55
|
<a href="#usage_data" onclick="load_usage_data('{{ tool.id }}');">Run Data History</a>
|
|
56
56
|
</li>
|
|
@@ -98,21 +98,61 @@
|
|
|
98
98
|
</div>
|
|
99
99
|
</div>
|
|
100
100
|
</form>
|
|
101
|
-
{% if run_data_table.rows or pre_run_data_table.rows %}
|
|
101
|
+
{% if data_table.rows or run_data_table.rows or pre_run_data_table.rows %}
|
|
102
102
|
<ul class="nav nav-tabs" id="usage-tabs">
|
|
103
|
+
{% if data_table.rows %}
|
|
104
|
+
<li class="active">
|
|
105
|
+
<a data-toggle="tab" href="#run-data-tab">Usage history</a>
|
|
106
|
+
</li>
|
|
107
|
+
{% endif %}
|
|
103
108
|
{% if pre_run_data_table.rows %}
|
|
104
|
-
<li
|
|
109
|
+
<li>
|
|
105
110
|
<a data-toggle="tab" href="#pre-run-data-tab">Pre-Run Data</a>
|
|
106
111
|
</li>
|
|
107
112
|
{% endif %}
|
|
108
113
|
{% if run_data_table.rows %}
|
|
109
|
-
<li
|
|
110
|
-
<a data-toggle="tab" href="#run-data-tab">Run Data</a>
|
|
114
|
+
<li>
|
|
115
|
+
<a data-toggle="tab" href="#post-run-data-tab">Post-Run Data</a>
|
|
111
116
|
</li>
|
|
112
117
|
{% endif %}
|
|
113
118
|
</ul>
|
|
114
119
|
<div class="tab-content" style="margin-bottom: 0;">
|
|
115
|
-
<div class="tab-pane
|
|
120
|
+
<div class="tab-pane active" id="run-data-tab">
|
|
121
|
+
<div class="col-md-12">
|
|
122
|
+
<div class="form-group pull-right extra-side-padding" style="padding-top: 10px">
|
|
123
|
+
{% button type="export" value="Export" onclick="load_usage_data('"|concat:tool_id|concat:"', 'all');" %}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="panel-body">
|
|
127
|
+
{% if data_table.rows %}
|
|
128
|
+
<table class="table table-bordered table-condensed" style="margin-top: 10px">
|
|
129
|
+
<thead>
|
|
130
|
+
<tr>
|
|
131
|
+
{% for header in data_table.flat_headers %}<th>{{ header }}</th>{% endfor %}
|
|
132
|
+
</tr>
|
|
133
|
+
</thead>
|
|
134
|
+
<tbody>
|
|
135
|
+
{% for row in data_table.flat_rows %}
|
|
136
|
+
<tr>
|
|
137
|
+
{% for item in row %}
|
|
138
|
+
<td>
|
|
139
|
+
{% if item|class_name == "list" %}
|
|
140
|
+
{{ item|join:", " }}
|
|
141
|
+
{% else %}
|
|
142
|
+
{{ item|default_if_none:"" }}
|
|
143
|
+
{% endif %}
|
|
144
|
+
</td>
|
|
145
|
+
{% endfor %}
|
|
146
|
+
</tr>
|
|
147
|
+
{% endfor %}
|
|
148
|
+
</tbody>
|
|
149
|
+
</table>
|
|
150
|
+
{% else %}
|
|
151
|
+
<span class="italic">No run data was found between these dates</span>
|
|
152
|
+
{% endif %}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="tab-pane" id="pre-run-data-tab">
|
|
116
156
|
<div class="col-md-12">
|
|
117
157
|
<div class="form-group pull-right extra-side-padding" style="padding-top: 10px">
|
|
118
158
|
{% button type="export" value="Export" onclick="load_usage_data('"|concat:tool_id|concat:"', 'pre-run');" %}
|
|
@@ -147,7 +187,7 @@
|
|
|
147
187
|
{% endif %}
|
|
148
188
|
</div>
|
|
149
189
|
</div>
|
|
150
|
-
<div class="tab-pane
|
|
190
|
+
<div class="tab-pane" id="post-run-data-tab">
|
|
151
191
|
<div class="col-md-12">
|
|
152
192
|
<div class="form-group pull-right extra-side-padding" style="padding-top: 10px">
|
|
153
193
|
{% button type="export" value="Export" onclick="load_usage_data('"|concat:tool_id|concat:"', 'run');" %}
|
|
@@ -7,7 +7,7 @@ from django.utils import timezone
|
|
|
7
7
|
|
|
8
8
|
from NEMO.models import Account, EmailLog, Project, Qualification, Tool, UsageEvent, User
|
|
9
9
|
from NEMO.tests.test_utilities import NEMOTestCaseMixin
|
|
10
|
-
from NEMO.views.customization import EmailsCustomization
|
|
10
|
+
from NEMO.views.customization import EmailsCustomization
|
|
11
11
|
from NEMO.views.timed_services import do_manage_tool_qualifications
|
|
12
12
|
|
|
13
13
|
|
|
@@ -45,7 +45,8 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
# Never used days set but not regular days
|
|
48
|
-
|
|
48
|
+
self.tool.qualification_expiration_never_used_days = 3
|
|
49
|
+
self.tool.save()
|
|
49
50
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
50
51
|
# Trigger the expiration timed service
|
|
51
52
|
do_manage_tool_qualifications()
|
|
@@ -59,7 +60,8 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
59
60
|
Qualification.objects.create(tool=self.tool, user=self.user, qualified_on=qualification_date)
|
|
60
61
|
|
|
61
62
|
# Expiration days are set but never used days are not
|
|
62
|
-
|
|
63
|
+
self.tool.qualification_expiration_days = 3
|
|
64
|
+
self.tool.save()
|
|
63
65
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
64
66
|
# Trigger the expiration timed service
|
|
65
67
|
do_manage_tool_qualifications()
|
|
@@ -72,8 +74,9 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
72
74
|
qualification_date = datetime.today() - timedelta(days=3)
|
|
73
75
|
Qualification.objects.create(tool=self.tool, user=self.user, qualified_on=qualification_date)
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
self.tool.qualification_expiration_days = 3
|
|
78
|
+
self.tool.qualification_expiration_never_used_days = 3
|
|
79
|
+
self.tool.save()
|
|
77
80
|
# Trigger the expiration timed service
|
|
78
81
|
do_manage_tool_qualifications()
|
|
79
82
|
# Qualification was NOT removed
|
|
@@ -84,8 +87,9 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
84
87
|
qualification_date = datetime.today() - timedelta(days=3)
|
|
85
88
|
Qualification.objects.create(tool=self.tool, user=self.user, qualified_on=qualification_date)
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
self.tool.qualification_expiration_days = 3
|
|
91
|
+
self.tool.qualification_expiration_never_used_days = 3
|
|
92
|
+
self.tool.save()
|
|
89
93
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
90
94
|
# Trigger the expiration timed service
|
|
91
95
|
do_manage_tool_qualifications()
|
|
@@ -96,15 +100,10 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
96
100
|
mock_exist.return_value = True
|
|
97
101
|
mock_open.return_value = ContentFile(b"Email template", name="template")
|
|
98
102
|
|
|
99
|
-
# Tool is exempt
|
|
100
|
-
self.tool._qualifications_never_expire = True
|
|
101
|
-
self.tool.save()
|
|
102
|
-
|
|
103
103
|
qualification_date = datetime.today() - timedelta(days=3)
|
|
104
104
|
Qualification.objects.create(tool=self.tool, user=self.user, qualified_on=qualification_date)
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
ToolCustomization.set("tool_qualification_expiration_days", 3)
|
|
106
|
+
# Tool is exempt, meaning no qualification expiration need to be added
|
|
108
107
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
109
108
|
# Trigger the expiration timed service
|
|
110
109
|
do_manage_tool_qualifications()
|
|
@@ -121,7 +120,8 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
121
120
|
user=self.user, operator=self.user, tool=self.tool, project=self.project, start=usage_date
|
|
122
121
|
)
|
|
123
122
|
|
|
124
|
-
|
|
123
|
+
self.tool.qualification_expiration_days = 3
|
|
124
|
+
self.tool.save()
|
|
125
125
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
126
126
|
# Trigger the expiration timed service
|
|
127
127
|
do_manage_tool_qualifications()
|
|
@@ -134,7 +134,8 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
134
134
|
qualification_date = datetime.today() - timedelta(days=2)
|
|
135
135
|
Qualification.objects.create(tool=self.tool, user=self.user, qualified_on=qualification_date)
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
self.tool.qualification_expiration_never_used_days = 3
|
|
138
|
+
self.tool.save()
|
|
138
139
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
139
140
|
# Trigger the expiration timed service
|
|
140
141
|
do_manage_tool_qualifications()
|
|
@@ -151,7 +152,8 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
151
152
|
user=self.user, operator=self.user, tool=self.tool, project=self.project, start=usage_date
|
|
152
153
|
)
|
|
153
154
|
|
|
154
|
-
|
|
155
|
+
self.tool.qualification_expiration_days = 3
|
|
156
|
+
self.tool.save()
|
|
155
157
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
156
158
|
# Trigger the expiration timed service
|
|
157
159
|
do_manage_tool_qualifications()
|
|
@@ -172,7 +174,8 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
172
174
|
user=self.user, operator=self.user, tool=self.tool, project=self.project, start=usage_date
|
|
173
175
|
)
|
|
174
176
|
|
|
175
|
-
|
|
177
|
+
self.tool.qualification_expiration_days = 3
|
|
178
|
+
self.tool.save()
|
|
176
179
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
177
180
|
# Trigger the expiration timed service
|
|
178
181
|
do_manage_tool_qualifications()
|
|
@@ -189,7 +192,8 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
189
192
|
qualification_date = datetime.today() - timedelta(days=3)
|
|
190
193
|
Qualification.objects.create(tool=self.tool, user=self.user, qualified_on=qualification_date)
|
|
191
194
|
|
|
192
|
-
|
|
195
|
+
self.tool.qualification_expiration_never_used_days = 3
|
|
196
|
+
self.tool.save()
|
|
193
197
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
194
198
|
# Trigger the expiration timed service
|
|
195
199
|
do_manage_tool_qualifications()
|
|
@@ -210,9 +214,10 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
210
214
|
user=self.user, operator=self.user, tool=self.tool, project=self.project, start=usage_date
|
|
211
215
|
)
|
|
212
216
|
|
|
213
|
-
|
|
217
|
+
self.tool.qualification_expiration_days = 3
|
|
218
|
+
self.tool.qualification_notification_email = "qualif_cc@example.com"
|
|
219
|
+
self.tool.save()
|
|
214
220
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
215
|
-
ToolCustomization.set("tool_qualification_cc", "qualif_cc@example.com")
|
|
216
221
|
# Set alternate email for user
|
|
217
222
|
prefs = self.user.get_preferences()
|
|
218
223
|
prefs.email_alternate = "user.alternate@example.com"
|
|
@@ -239,9 +244,10 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
239
244
|
qualification_date = datetime.today() - timedelta(days=20)
|
|
240
245
|
Qualification.objects.create(tool=self.tool, user=self.user, qualified_on=qualification_date)
|
|
241
246
|
|
|
242
|
-
|
|
247
|
+
self.tool.qualification_expiration_never_used_days = 3
|
|
248
|
+
self.tool.qualification_notification_email = "qualif_cc@example.com"
|
|
249
|
+
self.tool.save()
|
|
243
250
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
244
|
-
ToolCustomization.set("tool_qualification_cc", "qualif_cc@example.com")
|
|
245
251
|
# Set alternate email for user
|
|
246
252
|
prefs = self.user.get_preferences()
|
|
247
253
|
prefs.email_alternate = "user.alternate@example.com"
|
|
@@ -273,9 +279,10 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
273
279
|
user=self.user, operator=self.user, tool=self.tool, project=self.project, start=usage_date
|
|
274
280
|
)
|
|
275
281
|
|
|
276
|
-
ToolCustomization.set("tool_qualification_expiration_days", 3)
|
|
277
282
|
# Set reminder 2 days before expiration
|
|
278
|
-
|
|
283
|
+
self.tool.qualification_expiration_days = 3
|
|
284
|
+
self.tool.qualification_reminder_days = "3,2"
|
|
285
|
+
self.tool.save()
|
|
279
286
|
# Trigger the expiration timed service
|
|
280
287
|
do_manage_tool_qualifications()
|
|
281
288
|
# Qualification was NOT removed (3 days disqualified, but only 2 days since qualification)
|
|
@@ -292,9 +299,10 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
292
299
|
Qualification.objects.create(tool=self.tool, user=self.user, qualified_on=qualification_date)
|
|
293
300
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
294
301
|
|
|
295
|
-
ToolCustomization.set("tool_qualification_expiration_never_used_days", 3)
|
|
296
302
|
# Set reminder 2 days before expiration
|
|
297
|
-
|
|
303
|
+
self.tool.qualification_expiration_never_used_days = 3
|
|
304
|
+
self.tool.qualification_reminder_days = "3,2"
|
|
305
|
+
self.tool.save()
|
|
298
306
|
# Trigger the expiration timed service
|
|
299
307
|
do_manage_tool_qualifications()
|
|
300
308
|
# Qualification was NOT removed (3 days disqualified, but only 2 days since qualification)
|
|
@@ -315,9 +323,10 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
315
323
|
user=self.user, operator=self.user, tool=self.tool, project=self.project, start=usage_date
|
|
316
324
|
)
|
|
317
325
|
|
|
318
|
-
ToolCustomization.set("tool_qualification_expiration_days", 3)
|
|
319
326
|
# Set reminder 1 day before expiration
|
|
320
|
-
|
|
327
|
+
self.tool.qualification_expiration_days = 3
|
|
328
|
+
self.tool.qualification_reminder_days = "1"
|
|
329
|
+
self.tool.save()
|
|
321
330
|
# Trigger the expiration timed service
|
|
322
331
|
do_manage_tool_qualifications()
|
|
323
332
|
# Qualification was NOT removed (3 days disqualified, but only 2 days since qualification)
|
|
@@ -334,9 +343,10 @@ class ToolQualificationTestCase(NEMOTestCaseMixin, TestCase):
|
|
|
334
343
|
Qualification.objects.create(tool=self.tool, user=self.user, qualified_on=qualification_date)
|
|
335
344
|
EmailsCustomization.set("user_office_email_address", "user_office@example.com")
|
|
336
345
|
|
|
337
|
-
ToolCustomization.set("tool_qualification_expiration_never_used_days", 3)
|
|
338
346
|
# Set reminder 1 day before expiration
|
|
339
|
-
|
|
347
|
+
self.tool.qualification_expiration_never_used_days = 3
|
|
348
|
+
self.tool.qualification_reminder_days = "1"
|
|
349
|
+
self.tool.save()
|
|
340
350
|
# Trigger the expiration timed service
|
|
341
351
|
do_manage_tool_qualifications()
|
|
342
352
|
# Qualification was NOT removed (3 days disqualified, but only 2 days since qualification)
|
NEMO/views/api.py
CHANGED
|
@@ -413,10 +413,15 @@ class ToolViewSet(ModelViewSet):
|
|
|
413
413
|
"_category": string_filters,
|
|
414
414
|
"_operational": boolean_filters,
|
|
415
415
|
"_location": string_filters,
|
|
416
|
+
"_missed_reservation_threshold": number_filters,
|
|
417
|
+
"_late_cancellation_reservation_threshold": number_filters,
|
|
418
|
+
"_qualification_reminder_days": string_filters,
|
|
419
|
+
"_qualification_expiration_days": number_filters,
|
|
420
|
+
"_qualification_expiration_never_used_days": number_filters,
|
|
421
|
+
"_qualification_notification_email": string_filters,
|
|
416
422
|
"_requires_area_access": key_filters,
|
|
417
423
|
"_requires_area_occupancy_minimum": number_filters,
|
|
418
424
|
"_problem_shutdown_enabled": boolean_filters,
|
|
419
|
-
"_qualifications_never_expire": boolean_filters,
|
|
420
425
|
}
|
|
421
426
|
|
|
422
427
|
|
NEMO/views/calendar.py
CHANGED
|
@@ -440,7 +440,6 @@ def configuration_agenda_event_feed(request, start, end):
|
|
|
440
440
|
if item_id and not all_tools:
|
|
441
441
|
events = events.filter(**{f"{item_type.value}__id": item_id})
|
|
442
442
|
|
|
443
|
-
# TODO: Filter events that only have to do with the current user's primary, backup and superuser tools.
|
|
444
443
|
personal_schedule = request.GET.get("personal_schedule")
|
|
445
444
|
|
|
446
445
|
dictionary = {
|
|
@@ -877,8 +876,8 @@ def modify_reservation(request, current_user, start_delta, end_delta):
|
|
|
877
876
|
reservation_to_cancel.cancellation_time = now
|
|
878
877
|
reservation_to_cancel.cancelled_by = current_user
|
|
879
878
|
|
|
880
|
-
if reservation_to_cancel.start
|
|
881
|
-
# Only check if it has a different time
|
|
879
|
+
if reservation_to_cancel.start < new_reservation.start:
|
|
880
|
+
# Only check if it has a different time that is later. If moved earlier, it's fine
|
|
882
881
|
check_for_late_cancellation(current_user, reservation_to_cancel)
|
|
883
882
|
|
|
884
883
|
policy_problems, overridable = policy.check_to_save_reservation(
|
NEMO/views/customization.py
CHANGED
|
@@ -683,10 +683,6 @@ class ToolCustomization(CustomizationBase):
|
|
|
683
683
|
"tool_control_use_for_other_remote": "Use this tool for a remote project",
|
|
684
684
|
"tool_control_note_show": "",
|
|
685
685
|
"tool_control_note_copy_reservation": "",
|
|
686
|
-
"tool_qualification_reminder_days": "",
|
|
687
|
-
"tool_qualification_expiration_days": "",
|
|
688
|
-
"tool_qualification_expiration_never_used_days": "",
|
|
689
|
-
"tool_qualification_cc": "",
|
|
690
686
|
"tool_problem_max_image_size_pixels": "750",
|
|
691
687
|
"tool_problem_send_to_all_qualified_users": "",
|
|
692
688
|
"tool_problem_allow_regular_user_preferences": "",
|
|
@@ -709,17 +705,13 @@ class ToolCustomization(CustomizationBase):
|
|
|
709
705
|
if (
|
|
710
706
|
name
|
|
711
707
|
in [
|
|
712
|
-
"tool_qualification_expiration_days",
|
|
713
708
|
"tool_problem_max_image_size_pixels",
|
|
714
709
|
"tool_configuration_near_future_days",
|
|
715
710
|
]
|
|
716
711
|
and value
|
|
717
712
|
):
|
|
718
713
|
validate_integer(value)
|
|
719
|
-
if name == "
|
|
720
|
-
# Check that we have an integer or a list of integers
|
|
721
|
-
validate_comma_separated_integer_list(value)
|
|
722
|
-
elif name == "tool_qualification_cc" or name == "tool_grant_access_emails":
|
|
714
|
+
if name == "tool_grant_access_emails":
|
|
723
715
|
recipients = tuple([e for e in value.split(",") if e])
|
|
724
716
|
for email in recipients:
|
|
725
717
|
validate_email(email)
|
NEMO/views/timed_services.py
CHANGED
|
@@ -898,24 +898,17 @@ def manage_tool_qualifications(request):
|
|
|
898
898
|
|
|
899
899
|
def do_manage_tool_qualifications(request=None):
|
|
900
900
|
user_office_email = EmailsCustomization.get("user_office_email_address")
|
|
901
|
-
qualification_reminder_days = ToolCustomization.get("tool_qualification_reminder_days")
|
|
902
|
-
qualification_expiration_days = ToolCustomization.get("tool_qualification_expiration_days")
|
|
903
|
-
qualification_expiration_never_used = ToolCustomization.get("tool_qualification_expiration_never_used_days")
|
|
904
901
|
template = get_media_file_contents("tool_qualification_expiration_email.html")
|
|
905
902
|
if user_office_email and template:
|
|
906
|
-
if
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
.prefetch_related("tool", "user")
|
|
916
|
-
):
|
|
917
|
-
user = qualification.user
|
|
918
|
-
tool = qualification.tool
|
|
903
|
+
# Only applies if there is no qualification level or the qualification level has qualify_user set to True
|
|
904
|
+
for qualification in (
|
|
905
|
+
Qualification.objects.filter(user__is_active=True, user__is_staff=False)
|
|
906
|
+
.filter(Q(qualification_level__isnull=True) | Q(qualification_level__qualify_user=True))
|
|
907
|
+
.prefetch_related("tool", "user")
|
|
908
|
+
):
|
|
909
|
+
user = qualification.user
|
|
910
|
+
tool = qualification.tool
|
|
911
|
+
if tool.qualification_expiration_days or tool.qualification_expiration_never_used_days:
|
|
919
912
|
last_tool_use = None
|
|
920
913
|
try:
|
|
921
914
|
# Last tool use cannot be before the last time they qualified
|
|
@@ -924,15 +917,15 @@ def do_manage_tool_qualifications(request=None):
|
|
|
924
917
|
qualification.qualified_on,
|
|
925
918
|
)
|
|
926
919
|
expiration_date: date = (
|
|
927
|
-
last_tool_use + timedelta(days=qualification_expiration_days)
|
|
928
|
-
if qualification_expiration_days
|
|
920
|
+
last_tool_use + timedelta(days=tool.qualification_expiration_days)
|
|
921
|
+
if tool.qualification_expiration_days
|
|
929
922
|
else None
|
|
930
923
|
)
|
|
931
924
|
except UsageEvent.DoesNotExist:
|
|
932
925
|
# User never used the tool, use the qualification date
|
|
933
926
|
expiration_date: date = (
|
|
934
|
-
qualification.qualified_on + timedelta(days=
|
|
935
|
-
if
|
|
927
|
+
qualification.qualified_on + timedelta(days=tool.qualification_expiration_never_used_days)
|
|
928
|
+
if tool.qualification_expiration_never_used_days
|
|
936
929
|
else None
|
|
937
930
|
)
|
|
938
931
|
# Check for staff on tools
|
|
@@ -945,25 +938,37 @@ def do_manage_tool_qualifications(request=None):
|
|
|
945
938
|
"Qualification expired",
|
|
946
939
|
)
|
|
947
940
|
send_tool_qualification_expiring_email(
|
|
948
|
-
qualification,
|
|
941
|
+
qualification,
|
|
942
|
+
last_tool_use,
|
|
943
|
+
expiration_date,
|
|
944
|
+
tool.qualification_notification_email,
|
|
945
|
+
request=request,
|
|
949
946
|
)
|
|
950
|
-
if
|
|
951
|
-
for remaining_days in
|
|
947
|
+
if tool.get_qualification_reminder_days():
|
|
948
|
+
for remaining_days in tool.get_qualification_reminder_days():
|
|
952
949
|
if expiration_date - timedelta(days=remaining_days) == date.today():
|
|
953
950
|
send_tool_qualification_expiring_email(
|
|
954
|
-
qualification,
|
|
951
|
+
qualification,
|
|
952
|
+
last_tool_use,
|
|
953
|
+
expiration_date,
|
|
954
|
+
tool.qualification_notification_email,
|
|
955
|
+
remaining_days,
|
|
956
|
+
request=request,
|
|
955
957
|
)
|
|
956
958
|
return HttpResponse()
|
|
957
959
|
|
|
958
960
|
|
|
959
961
|
def send_tool_qualification_expiring_email(
|
|
960
|
-
qualification: Qualification,
|
|
962
|
+
qualification: Qualification,
|
|
963
|
+
last_tool_use: date,
|
|
964
|
+
expiration_date: date,
|
|
965
|
+
cc_email: List[str],
|
|
966
|
+
remaining_days: int = None,
|
|
967
|
+
request=None,
|
|
961
968
|
):
|
|
962
969
|
user_office_email = EmailsCustomization.get("user_office_email_address")
|
|
963
970
|
template = get_media_file_contents("tool_qualification_expiration_email.html")
|
|
964
971
|
# Add extra cc emails
|
|
965
|
-
tool_qualification_cc = ToolCustomization.get("tool_qualification_cc")
|
|
966
|
-
ccs = [e for e in tool_qualification_cc.split(",") if e]
|
|
967
972
|
if remaining_days:
|
|
968
973
|
subject_expiration = f" expires in {remaining_days} days!"
|
|
969
974
|
else:
|
|
@@ -980,7 +985,11 @@ def send_tool_qualification_expiring_email(
|
|
|
980
985
|
message = render_email_template(template, dictionary, request)
|
|
981
986
|
email_notification = qualification.user.get_preferences().email_send_tool_qualification_expiration_emails
|
|
982
987
|
qualification.user.email_user(
|
|
983
|
-
subject=subject,
|
|
988
|
+
subject=subject,
|
|
989
|
+
message=message,
|
|
990
|
+
from_email=user_office_email,
|
|
991
|
+
cc=cc_email,
|
|
992
|
+
email_notification=email_notification,
|
|
984
993
|
)
|
|
985
994
|
|
|
986
995
|
|
NEMO/views/tool_control.py
CHANGED
|
@@ -41,6 +41,7 @@ from NEMO.models import (
|
|
|
41
41
|
ToolUsageQuestionType,
|
|
42
42
|
ToolUsageQuestions,
|
|
43
43
|
ToolWaitList,
|
|
44
|
+
TrainingEvent,
|
|
44
45
|
UsageEvent,
|
|
45
46
|
User,
|
|
46
47
|
)
|
|
@@ -149,9 +150,6 @@ def tool_status(request, tool_id):
|
|
|
149
150
|
or (user_is_qualified and broadcast_upcoming_reservation == "qualified")
|
|
150
151
|
or broadcast_upcoming_reservation == "all",
|
|
151
152
|
"tool_control_show_task_details": ToolCustomization.get_bool("tool_control_show_task_details"),
|
|
152
|
-
"show_usage_data_tab": ToolUsageQuestions.objects.filter(enabled=True)
|
|
153
|
-
.filter(Q(only_for_tools=None) | Q(only_for_tools__in=[tool_id]))
|
|
154
|
-
.exists(),
|
|
155
153
|
"user_can_see_documents": user.is_any_part_of_staff
|
|
156
154
|
or not ToolCustomization.get_bool("tool_control_show_documents_only_qualified_users")
|
|
157
155
|
or user_is_qualified,
|
|
@@ -184,7 +182,8 @@ def tool_status(request, tool_id):
|
|
|
184
182
|
if ToolCustomization.get_bool("tool_control_note_copy_reservation"):
|
|
185
183
|
dictionary["reservation_note"] = current_reservation.note
|
|
186
184
|
|
|
187
|
-
dictionary["next_reservation"] =
|
|
185
|
+
dictionary["next_reservation"] = None
|
|
186
|
+
next_reservation = (
|
|
188
187
|
Reservation.objects.filter(
|
|
189
188
|
start__gt=timezone.now(),
|
|
190
189
|
cancelled=False,
|
|
@@ -195,6 +194,15 @@ def tool_status(request, tool_id):
|
|
|
195
194
|
.order_by("start")
|
|
196
195
|
.first()
|
|
197
196
|
)
|
|
197
|
+
next_training = (
|
|
198
|
+
TrainingEvent.objects.filter(start__gt=timezone.now(), tool=tool, cancelled=False).order_by("start").first()
|
|
199
|
+
)
|
|
200
|
+
if next_reservation:
|
|
201
|
+
dictionary["next_reservation"] = next_reservation
|
|
202
|
+
if next_training:
|
|
203
|
+
if not next_reservation or next_reservation.start > next_training.start:
|
|
204
|
+
next_training.user = next_training.trainer # for it to work with email link to user
|
|
205
|
+
dictionary["next_reservation"] = next_training
|
|
198
206
|
|
|
199
207
|
# Staff need the user list to be able to qualify users for the tool.
|
|
200
208
|
if user.is_staff_on_tool(tool):
|
|
@@ -254,7 +262,7 @@ def usage_data_history(request, tool_id):
|
|
|
254
262
|
if not last and not start and not end:
|
|
255
263
|
# Default to last 25 records
|
|
256
264
|
last = 25
|
|
257
|
-
usage_events = UsageEvent.objects.filter(
|
|
265
|
+
usage_events = UsageEvent.objects.filter(tool_id__in=Tool.objects.get(pk=tool_id).get_family_tool_ids())
|
|
258
266
|
|
|
259
267
|
if start:
|
|
260
268
|
usage_events = usage_events.filter(end__gte=start)
|
|
@@ -266,6 +274,7 @@ def usage_data_history(request, tool_id):
|
|
|
266
274
|
except ValueError:
|
|
267
275
|
pass
|
|
268
276
|
|
|
277
|
+
all_usage_events = usage_events.order_by("-end")
|
|
269
278
|
pre_usage_events = usage_events.order_by("-start")
|
|
270
279
|
post_usage_events = usage_events.filter(end__isnull=False).order_by("-end")
|
|
271
280
|
if last:
|
|
@@ -274,22 +283,34 @@ def usage_data_history(request, tool_id):
|
|
|
274
283
|
last = int(last)
|
|
275
284
|
except ValueError:
|
|
276
285
|
last = 25
|
|
286
|
+
all_usage_events = all_usage_events[:last]
|
|
277
287
|
pre_usage_events = pre_usage_events[:last]
|
|
278
288
|
post_usage_events = post_usage_events[:last]
|
|
279
289
|
|
|
290
|
+
table_data = BasicDisplayTable()
|
|
291
|
+
table_data.add_header(("user", "User"))
|
|
292
|
+
table_data.add_header(("operator", "Operator"))
|
|
293
|
+
if show_project_info:
|
|
294
|
+
table_data.add_header(("project", "Project"))
|
|
295
|
+
table_data.add_header(("start_date", "Start date"))
|
|
296
|
+
table_data.add_header(("end_date", "End date"))
|
|
297
|
+
|
|
280
298
|
table_pre_run_data = BasicDisplayTable()
|
|
281
299
|
table_pre_run_data.add_header(("user", "User"))
|
|
282
300
|
table_pre_run_data.add_header(("operator", "Operator"))
|
|
283
301
|
if show_project_info:
|
|
284
302
|
table_pre_run_data.add_header(("project", "Project"))
|
|
285
|
-
table_pre_run_data.add_header(("
|
|
303
|
+
table_pre_run_data.add_header(("start_date", "Start date"))
|
|
286
304
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
305
|
+
table_post_run_data = BasicDisplayTable()
|
|
306
|
+
table_post_run_data.add_header(("user", "User"))
|
|
307
|
+
table_post_run_data.add_header(("operator", "Operator"))
|
|
290
308
|
if show_project_info:
|
|
291
|
-
|
|
292
|
-
|
|
309
|
+
table_post_run_data.add_header(("project", "Project"))
|
|
310
|
+
table_post_run_data.add_header(("end_date", "End date"))
|
|
311
|
+
|
|
312
|
+
for usage_event in all_usage_events:
|
|
313
|
+
format_usage_data(table_data, usage_event, None, show_project_info, csv_export, True)
|
|
293
314
|
|
|
294
315
|
for usage_event in pre_usage_events:
|
|
295
316
|
if usage_event.pre_run_data:
|
|
@@ -297,20 +318,21 @@ def usage_data_history(request, tool_id):
|
|
|
297
318
|
table_pre_run_data,
|
|
298
319
|
usage_event,
|
|
299
320
|
usage_event.pre_run_data,
|
|
300
|
-
usage_event.start,
|
|
301
321
|
show_project_info,
|
|
302
322
|
csv_export,
|
|
303
323
|
)
|
|
304
324
|
|
|
305
325
|
for usage_event in post_usage_events:
|
|
306
326
|
if usage_event.run_data:
|
|
307
|
-
format_usage_data(
|
|
308
|
-
table_run_data, usage_event, usage_event.run_data, usage_event.end, show_project_info, csv_export
|
|
309
|
-
)
|
|
327
|
+
format_usage_data(table_post_run_data, usage_event, usage_event.run_data, show_project_info, csv_export)
|
|
310
328
|
|
|
311
329
|
if csv_export:
|
|
312
|
-
response =
|
|
313
|
-
|
|
330
|
+
response = (
|
|
331
|
+
table_data.to_csv()
|
|
332
|
+
if csv_export == "all"
|
|
333
|
+
else table_post_run_data.to_csv() if csv_export == "run" else table_pre_run_data.to_csv()
|
|
334
|
+
)
|
|
335
|
+
filename = f"tool{'' if csv_export == 'all' else 'post' if csv_export == 'run' else '_pre'}_usage_data_export_{export_format_datetime()}.csv"
|
|
314
336
|
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
315
337
|
return response
|
|
316
338
|
else:
|
|
@@ -319,7 +341,8 @@ def usage_data_history(request, tool_id):
|
|
|
319
341
|
"data_history_start": start.date() if start else None,
|
|
320
342
|
"data_history_end": end.date() if end else None,
|
|
321
343
|
"data_history_last": str(last),
|
|
322
|
-
"
|
|
344
|
+
"data_table": table_data,
|
|
345
|
+
"run_data_table": table_post_run_data,
|
|
323
346
|
"pre_run_data_table": table_pre_run_data,
|
|
324
347
|
"data_history_user": User.objects.get(id=user_id) if user_id else None,
|
|
325
348
|
"show_project_info": show_project_info or False,
|
|
@@ -899,17 +922,16 @@ def format_usage_data(
|
|
|
899
922
|
table_result: BasicDisplayTable,
|
|
900
923
|
usage_event: UsageEvent,
|
|
901
924
|
usage_run_data: str,
|
|
902
|
-
date_field: datetime,
|
|
903
925
|
show_project_info: str,
|
|
904
926
|
csv_export: str,
|
|
927
|
+
all_data: bool = False,
|
|
905
928
|
):
|
|
906
929
|
usage_data = {}
|
|
907
|
-
date_data = format_datetime(date_field, "SHORT_DATETIME_FORMAT")
|
|
908
930
|
|
|
909
931
|
try:
|
|
910
932
|
user_data = f"{usage_event.user.first_name} {usage_event.user.last_name}"
|
|
911
933
|
operator_data = f"{usage_event.operator.first_name} {usage_event.operator.last_name}"
|
|
912
|
-
run_data: Dict = loads(usage_run_data)
|
|
934
|
+
run_data: Dict = loads(usage_run_data) if usage_run_data else {}
|
|
913
935
|
for question_key, question in run_data.items():
|
|
914
936
|
if "user_input" in question and not question.get("readonly", False):
|
|
915
937
|
if question["type"] == "group":
|
|
@@ -945,7 +967,10 @@ def format_usage_data(
|
|
|
945
967
|
if group_usage_data:
|
|
946
968
|
group_usage_data["user"] = user_data
|
|
947
969
|
group_usage_data["operator"] = operator_data
|
|
948
|
-
group_usage_data["
|
|
970
|
+
group_usage_data["start_date"] = format_datetime(
|
|
971
|
+
usage_event.start, "SHORT_DATETIME_FORMAT"
|
|
972
|
+
)
|
|
973
|
+
group_usage_data["end_date"] = format_datetime(usage_event.end, "SHORT_DATETIME_FORMAT")
|
|
949
974
|
if show_project_info:
|
|
950
975
|
group_usage_data["project"] = usage_event.project.name
|
|
951
976
|
table_result.add_row(group_usage_data)
|
|
@@ -955,10 +980,13 @@ def format_usage_data(
|
|
|
955
980
|
usage_data[question_key] = (
|
|
956
981
|
table_result.formatted_value(question["user_input"]) + suffix if question["user_input"] else ""
|
|
957
982
|
)
|
|
958
|
-
if usage_data:
|
|
983
|
+
if usage_data or all_data:
|
|
959
984
|
usage_data["user"] = user_data
|
|
960
985
|
usage_data["operator"] = operator_data
|
|
961
|
-
usage_data["
|
|
986
|
+
usage_data["start_date"] = format_datetime(usage_event.start, "SHORT_DATETIME_FORMAT")
|
|
987
|
+
usage_data["end_date"] = (
|
|
988
|
+
format_datetime(usage_event.end, "SHORT_DATETIME_FORMAT") if usage_event.end else ""
|
|
989
|
+
)
|
|
962
990
|
if show_project_info:
|
|
963
991
|
usage_data["project"] = usage_event.project.name
|
|
964
992
|
table_result.add_row(usage_data)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: NEMO-CE
|
|
3
|
-
Version: 7.3.
|
|
3
|
+
Version: 7.3.8
|
|
4
4
|
Summary: NEMO Community Edition is a laboratory logistics web application based of NEMO. Use it to schedule reservations, control tool access, track maintenance issues, and more.
|
|
5
5
|
Author-email: Atlantis Labs LLC <atlantis@atlantislabs.io>
|
|
6
6
|
License: MIT License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
NEMO/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
NEMO/actions.py,sha256=CXWeXVdqqANcBANM-gBS1tuQmL1-eTItfsc4XBdWlZ4,14297
|
|
3
|
-
NEMO/admin.py,sha256=
|
|
3
|
+
NEMO/admin.py,sha256=6WEEMIva5e-E8FRcRbKo4rv9OQoSoxN5D-xWdpfO_Sk,87619
|
|
4
4
|
NEMO/constants.py,sha256=wwg5OX_uQtZjauK7puUJEhrpNHWGDexBsQqNF1v3vUE,662
|
|
5
5
|
NEMO/context_processors.py,sha256=iVqQa7ZOLW65jjP75IbiC2NPeolgCm_0eKABHpsz4ts,6712
|
|
6
6
|
NEMO/decorators.py,sha256=vBl836p-AMmZEV_k1QYCReUMC7-C_G2-BNpqCjcK_eI,7428
|
|
@@ -14,7 +14,7 @@ NEMO/middleware.py,sha256=JqLnixd23Elt-XMQ5X-DoKVaSsddpMwOcf1uPWaukxQ,6339
|
|
|
14
14
|
NEMO/migrations_utils.py,sha256=rEQTYq13rPbjRxWYeJMxt324L7SAaQrfXBB180-ZIBI,2755
|
|
15
15
|
NEMO/mixins.py,sha256=f5QMQ3OaCxSgHWa2vWZxRIx_Vk35zMekLMXAS6HI1RU,20114
|
|
16
16
|
NEMO/model_tree.py,sha256=Nj6u-5SkzbYQ7q0snyPt3OokH08WIUO6L-ivk8FjUHM,4632
|
|
17
|
-
NEMO/models.py,sha256=
|
|
17
|
+
NEMO/models.py,sha256=Z90C6tI3evrQY97mxXCqzb1UINXVKjCWbeKI6HGw4s0,300481
|
|
18
18
|
NEMO/parsers.py,sha256=FkKlN3_3DJ9NE6-ezSzdaFl5Qvj9b4mCxBaf6Xboyow,1707
|
|
19
19
|
NEMO/permissions.py,sha256=tq3dalidpxICq8zQdSyHoXKlsZa_DRdP6nJ-WnlzNKI,949
|
|
20
20
|
NEMO/policy.py,sha256=An893hR6S8-zBhTVUwOii09lPbUfO1kWRPhZAoiuuZ8,79917
|
|
@@ -216,6 +216,7 @@ NEMO/migrations/0137_area_nemo_area_tree_id_lft_idx.py,sha256=SGGh2K8TunyMzIRnl9
|
|
|
216
216
|
NEMO/migrations/0138_area_late_cancellation_reservation_threshold_and_more.py,sha256=snXfdPumUpInH8A5hKBbEZy6azcjdDdH1OBFBZ82w_w,1438
|
|
217
217
|
NEMO/migrations/0139_unplannedoutage.py,sha256=nfXnrZzXPWne-hvsYtyTWpL-oDaVQRyrBVvKrkYa03w,828
|
|
218
218
|
NEMO/migrations/0140_alter_user_options.py,sha256=_sxUgIG0h5HgrMpJ_HWZ412TWqiSOmG8A_ObY3yXoHk,768
|
|
219
|
+
NEMO/migrations/0141_toolqualificationexpiration.py,sha256=qFh_oEjtzGnUHSqGsoDFlDESPkYUII07GbzF73Jlw4w,4843
|
|
219
220
|
NEMO/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
220
221
|
NEMO/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
221
222
|
NEMO/plugins/utils.py,sha256=t-CJXiqjHbV16QYm7Y21RzIMBAfOaCgZ5s1J6ruFuOY,6054
|
|
@@ -358,7 +359,7 @@ NEMO/templates/customizations/customizations_requests.html,sha256=2XQpylF2TdweTi
|
|
|
358
359
|
NEMO/templates/customizations/customizations_safety.html,sha256=VXIbFwXcndqGNqvLTWgmhAzPfq6p7ZNGXZYmAfQ6cFs,5071
|
|
359
360
|
NEMO/templates/customizations/customizations_shadowing_verification.html,sha256=CecVbJIFSBibyywVIJQQO76Hpj6DEsbi1xD_O14xH9M,3619
|
|
360
361
|
NEMO/templates/customizations/customizations_templates.html,sha256=MHC1jI_0iVkbFvgWu-G-zNtoPuug4IX9p17rIPzUMys,39079
|
|
361
|
-
NEMO/templates/customizations/customizations_tool.html,sha256
|
|
362
|
+
NEMO/templates/customizations/customizations_tool.html,sha256=-ntTV8sY_RbjyXG3NEloRosMV397vUAabZDy8lB7c4A,34473
|
|
362
363
|
NEMO/templates/customizations/customizations_training.html,sha256=-dyuOvvKmx7uwCtknarrTCWm4hqfcIhMEPVB1vWEFbE,17629
|
|
363
364
|
NEMO/templates/customizations/customizations_upload.html,sha256=OR_bKCQaBiE1Pg6kL_uLMt0I160g2clKmRPjKABZcBg,1866
|
|
364
365
|
NEMO/templates/customizations/customizations_user.html,sha256=9tncqwlww01ko1H8ad3G-ptj0fsSwqnh0DRCTGU7eFc,15097
|
|
@@ -461,8 +462,8 @@ NEMO/templates/tool_control/logout_user.html,sha256=UCII2yaDkBFqC-9tyBo8QiqRiZNV
|
|
|
461
462
|
NEMO/templates/tool_control/past_tasks_and_comments.html,sha256=NnerYy8BKMqdvsz8PJuWuU4VN6zJfqARHm35gnwWeCM,5175
|
|
462
463
|
NEMO/templates/tool_control/qualified_users.html,sha256=ZPmdePYF0aW_oJO44woEb_PRlXfOB0V4PMJVub79b6g,5974
|
|
463
464
|
NEMO/templates/tool_control/tool_control.html,sha256=mAtFwqVJFzLcKgUcmKtPtvcjfpjFX6YiXRTrFSQacrc,19558
|
|
464
|
-
NEMO/templates/tool_control/tool_status.html,sha256=
|
|
465
|
-
NEMO/templates/tool_control/usage_data.html,sha256=
|
|
465
|
+
NEMO/templates/tool_control/tool_status.html,sha256=lB1FJcuGOYkPHO3cmmoSaODbJBjB2gqCKMexoFVMFzI,76934
|
|
466
|
+
NEMO/templates/tool_control/usage_data.html,sha256=gogSex9YAP7jE32Qeb1AyD4NJeLaSEFd6cVQuy7KVGA,12680
|
|
466
467
|
NEMO/templates/tool_control/use_tool_for_other.html,sha256=JqZM4rpy7UE_zuk8w42ShY3o7PPMd3LcW_L0RelNChk,430
|
|
467
468
|
NEMO/templates/tool_credentials/tool_credentials.html,sha256=nPZsZZJmRFTXMeE_IxW-M8_zYNPHaxzsCXXxMULrN9c,3551
|
|
468
469
|
NEMO/templates/training/get_projects.html,sha256=Q64ODiogehjiCJw-BWlheFXiGzEgojyTq_bA2F2mS8U,557
|
|
@@ -532,7 +533,7 @@ NEMO/tests/test_requests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
532
533
|
NEMO/tests/test_requests/test_adjustment_requests.py,sha256=nNycsuvDt3aYACTthQizB0aca9nJTMRV6kZu8T4v-gc,12533
|
|
533
534
|
NEMO/tests/test_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
534
535
|
NEMO/tests/test_tools/test_tool.py,sha256=tbF9XhEnqhVZWFiPNRToDRhARhXMcavaHr1KEtUCy6Q,18011
|
|
535
|
-
NEMO/tests/test_tools/test_tool_qualification_expiration.py,sha256=
|
|
536
|
+
NEMO/tests/test_tools/test_tool_qualification_expiration.py,sha256=rwfFCuqXh9hVC_7ymsfDlsVMLa-51pCAn3EzGA4mL0k,18846
|
|
536
537
|
NEMO/tests/test_tools/test_waitlist.py,sha256=FOkiAtjtmn8FYwZ4PZErGBFHdLraN7ZNsc375hgN8ao,35510
|
|
537
538
|
NEMO/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
538
539
|
NEMO/views/abuse.py,sha256=S53PaFKbBAhJkv82SU63Nat1h0YBjUZ4oPhK9LqP7VY,5811
|
|
@@ -541,18 +542,18 @@ NEMO/views/accounts_and_projects.py,sha256=2qgAfK6BRMI4Db_rWkSd8O8y7LZxTeMqNdveX
|
|
|
541
542
|
NEMO/views/adjustment_requests.py,sha256=du-vsgjapNjbQxaFewUZYEsOOBx-u2DtnP9E6OLrkTA,29021
|
|
542
543
|
NEMO/views/admin_autocomplete.py,sha256=vqAiQrg381-ga9a3XF9zrc5yCdP_SLG0L_cd1Ikfjz4,1601
|
|
543
544
|
NEMO/views/alerts.py,sha256=zKUFwn1ZnRl4AP8Bcg3A6KF6eSlPoOTvVcchA06R9yk,2004
|
|
544
|
-
NEMO/views/api.py,sha256=
|
|
545
|
+
NEMO/views/api.py,sha256=XbZHdNsWmDpPiuPN_y37lNbRZjc-w8jqxcOdHXdBULc,45833
|
|
545
546
|
NEMO/views/api_billing.py,sha256=txdVWKtrT-ghq5KG-ODdjdEtbm_bdCIL-ZunltGUuFw,16415
|
|
546
547
|
NEMO/views/area_access.py,sha256=iymFELXlRbwGhaYx-vLiTvfsuRQafhDk1QafhqBieEY,22538
|
|
547
548
|
NEMO/views/authentication.py,sha256=pnsBH84EcgnkwhscFleX_yIzTvzg9CIiUsX13d1r8Lc,15838
|
|
548
549
|
NEMO/views/buddy_requests.py,sha256=8BbQUdsHRp2kIoE5FdeY5ybH04VUBm-XHK6WonflnfA,7305
|
|
549
|
-
NEMO/views/calendar.py,sha256=
|
|
550
|
+
NEMO/views/calendar.py,sha256=846CPZshOS4TbuYZLAxt0QOQY_5RHz0oZjxZHn0fmdc,72559
|
|
550
551
|
NEMO/views/charge_validation.py,sha256=aJ1UfkFGQqyxV3GkZt8503NzOPEloyIKTylOxe0pRFY,4078
|
|
551
552
|
NEMO/views/configuration_agenda.py,sha256=JMW3svUkkGrov36WsbSLn4EEY5IPIon9tMAsRI3C2iw,3168
|
|
552
553
|
NEMO/views/constants.py,sha256=jhBysGdFauZE_ByAdqv9jH3s-6PZSncAacqh--0fKRE,97
|
|
553
554
|
NEMO/views/consumables.py,sha256=utVY-oKTi1YwgQElHgDZMuVahb3ZetdxNx_J-7jFLxM,14367
|
|
554
555
|
NEMO/views/contact_staff.py,sha256=_h2pvWsvlacmR1I-BmEXHlqr9Ms0PWcNn8Qe5I90UCY,488
|
|
555
|
-
NEMO/views/customization.py,sha256=
|
|
556
|
+
NEMO/views/customization.py,sha256=RIoUneMvgTLNWZTZ1Pdod1TiCut5uXarwiyRSVcwfcA,41807
|
|
556
557
|
NEMO/views/documents.py,sha256=JmDR6ImlNstjddVuMGuADEJS2cgracinD8IX0xIrfYY,3763
|
|
557
558
|
NEMO/views/email.py,sha256=-gJCDiX9uEULD33dpDOvLA38HNnOhp-gquPZzyJQ57c,17795
|
|
558
559
|
NEMO/views/event_details.py,sha256=b_xH-JA7uNXt5n9vVl2AUzEPUkBDn60QcirWaH8ByTM,2786
|
|
@@ -576,8 +577,8 @@ NEMO/views/sidebar.py,sha256=SH8--3cpjgThM2WfsiYJ3VzRgM12RKXfrUkcQG5QeP4,980
|
|
|
576
577
|
NEMO/views/staff_assistance_requests.py,sha256=zHBfH5XxYCJLfC9i30v7naMsC_zWFg4ZSP7KlZuTSRE,9231
|
|
577
578
|
NEMO/views/status_dashboard.py,sha256=Wv1yu4qiKLeHFcMhNASDGEKvj14jsJ3Jf5vJlQGtqgk,24990
|
|
578
579
|
NEMO/views/tasks.py,sha256=Vh3ipdSj40LRQD4_Al52C6HW4TvjMor_dDRmkzGBhCc,20447
|
|
579
|
-
NEMO/views/timed_services.py,sha256=
|
|
580
|
-
NEMO/views/tool_control.py,sha256=
|
|
580
|
+
NEMO/views/timed_services.py,sha256=exq83UVulBNGLIaW9E2xE2w_yF94ed3kPmqS_aLhdFk,61102
|
|
581
|
+
NEMO/views/tool_control.py,sha256=YP11yCM8Kha2egMtl9B7v8A37kK9CqsuVlUFyctjeAU,42681
|
|
581
582
|
NEMO/views/tool_credentials.py,sha256=2EYibZCuJ1rj8b0ubNv0P9SBhprWJCg403MVStO7n3w,2086
|
|
582
583
|
NEMO/views/training.py,sha256=Mnoea9ZFk7BNvOB2bYoQTAFO5G5yaz51FBOcoToRt-c,8992
|
|
583
584
|
NEMO/views/training_new.py,sha256=jPQewiKEoTIpRTOjoh1QXfMJaG9MglOnvKvL_Pn25lQ,52212
|
|
@@ -589,9 +590,9 @@ NEMO/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
589
590
|
NEMO/widgets/configuration_editor.py,sha256=aWgg6rYrgSZcPSURU0Fs48nPSBncXlRyGVPqvx89PaA,4496
|
|
590
591
|
NEMO/widgets/dynamic_form.py,sha256=GnXPEc9NMc6hRLGzR3DK3cglXX0zPyLwsfWXQ01CdhM,59705
|
|
591
592
|
NEMO/widgets/item_tree.py,sha256=tl1d0EgwVXFQQhCmZvcjB_1qmla9qvGxyWBLwWpgxfU,9964
|
|
592
|
-
nemo_ce-7.3.
|
|
593
|
-
nemo_ce-7.3.
|
|
594
|
-
nemo_ce-7.3.
|
|
595
|
-
nemo_ce-7.3.
|
|
596
|
-
nemo_ce-7.3.
|
|
597
|
-
nemo_ce-7.3.
|
|
593
|
+
nemo_ce-7.3.8.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
|
|
594
|
+
nemo_ce-7.3.8.dist-info/METADATA,sha256=DSfcSORVj7p46BZ2CbUKZ2ZrtGX6utK1NHJNQSpHwUw,3804
|
|
595
|
+
nemo_ce-7.3.8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
596
|
+
nemo_ce-7.3.8.dist-info/entry_points.txt,sha256=ESVtd5EFgPSKwlEZ92GSowvz-wM7K3f11Xr15Qk2a40,55
|
|
597
|
+
nemo_ce-7.3.8.dist-info/top_level.txt,sha256=PraGZBuSHU5e2U69ztvBNqTyC7zHCNCbjF3rpqqBu0o,5
|
|
598
|
+
nemo_ce-7.3.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|