From 4ef837d6c1ea9a5734f40e20a4a5e11bc8e225e8 Mon Sep 17 00:00:00 2001
From: mrampant
Date: Tue, 18 Jun 2024 09:09:09 -0400
Subject: [PATCH 001/108] - starting 6.1.0.dev
---
NEMO/migrations/0087_version_6_1_0.py | 19 +++++++++++++++++++
setup.py | 2 +-
2 files changed, 20 insertions(+), 1 deletion(-)
create mode 100644 NEMO/migrations/0087_version_6_1_0.py
diff --git a/NEMO/migrations/0087_version_6_1_0.py b/NEMO/migrations/0087_version_6_1_0.py
new file mode 100644
index 00000000..d5452c0c
--- /dev/null
+++ b/NEMO/migrations/0087_version_6_1_0.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.11 on 2024-06-11 02:00
+
+from django.db import migrations
+
+from NEMO.migrations_utils import create_news_for_version
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("NEMO", "0086_adjustmentrequest_new_quantity"),
+ ]
+
+ def new_version_news(apps, schema_editor):
+ create_news_for_version(apps, "6.1.0", "")
+
+ operations = [
+ migrations.RunPython(new_version_news),
+ ]
diff --git a/setup.py b/setup.py
index 0d6ac86c..4c99a280 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
setup(
name="NEMO",
- version="6.0.2",
+ version="6.1.0.dev",
python_requires=">=3.8, <4",
packages=find_namespace_packages(exclude=["resources", "resources.*", "build", "build.*"]),
include_package_data=True,
From f57e759dfd7e0df642c121fc81603ad83a0dbba7 Mon Sep 17 00:00:00 2001
From: mrampant
Date: Tue, 18 Jun 2024 12:32:42 -0400
Subject: [PATCH 002/108] - completely removed dependency on pytz. plugins will
have to require it if they need it
---
NEMO/templatetags/custom_tags_and_filters.py | 11 +++++--
NEMO/tests/test_formats.py | 6 ++--
NEMO/tests/test_settings.py | 2 +-
NEMO/utilities.py | 30 +++++++++-----------
setup.py | 1 -
5 files changed, 27 insertions(+), 23 deletions(-)
diff --git a/NEMO/templatetags/custom_tags_and_filters.py b/NEMO/templatetags/custom_tags_and_filters.py
index 73b4803b..0d9ee0b9 100644
--- a/NEMO/templatetags/custom_tags_and_filters.py
+++ b/NEMO/templatetags/custom_tags_and_filters.py
@@ -1,4 +1,5 @@
import datetime
+from collections.abc import Mapping, Sequence
from datetime import timedelta
from importlib.metadata import PackageNotFoundError, version
from urllib.parse import quote
@@ -125,8 +126,14 @@ def res_question_tbody(dictionary):
@register.filter
-def get_item(dictionary, key):
- return dictionary.get(key)
+def get_item(dict_or_array, key):
+ if isinstance(dict_or_array, Mapping):
+ return dict_or_array.get(key)
+ elif isinstance(dict_or_array, Sequence):
+ try:
+ return dict_or_array[key]
+ except IndexError:
+ pass
@register.simple_tag
diff --git a/NEMO/tests/test_formats.py b/NEMO/tests/test_formats.py
index de3f02fa..a4719239 100644
--- a/NEMO/tests/test_formats.py
+++ b/NEMO/tests/test_formats.py
@@ -1,6 +1,6 @@
import datetime
+import zoneinfo
-import pytz
from django.conf import settings
from django.test import TestCase
from django.utils import timezone
@@ -51,8 +51,8 @@ def test_format_daterange(self):
end_date = datetime.date(2022, 2, 11)
self.assertEqual(format_daterange(start_date, end_date, d_format=d_format), f"from 02/11/2022 to 02/11/2022")
- tz = pytz.timezone("US/Pacific")
- self.assertEqual(settings.TIME_ZONE, "America/New_York")
+ tz = zoneinfo.ZoneInfo("US/Pacific")
+ self.assertEqual(settings.TIME_ZONE, "US/Eastern")
start_tz = make_aware(datetime.datetime(2022, 2, 11, 5, 0, 0), tz) # 5AM Pacific => 8AM Eastern
end_tz = start_tz + datetime.timedelta(days=2)
self.assertNotEqual(
diff --git a/NEMO/tests/test_settings.py b/NEMO/tests/test_settings.py
index c40bc6dd..5a6bcbce 100644
--- a/NEMO/tests/test_settings.py
+++ b/NEMO/tests/test_settings.py
@@ -94,7 +94,7 @@
EMAIL_FILE_PATH = "./email_logs"
-TIME_ZONE = "America/New_York"
+TIME_ZONE = "US/Eastern"
DATABASES = {
"default": {
diff --git a/NEMO/utilities.py b/NEMO/utilities.py
index 09a23124..b57a3ac1 100644
--- a/NEMO/utilities.py
+++ b/NEMO/utilities.py
@@ -3,7 +3,7 @@
import os
from calendar import monthrange
from copy import deepcopy
-from datetime import date, datetime, time
+from datetime import date, datetime, time, timezone
from email import encoders
from email.mime.base import MIMEBase
from enum import Enum
@@ -12,7 +12,6 @@
from typing import Dict, List, Optional, Sequence, Set, Tuple, Union
from urllib.parse import urljoin
-import pytz
from PIL import Image
from dateutil import rrule
from dateutil.parser import parse
@@ -30,11 +29,10 @@
from django.template import Template
from django.template.context import make_context
from django.urls import NoReverseMatch, reverse
-from django.utils import timezone
+from django.utils import timezone as django_timezone
from django.utils.formats import date_format, get_format, time_format
from django.utils.html import format_html
from django.utils.text import slugify
-from django.utils.timezone import is_naive, localtime, make_aware
utilities_logger = getLogger(__name__)
@@ -241,7 +239,7 @@ def parse_parameter_string(
def month_list(since=datetime(year=2013, month=11, day=1)):
- month_count = (timezone.now().year - since.year) * 12 + (timezone.now().month - since.month) + 1
+ month_count = (django_timezone.now().year - since.year) * 12 + (django_timezone.now().month - since.month) + 1
result = list(rrule.rrule(rrule.MONTHLY, dtstart=since, count=month_count))
result = localize(result)
result.reverse()
@@ -252,7 +250,7 @@ def get_month_timeframe(date_str: str = None):
if date_str:
start = parse(date_str)
else:
- start = timezone.now()
+ start = django_timezone.now()
first_of_the_month = localize(datetime(start.year, start.month, 1))
last_of_the_month = localize(
datetime(start.year, start.month, monthrange(start.year, start.month)[1], 23, 59, 59, 999999)
@@ -354,7 +352,7 @@ def format_daterange(
def format_datetime(universal_time=None, df=None, as_current_timezone=True, use_l10n=None) -> str:
- this_time = universal_time if universal_time else timezone.now() if as_current_timezone else datetime.now()
+ this_time = universal_time if universal_time else django_timezone.now() if as_current_timezone else datetime.now()
local_time = as_timezone(this_time) if as_current_timezone else this_time
if isinstance(local_time, time):
return time_format(local_time, df or "TIME_FORMAT", use_l10n)
@@ -370,7 +368,7 @@ def export_format_datetime(
This function returns a formatted date/time for export files.
Default returns date + time format, with underscores
"""
- this_time = date_time if date_time else timezone.now() if as_current_timezone else datetime.now()
+ this_time = date_time if date_time else django_timezone.now() if as_current_timezone else datetime.now()
export_date_format = getattr(settings, "EXPORT_DATE_FORMAT", "m_d_Y").replace("-", "_")
export_time_format = getattr(settings, "EXPORT_TIME_FORMAT", "h_i_s").replace("-", "_")
if not underscore:
@@ -386,20 +384,20 @@ def export_format_datetime(
def as_timezone(dt):
- naive = type(dt) == date or is_naive(dt)
- return timezone.localtime(dt) if not naive else dt
+ naive = type(dt) == date or django_timezone.is_naive(dt)
+ return django_timezone.localtime(dt) if not naive else dt
def localize(dt, tz=None):
- tz = tz or timezone.get_current_timezone()
+ tz = tz or django_timezone.get_current_timezone()
if isinstance(dt, list):
- return [make_aware(d, tz) for d in dt]
+ return [django_timezone.make_aware(d, tz) for d in dt]
else:
- return make_aware(dt, tz)
+ return django_timezone.make_aware(dt, tz)
def naive_local_current_datetime():
- return localtime(timezone.now()).replace(tzinfo=None)
+ return django_timezone.localtime(django_timezone.now()).replace(tzinfo=None)
def beginning_of_the_day(t: datetime, in_local_timezone=True) -> datetime:
@@ -743,8 +741,8 @@ def create_ics(
sequence = "SEQUENCE:2\n" if cancelled else "SEQUENCE:0\n"
priority = "PRIORITY:5\n" if cancelled else "PRIORITY:0\n"
now = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
- start = start.astimezone(pytz.utc).strftime("%Y%m%dT%H%M%SZ")
- end = end.astimezone(pytz.utc).strftime("%Y%m%dT%H%M%SZ")
+ start = start.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
+ end = end.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
lines = [
"BEGIN:VCALENDAR\n",
"VERSION:2.0\n",
diff --git a/setup.py b/setup.py
index 4c99a280..dac5640e 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,6 @@
"Pillow==10.3.0",
"pymodbus==3.3.2",
"python-dateutil==2.9.0",
- "pytz==2024.1",
"requests==2.32.3",
],
extras_require={"dev-tools": ["pre-commit", "djlint", "black"]},
From e9840fbda3ab1482dfa560e6e117626e7ae1791b Mon Sep 17 00:00:00 2001
From: mrampant
Date: Tue, 18 Jun 2024 12:45:49 -0400
Subject: [PATCH 003/108] - fixed tests for python 3.8
---
NEMO/tests/test_formats.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/NEMO/tests/test_formats.py b/NEMO/tests/test_formats.py
index a4719239..53e5c342 100644
--- a/NEMO/tests/test_formats.py
+++ b/NEMO/tests/test_formats.py
@@ -1,5 +1,9 @@
import datetime
-import zoneinfo
+
+try:
+ import zoneinfo
+except ImportError:
+ from backports import zoneinfo
from django.conf import settings
from django.test import TestCase
From e564433bdf79d9f1c1514f8727d14789480dc049 Mon Sep 17 00:00:00 2001
From: r-xyz <100710244+r-xyz@users.noreply.github.com>
Date: Wed, 19 Jun 2024 00:41:09 +0200
Subject: [PATCH 004/108] Out of schedule email for areas.
Extends functionality to accesses outside the allowed schedule for areas not requiring a reservation.
Relevant area -related changes are mirrored from [NEMO-CE Merge Request](https://gitlab.com/nemo-community/nemo-ce/-/merge_requests/31).
---
NEMO/views/timed_services.py | 100 +++++++++++++-----
.../emails/out_of_time_reservation_email.html | 26 ++++-
2 files changed, 99 insertions(+), 27 deletions(-)
diff --git a/NEMO/views/timed_services.py b/NEMO/views/timed_services.py
index 0ebef423..9ecf3607 100644
--- a/NEMO/views/timed_services.py
+++ b/NEMO/views/timed_services.py
@@ -385,7 +385,7 @@ def email_out_of_time_reservation_notification(request):
def send_email_out_of_time_reservation_notification(request=None):
"""
- Out of time reservation notification for areas is when a user is still logged in an area but his reservation expired.
+ Out of time reservation notification for areas is when a user is still logged in an area but either their reservation expired or they are outside of their permitted access hours.
"""
# Exit early if the out of time reservation email template has not been customized for the organization yet.
# This feature only sends emails, so if the template is not defined there nothing to do.
@@ -394,7 +394,8 @@ def send_email_out_of_time_reservation_notification(request=None):
"The out of time reservation email template has not been customized for your organization yet. Please visit the customization page to upload a template, then out of time email notifications can be sent."
)
- out_of_time_user_area = []
+ out_of_time_user_reservations = []
+ trigger_time = timezone.now().replace(second=0, microsecond=0) # Round down to the nearest minute.
# Find all logged users
access_records: List[AreaAccessRecord] = (
@@ -409,31 +410,81 @@ def send_email_out_of_time_reservation_notification(request=None):
if customer.is_staff or customer.is_service_personnel:
continue
- if area.requires_reservation:
+ threshold = (
+ trigger_time if not area.logout_grace_period else trigger_time - timedelta(minutes=area.logout_grace_period)
+ )
+ physical_access = PhysicalAccessLevel.objects.filter(user=customer, area=area).first()
+ # Check first if allowed schedule has just expired
+ if (
+ physical_access
+ and physical_access.accessible_at(threshold - timedelta(minutes=1))
+ and not physical_access.accessible_at(threshold)
+ ):
+ out_of_time_user_reservations.append(Reservation(user=customer, area=area, end=threshold))
+ else:
+ if area.requires_reservation:
+ ending_reservations = Reservation.objects.filter(
+ cancelled=False,
+ missed=False,
+ shortened=False,
+ area=area,
+ user=customer,
+ start__lte=timezone.now(),
+ end=threshold,
+ )
+ # find out if a reservation is starting right at the same time (in case of back to back reservations, in which case customer is good)
+ starting_reservations = Reservation.objects.filter(
+ cancelled=False, missed=False, shortened=False, area=area, user=customer, start=threshold
+ )
+ if ending_reservations.exists() and not starting_reservations.exists():
+ out_of_time_user_reservations.append(ending_reservations[0])
+
+ # Find all users logged in tools
+ usage_records: List[UsageEvent] = (
+ UsageEvent.objects.filter(end=None, user=F("operator"))
+ .prefetch_related("operator", "tool")
+ .only("operator", "tool")
+ )
+ for usage_record in usage_records:
+ # staff and service personnel are exempt from out of time notification
+ operator = usage_record.operator
+ tool = usage_record.tool
+ if operator.is_staff or operator.is_service_personnel:
+ continue
# Calculate the timestamp of how late a user can be logged in after a reservation ended.
threshold = (
- timezone.now()
- if not area.logout_grace_period
- else timezone.now() - timedelta(minutes=area.logout_grace_period)
- )
- threshold = datetime.replace(threshold, second=0, microsecond=0) # Round down to the nearest minute.
- ending_reservations = Reservation.objects.filter(
- cancelled=False,
- missed=False,
- shortened=False,
- area=area,
- user=customer,
- start__lte=timezone.now(),
- end=threshold,
- )
- # find out if a reservation is starting right at the same time (in case of back to back reservations, in which case customer is good)
- starting_reservations = Reservation.objects.filter(
- cancelled=False, missed=False, shortened=False, area=area, user=customer, start=threshold
+ trigger_time
+ if not tool.logout_grace_period
+ else trigger_time - timedelta(minutes=tool.logout_grace_period)
)
- if ending_reservations.exists() and not starting_reservations.exists():
- out_of_time_user_area.append(ending_reservations[0])
+ user_qualification = Qualification.objects.filter(user=operator, tool=tool).first()
+ # Check first if allowed schedule has just expired
+ if (
+ user_qualification
+ and user_qualification.qualification_level
+ and user_qualification.qualification_level.is_allowed(threshold - timedelta(minutes=1))
+ and not user_qualification.qualification_level.is_allowed(threshold)
+ ):
+ out_of_time_user_reservations.append(Reservation(user=operator, tool=tool, end=threshold))
+ else:
+ if tool.reservation_required:
+ ending_reservations = Reservation.objects.filter(
+ cancelled=False,
+ missed=False,
+ shortened=False,
+ tool=tool,
+ user=operator,
+ start__lte=timezone.now(),
+ end=threshold,
+ )
+ # find out if a reservation is starting right at the same time (in case of back to back reservations, in which case operator is good)
+ starting_reservations = Reservation.objects.filter(
+ cancelled=False, missed=False, shortened=False, tool=tool, user=operator, start=threshold
+ )
+ if ending_reservations.exists() and not starting_reservations.exists():
+ out_of_time_user_reservations.append(ending_reservations[0])
- for reservation in out_of_time_user_area:
+ for reservation in out_of_time_user_reservations:
send_out_of_time_reservation_notification(reservation, request)
return HttpResponse()
@@ -443,7 +494,8 @@ def send_out_of_time_reservation_notification(reservation: Reservation, request=
message = get_media_file_contents("out_of_time_reservation_email.html")
user_office_email = EmailsCustomization.get("user_office_email_address")
if message and user_office_email:
- subject = "Out of time in the " + str(reservation.area.name)
+ name = str(reservation.area.name)
+ subject = "Out of time for the " + name if reservation.start else "Out of allowed schedule for the " + name
message = render_email_template(message, {"reservation": reservation}, request)
recipients = reservation.user.get_emails(
reservation.user.get_preferences().email_send_reservation_ending_reminders
diff --git a/resources/emails/out_of_time_reservation_email.html b/resources/emails/out_of_time_reservation_email.html
index 55e9291e..f287f1d1 100644
--- a/resources/emails/out_of_time_reservation_email.html
+++ b/resources/emails/out_of_time_reservation_email.html
@@ -11,18 +11,38 @@
font-family: 'Avenir Next', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'">
- OUT OF TIME IN THE {{ reservation.area }}
+
+ OUT OF
+ {% if reservation.start %}
+ TIME
+ {% else %}
+ ALLOWED SCHEDULE
+ {% endif %}
+ IN THE {{ reservation.area }}
+
|
Dear {{ reservation.user.first_name }},
- Your reservation of the {{ reservation.area }} in the facility ended at {{ reservation.end }}.
+
+ Your
+ {% if reservation.start %}
+ reservation of the
+ {% else %}
+ allowed schedule for the
+ {% endif %}
+ {{ reservation.area }} in the {{ facility.name }} ended at {{ reservation.end }}.
+
However, our records show that you are still logged in the {{ reservation.area }}.
If you simply forgot to log out, please contact a staff member
- Please remember that the facility is a shared resource, and overstaying a reservation may inhibit the productivity of other facility users.
+ {% if reservation.start %}
+ Please remember that the {{ facility.name }} is a shared resource, and overstaying a reservation may inhibit the productivity of other facility users.
+ {% else %}
+ Please remember that users are not allowed to overstay in areas outside their allowed schedule.
+ {% endif %}
|
From 65c35f3505813efd7bdf6cf554eb589e2c09514c Mon Sep 17 00:00:00 2001
From: r-xyz <100710244+r-xyz@users.noreply.github.com>
Date: Wed, 19 Jun 2024 15:26:41 +0200
Subject: [PATCH 005/108] Remove non-relevant code for out-of-schedule email.
---
NEMO/views/timed_services.py | 45 ------------------------------------
1 file changed, 45 deletions(-)
diff --git a/NEMO/views/timed_services.py b/NEMO/views/timed_services.py
index 9ecf3607..0dbcf3e1 100644
--- a/NEMO/views/timed_services.py
+++ b/NEMO/views/timed_services.py
@@ -439,51 +439,6 @@ def send_email_out_of_time_reservation_notification(request=None):
if ending_reservations.exists() and not starting_reservations.exists():
out_of_time_user_reservations.append(ending_reservations[0])
- # Find all users logged in tools
- usage_records: List[UsageEvent] = (
- UsageEvent.objects.filter(end=None, user=F("operator"))
- .prefetch_related("operator", "tool")
- .only("operator", "tool")
- )
- for usage_record in usage_records:
- # staff and service personnel are exempt from out of time notification
- operator = usage_record.operator
- tool = usage_record.tool
- if operator.is_staff or operator.is_service_personnel:
- continue
- # Calculate the timestamp of how late a user can be logged in after a reservation ended.
- threshold = (
- trigger_time
- if not tool.logout_grace_period
- else trigger_time - timedelta(minutes=tool.logout_grace_period)
- )
- user_qualification = Qualification.objects.filter(user=operator, tool=tool).first()
- # Check first if allowed schedule has just expired
- if (
- user_qualification
- and user_qualification.qualification_level
- and user_qualification.qualification_level.is_allowed(threshold - timedelta(minutes=1))
- and not user_qualification.qualification_level.is_allowed(threshold)
- ):
- out_of_time_user_reservations.append(Reservation(user=operator, tool=tool, end=threshold))
- else:
- if tool.reservation_required:
- ending_reservations = Reservation.objects.filter(
- cancelled=False,
- missed=False,
- shortened=False,
- tool=tool,
- user=operator,
- start__lte=timezone.now(),
- end=threshold,
- )
- # find out if a reservation is starting right at the same time (in case of back to back reservations, in which case operator is good)
- starting_reservations = Reservation.objects.filter(
- cancelled=False, missed=False, shortened=False, tool=tool, user=operator, start=threshold
- )
- if ending_reservations.exists() and not starting_reservations.exists():
- out_of_time_user_reservations.append(ending_reservations[0])
-
for reservation in out_of_time_user_reservations:
send_out_of_time_reservation_notification(reservation, request)
From 41b9d73a4ab1fe02712ad5d86a5e1dfa361c0fcc Mon Sep 17 00:00:00 2001
From: r-xyz <100710244+r-xyz@users.noreply.github.com>
Date: Wed, 19 Jun 2024 16:09:42 +0200
Subject: [PATCH 006/108] Fix out-of-time email template.
---
resources/emails/out_of_time_reservation_email.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/resources/emails/out_of_time_reservation_email.html b/resources/emails/out_of_time_reservation_email.html
index f287f1d1..bf208c00 100644
--- a/resources/emails/out_of_time_reservation_email.html
+++ b/resources/emails/out_of_time_reservation_email.html
@@ -32,14 +32,14 @@
{% else %}
allowed schedule for the
{% endif %}
- {{ reservation.area }} in the {{ facility.name }} ended at {{ reservation.end }}.
+ {{ reservation.area }} in the {{ facility_name }} ended at {{ reservation.end }}.
However, our records show that you are still logged in the {{ reservation.area }}.
If you simply forgot to log out, please contact a staff member
{% if reservation.start %}
- Please remember that the {{ facility.name }} is a shared resource, and overstaying a reservation may inhibit the productivity of other facility users.
+ Please remember that the {{ facility_name }} is a shared resource, and overstaying a reservation may inhibit the productivity of other facility users.
{% else %}
Please remember that users are not allowed to overstay in areas outside their allowed schedule.
{% endif %}
From 4d5749363620536ff31efc2f7a495947d3403a54 Mon Sep 17 00:00:00 2001
From: r-xyz <100710244+r-xyz@users.noreply.github.com>
Date: Wed, 19 Jun 2024 17:48:24 +0200
Subject: [PATCH 007/108] Pickadate format based on py format (#242)
* Pickadate format now based on js format.
Closes #237
* Pickadate format conversion: remove additional unsupported formats.
* Pickadate format conversion: fix submit format.
* Pickadate format: rename variable.
* Pickadate format: remove additional hardcoded value.
* Pickadate format: fix unsupported removal order.
* Pickadate format: fix typo.
* Pickadate format: fix typo.
* Pickadate format: remove duplicated formats.
* Pickadate format: improved logic.
Also renamed variables.
* Update NEMO/apps/kiosk/templates/kiosk/tool_reservation.html
* Update NEMO/templates/mobile/new_reservation.html
* fixed wrong format for date
---------
Co-authored-by: Mathieu Rampant
---
.../templates/kiosk/tool_reservation.html | 12 ++---
NEMO/context_processors.py | 10 ++++-
NEMO/templates/mobile/new_reservation.html | 12 ++---
NEMO/utilities.py | 44 ++++++++++++++++++-
resources/settings.py | 9 ++++
5 files changed, 73 insertions(+), 14 deletions(-)
diff --git a/NEMO/apps/kiosk/templates/kiosk/tool_reservation.html b/NEMO/apps/kiosk/templates/kiosk/tool_reservation.html
index ad055baa..1bc8d3f2 100644
--- a/NEMO/apps/kiosk/templates/kiosk/tool_reservation.html
+++ b/NEMO/apps/kiosk/templates/kiosk/tool_reservation.html
@@ -56,12 +56,12 @@ When would you like to reserve the {{ tool }}?
{% for item in tool_reservation_times %}
unavailable_times.push([{{ item.start|date:"U" }},{{ item.end|date:"U" }}]);
{% endfor %}
- let date_picker = $('#date').pickadate({format: "dddd, mmmm d", formatSubmit: "yyyy-mm-dd", firstDay: 1, hiddenName: true, onSet: refresh_times});
- let start_time_picker = $('#start').pickatime({interval: 15, formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
- let end_time_picker = $('#end').pickatime({interval: 15, formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
+ let date_picker = $('#date').pickadate({format: "{{ pickadate_date_format }}", formatSubmit: "yyyy-mm-dd", firstDay: 1, hiddenName: true, onSet: refresh_times});
+ let start_time_picker = $('#start').pickatime({interval: 15, format: "{{ pickadate_time_format }}", formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
+ let end_time_picker = $('#end').pickatime({interval: 15, format: "{{ pickadate_time_format }}", formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
// set initial date
if ('{{ date|default_if_none:'' }}') {
- date_picker.pickadate('picker').set('select', '{{ date }}', {format: 'yyyy-mm-dd'})
+ date_picker.pickadate('picker').set('select', '{{ date }}', {format: '{{ pickadate_date_format }}'})
}
function refresh_times() {
start_time_picker.pickatime('picker').render();
@@ -77,11 +77,11 @@ When would you like to reserve the {{ tool }}?
let start = times[0];
let end = times[1];
if (date_time_selected >= start && date_time_selected < end) {
- return 'h:i A !alre!ad!y re!serve!d';
+ return '{{ pickadate_time_format }} !alre!ad!y re!serve!d';
}
}
}
- return "h:i A";
+ return '{{ pickadate_time_format }}';
}
revert(120)
diff --git a/NEMO/context_processors.py b/NEMO/context_processors.py
index 18f3e5f3..9897cfc7 100644
--- a/NEMO/context_processors.py
+++ b/NEMO/context_processors.py
@@ -1,5 +1,11 @@
from NEMO.models import Area, Notification, PhysicalAccessLevel, Tool, User
-from NEMO.utilities import date_input_js_format, datetime_input_js_format, time_input_js_format
+from NEMO.utilities import (
+ date_input_js_format,
+ datetime_input_js_format,
+ time_input_js_format,
+ pickadate_date_format,
+ pickadate_time_format,
+)
from NEMO.views.customization import CustomizationBase
from NEMO.views.notifications import get_notification_counts
@@ -84,6 +90,8 @@ def base_context(request):
"time_input_js_format": time_input_js_format,
"date_input_js_format": date_input_js_format,
"datetime_input_js_format": datetime_input_js_format,
+ "pickadate_date_format": pickadate_date_format,
+ "pickadate_time_format": pickadate_time_format,
"no_header": request.session.get("no_header", False),
"safety_menu_item": customization_values.get("safety_main_menu") == "enabled",
"calendar_page_title": customization_values.get("calendar_page_title"),
diff --git a/NEMO/templates/mobile/new_reservation.html b/NEMO/templates/mobile/new_reservation.html
index eea20145..ea2b370d 100644
--- a/NEMO/templates/mobile/new_reservation.html
+++ b/NEMO/templates/mobile/new_reservation.html
@@ -69,13 +69,13 @@ When would you like to reserve the {{ item }}?
{% for times in item_reservation_times %}
unavailable_times.push([{{ times.start|date:"U" }},{{ times.end|date:"U" }}]);
{% endfor %}
- let date_picker = $('#date').pickadate({format: "dddd, mmmm d", formatSubmit: "yyyy-mm-dd", firstDay: 1, hiddenName: true, onSet: refresh_times});
- let start_time_picker = $('#start').pickatime({interval: 15, formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
- let end_time_picker = $('#end').pickatime({interval: 15, formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
+ let date_picker = $('#date').pickadate({format: "{{ pickadate_date_format }}", formatSubmit: "yyyy-mm-dd", firstDay: 1, hiddenName: true, onSet: refresh_times});
+ let start_time_picker = $('#start').pickatime({interval: 15, format: "{{ pickadate_time_format }}", formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
+ let end_time_picker = $('#end').pickatime({interval: 15, format: "{{ pickadate_time_format }}", formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
// set initial date
if ('{{ date|default_if_none:'' }}')
{
- date_picker.pickadate('picker').set('select', '{{ date }}', {format: 'yyyy-mm-dd'})
+ date_picker.pickadate('picker').set('select', '{{ date }}', {format: '{{ pickadate_date_format }}'})
}
function refresh_times()
{
@@ -93,11 +93,11 @@ When would you like to reserve the {{ item }}?
let start = times[0];
let end = times[1];
if (date_time_selected >= start && date_time_selected < end) {
- return 'h:i A !alre!ad!y re!serve!d';
+ return '{{ pickadate_time_format }} !alre!ad!y re!serve!d';
}
}
}
- return "h:i A";
+ return "{{ pickadate_time_format }}";
}
diff --git a/NEMO/utilities.py b/NEMO/utilities.py
index b57a3ac1..f75f5035 100644
--- a/NEMO/utilities.py
+++ b/NEMO/utilities.py
@@ -64,6 +64,24 @@
"%%": "%",
}
+py_to_pick_date_formats = {
+ "%A": "dddd",
+ "%a": "ddd",
+ "%B": "mmmm",
+ "%b": "mmm",
+ "%d": "dd",
+ "%H": "HH",
+ "%I": "hh",
+ "%M": "i",
+ "%m": "mm",
+ "%p": "A",
+ "%X": "HH:i",
+ "%x": "mm/dd/yyyy",
+ "%Y": "yyyy",
+ "%y": "yy",
+ "%%": "%",
+}
+
# Convert a python format string to javascript format string
def convert_py_format_to_js(string_format: str) -> str:
@@ -72,13 +90,37 @@ def convert_py_format_to_js(string_format: str) -> str:
return string_format
+def convert_py_format_to_pickadate(string_format: str) -> str:
+ string_format = (
+ string_format.replace("%w", "")
+ .replace("%s", "")
+ .replace("%f", "")
+ .replace("%:z", "")
+ .replace("%z", "")
+ .replace("%Z", "")
+ .replace("%j", "")
+ .replace(":%S", "")
+ .replace("%S", "")
+ .replace("%U", "")
+ .replace("%W", "")
+ .replace("%c", "")
+ .replace("%G", "")
+ .replace("%u", "")
+ .replace("%V", "")
+ )
+ for py, pick in py_to_pick_date_formats.items():
+ string_format = pick.join(string_format.split(py))
+ return string_format
+
+
time_input_format = get_format("TIME_INPUT_FORMATS")[0]
date_input_format = get_format("DATE_INPUT_FORMATS")[0]
datetime_input_format = get_format("DATETIME_INPUT_FORMATS")[0]
time_input_js_format = convert_py_format_to_js(time_input_format)
date_input_js_format = convert_py_format_to_js(date_input_format)
datetime_input_js_format = convert_py_format_to_js(datetime_input_format)
-
+pickadate_date_format = getattr(settings, "PICKADATE_DATE_FORMAT", convert_py_format_to_pickadate(date_input_format))
+pickadate_time_format = getattr(settings, "PICKADATE_TIME_FORMAT", convert_py_format_to_pickadate(time_input_format))
supported_embedded_video_extensions = [".mp4", ".ogv", ".webm", ".3gp"]
supported_embedded_pdf_extensions = [".pdf"]
diff --git a/resources/settings.py b/resources/settings.py
index 02af55e5..ac032dcc 100644
--- a/resources/settings.py
+++ b/resources/settings.py
@@ -78,6 +78,15 @@
DATE_INPUT_FORMATS = ["%m/%d/%Y", *global_settings.DATE_INPUT_FORMATS]
TIME_INPUT_FORMATS = ["%I:%M:%S %p", *global_settings.TIME_INPUT_FORMATS]
+# -------------------- Pick date and time formats --------------------
+# Those formats are optional in most cases and only used on kiosk or mobile views, when picking up date/time separately.
+# If not defined, a conversion from DATE_INPUT_FORMATS and TIME_INPUT_FORMATS will be attempted.
+# See allowed date formats at https://amsul.ca/pickadate.js/date/#formatting-rules
+# See allowed time formats at https://amsul.ca/pickadate.js/time/#formatting-rules
+# PICKADATE_DATE_FORMAT = "mm/dd/yyyy"
+# PICKADATE_TIME_FORMAT = "HH:i A"
+
+
# -------------------- Internationalization and localization --------------------
# A boolean that specifies whether Django’s translation system should be enabled.
# This provides an easy way to turn it off, for performance.
From 45feacb5e3cb9e003a4ee49cbf68e6b6c5ea95be Mon Sep 17 00:00:00 2001
From: mrampant
Date: Thu, 20 Jun 2024 18:37:21 -0400
Subject: [PATCH 008/108] - added hybrid calendar feed for reservation and
usage
---
NEMO/templates/calendar/calendar.html | 9 +++++++--
NEMO/views/calendar.py | 13 +++++++++++++
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/NEMO/templates/calendar/calendar.html b/NEMO/templates/calendar/calendar.html
index b1f830bb..732fba37 100644
--- a/NEMO/templates/calendar/calendar.html
+++ b/NEMO/templates/calendar/calendar.html
@@ -34,6 +34,11 @@
href="javascript:void(0)"
onclick="change_calendar_event_type(this)">{{ facility_name }} usage
+
+ Reservations and usage
+
Date: Thu, 20 Jun 2024 18:57:29 -0400
Subject: [PATCH 009/108] - made the required unanswered questions email into a
customizable template (when someone forces a user off a tool but there were
required questions to answer)
---
.../customizations_templates.html | 22 ++++++++
NEMO/views/customization.py | 1 +
NEMO/views/tool_control.py | 53 ++++++++-----------
...l_required_unanswered_questions_email.html | 36 +++++++++++++
4 files changed, 80 insertions(+), 32 deletions(-)
create mode 100644 resources/emails/tool_required_unanswered_questions_email.html
diff --git a/NEMO/templates/customizations/customizations_templates.html b/NEMO/templates/customizations/customizations_templates.html
index f9a835e1..5f4d7c10 100644
--- a/NEMO/templates/customizations/customizations_templates.html
+++ b/NEMO/templates/customizations/customizations_templates.html
@@ -481,6 +481,28 @@