From 4c440589961a5ac6a83cb1c212de301fb0583c82 Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 18 Apr 2024 22:04:07 -0400 Subject: [PATCH 01/56] - starting 6.0.0.dev - upgrading Django to 4.2 LTS --- NEMO/migrations/0076_version_6_0_0.py | 19 +++++++++++++++ ...userpreferences_tool_task_notifications.py | 23 +++++++++++++++++++ NEMO/tests/test_formats.py | 9 +++++--- NEMO/utilities.py | 6 ++--- setup.py | 4 ++-- 5 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 NEMO/migrations/0076_version_6_0_0.py create mode 100644 NEMO/migrations/0077_alter_userpreferences_tool_task_notifications.py diff --git a/NEMO/migrations/0076_version_6_0_0.py b/NEMO/migrations/0076_version_6_0_0.py new file mode 100644 index 000000000..c081573a0 --- /dev/null +++ b/NEMO/migrations/0076_version_6_0_0.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.25 on 2024-04-17 16:18 + +from django.db import migrations + +from NEMO.migrations_utils import create_news_for_version + + +class Migration(migrations.Migration): + + dependencies = [ + ("NEMO", "0075_adjustmentrequest_applied"), + ] + + def new_version_news(apps, schema_editor): + create_news_for_version(apps, "6.0.0", "") + + operations = [ + migrations.RunPython(new_version_news), + ] diff --git a/NEMO/migrations/0077_alter_userpreferences_tool_task_notifications.py b/NEMO/migrations/0077_alter_userpreferences_tool_task_notifications.py new file mode 100644 index 000000000..01a62fec6 --- /dev/null +++ b/NEMO/migrations/0077_alter_userpreferences_tool_task_notifications.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.11 on 2024-04-18 03:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("NEMO", "0076_version_6_0_0"), + ] + + operations = [ + migrations.AlterField( + model_name="userpreferences", + name="tool_task_notifications", + field=models.ManyToManyField( + blank=True, + help_text="Tools to see maintenance records and receive task notifications for. If empty all notifications will be received.", + related_name="+", + to="NEMO.tool", + ), + ), + ] diff --git a/NEMO/tests/test_formats.py b/NEMO/tests/test_formats.py index 126a8b9ec..de3f02fa4 100644 --- a/NEMO/tests/test_formats.py +++ b/NEMO/tests/test_formats.py @@ -5,6 +5,7 @@ from django.test import TestCase from django.utils import timezone from django.utils.formats import date_format, time_format +from django.utils.timezone import make_aware from NEMO.utilities import format_daterange, format_datetime @@ -52,7 +53,7 @@ def test_format_daterange(self): tz = pytz.timezone("US/Pacific") self.assertEqual(settings.TIME_ZONE, "America/New_York") - start_tz = tz.localize(datetime.datetime(2022, 2, 11, 5, 0, 0)) # 5AM Pacific => 8AM 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( format_daterange(start_tz, end_tz, dt_format=dt_format), @@ -63,8 +64,10 @@ def test_format_daterange(self): f"from 02/11/2022 @ 8:00 AM to 02/13/2022 @ 8:00 AM", ) - start_midnight_tz = tz.localize(datetime.datetime(2022, 2, 11, 0, 0, 0)) # midnight Pacific -> 3AM Eastern - end_midnight_tz = tz.localize(datetime.datetime(2022, 2, 11, 23, 59, 0)) # 11:59 PM Pacific -> 2:59AM Eastern + start_midnight_tz = make_aware(datetime.datetime(2022, 2, 11, 0, 0, 0), tz) # midnight Pacific -> 3AM Eastern + end_midnight_tz = make_aware( + datetime.datetime(2022, 2, 11, 23, 59, 0), tz + ) # 11:59 PM Pacific -> 2:59AM Eastern self.assertEqual( format_daterange(start_midnight_tz, end_midnight_tz, dt_format=dt_format), f"from 02/11/2022 @ 3:00 AM to 02/12/2022 @ 2:59 AM", diff --git a/NEMO/utilities.py b/NEMO/utilities.py index e58f24567..e0bb6f8fe 100644 --- a/NEMO/utilities.py +++ b/NEMO/utilities.py @@ -32,7 +32,7 @@ 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 +from django.utils.timezone import is_naive, localtime, make_aware utilities_logger = getLogger(__name__) @@ -383,9 +383,9 @@ def as_timezone(dt): def localize(dt, tz=None): tz = tz or timezone.get_current_timezone() if isinstance(dt, list): - return [tz.localize(d) for d in dt] + return [make_aware(d, tz) for d in dt] else: - return tz.localize(dt) + return make_aware(dt, tz) def naive_local_current_datetime(): diff --git a/setup.py b/setup.py index 2aa207a89..e9b558364 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="NEMO", - version="5.5.0", + version="6.0.0.dev", python_requires=">=3.8, <4", packages=find_namespace_packages(exclude=["resources", "resources.*", "build", "build.*"]), include_package_data=True, @@ -29,7 +29,7 @@ ], install_requires=[ "cryptography==42.0.5", - "Django==3.2.25", + "Django==4.2.11", "django-auditlog==3.0.0", "django-filter==23.5", "django-mptt==0.14.0", From 4fbe27e9f32ac2c7632553f22e0570ad0aa1c182 Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 18 Apr 2024 22:22:34 -0400 Subject: [PATCH 02/56] - added more admin filter for tool operation modes --- NEMO/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEMO/admin.py b/NEMO/admin.py index a7f14dcc9..2e4a27df5 100644 --- a/NEMO/admin.py +++ b/NEMO/admin.py @@ -228,6 +228,7 @@ class ToolAdmin(admin.ModelAdmin): "_category", "visible", "operational_display", + "_operation_mode", "problematic", "is_configurable", "has_pre_usage_questions", @@ -239,6 +240,7 @@ class ToolAdmin(admin.ModelAdmin): list_filter = ( "visible", "_operational", + "_operation_mode", "_category", "_location", ("_requires_area_access", admin.RelatedOnlyFieldListFilter), From f80a89cf928de4ff77173bfe7c2774c4e52debdd Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 18 Apr 2024 22:23:25 -0400 Subject: [PATCH 03/56] - added confirmation dialog for marking adjustment as applied --- .../requests/adjustment_requests/adjustment_requests.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/NEMO/templates/requests/adjustment_requests/adjustment_requests.html b/NEMO/templates/requests/adjustment_requests/adjustment_requests.html index c62f71bf5..e0e2e7fe6 100644 --- a/NEMO/templates/requests/adjustment_requests/adjustment_requests.html +++ b/NEMO/templates/requests/adjustment_requests/adjustment_requests.html @@ -80,8 +80,11 @@

function mark_adjustment_as_applied(adjustment_id) { - let apply_url = "{% url "mark_adjustment_as_applied" 99999 %}".replace("99999", adjustment_id); - ajax_get(apply_url, undefined, function () {$("#apply_"+adjustment_id+", #applied_"+adjustment_id+", #edit_"+adjustment_id).remove()}, ajax_failure_callback("Request update failed", "There was a problem marking this adjustment as applied.")); + if (confirm("Are you sure you want to mark this adjustment as applied?")) + { + let apply_url = "{% url "mark_adjustment_as_applied" 99999 %}".replace("99999", adjustment_id); + ajax_get(apply_url, undefined, function () {$("#apply_"+adjustment_id+", #applied_"+adjustment_id+", #edit_"+adjustment_id).remove()}, ajax_failure_callback("Request update failed", "There was a problem marking this adjustment as applied.")); + } } function apply_adjustment(adjustment_id) From d9c3c53975045b60f1a484f84a7187b7194ba7a3 Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 18 Apr 2024 22:24:39 -0400 Subject: [PATCH 04/56] - updated wording on single entrance/exit tablet customization option --- NEMO/templates/customizations/customizations_application.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEMO/templates/customizations/customizations_application.html b/NEMO/templates/customizations/customizations_application.html index fd82bd507..10b5c6c2c 100644 --- a/NEMO/templates/customizations/customizations_application.html +++ b/NEMO/templates/customizations/customizations_application.html @@ -48,7 +48,8 @@

Application settings

