diff --git a/NEMO/admin.py b/NEMO/admin.py index bf5e50c34..7ac381ef8 100644 --- a/NEMO/admin.py +++ b/NEMO/admin.py @@ -61,6 +61,7 @@ ConsumableWithdraw, ContactInformation, ContactInformationCategory, + CoreFacility, Customization, Door, EmailLog, @@ -129,7 +130,7 @@ record_remote_many_to_many_changes_and_save, ) from NEMO.utilities import admin_get_item, format_daterange -from NEMO.views.customization import ApplicationCustomization, ProjectsAccountsCustomization +from NEMO.views.customization import ApplicationCustomization, CoreFacilityCustomization, ProjectsAccountsCustomization from NEMO.widgets.dynamic_form import DynamicForm, PostUsageGroupQuestion, admin_render_dynamic_form_preview @@ -173,6 +174,70 @@ class DocumentModelAdmin(admin.TabularInline): extra = 1 +class CoreFacilityAdminForm(forms.ModelForm): + class Meta: + model = CoreFacility + fields = "__all__" + + core_facility_tools = forms.ModelMultipleChoiceField( + queryset=Tool.objects.all(), + required=False, + widget=FilteredSelectMultiple(verbose_name="Core facility tools", is_stacked=False), + ) + core_facility_areas = forms.ModelMultipleChoiceField( + queryset=Area.objects.all(), + required=False, + widget=FilteredSelectMultiple(verbose_name="Core facility areas", is_stacked=False), + ) + core_facility_consumables = forms.ModelMultipleChoiceField( + queryset=Consumable.objects.all(), + required=False, + widget=FilteredSelectMultiple(verbose_name="Core facility consumable", is_stacked=False), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # We are filtering out already set tools, areas and consumables + queryset_filter = Q(core_facility__isnull=True) | Q(core_facility_id=self.instance.pk) + if "external_id" in self.fields: + self.fields["external_id"].label = CoreFacilityCustomization.get("core_facility_external_id_name") + + # Exclude children tools since their core facility is their parent's + if "core_facility_tools" in self.fields: + self.fields["core_facility_tools"].queryset = Tool.objects.filter( + Q(_core_facility_id=self.instance.pk) | Q(_core_facility__isnull=True) + ).exclude(parent_tool__isnull=False) + if self.instance.pk: + self.fields["core_facility_tools"].initial = self.instance.tools.all() + if "core_facility_areas" in self.fields: + self.fields["core_facility_areas"].queryset = Area.objects.filter(queryset_filter) + if self.instance.pk: + self.fields["core_facility_areas"].initial = self.instance.areas.all() + if "core_facility_consumables" in self.fields: + self.fields["core_facility_consumables"].queryset = Consumable.objects.filter(queryset_filter) + if self.instance.pk: + self.fields["core_facility_consumables"].initial = self.instance.consumables.all() + + +@register(CoreFacility) +class CoreFacilityAdmin(admin.ModelAdmin): + list_display = ["name", "get_external_id"] + form = CoreFacilityAdminForm + + def save_model(self, request, obj: CoreFacility, form, change): + super().save_model(request, obj, form, change) + if "core_facility_tools" in form.changed_data: + obj.tools.set(form.cleaned_data["core_facility_tools"]) + if "core_facility_areas" in form.changed_data: + obj.areas.set(form.cleaned_data["core_facility_areas"]) + if "core_facility_consumables" in form.changed_data: + obj.consumables.set(form.cleaned_data["core_facility_consumables"]) + + @admin.display(ordering="external_id") + def get_external_id(self, core_facility: CoreFacility): + return core_facility.external_id + + class ToolAdminForm(forms.ModelForm): class Meta: model = Tool @@ -246,6 +311,7 @@ class ToolAdmin(admin.ModelAdmin): "visible", "operational_display", "_operation_mode", + "_core_facility", "problematic", "is_configurable", "id", @@ -258,8 +324,8 @@ class ToolAdmin(admin.ModelAdmin): "_operation_mode", "_category", "_location", + ("_core_facility", admin.RelatedOnlyFieldListFilter), ("_requires_area_access", admin.RelatedOnlyFieldListFilter), - has_fk_filter("staff_charge", "Staff Charge"), ) autocomplete_fields = [ "_primary_owner", @@ -277,6 +343,7 @@ class ToolAdmin(admin.ModelAdmin): "parent_tool", "_category", "_operation_mode", + "_core_facility", "qualified_users", "_problem_shutdown_enabled", ) @@ -535,6 +602,7 @@ class AreaAdmin(DraggableMPTTAdmin): "parent_area", "category", "requires_reservation", + "core_facility", "maximum_capacity", "reservation_warning", "buddy_system_allowed", @@ -542,7 +610,7 @@ class AreaAdmin(DraggableMPTTAdmin): ) filter_horizontal = ["adjustment_request_reviewers", "access_request_reviewers"] fieldsets = ( - (None, {"fields": ("name", "parent_area", "category", "reservation_email", "abuse_email")}), + (None, {"fields": ("name", "parent_area", "category", "reservation_email", "abuse_email", "core_facility")}), ("Additional Information", {"fields": ("area_calendar_color",)}), ( "Area access", @@ -597,7 +665,11 @@ class AreaAdmin(DraggableMPTTAdmin): ), ) list_display_links = ("indented_title",) - list_filter = ("requires_reservation", ("parent_area", TreeRelatedFieldListFilter)) + list_filter = ( + "requires_reservation", + ("parent_area", TreeRelatedFieldListFilter), + ("core_facility", admin.RelatedOnlyFieldListFilter), + ) search_fields = ("name",) actions = [rebuild_area_tree] @@ -663,12 +735,24 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): @register(StaffCharge) class StaffChargeAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin): - list_display = ("id", "staff_member", "customer", "start", "end", "waived", "has_area_record", "has_usage_event") + list_display = ( + "id", + "staff_member", + "customer", + "project", + "core_facility", + "start", + "end", + "waived", + "has_area_record", + "has_usage_event", + ) list_filter = ( "start", "waived", ("customer", admin.RelatedOnlyFieldListFilter), ("staff_member", admin.RelatedOnlyFieldListFilter), + ("core_facility", admin.RelatedOnlyFieldListFilter), has_fk_filter("areaaccessrecord", "Area Record"), has_fk_filter("usageevent", "Usage Event"), ("project__project_types", admin.RelatedOnlyFieldListFilter), @@ -1023,6 +1107,7 @@ class UsageEventAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.Mo "start", "end", "waived", + has_fk_filter("staff_charge", "Staff Charge"), ("tool", admin.RelatedOnlyFieldListFilter), ("project__project_types", admin.RelatedOnlyFieldListFilter), ("project__account__type", admin.RelatedOnlyFieldListFilter), @@ -1046,6 +1131,7 @@ class ConsumableAdmin(admin.ModelAdmin): "name", "quantity", "category", + "core_facility", "visible", "reusable", "allow_self_checkout", @@ -1053,7 +1139,13 @@ class ConsumableAdmin(admin.ModelAdmin): "reminder_email", "id", ) - list_filter = ("visible", ("category", admin.RelatedOnlyFieldListFilter), "reusable", "allow_self_checkout") + list_filter = ( + "visible", + ("category", admin.RelatedOnlyFieldListFilter), + "reusable", + "allow_self_checkout", + ("core_facility", admin.RelatedOnlyFieldListFilter), + ) filter_horizontal = ["self_checkout_only_users"] search_fields = ("name",) readonly_fields = ("reminder_threshold_reached",) diff --git a/NEMO/apps/__init__.py b/NEMO/apps/__init__.py index e17895a92..746131b7f 100644 --- a/NEMO/apps/__init__.py +++ b/NEMO/apps/__init__.py @@ -48,8 +48,12 @@ def patched_from_db_value(self, value, expression, connection): def init_admin_site(): - from NEMO.views.customization import ApplicationCustomization, ProjectsAccountsCustomization - from NEMO.admin import ProjectAdmin + from NEMO.views.customization import ( + ApplicationCustomization, + ProjectsAccountsCustomization, + CoreFacilityCustomization, + ) + from NEMO.admin import ProjectAdmin, CoreFacilityAdmin from django.contrib import admin # customize the site @@ -58,10 +62,13 @@ def init_admin_site(): admin.site.site_header = site_title admin.site.site_title = site_title admin.site.index_title = "Detailed administration" - # update the short_description for project's application identifier here after initialization + # update the short_description for project's application identifier and core facility's external identifier here after initialization ProjectAdmin.get_application_identifier.short_description = ProjectsAccountsCustomization.get( "project_application_identifier_name", raise_exception=False ) + CoreFacilityAdmin.get_external_id.short_description = CoreFacilityCustomization.get( + "core_facility_external_id_name", raise_exception=False + ) def init_rates(): diff --git a/NEMO/migrations/0149_version_8_1_0.py b/NEMO/migrations/0149_version_8_1_0.py new file mode 100644 index 000000000..d369cbe54 --- /dev/null +++ b/NEMO/migrations/0149_version_8_1_0.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2.14 on 2026-05-20 19:39 +from NEMO import migrations_utils +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("NEMO", "0148_unplannedoutage_resource_alter_unplannedoutage_tool"), + ] + + operations = [ + migrations.RunPython( + migrations_utils.news_for_version_forward("8.1.0"), migrations_utils.news_for_version_reverse("8.1.0") + ), + ] diff --git a/NEMO/migrations/0150_corefacility_area_core_facility_and_more.py b/NEMO/migrations/0150_corefacility_area_core_facility_and_more.py new file mode 100644 index 000000000..9edf23bb2 --- /dev/null +++ b/NEMO/migrations/0150_corefacility_area_core_facility_and_more.py @@ -0,0 +1,84 @@ +# Generated by Django 5.2.13 on 2026-05-21 14:33 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("NEMO", "0149_version_8_1_0"), + ] + + operations = [ + migrations.CreateModel( + name="CoreFacility", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(help_text="The name of this core facility.", max_length=255, unique=True)), + ( + "external_id", + models.CharField( + blank=True, + help_text="An external ID to associate with this core facility.", + max_length=255, + null=True, + ), + ), + ], + options={ + "verbose_name_plural": "Core facilities", + "ordering": ["name"], + }, + ), + migrations.AddField( + model_name="area", + name="core_facility", + field=models.ForeignKey( + blank=True, + help_text="The core facility this area belongs to.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="areas", + to="NEMO.corefacility", + ), + ), + migrations.AddField( + model_name="consumable", + name="core_facility", + field=models.ForeignKey( + blank=True, + help_text="The core facility this consumable belongs to.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="consumables", + to="NEMO.corefacility", + ), + ), + migrations.AddField( + model_name="staffcharge", + name="core_facility", + field=models.ForeignKey( + blank=True, + help_text="The core facility this staff charge belongs to.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="staff_charges", + to="NEMO.corefacility", + ), + ), + migrations.AddField( + model_name="tool", + name="_core_facility", + field=models.ForeignKey( + blank=True, + db_column="core_facility_id", + help_text="The core facility this tool belongs to.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tools", + to="NEMO.corefacility", + verbose_name="core facility", + ), + ), + ] diff --git a/NEMO/models.py b/NEMO/models.py index 40204972a..f8a78cd9c 100644 --- a/NEMO/models.py +++ b/NEMO/models.py @@ -298,6 +298,25 @@ class ToolUsageQuestionType(models.TextChoices): POST = "post", _("Post") +class CoreFacility(SerializationByNameModel): + name = models.CharField( + max_length=CHAR_FIELD_MEDIUM_LENGTH, unique=True, help_text="The name of this core facility." + ) + external_id = models.CharField( + max_length=CHAR_FIELD_MEDIUM_LENGTH, + null=True, + blank=True, + help_text="An external ID to associate with this core facility.", + ) + + def __str__(self): + return self.name + + class Meta: + ordering = ["name"] + verbose_name_plural = "Core facilities" + + class UserPreferences(BaseModel): attach_created_reservation = models.BooleanField( "Created reservation invite", @@ -1269,6 +1288,16 @@ class OperationMode(object): default=True, help_text="Whether or not users can shut down the tool when reporting a problem.", ) + _core_facility = models.ForeignKey( + CoreFacility, + db_column="core_facility_id", + verbose_name="core facility", + null=True, + blank=True, + related_name="tools", + help_text="The core facility this tool belongs to.", + on_delete=models.SET_NULL, + ) _properties = fields.JsonField( schema=load_properties_schemas("Tool"), db_column="properties", verbose_name="properties", null=True, blank=True ) @@ -1577,6 +1606,15 @@ def problem_shutdown_enabled(self, value): self.raise_setter_error_if_child_tool("problem_shutdown_enabled") self._problem_shutdown_enabled = value + @property + def core_facility(self): + return self.parent_tool.core_facility if self.is_child_tool() else self._core_facility + + @core_facility.setter + def core_facility(self, value): + self.raise_setter_error_if_child_tool("core_facility") + self._core_facility = value + @property def properties(self): return self.parent_tool.properties if self.is_child_tool() else self._properties @@ -2301,7 +2339,7 @@ def clean(self): if self.parent_tool_id == self.id: errors["parent_tool"] = _("You cannot select the parent to be the tool itself.") else: - from NEMO.views.customization import ToolCustomization + from NEMO.views.customization import ToolCustomization, CoreFacilityCustomization if not self._category: errors["_category"] = _("This field is required.") @@ -2325,6 +2363,8 @@ def clean(self): errors["_requires_area_occupancy_minimum"] = _( "You cannot have a minimum occupancy without requiring an active access record to that area" ) + if not self._core_facility_id and CoreFacilityCustomization.get_bool("core_facility_required_for_tools"): + errors["_core_facility"] = _("This field is required") if errors: raise ValidationError(errors) @@ -2651,6 +2691,14 @@ class StaffCharge(BaseModel, CalendarDisplayMixin, BillableItemMixin): staff_member = models.ForeignKey(User, related_name="staff_charge_actor", on_delete=models.CASCADE) customer = models.ForeignKey(User, related_name="staff_charge_customer", on_delete=models.CASCADE) project = models.ForeignKey("Project", on_delete=models.CASCADE) + core_facility = models.ForeignKey( + CoreFacility, + null=True, + blank=True, + related_name="staff_charges", + help_text="The core facility this staff charge belongs to.", + on_delete=models.SET_NULL, + ) start = models.DateTimeField(default=timezone.now) end = models.DateTimeField(null=True, blank=True) note = models.TextField(null=True, blank=True) @@ -2665,9 +2713,13 @@ class StaffCharge(BaseModel, CalendarDisplayMixin, BillableItemMixin): ) def clean(self): + from NEMO.views.customization import CoreFacilityCustomization + errors = validate_waive_information(self) if self.end and self.start and self.end < self.start: - raise ValidationError({"end": "The end must be on or after the start"}) + errors["end"] = _("The end must be on or after the start") + if not self.core_facility_id and CoreFacilityCustomization.get_bool("core_facility_required_for_staff_charges"): + errors["core_facility"] = _("This field is required") if errors: raise ValidationError(errors) @@ -2708,6 +2760,14 @@ class Area(MPTTModel): blank=True, help_text="An email will be sent to this address when users create or cancel reservations in the area or in children areas. A comma-separated list can be used.", ) + core_facility = models.ForeignKey( + CoreFacility, + null=True, + blank=True, + related_name="areas", + help_text="The core facility this area belongs to.", + on_delete=models.SET_NULL, + ) # Area permissions adjustment_request_reviewers = models.ManyToManyField( @@ -2978,6 +3038,15 @@ def get_reservation_questions(self, project: Project = None) -> MultiDynamicForm def location(self): return self.name + def clean(self): + from NEMO.views.customization import CoreFacilityCustomization + + errors = {} + if not self.core_facility_id and CoreFacilityCustomization.get_bool("core_facility_required_for_areas"): + errors["core_facility"] = _("This field is required") + if errors: + raise ValidationError(errors) + class AreaAccessRecord(BaseModel, CalendarDisplayMixin, BillableItemMixin): area = TreeForeignKey(Area, on_delete=models.CASCADE) @@ -3449,6 +3518,14 @@ def save(self, *args, **kwargs): class Consumable(BaseModel): name = models.CharField(max_length=CHAR_FIELD_SMALL_LENGTH) category = models.ForeignKey("ConsumableCategory", blank=True, null=True, on_delete=models.CASCADE) + core_facility = models.ForeignKey( + CoreFacility, + null=True, + blank=True, + related_name="consumables", + help_text="The core facility this consumable belongs to.", + on_delete=models.SET_NULL, + ) quantity = models.IntegerField(help_text="The number of items currently in stock.") reusable = models.BooleanField( default=False, @@ -3481,13 +3558,20 @@ class Meta: ordering = ["name"] def clean(self): + from NEMO.views.customization import CoreFacilityCustomization + + errors = {} + if not self.core_facility_id and CoreFacilityCustomization.get_bool("core_facility_required_for_consumables"): + errors["core_facility"] = _("This field is required") if not self.reusable and (not self.reminder_threshold or not self.reminder_email): - raise ValidationError( + errors.update( { "reminder_threshold": "This field is required when the item is not reusable", "reminder_email": "This field is required when the item is not reusable", } ) + if errors: + raise ValidationError(errors) def __str__(self): return self.name diff --git a/NEMO/serializers.py b/NEMO/serializers.py index 58af576dc..9c9763f40 100644 --- a/NEMO/serializers.py +++ b/NEMO/serializers.py @@ -35,6 +35,7 @@ Consumable, ConsumableCategory, ConsumableWithdraw, + CoreFacility, Customization, Interlock, InterlockCard, @@ -164,6 +165,12 @@ class Meta: } +class CoreFacilitySerializer(ModelSerializer): + class Meta: + model = CoreFacility + fields = "__all__" + + class UserSerializer(FlexFieldsSerializerMixin, ModelSerializer): user_documents = serializers.PrimaryKeyRelatedField(many=True, read_only=True) @@ -289,6 +296,7 @@ class Meta: "_superusers": ("NEMO.serializers.UserSerializer", {"many": True}), "_requires_area_access": "NEMO.serializers.AreaSerializer", "project": "NEMO.serializers.ProjectSerializer", + "core_facility": "NEMO.serializers.CoreFacilitySerializer", } @@ -296,7 +304,10 @@ class AreaSerializer(FlexFieldsSerializerMixin, ModelSerializer): class Meta: model = Area fields = "__all__" - expandable_fields = {"parent_area": "NEMO.serializers.AreaSerializer"} + expandable_fields = { + "parent_area": "NEMO.serializers.AreaSerializer", + "core_facility": "NEMO.serializers.CoreFacilitySerializer", + } class ConfigurationOptionSerializer(FlexFieldsSerializerMixin, ModelSerializer): @@ -445,6 +456,7 @@ class Meta: "project": "NEMO.serializers.ProjectSerializer", "validated_by": "NEMO.serializers.UserSerializer", "waived_by": "NEMO.serializers.UserSerializer", + "core_facility": "NEMO.serializers.CoreFacilitySerializer", } @@ -478,6 +490,7 @@ class Meta: fields = "__all__" expandable_fields = { "category": "NEMO.serializers.ConsumableCategorySerializer", + "core_facility": "NEMO.serializers.CoreFacilitySerializer", } @@ -692,12 +705,14 @@ class BillableItemSerializer(serializers.Serializer): project = CharField(max_length=CHAR_FIELD_MEDIUM_LENGTH, read_only=True) project_id = IntegerField(read_only=True) application = CharField(max_length=CHAR_FIELD_MEDIUM_LENGTH, read_only=True) + core_facility = CharField(max_length=CHAR_FIELD_MEDIUM_LENGTH, read_only=True) + core_facility_id = IntegerField(read_only=True) user = CharField(max_length=CHAR_FIELD_MEDIUM_LENGTH, read_only=True) username = CharField(max_length=CHAR_FIELD_MEDIUM_LENGTH, read_only=True) user_id = IntegerField(read_only=True) start = DateTimeField(read_only=True) end = DateTimeField(read_only=True) - quantity = DecimalField(read_only=True, decimal_places=2, max_digits=8) + quantity = DecimalField(read_only=True, decimal_places=2, max_digits=14) validated = BooleanField(read_only=True) validated_by = CharField(read_only=True, source="validated_by.username", allow_null=True) waived = BooleanField(read_only=True) diff --git a/NEMO/templates/calendar/reservation_event_feed.html b/NEMO/templates/calendar/reservation_event_feed.html index f347053d9..9665ad2d4 100644 --- a/NEMO/templates/calendar/reservation_event_feed.html +++ b/NEMO/templates/calendar/reservation_event_feed.html @@ -10,29 +10,29 @@ "title": "{{ x.reservation_item.name|escapejs }} ({{ x.user.username|escapejs }})\n{{ x.title|escapejs }}", "color": "#84CD84", {% elif all_tools or all_areastools and x.tool %} - "title": "{{ x.reservation_item.name|escapejs }} ({{ x.user.username|escapejs }})\n{{ x.title|escapejs }}", + "title": "{{ x.reservation_item.name|escapejs }} ({{ x.user.username|escapejs }}){% if user.id == x.user_id or user|is_staff_on_tool:x.tool %}\n{{ x.title|escapejs }}{% endif %}", "color": "{{ x.reservation_item.tool_calendar_color|default:'#33ad33'|escapejs }}", {% elif all_areas or all_areastools and x.area %} - "title": "{{ x.reservation_item.name|escapejs }} ({{ x.user.username|escapejs }})\n{{ x.title|escapejs }}", + "title": "{{ x.reservation_item.name|escapejs }} ({{ x.user.username|escapejs }}){% if user.id == x.user_id or user|is_staff_on_tool:x.tool %}\n{{ x.title|escapejs }}{% endif %}", "color": "{{ x.reservation_item.area_calendar_color|default:'#84CD84'|escapejs }}", {% else %} {% if display_name %} - "title": "{{ x.reservation_item.name|escapejs }} ({{ x.user.username|escapejs }})\n{{ x.title|escapejs }}", + "title": "{{ x.reservation_item.name|escapejs }} ({{ x.user.username|escapejs }}){% if user.id == x.user_id or user|is_staff_on_tool:x.tool %}\n{{ x.title|escapejs }}{% endif %}", {% else %} - "title": "{{ x.title|default:x.user|escapejs }}{% if display_configuration %}\n{{ x.get_configuration_options_display|escapejs }}{% endif %}", + "title": "{% if user.id == x.user_id or user|is_staff_on_tool:x.tool %}{{ x.title|default:x.user|escapejs }}{% else %}{{ x.user|escapejs }}{% endif %}{% if display_configuration %}\n{{ x.get_configuration_options_display|escapejs }}{% endif %}", {% if display_configuration and x.get_configuration_options_colors %}"colors": ["{{ x.get_configuration_options_colors|join:'","' }}"],{% endif %} {% endif %} "color": "{% if x.tool %}{% if x.reservation_item.tool_calendar_color|lower == "#33ad33" %}#88B7CD{% else %}{{ x.reservation_item.tool_calendar_color|default:"#88B7CD"|escapejs }}{% endif %}{% else %}{{ x.reservation_item.area_calendar_color|default:"#88B7CD"|escapejs }}{% endif %}", - {% if x.user.id == user.id %}"own-reservation": true,{% endif %} + {% if x.user_id == user.id %}"own-reservation": true,{% endif %} {% endif %} - {% if x.creator.id == x.user.id %} + {% if x.creator.id == x.user_id %} "tooltip": "{{ x.reservation_item.name }} reservation for {{ x.user }}", {% else %} "tooltip": "{{ x.reservation_item.name }} reservation for {{ x.user }}, created by {{ x.creator }}", {% endif %} "id": "Reservation {{ x.id }}", {# The reservation creator or staff may edit the event: #} - {% if user.id == x.user.id or user.is_staff or user|is_staff_on_tool:x.tool %}"editable": true,{% endif %} + {% if user.id == x.user_id or user.is_staff or user|is_staff_on_tool:x.tool %}"editable": true,{% endif %} "start": "{{ x.start|date:"c" }}", "end": "{{ x.get_visual_end|date:"c" }}", "details_url": "{% url 'reservation_details' x.id %}" diff --git a/NEMO/templates/customizations/customizations_core_facility.html b/NEMO/templates/customizations/customizations_core_facility.html new file mode 100644 index 000000000..f3ca9917d --- /dev/null +++ b/NEMO/templates/customizations/customizations_core_facility.html @@ -0,0 +1,64 @@ +{% load custom_tags_and_filters %} +
+

Core facilities

+
+ {% csrf_token %} +
+ +
+ +
+
+ {% if errors.core_facility_external_id_name %} + {{ errors.core_facility_external_id_name.error }} + {% else %} + The name of the external identifier property for Core Facilities. + {% endif %} +
+
+
+ Core facility required +
+
+ +
+ +
+ +
+ +
+
+
+
+
{% button type="save" value="Save settings" %}
+
+
diff --git a/NEMO/templates/event_details/reservation_details.html b/NEMO/templates/event_details/reservation_details.html index 5eab9a917..df09399db 100644 --- a/NEMO/templates/event_details/reservation_details.html +++ b/NEMO/templates/event_details/reservation_details.html @@ -66,19 +66,14 @@ {% endif %} - {% if user.is_staff or user|is_staff_on_tool:reservation.tool %} + {% if user.is_staff or user|is_staff_on_tool:reservation.tool or user == reservation.user %}
- + diff --git a/NEMO/templates/maintenance/maintenance.html b/NEMO/templates/maintenance/maintenance.html index 928edeecb..a94953b5b 100644 --- a/NEMO/templates/maintenance/maintenance.html +++ b/NEMO/templates/maintenance/maintenance.html @@ -11,23 +11,23 @@

Maintenance

-
+
- +
- @@ -87,15 +87,27 @@

M

-
+
+
+ +
+ +
+
- + + @@ -112,6 +124,7 @@

M {% endif %}

+ @@ -191,6 +204,7 @@

M { const this_url = new URL(window.location.href); this_url.searchParams.set("tool_category", search_selection.name); + this_url.searchParams.set("tab", $("#tabs li.active a").attr("href").substring(1)); window.location.href = this_url.toString(); } @@ -203,7 +217,7 @@

M let row = $("#pending_tasks tr[data-task-id='" + active_row + "']"); row.click(); } - $("#tool_category_search") + $(".tool-category-search") .autocomplete('tool_categories', filter_tool_category, {% json_search_base_with_extra_fields tool_categories %}, true) .on("input", function () { @@ -216,7 +230,5 @@

M } $(on_load); - - {% endblock %} diff --git a/NEMO/templates/mobile/individual_reservation.html b/NEMO/templates/mobile/individual_reservation.html index 9dd7fba03..f1cca4e93 100644 --- a/NEMO/templates/mobile/individual_reservation.html +++ b/NEMO/templates/mobile/individual_reservation.html @@ -9,8 +9,10 @@ data-target="#extended_reservation_information_{{ reservation.id }}"> {% if personal_schedule %} {{ reservation.title|default:reservation.reservation_item }} - {% else %} + {% elif reservation.user_id == user.id or user|is_staff_on_tool:reservation.tool %} {{ reservation.title|default:reservation.user }} + {% else %} + {{ reservation.user }} {% endif %}
@@ -63,7 +65,7 @@ {% endif %} {# Allow the user to cancel the reservation if they have that privilege. #} {% if not reservation.missed and not reservation.cancelled %} - {% if reservation.user.id == user.id and reservation.has_not_ended or user|is_staff_on_tool:reservation.tool %} + {% if reservation.user_id == user.id and reservation.has_not_ended or user|is_staff_on_tool:reservation.tool %}
{# Vertical spacer #}
{% csrf_token %} diff --git a/NEMO/templates/staff_charges/choose_project.html b/NEMO/templates/staff_charges/choose_project.html index fd9b40dcb..ddd3a1955 100644 --- a/NEMO/templates/staff_charges/choose_project.html +++ b/NEMO/templates/staff_charges/choose_project.html @@ -9,6 +9,7 @@ Customer: {{ customer }}
+
{% if customer.active_project_count == 1 and customer.active_projects.0.allow_staff_charges %} diff --git a/NEMO/templates/staff_charges/new_staff_charge.html b/NEMO/templates/staff_charges/new_staff_charge.html index 674852704..851a90311 100644 --- a/NEMO/templates/staff_charges/new_staff_charge.html +++ b/NEMO/templates/staff_charges/new_staff_charge.html @@ -8,18 +8,45 @@ {% if error %}
{{ error }}
{% endif %}
- +
- +
+ {% if customizations|get_item:"core_facility_required_for_staff_charges" or core_facilities %} +
+ +
+ +
+
+ {% endif %} +
+
{% button type="save" value="Continue" icon="glyphicon-ok-circle" %}
+

Urgency Severity ToolCategoryTool categoryProblem category Created Resolved Description{{ task.tool.name }}{{ task.tool.category }} {{ task.problem_category|default_if_none:"" }} {{ task.creation_time }} {{ task.resolution_time }}