Skip to content

Commit f401d6a

Browse files
authored
Merge pull request #275 from usnistgov/7.0.2.dev
7.0.2.dev
2 parents 0222f1b + 7d49abc commit f401d6a

15 files changed

+149
-54
lines changed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ RUN apt-get install -y less vim
55

66
# Intall NEMO (in the current directory) and Gunicorn
77
COPY . /nemo/
8-
RUN pip install /nemo/ gunicorn==20.1.0
8+
RUN python3 -m pip install /nemo/ gunicorn==23.0.0
99
RUN rm --recursive --force /nemo/
1010

1111
RUN mkdir /nemo

Dockerfile.splash_pad

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ FROM python:3.11
33
RUN apt-get update && apt-get upgrade -y && apt-get install -y systemctl rsync vim less
44

55
COPY . /nemo/
6-
RUN pip install /nemo/
6+
RUN python3 -m pip install /nemo/
77
RUN rm --recursive --force /nemo
88

99
RUN mkdir /nemo

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ <h1 style="color:lightgrey" id="badge_number"></h1>
459459
}
460460
{# Auto hide tooltips on kiosk page after 30 seconds #}
461461
$(document).on('shown.bs.tooltip', function () {
462-
var tooltip = $('.tooltip');
462+
let tooltip = $('.tooltip');
463463
setTimeout(function () {
464464
tooltip.hide();
465465
}, 30000);

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

+28-14
Original file line numberDiff line numberDiff line change
@@ -64,35 +64,49 @@ <h4>When would you like to reserve the {{ tool }}?</h4>
6464
{% for item in tool_reservation_times %}
6565
unavailable_times.push([{{ item.start|date:"U" }},{{ item.end|date:"U" }}]);
6666
{% endfor %}
67-
let start_date_picker = $('#start_date').pickadate({format: "{{ pickadate_date_format }}", formatSubmit: "yyyy-mm-dd", firstDay: 1, hiddenName: true, onSet: function(event) {
68-
var date = start_date_picker.pickadate('picker').get('select', '{{ pickadate_date_format }}');
69-
end_date_picker.pickadate('picker').set('select', date);
70-
refresh_times;
71-
}
72-
});
67+
let start_date_picker = $('#start_date').pickadate(
68+
{
69+
format: "{{ pickadate_date_format }}",
70+
formatSubmit: "yyyy-mm-dd",
71+
firstDay: 1,
72+
hiddenName: true,
73+
onSet: function(event)
74+
{
75+
let date = start_date_picker.pickadate('picker').get('select', '{{ pickadate_date_format }}');
76+
end_date_picker.pickadate('picker').set('select', date);
77+
refresh_times();
78+
}
79+
});
7380
let end_date_picker = $('#end_date').pickadate({format: "{{ pickadate_date_format }}", formatSubmit: "yyyy-mm-dd", firstDay: 1, hiddenName: true, onSet: refresh_times});
7481
let start_time_picker = $('#start').pickatime({interval: 15, format: "{{ pickadate_time_format }}", formatSubmit: "H:i", hiddenName: true, formatLabel: format_labels(true)});
7582
let end_time_picker = $('#end').pickatime({interval: 15, format: "{{ pickadate_time_format }}", formatSubmit: "H:i", hiddenName: true, formatLabel: format_labels(false)});
7683
// set initial date
77-
if ('{{ start_date|default_if_none:'' }}') {
78-
start_date_picker.pickadate('picker').set('select', '{{ start_date }}', {format: '{{ pickadate_date_format }}'})
84+
if ('{{ start_date|default_if_none:'' }}')
85+
{
86+
start_date_picker.pickadate('picker').set('select', '{{ start_date }}', {format: '{{ pickadate_date_format }}'});
7987
}
80-
function refresh_times() {
88+
function refresh_times()
89+
{
8190
start_time_picker.pickatime('picker').render();
8291
end_time_picker.pickatime('picker').render();
8392
}
84-
function format_labels(is_start){
85-
return function format_label(time) {
93+
function format_labels(is_start)
94+
{
95+
return function format_label(time)
96+
{
8697
let date_picker = is_start ? start_date_picker : end_date_picker;
87-
if (date_picker.pickadate('picker').get('select') && unavailable_times.length > 0) {
98+
if (date_picker.pickadate('picker').get('select') && unavailable_times.length > 0)
99+
{
88100
let date_selected = date_picker.pickadate('picker').get('select').pick; // selected date in milliseconds
89101
let time_selected = time.pick * 60 * 1000; // time in milliseconds
90102
let date_time_selected = (date_selected + time_selected)/1000; // back to seconds to compare with python timestamp
91-
for (let i=0 ; i < unavailable_times.length; i++) {
103+
for (let i=0 ; i < unavailable_times.length; i++)
104+
{
92105
let times = unavailable_times[i];
93106
let start = times[0];
94107
let end = times[1];
95-
if (date_time_selected >= start && date_time_selected < end) {
108+
if (date_time_selected >= start && date_time_selected < end)
109+
{
96110
return '<sp !an>{{ pickadate_time_format }}</sp !an> <sm !all> !alre!ad!y re!serve!d</sm !all>';
97111
}
98112
}

NEMO/interlocks.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
from __future__ import annotations
2+
13
import socket
24
import struct
35
from abc import ABC, abstractmethod
46
from logging import getLogger
57
from time import sleep
6-
from typing import Dict, List, Optional
8+
from typing import Dict, List, Optional, TYPE_CHECKING
79
from xml.etree import ElementTree
810

911
import requests
@@ -15,12 +17,14 @@
1517
from pymodbus.exceptions import ConnectionException
1618
from requests import Response
1719

18-
from NEMO.admin import InterlockAdminForm, InterlockCardAdminForm
1920
from NEMO.exceptions import InterlockError
2021
from NEMO.models import Interlock as Interlock_model, InterlockCardCategory, User
2122
from NEMO.typing import QuerySetType
2223
from NEMO.utilities import BasicDisplayTable, EmailCategory, export_format_datetime, format_datetime
2324

25+
if TYPE_CHECKING:
26+
from NEMO.admin import InterlockCardAdminForm, InterlockAdminForm
27+
2428
interlocks_logger = getLogger(__name__)
2529

2630
DEFAULT_CHANNEL_NAME = "Channel/Relay/Coil"

NEMO/static/mobile.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ function mobile_search(query_element, get_base_url_callback, hide_type)
22
{
33
hide_type = hide_type || false;
44
query_element = $(query_element);
5-
var results_target = $(query_element.data('search-results-target')).html('');
6-
var query = query_element.val();
5+
let results_target = $(query_element.data('search-results-target')).html('');
6+
let query = query_element.val();
77
if(query.length < 2)
88
return;
9-
var search_base = query_element.data('search-base');
10-
var result_count = 0;
11-
var matching_regular_expression = new RegExp(query, 'i');
12-
var results = '<div class="list-group">';
9+
let search_base = query_element.data('search-base');
10+
let result_count = 0;
11+
let matching_regular_expression = new RegExp(query, 'i');
12+
let results = '<div class="list-group">';
1313
$.each(search_base, function(item_index, item)
1414
{
1515
if(matching_regular_expression.test(item.name))

NEMO/templates/calendar/calendar.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,7 @@
800800
let item_tree_tools = $("#item-tree-tool");
801801
let item_tree_areas = $("#item-tree-area");
802802
let event_type = get_event_type();
803-
let extra_links_personal_schedule = $("#extra-links-personal-schedule");
803+
let extra_links_personal_schedule = $("#extra-links-personal-schedule, #checkbox-personal-schedule-personal-schedule");
804804
let extra_links_all_tools = $("#extra-links-all-tools");
805805
let extra_links_all_areas = $("#extra-links-all-areas");
806806
let extra_links_all_areastools = $("#extra-links-all-areastools");

NEMO/templates/customizations/customizations.html

+6-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ <h1 style="margin-top: 0;">Customization</h1>
1212
<div class="col-sm-3">
1313
<ul class="nav nav-pills nav-stacked" id="tabs">
1414
{% for customization_instance in customization.instances %}
15-
<li class="{% if customization_instance.key == customization.key %}active{% endif %}">
16-
<a id="{{ customization_instance.key }}-tab-link"
17-
href="{% url 'customization' customization_instance.key %}">{{ customization_instance.title }}</a>
18-
</li>
15+
{% if customization_instance.template %}
16+
<li class="{% if customization_instance.key == customization.key %}active{% endif %}">
17+
<a id="{{ customization_instance.key }}-tab-link"
18+
href="{% url 'customization' customization_instance.key %}">{{ customization_instance.title }}</a>
19+
</li>
20+
{% endif %}
1921
{% endfor %}
2022
</ul>
2123
</div>

NEMO/templates/customizations/customizations_templates.html

+5
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,11 @@ <h3 class="customization-section-title" id="weekend_access_email_id">Weekend acc
620620
</ul>
621621
{% include 'customizations/customizations_upload.html' with element=weekend_access_email name='weekend access email' key='templates' %}
622622
</div>
623+
{% for customization_instance in customization.instances %}
624+
{% if customization_instance.template_templates %}
625+
{% include customization_instance.template_templates %}
626+
{% endif %}
627+
{% endfor %}
623628
</div>
624629
<script>
625630
function sort_comp(element)

NEMO/tests/test_urls_api.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from datetime import datetime
2+
3+
from django.conf import settings
4+
from django.test import TestCase
5+
from django.urls import reverse
6+
from rest_framework import status
7+
from rest_framework.test import APIClient
8+
9+
from NEMO.models import User
10+
from NEMO.tests.test_utilities import login_as
11+
from NEMO.urls import router
12+
13+
14+
class TestAPIUrls(TestCase):
15+
def setUp(self):
16+
user = User.objects.create(username="test", is_superuser=True)
17+
self.client = APIClient()
18+
login_as(self.client, user)
19+
20+
def test_all_api_urls(self):
21+
# Iterate through all registered URLs in the DRF router
22+
for prefix, viewset, basename in router.registry:
23+
# Get list of routes for this prefix/viewset
24+
routes = router.get_routes(viewset)
25+
for route in routes:
26+
# Reverse the list path
27+
try:
28+
full_url = reverse(f"{basename}-{route.mapping['get']}")
29+
except Exception:
30+
continue # Skip if URL cannot be reversed
31+
32+
try:
33+
# Make a GET request to the URL (or any method supported)
34+
today = datetime.now().date().strftime(settings.DATE_INPUT_FORMATS[0])
35+
data = None if basename != "billing" else {"start": today, "end": today}
36+
response = self.client.get(full_url, data=data)
37+
# Assert that the response is acceptable
38+
self.assertEqual(
39+
response.status_code,
40+
status.HTTP_200_OK,
41+
msg=f"URL {full_url} returned unexpected status code {response.status_code}",
42+
)
43+
except Exception as error:
44+
self.fail(f"Error while testing URL {full_url}: {error}")

NEMO/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def sort_urls(url_path):
104104
router.register(r"reservation_configuration_options", api.ConfigurationOptionViewSet)
105105
router.register(r"resources", api.ResourceViewSet)
106106
router.register(r"scheduled_outages", api.ScheduledOutageViewSet)
107+
router.register(r"staff_assistance_requests", api.StaffAssistanceRequestsViewSet)
107108
router.register(r"staff_charges", api.StaffChargeViewSet)
108109
router.register(r"tasks", api.TaskViewSet)
109110
router.register(r"tools", api.ToolViewSet)

NEMO/views/calendar.py

+5-13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import re
23
from datetime import datetime, timedelta
34
from http import HTTPStatus
@@ -7,7 +8,7 @@
78

89
from django.contrib.auth.decorators import login_required
910
from django.db.models import Q
10-
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound
11+
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound, JsonResponse
1112
from django.shortcuts import get_object_or_404, redirect, render
1213
from django.urls import reverse
1314
from django.utils import timezone
@@ -177,18 +178,9 @@ def event_feed(request):
177178
if event_type == "reservations":
178179
return reservation_event_feed(request, start, end)
179180
if event_type == "reservations and use":
180-
# We need to remove the json array brackets from the original responses (last [ of reservations and first ] of usage)
181-
reservation_feed = reservation_event_feed(request, start, end)
182-
reservation_feed_content = reservation_feed.content
183-
position = reservation_feed_content.rfind("]".encode())
184-
if position != -1:
185-
reservation_feed_content = (
186-
reservation_feed_content[:position] + "".encode() + reservation_feed_content[position + 1 :]
187-
)
188-
reservation_feed.content = reservation_feed_content + usage_event_feed(request, start, end).content.replace(
189-
"[".encode(), "".encode(), 1
190-
)
191-
return reservation_feed
181+
reservation_feed = json.loads(reservation_event_feed(request, start, end).content)
182+
usage_feed = json.loads(usage_event_feed(request, start, end).content)
183+
return JsonResponse(reservation_feed + usage_feed, safe=False)
192184
elif event_type == f"{facility_name.lower()} use":
193185
return usage_event_feed(request, start, end)
194186
# Only staff may request a specific user's history...

NEMO/views/customization.py

+35-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from abc import ABC
22
from datetime import date, datetime
3-
from typing import Dict, Iterable, List
3+
from logging import getLogger
4+
from typing import Dict, Iterable, List, Optional
45

56
from dateutil.relativedelta import relativedelta
7+
from django.apps import apps
68
from django.contrib import messages
79
from django.core.exceptions import ValidationError
810
from django.core.files.storage import default_storage
@@ -13,7 +15,8 @@
1315
)
1416
from django.http import HttpResponseNotFound
1517
from django.shortcuts import redirect, render
16-
from django.template import Context, Template
18+
from django.template import Context, Template, TemplateDoesNotExist
19+
from django.template.loader import get_template
1720
from django.utils import timezone
1821
from django.views.decorators.http import require_GET, require_POST
1922

@@ -34,6 +37,8 @@
3437
)
3538
from NEMO.utilities import RecurrenceFrequency, date_input_format, datetime_input_format, quiet_int
3639

40+
customization_logger = getLogger(__name__)
41+
3742

3843
class CustomizationBase(ABC):
3944
_instances = {}
@@ -45,8 +50,34 @@ def __init__(self, key, title):
4550
self.key = key
4651
self.title = title
4752

48-
def template(self) -> str:
49-
return f"customizations/customizations_{self.key}.html"
53+
def template(self) -> Optional[str]:
54+
# We want to check if there is a customization template file in the app template dir
55+
# Otherwise we load it from the main template dir
56+
app = apps.get_containing_app_config(type(self).__module__)
57+
if app:
58+
for app_dir in [app.label, app.name, ""]:
59+
try:
60+
template_path = f"{app_dir}{'/' if app_dir else ''}customizations/customizations_{self.key}.html"
61+
get_template(template_path) # will raise TemplateDoesNotExist if not found
62+
return template_path
63+
except TemplateDoesNotExist:
64+
pass
65+
customization_logger.debug(f"could not find any template for customization: {self.key}")
66+
67+
def template_templates(self) -> Optional[str]:
68+
# We have a similar approach here except we are looking for a very specific file
69+
# named customizations_{key}_templates.html, and we are only looking in the app template dirs
70+
app = apps.get_containing_app_config(type(self).__module__)
71+
if app:
72+
for app_dir in [app.label, app.name, ""]:
73+
try:
74+
template_path = (
75+
f"{app_dir}{'/' if app_dir else ''}customizations/customizations_{self.key}_templates.html"
76+
)
77+
get_template(template_path) # will raise TemplateDoesNotExist if it doesn't exist
78+
return template_path
79+
except TemplateDoesNotExist:
80+
pass
5081

5182
def context(self) -> Dict:
5283
files_dict = {name: get_media_file_contents(name + extension) for name, extension in type(self).files}

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "NEMO"
3-
version = "7.0.1"
3+
version = "7.0.2"
44
description = "NEMO is a laboratory logistics web application. Use it to schedule reservations, control tool access, track maintenance issues, and more."
55
keywords = ["NEMO"]
66
readme = "README.md"

resources/settings.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django.conf import global_settings
44
from rest_framework.settings import DEFAULTS
55

6+
BASE_DIR = "/nemo"
7+
68
# ------------------------------------------------------------------
79
# -------------------- Django settings for NEMO --------------------
810
# ------------------------------------------------------------------
@@ -237,7 +239,7 @@
237239
# -------------------- Email written to files --------------------
238240
# Uncomment the following for testing
239241
# EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
240-
# EMAIL_FILE_PATH = "/nemo/emails/"
242+
# EMAIL_FILE_PATH = BASE_DIR + "/emails/"
241243

242244
# See the list of timezones https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
243245
TIME_ZONE = "America/New_York"
@@ -246,16 +248,16 @@
246248
DATABASES = {
247249
"default": {
248250
"ENGINE": "django.db.backends.sqlite3",
249-
"NAME": "/nemo/nemo.db",
251+
"NAME": BASE_DIR + "/nemo.db",
250252
}
251253
}
252254

253255
# Comment this line out for dev. This makes sure static files have a version to avoid
254256
# having to clear browser cache between releases.
255257
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
256-
STATIC_ROOT = "/nemo/static/"
258+
STATIC_ROOT = BASE_DIR + "/static/"
257259
STATIC_URL = "/static/"
258-
MEDIA_ROOT = "/nemo/media/"
260+
MEDIA_ROOT = BASE_DIR + "/media/"
259261
MEDIA_URL = "/media/"
260262

261263
# Make this unique, and do not share it with anybody.
@@ -284,12 +286,12 @@
284286
"error_file": {
285287
"level": "WARNING", # Log all warnings and errors to this file
286288
"class": "logging.FileHandler",
287-
"filename": "/nemo/logs/nemo_error.log",
289+
"filename": BASE_DIR + "/logs/nemo_error.log",
288290
"formatter": "verbose",
289291
},
290292
"file": {
291293
"class": "logging.FileHandler",
292-
"filename": "/nemo/logs/nemo.log",
294+
"filename": BASE_DIR + "/logs/nemo.log",
293295
"formatter": "simple",
294296
},
295297
"console": {

0 commit comments

Comments
 (0)