Skip to content

Commit a06186b

Browse files
authored
Merge pull request #310 from usnistgov/7.4.0.dev
7.4.0.dev
2 parents 0f6748e + de380ef commit a06186b

41 files changed

Lines changed: 1099 additions & 265 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

NEMO/admin.py

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,32 @@
133133
from NEMO.widgets.dynamic_form import DynamicForm, PostUsageGroupQuestion, admin_render_dynamic_form_preview
134134

135135

136+
def has_fk_filter(fk_field_name: str, title: str):
137+
filter_title = title
138+
param = f"has_{fk_field_name}"
139+
140+
class HasForeignKeyFilter(admin.SimpleListFilter):
141+
title = filter_title
142+
parameter_name = param
143+
144+
def lookups(self, request, model_admin):
145+
return (
146+
("yes", f"Has {title}"),
147+
("no", f"No {title}"),
148+
)
149+
150+
def queryset(self, request, queryset):
151+
value = self.value()
152+
if value == "yes":
153+
return queryset.filter(**{f"{fk_field_name}__isnull": False})
154+
if value == "no":
155+
return queryset.filter(**{f"{fk_field_name}__isnull": True})
156+
return queryset
157+
158+
HasForeignKeyFilter.__name__ = f"Has{title.replace(' ', '')}Filter"
159+
return HasForeignKeyFilter
160+
161+
136162
# Formset to require at least one inline form
137163
class AtLeastOneRequiredInlineFormSet(BaseInlineFormSet):
138164
def clean(self):
@@ -174,7 +200,11 @@ class Media:
174200
)
175201

176202
_tool_calendar_color = forms.CharField(
177-
required=False, max_length=9, initial="#33ad33", widget=forms.TextInput(attrs={"type": "color"})
203+
label="Tool calendar color",
204+
required=False,
205+
max_length=9,
206+
initial="#33ad33",
207+
widget=forms.TextInput(attrs={"type": "color"}),
178208
)
179209

180210
def __init__(self, *args, **kwargs):
@@ -229,6 +259,7 @@ class ToolAdmin(admin.ModelAdmin):
229259
"_category",
230260
"_location",
231261
("_requires_area_access", admin.RelatedOnlyFieldListFilter),
262+
has_fk_filter("staff_charge", "Staff Charge"),
232263
)
233264
autocomplete_fields = [
234265
"_primary_owner",
@@ -628,27 +659,46 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs):
628659

629660
@register(StaffCharge)
630661
class StaffChargeAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
631-
list_display = ("id", "staff_member", "customer", "start", "end", "waived")
662+
list_display = ("id", "staff_member", "customer", "start", "end", "waived", "has_area_record", "has_usage_event")
632663
list_filter = (
633664
"start",
634665
"waived",
635666
("customer", admin.RelatedOnlyFieldListFilter),
636667
("staff_member", admin.RelatedOnlyFieldListFilter),
668+
has_fk_filter("areaaccessrecord", "Area Record"),
669+
has_fk_filter("usageevent", "Usage Event"),
637670
)
638671
date_hierarchy = "start"
639672
autocomplete_fields = ["staff_member", "customer", "project", "validated_by", "waived_by"]
640673
actions = [waive_selected_charges]
641674

675+
@admin.display(boolean=True, description="Usage Event")
676+
def has_usage_event(self, obj) -> bool:
677+
return obj.usageevent_set.exists()
678+
679+
@admin.display(boolean=True, description="Area Record")
680+
def has_area_record(self, obj) -> bool:
681+
return obj.areaaccessrecord_set.exists()
682+
642683

643684
@register(AreaAccessRecord)
644685
class AreaAccessRecordAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
645-
list_display = ("id", "customer", "area", "project", "start", "end", "waived")
646-
list_filter = (("area", TreeRelatedFieldListFilter), "start", "waived")
686+
list_display = ("id", "customer", "area", "project", "start", "end", "waived", "has_staff_charge")
687+
list_filter = (
688+
("area", TreeRelatedFieldListFilter),
689+
"start",
690+
"waived",
691+
has_fk_filter("staff_charge", "Staff Charge"),
692+
)
647693
date_hierarchy = "start"
648694
autocomplete_fields = ["customer", "project", "validated_by", "waived_by"]
649695
readonly_fields = ["has_ended"]
650696
actions = [waive_selected_charges]
651697

