Skip to content

Commit a0dd264

Browse files
authored
Merge pull request #307 from usnistgov/7.3.4.dev
7.3.4.dev
2 parents e6ccdc3 + 3166364 commit a0dd264

File tree

13 files changed

+371
-205
lines changed

13 files changed

+371
-205
lines changed

NEMO/admin.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,6 @@ class ToolAdmin(admin.ModelAdmin):
247247
"_category",
248248
"_operation_mode",
249249
"qualified_users",
250-
"_qualifications_never_expire",
251250
"_problem_shutdown_enabled",
252251
)
253252
},
@@ -300,6 +299,17 @@ class ToolAdmin(admin.ModelAdmin):
300299
)
301300
},
302301
),
302+
(
303+
"Qualification expiration",
304+
{
305+
"fields": (
306+
"_qualification_reminder_days",
307+
"_qualification_expiration_days",
308+
"_qualification_expiration_never_used_days",
309+
"_qualification_notification_email",
310+
)
311+
},
312+
),
303313
(
304314
"Area Access",
305315
{
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,10 +1310,32 @@ class OperationMode(object):
13101310
_interlock = models.OneToOneField(
13111311
"Interlock", db_column="interlock_id", blank=True, null=True, on_delete=models.SET_NULL
13121312
)
1313-
_qualifications_never_expire = models.BooleanField(
1314-
default=False,
1315-
db_column="qualifications_never_expire",
1316-
help_text="Check this box if qualifications for this tool should never expire (even if the tool qualification expiration feature is enabled).",
1313+
# Qualification expiration fields
1314+
_qualification_reminder_days = models.CharField(
1315+
db_column="qualification_reminder_days",
1316+
null=True,
1317+
blank=True,
1318+
max_length=CHAR_FIELD_MEDIUM_LENGTH,
1319+
validators=[validate_comma_separated_integer_list],
1320+
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.",
1321+
)
1322+
_qualification_expiration_days = models.PositiveIntegerField(
1323+
db_column="qualification_expiration_days",
1324+
null=True,
1325+
blank=True,
1326+
help_text="The number of days from the user’s last tool use until the qualification expires.",
1327+
)
1328+
_qualification_expiration_never_used_days = models.PositiveIntegerField(
1329+
db_column="qualification_expiration_never_used_days",
1330+
null=True,
1331+
blank=True,
1332+
help_text="Number of days from the user's first qualification until the qualification expires (if the user never used the tool).",
1333+
)
1334+
_qualification_notification_email = fields.MultiEmailField(
1335+
db_column="qualification_notification_email",
1336+
null=True,
1337+
blank=True,
1338+
help_text="The email addresses to cc on tool qualification expiration and on reminders. Separate multiple emails with commas.",
13171339
)
13181340
# Policy fields:
13191341
_requires_area_access = TreeForeignKey(
@@ -1459,17 +1481,6 @@ def category(self, value):
14591481
self.raise_setter_error_if_child_tool("category")
14601482
self._category = value
14611483

1462-
@property
1463-
def qualifications_never_expire(self):
1464-
return (
1465-
self.parent_tool.qualifications_never_expire if self.is_child_tool() else self._qualifications_never_expire
1466-
)
1467-
1468-
@qualifications_never_expire.setter
1469-
def qualifications_never_expire(self, value):
1470-
self.raise_setter_error_if_child_tool("qualifications_never_expire")
1471-
self._qualifications_never_expire = value
1472-
14731484
@property
14741485
def description(self):
14751486
return self.parent_tool.description if self.is_child_tool() else self._description
@@ -1618,6 +1629,56 @@ def interlock(self, value):
16181629
self.raise_setter_error_if_child_tool("interlock")
16191630
self._interlock = value
16201631

1632+
@property
1633+
def qualification_reminder_days(self):
1634+
return (
1635+
self.parent_tool.qualification_reminder_days if self.is_child_tool() else self._qualification_reminder_days
1636+
)
1637+
1638+
@qualification_reminder_days.setter
1639+
def qualification_reminder_days(self, value):
1640+
self.raise_setter_error_if_child_tool("qualification_reminder_days")
1641+
self._qualification_reminder_days = value
1642+
1643+
@property
1644+
def qualification_expiration_days(self):
1645+
return (
1646+
self.parent_tool.qualification_expiration_days
1647+
if self.is_child_tool()
1648+
else self._qualification_expiration_days
1649+
)
1650+
1651+
@qualification_expiration_days.setter
1652+
def qualification_expiration_days(self, value):
1653+
self.raise_setter_error_if_child_tool("qualification_expiration_days")
1654+
self._qualification_expiration_days = value
1655+
1656+
@property
1657+
def qualification_expiration_never_used_days(self):
1658+
return (
1659+
self.parent_tool.qualification_expiration_never_used_days
1660+
if self.is_child_tool()
1661+
else self._qualification_expiration_never_used_days
1662+
)
1663+
1664+
@qualification_expiration_never_used_days.setter
1665+
def qualification_expiration_never_used_days(self, value):
1666+
self.raise_setter_error_if_child_tool("qualification_expiration_never_used_days")
1667+
self._qualification_expiration_never_used_days = value
1668+
1669+
@property
1670+
def qualification_notification_email(self):
1671+
return (
1672+
self.parent_tool.qualification_notification_email
1673+
if self.is_child_tool()
1674+
else self._qualification_notification_email
1675+
)
1676+
1677+
@qualification_notification_email.setter
1678+
def qualification_notification_email(self, value):
1679+
self.raise_setter_error_if_child_tool("qualification_notification_email")
1680+
self._qualification_notification_email = value
1681+
16211682
@property
16221683
def requires_area_access(self):
16231684
return self.parent_tool.requires_area_access if self.is_child_tool() else self._requires_area_access
@@ -2194,6 +2255,11 @@ def get_usage_questions(
21942255
else:
21952256
raise ValueError(f"A {'project' if user else 'user'} must be provided for usage questions")
21962257

2258+
def get_qualification_reminder_days(self) -> List[int]:
2259+
if not self.qualification_reminder_days:
2260+
return []
2261+
return [int(days) for days in self.qualification_reminder_days.split(",") if days]
2262+
21972263
def clean(self):
21982264
errors = {}
21992265
if self.parent_tool_id:

NEMO/templates/customizations/customizations_tool.html

Lines changed: 5 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -541,97 +541,14 @@ <h3 class="customization-section-title">Tool wait list</h3>
541541
</div>
542542
</div>
543543
<div class="customization-separation" style="margin-bottom: 15px"></div>
544-
<h3 class="customization-section-title">Tool qualification expiration</h3>
545-
<p>
546-
If active, this feature will remove tool qualification from a user if the user has not used the tool after a while or never used it since qualified (configured separately).
547-
<p>
548-
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.
549-
</p>
550-
<br />
551-
<div class="form-group {% if errors.tool_qualification_reminder_days %}has-error{% endif %}">
552-
<label class="control-label col-md-3" for="tool_qualification_reminder_days">Reminder days</label>
553-
<div class="col-md-5">
554-
<input type="text"
555-
id="tool_qualification_reminder_days"
556-
name="tool_qualification_reminder_days"
557-
class="form-control"
558-
value="{% if errors.tool_qualification_reminder_days %}{{ errors.tool_qualification_reminder_days.value }}{% else %}{{ tool_qualification_reminder_days }}{% endif %}" />
559-
</div>
560-
<div class="col-md-offset-3 col-md-9 help-block light-grey">
561-
{% if errors.tool_qualification_reminder_days %}
562-
{{ errors.tool_qualification_reminder_days.error }}
563-
{% else %}
564-
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.
565-
{% endif %}
566-
</div>
567-
</div>
568-
<div class="form-group {% if errors.tool_qualification_expiration_days %}has-error{% endif %}">
569-
<label class="control-label col-md-3" for="tool_qualification_expiration_days">
570-
Expiration days (previous tool usage)
571-
</label>
572-
<div class="col-md-5">
573-
<input type="number"
574-
step="1"
575-
id="tool_qualification_expiration_days"
576-
name="tool_qualification_expiration_days"
577-
class="form-control"
578-
value="{% if errors.tool_qualification_expiration_days %}{{ errors.tool_qualification_expiration_days.value }}{% else %}{{ tool_qualification_expiration_days }}{% endif %}" />
579-
</div>
580-
<div class="col-md-offset-3 col-md-9 help-block light-grey">
581-
{% if errors.tool_qualification_expiration_days %}
582-
{{ errors.tool_qualification_expiration_days.error }}
583-
{% else %}
584-
The number of days before the user's tool qualification expires since the user last used the tool.
585-
{% endif %}
586-
</div>
587-
</div>
588-
<div class="form-group {% if errors.tool_qualification_expiration_never_used_days %}has-error{% endif %}">
589-
<label class="control-label col-md-3" for="tool_qualification_expiration_never_used_days">
590-
Expiration days (no tool usage)
591-
</label>
592-
<div class="col-md-5">
593-
<input type="number"
594-
step="1"
595-
id="tool_qualification_expiration_never_used_days"
596-
name="tool_qualification_expiration_never_used_days"
597-
class="form-control"
598-
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 %}" />
599-
</div>
600-
<div class="col-md-offset-3 col-md-9 help-block light-grey">
601-
{% if errors.tool_qualification_expiration_never_used_days %}
602-
{{ errors.tool_qualification_expiration_never_used_days.error }}
603-
{% else %}
604-
The number of days before the user's tool qualification expires since the user qualified for the first time.
605-
{% endif %}
606-
</div>
607-
</div>
608-
<div class="form-group {% if errors.tool_qualification_cc %}has-error{% endif %}">
609-
<label class="control-label col-md-3" for="tool_qualification_cc">Reminder/expiration CC</label>
610-
<div class="col-md-5">
611-
<input type="text"
612-
id="tool_qualification_cc"
613-
name="tool_qualification_cc"
614-
class="form-control"
615-
value="{% if errors.tool_qualification_cc %}{{ errors.tool_qualification_cc.value }}{% else %}{{ tool_qualification_cc }}{% endif %}"
616-
placeholder="information@example.org" />
617-
</div>
618-
<div class="col-md-offset-3 col-md-9 help-block light-grey">
619-
{% if errors.tool_qualification_cc %}
620-
{{ errors.tool_qualification_cc.error }}
621-
{% else %}
622-
Extra email address to copy when a user's tool qualification reminder/expiration email is sent. A comma-separated list can be used.
623-
{% endif %}
624-
</div>
625-
</div>
626-
<div class="customization-separation" style="margin-bottom: 15px"></div>
627-
<div class="text-center">{% button type="save" value="Save settings" %}</div>
628-
</div>
629-
<script type="text/javascript">
544+
<div class="text-center">{% button type="save" value="Save settings" %}</div>
545+
</div>
546+
<script type="text/javascript">
630547
$("#tool-tab-link").click(function() {setTimeout(on_tool_tab_show, 50)});
631548
function on_tool_tab_show()
632549
{
633550
auto_size_textarea(document.getElementById('tool_control_configuration_setting_template'))
634551
}
635552
on_tool_tab_show();
636-
</script>
637-
</form>
553+
</script>
554+
</form>

NEMO/templates/tool_control/tool_status.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ <h1>{{ tool.name_or_child_in_use_name }}</h1>
1616
History</a>
1717
</li>
1818
{% endif %}
19-
{% if show_usage_data_tab and not hide_data_history or show_usage_data_tab and user.is_any_part_of_staff %}
19+
{% if not hide_data_history or user.is_any_part_of_staff %}
2020
<li style="width: 48%; text-align: center">
2121
<a href="#usage_data" style="padding: 10px 5px;" onclick="load_usage_data('{{ tool.id }}');">Usage Data History</a>
2222
</li>
@@ -49,7 +49,7 @@ <h1 class="pull-left" style="margin-right:20px; margin-top:0; margin-bottom:10px
4949
<a href="#config" onclick="load_config_history('{{ tool.id }}');">Config History</a>
5050
</li>
5151
{% endif %}
52-
{% if show_usage_data_tab and not hide_data_history or show_usage_data_tab and user.is_any_part_of_staff %}
52+
{% if not hide_data_history or user.is_any_part_of_staff %}
5353
<li>
5454
<a href="#usage_data" onclick="load_usage_data('{{ tool.id }}');">Run Data History</a>
5555
</li>

0 commit comments

Comments
 (0)