Repository: usnistgov/NEMO (language: python)

Issue:
Enable training for non-visible tools.
According to suggestion in  #220, I open this PR to enable training for non-visible tools.
Training on hidden tools can now be re-enabled using the corresponding customization setting.

This can be used to track trainings for the initial induction, setting an hidden tool with Facility name.
Datetimepicker do not follow 'First day of the week' Calendar Customization
NEMO has built-in functionality to select first day of week.
<img src="https://github.com/usnistgov/NEMO/assets/100710244/86687a97-7e3b-44a6-823d-3676812f8d98" height="150">


This apply correctly to weekly/monthly Calendar views:
<img src="https://github.com/usnistgov/NEMO/assets/100710244/34118b7c-7a04-437d-9d35-06f85baffb7d" width="600">

Unfortunately, the same setting is not consistently implemented in datetimepicker.

<img src="https://github.com/usnistgov/NEMO/assets/100710244/3195386a-a0df-4e57-87ad-a12e19971e3f">

Current datetimepicker do not implement any `weekStart` option, but if you have any suggestions on how to implement it, I can submit a PR.


[feature request] Title field within contact information
Request:
When entering contact information for people in our facility, it would be helpful to be able to enter descriptive titles/roles for each person -- just an additional one-line text field.  

Purpose:
I want to be able to label this person as the operations manager, that person as the safety officer, this other person as the equipment support supervisor, the next person as chief engineer, someone else as our administrative support, etc.


Discussion / hints:


It might not be as flexible as a free text field, but you can set a list of possible User Types in Detailed administration -> NEMO -> User Types and set those on people
That user type information doesn't show up on the contact info page, though!  Also, not all names in the contact list are facility users -- our ES&H team is a perfect example.  Here I've assigned Anand the user type "operations manager" and Manish the user type "safety officer," and I gave them skeleton contact info where the contact is linked to their NEMO username.  
![image](https://github.com/usnistgov/NEMO/assets/98626874/df39516d-6165-4401-bbbb-3dc5ec7ae482)

This might be a better example of the per-person type of role info I need to capture.
![image](https://github.com/usnistgov/NEMO/assets/98626874/5fd5cdc6-7899-426f-814c-c78aa5b1e8e0)

Oh that makes sense now, I read too quick and didn't see the "contact information" part.
That should be pretty straightforward, we'll accept a PR if anyone is interested in contributing

Fix patch:
diff --git a/NEMO/admin.py b/NEMO/admin.py
index 591391a2..41f67627 100644
--- a/NEMO/admin.py
+++ b/NEMO/admin.py
@@ -101,6 +101,7 @@
     TemporaryPhysicalAccess,
     TemporaryPhysicalAccessRequest,
     Tool,
+    ToolCredentials,
     ToolDocuments,
     ToolQualificationGroup,
     ToolUsageCounter,
@@ -312,7 +313,7 @@ class ToolAdmin(admin.ModelAdmin):
                     "_grant_physical_access_level_upon_qualification",
                     "_grant_badge_reader_access_upon_qualification",
                     "_interlock",
-                    "_allow_delayed_logoff",
+                    "_max_delayed_logoff",
                     "_ask_to_leave_area_when_done_using",
                 )
             },
@@ -1448,7 +1449,7 @@ def alert_preview(self, obj: Closure):
                 display_title = f'<span style="font-weight:bold">{obj.name}</span><br>' if obj.name else ""
                 return iframe_content(
                     f'<div style="{alert_style}">{display_title}{linebreaksbr(obj.closuretime_set.first().alert_contents())}</div>',
-                    extra_style="padding-bottom: 20%",
+                    extra_style="padding-bottom: 15%",
                 )
             except Exception:
                 pass
@@ -1571,6 +1572,7 @@ class LandingPageChoiceAdmin(admin.ModelAdmin):
 @register(Customization)
 class CustomizationAdmin(admin.ModelAdmin):
     list_display = ("name", "value")
+    search_fields = ["name"]
 
 
 @register(ScheduledOutageCategory)
@@ -1836,6 +1838,29 @@ class OnboardingPhaseAdmin(admin.ModelAdmin):
     list_display = ("name", "display_order")
 
 
+class ToolCredentialsAdminForm(forms.ModelForm):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields["authorized_staff"].queryset = User.objects.filter(is_staff=True)
+
+
+@register(ToolCredentials)
+class ToolCredentialsAdmin(ModelAdminRedirectMixin, admin.ModelAdmin):
+    list_display = ["get_tool_category", "tool", "is_tool_visible", "username", "comments"]
+    list_filter = [("tool", admin.RelatedOnlyFieldListFilter), "tool__visible"]
+    autocomplete_fields = ["tool"]
+    filter_horizontal = ["authorized_staff"]
+    form = ToolCredentialsAdminForm
+
+    @display(ordering="tool___category", description="Tool category")
+    def get_tool_category(self, obj: ToolCredentials) -> str:
+        return obj.tool._category
+
+    @admin.display(ordering="tool__visible", boolean=True, description="Tool visible")
+    def is_tool_visible(self, obj: Configuration):
+        return obj.tool.visible
+
+
 @register(EmailLog)
 class EmailLogAdmin(admin.ModelAdmin):
     list_display = ["id", "category", "sender", "to", "subject", "when", "ok"]
@@ -1916,9 +1941,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'<div style="position: relative; display: block; overflow: hidden; {extra_style}"><iframe style="position: absolute; width:100%; height:100%; border:none" src="data:text/html,{urlencode(content)}"></iframe></div>'
+        f'<div id="iframe-container" style="position: relative; display: block; overflow: hidden; width:100%; height:100%; {extra_style}"><iframe style="width:100%; height:100%; border:none" src="data:text/html,{urlencode(content)}"></iframe></div><script>django.jQuery("#iframe-container").parent(".readonly").css("flex","1");</script>'
     )
 
 
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 c4b95350..a428fe81 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 @@ <h2>However, a scheduled outage is in effect in the {{ scheduled_outage_in_progr
 {% elif reservation_requirement_failed %}
     <h2>However, you cannot enter since you don't have a current reservation for this area.</h2>
 {% endif %}
-<h2>Please visit the {{ facility_name }} staff if you believe this is an error.</h2>
+<h2>Please contact {{ facility_name }} staff if you believe this is an error.</h2>
 {% if not reservation_requirement_failed and not max_capacity_reached and not scheduled_outage_in_progress %}
     <h2>What would you like to do?</h2>
     <p>
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 881ff6ce..cb25b4e4 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 @@
 <script>$('body').removeClass().addClass('logout-error')</script>
 <h1>Your badge wasn't recognized.</h1>
 <h2>If you got a new one recently then we'll need to update your account.</h2>
-<h2>Please visit the {{ facility_name }} staff to resolve the problem.</h2>
+<h2>Please contact {{ facility_name }} staff to resolve the problem.</h2>
 <audio src="{% url 'media' 'error.mp3' %}" autoplay></audio>
 <script>revert(15);</script>
diff --git a/NEMO/apps/area_access/templates/area_access/inactive.html b/NEMO/apps/area_access/templates/area_access/inactive.html
index ade36981..fb9577db 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 @@
 <h1>Your account was deactivated.</h1>
-<h2>Please visit the {{ facility_name }} staff to resolve the problem.</h2>
+<h2>Please contact the {{ facility_name }} staff to resolve the problem.</h2>
 <script>revert(15);</script>
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 5777d073..a38dc216 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 @@
 <h1>You are not a member of any active projects.</h1>
 <h2>You won't be able to use any interlocked {{ facility_name }} tools.</h2>
-<h2>Please visit the {{ facility_name }} user office for more information.</h2>
+<h2>Please contact {{ facility_name }} staff for more information.</h2>
 <script>revert(15);</script>
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 9af49416..2916f9a6 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 @@
 <script>$('body').removeClass().addClass('logout-warning')</script>
 <h1>According to our records, you're not logged in to any access controlled areas.</h1>
-<h2>Please visit the {{ facility_name }} user office if you believe this is an error.</h2>
+<h2>Please contact {{ facility_name }} staff if you believe this is an error.</h2>
 <audio src="{% url 'media' 'warning.mp3' %}" autoplay></audio>
 <script>revert(15);</script>
diff --git a/NEMO/apps/kiosk/templates/kiosk/tool_information.html b/NEMO/apps/kiosk/templates/kiosk/tool_information.html
index 1c081f28..21ba6658 100644
--- a/NEMO/apps/kiosk/templates/kiosk/tool_information.html
+++ b/NEMO/apps/kiosk/templates/kiosk/tool_information.html
@@ -149,6 +149,30 @@ <h2>
         {% endfor %}
     </div>
 {% endif %}
+{% if user.is_staff and tool_credentials %}
+    <div class="media" style="margin-bottom: 20px">
+        <a onclick="toggle_details(this)"
+           data-toggle="collapse"
+           data-target="#tool_credentials"
+           class="pointer">
+            <span class="glyphicon glyphicon-lock pull-left notification-icon warning-highlight"></span><span class="glyphicon glyphicon-chevron-right pull-left chevron"></span>
+        </a>
+        <div class="media-body">
+            <div style="margin-bottom: 15px">Credentials</div>
+            <div id="tool_credentials" class="collapse">
+                <ul>
+                    {% for cred in tool_credentials %}
+                        <li>
+                            <i class="glyphicon glyphicon-user"></i> {{ cred.username|default_if_none:"" }}
+                            {% if cred.password %}<i class="glyphicon glyphicon-lock"></i> {{ cred.password }}{% endif %}
+                            {% if cred.comments %}({{ cred.comments }}){% endif %}
+                        </li>
+                    {% endfor %}
+                </ul>
+            </div>
+        </div>
+    </div>
+{% endif %}
 {% if tool.problems %}
     <div class="tool-problems">
         {# Display all problems and shutdowns... #}
@@ -248,7 +272,7 @@ <h2>Configuration</h2>
                 <h2>Post usage questions</h2>
                 {{ post_usage_questions }}
             {% endif %}
-            {% if tool.allow_delayed_logoff and not tool.delayed_logoff_in_progress %}
+            {% if tool.max_delayed_logoff is not None and not tool.delayed_logoff_in_progress %}
                 <h2>Delayed logoff</h2>
                 <div class="form-group">
                     Use the following field to prevent others from using the tool for
@@ -259,7 +283,7 @@ <h2>Delayed logoff</h2>
                            style="display:inline;
                                   width:auto"
                            min="5"
-                           max="120"
+                           max="{{ tool.max_delayed_logoff }}"
                            inputmode="numeric"
                            pattern="[0-9]*"
                            placeholder="0">
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 75c181f2..26149338 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 @@ <h2>
             {% 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.
         </h2>
     {% elif active_projects|length == 1 %}
         <h2>
diff --git a/NEMO/apps/kiosk/views.py b/NEMO/apps/kiosk/views.py
index 19b082e7..507b3458 100644
--- a/NEMO/apps/kiosk/views.py
+++ b/NEMO/apps/kiosk/views.py
@@ -1,26 +1,29 @@
 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.db.models import Q
+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
 from django.views.decorators.http import require_GET, require_POST
 
 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
@@ -33,7 +36,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,
@@ -407,7 +409,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,
     }
@@ -433,7 +434,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)
 
@@ -458,9 +458,20 @@ def tool_information(request, tool_id, user_id, back):
         if user_wait_list_entry
         else 0
     )
+    tool_credentials = []
+    if ToolCustomization.get_bool("tool_control_show_tool_credentials") and (
+        customer.is_staff or customer.is_facility_manager
+    ):
+        if customer.is_facility_manager:
+            tool_credentials = tool.toolcredentials_set.all()
+        else:
+            tool_credentials = tool.toolcredentials_set.filter(
+                Q(authorized_staff__isnull=True) | Q(authorized_staff__in=[customer])
+            )
     dictionary = {
         "customer": customer,
         "tool": tool,
+        "tool_credentials": tool_credentials,
         "rendered_configuration_html": tool.configuration_widget(customer),
         "pre_usage_questions": DynamicForm(tool.pre_usage_questions).render(
             "tool_usage_group_question", tool.id, virtual_inputs=True
@@ -597,15 +608,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(
+            '<ul class="errorlist"><li>{}</li></ul>'.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, customer)
-
-    if save_error:
-        dictionary["message"] = save_error
-        dictionary["form"] = form
-        return render(request, "kiosk/tool_report_problem.html", dictionary)
+    save_task(request, task, customer)
 
     return redirect("kiosk_tool_information", tool_id=tool.id, user_id=customer.id, back=back)
 
diff --git a/NEMO/context_processors.py b/NEMO/context_processors.py
index b12c3e36..18f3e5f3 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,12 @@ 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"),
+        "calendar_first_day_of_week": customization_values.get("calendar_first_day_of_week"),
+        "allow_profile_view": customization_values.get("user_allow_profile_view", "") == "enabled",
     }
diff --git a/NEMO/decorators.py b/NEMO/decorators.py
index d131146b..fe23c6dd 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/migrations/0080_version_6_0_0.py b/NEMO/migrations/0080_version_6_0_0.py
new file mode 100644
index 00000000..a0c0bc71
--- /dev/null
+++ b/NEMO/migrations/0080_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", "0079_user_active_access_expiration_verbose_names"),
+    ]
+
+    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/0081_oracle_django_4_rename.py b/NEMO/migrations/0081_oracle_django_4_rename.py
new file mode 100644
index 00000000..375e3534
--- /dev/null
+++ b/NEMO/migrations/0081_oracle_django_4_rename.py
@@ -0,0 +1,170 @@
+# Generated by Django 3.2.25 on 2024-05-29 15:35
+import hashlib
+
+from django.db import connection, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("NEMO", "0080_version_6_0_0"),
+    ]
+
+    def check_nemo_6_0_0_oracle_long_names(apps, schema_editor):
+        rename_table_sql = "ALTER TABLE %s RENAME TO %s;"
+        rename_column_sql = "ALTER TABLE %s RENAME COLUMN %s to %s;"
+        max_name_length = connection.ops.max_name_length()
+
+        table_renames = []
+        column_renames = []
+
+        if not getattr(connection, "vendor", "") == "oracle":
+            print("This is only needed for ORACLE databases")
+        else:
+            with connection.cursor() as cursor:
+                table_list = connection.introspection.get_table_list(cursor)
+
+                for _model in apps.get_models(include_auto_created=True):
+                    # Table names.
+                    db_table = _model._meta.db_table
+                    new_quoted_name = connection.ops.quote_name(db_table)
+                    old_quoted_name = new_quoted_name
+                    if len(db_table) > max_name_length:
+                        if new_quoted_name.lower() not in table_list:
+                            old_quoted_name = old_quote_name(_model._meta.db_table, max_name_length)
+                            table_renames.append(rename_table_sql % (old_quoted_name, new_quoted_name))
+
+                    # Column names:
+                    column_list = connection.introspection.get_table_description(cursor, strip_quotes(old_quoted_name))
+                    for field in _model._meta.local_fields:
+                        if len(field.column) > max_name_length:
+                            field_quoted_name = connection.ops.quote_name(field.column)
+                            if field_quoted_name.lower() not in column_list:
+                                column_renames.append(
+                                    rename_column_sql
+                                    % (
+                                        new_quoted_name,
+                                        old_quote_name(field.column, max_name_length),
+                                        field_quoted_name,
+                                    )
+                                )
+
+                sql_queries = table_renames + column_renames
+                if not sql_queries:
+                    print("No changes needed")
+                else:
+                    with connection.cursor() as cursor:
+                        for sql in table_renames + column_renames:
+                            print("executing: " + sql)
+                            cursor.execute(sql)
+                            print("done")
+
+    def check_nemo_6_0_0_oracle_long_names_reverse(apps, schema_editor):
+        rename_table_sql = "ALTER TABLE %s RENAME TO %s;"
+        rename_column_sql = "ALTER TABLE %s RENAME COLUMN %s to %s;"
+        max_name_length = connection.ops.max_name_length()
+
+        table_renames = []
+        column_renames = []
+
+        if not getattr(connection, "vendor", "") == "oracle":
+            print("This is only needed for ORACLE databases")
+        else:
+            with connection.cursor() as cursor:
+                table_list = connection.introspection.get_table_list(cursor)
+
+                for _model in apps.get_models(include_auto_created=True):
+                    # Table names.
+                    db_table = _model._meta.db_table
+                    new_quoted_name = connection.ops.quote_name(db_table)
+                    if len(db_table) > max_name_length:
+                        old_quoted_name = old_quote_name(_model._meta.db_table, max_name_length)
+                        if old_quoted_name.lower() not in table_list:
+                            table_renames.append(rename_table_sql % (new_quoted_name, old_quoted_name))
+
+                    # Column names:
+                    column_list = connection.introspection.get_table_description(cursor, strip_quotes(new_quoted_name))
+                    for field in _model._meta.local_fields:
+                        if len(field.column) > max_name_length:
+                            old_quoted_name = old_quote_name(field.column, max_name_length)
+                            if old_quoted_name.lower() not in column_list:
+                                field_quoted_name = connection.ops.quote_name(field.column)
+                                column_renames.append(
+                                    rename_column_sql
+                                    % (
+                                        new_quoted_name,
+                                        field_quoted_name,
+                                        old_quote_name(field.column, max_name_length),
+                                    )
+                                )
+
+                sql_queries = table_renames + column_renames
+                if not sql_queries:
+                    print("No changes needed")
+                else:
+                    with connection.cursor() as cursor:
+                        for sql in table_renames + column_renames:
+                            print("executing: " + sql)
+                            cursor.execute(sql)
+                            print("done")
+
+    operations = [migrations.RunPython(check_nemo_6_0_0_oracle_long_names, check_nemo_6_0_0_oracle_long_names_reverse)]
+
+
+def old_quote_name(name, max_name_length):
+    if not name.startswith('"') and not name.endswith('"'):
+        name = '"%s"' % truncate_name(name.upper(), max_name_length)
+    name = name.replace("%", "%%")
+    return name.upper()
+
+
+def split_identifier(identifier):
+    """
+    Split an SQL identifier into a two element tuple of (namespace, name).
+
+    The identifier could be a table, column, or sequence name might be prefixed
+    by a namespace.
+    """
+    try:
+        namespace, name = identifier.split('"."')
+    except ValueError:
+        namespace, name = "", identifier
+    return namespace.strip('"'), name.strip('"')
+
+
+def truncate_name(identifier, length=None, hash_len=4):
+    """
+    Shorten an SQL identifier to a repeatable mangled version with the given
+    length.
+
+    If a quote stripped name contains a namespace, e.g. USERNAME"."TABLE,
+    truncate the table portion only.
+    """
+    namespace, name = split_identifier(identifier)
+
+    if length is None or len(name) <= length:
+        return identifier
+
+    digest = names_digest(name, length=hash_len)
+    return "%s%s%s" % ('%s"."' % namespace if namespace else "", name[: length - hash_len], digest)
+
+
+def names_digest(*args, length):
+    """
+    Generate a 32-bit digest of a set of arguments that can be used to shorten
+    identifying names.
+    """
+    h = hashlib.md5()
+    for arg in args:
+        h.update(arg.encode())
+    return h.hexdigest()[:length]
+
+
+def strip_quotes(table_name):
+    """
+    Strip quotes off of quoted table names to make them safe for use in index
+    names, sequence names, etc. For example '"USER"."TABLE"' (an Oracle naming
+    scheme) becomes 'USER"."TABLE'.
+    """
+    has_quotes = table_name.startswith('"') and table_name.endswith('"')
+    return table_name[1:-1] if has_quotes else table_name
diff --git a/NEMO/migrations/0082_alter_userpreferences_tool_task_notifications.py b/NEMO/migrations/0082_alter_userpreferences_tool_task_notifications.py
new file mode 100644
index 00000000..dfd3df53
--- /dev/null
+++ b/NEMO/migrations/0082_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", "0081_oracle_django_4_rename"),
+    ]
+
+    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/migrations/0083_toolcredentials.py b/NEMO/migrations/0083_toolcredentials.py
new file mode 100644
index 00000000..3338f04a
--- /dev/null
+++ b/NEMO/migrations/0083_toolcredentials.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.2.11 on 2024-05-21 17:28
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("NEMO", "0082_alter_userpreferences_tool_task_notifications"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="ToolCredentials",
+            fields=[
+                ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+                ("username", models.CharField(blank=True, max_length=255, null=True)),
+                ("password", models.CharField(blank=True, max_length=255, null=True)),
+                ("comments", models.CharField(blank=True, max_length=255, null=True)),
+                (
+                    "authorized_staff",
+                    models.ManyToManyField(
+                        blank=True,
+                        help_text="Selected staff will be the only ones allowed to see these credentials. Leave blank for all staff.",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+                ("tool", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="NEMO.tool")),
+            ],
+            options={
+                "verbose_name": "Tool credentials",
+                "verbose_name_plural": "Tool credentials",
+                "ordering": ["-tool__visible", "tool___category", "tool__name"],
+            },
+        ),
+    ]
diff --git a/NEMO/migrations/0084_remove_tool__allow_delayed_logoff_and_more.py b/NEMO/migrations/0084_remove_tool__allow_delayed_logoff_and_more.py
new file mode 100644
index 00000000..cfd14bea
--- /dev/null
+++ b/NEMO/migrations/0084_remove_tool__allow_delayed_logoff_and_more.py
@@ -0,0 +1,40 @@
+# Generated by Django 4.2.11 on 2024-05-24 18:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("NEMO", "0083_toolcredentials"),
+    ]
+
+    def migrate_tool_delayed_logoff(apps, schema_editor):
+        Tool = apps.get_model("NEMO", "Tool")
+        for tool in Tool.objects.filter(_allow_delayed_logoff=True):
+            tool._max_delayed_logoff = 120
+            tool.save(update_fields=["_max_delayed_logoff"])
+
+    def reverse_tool_delayed_logoff(apps, schema_editor):
+        Tool = apps.get_model("NEMO", "Tool")
+        for tool in Tool.objects.filter(_max_delayed_logoff__isnull=False):
+            tool._allow_delayed_logoff = True
+            tool.save(update_fields=["_allow_delayed_logoff"])
+
+    operations = [
+        migrations.AddField(
+            model_name="tool",
+            name="_max_delayed_logoff",
+            field=models.PositiveIntegerField(
+                blank=True,
+                db_column="max_delayed_logoff",
+                help_text='[Optional] Maximum delay in minutes that users may enter upon logging off before another user may use the tool. Some tools require "spin-down" or cleaning time after use. Leave blank to disable.',
+                null=True,
+            ),
+        ),
+        migrations.RunPython(migrate_tool_delayed_logoff, reverse_tool_delayed_logoff),
+        migrations.RemoveField(
+            model_name="tool",
+            name="_allow_delayed_logoff",
+        ),
+    ]
diff --git a/NEMO/migrations/0085_contactinformation_title_and_more.py b/NEMO/migrations/0085_contactinformation_title_and_more.py
new file mode 100644
index 00000000..dd224c73
--- /dev/null
+++ b/NEMO/migrations/0085_contactinformation_title_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.11 on 2024-06-04 02:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("NEMO", "0084_remove_tool__allow_delayed_logoff_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="contactinformation",
+            name="title",
+            field=models.CharField(blank=True, max_length=255, null=True),
+        ),
+        migrations.AlterField(
+            model_name="contactinformation",
+            name="name",
+            field=models.CharField(max_length=255),
+        ),
+        migrations.AlterField(
+            model_name="contactinformation",
+            name="office_location",
+            field=models.CharField(blank=True, max_length=255),
+        ),
+    ]
diff --git a/NEMO/migrations/0086_adjustmentrequest_new_quantity.py b/NEMO/migrations/0086_adjustmentrequest_new_quantity.py
new file mode 100644
index 00000000..f6c29427
--- /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 8316f0c1..6fb12442 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 ba796304..d3ca6b64 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,
@@ -832,20 +833,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."
+                    ),
                 }
             )
 