698+
@admin.display(boolean=True, description="Staff Charge")
699+
def has_staff_charge(self, obj) -> bool:
700+
return obj.staff_charge_id is not None
701+
652702

653703
@register(Configuration)
654704
class ConfigurationAdmin(admin.ModelAdmin):
@@ -933,14 +983,30 @@ def questions_preview(self, obj):
933983

934984
@register(UsageEvent)
935985
class UsageEventAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
936-
list_display = ("id", "tool", "user", "operator", "project", "start", "end", "duration", "remote_work", "waived")
986+
list_display = (
987+
"id",
988+
"tool",
989+
"user",
990+
"operator",
991+
"project",
992+
"start",
993+
"end",
994+
"duration",
995+
"remote_work",
996+
"waived",
997+
"has_staff_charge",
998+
)
937999
list_filter = ("remote_work", "training", "start", "end", "waived", ("tool", admin.RelatedOnlyFieldListFilter))
9381000
date_hierarchy = "start"
9391001
autocomplete_fields = ["tool", "user", "operator", "project", "validated_by", "waived_by"]
9401002
readonly_fields = ["has_ended"]
9411003
actions = [waive_selected_charges]
9421004
search_fields = ["user__username", "user__first_name", "user__last_name", "tool__name"]
9431005

1006+
@admin.display(boolean=True, description="Staff Charge")
1007+
def has_staff_charge(self, obj) -> bool:
1008+
return obj.staff_charge_id is not None
1009+
9441010

9451011
@register(Consumable)
9461012
class ConsumableAdmin(admin.ModelAdmin):
@@ -969,9 +1035,14 @@ class ConsumableCategoryAdmin(admin.ModelAdmin):
9691035
@register(ConsumableWithdraw)
9701036
class ConsumableWithdrawAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
9711037
list_display = ("id", "customer", "merchant", "consumable", "quantity", "project", "date", "waived")
972-
list_filter = ("date", "waived", ("consumable", admin.RelatedOnlyFieldListFilter))
1038+
list_filter = (
1039+
"date",
1040+
"waived",
1041+
("consumable__category", admin.RelatedOnlyFieldListFilter),
1042+
("consumable", admin.RelatedOnlyFieldListFilter),
1043+
)
9731044
date_hierarchy = "date"
974-
autocomplete_fields = ["customer", "merchant", "consumable", "project", "validated_by", "waived_by"]
1045+
autocomplete_fields = ["customer", "merchant", "consumable", "project", "validated_by", "waived_by", "usage_event"]
9751046
actions = [waive_selected_charges]
9761047

9771048

NEMO/apps/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@
44
from django.contrib.auth.decorators import login_required
55

66

7+
def monkey_patch_json_field_for_oracle():
8+
from django.db.models.fields.json import JSONField
9+
10+
# Save the original method
11+
original_from_db_value = JSONField.from_db_value
12+
13+
# Define a wrapper that checks the type first
14+
def patched_from_db_value(self, value, expression, connection):
15+
# If the driver already parsed it into a native Python type, just return it
16+
if isinstance(value, (dict, list)):
17+
return value
18+
19+
# Otherwise, let Django handle it as usual (string/bytes parsing)
20+
return original_from_db_value(self, value, expression, connection)
21+
22+
# Apply the patch
23+
JSONField.from_db_value = patched_from_db_value
24+
25+
726
def init_admin_site():
827
from NEMO.views.customization import ApplicationCustomization, ProjectsAccountsCustomization
928
from NEMO.admin import ProjectAdmin
@@ -34,6 +53,8 @@ class NEMOConfig(AppConfig):
3453
def ready(self):
3554
from NEMO.plugins import utils # needed for checks
3655

56+
monkey_patch_json_field_for_oracle()
57+
3758
if "migrate" in sys.argv or "makemigrations" in sys.argv:
3859
return
3960
from django.apps import apps