name="area_logout_already_logged_in" {% if area_logout_already_logged_in %}checked{% endif %} value="enabled"> - Automatically log users out when they try to log in to an area they are already logged in (single entrance/exit tablet mode) + Automatically log users out when they try to log in to an area and they are already logged + into an area (single entrance/exit tablet mode) {% endif %} From 4589997a0bfa42698b6329050ee3d869a2f3a207 Mon Sep 17 00:00:00 2001 From: mrampant Date: Sun, 21 Apr 2024 16:46:30 -0400 Subject: [PATCH 05/56] - fixed loading of customizations not loading defaults in context_processors.py --- NEMO/context_processors.py | 23 ++++++++++++----------- NEMO/decorators.py | 3 +++ NEMO/views/customization.py | 6 ++++++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/NEMO/context_processors.py b/NEMO/context_processors.py index b12c3e360..cf814252f 100644 --- a/NEMO/context_processors.py +++ b/NEMO/context_processors.py @@ -1,5 +1,6 @@ -from NEMO.models import Area, Customization, Notification, PhysicalAccessLevel, Tool, User +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.views.customization import CustomizationBase from NEMO.views.notifications import get_notification_counts @@ -63,11 +64,11 @@ def base_context(request): facility_managers_exist = User.objects.filter(is_active=True, is_facility_manager=True).exists() except: facility_managers_exist = False - customization_values = {customization.name: customization.value for customization in Customization.objects.all()} + customization_values = CustomizationBase.get_all() return { - "facility_name": customization_values.get("facility_name", "Facility"), - "recurring_charges_name": customization_values.get("recurring_charges_name", "Recurring charges"), - "site_title": customization_values.get("site_title", ""), + "facility_name": customization_values.get("facility_name"), + "recurring_charges_name": customization_values.get("recurring_charges_name"), + "site_title": customization_values.get("site_title"), "device": getattr(request, "device", "desktop"), "tools_exist": tools_exist, "areas_exist": areas_exist, @@ -84,10 +85,10 @@ def base_context(request): "date_input_js_format": date_input_js_format, "datetime_input_js_format": datetime_input_js_format, "no_header": request.session.get("no_header", False), - "safety_menu_item": customization_values.get("safety_main_menu", "enabled") == "enabled", - "calendar_page_title": customization_values.get("calendar_page_title", "Calendar"), - "tool_control_page_title": customization_values.get("tool_control_page_title", "Tool control"), - "status_dashboard_page_title": customization_values.get("status_dashboard_page_title", "Status dashboard"), - "requests_page_title": customization_values.get("requests_page_title", "Requests"), - "safety_page_title": customization_values.get("safety_page_title", "Safety"), + "safety_menu_item": customization_values.get("safety_main_menu") == "enabled", + "calendar_page_title": customization_values.get("calendar_page_title"), + "tool_control_page_title": customization_values.get("tool_control_page_title"), + "status_dashboard_page_title": customization_values.get("status_dashboard_page_title"), + "requests_page_title": customization_values.get("requests_page_title"), + "safety_page_title": customization_values.get("safety_page_title"), } diff --git a/NEMO/decorators.py b/NEMO/decorators.py index d131146bc..fe23c6ddf 100644 --- a/NEMO/decorators.py +++ b/NEMO/decorators.py @@ -133,6 +133,9 @@ def decorator(view_func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url # For example, to replace NEMO.views.policy.check_policy_to_save_reservation(arg1, arg2) # @replace_function("NEMO.views.policy.check_policy_to_save_reservation") # def new_function(old_function, arg1, arg2) +# +# Note: this won't be executed when running management commands. To fix that, +# in the apps.py "ready" function, import the file where the annotated function is def replace_function(old_function_name, raise_exception=True): try: pkg, fun_name = old_function_name.rsplit(".", 1) diff --git a/NEMO/views/customization.py b/NEMO/views/customization.py index 019d04352..e5319e2b3 100644 --- a/NEMO/views/customization.py +++ b/NEMO/views/customization.py @@ -118,6 +118,12 @@ def get(cls, name: str, raise_exception=True) -> str: else: return default_value + @classmethod + def get_all(cls) -> Dict: + customization_values = cls.all_variables() + customization_values.update({cust.name: cust.value for cust in Customization.objects.all() if cust.value}) + return customization_values + @classmethod def get_int(cls, name: str, default=None, raise_exception=True) -> int: return quiet_int(cls.get(name, raise_exception), default) From df308273d6fe8fabed540ee70a8ebb3bf253fa7c Mon Sep 17 00:00:00 2001 From: mrampant Date: Sun, 21 Apr 2024 17:28:04 -0400 Subject: [PATCH 06/56] - project usage and project billing will now allow searching for any user (even inactives) --- NEMO/views/usage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEMO/views/usage.py b/NEMO/views/usage.py index 9c2d6d033..ade864cc3 100644 --- a/NEMO/views/usage.py +++ b/NEMO/views/usage.py @@ -258,7 +258,7 @@ def project_usage(request): "search_items": set(Account.objects.all()) | set(Project.objects.all()) | set(get_project_applications()) - | set(User.objects.filter(is_active=True)), + | set(User.objects.all()), "area_access": area_access, "consumables": consumables, "missed_reservations": missed_reservations, @@ -290,7 +290,7 @@ def project_billing(request): set(Account.objects.all()) | set(Project.objects.all()) | set(get_project_applications()) - | set(User.objects.filter(is_active=True)) + | set(User.objects.all()) ) project_id = None From 73152e56a2ddb53cbcb3f8428644b581871fa3c4 Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 25 Apr 2024 09:22:22 -0400 Subject: [PATCH 07/56] - fixed migration path and names --- .../migrations/{0076_version_6_0_0.py => 0078_version_6_0_0.py} | 2 +- ...py => 0079_alter_userpreferences_tool_task_notifications.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename NEMO/migrations/{0076_version_6_0_0.py => 0078_version_6_0_0.py} (88%) rename NEMO/migrations/{0077_alter_userpreferences_tool_task_notifications.py => 0079_alter_userpreferences_tool_task_notifications.py} (93%) diff --git a/NEMO/migrations/0076_version_6_0_0.py b/NEMO/migrations/0078_version_6_0_0.py similarity index 88% rename from NEMO/migrations/0076_version_6_0_0.py rename to NEMO/migrations/0078_version_6_0_0.py index c081573a0..22df1be41 100644 --- a/NEMO/migrations/0076_version_6_0_0.py +++ b/NEMO/migrations/0078_version_6_0_0.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ("NEMO", "0075_adjustmentrequest_applied"), + ("NEMO", "0077_interlock_name"), ] def new_version_news(apps, schema_editor): diff --git a/NEMO/migrations/0077_alter_userpreferences_tool_task_notifications.py b/NEMO/migrations/0079_alter_userpreferences_tool_task_notifications.py similarity index 93% rename from NEMO/migrations/0077_alter_userpreferences_tool_task_notifications.py rename to NEMO/migrations/0079_alter_userpreferences_tool_task_notifications.py index 01a62fec6..d4b74ea34 100644 --- a/NEMO/migrations/0077_alter_userpreferences_tool_task_notifications.py +++ b/NEMO/migrations/0079_alter_userpreferences_tool_task_notifications.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("NEMO", "0076_version_6_0_0"), + ("NEMO", "0078_version_6_0_0"), ] operations = [ From 8f82c4052b9e52b3e83b95f09eb03cf503185e83 Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 25 Apr 2024 22:53:25 -0400 Subject: [PATCH 08/56] - fixed iframe preview not working in admin with django 4.2 --- NEMO/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NEMO/admin.py b/NEMO/admin.py index 591391a22..8fadbcdd0 100644 --- a/NEMO/admin.py +++ b/NEMO/admin.py @@ -1448,7 +1448,7 @@ def alert_preview(self, obj: Closure): display_title = f'{obj.name}
' if obj.name else "" return iframe_content( f'
{display_title}{linebreaksbr(obj.closuretime_set.first().alert_contents())}
', - extra_style="padding-bottom: 20%", + extra_style="padding-bottom: 15%", ) except Exception: pass @@ -1916,9 +1916,9 @@ class PermissionAdmin(admin.ModelAdmin): form = PermissionAdminForm -def iframe_content(content, extra_style="padding-bottom: 75%") -> str: +def iframe_content(content, extra_style="padding-bottom: 65%") -> str: return mark_safe( - f'
' + f'
' ) From 1bd5537f5f0f63eba7be256d58ff7049c3986cc9 Mon Sep 17 00:00:00 2001 From: mrampant Date: Mon, 29 Apr 2024 19:02:26 -0400 Subject: [PATCH 09/56] - added customization to hide inactive projects in the individual account page --- NEMO/models.py | 3 +++ .../account_and_projects.html | 2 +- .../customizations_projects_and_accounts.html | 17 +++++++++++++++++ NEMO/views/accounts_and_projects.py | 5 +++++ NEMO/views/customization.py | 1 + 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/NEMO/models.py b/NEMO/models.py index 724a7ded4..a8656e1e0 100644 --- a/NEMO/models.py +++ b/NEMO/models.py @@ -2533,6 +2533,9 @@ class Account(SerializationByNameModel): class Meta: ordering = ["name"] + def sorted_active_projects(self): + return self.sorted_projects().filter(active=True) + def sorted_projects(self): return self.project_set.all().order_by("-active", "name") diff --git a/NEMO/templates/accounts_and_projects/account_and_projects.html b/NEMO/templates/accounts_and_projects/account_and_projects.html index e9f31f445..7a4d6fd60 100644 --- a/NEMO/templates/accounts_and_projects/account_and_projects.html +++ b/NEMO/templates/accounts_and_projects/account_and_projects.html @@ -41,7 +41,7 @@

{% endif %} - {% for project in account.project_set.all %} + {% for project in account_projects %} {% if not selected_project or project == selected_project %}

Check this box to only show active accounts in the account list. +
+
+
+ +
+
+
+ Check this box to only show active projects when looking at an individual account. +
+
diff --git a/NEMO/views/accounts_and_projects.py b/NEMO/views/accounts_and_projects.py index 392a77e11..1e3c1a33f 100644 --- a/NEMO/views/accounts_and_projects.py +++ b/NEMO/views/accounts_and_projects.py @@ -90,8 +90,13 @@ def select_accounts_and_projects(request, kind=None, identifier=None): account = None except: account = None + account_projects = [] + if account: + active_only = ProjectsAccountsCustomization.get_bool("account_project_list_active_only") + account_projects = account.sorted_active_projects() if active_only else account.sorted_projects() dictionary = { "account": account, + "account_projects": account_projects, "selected_project": selected_project, "accounts_and_projects": set(get_accounts_and_projects()), "users": User.objects.all(), diff --git a/NEMO/views/customization.py b/NEMO/views/customization.py index e5319e2b3..d086d5707 100644 --- a/NEMO/views/customization.py +++ b/NEMO/views/customization.py @@ -210,6 +210,7 @@ class ProjectsAccountsCustomization(CustomizationBase): "project_application_identifier_name": "Application identifier", "project_allow_document_upload": "", "account_list_active_only": "", + "account_project_list_active_only": "", "project_list_active_only": "", "account_list_collapse": "", "project_allow_pi_manage_users": "", From 78b0589620b91d352c9a96bf13aec5336e905420 Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 2 May 2024 22:56:07 -0400 Subject: [PATCH 10/56] - fixed seeing link text decoration in button when impersonating --- NEMO/templates/base/impersonate_header.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEMO/templates/base/impersonate_header.html b/NEMO/templates/base/impersonate_header.html index 438e473c8..4e75659ef 100644 --- a/NEMO/templates/base/impersonate_header.html +++ b/NEMO/templates/base/impersonate_header.html @@ -1,5 +1,6 @@ From 5ac2803062da03b179a3405576c4ec20d2f5fcb3 Mon Sep 17 00:00:00 2001 From: mrampant Date: Fri, 3 May 2024 17:46:32 -0400 Subject: [PATCH 11/56] - fixed reporting a problem requiring tool shutdown when ALLOW_CONDITIONAL_URLS is False (off campus) --- NEMO/apps/kiosk/views.py | 19 +++++++++++++------ NEMO/views/tasks.py | 21 +++++++++------------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/NEMO/apps/kiosk/views.py b/NEMO/apps/kiosk/views.py index 18288724a..52a8b163d 100644 --- a/NEMO/apps/kiosk/views.py +++ b/NEMO/apps/kiosk/views.py @@ -1,10 +1,12 @@ from datetime import datetime, timedelta from http import HTTPStatus +from django.conf import settings from django.contrib.auth.decorators import login_required, permission_required from django.shortcuts import render, redirect from django.utils import timezone from django.utils.dateparse import parse_date, parse_time +from django.utils.html import format_html from django.views.decorators.http import require_GET, require_POST from NEMO.decorators import synchronized @@ -597,15 +599,20 @@ def report_problem(request): return render(request, "kiosk/tool_report_problem.html", dictionary) + if not settings.ALLOW_CONDITIONAL_URLS and form.cleaned_data["force_shutdown"]: + site_title = ApplicationCustomization.get("site_title") + dictionary["message"] = format_html( + '
  • {}
'.format( + f"Tool control is only available on campus. When creating a task, you can't force a tool shutdown while using {site_title} off campus.", + ) + ) + dictionary["form"] = form + return render(request, "kiosk/tool_report_problem.html", dictionary) + task = form.save() task.estimated_resolution_time = estimated_resolution_time - save_error = save_task(request, task) - - if save_error: - dictionary["message"] = save_error - dictionary["form"] = form - return render(request, "kiosk/tool_report_problem.html", dictionary) + save_task(request, task) return redirect("kiosk_tool_information", tool_id=tool.id, user_id=customer.id, back=back) diff --git a/NEMO/views/tasks.py b/NEMO/views/tasks.py index 91060d1db..044ab6308 100644 --- a/NEMO/views/tasks.py +++ b/NEMO/views/tasks.py @@ -51,7 +51,7 @@ @require_POST def create(request): """ - This function handles feedback from users. This could be a problem report or shutdown notification. + This could be a problem report or shutdown notification. """ images_form = TaskImagesForm(request.POST, request.FILES) form = TaskForm(request.user, data=request.POST) @@ -64,28 +64,27 @@ def create(request): "content": errors.as_ul(), } return render(request, "acknowledgement.html", dictionary) - task = form.save() - task_images = save_task_images(request, task) - save_error = save_task(request, task, task_images) + if not settings.ALLOW_CONDITIONAL_URLS and form.cleaned_data["force_shutdown"]: + site_title = ApplicationCustomization.get("site_title") - if save_error: dictionary = { "title": "Task creation failed", "heading": "Something went wrong while reporting the problem", - "content": save_error, + "content": f"Tool control is only available on campus. When creating a task, you can't force a tool shutdown while using {site_title} off campus.", } return render(request, "acknowledgement.html", dictionary) + task = form.save() + task_images = save_task_images(request, task) + + save_task(request, task, task_images) + return redirect("tool_control") def save_task(request, task: Task, task_images: List[TaskImages] = None): task.save() - if not settings.ALLOW_CONDITIONAL_URLS and task.force_shutdown: - site_title = ApplicationCustomization.get("site_title") - - return f"Tool control is only available on campus. When creating a task, you can't force a tool shutdown while using {site_title} off campus." if task.force_shutdown: # Shut down the tool. @@ -113,8 +112,6 @@ def save_task(request, task: Task, task_images: List[TaskImages] = None): send_new_task_emails(request, task, task_images) set_task_status(request, task, request.POST.get("status"), request.user) - return None - def send_new_task_emails(request, task: Task, task_images: List[TaskImages]): message = get_media_file_contents("new_task_email.html") From d4d0e84faec173953f1d6cec930426739d72f1cb Mon Sep 17 00:00:00 2001 From: mrampant Date: Sat, 4 May 2024 07:53:51 -0400 Subject: [PATCH 12/56] - adding tool owners, backup owners and superusers to the list when emailing all qualified users of a tool --- NEMO/views/email.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/NEMO/views/email.py b/NEMO/views/email.py index cc18ec4e9..354f1789d 100644 --- a/NEMO/views/email.py +++ b/NEMO/views/email.py @@ -313,7 +313,18 @@ def get_users_for_email(audience: str, selection: List, no_type: bool) -> (Query users = User.objects.none() topic = None if audience == "tool": - users = User.objects.filter(qualifications__id__in=selection).distinct() + # add all owners to the list in case they are not directly qualified + owners_user_ids = [] + for tool in Tool.objects.filter(id__in=selection).prefetch_related( + "_primary_owner", "_backup_owners", "_superusers" + ): + owners_user_ids.append(tool.primary_owner.id) + owners_user_ids.extend(tool.backup_owners.values_list("id", flat=True)) + owners_user_ids.extend(tool.superusers.values_list("id", flat=True)) + # combine the querysets + users = User.objects.filter(qualifications__id__in=selection) + users = users | User.objects.filter(id__in=owners_user_ids) + users = users.distinct() if len(selection) == 1: topic = Tool.objects.filter(pk=selection[0]).first().name elif audience == "tool-reservation": From 1d02d3efdd759f7013f053025de803f863565c33 Mon Sep 17 00:00:00 2001 From: mrampant Date: Sat, 4 May 2024 17:18:00 -0400 Subject: [PATCH 13/56] - fixed wording in inactive.html --- NEMO/apps/area_access/templates/area_access/inactive.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEMO/apps/area_access/templates/area_access/inactive.html b/NEMO/apps/area_access/templates/area_access/inactive.html index ade369814..fb9577dbb 100644 --- a/NEMO/apps/area_access/templates/area_access/inactive.html +++ b/NEMO/apps/area_access/templates/area_access/inactive.html @@ -1,3 +1,3 @@

Your account was deactivated.

-

Please visit the {{ facility_name }} staff to resolve the problem.

+

Please contact the {{ facility_name }} staff to resolve the problem.

From 353a6e925e4f49edac1af16f75671e8d7ffaf097 Mon Sep 17 00:00:00 2001 From: mrampant Date: Sat, 4 May 2024 17:24:56 -0400 Subject: [PATCH 14/56] - fixed wording in no_active_projects.html --- .../area_access/templates/area_access/no_active_projects.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEMO/apps/area_access/templates/area_access/no_active_projects.html b/NEMO/apps/area_access/templates/area_access/no_active_projects.html index 5777d0738..a38dc2166 100644 --- a/NEMO/apps/area_access/templates/area_access/no_active_projects.html +++ b/NEMO/apps/area_access/templates/area_access/no_active_projects.html @@ -1,4 +1,4 @@

You are not a member of any active projects.

You won't be able to use any interlocked {{ facility_name }} tools.

-

Please visit the {{ facility_name }} user office for more information.

+

Please contact {{ facility_name }} staff for more information.

From 12a847996403afde796035d763a39b3f8b61fa1b Mon Sep 17 00:00:00 2001 From: mrampant Date: Mon, 6 May 2024 23:36:17 -0400 Subject: [PATCH 15/56] - added option in Customization -> User to require user types when adding/editing users --- NEMO/models.py | 15 +++++++++---- .../customizations/customizations_user.html | 22 +++++++++++++++++++ .../users/create_or_modify_user.html | 3 +++ NEMO/views/customization.py | 1 + 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/NEMO/models.py b/NEMO/models.py index 1adee5b87..ad92a0de5 100644 --- a/NEMO/models.py +++ b/NEMO/models.py @@ -832,20 +832,27 @@ def natural_key(self): return (self.get_username(),) def clean(self): + from NEMO.views.customization import UserCustomization + + user_type_required = UserCustomization.get_bool("user_type_required") + if user_type_required and UserType.objects.exists() and not self.type_id: + raise ValidationError({"type": _("This field is required.")}) username_pattern = getattr(settings, "USERNAME_REGEX", None) if self.username: if username_pattern and not match(username_pattern, self.username): - raise ValidationError({"username": "Invalid username format"}) + raise ValidationError({"username": _("Invalid username format")}) username_taken = User.objects.filter(username__iexact=self.username) if self.pk: username_taken = username_taken.exclude(pk=self.pk) if username_taken.exists(): - raise ValidationError({"username": "This username has already been taken"}) + raise ValidationError({"username": _("This username has already been taken")}) if self.is_staff and self.is_service_personnel: raise ValidationError( { - "is_staff": "A user cannot be both staff and service personnel. Please choose one or the other.", - "is_service_personnel": "A user cannot be both staff and service personnel. Please choose one or the other.", + "is_staff": _("A user cannot be both staff and service personnel. Please choose one or the other."), + "is_service_personnel": _( + "A user cannot be both staff and service personnel. Please choose one or the other." + ), } ) diff --git a/NEMO/templates/customizations/customizations_user.html b/NEMO/templates/customizations/customizations_user.html index e105f6862..b3d37d9c4 100644 --- a/NEMO/templates/customizations/customizations_user.html +++ b/NEMO/templates/customizations/customizations_user.html @@ -30,6 +30,28 @@

General

Whether the facility rules tutorial is required for new users by default.
+
+ +
+
+ + +
+
+
+ Whether the user type is required when adding new users. +
+
diff --git a/NEMO/templates/users/create_or_modify_user.html b/NEMO/templates/users/create_or_modify_user.html index 06682554c..034d32c12 100644 --- a/NEMO/templates/users/create_or_modify_user.html +++ b/NEMO/templates/users/create_or_modify_user.html @@ -135,6 +135,9 @@ {% endfor %}
+ {% if form.type.errors %} +
{{ form.type.errors|striptags }}
+ {% endif %}
{% endif %}
diff --git a/NEMO/views/customization.py b/NEMO/views/customization.py index 628767b76..5c7b1431b 100644 --- a/NEMO/views/customization.py +++ b/NEMO/views/customization.py @@ -230,6 +230,7 @@ def validate(self, name, value): class UserCustomization(CustomizationBase): variables = { "default_user_training_not_required": "", + "user_type_required": "", "user_list_active_only": "", "user_access_expiration_reminder_days": "", "user_access_expiration_reminder_cc": "", From 7a457e04b95e25dea1ac7ee8e3e1aa5bce9000af Mon Sep 17 00:00:00 2001 From: r-xyz <100710244+r-xyz@users.noreply.github.com> Date: Tue, 7 May 2024 17:16:24 +0200 Subject: [PATCH 16/56] Enable training for non-visible tools. (#221) * Enable training for non-visible tools. Training on hidden tools can now be re-enabled using the corresponding customization setting. --- .../customizations/customizations_training.html | 15 +++++++++++++++ NEMO/views/customization.py | 2 +- NEMO/views/training.py | 5 ++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/NEMO/templates/customizations/customizations_training.html b/NEMO/templates/customizations/customizations_training.html index 3845178c2..3083ea3e8 100644 --- a/NEMO/templates/customizations/customizations_training.html +++ b/NEMO/templates/customizations/customizations_training.html @@ -36,6 +36,21 @@

Training

Qualifications will still happen in real time
+
+ +
+
+ +
+
+
{% button type="save" value="Save settings" %}
diff --git a/NEMO/views/customization.py b/NEMO/views/customization.py index 5c7b1431b..1e6374f86 100644 --- a/NEMO/views/customization.py +++ b/NEMO/views/customization.py @@ -551,7 +551,7 @@ class RemoteWorkCustomization(CustomizationBase): @customization(key="training", title="Training") class TrainingCustomization(CustomizationBase): - variables = {"training_only_type": "", "training_allow_date": ""} + variables = {"training_only_type": "", "training_allow_date": "", "training_allow_hidden_tools": ""} def context(self) -> Dict: dictionary = super().context() diff --git a/NEMO/views/training.py b/NEMO/views/training.py index feeb68615..9e4db23bd 100644 --- a/NEMO/views/training.py +++ b/NEMO/views/training.py @@ -28,7 +28,10 @@ def training(request): """Present a web page to allow staff or tool superusers to charge training and qualify users on particular tools.""" user: User = request.user users = User.objects.filter(is_active=True).exclude(id=user.id) - tools = Tool.objects.filter(visible=True) + hidden_allowed = TrainingCustomization.get_bool("training_allow_hidden_tools") + tools = Tool.objects.all() + if not hidden_allowed: + tools = tools.filter(visible=True) tool_groups = ToolQualificationGroup.objects.all() if not user.is_staff and user.is_tool_superuser: tools = tools.filter(_superusers__in=[user]) From 0349c5e1537cad88a9f7b22431674d80706cdb62 Mon Sep 17 00:00:00 2001 From: mrampant Date: Tue, 7 May 2024 11:37:53 -0400 Subject: [PATCH 17/56] - [API] added recurring consumable charges endpoint --- NEMO/serializers.py | 13 +++++++++++++ NEMO/urls.py | 1 + NEMO/views/api.py | 27 +++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/NEMO/serializers.py b/NEMO/serializers.py index 4f8ddc921..303934552 100644 --- a/NEMO/serializers.py +++ b/NEMO/serializers.py @@ -35,6 +35,7 @@ Project, ProjectDiscipline, Qualification, + RecurringConsumableCharge, Reservation, Resource, ScheduledOutage, @@ -398,6 +399,18 @@ class Meta: } +class RecurringConsumableChargeSerializer(FlexFieldsSerializerMixin, ModelSerializer): + class Meta: + model = RecurringConsumableCharge + fields = "__all__" + expandable_fields = { + "customer": "NEMO.serializers.UserSerializer", + "last_updated_by": "NEMO.serializers.UserSerializer", + "consumable": "NEMO.serializers.ConsumableSerializer", + "project": "NEMO.serializers.ProjectSerializer", + } + + class PermissionSerializer(FlexFieldsSerializerMixin, ModelSerializer): users = PrimaryKeyRelatedField( source="user_set", many=True, queryset=User.objects.all(), allow_null=True, required=False diff --git a/NEMO/urls.py b/NEMO/urls.py index 362fdb169..37a91a4b8 100644 --- a/NEMO/urls.py +++ b/NEMO/urls.py @@ -92,6 +92,7 @@ def sort_urls(url_path): router.register(r"project_disciplines", api.ProjectDisciplineViewSet) router.register(r"projects", api.ProjectViewSet) router.register(r"qualifications", api.QualificationViewSet) +router.register(r"recurring_consumable_charges", api.RecurringConsumableChargesViewSet) router.register(r"reservations", api.ReservationViewSet) router.register(r"reservation_configuration_options", api.ConfigurationOptionViewSet) router.register(r"resources", api.ResourceViewSet) diff --git a/NEMO/views/api.py b/NEMO/views/api.py index c969f2e2e..7317aa415 100644 --- a/NEMO/views/api.py +++ b/NEMO/views/api.py @@ -27,6 +27,7 @@ Project, ProjectDiscipline, Qualification, + RecurringConsumableCharge, Reservation, Resource, ScheduledOutage, @@ -60,6 +61,7 @@ ProjectDisciplineSerializer, ProjectSerializer, QualificationSerializer, + RecurringConsumableChargeSerializer, ReservationSerializer, ResourceSerializer, ScheduledOutageSerializer, @@ -319,6 +321,31 @@ class ConfigurationOptionViewSet(ModelViewSet): } +class RecurringConsumableChargesViewSet(ModelViewSet): + filename = "recurring_consumable_charges" + queryset = RecurringConsumableCharge.objects.all() + serializer_class = RecurringConsumableChargeSerializer + filterset_fields = { + "id": ["exact", "in"], + "customer_id": ["exact", "in", "isnull"], + "customer": ["exact", "in", "isnull"], + "consumable_id": ["exact", "in"], + "consumable": ["exact", "in"], + "project_id": ["exact", "in", "isnull"], + "project": ["exact", "in", "isnull"], + "quantity": ["exact", "gte", "lte", "gt", "lt"], + "last_charge": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"], + "rec_start": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"], + "rec_frequency": ["exact", "in", "isnull"], + "rec_interval": ["exact", "in", "isnull"], + "rec_until": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"], + "rec_count": ["exact", "in", "isnull"], + "last_updated": ["month", "year", "day", "gte", "gt", "lte", "lt"], + "last_updated_by": ["exact", "in", "isnull"], + "last_updated_by_id": ["exact", "in", "isnull"], + } + + class ReservationViewSet(ModelViewSet): filename = "reservations" queryset = Reservation.objects.all() From 7510ac6b80ee85c5b5d8ede7a626f6b3e6547f93 Mon Sep 17 00:00:00 2001 From: mrampant Date: Tue, 7 May 2024 12:17:44 -0400 Subject: [PATCH 18/56] - fixed wording with facility staff in multiple html files --- .../area_access/templates/area_access/already_logged_in.html | 2 +- .../area_access/templates/area_access/badge_not_found.html | 2 +- .../apps/area_access/templates/area_access/not_logged_in.html | 2 +- .../kiosk/templates/kiosk/tool_project_selection_snippet.html | 2 +- NEMO/views/area_access.py | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/NEMO/apps/area_access/templates/area_access/already_logged_in.html b/NEMO/apps/area_access/templates/area_access/already_logged_in.html index c4b953503..a428fe818 100644 --- a/NEMO/apps/area_access/templates/area_access/already_logged_in.html +++ b/NEMO/apps/area_access/templates/area_access/already_logged_in.html @@ -10,7 +10,7 @@

However, a scheduled outage is in effect in the {{ scheduled_outage_in_progr {% elif reservation_requirement_failed %}

However, you cannot enter since you don't have a current reservation for this area.

{% endif %} -

Please visit the {{ facility_name }} staff if you believe this is an error.

+

Please contact {{ facility_name }} staff if you believe this is an error.

{% if not reservation_requirement_failed and not max_capacity_reached and not scheduled_outage_in_progress %}

What would you like to do?

diff --git a/NEMO/apps/area_access/templates/area_access/badge_not_found.html b/NEMO/apps/area_access/templates/area_access/badge_not_found.html index 881ff6ce4..cb25b4e46 100644 --- a/NEMO/apps/area_access/templates/area_access/badge_not_found.html +++ b/NEMO/apps/area_access/templates/area_access/badge_not_found.html @@ -1,6 +1,6 @@

Your badge wasn't recognized.

If you got a new one recently then we'll need to update your account.

-

Please visit the {{ facility_name }} staff to resolve the problem.

+

Please contact {{ facility_name }} staff to resolve the problem.

diff --git a/NEMO/apps/area_access/templates/area_access/not_logged_in.html b/NEMO/apps/area_access/templates/area_access/not_logged_in.html index 9af494169..2916f9a6a 100644 --- a/NEMO/apps/area_access/templates/area_access/not_logged_in.html +++ b/NEMO/apps/area_access/templates/area_access/not_logged_in.html @@ -1,5 +1,5 @@

According to our records, you're not logged in to any access controlled areas.

-

Please visit the {{ facility_name }} user office if you believe this is an error.

+

Please contact {{ facility_name }} staff if you believe this is an error.

diff --git a/NEMO/apps/kiosk/templates/kiosk/tool_project_selection_snippet.html b/NEMO/apps/kiosk/templates/kiosk/tool_project_selection_snippet.html index 75c181f22..26149338c 100644 --- a/NEMO/apps/kiosk/templates/kiosk/tool_project_selection_snippet.html +++ b/NEMO/apps/kiosk/templates/kiosk/tool_project_selection_snippet.html @@ -8,7 +8,7 @@

{% else %} use {% endif %} - any tools because you are not a member of an active project. Please visit the {{ facility_name }} staff to begin a project. + any tools because you are not a member of an active project. Please contact {{ facility_name }} staff to begin a project.

{% elif active_projects|length == 1 %}

diff --git a/NEMO/views/area_access.py b/NEMO/views/area_access.py index cf1297e7a..4995479a2 100644 --- a/NEMO/views/area_access.py +++ b/NEMO/views/area_access.py @@ -308,12 +308,12 @@ def self_log_in(request, load_areas=True): policy.check_to_enter_any_area(user) except InactiveUserError: dictionary["error_message"] = ( - f"Your account has been deactivated. Please visit the {facility_name} staff to resolve the problem." + f"Your account has been deactivated. Please contact {facility_name} staff to resolve the problem." ) return render(request, "area_access/self_login.html", dictionary) except NoActiveProjectsForUserError: dictionary["error_message"] = ( - f"You are not a member of any active projects. You won't be able to use any interlocked {facility_name} tools. Please visit the {facility_name} user office for more information." + f"You are not a member of any active projects. You won't be able to use any interlocked {facility_name} tools. Please contact {facility_name} staff for more information." ) return render(request, "area_access/self_login.html", dictionary) except PhysicalAccessExpiredUserError: From 1735b34e2ee890df819ef1f2931bed4e7443f3d2 Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 9 May 2024 15:03:23 -0400 Subject: [PATCH 19/56] - removed unused parameter in kiosk --- NEMO/apps/kiosk/views.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/NEMO/apps/kiosk/views.py b/NEMO/apps/kiosk/views.py index 52a8b163d..c4fef8cbc 100644 --- a/NEMO/apps/kiosk/views.py +++ b/NEMO/apps/kiosk/views.py @@ -3,7 +3,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required, permission_required -from django.shortcuts import render, redirect +from django.shortcuts import redirect, render from django.utils import timezone from django.utils.dateparse import parse_date, parse_time from django.utils.html import format_html @@ -11,18 +11,18 @@ from NEMO.decorators import synchronized from NEMO.exceptions import RequiredUnansweredQuestionsException -from NEMO.forms import CommentForm, nice_errors, TaskForm +from NEMO.forms import CommentForm, TaskForm, nice_errors from NEMO.models import ( BadgeReader, Project, Reservation, ReservationItemType, + TaskCategory, + TaskStatus, Tool, ToolWaitList, UsageEvent, User, - TaskCategory, - TaskStatus, ) from NEMO.policy import policy_class as policy from NEMO.utilities import localize, quiet_int @@ -35,7 +35,6 @@ shorten_reservation, ) from NEMO.views.customization import ApplicationCustomization, ToolCustomization -from NEMO.views.status_dashboard import create_tool_summary from NEMO.views.tasks import save_task from NEMO.views.tool_control import ( email_managers_required_questions_disable_tool, @@ -409,7 +408,6 @@ def choices(request): "customer": customer, "usage_events": list(usage_events), "upcoming_reservations": tool_reservations, - "tool_summary": create_tool_summary(), "categories": categories, "unqualified_categories": unqualified_categories, } @@ -435,7 +433,6 @@ def category_choices(request, category, user_id): "unqualified_tools": [ tool for tool in tools if not customer.is_staff and tool not in customer.qualifications.all() ], - "tool_summary": create_tool_summary(), } return render(request, "kiosk/category_choices.html", dictionary) From 54a3a1dfa510f0a7d2309b34c451de91c8a8a7b3 Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 9 May 2024 15:41:45 -0400 Subject: [PATCH 20/56] - allowing to apply adjustment request for missed reservations --- NEMO/models.py | 34 ++++++++++++------- .../adjustment_request.html | 2 +- .../adjustment_requests_table.html | 2 +- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/NEMO/models.py b/NEMO/models.py index ad92a0de5..c871197bd 100644 --- a/NEMO/models.py +++ b/NEMO/models.py @@ -4207,7 +4207,10 @@ def get_time_difference(self) -> str: else f"- {(previous_duration - new_duration)}" ) - def editable_charge(self) -> bool: + def adjustable_charge(self): + return self.has_changed_time() or isinstance(self.item, Reservation) + + def has_changed_time(self) -> bool: """Returns whether the original charge is editable, i.e. if it has a changed start or end""" return self.item and (self.get_new_end() or self.get_new_start()) @@ -4233,17 +4236,24 @@ def reviewers(self) -> QuerySetType[User]: return facility_managers def apply_adjustment(self, user): - if self.status == RequestStatus.APPROVED and self.editable_charge(): - new_start = self.get_new_start() - new_end = self.get_new_end() - if new_start: - self.item.start = new_start - if new_end: - self.item.end = new_end - self.item.save() - self.applied = True - self.applied_by = user - self.save() + if self.status == RequestStatus.APPROVED: + if self.has_changed_time(): + new_start = self.get_new_start() + new_end = self.get_new_end() + if new_start: + self.item.start = new_start + if new_end: + self.item.end = new_end + self.item.save() + self.applied = True + self.applied_by = user + self.save() + elif isinstance(self.item, Reservation): + self.item.missed = False + self.item.save() + self.applied = True + self.applied_by = user + self.save() def delete(self, using=None, keep_parents=False): adjustment_id = self.id diff --git a/NEMO/templates/requests/adjustment_requests/adjustment_request.html b/NEMO/templates/requests/adjustment_requests/adjustment_request.html index 0208fe475..6f709cfcb 100644 --- a/NEMO/templates/requests/adjustment_requests/adjustment_request.html +++ b/NEMO/templates/requests/adjustment_requests/adjustment_request.html @@ -128,7 +128,7 @@

{% if form.instance.get_status_display == "Pending" %} {% if instance_id and user in form.instance.reviewers %} - {% if form.instance.editable_charge and "requests"|customization:"adjustment_requests_apply_button" == "enabled" %} + {% if form.instance.adjustable_charge and "requests"|customization:"adjustment_requests_apply_button" == "enabled" %} {% button type="save" submit=False name="approve_apply_request" title="Approve and adjust the original charge" icon="glyphicon-ok-circle" value="Approve and adjust" onclick="confirm_review_dialog(this, 'approve and adjust the actual charge of');" %} {% button type="save" submit=False name="approve_request" title="Approve without adjusting" icon="glyphicon-ok-circle" value="Approve only" onclick="confirm_review_dialog(this);" %} {% else %} diff --git a/NEMO/templates/requests/adjustment_requests/adjustment_requests_table.html b/NEMO/templates/requests/adjustment_requests/adjustment_requests_table.html index e4fd7ef5c..bd06f3096 100644 --- a/NEMO/templates/requests/adjustment_requests/adjustment_requests_table.html +++ b/NEMO/templates/requests/adjustment_requests/adjustment_requests_table.html @@ -34,7 +34,7 @@ {% if request_status == 'approved' or request_status == 'denied' %} {{ a_request.reviewer.get_name }} - {% if not a_request.applied and request_status == 'approved' and a_request.editable_charge %} + {% if not a_request.applied and request_status == 'approved' and a_request.adjustable_charge %} {% if "requests"|customization:"adjustment_requests_edit_charge_button" == "enabled" %} {% url "user_requests" "adjustment" as redirect_adjustment_url %} {% admin_edit_url a_request.item redirect_adjustment_url as edit_url %} From e1fc562e3c4f877eaf7a05bd742ae9caf5c328ac Mon Sep 17 00:00:00 2001 From: mrampant Date: Thu, 9 May 2024 17:13:06 -0400 Subject: [PATCH 21/56] - added settings to allow tool problems and updates to be sent to users if they add them in preferences. only qualified tools are allowed --- .../customizations/customizations_tool.html | 15 +++++++++++++++ NEMO/templates/users/preferences.html | 6 +++--- NEMO/views/customization.py | 2 ++ NEMO/views/tasks.py | 15 +++++++++++++-- NEMO/views/users.py | 5 +++++ 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/NEMO/templates/customizations/customizations_tool.html b/NEMO/templates/customizations/customizations_tool.html index 276c8fe06..5bb1ecca7 100644 --- a/NEMO/templates/customizations/customizations_tool.html +++ b/NEMO/templates/customizations/customizations_tool.html @@ -113,6 +113,13 @@

Tool problems

value="enabled"> Send new tool problems to all qualified users (but not updates) +
@@ -136,6 +143,14 @@

Tool problems

value="enabled"> Send all tool problem & updates to tool superusers +
+ diff --git a/NEMO/templates/users/preferences.html b/NEMO/templates/users/preferences.html index 5f6d554e9..3e4146f30 100644 --- a/NEMO/templates/users/preferences.html +++ b/NEMO/templates/users/preferences.html @@ -155,7 +155,7 @@

Adjustment Requests

- {% if user.is_staff or user.is_facility_manager or user.is_service_personnel %} + {% if "tool"|customization:"tool_problem_allow_regular_user_preferences" == "enabled" or "tool"|customization:"tool_task_updates_allow_regular_user_preferences" == "enabled" or user.is_staff or user.is_facility_manager or user.is_service_personnel %}

Tool maintenance & notifications

@@ -342,10 +342,10 @@

Email notifications

{% endfor %} {% endfor %} {% endif %} - $('#tool_task_notification_search').autocomplete('tools', add_tool_task_notification, {{ form.fields.tool_task_notifications.queryset|json_search_base }}); + $('#tool_task_notification_search').autocomplete('tools', add_tool_task_notification, {{ tool_list|json_search_base }}); {% if form.instance.id %} {% for tool_id in form.tool_task_notifications.value %} - {% for tool in form.fields.tool_task_notifications.queryset %} + {% for tool in tool_list %} {% if tool.id == tool_id|to_int %} add_tool_task_notification(null, {'name': '{{ tool.name }}', 'id': {{ tool.id }} }); {% endif %} diff --git a/NEMO/views/customization.py b/NEMO/views/customization.py index 1e6374f86..b2aad372b 100644 --- a/NEMO/views/customization.py +++ b/NEMO/views/customization.py @@ -477,6 +477,7 @@ class ToolCustomization(CustomizationBase): "tool_location_required": "enabled", "tool_task_updates_facility_managers": "enabled", "tool_task_updates_superusers": "", + "tool_task_updates_allow_regular_user_preferences": "", "tool_control_hide_data_history_users": "", "tool_control_configuration_setting_template": "{{ current_setting }}", "tool_control_broadcast_upcoming_reservation": "", @@ -489,6 +490,7 @@ class ToolCustomization(CustomizationBase): "tool_qualification_cc": "", "tool_problem_max_image_size_pixels": "750", "tool_problem_send_to_all_qualified_users": "", + "tool_problem_allow_regular_user_preferences": "", "tool_configuration_near_future_days": "1", "tool_reservation_policy_superusers_bypass": "", "tool_wait_list_spot_expiration": "15", diff --git a/NEMO/views/tasks.py b/NEMO/views/tasks.py index 044ab6308..8cbdae91f 100644 --- a/NEMO/views/tasks.py +++ b/NEMO/views/tasks.py @@ -135,7 +135,7 @@ def send_new_task_emails(request, task: Task, task_images: List[TaskImages]): + (" shutdown" if task.force_shutdown else " problem") ) message = render_email_template(message, dictionary, request) - recipients = get_task_email_recipients(task) + recipients = get_task_email_recipients(task, new=True) if ToolCustomization.get_bool("tool_problem_send_to_all_qualified_users"): recipients = set(recipients) for user in task.tool.user_set.all(): @@ -395,7 +395,7 @@ def save_task_images(request, task: Task) -> List[TaskImages]: return task_images -def get_task_email_recipients(task: Task) -> List[str]: +def get_task_email_recipients(task: Task, new=False) -> List[str]: # Add all recipients, starting with primary owner recipient_users: Set[User] = {task.tool.primary_owner} # Add backup owners @@ -416,6 +416,17 @@ def get_task_email_recipients(task: Task) -> List[str]: .filter(Q(is_staff=True) | Q(is_service_personnel=True)) .filter(Q(preferences__tool_task_notifications__in=[task.tool])) ) + # Add regular users with preferences set to receive notifications for this tool if it's allowed + send_email_to_regular_user = ( + new + and ToolCustomization.get_bool("tool_problem_allow_regular_user_preferences") + or not new + and ToolCustomization.get_bool("tool_task_updates_allow_regular_user_preferences") + ) + if send_email_to_regular_user: + recipient_users.update( + User.objects.filter(is_active=True).filter(Q(preferences__tool_task_notifications__in=[task.tool])) + ) recipients = [ email for user in recipient_users for email in user.get_emails(user.get_preferences().email_send_task_updates) ] diff --git a/NEMO/views/users.py b/NEMO/views/users.py index c8815675b..42e7aeedb 100644 --- a/NEMO/views/users.py +++ b/NEMO/views/users.py @@ -468,6 +468,11 @@ def user_preferences(request): "form": form, "user_preferences": user.get_preferences(), "user_view": user_view, + "tool_list": ( + user.qualifications.all() + if not (user.is_staff or user.is_facility_manager or user.is_service_personnel) + else Tool.objects.filter(visible=True) + ), } return render(request, "users/preferences.html", dictionary) From dd1ba18db784a7c5c48f19788a44149cd2126c12 Mon Sep 17 00:00:00 2001 From: r-xyz <100710244+r-xyz@users.noreply.github.com> Date: Sun, 12 May 2024 15:38:25 +0200 Subject: [PATCH 22/56] Explicit list hidden tools training (#224) * Revert "Enable training for non-visible tools. (#221)" This reverts commit 7a457e04b95e25dea1ac7ee8e3e1aa5bce9000af. * Enable training for specific non-visible tools. As discussed in #221 and https://gitlab.com/nemo-community/nemo-ce/-/merge_requests/27. Re-introducing the feature, but limited to a selected list of tools. --- .../customizations_training.html | 43 +++++++++++++------ NEMO/views/customization.py | 17 +++++++- NEMO/views/training.py | 10 ++--- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/NEMO/templates/customizations/customizations_training.html b/NEMO/templates/customizations/customizations_training.html index 3083ea3e8..6089e9d03 100644 --- a/NEMO/templates/customizations/customizations_training.html +++ b/NEMO/templates/customizations/customizations_training.html @@ -36,22 +36,39 @@

Training

Qualifications will still happen in real time
-
- -
-
- -
+
+ +
+
+ {% if errors.training_included_hidden_tools %} +
{{ errors.training_included_hidden_tools.error }}
+ {% endif %} +
+
+
No hidden tools are included.
{% button type="save" value="Save settings" %}
+ diff --git a/NEMO/views/customization.py b/NEMO/views/customization.py index b2aad372b..b4f723d16 100644 --- a/NEMO/views/customization.py +++ b/NEMO/views/customization.py @@ -27,6 +27,7 @@ Notification, Project, RecurringConsumableCharge, + Tool, TrainingSession, UserPreferences, UserType, @@ -553,16 +554,30 @@ class RemoteWorkCustomization(CustomizationBase): @customization(key="training", title="Training") class TrainingCustomization(CustomizationBase): - variables = {"training_only_type": "", "training_allow_date": "", "training_allow_hidden_tools": ""} + variables = {"training_only_type": "", "training_allow_date": "", "training_included_hidden_tools": ""} def context(self) -> Dict: dictionary = super().context() + dictionary["tools"] = Tool.objects.all() dictionary["training_types"] = TrainingSession.Type.Choices + dictionary["included_hidden_tools"] = Tool.objects.filter( + id__in=self.get_list_int("training_included_hidden_tools") + ) return dictionary + def validate(self, name, value): + if name == "training_included_hidden_tools" and value: + validate_comma_separated_integer_list(value) + def save(self, request, element=None) -> Dict[str, Dict[str, str]]: errors = super().save(request, element) training_types = request.POST.getlist("training_type_list", []) + include_hidden_tools = ",".join(request.POST.getlist("training_included_hidden_tools_list", [])) + try: + self.validate("training_included_hidden_tools", include_hidden_tools) + type(self).set("training_included_hidden_tools", include_hidden_tools) + except (ValidationError, InvalidCustomizationException) as e: + errors["training_included_hidden_tools"] = {"error": str(e.message or e.msg), "value": include_hidden_tools} if training_types and len(training_types) == 1: type(self).set("training_only_type", training_types[0]) return errors diff --git a/NEMO/views/training.py b/NEMO/views/training.py index 9e4db23bd..7dd723a33 100644 --- a/NEMO/views/training.py +++ b/NEMO/views/training.py @@ -5,7 +5,7 @@ import requests from django.conf import settings -from django.db.models import Count +from django.db.models import Count, Q from django.http import HttpResponseBadRequest from django.shortcuts import render from django.urls import reverse @@ -28,10 +28,10 @@ def training(request): """Present a web page to allow staff or tool superusers to charge training and qualify users on particular tools.""" user: User = request.user users = User.objects.filter(is_active=True).exclude(id=user.id) - hidden_allowed = TrainingCustomization.get_bool("training_allow_hidden_tools") - tools = Tool.objects.all() - if not hidden_allowed: - tools = tools.filter(visible=True) + tools = Tool.objects.filter( + Q(visible=True) + | Q(visible=False) & Q(id__in=TrainingCustomization.get_list_int("training_included_hidden_tools")) + ) tool_groups = ToolQualificationGroup.objects.all() if not user.is_staff and user.is_tool_superuser: tools = tools.filter(_superusers__in=[user]) From 7673a3da9cdda0b56b1bebae0d559064d0237040 Mon Sep 17 00:00:00 2001 From: mrampant Date: Mon, 13 May 2024 11:31:35 -0400 Subject: [PATCH 23/56] - added last area access date in user page --- .../users/create_or_modify_user.html | 32 +++++++++++++------ NEMO/views/users.py | 4 +++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/NEMO/templates/users/create_or_modify_user.html b/NEMO/templates/users/create_or_modify_user.html index 034d32c12..c5b5189da 100644 --- a/NEMO/templates/users/create_or_modify_user.html +++ b/NEMO/templates/users/create_or_modify_user.html @@ -217,7 +217,7 @@
{{ form.instance.date_joined }}
-
Last login
+
Last website login
{{ form.instance.last_login|default_if_none:"Never" }}
{% endif %} @@ -319,15 +319,27 @@ {% for level in area_access_dict|get_item:node.id %}
- + {% with last_user_access=last_access|get_item:level.area.id %} + + {% endwith %}
{% endfor %} {% if not node.is_leaf_node %} diff --git a/NEMO/views/users.py b/NEMO/views/users.py index 42e7aeedb..bbfe490e7 100644 --- a/NEMO/views/users.py +++ b/NEMO/views/users.py @@ -7,6 +7,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.db.models import Max from django.http import HttpResponseBadRequest from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone @@ -90,6 +91,9 @@ def create_or_modify_user(request, user_id): except: user = None + last_access = AreaAccessRecord.objects.filter(customer=user).values("area_id").annotate(max_date=Max("start")) + dictionary["last_access"] = {item["area_id"]: item["max_date"] for item in last_access} + timeout = identity_service.get("timeout", 3) site_title = ApplicationCustomization.get("site_title") if dictionary["identity_service_available"]: From 2b2d84710b806c6b2538ed7228c1d4f6d30c1ba1 Mon Sep 17 00:00:00 2001 From: mrampant Date: Mon, 13 May 2024 23:31:13 -0400 Subject: [PATCH 24/56] - personal schedule can now be selected as one of multiple feeds in calendar view --- NEMO/static/nemo.css | 2 +- NEMO/static/nemo.js | 4 +-- NEMO/templates/calendar/calendar.html | 40 +++++++++++++++++++++------ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/NEMO/static/nemo.css b/NEMO/static/nemo.css index 722468774..1667c6096 100644 --- a/NEMO/static/nemo.css +++ b/NEMO/static/nemo.css @@ -77,7 +77,7 @@ body.impersonating .application-sidebar, body.impersonating .application-content padding-right: 4px; } -#sidebar #tool_tree input[type=checkbox], #sidebar #area_tree input[type=checkbox] +#sidebar #tool_tree input[type=checkbox], #sidebar #area_tree input[type=checkbox], #sidebar #extra-links input[type=checkbox] { margin-right: 5px; margin-top: 0; diff --git a/NEMO/static/nemo.js b/NEMO/static/nemo.js index 29b29f7f9..dd8f74d0f 100644 --- a/NEMO/static/nemo.js +++ b/NEMO/static/nemo.js @@ -52,7 +52,7 @@ function switch_tab(element) function set_item_link_callback(callback) { - $("a[data-item-type='tool'], a[data-item-type='area']").each(function() + $("a[data-item-type='tool'], a[data-item-type='area'], a[data-item-type='personal-schedule']").each(function() { $(this).click({"callback": callback}, callback); }); @@ -60,7 +60,7 @@ function set_item_link_callback(callback) function set_item_checkbox_callback(callback) { - $("input[type='checkbox'][data-item-type='tool'], input[type='checkbox'][data-item-type='area']").each(function() + $("input[type='checkbox'][data-item-type='tool'], input[type='checkbox'][data-item-type='area'], input[type='checkbox'][data-item-type='personal-schedule']").each(function() { $(this).click({"callback": callback}, callback); }); diff --git a/NEMO/templates/calendar/calendar.html b/NEMO/templates/calendar/calendar.html index 3085973f4..b1f830bb7 100644 --- a/NEMO/templates/calendar/calendar.html +++ b/NEMO/templates/calendar/calendar.html @@ -98,12 +98,20 @@ onclick="clear_specific_user()"> {# The following menu tree code was take from an example at http://www.bootply.com/120625 #}

placeholder="Search for a non-visible tool to include"> {% if errors.training_included_hidden_tools %} -
{{ errors.training_included_hidden_tools.error }}
+
+ {{ errors.training_included_hidden_tools.error }} +
{% endif %}
-
No hidden tools are included.
+
+ No hidden tools are included. +
{% button type="save" value="Save settings" %}
From 93279bf099a49c9f9700d5fb533fdeaaa8fb8767 Mon Sep 17 00:00:00 2001 From: mrampant Date: Mon, 3 Jun 2024 20:38:07 -0400 Subject: [PATCH 46/56] - fixes #235 - fixed invalid date when selecting blank month in my usage/project billing --- NEMO/templates/usage/usage_base.html | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/NEMO/templates/usage/usage_base.html b/NEMO/templates/usage/usage_base.html index 5927700fa..443905d85 100644 --- a/NEMO/templates/usage/usage_base.html +++ b/NEMO/templates/usage/usage_base.html @@ -150,12 +150,15 @@

function set_dates_from_month(month_input) { - let month = moment(month_input, "MMMM, YYYY"); - let firstOfMonth = month.startOf('month').format(js_date_format); - let lastOfMonth = month.endOf('month').format(js_date_format); - - start_date_jq.val(firstOfMonth); - end_date_jq.val(lastOfMonth); + if (month_input) + { + let month = moment(month_input, "MMMM, YYYY"); + let firstOfMonth = month.startOf('month').format(js_date_format); + let lastOfMonth = month.endOf('month').format(js_date_format); + + start_date_jq.val(firstOfMonth); + end_date_jq.val(lastOfMonth); + } } function set_dropdown_selected() @@ -167,7 +170,7 @@

if (start.month() === end.month() && start.year() === end.year() && start.day() === start.startOf('month').day() && end.day() === end.endOf('month').day()) { let month = start.format("MMMM, YYYY"); - $('#month_list').val(month) + $('#month_list').val(month); } } } @@ -188,10 +191,8 @@

{% button type="save" submit=False onclick="$('#csv_export').val('');this.form.submit();" value="Update" icon="glyphicon-refresh" %} - {% if can_export %} - - {% button style="margin-left: 15px" type="export" onclick="$('#csv_export').val('true');this.form.submit();" value="Export" %} - {% endif %} + + {% button style="margin-left: 15px" type="export" onclick="$('#csv_export').val('true');this.form.submit();" value="Export" %}
{% endif %} + {% if allow_profile_view %} +
  • + My profile +
  • + {% endif %}
  • Preferences
  • diff --git a/NEMO/templates/users/view_user.html b/NEMO/templates/users/view_user.html index 1c339ba29..a72a32a84 100644 --- a/NEMO/templates/users/view_user.html +++ b/NEMO/templates/users/view_user.html @@ -5,7 +5,7 @@ {% block content %}
    -

    Basic Information

    +

    Basic Information

    • First name: {{ user.first_name }} @@ -35,7 +35,7 @@

      Basic Information

    -

    Projects

    +

    Projects

    {% if user.projects %}
      {% for project in user.projects.all %} @@ -49,7 +49,7 @@

      Projects

    -

    Tool Qualifications

    +

    Tool Qualifications

    {% if user.qualifications.exists %}
      {% for tool_qualification in user.qualifications.all %}
    • {{ tool_qualification }}
    • {% endfor %} @@ -62,7 +62,7 @@

      Tool Qualifications

      {% if user.accessible_access_levels.exists %}
      -

      Physical access levels

      +

      Physical access levels

        {% for physical_access in user.accessible_access_levels.all %}
      • {{ physical_access }}
      • {% endfor %}
      @@ -72,7 +72,7 @@

      Physical access levels

      {% if user.is_any_part_of_staff and user.groups.exists %}
      -

      Groups

      +

      Groups

        {% for group in user.groups.all %}
      • {{ group }}
      • {% endfor %}
      From c7511d3217622501684edca26e936f3cd204bc4c Mon Sep 17 00:00:00 2001 From: mrampant Date: Fri, 7 Jun 2024 09:43:47 -0400 Subject: [PATCH 53/56] - fixed issue when there is no downtime but we are trying to compare it in policy.py --- NEMO/policy.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/NEMO/policy.py b/NEMO/policy.py index f25909649..d94f879fe 100644 --- a/NEMO/policy.py +++ b/NEMO/policy.py @@ -196,16 +196,19 @@ def check_to_disable_tool(self, tool, operator, downtime) -> HttpResponse: return HttpResponseBadRequest( "You may not disable a tool while another user is using it unless you are a staff member." ) - if downtime < timedelta(): - return HttpResponseBadRequest("Downtime cannot be negative.") - if downtime > timedelta(minutes=tool.max_delayed_logoff): - return HttpResponseBadRequest(f"Post-usage tool downtime may not exceed {tool.max_delayed_logoff} minutes.") - if tool.delayed_logoff_in_progress() and downtime > timedelta(): - return HttpResponseBadRequest( - "The tool is already in a delayed-logoff state. You may not issue additional delayed logoffs until the existing one expires." - ) - if tool.max_delayed_logoff is None and downtime > timedelta(): - return HttpResponseBadRequest("Delayed logoff is not allowed for this tool.") + if downtime: + if downtime < timedelta(): + return HttpResponseBadRequest("Downtime cannot be negative.") + if downtime > timedelta(minutes=tool.max_delayed_logoff): + return HttpResponseBadRequest( + f"Post-usage tool downtime may not exceed {tool.max_delayed_logoff} minutes." + ) + if tool.delayed_logoff_in_progress() and downtime > timedelta(): + return HttpResponseBadRequest( + "The tool is already in a delayed-logoff state. You may not issue additional delayed logoffs until the existing one expires." + ) + if tool.max_delayed_logoff is None and downtime > timedelta(): + return HttpResponseBadRequest("Delayed logoff is not allowed for this tool.") return HttpResponse() def check_to_save_reservation( From 1490923bc355173fdf7a72657531471ecfc4d3aa Mon Sep 17 00:00:00 2001 From: mrampant Date: Fri, 7 Jun 2024 09:46:58 -0400 Subject: [PATCH 54/56] - Added consumable adjustments requests --- .../0086_adjustmentrequest_new_quantity.py | 18 ++ NEMO/mixins.py | 62 +++- NEMO/models.py | 28 +- .../customizations_requests.html | 44 ++- .../adjustment_request.html | 25 +- .../adjustment_requests_table.html | 15 +- NEMO/templates/usage/usage.html | 288 ++++++++---------- NEMO/templatetags/custom_tags_and_filters.py | 8 + NEMO/utilities.py | 9 + NEMO/views/adjustment_requests.py | 42 ++- NEMO/views/customization.py | 4 + 11 files changed, 368 insertions(+), 175 deletions(-) create mode 100644 NEMO/migrations/0086_adjustmentrequest_new_quantity.py diff --git a/NEMO/migrations/0086_adjustmentrequest_new_quantity.py b/NEMO/migrations/0086_adjustmentrequest_new_quantity.py new file mode 100644 index 000000000..f6c294277 --- /dev/null +++ b/NEMO/migrations/0086_adjustmentrequest_new_quantity.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2024-06-04 19:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("NEMO", "0085_contactinformation_title_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="adjustmentrequest", + name="new_quantity", + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/NEMO/mixins.py b/NEMO/mixins.py index 8316f0c1d..6fb124427 100644 --- a/NEMO/mixins.py +++ b/NEMO/mixins.py @@ -86,7 +86,7 @@ def get_item(self) -> str: elif self.get_real_type() == BillableItemMixin.TRAINING: return f"{self.get_type_display()} training" elif self.get_real_type() == BillableItemMixin.CONSUMABLE: - quantity = f" (x {self.quantity}" if self.quantity > 1 else "" + quantity = f" (x {self.quantity})" if self.quantity > 1 else "" return f"{self.consumable}{quantity}" elif self.get_real_type() == BillableItemMixin.MISSED_RESERVATION: return f"{self.tool or self.area} missed reservation" @@ -111,6 +111,66 @@ def get_end(self) -> Optional[datetime.datetime]: elif self.get_real_type() in [BillableItemMixin.TRAINING, BillableItemMixin.CONSUMABLE]: return self.date + def can_be_adjusted(self, user: User): + # determine if a charge can be adjusted + from NEMO.views.customization import UserRequestsCustomization + from NEMO.views.usage import get_managed_projects + + pi_projects = get_managed_projects(user) + + time_limit = UserRequestsCustomization.get_date_limit() + time_limit_condition = not time_limit or time_limit <= self.get_end() + user_project_condition = self.get_customer() == user or self.project in pi_projects + operator_is_staff = self.get_operator() == user and user.is_staff + if self.get_real_type() == BillableItemMixin.AREA_ACCESS: + access_enabled = UserRequestsCustomization.get_bool("adjustment_requests_area_access_enabled") + remote_enabled = UserRequestsCustomization.get_bool("adjustment_requests_staff_staff_charges_enabled") + if self.staff_charge: + return remote_enabled and time_limit_condition and operator_is_staff + else: + return access_enabled and user_project_condition and time_limit_condition + elif self.get_real_type() == BillableItemMixin.TOOL_USAGE: + remote_enabled = UserRequestsCustomization.get_bool("adjustment_requests_staff_staff_charges_enabled") + usage_enabled = UserRequestsCustomization.get_bool("adjustment_requests_tool_usage_enabled") + if self.remote_work: + return remote_enabled and time_limit_condition and operator_is_staff + else: + return ( + usage_enabled + and time_limit_condition + and user_project_condition + and self.get_customer() == self.get_operator() + ) + elif self.get_real_type() == BillableItemMixin.REMOTE_WORK: + remote_enabled = UserRequestsCustomization.get_bool("adjustment_requests_staff_staff_charges_enabled") + return remote_enabled and time_limit_condition and operator_is_staff + elif self.get_real_type() == BillableItemMixin.TRAINING: + return f"{self.get_type_display()} training" + elif self.get_real_type() == BillableItemMixin.CONSUMABLE: + withdrawal_enabled = UserRequestsCustomization.get_bool("adjustment_requests_consumable_withdrawal_enabled") + self_check = UserRequestsCustomization.get_bool("adjustment_requests_consumable_withdrawal_self_checkout") + staff_check = UserRequestsCustomization.get_bool("adjustment_requests_consumable_withdrawal_staff_checkout") + usage_event = UserRequestsCustomization.get_bool("adjustment_requests_consumable_withdrawal_usage_event") + type_condition = True + if not self_check: + consumable_self_checked = ( + self.consumable.allow_self_checkout + and self.get_operator() == self.get_customer() + and not self.usage_event + ) + type_condition = type_condition and not consumable_self_checked + if not staff_check: + consumable_staff_check = ( + not self.usage_event and self.get_operator().is_staff and self.get_operator() != self.get_customer() + ) + type_condition = type_condition and not consumable_staff_check + if not usage_event: + type_condition = type_condition and not self.usage_event + return withdrawal_enabled and time_limit_condition and type_condition and user_project_condition + elif self.get_real_type() == BillableItemMixin.MISSED_RESERVATION: + missed_resa_enabled = UserRequestsCustomization.get_bool("adjustment_requests_missed_reservation_enabled") + return missed_resa_enabled and time_limit_condition and user_project_condition + def get_operator_action(self) -> str: if self.get_real_type() == BillableItemMixin.AREA_ACCESS: return "entered " diff --git a/NEMO/models.py b/NEMO/models.py index cca8306e0..d3ca6b641 100644 --- a/NEMO/models.py +++ b/NEMO/models.py @@ -46,6 +46,7 @@ get_chemical_document_filename, get_full_url, get_hazard_logo_filename, + get_model_instance, get_task_image_filename, get_tool_image_filename, new_model_copy, @@ -4159,6 +4160,7 @@ class AdjustmentRequest(BaseModel): ) new_start = models.DateTimeField(null=True, blank=True) new_end = models.DateTimeField(null=True, blank=True) + new_quantity = models.PositiveIntegerField(null=True, blank=True) status = models.IntegerField(choices=RequestStatus.choices_without_expired(), default=RequestStatus.PENDING) reviewer = models.ForeignKey( "User", null=True, blank=True, related_name="adjustment_requests_reviewed", on_delete=models.CASCADE @@ -4197,6 +4199,11 @@ def get_new_end(self) -> Optional[datetime]: else None ) + def get_quantity_difference(self) -> int: + if self.item and self.new_quantity is not None: + return self.new_quantity - self.item.quantity + return 0 + def get_time_difference(self) -> str: if self.item and self.new_start and self.new_end: previous_duration = self.item.end.replace(microsecond=0, second=0) - self.item.start.replace( @@ -4209,8 +4216,11 @@ def get_time_difference(self) -> str: else f"- {(previous_duration - new_duration)}" ) + def get_difference(self): + return self.get_time_difference() or self.get_quantity_difference() + def adjustable_charge(self): - return self.has_changed_time() or isinstance(self.item, Reservation) + return self.has_changed_time() or isinstance(self.item, Reservation) or self.get_quantity_difference() def has_changed_time(self) -> bool: """Returns whether the original charge is editable, i.e. if it has a changed start or end""" @@ -4250,6 +4260,11 @@ def apply_adjustment(self, user): self.applied = True self.applied_by = user self.save() + elif self.get_quantity_difference(): + self.item.quantity = self.new_quantity + self.item.save() + self.applied = True + self.applied_by = user elif isinstance(self.item, Reservation): self.item.missed = False self.item.save() @@ -4272,7 +4287,8 @@ def delete(self, using=None, keep_parents=False): def clean(self): if not self.description: raise ValidationError({"description": _("This field is required.")}) - if self.item: + item = get_model_instance(self.item_type, self.item_id) + if item: already_adjusted = AdjustmentRequest.objects.filter( deleted=False, item_type_id=self.item_type_id, item_id=self.item_id ) @@ -4284,12 +4300,12 @@ def clean(self): raise ValidationError({"new_end": _("The end must be later than the start")}) if ( self.new_start - and format_datetime(self.new_start) == format_datetime(self.item.start) + and format_datetime(self.new_start) == format_datetime(item.start) and self.new_end - and format_datetime(self.new_end) == format_datetime(self.item.end) - ): + and format_datetime(self.new_end) == format_datetime(item.end) + ) or (self.new_quantity is not None and self.new_quantity == item.quantity): raise ValidationError( - {NON_FIELD_ERRORS: _("One of the dates must be different from the original charge")} + {NON_FIELD_ERRORS: _("You must change at least one attribute (dates or quantity)")} ) class Meta: diff --git a/NEMO/templates/customizations/customizations_requests.html b/NEMO/templates/customizations/customizations_requests.html index 6f517fce0..b883619c4 100644 --- a/NEMO/templates/customizations/customizations_requests.html +++ b/NEMO/templates/customizations/customizations_requests.html @@ -106,7 +106,47 @@

      Adjustment requests settings


      -
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +

    Missed reservation -
    +
    {% endif %} + {% if change_quantity_allowed and form.item_id.value and form.item_type.value %} +
    +
    +
    + + +
    +
    + {% if form.new_quantity.errors %} +
    +
    + {% if form.new_quantity.errors %}{{ form.new_quantity.errors|striptags }}{% endif %} +
    +
    + {% endif %} +
    + {% endif %}
    {% if form.description.errors %}- {{ form.description.errors|striptags }}{% endif %} @@ -117,7 +138,9 @@

    {% if form.instance.get_status_display == "Pending" and instance_id and user in form.instance.reviewers %}
    - {% if form.description.errors %}- {{ form.manager_note.errors|striptags }}{% endif %} + {% if form.description.errors %} + - {{ form.manager_note.errors|striptags }} + {% endif %}