@@ -1271,10 +1279,11 @@ class OperationMode(object):
         blank=True,
         help_text='The amount of time (in minutes) that a tool reservation may go unused before it is automatically marked as "missed" and hidden from the calendar. Usage can be from any user, regardless of who the reservation was originally created for. The cancellation process is triggered by a timed job on the web server.',
     )
-    _allow_delayed_logoff = models.BooleanField(
-        db_column="allow_delayed_logoff",
-        default=False,
-        help_text='Upon logging off users may enter a delay before another user may use the tool. Some tools require "spin-down" or cleaning time after use.',
+    _max_delayed_logoff = models.PositiveIntegerField(
+        null=True,
+        blank=True,
+        db_column="max_delayed_logoff",
+        help_text='[Optional] Maximum delay in minutes that users may enter upon logging off before another user may use the tool. Some tools require "spin-down" or cleaning time after use. Leave blank to disable.',
     )
     _pre_usage_questions = models.TextField(
         db_column="pre_usage_questions",
@@ -1579,13 +1588,13 @@ def missed_reservation_threshold(self, value):
         self._missed_reservation_threshold = value
 
     @property
-    def allow_delayed_logoff(self):
-        return self.parent_tool.allow_delayed_logoff if self.is_child_tool() else self._allow_delayed_logoff
+    def max_delayed_logoff(self):
+        return self.parent_tool.max_delayed_logoff if self.is_child_tool() else self._max_delayed_logoff
 
-    @allow_delayed_logoff.setter
-    def allow_delayed_logoff(self, value):
-        self.raise_setter_error_if_child_tool("allow_delayed_logoff")
-        self._allow_delayed_logoff = value
+    @max_delayed_logoff.setter
+    def max_delayed_logoff(self, value):
+        self.raise_setter_error_if_child_tool("max_delayed_logoff")
+        self._max_delayed_logoff = value
 
     @property
     def pre_usage_questions(self):
@@ -2537,6 +2546,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")
 
@@ -3753,7 +3765,8 @@ class Meta(BaseCategory.Meta):
 
 
 class ContactInformation(BaseModel):
-    name = models.CharField(max_length=200)
+    name = models.CharField(max_length=CHAR_FIELD_MAXIMUM_LENGTH)
+    title = models.CharField(max_length=CHAR_FIELD_MAXIMUM_LENGTH, blank=True, null=True)
     image = models.ImageField(
         blank=True,
         help_text="Portraits are resized to 266 pixels high and 200 pixels wide. Crop portraits to these dimensions before uploading for optimal bandwidth usage",
@@ -3761,7 +3774,7 @@ class ContactInformation(BaseModel):
     category = models.ForeignKey(ContactInformationCategory, on_delete=models.CASCADE)
     email = models.EmailField(blank=True)
     office_phone = models.CharField(max_length=40, blank=True)
-    office_location = models.CharField(max_length=200, blank=True)
+    office_location = models.CharField(max_length=CHAR_FIELD_MAXIMUM_LENGTH, blank=True)
     mobile_phone = models.CharField(max_length=40, blank=True)
     mobile_phone_is_sms_capable = models.BooleanField(
         default=True,
@@ -4147,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
@@ -4185,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(
@@ -4197,7 +4216,13 @@ def get_time_difference(self) -> str:
                 else f"- {(previous_duration - new_duration)}"
             )
 
-    def editable_charge(self) -> bool:
+    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) 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"""
         return self.item and (self.get_new_end() or self.get_new_start())
 
@@ -4223,17 +4248,29 @@ 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 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()
+                self.applied = True
+                self.applied_by = user
+                self.save()
 
     def delete(self, using=None, keep_parents=False):
         adjustment_id = self.id
@@ -4250,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
             )
@@ -4262,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:
@@ -4545,6 +4583,23 @@ class Meta(BaseDocumentModel.Meta):
         verbose_name_plural = "User knowledge base item documents"
 
 
+class ToolCredentials(BaseModel):
+    tool = models.ForeignKey(Tool, on_delete=models.CASCADE)
+    username = models.CharField(null=True, blank=True, max_length=CHAR_FIELD_MAXIMUM_LENGTH)
+    password = models.CharField(null=True, blank=True, max_length=CHAR_FIELD_MAXIMUM_LENGTH)
+    comments = models.CharField(null=True, blank=True, max_length=CHAR_FIELD_MAXIMUM_LENGTH)
+    authorized_staff = models.ManyToManyField(
+        User,
+        blank=True,
+        help_text="Selected staff will be the only ones allowed to see these credentials. Leave blank for all staff.",
+    )
+
+    class Meta:
+        ordering = ["-tool__visible", "tool___category", "tool__name"]
+        verbose_name = "Tool credentials"
+        verbose_name_plural = "Tool credentials"
+
+
 class EmailLog(BaseModel):
     category = models.IntegerField(choices=EmailCategory.Choices, default=EmailCategory.GENERAL)
     when = models.DateTimeField(null=False, auto_now_add=True)
diff --git a/NEMO/policy.py b/NEMO/policy.py
index e4afb506..d94f879f 100644
--- a/NEMO/policy.py
+++ b/NEMO/policy.py
@@ -102,7 +102,7 @@ def check_to_enable_tool(
             abuse_email_address = EmailsCustomization.get("abuse_email_address")
             message = get_media_file_contents("unauthorized_tool_access_email.html")
             if abuse_email_address and message:
-                dictionary = {"operator": operator, "tool": tool, "type": "access"}
+                dictionary = {"operator": operator, "tool": tool, "type": "area-access"}
                 rendered_message = render_email_template(message, dictionary)
                 send_mail(
                     subject="Area access requirement",
@@ -126,7 +126,7 @@ def check_to_enable_tool(
                     dictionary = {
                         "operator": operator,
                         "tool": tool,
-                        "type": "reservation",
+                        "type": "area-reservation",
                     }
                     rendered_message = render_email_template(message, dictionary)
                     send_mail(
@@ -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=120):
-            return HttpResponseBadRequest("Post-usage tool downtime may not exceed 120 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 not tool.allow_delayed_logoff 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(
diff --git a/NEMO/serializers.py b/NEMO/serializers.py
index 4f8ddc92..8dffb809 100644
--- a/NEMO/serializers.py
+++ b/NEMO/serializers.py
@@ -20,10 +20,12 @@
 from NEMO.models import (
     Account,
     AccountType,
+    AdjustmentRequest,
     Alert,
     AlertCategory,
     Area,
     AreaAccessRecord,
+    BuddyRequest,
     Configuration,
     ConfigurationOption,
     Consumable,
@@ -32,16 +34,20 @@
     Interlock,
     InterlockCard,
     InterlockCardCategory,
+    PhysicalAccessLevel,
     Project,
     ProjectDiscipline,
     Qualification,
+    RecurringConsumableCharge,
     Reservation,
     Resource,
     ScheduledOutage,
     StaffCharge,
     Task,
     TaskHistory,
+    TemporaryPhysicalAccessRequest,
     Tool,
+    ToolCredentials,
     TrainingSession,
     UsageEvent,
     User,
@@ -398,6 +404,70 @@ 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 PhysicalAccessLevelSerializer(FlexFieldsSerializerMixin, ModelSerializer):
+    class Meta:
+        model = PhysicalAccessLevel
+        fields = "__all__"
+        expandable_fields = {"area": "NEMO.serializers.AreaSerializer"}
+
+
+class BuddyRequestSerializer(FlexFieldsSerializerMixin, ModelSerializer):
+    class Meta:
+        model = BuddyRequest
+        fields = "__all__"
+        expandable_fields = {
+            "area": "NEMO.serializers.AreaSerializer",
+            "user": "NEMO.serializers.UserSerializer",
+        }
+
+
+class TemporaryPhysicalAccessRequestSerializer(FlexFieldsSerializerMixin, ModelSerializer):
+    class Meta:
+        model = TemporaryPhysicalAccessRequest
+        fields = "__all__"
+        expandable_fields = {
+            "creator": "NEMO.serializers.UserSerializer",
+            "last_updated_by": "NEMO.serializers.UserSerializer",
+            "physical_access_level": "NEMO.serializers.PhysicalAccessLevelSerializer",
+            "other_users": ("NEMO.serializers.UserSerializer", {"many": True}),
+        }
+
+
+class AdjustmentRequestSerializer(FlexFieldsSerializerMixin, ModelSerializer):
+    class Meta:
+        model = AdjustmentRequest
+        fields = "__all__"
+        expandable_fields = {
+            "creator": "NEMO.serializers.UserSerializer",
+            "last_updated_by": "NEMO.serializers.UserSerializer",
+            "reviewer": "NEMO.serializers.UserSerializer",
+            "item_type": "NEMO.serializers.ContentTypeSerializer",
+            "applied_by": "NEMO.serializers.UserSerializer",
+        }
+
+
+class ToolCredentialsSerializer(FlexFieldsSerializerMixin, ModelSerializer):
+    class Meta:
+        model = ToolCredentials
+        fields = "__all__"
+        expandable_fields = {
+            "tool": "NEMO.serializers.ToolSerializer",
+            "authorized_staff": ("NEMO.serializers.UserSerializer", {"many": True}),
+        }
+
+
 class PermissionSerializer(FlexFieldsSerializerMixin, ModelSerializer):
     users = PrimaryKeyRelatedField(
         source="user_set", many=True, queryset=User.objects.all(), allow_null=True, required=False
diff --git a/NEMO/static/nemo.css b/NEMO/static/nemo.css
index 72246877..1667c609 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 29b29f7f..c608ca32 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);
 	});
@@ -167,7 +167,7 @@ function hide_empty_tool_categories()
 		let categoryHasItem = false;
 		$(category).find("li>a").each((toolIdx, tool) =>
 		{
-			let toolStyle = $(tool).attr("style");
+			let toolStyle = $(tool).parent("li").attr("style");
 
 			if (toolStyle === undefined || toolStyle !== "display: none;")
 			{
@@ -865,3 +865,33 @@ function collapse_navbar(max_width, max_height)
 		$("body").addClass("force-navbar-collapse");
 	}
 }
+
+function table_search(table_id, always_show_rows)
+{
+	always_show_rows = always_show_rows || [];
+	return function ()
+	{
+		let rows = $("#"+table_id).find("tr").hide();
+		if (this.value.length)
+		{
+			let data = this.value.split(" ");
+			$.each(data, function (i, v)
+			{
+				$.each(rows, function(i, row)
+				{
+				   let $row = $(row);
+				   if (always_show_rows.includes(i)) {$row.show();return true;}
+				   // Only look in td within the row that don't have display:none, so we don't only look at visible cells
+				   if ($row.find("td").filter(function() { return $(this).css('display') !== 'none'; }).filter(":icontains('" + v + "')").length !== 0)
+				   {
+					   $row.show();
+				   }
+				});
+			});
+		}
+		else
+		{
+			rows.show();
+		}
+	}
+}
\ No newline at end of file
diff --git a/NEMO/templates/accounts_and_projects/account_and_projects.html b/NEMO/templates/accounts_and_projects/account_and_projects.html
index e9f31f44..7a4d6fd6 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 @@ <h2>
                         </form>
                     </h2>
                 {% endif %}
-                {% for project in account.project_set.all %}
+                {% for project in account_projects %}
                     {% if not selected_project or project == selected_project %}
                         <div class="h3">
                             <a onclick="toggle_details(this)"
diff --git a/NEMO/templates/base.html b/NEMO/templates/base.html
index 534848ec..d82b7380 100644
--- a/NEMO/templates/base.html
+++ b/NEMO/templates/base.html
@@ -44,6 +44,11 @@
             <link rel="stylesheet" type="text/css" href="{% static "bootstrap/css/bootstrap-theme.min.css" %}" />
             {# Typeahead #}
             <script type="text/javascript" src="{% static "typeahead.jquery.min.js" %}"></script>
+            <script>
+                moment.updateLocale('en', {
+                    week: { dow: "{{ calendar_first_day_of_week }}" }
+                });
+            </script>
         {% endblock %}
         {# NEMO #}
         <link rel="shortcut icon" type="image/x-icon" href="{% static 'favicon.ico' %}" />
diff --git a/NEMO/templates/base/impersonate_header.html b/NEMO/templates/base/impersonate_header.html
index 438e473c..4e75659e 100644
--- a/NEMO/templates/base/impersonate_header.html
+++ b/NEMO/templates/base/impersonate_header.html
@@ -1,5 +1,6 @@
 <div class="alert-warning" style="padding: 10px; text-align: center">
-    <span style="margin-right: 15px">You are currently impersonating {{ request.session.impersonated_user }}</span><a href="{% url 'impersonate' %}?unimpersonate" style="color: initial">
+    <span style="margin-right: 15px">You are currently impersonating {{ request.session.impersonated_user }}</span>
+    <a href="{% url 'impersonate' %}?unimpersonate" style="color: initial; text-decoration: none">
         <button>Switch back</button>
     </a>
 </div>
diff --git a/NEMO/templates/base/navbar_base.html b/NEMO/templates/base/navbar_base.html
index f148ab3f..0502fa7d 100644
--- a/NEMO/templates/base/navbar_base.html
+++ b/NEMO/templates/base/navbar_base.html
@@ -73,19 +73,17 @@
                                     {% endif %}
                                     {% if tools_exist %}
                                         {% navigation_url 'configuration_agenda' 'Configuration agenda' user.is_staff user.is_superuser %}
+                                        {% navigation_url 'maintenance' 'Maintenance' user.is_staff  user.is_superuser %}
+                                        {% navigation_url 'qualifications' 'Qualifications' user.is_staff user.is_superuser %}
+                                        {% navigation_url 'training' 'Training' user.is_staff user.is_superuser %}
+                                        {% navigation_url 'tool_credentials' 'Tool credentials' user.is_staff user.is_superuser %}
                                     {% endif %}
                                     {% if user.is_facility_manager or user.is_superuser or user.is_staff and "contracts"|customization:"contracts_view_staff" == "enabled" or user.is_user_office and "contracts"|customization:"contracts_view_user_office" == "enabled" or user.is_accounting_officer and "contracts"|customization:"contracts_view_accounting_officer" == "enabled" %}
                                         {% navigation_url 'service_contracts' 'Contracts & procurements' %}
                                     {% endif %}
                                     {% navigation_url 'customization' 'Customization' user.is_superuser %}
                                     {% navigation_url 'email_broadcast' 'Email' %}
-                                    {% if tools_exist %}
-                                        {% navigation_url 'maintenance' 'Maintenance' user.is_staff  user.is_superuser %}
-                                    {% endif %}
                                     {% navigation_url 'project_billing' 'Project billing' user.is_accounting_officer user.is_user_office user.is_superuser %}
-                                    {% if tools_exist %}
-                                        {% navigation_url 'qualifications' 'Qualifications' user.is_staff user.is_superuser %}
-                                    {% endif %}
                                     {% navigation_url 'recurring_charges' recurring_charges_name user.is_user_office user.is_facility_manager user.is_superuser %}
                                     {% navigation_url 'staff_charges' 'Remote work' user.is_staff user.is_superuser %}
                                     {% navigation_url 'resources' 'Resources' user.is_staff user.is_superuser %}
@@ -98,9 +96,6 @@
                                         </li>
                                     {% endif %}
                                     {% navigation_url 'consumables' 'Supplies' user.is_staff user.is_user_office user.is_superuser %}
-                                    {% if tools_exist %}
-                                        {% navigation_url 'training' 'Training' user.is_staff user.is_superuser %}
-                                    {% endif %}
                                     {% navigation_url 'users' 'Users' %}
                                     {% if "projects_and_accounts"|customization:"project_allow_transferring_charges" == "enabled" %}
                                         {% navigation_url 'transfer_charges' 'Transfer charges' user.is_accounting_officer user.is_facility_manager user.is_superuser %}
@@ -130,6 +125,11 @@
                         <script>$("#staff_charge_link").tooltip();</script>
                     </li>
                 {% endif %}
+                {% if allow_profile_view %}
+                    <li class="visible-xs visible-sm">
+                        <a href="{% url 'view_user' user.id %}">My profile</a>
+                    </li>
+                {% endif %}
                 <li class="visible-xs visible-sm">
                     <a href="{% url 'user_preferences' %}">Preferences</a>
                 </li>
@@ -157,9 +157,17 @@
                         <script>$("#staff_charge_link").tooltip();</script>
                     </li>
                 {% endif %}
-                <li>
-                    <p class="navbar-text">Welcome, {{ user.first_name }}</p>
-                </li>
+                {% block welcome_user %}
+                    <li>
+                        <p class="navbar-text">
+                            Welcome,
+                            {% if allow_profile_view %}
+                                <a href="{% url 'view_user' user.id %}">
+                                {% endif %}
+                            {{ user.first_name }}</a>
+                        </p>
+                    </li>
+                {% endblock %}
                 <li class="navbar-preferences">
                     <a href="{% url 'user_preferences' %}">
                         <img src="{% static 'icons/preferences.png' %}" alt="preferences" />
diff --git a/NEMO/templates/calendar/calendar.html b/NEMO/templates/calendar/calendar.html
index 3085973f..b1f830bb 100644
--- a/NEMO/templates/calendar/calendar.html
+++ b/NEMO/templates/calendar/calendar.html
@@ -98,12 +98,20 @@
                     onclick="clear_specific_user()"></button>
             {# The following menu tree code was take from an example at http://www.bootply.com/120625 #}
             <ul id="extra-links" class="nav nav-list" style="margin-top: 10px; margin-bottom: 8px;">
-                <li>
+                <li style="display: flex">
+                    <input type="checkbox"
+                           id="checkbox-personal-schedule-personal-schedule"
+                           name="item-checkbox"
+                           class="item-checkbox"
+                           data-item-id="personal-schedule"
+                           data-item-type="personal-schedule"
+                           style="display: none">
                     <a href="javascript:void(0)"
                        id="extra-links-personal-schedule"
                        class="selected personal_schedule"
                        data-item-id="personal_schedule"
-                       onclick="on_extra_link_click(this)">Personal schedule</a>
+                       data-item-type="personal-schedule"
+                       onclick="set_selected_item(this, true)">Personal schedule</a>
                 </li>
                 {% if calendar_all_tools %}
                     <li>
@@ -144,6 +152,7 @@
         <div id="calendar" class="application-content"></div>
         <input type="hidden" id="impersonate" name="impersonate" value="">
         <script type="text/javascript">
+let all_event_ids = {}; // we'll use this to remove potential duplicates
 let logged_in = false;
 {% if user.in_area %}logged_in = "{{ user.area_access_record.area.name }}";{% endif %}
 
@@ -173,9 +182,11 @@
     if (event_type === 'reservations' || event_type === 'configuration agenda' || event_type === '{{ facility_name|lower }} usage')
 	{
 		let item = get_selected_item();
+        const checked_items = get_checked_items();
+		const has_checked_items = checked_items && checked_items.length > 0;
 		let scheduled_outage_button = $(".fc-scheduledOutage-button")[0];
 		$(scheduled_outage_button).attr("disabled", "true").addClass('fc-state-disabled');
-		if(item === 'personal_schedule')
+		if(item === 'personal_schedule' && !has_checked_items)
 		{
             add_item_to_event_feed_list(item_event_source_list, url, event_type, { personal_schedule: true });
 		}
@@ -193,9 +204,8 @@
 		}
         else
         {
-            let selected_tree_item = item ? JSON.parse(item) : undefined;
-            const checked_items = get_checked_items();
-            const has_checked_items = checked_items && checked_items.length > 0;
+            let selected_tree_item = undefined;
+            try { if (item) { selected_tree_item = JSON.parse(item); } } catch (error) {}
             const one_item_checked_and_selected = selected_tree_item && checked_items && checked_items.length === 1 && checked_items[0].id === selected_tree_item.id && checked_items[0].type === selected_tree_item.type;
             const display_name = has_checked_items && !one_item_checked_and_selected;
 
@@ -214,11 +224,13 @@
             {
                 {# If the checked item is not already selected #}
                 if(!selected_tree_item || selected_tree_item.id !== checked_item.id || selected_tree_item.type !== checked_item.type) {
-                    add_item_to_event_feed_list(item_event_source_list, url, event_type, {
+                    let feed_data = {
                         item_id: checked_item.id,
                         item_type: checked_item.type,
                         display_name,
-                    });
+                    }
+                    if (checked_item.type === "personal-schedule") feed_data = { personal_schedule: true };
+                    add_item_to_event_feed_list(item_event_source_list, url, event_type, feed_data);
                 }
             });
         }
@@ -310,6 +322,17 @@
 
 function event_render(event, element)
 {
+    // We should only have duplicates when there is a different origin (tool vs personal schedule for example)
+    // It turns out a event that spans multiple days is rendered more than once with the same id
+    let event_origin = event.source?.data?.personal_schedule || event.source?.data?.item_type;
+    if (event.id)
+    {
+        if (all_event_ids[event.id] && all_event_ids[event.id] !== event_origin)
+        {
+            return false;
+        }
+        all_event_ids[event.id] = event_origin;
+    }
 	if ('tooltip' in event)
 		element.attr('title', event.tooltip)
 	else if ('title' in event)
@@ -327,6 +350,7 @@
 
 function event_after_all_render()
 {
+    all_event_ids = {}; // clear the list of events rendered
 	{# Remove visible tooltips first otherwise they get stuck #}
 	$('.tooltip').remove();
 	{# Show tooltips for events #}
diff --git a/NEMO/templates/customizations/customizations_dashboard.html b/NEMO/templates/customizations/customizations_dashboard.html
index 18150508..d954e61a 100644
--- a/NEMO/templates/customizations/customizations_dashboard.html
+++ b/NEMO/templates/customizations/customizations_dashboard.html
@@ -38,6 +38,40 @@ <h5>Area occupancy</h5>
                 </div>
             </div>
         </div>
+        <div class="form-group" style="margin-bottom: 0; border-top: dotted 1px #ddd">
+            <div class="col-md-5">
+                <h5>Tools</h5>
+            </div>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-md-3">Sort</label>
+            <div class="col-md-9">
+                <div class="radio">
+                    <label>
+                        <input type="radio"
+                               name="dashboard_tool_sort"
+                               {% if dashboard_tool_sort == "name" %}checked{% endif %}
+                               value="name">
+                        By name
+                    </label>
+                    <label>
+                        <input type="radio"
+                               name="dashboard_tool_sort"
+                               {% if dashboard_tool_sort == "time_desc" %}checked{% endif %}
+                               value="time_desc">
+                        By time descending
+                    </label>
+                    <label>
+                        <input type="radio"
+                               name="dashboard_tool_sort"
+                               {% if dashboard_tool_sort == "time_asc" %}checked{% endif %}
+                               value="time_asc">
+                        By time ascending
+                    </label>
+                    <br />
+                </div>
+            </div>
+        </div>
         <div class="form-group" style="margin-bottom: 0; border-top: dotted 1px #ddd">
             <div class="col-md-5">
                 <h5>Staff status</h5>
diff --git a/NEMO/templates/customizations/customizations_projects_and_accounts.html b/NEMO/templates/customizations/customizations_projects_and_accounts.html
index 54558eaa..2c7c9614 100644
--- a/NEMO/templates/customizations/customizations_projects_and_accounts.html
+++ b/NEMO/templates/customizations/customizations_projects_and_accounts.html
@@ -21,6 +21,23 @@ <h3 class="customization-section-title">Projects & accounts</h3>
                 Check this box to only show active accounts in the account list.
             </div>
         </div>
+        <div class="form-group" style="margin-bottom: 0">
+            <div class="col-md-offset-3 col-md-9">
+                <div class="checkbox">
+                    <label>
+                        <input type="checkbox"
+                               id="account_project_list_active_only"
+                               name="account_project_list_active_only"
+                               {% if account_project_list_active_only %}checked{% endif %}
+                               value="enabled">
+                        Hide inactive projects in individual account page
+                    </label>
+                </div>
+            </div>
+            <div class="col-md-offset-3 col-md-9 help-block light-grey">
+                Check this box to only show active projects when looking at an individual account.
+            </div>
+        </div>
         <div class="form-group" style="margin-bottom: 0">
             <div class="col-md-offset-3 col-md-9">
                 <div class="checkbox">
diff --git a/NEMO/templates/customizations/customizations_requests.html b/NEMO/templates/customizations/customizations_requests.html
index 6f517fce..b883619c 100644
--- a/NEMO/templates/customizations/customizations_requests.html
+++ b/NEMO/templates/customizations/customizations_requests.html
@@ -106,7 +106,47 @@ <h3 class="customization-section-title">Adjustment requests settings</h3>
                         </label>
                         <br />
                     </div>
-                    <div class="row col-lg-4 col-xs-5">
+                    <div class="row col-xs-12">
+                        <input type="hidden" name="adjustment_requests_consumable_withdrawal_enabled" value="off" />
+                        <label>
+                            <input type="checkbox"
+                                   name="adjustment_requests_consumable_withdrawal_enabled"
+                                   {% if adjustment_requests_consumable_withdrawal_enabled == "enabled" %}checked{% endif %}
+                                   value="enabled">
+                            Consumable withdrawals
+                        </label>
+                    </div>
+                    <div class="row col-xs-offset-1 col-xs-8">
+                        <input type="hidden" name="adjustment_requests_consumable_withdrawal_self_checkout" value="off" />
+                        <label>
+                            <input type="checkbox"
+                                   name="adjustment_requests_consumable_withdrawal_self_checkout"
+                                   {% if adjustment_requests_consumable_withdrawal_self_checkout == "enabled" %}checked{% endif %}
+                                   value="enabled">
+                            self checked out items
+                        </label>
+                    </div>
+                    <div class="row col-xs-offset-1 col-xs-8">
+                        <input type="hidden" name="adjustment_requests_consumable_withdrawal_staff_checkout" value="off" />
+                        <label>
+                            <input type="checkbox"
+                                   name="adjustment_requests_consumable_withdrawal_staff_checkout"
+                                   {% if adjustment_requests_consumable_withdrawal_staff_checkout == "enabled" %}checked{% endif %}
+                                   value="enabled">
+                            staff checked out items
+                        </label>
+                    </div>
+                    <div class="row col-xs-offset-1 col-xs-8">
+                        <input type="hidden" name="adjustment_requests_consumable_withdrawal_usage_event" value="off" />
+                        <label>
+                            <input type="checkbox"
+                                   name="adjustment_requests_consumable_withdrawal_usage_event"
+                                   {% if adjustment_requests_consumable_withdrawal_usage_event == "enabled" %}checked{% endif %}
+                                   value="enabled">
+                            pre-post usage question items
+                        </label>
+                    </div>
+                    <div class="row col-xs-12">
                         <input type="hidden" name="adjustment_requests_missed_reservation_enabled" value="off" />
                         <label>
                             <input type="checkbox"
@@ -116,7 +156,7 @@ <h3 class="customization-section-title">Adjustment requests settings</h3>
                             Missed reservation
                         </label>
                     </div>
-                    <div class="row col-xs-8">
+                    <div class="row col-xs-offset-1 col-xs-8">
                         <label>
                             <input type="checkbox"
                                    name="adjustment_requests_missed_reservation_times"
diff --git a/NEMO/templates/customizations/customizations_templates.html b/NEMO/templates/customizations/customizations_templates.html
index 7a9378c5..f9a835e1 100644
--- a/NEMO/templates/customizations/customizations_templates.html
+++ b/NEMO/templates/customizations/customizations_templates.html
@@ -485,8 +485,8 @@ <h3 class="customization-section-title" id="tool_qualification_expiration_email_
         <h3 class="customization-section-title" id="unauthorized_tool_access_email_id">Unauthorized tool access email</h3>
         <p>This email is sent when a user tries to access a tool:</p>
         <ul>
-            <li>without being logged in to the area in which the tool resides (type 'area').</li>
-            <li>without having a current area reservation (type 'reservation')</li>
+            <li>without being logged in to the area in which the tool resides (type 'area-access').</li>
+            <li>without having a current area reservation (type 'area-reservation')</li>
         </ul>
         <p>The following context variables are provided when the email is rendered:</p>
         <ul>
@@ -497,7 +497,7 @@ <h3 class="customization-section-title" id="unauthorized_tool_access_email_id">U
                 <b>tool</b> - the tool that the user was denied access to
             </li>
             <li>
-                <b>type</b> - the type of abuse ('area' or 'reservation')
+                <b>type</b> - the type of abuse ('area-access' or 'area-reservation')
             </li>
         </ul>
         {% include 'customizations/customizations_upload.html' with element=unauthorized_tool_access_email name='unauthorized tool access email' key='templates' %}
diff --git a/NEMO/templates/customizations/customizations_tool.html b/NEMO/templates/customizations/customizations_tool.html
index 276c8fe0..911e63ee 100644
--- a/NEMO/templates/customizations/customizations_tool.html
+++ b/NEMO/templates/customizations/customizations_tool.html
@@ -113,6 +113,13 @@ <h3 class="customization-section-title">Tool problems</h3>
                                value="enabled">
                         Send new tool problems to all qualified users (but not updates)
                     </label>
+                    <label>
+                        <input type="checkbox"
+                               name="tool_problem_allow_regular_user_preferences"
+                               {% if tool_problem_allow_regular_user_preferences %}checked{% endif %}
+                               value="enabled">
+                        Allow users to subscribe to new tools problems in their preferences (only on qualified tools)
+                    </label>
                 </div>
             </div>
         </div>
@@ -136,6 +143,14 @@ <h3 class="customization-section-title">Tool problems</h3>
                                value="enabled">
                         Send all tool problem & updates to tool superusers
                     </label>
+                    <br>
+                    <label>
+                        <input type="checkbox"
+                               name="tool_task_updates_allow_regular_user_preferences"
+                               {% if tool_task_updates_allow_regular_user_preferences %}checked{% endif %}
+                               value="enabled">
+                        Allow users to subscribe to tools updates in their preferences (only on qualified tools)
+                    </label>
                 </div>
             </div>
         </div>
@@ -257,6 +272,22 @@ <h3 class="customization-section-title">Tool control</h3>
                 </div>
             </div>
         </div>
+        <div class="form-group">
+            <label class="control-label col-sm-4 col-md-3">Tool credentials</label>
+            <div class="col-sm-8 col-md-9">
+                <div class="checkbox">
+                    <label>
+                        <input type="hidden" name="tool_control_show_tool_credentials" value="off" />
+                        <input type="checkbox"
+                               id="tool_control_show_tool_credentials"
+                               name="tool_control_show_tool_credentials"
+                               {% if tool_control_show_tool_credentials == "enabled" %}checked{% endif %}
+                               value="enabled">
+                        Show tool credentials to staff in tool control -> details tab
+                    </label>
+                </div>
+            </div>
+        </div>
         <div class="customization-separation" style="margin-bottom: 15px"></div>
         <h3 class="customization-section-title">Tool wait list</h3>
         <div class="form-group">
diff --git a/NEMO/templates/customizations/customizations_training.html b/NEMO/templates/customizations/customizations_training.html
index 3845178c..9c40de58 100644
--- a/NEMO/templates/customizations/customizations_training.html
+++ b/NEMO/templates/customizations/customizations_training.html
@@ -36,7 +36,44 @@ <h3 class="customization-section-title">Training</h3>
                 <div class="help-block light-grey" style="margin-bottom: 0">Qualifications will still happen in real time</div>
             </div>
         </div>
+        <div class="form-group {% if errors.training_included_hidden_tools %}has-error{% endif %}">
+            <label class="control-label col-lg-3 col-md-4 col-sm-5" for="include_hidden_tool">Included hidden tools</label>
+            <div class="col-lg-9 col-md-8 col-sm-7">
+                <input id="include_hidden_tool"
+                       type="text"
+                       autocomplete="off"
+                       class="form-control"
+                       placeholder="Search for a non-visible tool to include">
+            </div>
+            {% if errors.training_included_hidden_tools %}
+                <div class="col-lg-offset-3 col-md-offset-4 col-sm-offset-5 col-lg-9 col-md-8 col-sm-7 help-block light-grey">
+                    {{ errors.training_included_hidden_tools.error }}
+                </div>
+            {% endif %}
+        </div>
+        <div class="form-group">
+            <div id="include-hidden-list"
+                 class="col-lg-offset-3 col-md-offset-4 col-sm-offset-5 col-lg-9 col-md-8 col-sm-7">
+                No hidden tools are included.
+            </div>
+        </div>
         <div class="customization-separation" style="margin-bottom: 15px"></div>
         <div class="text-center">{% button type="save" value="Save settings" %}</div>
     </form>
 </div>
+<script>
+	function add_tool(jquery_event, search_selection, dataset_name)
+	{
+        $(this).typeahead('val', '').focus();
+		add_to_list("#include-hidden-list", "remove_tool", search_selection.id, search_selection.name, "Remove " + search_selection.name, "training_included_hidden_tools_list");
+	}
+    function remove_tool(tool_id)
+	{
+		remove_from_list("#include-hidden-list", "#training_included_hidden_tools_list_" + tool_id, "No hidden tools are included.");
+	}
+	$('#include_hidden_tool').autocomplete('tools', add_tool, {{ tools|json_search_base }});
+    {% for tool in included_hidden_tools %}
+		add_tool(undefined, {name:'{{ tool.name }}', id:{{ tool.id }}});
+	{% endfor %}
+	auto_size_textarea(document.getElementById("training_request_default_message_placeholder"));
+</script>
diff --git a/NEMO/templates/customizations/customizations_user.html b/NEMO/templates/customizations/customizations_user.html
index e105f686..4d80fd28 100644
--- a/NEMO/templates/customizations/customizations_user.html
+++ b/NEMO/templates/customizations/customizations_user.html
@@ -30,6 +30,28 @@ <h3 class="customization-section-title">General</h3>
                 Whether the facility rules tutorial is required for new users by default.
             </div>
         </div>
+        <div class="form-group">
+            <label class="control-label col-md-4 col-lg-3" for="user_type_required">User type</label>
+            <div class="col-md-8 col-lg-9">
+                <div class="radio">
+                    <label>
+                        <input type="radio"
+                               id="user_type_required"
+                               name="user_type_required"
+                               {% if user_type_required %}checked{% endif %}
+                               value="enabled">
+                        Required
+                    </label>
+                    <label>
+                        <input type="radio" name="user_type_required" {% if not user_type_required %}checked{% endif %} value="">
+                        Not required
+                    </label>
+                </div>
+            </div>
+            <div class="col-lg-offset-3 col-md-offset-4 col-md-8 col-lg-9 help-block light-grey">
+                Whether the user type is required when adding new users.
+            </div>
+        </div>
         <div class="form-group">
             <label class="control-label col-md-4 col-lg-3" for="user_list_active_only">User list</label>
             <div class="col-md-8 col-lg-9">
@@ -65,6 +87,24 @@ <h3 class="customization-section-title">General</h3>
                 Check this box to allow uploading documents to users.
             </div>
         </div>
+        <div class="form-group">
+            <label class="control-label col-md-4 col-lg-3" for="user_allow_profile_view">Enable user profile view</label>
+            <div class="col-md-8 col-lg-9">
+                <div class="checkbox">
+                    <label>
+                        <input type="checkbox"
+                               id="user_allow_profile_view"
+                               name="user_allow_profile_view"
+                               {% if user_allow_profile_view %}checked{% endif %}
+                               value="enabled">
+                        Enable
+                    </label>
+                </div>
+            </div>
+            <div class="col-lg-offset-3 col-md-offset-4 col-md-8 col-lg-9 help-block light-grey">
+                Check this box to enable users to view their own profile by clicking on their name in the navigation bar.
+            </div>
+        </div>
         <div class="customization-separation" style="margin-bottom: 15px"></div>
         <h3 class="customization-section-title">Access expiration - reminders</h3>
         <div class="form-group {% if errors.user_access_expiration_reminder_days %}has-error{% endif %}">
diff --git a/NEMO/templates/email/compose_email.html b/NEMO/templates/email/compose_email.html
index 0ba949c1..527d240a 100644
--- a/NEMO/templates/email/compose_email.html
+++ b/NEMO/templates/email/compose_email.html
@@ -32,13 +32,13 @@ <h1>Compose an email</h1>
                         <div class="checkbox">
                             <label>
                                 <input type="checkbox" name="only_active_users" onclick="updated_email_link()" checked>
-                                Only send this email to active users
+                                Only send this email to users with an active account
                             </label>
                         </div>
                         <div class="checkbox">
                             <label>
                                 <input type="checkbox" name="only_active_access_users" onclick="updated_email_link()" checked>
-                                Only send this email to users with active {{ site_title }} access
+                                Only send this email to users with active access (no access expiration or set in the future)
                             </label>
                         </div>
                         <div class="checkbox">
diff --git a/NEMO/templates/requests/adjustment_requests/adjustment_request.html b/NEMO/templates/requests/adjustment_requests/adjustment_request.html
index 0208fe47..7be3f262 100644
--- a/NEMO/templates/requests/adjustment_requests/adjustment_request.html
+++ b/NEMO/templates/requests/adjustment_requests/adjustment_request.html
@@ -104,6 +104,27 @@ <h1 class="form-group">
                     {% endif %}
                 </div>
             {% endif %}
+            {% if change_quantity_allowed and form.item_id.value and form.item_type.value %}
+                <div class="form-group">
+                    <div class="row">
+                        <div class="col-md-3 col-sm-6">
+                            <label for="new_quantity">New quantity</label>
+                            <input type="text"
+                                   class="form-control text-center"
+                                   name="new_quantity"
+                                   id="new_quantity"
+                                   value="{{ form.new_quantity.value }}">
+                        </div>
+                    </div>
+                    {% if form.new_quantity.errors %}
+                        <div class="row">
+                            <div class="col-md-3 col-sm-6">
+                                {% if form.new_quantity.errors %}<span style="color:red">{{ form.new_quantity.errors|striptags }}</span>{% endif %}
+                            </div>
+                        </div>
+                    {% endif %}
+                </div>
+            {% endif %}
             <div class="form-group">
                 <label for="description" class="control-label">Description</label>
                 {% if form.description.errors %}- <span style="color:red">{{ form.description.errors|striptags }}</span>{% endif %}
@@ -117,7 +138,9 @@ <h1 class="form-group">
             {% if form.instance.get_status_display == "Pending" and instance_id and user in form.instance.reviewers %}
                 <div class="form-group">
                     <label for="manager_note" class="control-label">Feedback/Instructions</label>
-                    {% if form.description.errors %}- <span style="color:red">{{ form.manager_note.errors|striptags }}</span>{% endif %}
+                    {% if form.description.errors %}
+                        - <span style="color:red">{{ form.manager_note.errors|striptags }}</span>
+                    {% endif %}
                     <textarea class="form-control"
                               rows="3"
                               name="manager_note"
@@ -128,7 +151,7 @@ <h1 class="form-group">
             <div class="text-right">
                 {% 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 e4fd7ef5..09750416 100644
--- a/NEMO/templates/requests/adjustment_requests/adjustment_requests_table.html
+++ b/NEMO/templates/requests/adjustment_requests/adjustment_requests_table.html
@@ -5,8 +5,7 @@
             <th>Created</th>
             {% if user_is_reviewer or user.is_any_part_of_staff %}<th>User</th>{% endif %}
             <th class="text-center">Charge</th>
-            <th class="text-center text-nowrap">New start</th>
-            <th class="text-center text-nowrap">New end</th>
+            <th class="text-center text-nowrap">Change</th>
             <th class="text-center">Difference</th>
             <th class="text-right text-nowrap">
                 {% if request_status == 'pending' and user_is_reviewer %}
@@ -28,13 +27,19 @@
                 </td>
                 {% if user_is_reviewer or user.is_any_part_of_staff %}<td>{{ a_request.creator.get_name }}</td>{% endif %}
                 <td class="text-center">{{ a_request.item.get_display }}</td>
-                <td class="text-center">{{ a_request.get_new_start|date:'SHORT_DATETIME_FORMAT' }}</td>
-                <td class="text-center">{{ a_request.get_new_end|date:'SHORT_DATETIME_FORMAT' }}</td>
-                <td class="text-center">{{ a_request.get_time_difference|default_if_none:"" }}</td>
+                <td class="text-center text-nowrap">
+                    {% if a_request.get_new_start %}
+                        start -> {{ a_request.get_new_start|date:'SHORT_DATETIME_FORMAT' }}
+                        {% if a_request.get_new_end %}<br>{% endif %}
+                    {% endif %}
+                    {% if a_request.get_new_end %}end -> {{ a_request.get_new_end|date:'SHORT_DATETIME_FORMAT' }}{% endif %}
+                    {% if a_request.new_quantity is not None %}quantity -> {{ a_request.new_quantity }}{% endif %}
+                </td>
+                <td class="text-center">{{ a_request.get_difference|default_if_none:"" }}</td>
                 <td class="text-right text-nowrap" rowspan="2">
                     {% 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 %}
diff --git a/NEMO/templates/safety/safety_data_sheets.html b/NEMO/templates/safety/safety_data_sheets.html
index 03e96125..f57ddb12 100644
--- a/NEMO/templates/safety/safety_data_sheets.html
+++ b/NEMO/templates/safety/safety_data_sheets.html
@@ -141,30 +141,7 @@
     {% endif %}
     <script>
         $("html").css("overflow", "hidden");
-        $("#search").keyup(function ()
-        {
-            let rows = $("#table-to-filter").find("tr").hide();
-            if (this.value.length)
-            {
-                let data = this.value.split(" ");
-                $.each(data, function (i, v)
-                {
-                    $.each(rows, function(i, row)
-                    {
-                       let $row = $(row);
-                       // Only look in td within the row that don't have display:none, so we don't look in keyworkds unless they are shown
-                       if ($row.find("td").filter(function() { return $(this).css('display') !== 'none'; }).filter(":icontains('" + v + "')").length !== 0)
-                       {
-                           $row.show();
-                       }
-                    });
-                });
-            }
-            else
-            {
-                rows.show();
-            }
-        });
+        $("#search").keyup(table_search("table-to-filter"));
 
         function resize_container()
         {
diff --git a/NEMO/templates/snippets/contact_person.html b/NEMO/templates/snippets/contact_person.html
index 7998489e..ef715711 100644
--- a/NEMO/templates/snippets/contact_person.html
+++ b/NEMO/templates/snippets/contact_person.html
@@ -1,4 +1,7 @@
 <h4 style="margin-top:0; text-align: center">{{ person.name }}</h4>
+{% if person.title %}
+    <div style="margin-top:0;margin-bottom: 5px;text-align: center">{{ person.title }}</div>
+{% endif %}
 {% if person.email %}
     Email: <a target="_blank"
     href="{% if email_form %}{% url 'get_email_form' %}?recipient={{ person.email }}{% else %}mailto:{{ person.email }}{% endif %}">{{ person.email }}</a>
diff --git a/NEMO/templates/tool_control/tool_status.html b/NEMO/templates/tool_control/tool_status.html
index dbea0a9f..44bcdcca 100644
--- a/NEMO/templates/tool_control/tool_status.html
+++ b/NEMO/templates/tool_control/tool_status.html
@@ -348,7 +348,7 @@ <h5>{{ o.creator }} scheduled this outage from {{ o.start }} until {{ o.end }}.<
             {% if tool.in_use %}
                 {% if tool.get_current_usage_event.operator.id == user.id or tool.get_current_usage_event.user.id == user.id %}
                     {{ post_usage_questions }}
-                    {% if tool.allow_delayed_logoff and not tool.delayed_logoff_in_progress %}
+                    {% if tool.max_delayed_logoff is not None and not tool.delayed_logoff_in_progress %}
                         <div class="form-group">
                             Prevent others from using the tool for
                             <input type="number"
@@ -359,7 +359,7 @@ <h5>{{ o.creator }} scheduled this outage from {{ o.start }} until {{ o.end }}.<
                                    style="display:inline;
                                           width:auto"
                                    min="5"
-                                   max="120"
+                                   max="{{ tool.max_delayed_logoff }}"
                                    inputmode="numeric"
                                    pattern="[0-9]*"
                                    placeholder="0">
@@ -658,6 +658,30 @@ <h4 id="what_to_do_label">What would you like to do?</h4>
                 {% endif %}
             </div>
         </div>
+        {% if user.is_staff and tool_credentials %}
+            <div class="media">
+                <a onclick="toggle_details(this)"
+                   data-toggle="collapse"
+                   data-target="#tool_credentials"
+                   class="pointer">
+                    <span class="glyphicon glyphicon-lock pull-left notification-icon warning-highlight"></span><span class="glyphicon glyphicon-chevron-right pull-left chevron"></span>
+                </a>
+                <div class="media-body">
+                    <h4 class="media-heading">Credentials</h4>
+                    <div id="tool_credentials" class="collapse">
+                        <ul>
+                            {% for cred in tool_credentials %}
+                                <li>
+                                    <i class="glyphicon glyphicon-user"></i> {{ cred.username|default_if_none:"" }}
+                                    {% if cred.password %}<i class="glyphicon glyphicon-lock"></i> {{ cred.password }}{% endif %}
+                                    {% if cred.comments %}({{ cred.comments }}){% endif %}
+                                </li>
+                            {% endfor %}
+                        </ul>
+                    </div>
+                </div>
+            </div>
+        {% endif %}
         {% if user.is_staff %}
             <div id="qualified_users_container" class="media">{% include 'tool_control/qualified_users.html' %}</div>
         {% elif user in tool.superusers.all or "tool"|customization:"tool_control_show_qualified_users_to_all" == "enabled" %}
diff --git a/NEMO/templates/tool_credentials/tool_credentials.html b/NEMO/templates/tool_credentials/tool_credentials.html
new file mode 100644
index 00000000..d735a379
--- /dev/null
+++ b/NEMO/templates/tool_credentials/tool_credentials.html
@@ -0,0 +1,72 @@
+{% extends "base.html" %}
+{% load custom_tags_and_filters %}
+{% block title %}Tool credentials{% endblock %}
+{% block content %}
+    <h1 style="margin-top: 0;margin-bottom: 25px">Tool credentials</h1>
+    <div class="row">
+        <div class="col-xs-9 col-sm-6" style="margin-bottom: 25px">
+            <input id="search"
+                   type="text"
+                   aria-label="Credential search"
+                   placeholder="Search for a tool credential"
+                   class="form-control"
+                   autofocus>
+        </div>
+        <div class="text-right col-xs-3 col-sm-6" style="margin-bottom: 25px">
+            {% if user.is_facility_manager or user.is_superuser %}
+                {% url "export_tool_credentials" as export_tool_credentials_url %}
+                {% button type="export" value="Export" url=export_tool_credentials_url target="_blank" %}
+            {% endif %}
+            {% admin_add_url 'NEMO' 'toolcredentials' request.path as add_tool_credentials_url %}
+            {% if add_tool_credentials_url %}
+                {% button type="add" value="Add new credentials" url=add_tool_credentials_url %}
+            {% endif %}
+        </div>
+    </div>
+    {% if tool_credentials %}
+        {% regroup tool_credentials by tool.category as tool_credentials_by_category %}
+        <table id="table-to-filter" class="table table-bordered table-hover table-align-middle table-condensed">
+            {% for category in tool_credentials_by_category %}
+                <thead>
+                    <tr class="info">
+                        <th class="text-center" colspan="{% if perms.NEMO.change_toolcredentials %}5{% else %}4{% endif %}">
+                            {{ category.grouper }}
+                        </th>
+                    </tr>
+                    <tr class="warning">
+                        <th>Tool</th>
+                        <th>Username</th>
+                        <th>Password</th>
+                        <th>Comments</th>
+                        {% if perms.NEMO.change_toolcredentials %}<th class="button-column-minimum"></th>{% endif %}
+                    </tr>
+                </thead>
+                <tbody>
+                    {% for tool_credential in category.list %}
+                        <tr>
+                            <td>{{ tool_credential.tool }}</td>
+                            <td>{{ tool_credential.username|default_if_none:"" }}</td>
+                            <td>{{ tool_credential.password|default_if_none:"" }}</td>
+                            <td>{{ tool_credential.comments|default_if_none:"" }}</td>
+                            {% if perms.NEMO.change_toolcredentials %}
+                                <td>
+                                    {% admin_edit_url tool_credential request.path as change_tool_credentials_url %}
+                                    {% if change_tool_credentials_url %}
+                                        {% button size="small" type="edit" value="Edit" url=change_tool_credentials_url %}
+                                    {% endif %}
+                                </td>
+                            {% endif %}
+                        </tr>
+                    {% endfor %}
+                </tbody>
+            {% endfor %}
+        </table>
+    {% else %}
+        <div class="pull-left form-control-static">
+            <i>
+                There are no tool credentials.
+            </i>
+        </div>
+    {% endif %}
+    <script>$("#search").keyup(table_search("table-to-filter", [1]));</script>
+{% endblock %}
diff --git a/NEMO/templates/usage/usage.html b/NEMO/templates/usage/usage.html
index aea39f2e..c34fe622 100644
--- a/NEMO/templates/usage/usage.html
+++ b/NEMO/templates/usage/usage.html
@@ -24,178 +24,152 @@
                 {% endif %}
             </p>
             {% if no_charges %}<h3>There was no usage between {{ start_date|date }} and {{ end_date|date }}.</h3>{% endif %}
-            {% with adj_reservation="requests"|customization:"adjustment_requests_missed_reservation_enabled" adj_tool_usage="requests"|customization:"adjustment_requests_tool_usage_enabled" adj_area_access="requests"|customization:"adjustment_requests_area_access_enabled" adj_staff_charge="requests"|customization:"adjustment_requests_staff_staff_charges_enabled" %}
-                {% if missed_reservations %}
-                    <h3>Missed reservations</h3>
-                    {% for m in missed_reservations %}
-                        <div class="usage-charge alert alert-danger">
-                            <span style="font-weight:bold">{{ m.reservation_item }}</span>
-                            <br>
-                            {% if project_autocomplete or selected_project %}
-                                Missed by {{ m.user }}
-                                <br>
-                            {% endif %}
-                            {{ m.start }}
-                            <br>
-                            Charged to project {{ m.project }}
-                            {% if adj_reservation == "enabled" %}
-                                {% if not adjustment_time_limit or adjustment_time_limit <= m.end %}
-                                    {% if m.user == user or m.project in pi_projects %}
-                                        {% include "usage/adjustment_request_button.html" with charge=m %}
-                                    {% endif %}
-                                {% endif %}
-                            {% endif %}
-                        </div>
-                    {% endfor %}
-                {% endif %}
-                {% if consumables %}
-                    <h3>Supplies and consumables</h3>
-                    {% for c in consumables %}
-                        <div class="usage-charge alert alert-info">
-                            <span style="font-weight:bold">{{ c.consumable }}</span>
-                            <br>
+            {% if missed_reservations %}
+                <h3>Missed reservations</h3>
+                {% for m in missed_reservations %}
+                    <div class="usage-charge alert alert-danger">
+                        <span style="font-weight:bold">{{ m.reservation_item }}</span>
+                        <br>
+                        {% if project_autocomplete or selected_project %}
+                            Missed by {{ m.user }}
+                            <br>
+                        {% endif %}
+                        {{ m.start }}
+                        <br>
+                        Charged to project {{ m.project }}
+                        {% if m|can_be_adjusted:user %}
+                            {% include "usage/adjustment_request_button.html" with charge=m %}
+                        {% endif %}
+                    </div>
+                {% endfor %}
+            {% endif %}
+            {% if consumables %}
+                <h3>Supplies and consumables</h3>
+                {% for c in consumables %}
+                    <div class="usage-charge alert alert-info">
+                        <span style="font-weight:bold">{{ c.consumable }}</span>
+                        <br>
+                        {% if project_autocomplete or selected_project %}
+                            <span style="font-weight:bold">For {{ c.customer }}</span>
+                            <br>
+                        {% endif %}
+                        Quantity {{ c.quantity }}
+                        <br>
+                        Purchased from {{ c.merchant }} on {{ c.date }}
+                        <br>
+                        Charged to project {{ c.project }}
+                        {% if c|can_be_adjusted:user %}
+                            {% include "usage/adjustment_request_button.html" with charge=c %}
+                        {% endif %}
+                    </div>
+                {% endfor %}
+            {% endif %}
+            {% if staff_charges %}
+                <h3>Staff charges</h3>
+                {% for s in staff_charges %}
+                    <div class="usage-charge alert alert-info">
+                        <span style="font-weight:bold">Work performed by {{ s.staff_member }}
                             {% if project_autocomplete or selected_project %}
-                                <span style="font-weight:bold">For {{ c.customer }}</span>
-                                <br>
+                                on behalf of {{ s.customer }}
+                            {% else %}
+                                on your behalf
                             {% endif %}
-                            Quantity {{ c.quantity }}
-                            <br>
-                            Purchased from {{ c.merchant }} on {{ c.date }}
-                            <br>
-                            Charged to project {{ c.project }}
-                        </div>
-                    {% endfor %}
-                {% endif %}
-                {% if staff_charges %}
-                    <h3>Staff charges</h3>
-                    {% for s in staff_charges %}
-                        <div class="usage-charge alert alert-info">
-                            <span style="font-weight:bold">Work performed by {{ s.staff_member }}
+                        </span>
+                        <br>
+                        {% if s.note %}
+                            Charge note: {{ s.note|linebreaksbr }}
+                            <br>
+                        {% endif %}
+                        {{ s.start }}
+                        <br>
+                        {{ s.end }}
+                        <br>
+                        Charged to project {{ s.project }}
+                        {% if s|can_be_adjusted:user %}
+                            {% include "usage/adjustment_request_button.html" with charge=s %}
+                        {% endif %}
+                    </div>
+                {% endfor %}
+            {% endif %}
+            {% if training_sessions %}
+                <h3>Training sessions</h3>
+                {% for t in training_sessions %}
+                    <div class="usage-charge alert alert-info">
+                        <span style="font-weight:bold">{{ t.tool }}</span> {{ t.get_type_display }} training
+                        {% if project_autocomplete or selected_project %}for {{ t.trainee }}{% endif %}
+                        <br>
+                        Trained by <span style="font-weight:bold">{{ t.trainer }}</span>
+                        <br>
+                        {{ t.date }} ({{ t.duration }} minutes)
+                        <br>
+                        Charged to project {{ t.project }}
+                    </div>
+                {% endfor %}
+            {% endif %}
+            {% if area_access %}
+                <h3>Area access</h3>
+                {% for a in area_access %}
+                    <div class="usage-charge alert {% if not a.staff_charge %}alert-info{% else %}alert-warning{% endif %}">
+                        <span style="font-weight:bold">{{ a.area }}</span>
+                        <br>
+                        {% if a.staff_charge %}
+                            <span style="font-weight:bold">Area accessed by {{ a.staff_charge.staff_member }}
                                 {% if project_autocomplete or selected_project %}
-                                    on behalf of {{ s.customer }}
+                                    on behalf of {{ a.customer }}
                                 {% else %}
                                     on your behalf
                                 {% endif %}
                             </span>
                             <br>
-                            {% if s.note %}
-                                Charge note: {{ s.note|linebreaksbr }}
+                        {% else %}
+                            {% if project_autocomplete or selected_project %}
+                                <span style="font-weight:bold">Area accessed by {{ a.customer }}</span>
                                 <br>
                             {% endif %}
-                            {{ s.start }}
-                            <br>
-                            {{ s.end }}
-                            <br>
-                            Charged to project {{ s.project }}
-                            {% if adj_staff_charge == "enabled" and s.staff_member == user and user.is_staff %}
-                                {% if not adjustment_time_limit or adjustment_time_limit <= s.end %}
-                                    {% include "usage/adjustment_request_button.html" with charge=s %}
-                                {% endif %}
-                            {% endif %}
-                        </div>
-                    {% endfor %}
-                {% endif %}
-                {% if training_sessions %}
-                    <h3>Training sessions</h3>
-                    {% for t in training_sessions %}
-                        <div class="usage-charge alert alert-info">
-                            <span style="font-weight:bold">{{ t.tool }}</span> {{ t.get_type_display }} training
-                            {% if project_autocomplete or selected_project %}for {{ t.trainee }}{% endif %}
-                            <br>
-                            Trained by <span style="font-weight:bold">{{ t.trainer }}</span>
-                            <br>
-                            {{ t.date }} ({{ t.duration }} minutes)
-                            <br>
-                            Charged to project {{ t.project }}
-                        </div>
-                    {% endfor %}
-                {% endif %}
-                {% if area_access %}
-                    <h3>Area access</h3>
-                    {% for a in area_access %}
-                        <div class="usage-charge alert {% if not a.staff_charge %}alert-info{% else %}alert-warning{% endif %}">
-                            <span style="font-weight:bold">{{ a.area }}</span>
-                            <br>
-                            {% if a.staff_charge %}
-                                <span style="font-weight:bold">Area accessed by {{ a.staff_charge.staff_member }}
-                                    {% if project_autocomplete or selected_project %}
-                                        on behalf of {{ a.customer }}
-                                    {% else %}
-                                        on your behalf
-                                    {% endif %}
-                                </span>
-                                <br>
-                            {% else %}
+                        {% endif %}
+                        {{ a.start }}
+                        <br>
+                        {{ a.end|default_if_none:"In progress" }}
+                        <br>
+                        Charged to project {{ a.project }}
+                        {% if a|can_be_adjusted:user %}
+                            {% include "usage/adjustment_request_button.html" with charge=a %}
+                        {% endif %}
+                    </div>
+                {% endfor %}
+            {% endif %}
+            {% if usage_events %}
+                <h3>Tool usage</h3>
+                {% for u in usage_events %}
+                    <div class="usage-charge alert {% if u.user == u.operator %}alert-info{% else %}alert-warning{% endif %}">
+                        <span style="font-weight:bold">{{ u.tool }}</span>
+                        <br>
+                        {% if u.user != u.operator %}
+                            <span style="font-weight:bold">Operated by {{ u.operator }}
                                 {% if project_autocomplete or selected_project %}
-                                    <span style="font-weight:bold">Area accessed by {{ a.customer }}</span>
-                                    <br>
-                                {% endif %}
-                            {% endif %}
-                            {{ a.start }}
-                            <br>
-                            {{ a.end|default_if_none:"In progress" }}
-                            <br>
-                            Charged to project {{ a.project }}
-                            {% if not a.staff_charge and adj_area_access == "enabled" %}
-                                {% if not adjustment_time_limit or adjustment_time_limit <= a.end %}
-                                    {% if a.customer == user or a.project in pi_projects %}
-                                        {% include "usage/adjustment_request_button.html" with charge=a %}
-                                    {% endif %}
-                                {% endif %}
-                            {% elif a.staff_charge and adj_staff_charge == "enabled" %}
-                                {% if not adjustment_time_limit or adjustment_time_limit <= a.end %}
-                                    {% if a.staff_charge.staff_member == user and user.is_staff %}
-                                        {% include "usage/adjustment_request_button.html" with charge=a %}
-                                    {% endif %}
+                                    on behalf of {{ u.user }}
+                                {% else %}
+                                    on your behalf
                                 {% endif %}
-                            {% endif %}
-                        </div>
-                    {% endfor %}
-                {% endif %}
-                {% if usage_events %}
-                    <h3>Tool usage</h3>
-                    {% for u in usage_events %}
-                        <div class="usage-charge alert {% if u.user == u.operator %}alert-info{% else %}alert-warning{% endif %}">
-                            <span style="font-weight:bold">{{ u.tool }}</span>
+                            </span>
                             <br>
-                            {% if u.user != u.operator %}
-                                <span style="font-weight:bold">Operated by {{ u.operator }}
-                                    {% if project_autocomplete or selected_project %}
-                                        on behalf of {{ u.user }}
-                                    {% else %}
-                                        on your behalf
-                                    {% endif %}
-                                </span>
+                        {% else %}
+                            {% if project_autocomplete or selected_project %}
+                                <span style="font-weight:bold">Operated by {{ u.user }}</span>
                                 <br>
-                            {% else %}
-                                {% if project_autocomplete or selected_project %}
-                                    <span style="font-weight:bold">Operated by {{ u.user }}</span>
-                                    <br>
-                                {% endif %}
                             {% endif %}
-                            {{ u.start }}
-                            <br>
-                            {{ u.end }}
-                            <br>
-                            Charged to project {{ u.project }}
-                            {% if adj_tool_usage == "enabled" %}
-                                {% if not adjustment_time_limit or adjustment_time_limit <= u.end %}
-                                    {% if u.user == u.operator and u.user == user or u.project in pi_projects %}
-                                        {% include "usage/adjustment_request_button.html" with charge=u %}
-                                    {% endif %}
-                                {% endif %}
-                            {% endif %}
-                            {% if adj_staff_charge == "enabled" %}
-                                {% if not adjustment_time_limit or adjustment_time_limit <= u.end %}
-                                    {% if u.operator == user and user.is_staff and u.remote_work %}
-                                        {% include "usage/adjustment_request_button.html" with charge=u %}
-                                    {% endif %}
-                                {% endif %}
-                            {% endif %}
-                        </div>
-                    {% endfor %}
-                {% endif %}
-            {% endwith %}
+                        {% endif %}
+                        {{ u.start }}
+                        <br>
+                        {{ u.end }}
+                        <br>
+                        Charged to project {{ u.project }}
+                        {% if u|can_be_adjusted:user %}
+                            {% include "usage/adjustment_request_button.html" with charge=u %}
+                        {% endif %}
+                    </div>
+                {% endfor %}
+            {% endif %}
         </div>
     </div>
 {% endblock %}
diff --git a/NEMO/templates/usage/usage_base.html b/NEMO/templates/usage/usage_base.html
index 5927700f..443905d8 100644
--- a/NEMO/templates/usage/usage_base.html
+++ b/NEMO/templates/usage/usage_base.html
@@ -150,12 +150,15 @@ <h1>
 
 			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 @@ <h1>
 					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 @@ <h1>
                 <div class="hidden-lg hidden-md hidden-sm">&nbsp;</div>
                 <div class="col-md-3">
                     {% button type="save" submit=False onclick="$('#csv_export').val('');this.form.submit();" value="Update" icon="glyphicon-refresh" %}
-                    {% if can_export %}
-                        <input id="csv_export" type="hidden" name="csv" value="">
-                        {% button style="margin-left: 15px" type="export" onclick="$('#csv_export').val('true');this.form.submit();" value="Export" %}
-                    {% endif %}
+                    <input id="csv_export" type="hidden" name="csv" value="">
+                    {% button style="margin-left: 15px" type="export" onclick="$('#csv_export').val('true');this.form.submit();" value="Export" %}
                 </div>
             </div>
             <script>
diff --git a/NEMO/templates/users/create_or_modify_user.html b/NEMO/templates/users/create_or_modify_user.html
index 06682554..c5b5189d 100644
--- a/NEMO/templates/users/create_or_modify_user.html
+++ b/NEMO/templates/users/create_or_modify_user.html
@@ -135,6 +135,9 @@
                         {% endfor %}
                     </select>
                 </div>
+                {% if form.type.errors %}
+                    <div class="col-sm-6 form-control-static danger-highlight">{{ form.type.errors|striptags }}</div>
+                {% endif %}
             </div>
         {% endif %}
         <div class="form-group">
@@ -214,7 +217,7 @@
                 <div class="col-sm-4 form-control-static">{{ form.instance.date_joined }}</div>
             </div>
             <div class="form-group">
-                <div class="control-label col-sm-2">Last login</div>
+                <div class="control-label col-sm-2">Last website login</div>
                 <div class="col-sm-4 form-control-static">{{ form.instance.last_login|default_if_none:"Never" }}</div>
             </div>
         {% endif %}
@@ -316,15 +319,27 @@
                                                 {% for level in area_access_dict|get_item:node.id %}
                                                     <div class="checkbox"
                                                          {% if not node.is_leaf_node %}style="display: inline-block;margin-left: -3px"{% endif %}>
-                                                        <label>
-                                                            <input name="physical_access_levels"
-                                                                   type="checkbox"
-                                                                   {% if form.instance.is_staff and level.allow_staff_access or form.instance.is_user_office and level.allow_staff_access %}disabled title="This access is automatically granted to staff users"{% endif %}
-                                                                   value="{{ level.id }}"
-                                                                   {% if level in checked_access_levels %}checked{% endif %}>
-                                                            {% if node.category %}({{ node.category }}){% endif %}
-                                                            {{ level }}
-                                                        </label>
+                                                        {% with last_user_access=last_access|get_item:level.area.id %}
+                                                            <label>
+                                                                <input name="physical_access_levels"
+                                                                       type="checkbox"
+                                                                       {% if form.instance.is_staff and level.allow_staff_access or form.instance.is_user_office and level.allow_staff_access %}disabled title="This access is automatically granted to staff users"{% endif %}
+                                                                       value="{{ level.id }}"
+                                                                       {% if level in checked_access_levels %}checked{% endif %}>
+                                                                {% if node.category %}({{ node.category }}){% endif %}
+                                                                {{ level }} (Last access:
+                                                                {% if last_user_access %}
+                                                                    {{ last_user_access|date:"SHORT_DATETIME_FORMAT" }}
+                                                                {% else %}
+                                                                    {% if level in checked_access_levels %}
+                                                                        <span class="danger-highlight">Never</span>
+                                                                    {% else %}
+                                                                        Never
+                                                                    {% endif %}
+                                                                {% endif %}
+                                                                )
+                                                            </label>
+                                                        {% endwith %}
                                                     </div>
                                                 {% endfor %}
                                                 {% if not node.is_leaf_node %}
diff --git a/NEMO/templates/users/preferences.html b/NEMO/templates/users/preferences.html
index 5f6d554e..3e4146f3 100644
--- a/NEMO/templates/users/preferences.html
+++ b/NEMO/templates/users/preferences.html
@@ -155,7 +155,7 @@ <h3 id="adjustment_requests_label">Adjustment Requests</h3>
                 </div>
             </div>
         </div>
-        {% 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 %}
             <div class="panel panel-default">
                 <div class="panel-body">
                     <h3 id="adjustment_requests_label">Tool maintenance & notifications</h3>
@@ -342,10 +342,10 @@ <h3 id="email_notifications_label">Email notifications</h3>
                         {% 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/templates/users/view_user.html b/NEMO/templates/users/view_user.html
new file mode 100644
index 00000000..a72a32a8
--- /dev/null
+++ b/NEMO/templates/users/view_user.html
@@ -0,0 +1,82 @@
+{% extends 'base.html' %}
+{% load custom_tags_and_filters %}
+{% load mptt_tags %}
+{% block title %}View user{% endblock %}
+{% block content %}
+    <div class="panel panel-default">
+        <div class="panel-body">
+            <h3 style="margin-top: 0">Basic Information</h3>
+            <ul>
+                <li>
+                    <b>First name</b>: {{ user.first_name }}
+                </li>
+                <li>
+                    <b>Last name</b>: {{ user.last_name }}
+                </li>
+                <li>
+                    <b>Username</b>: {{ user.username }}
+                </li>
+                <li>
+                    <b>Email</b>: {{ user.email }}
+                </li>
+                {% if user.is_any_part_of_staff and user.type %}
+                    <li>
+                        <b>Type</b>: {{ user.type }}
+                    </li>
+                {% endif %}
+                <li>
+                    <b>Date joined</b>: {{ user.date_joined }}
+                </li>
+                <li>
+                    <b>Last login</b>: {{ user.last_login }}
+                </li>
+            </ul>
+        </div>
+    </div>
+    <div class="panel panel-default">
+        <div class="panel-body">
+            <h3 style="margin-top: 0">Projects</h3>
+            {% if user.projects %}
+                <ul>
+                    {% for project in user.projects.all %}
+                        <li>{% project_selection_display project %}</li>
+                    {% endfor %}
+                </ul>
+            {% else %}
+                <p>You do not have any projects.</p>
+            {% endif %}
+        </div>
+    </div>
+    <div class="panel panel-default">
+        <div class="panel-body">
+            <h3 style="margin-top: 0">Tool Qualifications</h3>
+            {% if user.qualifications.exists %}
+                <ul>
+                    {% for tool_qualification in user.qualifications.all %}<li>{{ tool_qualification }}</li>{% endfor %}
+                </ul>
+            {% else %}
+                <p>You are not qualified for any tools.</p>
+            {% endif %}
+        </div>
+    </div>
+    {% if user.accessible_access_levels.exists %}
+        <div class="panel panel-default">
+            <div class="panel-body">
+                <h3 style="margin-top: 0">Physical access levels</h3>
+                <ul>
+                    {% for physical_access in user.accessible_access_levels.all %}<li>{{ physical_access }}</li>{% endfor %}
+                </ul>
+            </div>
+        </div>
+    {% endif %}
+    {% if user.is_any_part_of_staff and user.groups.exists %}
+        <div class="panel panel-default">
+            <div class="panel-body">
+                <h3 style="margin-top: 0">Groups</h3>
+                <ul>
+                    {% for group in user.groups.all %}<li>{{ group }}</li>{% endfor %}
+                </ul>
+            </div>
+        </div>
+    {% endif %}
+{% endblock %}
diff --git a/NEMO/templatetags/custom_tags_and_filters.py b/NEMO/templatetags/custom_tags_and_filters.py
index 82bdbbe3..73b4803b 100644
--- a/NEMO/templatetags/custom_tags_and_filters.py
+++ b/NEMO/templatetags/custom_tags_and_filters.py
@@ -284,3 +284,11 @@ def button(
 def absolute_url(context, view_name, *args, **kwargs):
     url = reverse(view_name, args=args, kwargs=kwargs)
     return get_full_url(url, request=context["request"])
+
+
+@register.filter(name="can_be_adjusted")
+def can_be_adjusted(value, arg) -> bool:
+    if not issubclass(type(value), BillableItemMixin):
+        return False
+    else:
+        return value.can_be_adjusted(arg)
diff --git a/NEMO/urls.py b/NEMO/urls.py
index 362fdb16..e2f08ab4 100644
--- a/NEMO/urls.py
+++ b/NEMO/urls.py
@@ -55,6 +55,7 @@
     tasks,
     timed_services,
     tool_control,
+    tool_credentials,
     training,
     tutorials,
     usage,
@@ -74,6 +75,7 @@ def sort_urls(url_path):
 router = routers.DefaultRouter()
 router.register(r"account_types", api.AccountTypeViewSet)
 router.register(r"accounts", api.AccountViewSet)
+router.register(r"adjustment_requests", api.AdjustmentRequestViewSet)
 router.register(r"alert_categories", api.AlertCategoryViewSet)
 router.register(r"alerts", api.AlertViewSet)
 router.register(r"area_access_records", api.AreaAccessRecordViewSet)
@@ -81,6 +83,7 @@ def sort_urls(url_path):
 router.register(r"auth_groups", api.GroupViewSet)
 router.register(r"auth_permissions", api.PermissionViewSet)
 router.register(r"billing", api.BillingViewSet, basename="billing")
+router.register(r"buddy_requests", api.BuddyRequestViewSet)
 router.register(r"configurations", api.ConfigurationViewSet)
 router.register(r"consumable_categories", api.ConsumableCategoryViewSet)
 router.register(r"consumable_withdrawals", api.ConsumableWithdrawViewSet)
@@ -89,9 +92,12 @@ def sort_urls(url_path):
 router.register(r"interlock_card_categories", api.InterlockCardCategoryViewSet)
 router.register(r"interlock_cards", api.InterlockCardViewSet)
 router.register(r"interlocks", api.InterlockViewSet)
+router.register(r"physical_access_levels", api.PhysicalAccessLevelViewSet)
+router.register(r"temporary_physical_access_requests", api.TemporaryPhysicalAccessRequestViewSet)
 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)
@@ -99,6 +105,7 @@ def sort_urls(url_path):
 router.register(r"staff_charges", api.StaffChargeViewSet)
 router.register(r"tasks", api.TaskViewSet)
 router.register(r"tools", api.ToolViewSet)
+router.register(r"tool_credentials", api.ToolCredentialsViewSet)
 router.register(r"tool_status", api.ToolStatusViewSet, basename="tool_status")
 router.register(r"training_sessions", api.TrainingSessionViewSet)
 router.register(r"usage_events", api.UsageEventViewSet)
@@ -149,6 +156,8 @@ def sort_urls(url_path):
         name="get_projects_for_tool_control",
     ),
     path("get_projects_for_self/", get_projects.get_projects_for_self, name="get_projects_for_self"),
+    # User Profile:
+    path("user/view_user/<int:user_id>/", users.view_user, name="view_user"),
     # Tool control:
     # This tool_control URL is needed to be able to reverse when choosing items on mobile using next_page.
     # (see choose_item.html for details)
@@ -673,6 +682,9 @@ def sort_urls(url_path):
         path(
             "delete_staff_absence/<int:absence_id>/", status_dashboard.delete_staff_absence, name="delete_staff_absence"
         ),
+        # Tool credentials
+        path("tool_credentials/", tool_credentials.tool_credentials_list, name="tool_credentials"),
+        path("tool_credentials/export/", tool_credentials.export_tool_credentials, name="export_tool_credentials"),
         # Billing:
         path("billing/", usage.billing, name="billing"),
     ]
diff --git a/NEMO/utilities.py b/NEMO/utilities.py
index 9b57dba4..09a23124 100644
--- a/NEMO/utilities.py
+++ b/NEMO/utilities.py
@@ -20,6 +20,7 @@
 from django.conf import global_settings, settings
 from django.contrib.admin import ModelAdmin
 from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist
 from django.core.files.base import ContentFile
 from django.core.files.uploadedfile import InMemoryUploadedFile
 from django.core.mail import EmailMessage
@@ -33,7 +34,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__)
 
@@ -392,9 +393,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():
@@ -690,6 +691,14 @@ def get_model_name(content_type: ContentType):
         return ""
 
 
+def get_model_instance(content_type: ContentType, object_id: int):
+    try:
+        model = apps.get_model(content_type.app_label, content_type.model)
+        return model.objects.get(pk=object_id)
+    except (ObjectDoesNotExist, LookupError, AttributeError):
+        return None
+
+
 def get_email_from_settings() -> str:
     """
     Return the default from email if it has been overriden, otherwise the server email
diff --git a/NEMO/views/accounts_and_projects.py b/NEMO/views/accounts_and_projects.py
index 392a77e1..1e3c1a33 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/adjustment_requests.py b/NEMO/views/adjustment_requests.py
index acf50ba5..97a05b0e 100644
--- a/NEMO/views/adjustment_requests.py
+++ b/NEMO/views/adjustment_requests.py
@@ -3,6 +3,7 @@
 
 from django.contrib.auth.decorators import login_required
 from django.contrib.contenttypes.models import ContentType
+from django.db.models import F, Q
 from django.http import HttpResponse, HttpResponseBadRequest
 from django.shortcuts import get_object_or_404, redirect, render
 from django.template.defaultfilters import linebreaksbr
@@ -17,6 +18,7 @@
     AdjustmentRequest,
     Area,
     AreaAccessRecord,
+    ConsumableWithdraw,
     Notification,
     RequestMessage,
     RequestStatus,
@@ -102,19 +104,24 @@ def create_adjustment_request(request, request_id=None, item_type_id=None, item_
     try:
         item_type = ContentType.objects.get_for_id(item_type_id)
         adjustment_request.item = item_type.get_object_for_this_type(pk=item_id)
-    # Show times if not missed reservation or if missed reservation but customization is set to show times anyway
     except ContentType.DoesNotExist:
         pass
 
+    # Show times if not missed reservation or if missed reservation but customization is set to show times anyway
     change_times_allowed = can_change_times(adjustment_request.item)
+    # Show quantity field if we have a consumable
+    change_quantity_allowed = isinstance(adjustment_request.item, ConsumableWithdraw)
 
     # only change the times if we are provided with a charge and it's allowed
     if item_type_id and adjustment_request.item and change_times_allowed:
         adjustment_request.new_start = adjustment_request.item.start
         adjustment_request.new_end = adjustment_request.item.end
+    if item_type_id and adjustment_request.item and change_quantity_allowed:
+        adjustment_request.new_quantity = adjustment_request.item.quantity
 
     dictionary = {
         "change_times_allowed": change_times_allowed,
+        "change_quantity_allowed": change_quantity_allowed,
         "eligible_items": adjustment_eligible_items(user, adjustment_request.item),
     }
 
@@ -184,6 +191,9 @@ def create_adjustment_request(request, request_id=None, item_type_id=None, item_
             item_id = form.cleaned_data.get("item_id")
             if item_type and item_id:
                 dictionary["change_times_allowed"] = can_change_times(item_type.get_object_for_this_type(pk=item_id))
+                dictionary["change_quantity_allowed"] = isinstance(
+                    item_type.get_object_for_this_type(pk=item_id), ConsumableWithdraw
+                )
             dictionary["form"] = form
             return render(request, "requests/adjustment_requests/adjustment_request.html", dictionary)
     else:
@@ -366,7 +376,11 @@ def email_interested_parties(reply: RequestMessage, reply_url):
 
 def can_change_times(item):
     can_change_reservation_times = UserRequestsCustomization.get_bool("adjustment_requests_missed_reservation_times")
-    return item and (not isinstance(item, Reservation) or can_change_reservation_times)
+    return (
+        item
+        and not isinstance(item, ConsumableWithdraw)
+        and (not isinstance(item, Reservation) or can_change_reservation_times)
+    )
 
 
 def adjustment_eligible_items(user: User, current_item=None) -> List[BillableItemMixin]:
@@ -390,6 +404,27 @@ def adjustment_eligible_items(user: User, current_item=None) -> List[BillableIte
             .filter(**end_filter)
             .order_by("-end")[:item_number]
         )
+    if UserRequestsCustomization.get_bool("adjustment_requests_consumable_withdrawal_enabled"):
+        date_filter = {"date__gte": date_limit} if date_limit else {}
+        consumable_withdrawals = (
+            ConsumableWithdraw.objects.filter(customer=user).filter(**date_filter).order_by("-date")
+        )
+        self_checkout = UserRequestsCustomization.get_bool("adjustment_requests_consumable_withdrawal_self_checkout")
+        staff_checkout = UserRequestsCustomization.get_bool("adjustment_requests_consumable_withdrawal_staff_checkout")
+        usage_event = UserRequestsCustomization.get_bool("adjustment_requests_consumable_withdrawal_usage_event")
+        type_filter = Q()
+        if not self_checkout:
+            type_filter = type_filter & ~Q(
+                consumable__allow_self_checkout=True, merchant=F("customer"), usage_event__isnull=True
+            )
+        if not staff_checkout:
+            type_filter = type_filter & (
+                Q(usage_event__isnull=False) | Q(merchant__is_staff=False) | Q(merchant=F("customer"))
+            )
+        if not usage_event:
+            type_filter = type_filter & ~Q(usage_event__isnull=False)
+        consumable_withdrawals = consumable_withdrawals.filter(type_filter)
+        items.extend(consumable_withdrawals[:item_number])
     if user.is_staff and UserRequestsCustomization.get_bool("adjustment_requests_staff_staff_charges_enabled"):
         # Add all remote charges for staff to request for adjustment
         items.extend(
@@ -448,7 +483,8 @@ def adjustments_csv_export(request_list: List[AdjustmentRequest]) -> HttpRespons
                 "item": req.item.get_display() if req.item else "",
                 "new_start": req.new_start,
                 "new_end": req.new_end,
-                "difference": req.get_time_difference(),
+                "new_quantity": req.new_quantity,
+                "difference": req.get_time_difference() or req.get_quantity_difference(),
                 "reviewer": req.reviewer,
                 "applied": req.applied,
                 "applied_by": req.applied_by,
diff --git a/NEMO/views/api.py b/NEMO/views/api.py
index c969f2e2..91dac8f8 100644
--- a/NEMO/views/api.py
+++ b/NEMO/views/api.py
@@ -12,10 +12,12 @@
 from NEMO.models import (
     Account,
     AccountType,
+    AdjustmentRequest,
     Alert,
     AlertCategory,
     Area,
     AreaAccessRecord,
+    BuddyRequest,
     Configuration,
     ConfigurationOption,
     Consumable,
@@ -24,15 +26,19 @@
     Interlock,
     InterlockCard,
     InterlockCardCategory,
+    PhysicalAccessLevel,
     Project,
     ProjectDiscipline,
     Qualification,
+    RecurringConsumableCharge,
     Reservation,
     Resource,
     ScheduledOutage,
     StaffCharge,
     Task,
+    TemporaryPhysicalAccessRequest,
     Tool,
+    ToolCredentials,
     TrainingSession,
     UsageEvent,
     User,
@@ -41,11 +47,13 @@
 from NEMO.serializers import (
     AccountSerializer,
     AccountTypeSerializer,
+    AdjustmentRequestSerializer,
     AlertCategorySerializer,
     AlertSerializer,
     AreaAccessRecordSerializer,
     AreaSerializer,
     BillableItemSerializer,
+    BuddyRequestSerializer,
     ConfigurationOptionSerializer,
     ConfigurationSerializer,
     ConsumableCategorySerializer,
@@ -57,14 +65,18 @@
     InterlockCardSerializer,
     InterlockSerializer,
     PermissionSerializer,
+    PhysicalAccessLevelSerializer,
     ProjectDisciplineSerializer,
     ProjectSerializer,
     QualificationSerializer,
+    RecurringConsumableChargeSerializer,
     ReservationSerializer,
     ResourceSerializer,
     ScheduledOutageSerializer,
     StaffChargeSerializer,
     TaskSerializer,
+    TemporaryPhysicalAccessRequestSerializer,
+    ToolCredentialsSerializer,
     ToolSerializer,
     ToolStatusSerializer,
     TrainingSessionSerializer,
@@ -72,12 +84,21 @@
     UserSerializer,
 )
 from NEMO.typing import QuerySetType
-from NEMO.utilities import export_format_datetime
+from NEMO.utilities import export_format_datetime, remove_duplicates
 from NEMO.views.api_billing import (
     BillingFilterForm,
     get_billing_charges,
 )
 
+date_filters = ["exact", "in", "month", "year", "day", "gte", "gt", "lte", "lt", "isnull"]
+time_filters = ["exact", "in", "hour", "minute", "second", "gte", "gt", "lte", "lt", "isnull"]
+datetime_filters = remove_duplicates(date_filters + time_filters + ["week"])
+string_filters = ["exact", "iexact", "in", "contains", "icontains", "isempty"]
+number_filters = ["exact", "in", "gte", "gt", "lte", "lt", "isnull"]
+key_filters = ["exact", "in", "isnull"]
+manykey_filters = ["exact", "isnull"]
+boolean_filters = ["exact"]
+
 
 class SingleInstanceHTMLFormBrowsableAPIRenderer(BrowsableAPIRenderer):
     """
@@ -140,8 +161,8 @@ class AlertCategoryViewSet(viewsets.ModelViewSet):
     queryset = AlertCategory.objects.all()
     serializer_class = AlertCategorySerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "name": ["exact", "iexact", "contains", "icontains"],
+        "id": key_filters,
+        "name": string_filters,
     }
 
 
@@ -150,18 +171,18 @@ class AlertViewSet(ModelViewSet):
     queryset = Alert.objects.all()
     serializer_class = AlertSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "title": ["exact", "iexact", "contains", "icontains"],
-        "category": ["exact", "iexact"],
-        "contents": ["contains", "icontains"],
-        "creation_time": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "creator": ["exact", "in"],
-        "user": ["exact", "in"],
-        "debut_time": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
-        "expiration_time": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
-        "dismissible": ["exact"],
-        "expired": ["exact"],
-        "deleted": ["exact"],
+        "id": key_filters,
+        "title": string_filters,
+        "category": string_filters,
+        "contents": string_filters,
+        "creation_time": datetime_filters,
+        "creator": key_filters,
+        "user": key_filters,
+        "debut_time": datetime_filters,
+        "expiration_time": datetime_filters,
+        "dismissible": boolean_filters,
+        "expired": boolean_filters,
+        "deleted": boolean_filters,
     }
 
 
@@ -170,25 +191,25 @@ class UserViewSet(ModelViewSet):
     queryset = User.objects.all()
     serializer_class = UserSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "type": ["exact", "in"],
-        "domain": ["exact", "in", "isempty"],
-        "username": ["exact", "iexact", "in"],
-        "first_name": ["exact", "iexact", "icontains"],
-        "last_name": ["exact", "iexact", "icontains"],
-        "email": ["exact", "iexact", "icontains"],
-        "badge_number": ["exact", "iexact", "isempty"],
-        "is_active": ["exact"],
-        "is_staff": ["exact"],
-        "is_facility_manager": ["exact"],
-        "is_superuser": ["exact"],
-        "is_service_personnel": ["exact"],
-        "is_technician": ["exact"],
-        "training_required": ["exact"],
-        "date_joined": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "last_login": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
-        "access_expiration": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
-        "physical_access_levels": ["exact"],
+        "id": key_filters,
+        "type": key_filters,
+        "domain": string_filters,
+        "username": string_filters,
+        "first_name": string_filters,
+        "last_name": string_filters,
+        "email": string_filters,
+        "badge_number": string_filters,
+        "is_active": boolean_filters,
+        "is_staff": boolean_filters,
+        "is_facility_manager": boolean_filters,
+        "is_superuser": boolean_filters,
+        "is_service_personnel": boolean_filters,
+        "is_technician": boolean_filters,
+        "training_required": boolean_filters,
+        "date_joined": datetime_filters,
+        "last_login": datetime_filters,
+        "access_expiration": date_filters,
+        "physical_access_levels": manykey_filters,
     }
 
 
@@ -196,7 +217,7 @@ class ProjectDisciplineViewSet(ModelViewSet):
     filename = "project_disciplines"
     queryset = ProjectDiscipline.objects.all()
     serializer_class = ProjectDisciplineSerializer
-    filterset_fields = {"id": ["exact", "in"], "name": ["exact", "iexact"], "display_order": ["exact"]}
+    filterset_fields = {"id": key_filters, "name": string_filters, "display_order": number_filters}
 
 
 class ProjectViewSet(ModelViewSet):
@@ -204,12 +225,12 @@ class ProjectViewSet(ModelViewSet):
     queryset = Project.objects.all()
     serializer_class = ProjectSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "name": ["exact", "iexact"],
-        "application_identifier": ["exact", "iexact"],
-        "active": ["exact"],
-        "account_id": ["exact", "in"],
-        "account": ["exact", "in"],
+        "id": key_filters,
+        "name": string_filters,
+        "application_identifier": string_filters,
+        "active": boolean_filters,
+        "account_id": key_filters,
+        "account": key_filters,
     }
 
 
@@ -217,14 +238,14 @@ class AccountTypeViewSet(ModelViewSet):
     filename = "account_types"
     queryset = AccountType.objects.all()
     serializer_class = AccountTypeSerializer
-    filterset_fields = {"id": ["exact", "in"], "name": ["exact", "iexact"], "display_order": ["exact"]}
+    filterset_fields = {"id": key_filters, "name": string_filters, "display_order": number_filters}
 
 
 class AccountViewSet(ModelViewSet):
     filename = "accounts"
     queryset = Account.objects.all()
     serializer_class = AccountSerializer
-    filterset_fields = {"id": ["exact", "in"], "name": ["exact", "iexact"], "active": ["exact"]}
+    filterset_fields = {"id": key_filters, "name": string_filters, "active": boolean_filters}
 
 
 class ToolViewSet(ModelViewSet):
@@ -232,15 +253,15 @@ class ToolViewSet(ModelViewSet):
     queryset = Tool.objects.all()
     serializer_class = ToolSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "name": ["exact", "iexact"],
-        "visible": ["exact"],
-        "_category": ["exact", "iexact"],
-        "_operational": ["exact"],
-        "_location": ["exact", "iexact"],
-        "_requires_area_access": ["exact", "isnull"],
-        "_post_usage_questions": ["isempty"],
-        "_pre_usage_questions": ["isempty"],
+        "id": key_filters,
+        "name": string_filters,
+        "visible": boolean_filters,
+        "_category": string_filters,
+        "_operational": boolean_filters,
+        "_location": string_filters,
+        "_requires_area_access": key_filters,
+        "_post_usage_questions": string_filters,
+        "_pre_usage_questions": string_filters,
     }
 
 
@@ -249,10 +270,10 @@ class QualificationViewSet(ModelViewSet):
     queryset = Qualification.objects.all()
     serializer_class = QualificationSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "user": ["exact", "in"],
-        "tool": ["exact", "in"],
-        "qualified_on": ["exact", "month", "year", "day", "gte", "gt", "lte", "lt"],
+        "id": key_filters,
+        "user": key_filters,
+        "tool": key_filters,
+        "qualified_on": date_filters,
     }
 
 
@@ -261,15 +282,15 @@ class AreaViewSet(ModelViewSet):
     queryset = Area.objects.all()
     serializer_class = AreaSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "name": ["exact", "iexact"],
-        "parent_area": ["exact", "in"],
-        "category": ["exact", "iexact"],
-        "requires_reservation": ["exact"],
-        "buddy_system_allowed": ["exact"],
-        "maximum_capacity": ["exact", "gte", "gt", "lte", "lt", "isnull"],
-        "count_staff_in_occupancy": ["exact"],
-        "count_service_personnel_in_occupancy": ["exact"],
+        "id": key_filters,
+        "name": string_filters,
+        "parent_area": key_filters,
+        "category": string_filters,
+        "requires_reservation": boolean_filters,
+        "buddy_system_allowed": boolean_filters,
+        "maximum_capacity": number_filters,
+        "count_staff_in_occupancy": boolean_filters,
+        "count_service_personnel_in_occupancy": boolean_filters,
     }
 
 
@@ -278,12 +299,12 @@ class ResourceViewSet(ModelViewSet):
     queryset = Resource.objects.all()
     serializer_class = ResourceSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "name": ["exact", "iexact"],
-        "available": ["exact"],
-        "fully_dependent_tools": ["exact"],
-        "partially_dependent_tools": ["exact"],
-        "dependent_areas": ["exact"],
+        "id": key_filters,
+        "name": string_filters,
+        "available": boolean_filters,
+        "fully_dependent_tools": manykey_filters,
+        "partially_dependent_tools": manykey_filters,
+        "dependent_areas": manykey_filters,
     }
 
 
@@ -292,16 +313,16 @@ class ConfigurationViewSet(ModelViewSet):
     queryset = Configuration.objects.all()
     serializer_class = ConfigurationSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "name": ["exact", "iexact", "in"],
-        "tool_id": ["exact", "in", "isnull"],
-        "tool": ["exact", "in", "isnull"],
-        "advance_notice_limit": ["exact", "in", "gte", "gt", "lte", "lt"],
-        "display_order": ["exact", "in", "gte", "gt", "lte", "lt"],
-        "maintainers": ["exact", "isnull"],
-        "qualified_users_are_maintainers": ["exact"],
-        "exclude_from_configuration_agenda": ["exact"],
-        "enabled": ["exact"],
+        "id": key_filters,
+        "name": string_filters,
+        "tool_id": key_filters,
+        "tool": key_filters,
+        "advance_notice_limit": number_filters,
+        "display_order": number_filters,
+        "maintainers": manykey_filters,
+        "qualified_users_are_maintainers": boolean_filters,
+        "exclude_from_configuration_agenda": boolean_filters,
+        "enabled": boolean_filters,
     }
 
 
@@ -310,12 +331,37 @@ class ConfigurationOptionViewSet(ModelViewSet):
     queryset = ConfigurationOption.objects.all()
     serializer_class = ConfigurationOptionSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "name": ["exact", "iexact", "in"],
-        "reservation_id": ["exact", "in", "isnull"],
-        "reservation": ["exact", "in", "isnull"],
-        "configuration_id": ["exact", "in", "isnull"],
-        "configuration": ["exact", "in", "isnull"],
+        "id": key_filters,
+        "name": string_filters,
+        "reservation_id": key_filters,
+        "reservation": key_filters,
+        "configuration_id": key_filters,
+        "configuration": key_filters,
+    }
+
+
+class RecurringConsumableChargesViewSet(ModelViewSet):
+    filename = "recurring_consumable_charges"
+    queryset = RecurringConsumableCharge.objects.all()
+    serializer_class = RecurringConsumableChargeSerializer
+    filterset_fields = {
+        "id": key_filters,
+        "customer_id": key_filters,
+        "customer": key_filters,
+        "consumable_id": key_filters,
+        "consumable": key_filters,
+        "project_id": key_filters,
+        "project": key_filters,
+        "quantity": number_filters,
+        "last_charge": datetime_filters,
+        "rec_start": date_filters,
+        "rec_frequency": number_filters,
+        "rec_interval": number_filters,
+        "rec_until": date_filters,
+        "rec_count": number_filters,
+        "last_updated": datetime_filters,
+        "last_updated_by": key_filters,
+        "last_updated_by_id": key_filters,
     }
 
 
@@ -324,24 +370,24 @@ class ReservationViewSet(ModelViewSet):
     queryset = Reservation.objects.all()
     serializer_class = ReservationSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "start": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "end": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
-        "project_id": ["exact", "in"],
-        "project": ["exact", "in"],
-        "user_id": ["exact", "in"],
-        "user": ["exact", "in"],
-        "creator_id": ["exact", "in"],
-        "creator": ["exact", "in"],
-        "tool_id": ["exact", "in", "isnull"],
-        "tool": ["exact", "in", "isnull"],
-        "area_id": ["exact", "in", "isnull"],
-        "area": ["exact", "in", "isnull"],
-        "cancelled": ["exact"],
-        "missed": ["exact"],
-        "validated": ["exact"],
-        "validated_by": ["exact", "in", "isnull"],
-        "question_data": ["isnull", "isempty"],
+        "id": key_filters,
+        "start": datetime_filters,
+        "end": datetime_filters,
+        "project_id": key_filters,
+        "project": key_filters,
+        "user_id": key_filters,
+        "user": key_filters,
+        "creator_id": key_filters,
+        "creator": key_filters,
+        "tool_id": key_filters,
+        "tool": key_filters,
+        "area_id": key_filters,
+        "area": key_filters,
+        "cancelled": boolean_filters,
+        "missed": boolean_filters,
+        "validated": boolean_filters,
+        "validated_by": key_filters,
+        "question_data": string_filters,
     }
 
 
@@ -350,19 +396,19 @@ class UsageEventViewSet(ModelViewSet):
     queryset = UsageEvent.objects.all()
     serializer_class = UsageEventSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "start": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "end": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
-        "project_id": ["exact", "in"],
-        "project": ["exact", "in"],
-        "user_id": ["exact", "in"],
-        "user": ["exact", "in"],
-        "operator_id": ["exact", "in"],
-        "operator": ["exact", "in"],
-        "tool_id": ["exact", "in"],
-        "tool": ["exact", "in"],
-        "validated": ["exact"],
-        "validated_by": ["exact", "in", "isnull"],
+        "id": key_filters,
+        "start": datetime_filters,
+        "end": datetime_filters,
+        "project_id": key_filters,
+        "project": key_filters,
+        "user_id": key_filters,
+        "user": key_filters,
+        "operator_id": key_filters,
+        "operator": key_filters,
+        "tool_id": key_filters,
+        "tool": key_filters,
+        "validated": boolean_filters,
+        "validated_by": key_filters,
     }
 
 
@@ -371,19 +417,19 @@ class AreaAccessRecordViewSet(ModelViewSet):
     queryset = AreaAccessRecord.objects.all().order_by("-start")
     serializer_class = AreaAccessRecordSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "start": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "end": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
-        "project_id": ["exact", "in"],
-        "project": ["exact", "in"],
-        "customer_id": ["exact", "in"],
-        "customer": ["exact", "in"],
-        "area_id": ["exact", "in"],
-        "area": ["exact", "in"],
-        "staff_charge_id": ["exact", "isnull", "in"],
-        "staff_charge": ["exact", "isnull", "in"],
-        "validated": ["exact"],
-        "validated_by": ["exact", "in", "isnull"],
+        "id": key_filters,
+        "start": datetime_filters,
+        "end": datetime_filters,
+        "project_id": key_filters,
+        "project": key_filters,
+        "customer_id": key_filters,
+        "customer": key_filters,
+        "area_id": key_filters,
+        "area": key_filters,
+        "staff_charge_id": key_filters,
+        "staff_charge": key_filters,
+        "validated": boolean_filters,
+        "validated_by": key_filters,
     }
 
 
@@ -392,23 +438,23 @@ class TaskViewSet(ModelViewSet):
     queryset = Task.objects.all()
     serializer_class = TaskSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "urgency": ["exact", "gte", "gt", "lte", "lt"],
-        "tool_id": ["exact", "in"],
-        "tool": ["exact", "in"],
-        "force_shutdown": ["exact"],
-        "safety_hazard": ["exact"],
-        "creator_id": ["exact", "in"],
-        "creator": ["exact", "in"],
-        "creation_time": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "estimated_resolution_time": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "problem_category": ["exact", "in"],
-        "cancelled": ["exact"],
-        "resolved": ["exact"],
-        "resolution_time": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "resolver_id": ["exact", "in"],
-        "resolver": ["exact", "in"],
-        "resolution_category": ["exact", "in"],
+        "id": key_filters,
+        "urgency": number_filters,
+        "tool_id": key_filters,
+        "tool": key_filters,
+        "force_shutdown": boolean_filters,
+        "safety_hazard": boolean_filters,
+        "creator_id": key_filters,
+        "creator": key_filters,
+        "creation_time": datetime_filters,
+        "estimated_resolution_time": datetime_filters,
+        "problem_category": key_filters,
+        "cancelled": boolean_filters,
+        "resolved": boolean_filters,
+        "resolution_time": datetime_filters,
+        "resolver_id": key_filters,
+        "resolver": key_filters,
+        "resolution_category": key_filters,
     }
 
 
@@ -417,18 +463,18 @@ class ScheduledOutageViewSet(ModelViewSet):
     queryset = ScheduledOutage.objects.all()
     serializer_class = ScheduledOutageSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "start": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "end": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
-        "creator_id": ["exact", "in"],
-        "creator": ["exact", "in"],
-        "category": ["exact", "in"],
-        "tool_id": ["exact", "in", "isnull"],
-        "tool": ["exact", "in", "isnull"],
-        "area_id": ["exact", "in", "isnull"],
-        "area": ["exact", "in", "isnull"],
-        "resource_id": ["exact", "in", "isnull"],
-        "resource": ["exact", "in", "isnull"],
+        "id": key_filters,
+        "start": datetime_filters,
+        "end": datetime_filters,
+        "creator_id": key_filters,
+        "creator": key_filters,
+        "category": string_filters,
+        "tool_id": key_filters,
+        "tool": key_filters,
+        "area_id": key_filters,
+        "area": key_filters,
+        "resource_id": key_filters,
+        "resource": key_filters,
     }
 
 
@@ -437,18 +483,18 @@ class StaffChargeViewSet(ModelViewSet):
     queryset = StaffCharge.objects.all()
     serializer_class = StaffChargeSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "staff_member_id": ["exact", "in"],
-        "staff_member": ["exact", "in"],
-        "customer_id": ["exact", "in"],
-        "customer": ["exact", "in"],
-        "project_id": ["exact", "in"],
-        "project": ["exact", "in"],
-        "start": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "end": ["month", "year", "day", "gte", "gt", "lte", "lt", "isnull"],
-        "validated": ["exact"],
-        "validated_by": ["exact", "in", "isnull"],
-        "note": ["contains"],
+        "id": key_filters,
+        "staff_member_id": key_filters,
+        "staff_member": key_filters,
+        "customer_id": key_filters,
+        "customer": key_filters,
+        "project_id": key_filters,
+        "project": key_filters,
+        "start": datetime_filters,
+        "end": datetime_filters,
+        "validated": boolean_filters,
+        "validated_by": key_filters,
+        "note": string_filters,
     }
 
 
@@ -457,21 +503,21 @@ class TrainingSessionViewSet(ModelViewSet):
     queryset = TrainingSession.objects.all()
     serializer_class = TrainingSessionSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "trainer_id": ["exact", "in"],
-        "trainer": ["exact", "in"],
-        "trainee_id": ["exact", "in"],
-        "trainee": ["exact", "in"],
-        "tool_id": ["exact", "in"],
-        "tool": ["exact", "in"],
-        "project_id": ["exact", "in"],
-        "project": ["exact", "in"],
-        "duration": ["exact", "gte", "lte", "gt", "lt"],
-        "type": ["exact", "in"],
-        "date": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "qualified": ["exact"],
-        "validated": ["exact"],
-        "validated_by": ["exact", "in", "isnull"],
+        "id": key_filters,
+        "trainer_id": key_filters,
+        "trainer": key_filters,
+        "trainee_id": key_filters,
+        "trainee": key_filters,
+        "tool_id": key_filters,
+        "tool": key_filters,
+        "project_id": key_filters,
+        "project": key_filters,
+        "duration": number_filters,
+        "type": number_filters,
+        "date": datetime_filters,
+        "qualified": boolean_filters,
+        "validated": boolean_filters,
+        "validated_by": key_filters,
     }
 
 
@@ -479,7 +525,7 @@ class ConsumableCategoryViewSet(ModelViewSet):
     filename = "consumable_categories"
     queryset = ConsumableCategory.objects.all()
     serializer_class = ConsumableCategorySerializer
-    filterset_fields = {"id": ["exact", "in"], "name": ["exact", "iexact"]}
+    filterset_fields = {"id": key_filters, "name": string_filters}
 
 
 class ConsumableViewSet(ModelViewSet):
@@ -487,14 +533,14 @@ class ConsumableViewSet(ModelViewSet):
     queryset = Consumable.objects.all()
     serializer_class = ConsumableSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "category_id": ["exact", "in"],
-        "category": ["exact", "in"],
-        "quantity": ["exact", "gte", "lte", "gt", "lt"],
-        "reminder_threshold": ["exact", "gte", "lte", "gt", "lt"],
-        "visible": ["exact"],
-        "reusable": ["exact"],
-        "reminder_threshold_reached": ["exact"],
+        "id": key_filters,
+        "category_id": key_filters,
+        "category": key_filters,
+        "quantity": number_filters,
+        "reminder_threshold": number_filters,
+        "visible": boolean_filters,
+        "reusable": boolean_filters,
+        "reminder_threshold_reached": boolean_filters,
     }
 
 
@@ -503,19 +549,19 @@ class ConsumableWithdrawViewSet(ModelViewSet):
     queryset = ConsumableWithdraw.objects.all()
     serializer_class = ConsumableWithdrawSerializer
     filterset_fields = {
-        "id": ["exact", "in"],
-        "customer_id": ["exact", "in"],
-        "customer": ["exact", "in"],
-        "merchant_id": ["exact", "in"],
-        "merchant": ["exact", "in"],
-        "consumable_id": ["exact", "in"],
-        "consumable": ["exact", "in"],
-        "project_id": ["exact", "in"],
-        "project": ["exact", "in"],
-        "quantity": ["exact", "gte", "lte", "gt", "lt"],
-        "date": ["month", "year", "day", "gte", "gt", "lte", "lt"],
-        "validated": ["exact"],
-        "validated_by": ["exact", "in", "isnull"],
+        "id": key_filters,
+        "customer_id": key_filters,
+        "customer": key_filters,
+        "merchant_id": key_filters,
+        "merchant": key_filters,
+        "consumable_id": key_filters,
+        "consumable": key_filters,
+        "project_id": key_filters,
+        "project": key_filters,
+        "quantity": number_filters,
+        "date": datetime_filters,
+        "validated": boolean_filters,
+        "validated_by": key_filters,
     }
 
 
@@ -524,8 +570,9 @@ class ContentTypeViewSet(XLSXFileMixin, viewsets.ReadOnlyModelViewSet):
     queryset = ContentType.objects.all()
     serializer_class = ContentTypeSerializer
     filterset_fields = {
-        "app_label": ["exact", "in"],
-        "model": ["exact", "in"],
+        "id": key_filters,
+        "app_label": string_filters,
+        "model": string_filters,
     }
 
     def get_filename(self, *args, **kwargs):
@@ -537,8 +584,9 @@ class InterlockCardCategoryViewSet(ModelViewSet):
     queryset = InterlockCardCategory.objects.all()
     serializer_class = InterlockCardCategorySerializer
     filterset_fields = {
-        "name": ["exact", "iexact", "in"],
-        "key": ["exact", "iexact", "in"],
+        "id": key_filters,
+        "name": string_filters,
+        "key": string_filters,
     }
 
 
@@ -547,13 +595,14 @@ class InterlockCardViewSet(ModelViewSet):
     queryset = InterlockCard.objects.all()
     serializer_class = InterlockCardSerializer
     filterset_fields = {
-        "name": ["exact", "iexact", "in"],
-        "server": ["exact", "iexact", "in"],
-        "number": ["exact", "in", "gte", "lte", "gt", "lt"],
-        "even_port": ["exact", "in", "gte", "lte", "gt", "lt"],
-        "odd_port": ["exact", "in", "gte", "lte", "gt", "lt"],
-        "category": ["exact", "in"],
-        "enabled": ["exact"],
+        "id": key_filters,
+        "name": string_filters,
+        "server": string_filters,
+        "number": number_filters,
+        "even_port": number_filters,
+        "odd_port": number_filters,
+        "category": key_filters,
+        "enabled": boolean_filters,
     }
 
 
@@ -562,11 +611,104 @@ class InterlockViewSet(ModelViewSet):
     queryset = Interlock.objects.all()
     serializer_class = InterlockSerializer
     filterset_fields = {
-        "card": ["exact", "in"],
-        "channel": ["exact", "in", "gte", "lte", "gt", "lt"],
-        "unit_id": ["exact", "in", "gte", "lte", "gt", "lt"],
-        "state": ["exact", "in", "gte", "lte", "gt", "lt"],
-        "most_recent_reply_time": ["exact", "in", "gte", "lte", "gt", "lt"],
+        "id": key_filters,
+        "card": key_filters,
+        "channel": number_filters,
+        "unit_id": number_filters,
+        "state": number_filters,
+        "most_recent_reply_time": datetime_filters,
+    }
+
+
+class PhysicalAccessLevelViewSet(ModelViewSet):
+    filename = "physical_access_levels"
+    queryset = PhysicalAccessLevel.objects.all()
+    serializer_class = PhysicalAccessLevelSerializer
+    filterset_fields = {
+        "id": key_filters,
+        "name": string_filters,
+        "area": key_filters,
+        "schedule": number_filters,
+        "weekdays_start_time": datetime_filters,
+        "weekdays_end_time": datetime_filters,
+        "allow_staff_access": boolean_filters,
+        "allow_user_request": boolean_filters,
+    }
+
+
+class BuddyRequestViewSet(ModelViewSet):
+    filename = "buddy_requests"
+    queryset = BuddyRequest.objects.all()
+    serializer_class = BuddyRequestSerializer
+    filterset_fields = {
+        "id": key_filters,
+        "creation_time": datetime_filters,
+        "start": date_filters,
+        "end": date_filters,
+        "description": string_filters,
+        "area": key_filters,
+        "user": key_filters,
+        "expired": boolean_filters,
+        "deleted": boolean_filters,
+    }
+
+
+class TemporaryPhysicalAccessRequestViewSet(ModelViewSet):
+    filename = "physical_access_requests"
+    queryset = TemporaryPhysicalAccessRequest.objects.all()
+    serializer_class = TemporaryPhysicalAccessRequestSerializer
+    filterset_fields = {
+        "id": key_filters,
+        "creation_time": datetime_filters,
+        "creator": key_filters,
+        "last_updated": datetime_filters,
+        "last_updated_by": key_filters,
+        "physical_access_level": key_filters,
+        "description": string_filters,
+        "start_time": datetime_filters,
+        "end_time": datetime_filters,
+        "other_users": manykey_filters,
+        "status": number_filters,
+        "reviewer": key_filters,
+        "deleted": boolean_filters,
+    }
+
+
+class AdjustmentRequestViewSet(ModelViewSet):
+    filename = "adjustment_requests"
+    queryset = AdjustmentRequest.objects.all()
+    serializer_class = AdjustmentRequestSerializer
+    filterset_fields = {
+        "id": key_filters,
+        "creation_time": datetime_filters,
+        "creator": key_filters,
+        "last_updated": datetime_filters,
+        "last_updated_by": key_filters,
+        "item_type": key_filters,
+        "item_id": number_filters,
+        "description": string_filters,
+        "manager_note": string_filters,
+        "new_start": datetime_filters,
+        "new_end": datetime_filters,
+        "status": number_filters,
+        "reviewer": key_filters,
+        "applied": boolean_filters,
+        "applied_by": key_filters,
+        "deleted": boolean_filters,
+    }
+
+
+class ToolCredentialsViewSet(ModelViewSet):
+    filename = "tool_credentials"
+    queryset = ToolCredentials.objects.all()
+    serializer_class = ToolCredentialsSerializer
+    filterset_fields = {
+        "id": key_filters,
+        "tool": key_filters,
+        "username": string_filters,
+        "password": string_filters,
+        "comments": string_filters,
+        "authorized_staff": manykey_filters,
     }
 
 
@@ -575,8 +717,8 @@ class GroupViewSet(ModelViewSet):
     queryset = Group.objects.all()
     serializer_class = GroupSerializer
     filterset_fields = {
-        "name": ["exact", "iexact", "in"],
-        "permissions": ["exact"],
+        "name": string_filters,
+        "permissions": manykey_filters,
     }
 
 
@@ -586,10 +728,10 @@ class PermissionViewSet(XLSXFileMixin, viewsets.ReadOnlyModelViewSet):
     queryset = Permission.objects.all()
     serializer_class = PermissionSerializer
     filterset_fields = {
-        "name": ["exact", "iexact", "in"],
-        "codename": ["exact", "iexact", "in"],
-        "content_type_id": ["exact", "in"],
-        "content_type": ["exact", "in"],
+        "name": string_filters,
+        "codename": string_filters,
+        "content_type_id": key_filters,
+        "content_type": key_filters,
     }
 
     def get_filename(self, *args, **kwargs):
diff --git a/NEMO/views/area_access.py b/NEMO/views/area_access.py
index cf1297e7..4995479a 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:
diff --git a/NEMO/views/customization.py b/NEMO/views/customization.py
index e892b46f..285bc5cc 100644
--- a/NEMO/views/customization.py
+++ b/NEMO/views/customization.py
@@ -27,6 +27,7 @@
     Notification,
     Project,
     RecurringConsumableCharge,
+    Tool,
     TrainingSession,
     UserPreferences,
     UserType,
@@ -119,6 +120,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)
@@ -205,6 +212,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": "",
@@ -223,6 +231,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": "",
@@ -230,6 +239,7 @@ class UserCustomization(CustomizationBase):
         "user_access_expiration_no_type": "",
         "user_access_expiration_types": "-1",
         "user_allow_document_upload": "",
+        "user_allow_profile_view": "",
     }
 
     def context(self) -> Dict:
@@ -335,6 +345,7 @@ class StatusDashboardCustomization(CustomizationBase):
         "dashboard_staff_status_absence_view_staff": "",
         "dashboard_staff_status_absence_view_user_office": "",
         "dashboard_staff_status_absence_view_accounting_officer": "",
+        "dashboard_tool_sort": "name",
     }
 
 
@@ -363,6 +374,10 @@ class UserRequestsCustomization(CustomizationBase):
         "adjustment_requests_missed_reservation_enabled": "enabled",
         "adjustment_requests_missed_reservation_times": "",
         "adjustment_requests_staff_staff_charges_enabled": "enabled",
+        "adjustment_requests_consumable_withdrawal_enabled": "enabled",
+        "adjustment_requests_consumable_withdrawal_self_checkout": "enabled",
+        "adjustment_requests_consumable_withdrawal_staff_checkout": "enabled",
+        "adjustment_requests_consumable_withdrawal_usage_event": "enabled",
         "adjustment_requests_title": "Adjustment requests",
         "adjustment_requests_description": "",
         "adjustment_requests_charges_display_number": "10",
@@ -469,18 +484,21 @@ 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": "",
         "tool_control_show_task_details": "",
         "tool_control_show_qualified_users_to_all": "",
         "tool_control_show_documents_only_qualified_users": "",
+        "tool_control_show_tool_credentials": "enabled",
         "tool_qualification_reminder_days": "",
         "tool_qualification_expiration_days": "",
         "tool_qualification_expiration_never_used_days": "",
         "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",
@@ -543,16 +561,30 @@ 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_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/email.py b/NEMO/views/email.py
index cc18ec4e..354f1789 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":
diff --git a/NEMO/views/status_dashboard.py b/NEMO/views/status_dashboard.py
index 1d2584fc..fc0c5e1b 100644
--- a/NEMO/views/status_dashboard.py
+++ b/NEMO/views/status_dashboard.py
@@ -14,7 +14,7 @@
 
 from NEMO.decorators import disable_session_expiry_refresh, facility_manager_required
 from NEMO.forms import StaffAbsenceForm
-from NEMO.model_tree import get_area_model_tree, ModelTreeHelper, TreeItem
+from NEMO.model_tree import ModelTreeHelper, TreeItem, get_area_model_tree
 from NEMO.models import (
     Area,
     AreaAccessRecord,
@@ -30,8 +30,8 @@
 )
 from NEMO.typing import QuerySetType
 from NEMO.utilities import (
-    as_timezone,
     BasicDisplayTable,
+    as_timezone,
     beginning_of_the_day,
     end_of_the_day,
     export_format_datetime,
@@ -354,7 +354,19 @@ def create_tool_summary(tooltip_info=False):
     )
     tool_summary = merge(tools, tasks, unavailable_resources, usage_events, scheduled_outages, tooltip_info)
     tool_summary = list(tool_summary.values())
-    tool_summary.sort(key=lambda x: x["name"])
+    tool_sort = StatusDashboardCustomization.get("dashboard_tool_sort")
+    max_date_aware = (datetime.max - timedelta(days=1)).astimezone()
+    if tool_sort == "name":
+        tool_summary.sort(key=lambda x: x["name"].lower())
+    elif tool_sort == "time_desc":
+        tool_summary.sort(
+            key=lambda x: (
+                (max_date_aware - x["in_use_since"]) if x["in_use_since"] else timedelta.max,
+                x["name"].lower(),
+            )
+        )
+    else:
+        tool_summary.sort(key=lambda x: (x["in_use_since"] if x["in_use_since"] else max_date_aware, x["name"].lower()))
     return tool_summary
 
 
diff --git a/NEMO/views/tasks.py b/NEMO/views/tasks.py
index 5a4ecbcc..a34e1b83 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.
     """
     user: User = request.user
     images_form = TaskImagesForm(request.POST, request.FILES)
@@ -65,28 +65,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, user, 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, user, task_images)
+
     return redirect("tool_control")
 
 
 def save_task(request, task: Task, user: User, 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.
@@ -114,8 +113,6 @@ def save_task(request, task: Task, user: User, task_images: List[TaskImages] = N
     send_new_task_emails(request, task, user, task_images)
     set_task_status(request, task, request.POST.get("status"), user)
 
-    return None
-
 
 def send_new_task_emails(request, task: Task, user, task_images: List[TaskImages]):
     message = get_media_file_contents("new_task_email.html")
@@ -139,7 +136,7 @@ def send_new_task_emails(request, task: Task, user, 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():
@@ -399,7 +396,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
@@ -420,6 +417,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/timed_services.py b/NEMO/views/timed_services.py
index 97cbee87..0ebef423 100644
--- a/NEMO/views/timed_services.py
+++ b/NEMO/views/timed_services.py
@@ -500,6 +500,16 @@ def send_email_reservation_ending_reminders(request=None):
     ending_reservations = user_area_reservations.filter(time_filter)
     # Email a reminder to each user with a reservation ending soon.
     for reservation in ending_reservations:
+        starting_reservation = Reservation.objects.filter(
+            cancelled=False,
+            missed=False,
+            shortened=False,
+            area=reservation.area,
+            user=reservation.user,
+            start=reservation.end,
+        )
+        if starting_reservation.exists():
+            continue
         subject = reservation.reservation_item.name + " reservation ending soon"
         rendered_message = render_email_template(
             reservation_ending_reminder_message, {"reservation": reservation}, request
diff --git a/NEMO/views/tool_control.py b/NEMO/views/tool_control.py
index 76ec4282..000fb16e 100644
--- a/NEMO/views/tool_control.py
+++ b/NEMO/views/tool_control.py
@@ -110,8 +110,17 @@ def tool_status(request, tool_id):
         if user_wait_list_entry
         else 0
     )
+    tool_credentials = []
+    if ToolCustomization.get_bool("tool_control_show_tool_credentials") and (user.is_staff or user.is_facility_manager):
+        if user.is_facility_manager:
+            tool_credentials = tool.toolcredentials_set.all()
+        else:
+            tool_credentials = tool.toolcredentials_set.filter(
+                Q(authorized_staff__isnull=True) | Q(authorized_staff__in=[user])
+            )
     dictionary = {
         "tool": tool,
+        "tool_credentials": tool_credentials,
         "tool_rate": rate_class.get_tool_rate(tool, user),
         "task_categories": TaskCategory.objects.filter(stage=TaskCategory.Stage.INITIAL_ASSESSMENT),
         "rendered_configuration_html": tool.configuration_widget(user),
diff --git a/NEMO/views/tool_credentials.py b/NEMO/views/tool_credentials.py
new file mode 100644
index 00000000..0a4190c4
--- /dev/null
+++ b/NEMO/views/tool_credentials.py
@@ -0,0 +1,49 @@
+from django.db.models import Q
+from django.shortcuts import render
+from django.views.decorators.http import require_GET
+
+from NEMO.decorators import facility_manager_required, staff_member_required
+from NEMO.models import ToolCredentials, User
+from NEMO.utilities import BasicDisplayTable, export_format_datetime
+
+
+@staff_member_required
+@require_GET
+def tool_credentials_list(request):
+    user: User = request.user
+    tool_credentials = ToolCredentials.objects.filter(tool__visible=True)
+    if not user.is_facility_manager:
+        tool_credentials = tool_credentials.filter(Q(authorized_staff__isnull=True) | Q(authorized_staff__in=[user]))
+    return render(request, "tool_credentials/tool_credentials.html", {"tool_credentials": tool_credentials})
+
+
+@facility_manager_required
+@require_GET
+def export_tool_credentials(request):
+    table = BasicDisplayTable()
+    table.headers = [
+        ("tool", "Tool"),
+        ("username", "Username"),
+        ("password", "Password"),
+        ("comments", "Comments"),
+        ("authorized_staff", "Authorized staff"),
+    ]
+    for tool_cred in ToolCredentials.objects.all():
+        tool_cred: ToolCredentials = tool_cred
+        table.add_row(
+            {
+                "tool": tool_cred.tool.name,
+                "username": tool_cred.username,
+                "password": tool_cred.password,
+                "comments": tool_cred.comments,
+                "authorized_staff": (
+                    "\n".join([authorized_user for authorized_user in tool_cred.authorized_staff.all()])
+                    if tool_cred.authorized_staff.exists()
+                    else "All"
+                ),
+            }
+        )
+    filename = f"tool_credentials_{export_format_datetime()}.csv"
+    response = table.to_csv()
+    response["Content-Disposition"] = f'attachment; filename="{filename}"'
+    return response
diff --git a/NEMO/views/training.py b/NEMO/views/training.py
index feeb6861..7dd723a3 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,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)
+    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])
diff --git a/NEMO/views/usage.py b/NEMO/views/usage.py
index 96b26fa3..587e3fb4 100644
--- a/NEMO/views/usage.py
+++ b/NEMO/views/usage.py
@@ -40,7 +40,7 @@
     billable_items_training_sessions,
     billable_items_usage_events,
 )
-from NEMO.views.customization import UserRequestsCustomization
+from NEMO.views.customization import ProjectsAccountsCustomization, UserRequestsCustomization
 
 logger = getLogger(__name__)
 
@@ -152,7 +152,7 @@ def usage(request):
     usage_events = UsageEvent.objects.filter(user_filter).filter(end__gt=start_date, end__lte=end_date)
     if csv_export:
         return csv_export_response(
-            usage_events, area_access, training_sessions, staff_charges, consumables, missed_reservations
+            user, usage_events, area_access, training_sessions, staff_charges, consumables, missed_reservations
         )
     else:
         dictionary = {
@@ -251,7 +251,13 @@ def project_usage(request):
             usage_events = usage_events.filter(user=user)
         if bool(request.GET.get("csv", False)):
             return csv_export_response(
-                usage_events, area_access, training_sessions, staff_charges, consumables, missed_reservations
+                request.user,
+                usage_events,
+                area_access,
+                training_sessions,
+                staff_charges,
+                consumables,
+                missed_reservations,
             )
     except:
         pass
@@ -259,7 +265,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,
@@ -291,7 +297,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
@@ -435,13 +441,19 @@ def billing_dict(start_date, end_date, user, formatted_applications, project_id=
     return dictionary
 
 
-def csv_export_response(usage_events, area_access, training_sessions, staff_charges, consumables, missed_reservations):
+def csv_export_response(
+    user: User, usage_events, area_access, training_sessions, staff_charges, consumables, missed_reservations
+):
     table_result = BasicDisplayTable()
     table_result.add_header(("type", "Type"))
     table_result.add_header(("user", "User"))
     table_result.add_header(("name", "Item"))
     table_result.add_header(("details", "Details"))
     table_result.add_header(("project", "Project"))
+    if user.is_any_part_of_staff:
+        table_result.add_header(
+            ("application", ProjectsAccountsCustomization.get("project_application_identifier_name"))
+        )
     table_result.add_header(("start", "Start time"))
     table_result.add_header(("end", "End time"))
     table_result.add_header(("quantity", "Quantity"))
diff --git a/NEMO/views/users.py b/NEMO/views/users.py
index c8815675..bccdb014 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"]:
@@ -468,10 +472,34 @@ 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)
 
 
+@login_required
+@require_GET
+def view_user(request, user_id):
+    if UserCustomization.get_bool("user_allow_profile_view"):
+        user = get_object_or_404(User, pk=user_id)
+
+        if request.user.id != user_id:
+            return HttpResponseBadRequest("You are not allowed to view this user's profile")
+
+        dictionary = {
+            "user": user,
+            "projects": Project.objects.filter(active=True, account__active=True),
+        }
+
+        return render(request, "users/view_user.html", dictionary)
+    else:
+        return HttpResponseBadRequest("You are not allowed to view this page")
+
+
 def readonly_users(request):
     # Only user office and facility managers can edit user information
     user: User = request.user
diff --git a/resources/emails/unauthorized_tool_access_email.html b/resources/emails/unauthorized_tool_access_email.html
index 03dafd4b..d8e62695 100644
--- a/resources/emails/unauthorized_tool_access_email.html
+++ b/resources/emails/unauthorized_tool_access_email.html
@@ -17,9 +17,17 @@ <h1 style="max-width: 90%; margin: 0 auto; padding: 0;">UNAUTHORIZED TOOL ACCESS
             <tr>
                 <td style="padding: 10px;">
                     <p>
-                        {{ operator }} attempted to access the {{ tool }}, which requires {{ tool.requires_area_access|lower }} access.
-                        <br>
-                        The user was not signed into the {{ tool.requires_area_access|lower }} at the time so the tool usage request was denied.
+                        {{ operator }} attempted to access the {{ tool }}, which requires
+                        {% if type == "area-access" %}
+                            {{ tool.requires_area_access|lower }} access.
+                            <br>
+                            The user was not logged into the {{ tool.requires_area_access|lower }} at the time so the tool usage request was denied.
+                        {% else %}
+                            an active {{ tool.requires_area_access|lower }} reservation.
+                            <br>
+                            The user was logged into the {{ tool.requires_area_access|lower }} at the time, but their area reservation expired,
+                            so the tool usage request was denied.
+                        {% endif %}
                     </p>
                 </td>
             </tr>
diff --git a/resources/fixtures/splash_pad.json b/resources/fixtures/splash_pad.json
index 8680bf79..1ffb4f0c 100644
--- a/resources/fixtures/splash_pad.json
+++ b/resources/fixtures/splash_pad.json
@@ -1079,6 +1079,13 @@
       "value": "enabled"
     }
   },
+  {
+    "model": "NEMO.customization",
+    "pk": "user_allow_profile_view",
+    "fields": {
+      "value": "enabled"
+    }
+  },
   {
     "model": "NEMO.customization",
     "pk": "consumable_user_self_checkout",
@@ -2457,7 +2464,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": null,
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -2505,7 +2512,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": null,
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -2553,7 +2560,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": null,
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -2601,7 +2608,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -2646,7 +2653,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -2691,7 +2698,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -2736,7 +2743,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -2781,7 +2788,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -2827,7 +2834,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -2873,7 +2880,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": 540,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": true,
       "_policy_off_start_time": "18:00:00",
@@ -2919,7 +2926,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -2965,7 +2972,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3011,7 +3018,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3057,7 +3064,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3103,7 +3110,7 @@
       "_minimum_time_between_reservations": 240,
       "_maximum_future_reservation_time": 720,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": true,
       "_policy_off_start_time": "18:00:00",
@@ -3149,7 +3156,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3195,7 +3202,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3241,7 +3248,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3287,7 +3294,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3333,7 +3340,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3379,7 +3386,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3425,7 +3432,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3471,7 +3478,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3517,7 +3524,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3563,7 +3570,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3609,7 +3616,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3655,7 +3662,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3701,7 +3708,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3747,7 +3754,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3793,7 +3800,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3839,7 +3846,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3885,7 +3892,7 @@
       "_minimum_time_between_reservations": 180,
       "_maximum_future_reservation_time": 1440,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3931,7 +3938,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -3977,7 +3984,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4023,7 +4030,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4069,7 +4076,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4115,7 +4122,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4161,7 +4168,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4207,7 +4214,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4253,7 +4260,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4299,7 +4306,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4345,7 +4352,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4391,7 +4398,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4437,7 +4444,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4483,7 +4490,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4529,7 +4536,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4575,7 +4582,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4621,7 +4628,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4667,7 +4674,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4713,7 +4720,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4759,7 +4766,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4805,7 +4812,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4851,7 +4858,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4897,7 +4904,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4943,7 +4950,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -4989,7 +4996,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5035,7 +5042,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5081,7 +5088,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5127,7 +5134,7 @@
       "_minimum_time_between_reservations": 240,
       "_maximum_future_reservation_time": 720,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": true,
       "_policy_off_start_time": "18:00:00",
@@ -5173,7 +5180,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5219,7 +5226,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5265,7 +5272,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5311,7 +5318,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "[\n   {\n      \"type\":\"group\",\n      \"name\":\"group1\",\n      \"title\":\"Add up to 3 processes:\",\n      \"max_number\":3,\n      \"group_add_button_name\": \"Add another process\",\n      \"questions\":[\n         {\n            \"type\":\"number\",\n            \"precision\": 5,\n            \"title\":\"How much gold was deposited?\",\n            \"max-width\":250,\n            \"suffix\":\"nl\",\n            \"required\":true,\n            \"placeholder\":\"0\",\n            \"name\":\"gold_used\",\n            \"consumable\": \"Sputter gold\"\n         },\n         {\n            \"type\": \"textarea\",\n            \"title\": \"Comments\",\n            \"name\": \"comments\",\n            \"required\": false,\n            \"max-width\":250,\n            \"rows\": 5\n         }\n      ]\n   }\n]",
       "_pre_usage_questions": "[\r\n         {\r\n            \"type\":\"number\",\r\n            \"precision\": 5,\r\n            \"title\":\"How much gold do you plan on depositing?\",\r\n            \"max-width\":250,\r\n            \"suffix\":\"nl\",\r\n            \"required\":true,\r\n            \"placeholder\":\"0\",\r\n            \"name\":\"planned_gold_usage\"\r\n          },\r\n         {\r\n            \"type\": \"textarea\",\r\n            \"title\": \"Comments\",\r\n            \"name\": \"comments\",\r\n            \"required\": false,\r\n            \"max-width\":250,\r\n            \"rows\": 5\r\n         }\r\n]",
       "_policy_off_start_time": null,
@@ -5357,7 +5364,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5403,7 +5410,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5449,7 +5456,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5495,7 +5502,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5541,7 +5548,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5587,7 +5594,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5633,7 +5640,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5679,7 +5686,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5725,7 +5732,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5771,7 +5778,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5817,7 +5824,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5863,7 +5870,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5909,7 +5916,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -5955,7 +5962,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6001,7 +6008,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6047,7 +6054,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6093,7 +6100,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6139,7 +6146,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6185,7 +6192,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6231,7 +6238,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6277,7 +6284,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6323,7 +6330,7 @@
       "_minimum_time_between_reservations": 240,
       "_maximum_future_reservation_time": 480,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": true,
       "_policy_off_start_time": "19:00:00",
@@ -6369,7 +6376,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6415,7 +6422,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6461,7 +6468,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6507,7 +6514,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6553,7 +6560,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6599,7 +6606,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6645,7 +6652,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6691,7 +6698,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6737,7 +6744,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6783,7 +6790,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6829,7 +6836,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6875,7 +6882,7 @@
       "_minimum_time_between_reservations": 60,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6921,7 +6928,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -6967,7 +6974,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7013,7 +7020,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7059,7 +7066,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7105,7 +7112,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7151,7 +7158,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7197,7 +7204,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7243,7 +7250,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7289,7 +7296,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7335,7 +7342,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7381,7 +7388,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7427,7 +7434,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7473,7 +7480,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7519,7 +7526,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7565,7 +7572,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7611,7 +7618,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7657,7 +7664,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7703,7 +7710,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7749,7 +7756,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7795,7 +7802,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7841,7 +7848,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7887,7 +7894,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7933,7 +7940,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -7979,7 +7986,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8025,7 +8032,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8071,7 +8078,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8117,7 +8124,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8163,7 +8170,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8209,7 +8216,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8255,7 +8262,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8301,7 +8308,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": null,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8347,7 +8354,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8393,7 +8400,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8439,7 +8446,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8485,7 +8492,7 @@
       "_minimum_time_between_reservations": 180,
       "_maximum_future_reservation_time": 1440,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8531,7 +8538,7 @@
       "_minimum_time_between_reservations": 180,
       "_maximum_future_reservation_time": 1440,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8577,7 +8584,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8623,7 +8630,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8669,7 +8676,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": true,
+      "_max_delayed_logoff": 120,
       "_post_usage_questions": "",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
@@ -8715,7 +8722,7 @@
       "_minimum_time_between_reservations": null,
       "_maximum_future_reservation_time": 540,
       "_missed_reservation_threshold": 30,
-      "_allow_delayed_logoff": false,
+      "_max_delayed_logoff": null,
       "_post_usage_questions": "",
       "_policy_off_between_times": true,
       "_policy_off_start_time": "18:00:00",
@@ -9710,6 +9717,7 @@
     "pk": 1,
     "fields": {
       "name": "Captain Nemo",
+      "title": "Commander",
       "image": "Captain_Nemo.png",
       "category": [
         "Staff"
@@ -9729,6 +9737,7 @@
     "pk": 2,
     "fields": {
       "name": "Pierre Aronnax",
+      "title": "Professor of marine biology",
       "image": "Pierre_Aronnax.png",
       "category": [
         "Users"
@@ -9748,6 +9757,7 @@
     "pk": 3,
     "fields": {
       "name": "Ned Land",
+      "title": "Sailor",
       "image": "Ned_Land.png",
       "category": [
         "Users"
@@ -9927,6 +9937,28 @@
       "validated": false
     }
   },
+  {
+    "model": "NEMO.toolcredentials",
+    "pk": 1,
+    "fields": {
+      "tool": 4,
+      "username": "user",
+      "password": "password",
+      "comments": "user account",
+      "authorized_staff": []
+    }
+  },
+  {
+    "model": "NEMO.toolcredentials",
+    "pk": 2,
+    "fields": {
+      "tool": 4,
+      "username": "admin",
+      "password": "password",
+      "comments": "admin account",
+      "authorized_staff": []
+    }
+  },
   {
     "model": "NEMO.trainingsession",
     "pk": 1,
diff --git a/resources/settings.py b/resources/settings.py
index d68b54ef..02af55e5 100644
--- a/resources/settings.py
+++ b/resources/settings.py
@@ -194,6 +194,7 @@
 # Uncomment the following line to use the "X_FORWARDED_HOST" header of the web server to build URLs.
 # This header needs to be configured in your Nginx/Apache web server. The host also needs to be in ALLOWED_HOSTS.
 # USE_X_FORWARDED_HOST = True
+CSRF_TRUSTED_ORIGINS = ["https://{}".format(ALLOWED_HOSTS[0])]
 
 # -------------------- Elevated roles --------------------
 # Admins will receive error emails when email admin handler is set in the logging configuration
diff --git a/setup.py b/setup.py
index a3dda1c6..c5271bd2 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 
 setup(
     name="NEMO",
-    version="5.6.1",
+    version="6.0.0",
     python_requires=">=3.8, <4",
     packages=find_namespace_packages(exclude=["resources", "resources.*", "build", "build.*"]),
     include_package_data=True,
@@ -28,8 +28,8 @@
         "Programming Language :: Python :: 3.12",
     ],
     install_requires=[
-        "cryptography==42.0.5",
-        "Django==3.2.25",
+        "cryptography==42.0.8",
+        "Django==4.2.11",
         "django-auditlog==3.0.0",
         "django-filter==23.5",
         "django-mptt==0.14.0",
@@ -41,7 +41,7 @@
         "pymodbus==3.3.2",
         "python-dateutil==2.9.0",
         "pytz==2024.1",
-        "requests==2.31.0",
+        "requests==2.32.3",
     ],
     extras_require={"dev-tools": ["pre-commit", "djlint", "black"]},
     entry_points={


Test patch:
diff --git a/NEMO/tests/test_formats.py b/NEMO/tests/test_formats.py
index 126a8b9e..de3f02fa 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/tests/test_tool.py b/NEMO/tests/test_tool.py
index 5dfaba31..17234520 100644
--- a/NEMO/tests/test_tool.py
+++ b/NEMO/tests/test_tool.py
@@ -60,7 +60,7 @@ def setUp(self):
             "_minimum_time_between_reservations": 10,
             "_maximum_future_reservation_time": 20,
             "_missed_reservation_threshold": 30,
-            "_allow_delayed_logoff": True,
+            "_max_delayed_logoff": 120,
             "_post_usage_questions": '[{"type": "textbox", "name": "question_name", "title": "question_title", "max-width": "100%"}]',
             "_policy_off_between_times": True,
             "_policy_off_start_time": "5:00 PM",
@@ -115,7 +115,7 @@ def test_tool_and_parent_properties(self):
         self.assertEqual(tool.minimum_time_between_reservations, alternate_tool.minimum_time_between_reservations)
         self.assertEqual(tool.maximum_future_reservation_time, alternate_tool.maximum_future_reservation_time)
         self.assertEqual(tool.missed_reservation_threshold, alternate_tool.missed_reservation_threshold)
-        self.assertEqual(tool.allow_delayed_logoff, alternate_tool.allow_delayed_logoff)
+        self.assertEqual(tool.max_delayed_logoff, alternate_tool.max_delayed_logoff)
         self.assertEqual(tool.post_usage_questions, alternate_tool.post_usage_questions)
         self.assertEqual(tool.policy_off_between_times, alternate_tool.policy_off_between_times)
         self.assertEqual(tool.policy_off_start_time, alternate_tool.policy_off_start_time)
diff --git a/NEMO/tests/test_urls.py b/NEMO/tests/test_urls.py
index d7d26325..5e697c5a 100644
--- a/NEMO/tests/test_urls.py
+++ b/NEMO/tests/test_urls.py
@@ -136,6 +136,7 @@
     "knowledge_base_items_search": {"kwargs": {"kind": "user"}},
     "knowledge_base_categories": {"kwargs": {"kind": "user"}},
     "knowledge_base_all_in_one": {"kwargs": {"kind": "user"}},
+    "view_user": {"login_id": 1},
 }
 
 urls_to_skip = [
diff --git a/NEMO/tests/test_users.py b/NEMO/tests/test_users.py
new file mode 100644
index 00000000..9f009d03
--- /dev/null
+++ b/NEMO/tests/test_users.py
@@ -0,0 +1,24 @@
+from django.test import TestCase
+from django.urls import reverse
+
+from NEMO.tests.test_utilities import create_user_and_project, login_as, login_as_staff
+from NEMO.views.customization import UserCustomization
+
+
+class UserTestCase(TestCase):
+
+    def test_view_user_profile(self):
+        customer, customer_project = create_user_and_project()
+        login_as(self.client, customer)
+        # Cannot see one's profile if the feature is disabled
+        UserCustomization.set("user_allow_profile_view", "")
+        response = self.client.get(reverse("view_user", args=[customer.id]))
+        self.assertEqual(response.status_code, 400)
+        # Now it should work
+        UserCustomization.set("user_allow_profile_view", "enabled")
+        response = self.client.get(reverse("view_user", args=[customer.id]))
+        self.assertEqual(response.status_code, 200)
+        # Cannot see someone else's profile even as staff
+        login_as_staff(self.client)
+        response = self.client.get(reverse("view_user", args=[customer.id]))
+        self.assertEqual(response.status_code, 400)
diff --git a/NEMO/tests/test_waitlist.py b/NEMO/tests/test_waitlist.py
index 632bb570..4e90369c 100644
--- a/NEMO/tests/test_waitlist.py
+++ b/NEMO/tests/test_waitlist.py
@@ -10,7 +10,7 @@
 from NEMO.views.tool_control import do_exit_wait_list
 
 
-class ToolTestCase(TestCase):
+class ToolWaitListTestCase(TestCase):
     def setUp(self) -> None:
         EmailsCustomization.set("user_office_email_address", "user_office_email_address@wait_list_test.com")
         store_media_file(