NEMO/apps/area_access/views.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
ReservationRequiredUserError,
2222
ScheduledOutageInProgressError,
2323
UnavailableResourcesUserError,
24+
UserAccessError,
2425
)
2526
from NEMO.models import (
2627
Area,
@@ -122,6 +123,11 @@ def login_to_area(request, door_id):
122123
message = f"You have not been granted physical access to any {facility_name} area. Please visit the User Office if you believe this is an error."
123124
return render(request, "area_access/physical_access_denied.html", {"message": message})
124125

126+
except UserAccessError as e:
127+
log.details = f"This user raised the following error: {e.msg}"
128+
log.save()
129+
return render(request, "area_access/physical_access_denied.html", {"message": e.msg})
130+
125131
max_capacity_reached = False
126132
reservation_requirement_failed = False
127133
scheduled_outage_in_progress = False

NEMO/apps/kiosk/templates/kiosk/tool_information.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ <h2>
121121
<div class="media">
122122
<span class="glyphicon glyphicon-leaf pull-left notification-icon danger-highlight"></span>
123123
<div class="media-body">
124-
<span class="media-heading">A required resource is unavailable: {{ r.name }} <span class="light-grey">({{ r.category }})</span></span>
124+
<span class="media-heading">A required resource is unavailable: {{ r.name }}
125+
{% if r.category %}<span class="light-grey">({{ r.category }})</span>{% endif %}
126+
</span>
125127
<span class="media-middle">{{ r.restriction_message }}</span>
126128
</div>
127129
</div>

NEMO/apps/kiosk/templates/kiosk/tool_reservation.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ <h4>When would you like to reserve the {{ tool }}?</h4>
8282
}
8383
});
8484
let end_date_picker = $('#end_date').pickadate({format: "{{ pickadate_date_format }}", formatSubmit: "yyyy-mm-dd", firstDay: 1, hiddenName: true, onSet: refresh_times});
85-
let start_time_picker = $('#start').pickatime({interval: 15, format: "{{ pickadate_time_format }}", formatSubmit: "H:i", hiddenName: true, formatLabel: format_labels(true)});
86-
let end_time_picker = $('#end').pickatime({interval: 15, format: "{{ pickadate_time_format }}", formatSubmit: "H:i", hiddenName: true, formatLabel: format_labels(false)});
85+
let start_time_picker = $('#start').pickatime({interval: {{ calendar_slot_duration }}, format: "{{ pickadate_time_format }}", formatSubmit: "H:i", hiddenName: true, formatLabel: format_labels(true)});
86+
let end_time_picker = $('#end').pickatime({interval: {{ calendar_slot_duration }}, format: "{{ pickadate_time_format }}", formatSubmit: "H:i", hiddenName: true, formatLabel: format_labels(false)});
8787
function refresh_times()
8888
{
8989
start_time_picker.pickatime('picker').render();

NEMO/apps/kiosk/views.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@
4242
make_withdrawal_success_message,
4343
self_checkout,
4444
)
45-
from NEMO.views.customization import ApplicationCustomization, ToolCustomization, UserCustomization
45+
from NEMO.views.customization import (
46+
ApplicationCustomization,
47+
CalendarCustomization,
48+
ToolCustomization,
49+
UserCustomization,
50+
)
4651
from NEMO.views.get_projects import get_projects
4752
from NEMO.views.tasks import save_task
4853
from NEMO.views.tool_control import (
@@ -387,6 +392,7 @@ def tool_reservation(request, tool_id, user_id, back):
387392
"project": project,
388393
"customer": customer,
389394
"back": back,
395+
"calendar_slot_duration": CalendarCustomization.get_slot_resolution_minutes(),
390396
"tool_reservation_times": list(
391397
Reservation.objects.filter(
392398
cancelled=False, missed=False, shortened=False, tool=tool, start__gte=timezone.now()

NEMO/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@
1616

1717
# Name of the media folder under which only staff can see files
1818
MEDIA_PROTECTED = "protected"
19+
20+
# Policy setting names
21+
MAIN_POLICY_SETTING = "MAIN_NEMO_POLICY_CLASS"
22+
EXTRA_POLICIES_SETTING = "EXTRA_NEMO_POLICY_CLASSES"

NEMO/middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def process_request(self, request):
114114

115115
class NEMOAuditlogMiddleware:
116116
"""
117-
Middleware to couple the request's user to log items. This is accomplished by currying the
117+
Middleware to couple the request's user to log items. This is carried out by currying the
118118
signal receiver with the user from the request (or None if the user is not authenticated).
119119
"""
120120

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.2.27 on 2026-02-14 16:02
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("NEMO", "0142_alter_tool__adjustment_request_reviewers_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="usageevent",
16+
name="staff_charge",
17+
field=models.ForeignKey(
18+
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="NEMO.staffcharge"
19+
),
20+
),
21+
]

0 commit comments

Comments
 (0)