Repository: usnistgov/NEMO (language: python)

Issue:
[Feature Request] Add Date to Status Dashboard
The status/occupancy dashboard (located at /status_dashboard/) has a "Since" column that lists (for example "Thursday @ 6:20 PM" as the occupancy timeframe).

This makes it difficult (at a glance) for managers to know when someone just forgot to log out of a tool or area.

This request is to add the MM/DD date to the "Since" column, so it would read "Thursday 7/27 @ 6:20 PM." This way facility/lab/instrument managers can quickly locate someone who has been logged in too long (accidentally) and contact them to adjust their reservation.

The date format in MM/DD/YYYY would also be fine, but it seems unlikely that the year is necessary.
Fix django for CVE-2023-41164
See https://bugzilla.redhat.com/show_bug.cgi?id=2237258


Discussion / hints:
Thank you, this is something I have been thinking about as well, especially looking at the demo website where there is really no telling how long people have been logged in (I think the captain user has been logged in for years).

Since the current (hardcoded) format works with any locale/international format, we would want to keep it that way.
If someone uses NEMO with DD/MM as it is the case in other countries, we want to be careful to make this configurable.

I did some research, and I am thinking about using the Django "MONTH_DAY_FORMAT" preset format which can be customized in settings just like the other date formats.

The result would be "[DAY] [MONTH_DAY_FORMAT] @ [TIME_FORMAT]" and would be displayed in occupancy, tool status and jumbotron.

Looking into implementing this in the next version (4.7.0)



Fix patch:
diff --git a/NEMO/actions.py b/NEMO/actions.py
index 9bc44351..105843df 100644
--- a/NEMO/actions.py
+++ b/NEMO/actions.py
@@ -3,11 +3,26 @@
 from django.urls import reverse
 from django.utils.safestring import mark_safe
 
-from NEMO.models import Area, Interlock, Tool, User
+from NEMO.models import Area, Interlock, InterlockCard, Tool, User
+from NEMO.typing import QuerySetType
 from NEMO.views.access_requests import access_csv_export
 from NEMO.views.adjustment_requests import adjustments_csv_export
 
 
+@admin.action(description="Disable selected cards")
+def disable_selected_cards(model_admin, request, queryset: QuerySetType[InterlockCard]):
+	for interlock_card in queryset:
+		interlock_card.enabled = False
+		interlock_card.save(update_fields=["enabled"])
+
+
+@admin.action(description="Enable selected cards")
+def enable_selected_cards(model_admin, request, queryset: QuerySetType[InterlockCard]):
+	for interlock_card in queryset:
+		interlock_card.enabled = True
+		interlock_card.save(update_fields=["enabled"])
+
+
 @admin.action(description="Lock selected interlocks")
 def lock_selected_interlocks(model_admin, request, queryset):
 	for interlock in queryset:
@@ -79,6 +94,7 @@ def duplicate_tool_configuration(model_admin, request, queryset):
 				old_nonrequired_resources = tool.nonrequired_resource_set.all()
 				old_backup_users = tool.backup_owners.all()
 				old_superusers = tool.superusers.all()
+				old_reviewers = tool.adjustment_request_reviewers.all()
 				old_qualified_users = User.objects.filter(qualifications__id=tool.pk).distinct()
 				tool.pk = None
 				tool.interlock = None
@@ -93,6 +109,7 @@ def duplicate_tool_configuration(model_admin, request, queryset):
 				tool.nonrequired_resource_set.set(old_nonrequired_resources)
 				tool.backup_owners.set(old_backup_users)
 				tool.superusers.set(old_superusers)
+				tool.adjustment_request_reviewers.set(old_reviewers)
 				for user in old_qualified_users:
 					user.qualifications.add(tool)
 				messages.success(
diff --git a/NEMO/admin.py b/NEMO/admin.py
index 49687550..2b61766a 100644
--- a/NEMO/admin.py
+++ b/NEMO/admin.py
@@ -1,6 +1,4 @@
 import datetime
-import sys
-from json import loads
 
 from django import forms
 from django.contrib import admin
@@ -12,7 +10,6 @@
 from django.db.models import Q
 from django.db.models.fields.files import FieldFile
 from django.forms import BaseInlineFormSet
-from django.shortcuts import redirect
 from django.template.defaultfilters import linebreaksbr, urlencode
 from django.utils.safestring import mark_safe
 from mptt.admin import DraggableMPTTAdmin, MPTTAdminForm, TreeRelatedFieldListFilter
@@ -21,13 +18,16 @@
 	access_requests_export_csv,
 	adjustment_requests_export_csv,
 	create_next_interlock,
+	disable_selected_cards,
 	duplicate_tool_configuration,
+	enable_selected_cards,
 	lock_selected_interlocks,
 	rebuild_area_tree,
 	synchronize_with_tool_usage,
 	unlock_selected_interlocks,
 )
 from NEMO.forms import BuddyRequestForm, RecurringConsumableChargeForm, UserPreferencesForm
+from NEMO.mixins import ObjPermissionAdminMixin, ModelAdminRedirectMixin
 from NEMO.models import (
 	Account,
 	AccountType,
@@ -107,27 +107,14 @@
 	record_remote_many_to_many_changes_and_save,
 )
 from NEMO.utilities import admin_get_item, format_daterange
-from NEMO.views.constants import NEXT_PARAMETER_NAME
 from NEMO.views.customization import ProjectsAccountsCustomization
-from NEMO.widgets.dynamic_form import DynamicForm, PostUsageFloatFieldQuestion, PostUsageNumberFieldQuestion
-
-
-# Admin class to allow redirect after add or change
-class ModelAdminRedirect(admin.ModelAdmin):
-
-	def response_post_save_add(self, request, obj):
-		return self.response_redirect(request, super().response_post_save_add(request, obj))
-
-	def response_post_save_change(self, request, obj):
-		return self.response_redirect(request, super().response_post_save_change(request, obj))
-
-	def response_delete(self, request, obj_display, obj_id):
-		return self.response_redirect(request, super().response_delete(request, obj_display, obj_id))
-
-	def response_redirect(self, request, original_response):
-		if NEXT_PARAMETER_NAME in request.GET:
-			return redirect(request.GET[NEXT_PARAMETER_NAME])
-		return original_response
+from NEMO.widgets.dynamic_form import (
+	DynamicForm,
+	PostUsageFloatFieldQuestion,
+	PostUsageNumberFieldQuestion,
+	admin_render_dynamic_form_preview,
+	validate_dynamic_form_model,
+)
 
 
 # Formset to require at least one inline form
@@ -146,8 +133,8 @@ class Meta:
 		fields = "__all__"
 
 	class Media:
-		js = ("admin/tool/tool.js", "admin/questions_preview/questions_preview.js")
-		css = {"": ("admin/questions_preview/questions_preview.css",)}
+		js = ("admin/tool/tool.js", "admin/dynamic_form_preview/dynamic_form_preview.js")
+		css = {"": ("admin/dynamic_form_preview/dynamic_form_preview.css",)}
 
 	qualified_users = forms.ModelMultipleChoiceField(
 		queryset=User.objects.all(),
@@ -213,7 +200,7 @@ class ToolAdmin(admin.ModelAdmin):
 		"has_post_usage_questions",
 		"id",
 	)
-	filter_horizontal = ("_backup_owners", "_superusers")
+	filter_horizontal = ("_backup_owners", "_superusers", "_adjustment_request_reviewers")
 	search_fields = ("name", "_description", "_serial")
 	list_filter = ("visible", "_operational", "_category", "_location", ("_requires_area_access", admin.RelatedOnlyFieldListFilter))
 	readonly_fields = ("_post_usage_preview",)
@@ -249,6 +236,7 @@ class ToolAdmin(admin.ModelAdmin):
 				)
 			},
 		),
+		("Approval", {"fields": ("_adjustment_request_reviewers",)}),
 		("Reservation", {"fields": ("_reservation_horizon", "_missed_reservation_threshold")}),
 		(
 			"Usage policy",
@@ -281,18 +269,12 @@ class ToolAdmin(admin.ModelAdmin):
 		("Dependencies", {"fields": ("required_resources", "nonrequired_resources")}),
 	)
 
-	@admin.display(description="Questions", ordering="post_usage_questions", boolean=True)
+	@admin.display(description="Questions", ordering="_post_usage_questions", boolean=True)
 	def has_post_usage_questions(self, obj: Tool):
 		return True if obj.post_usage_questions else False
 
 	def _post_usage_preview(self, obj):
-		if obj.id:
-			form_validity_div = '<div id="form_validity"></div>' if obj.post_usage_questions else ""
-			return mark_safe(
-				'<div class="questions_preview">{}{}</div><div class="help questions_preview_help">Save form to preview post usage questions</div>'.format(
-					DynamicForm(obj.post_usage_questions).render("tool_usage_group_question", obj.id), form_validity_div
-				)
-			)
+		return admin_render_dynamic_form_preview(obj.post_usage_questions, "tool_usage_group_question", obj.id)
 
 	def formfield_for_foreignkey(self, db_field, request, **kwargs):
 		""" We only want non children tool to be eligible as parents """
@@ -350,12 +332,13 @@ class AreaAdmin(DraggableMPTTAdmin):
 		"buddy_system_allowed",
 		"id",
 	)
+	filter_horizontal = ["adjustment_request_reviewers", "access_request_reviewers"]
 	fieldsets = (
 		(None, {"fields": ("name", "parent_area", "category", "reservation_email", "abuse_email")}),
 		("Additional Information", {"fields": ("area_calendar_color",)}),
 		(
 			"Area access",
-			{"fields": ("requires_reservation", "logout_grace_period", "welcome_message", "buddy_system_allowed")},
+			{"fields": ("requires_reservation", "logout_grace_period", "welcome_message", "auto_logout_time", "buddy_system_allowed")},
 		),
 		(
 			"Occupancy",
@@ -368,6 +351,7 @@ class AreaAdmin(DraggableMPTTAdmin):
 				)
 			},
 		),
+		("Approval", {"fields": ("adjustment_request_reviewers", "access_request_reviewers")}),
 		("Reservation", {"fields": ("reservation_horizon", "missed_reservation_threshold")}),
 		(
 			"Policy",
@@ -411,7 +395,7 @@ def save_model(self, request, obj: Area, form, change):
 
 
 @register(TrainingSession)
-class TrainingSessionAdmin(admin.ModelAdmin):
+class TrainingSessionAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
 	list_display = ("id", "trainer", "trainee", "tool", "project", "type", "date", "duration", "qualified")
 	list_filter = ("qualified", "date", "type", ("tool", admin.RelatedOnlyFieldListFilter), ("project", admin.RelatedOnlyFieldListFilter), ("trainer", admin.RelatedOnlyFieldListFilter), ("trainee", admin.RelatedOnlyFieldListFilter))
 	date_hierarchy = "date"
@@ -424,14 +408,14 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs):
 
 
 @register(StaffCharge)
-class StaffChargeAdmin(ModelAdminRedirect):
+class StaffChargeAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
 	list_display = ("id", "staff_member", "customer", "start", "end")
 	list_filter = ("start", ("customer", admin.RelatedOnlyFieldListFilter), ("staff_member", admin.RelatedOnlyFieldListFilter))
 	date_hierarchy = "start"
 
 
 @register(AreaAccessRecord)
-class AreaAccessRecordAdmin(ModelAdminRedirect):
+class AreaAccessRecordAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
 	list_display = ("id", "customer", "area", "project", "start", "end")
 	list_filter = (("area", TreeRelatedFieldListFilter), "start")
 	date_hierarchy = "start"
@@ -547,7 +531,7 @@ def save_model(self, request, obj, form, change):
 
 
 @register(Reservation)
-class ReservationAdmin(ModelAdminRedirect):
+class ReservationAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
 	list_display = (
 		"id",
 		"user",
@@ -573,8 +557,8 @@ class Meta:
 		fields = "__all__"
 
 	class Media:
-		js = ("admin/reservation_questions/reservation_questions.js", "admin/questions_preview/questions_preview.js")
-		css = {"": ("admin/questions_preview/questions_preview.css",)}
+		js = ("admin/dynamic_form_preview/dynamic_form_preview.js",)
+		css = {"": ("admin/dynamic_form_preview/dynamic_form_preview.css",)}
 
 	def clean(self):
 		cleaned_data = super().clean()
@@ -596,18 +580,9 @@ def clean(self):
 			)
 		# Validate reservation_questions JSON format
 		if reservation_questions:
-			try:
-				loads(reservation_questions)
-			except ValueError:
-				self.add_error("questions", "This field needs to be a valid JSON string")
-			try:
-				dynamic_form = DynamicForm(reservation_questions)
-				dynamic_form.validate("reservation_group_question", self.instance.id)
-			except KeyError as e:
-				self.add_error("questions", f"{e} property is required")
-			except Exception:
-				error_info = sys.exc_info()
-				self.add_error("questions", error_info[0].__name__ + ": " + str(error_info[1]))
+			errors = validate_dynamic_form_model(reservation_questions, "reservation_group_question", self.instance.id)
+			for error in errors:
+				self.add_error("questions", error)
 		return cleaned_data
 
 
@@ -635,23 +610,11 @@ class ReservationQuestionsAdmin(admin.ModelAdmin):
 	)
 
 	def questions_preview(self, obj):
-		form_validity_div = ""
-		rendered_form = ""
-		try:
-			rendered_form = DynamicForm(obj.questions).render("reservation_group_question", obj.id)
-			if obj.questions:
-				form_validity_div = '<div id="form_validity"></div>'
-		except:
-			pass
-		return mark_safe(
-			'<div class="questions_preview">{}{}</div><div class="help questions_preview_help">Save form to preview reservation questions</div>'.format(
-				rendered_form, form_validity_div
-			)
-		)
+		return admin_render_dynamic_form_preview(obj.questions, "reservation_group_question", obj.id)
 
 
 @register(UsageEvent)
-class UsageEventAdmin(ModelAdminRedirect):
+class UsageEventAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
 	list_display = ("id", "tool", "user", "operator", "project", "start", "end", "duration", "remote_work")
 	list_filter = ("remote_work", "start", "end", ("tool", admin.RelatedOnlyFieldListFilter))
 	date_hierarchy = "start"
@@ -659,8 +622,8 @@ class UsageEventAdmin(ModelAdminRedirect):
 
 @register(Consumable)
 class ConsumableAdmin(admin.ModelAdmin):
-	list_display = ("name", "quantity", "category", "visible", "reusable", "reminder_threshold", "reminder_email", "id")
-	list_filter = ("visible", ("category", admin.RelatedOnlyFieldListFilter), "reusable")
+	list_display = ("name", "quantity", "category", "visible", "reusable", "allow_self_checkout", "reminder_threshold", "reminder_email", "id")
+	list_filter = ("visible", ("category", admin.RelatedOnlyFieldListFilter), "reusable", "allow_self_checkout")
 	search_fields = ("name",)
 	readonly_fields = ("reminder_threshold_reached",)
 
@@ -671,7 +634,7 @@ class ConsumableCategoryAdmin(admin.ModelAdmin):
 
 
 @register(ConsumableWithdraw)
-class ConsumableWithdrawAdmin(admin.ModelAdmin):
+class ConsumableWithdrawAdmin(ObjPermissionAdminMixin, ModelAdminRedirectMixin, admin.ModelAdmin):
 	list_display = ("id", "customer", "merchant", "consumable", "quantity", "project", "date")
 	list_filter = ("date", ("consumable", admin.RelatedOnlyFieldListFilter))
 	date_hierarchy = "date"
@@ -709,6 +672,7 @@ def clean(self):
 class InterlockCardAdmin(admin.ModelAdmin):
 	form = InterlockCardAdminForm
 	list_display = ("name", "enabled", "server", "port", "number", "category", "even_port", "odd_port")
+	actions = [disable_selected_cards, enable_selected_cards]
 
 
 class InterlockAdminForm(forms.ModelForm):
@@ -872,7 +836,7 @@ class UserTypeAdmin(admin.ModelAdmin):
 class UserPreferencesAdmin(admin.ModelAdmin):
 	list_display = ("user",)
 	search_fields = ["user_preferences__user__first_name", "user_preferences__user__last_name", "user_preferences__user__username"]
-	filter_horizontal = ["tool_freed_time_notifications", "tool_adjustment_notifications", "area_adjustment_notifications"]
+	filter_horizontal = ["tool_freed_time_notifications", "tool_task_notifications"]
 	form = UserPreferencesForm
 
 
diff --git a/NEMO/apps/area_access/templates/area_access/base.html b/NEMO/apps/area_access/templates/area_access/base.html
index 4af3be0a..53b6c631 100644
--- a/NEMO/apps/area_access/templates/area_access/base.html
+++ b/NEMO/apps/area_access/templates/area_access/base.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
 	<title>{% block title %}{% endblock %}</title>
 	{% load static %}
diff --git a/NEMO/apps/contracts/admin.py b/NEMO/apps/contracts/admin.py
index d9ce59af..748be1cd 100644
--- a/NEMO/apps/contracts/admin.py
+++ b/NEMO/apps/contracts/admin.py
@@ -1,7 +1,7 @@
 from django import forms
 from django.contrib import admin
 
-from NEMO.admin import ModelAdminRedirect
+from NEMO.mixins import ModelAdminRedirectMixin
 from NEMO.apps.contracts.customization import ContractsCustomization
 from NEMO.apps.contracts.models import (
     ContractorAgreement,
@@ -34,7 +34,7 @@ class ProcurementDocumentsInline(admin.TabularInline):
 
 
 @admin.register(Procurement)
-class ProcurementAdmin(ModelAdminRedirect):
+class ProcurementAdmin(ModelAdminRedirectMixin, admin.ModelAdmin):
     list_display = ["name", "submitted_date", "award_date", "contract_number", "requisition_number", "cost"]
     list_filter = ["submitted_date", "award_date", "cost"]
     actions = [procurements_export_csv]
@@ -45,7 +45,7 @@ def get_queryset(self, request):
 
 
 @admin.register(ServiceContract)
-class ServiceContractAdmin(ModelAdminRedirect):
+class ServiceContractAdmin(ModelAdminRedirectMixin, admin.ModelAdmin):
     list_display = [
         "get_display_name",
         "get_display_current_year",
@@ -92,7 +92,7 @@ def __init__(self, *args, **kwargs):
 
 
 @admin.register(ContractorAgreement)
-class ContractorAgreementAdmin(ModelAdminRedirect):
+class ContractorAgreementAdmin(ModelAdminRedirectMixin, admin.ModelAdmin):
     list_display = ["name", "get_contract_name", "get_contract_number", "start", "end", "reminder_date"]
     list_filter = ["start", "end", "reminder_date", "contract"]
     actions = [contractor_agreements_export_csv]
diff --git a/NEMO/apps/contracts/migrations/0002_increase_document_path_length.py b/NEMO/apps/contracts/migrations/0002_increase_document_path_length.py
new file mode 100644
index 00000000..453efba6
--- /dev/null
+++ b/NEMO/apps/contracts/migrations/0002_increase_document_path_length.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.2.20 on 2023-08-30 21:49
+
+from django.db import migrations, models
+
+import NEMO.utilities
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contracts', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='contractoragreementdocuments',
+            name='document',
+            field=models.FileField(blank=True, max_length=255, null=True, upload_to=NEMO.utilities.document_filename_upload, verbose_name='Document'),
+        ),
+        migrations.AlterField(
+            model_name='procurementdocuments',
+            name='document',
+            field=models.FileField(blank=True, max_length=255, null=True, upload_to=NEMO.utilities.document_filename_upload, verbose_name='Document'),
+        ),
+    ]
diff --git a/NEMO/apps/contracts/templates/contracts/contractors.html b/NEMO/apps/contracts/templates/contracts/contractors.html
index 62ed50fb..c184af1d 100644
--- a/NEMO/apps/contracts/templates/contracts/contractors.html
+++ b/NEMO/apps/contracts/templates/contracts/contractors.html
@@ -44,14 +44,14 @@
                                 {% button icon="glyphicon-list-alt" size="small" type="export" value="Docs" onclick="document_view(this.form);" %}
                             </form>
                         {% endif %}
-                        {% if perms.contracts.change_contractoragreement %}
-                            {% url "contractor_agreement_renew" contractor_agreement.id as renew_contractor_agreement_url %}
-                            {% button icon="glyphicon-refresh" size="small" type="default" value="Renew" url=renew_contractor_agreement_url %}
-                        {% endif %}
                         {% admin_edit_url contractor_agreement request.path as change_contractors_url %}
                         {% if change_contractors_url %}
                             {% button size="small" type="edit" value="Edit" url=change_contractors_url %}
                         {% endif %}
+                        {% if perms.contracts.change_contractoragreement %}
+                            {% url "contractor_agreement_renew" contractor_agreement.id as renew_contractor_agreement_url %}
+                            {% button icon="glyphicon-refresh" size="small" type="default" value="Renew" url=renew_contractor_agreement_url %}
+                        {% endif %}
                     </td>
                 </tr>
             {% endfor %}
diff --git a/NEMO/apps/contracts/templates/contracts/service_contracts.html b/NEMO/apps/contracts/templates/contracts/service_contracts.html
index c5b26fc9..0ac37aca 100644
--- a/NEMO/apps/contracts/templates/contracts/service_contracts.html
+++ b/NEMO/apps/contracts/templates/contracts/service_contracts.html
@@ -14,8 +14,7 @@
                 <th class="text-center">Year</th>
                 <th>{% include 'pagination/pagination_column.html' with order_by='submitted_date' name='Submitted' %}</th>
                 <th>{% include 'pagination/pagination_column.html' with order_by='award_date' name='Awarded' %}</th>
-                <th>{% include 'pagination/pagination_column.html' with order_by='contract_number' name='Contract number' %}</th>
-                <th>{% include 'pagination/pagination_column.html' with order_by='requisition_number' name='Requisition number' %}</th>
+                <th>{% include 'pagination/pagination_column.html' with order_by='contract_number' name='Contract/Requisition number' %}</th>
                 <th>{% include 'pagination/pagination_column.html' with order_by='start' name='Start' %}</th>
                 <th>{% include 'pagination/pagination_column.html' with order_by='natural_end' name='End' %}</th>
                 <th>{% include 'pagination/pagination_column.html' with order_by='reminder_date' name='Reminder' %}</th>
@@ -30,8 +29,12 @@
                     <td class="text-center text-nowrap">{{ service_contract.display_current_year }}</td>
                     <td>{{ service_contract.submitted_date|date:"SHORT_DATE_FORMAT" }}</td>
                     <td>{{ service_contract.award_date|date:"SHORT_DATE_FORMAT" }}</td>
-                    <td>{{ service_contract.contract_number|default_if_none:"" }}</td>
-                    <td>{{ service_contract.requisition_number|default_if_none:"" }}</td>
+                    <td>
+                        {{ service_contract.contract_number|default_if_none:"" }}
+                        {% if service_contract.requisition_number %}
+                            <br>{{ service_contract.requisition_number|default_if_none:"" }}
+                        {% endif %}
+                    </td>
                     <td>{{ service_contract.start|date:"SHORT_DATE_FORMAT" }}</td>
                     <td>{{ service_contract.end|date:"SHORT_DATE_FORMAT" }}</td>
                     <td>{{ service_contract.reminder_date|date:"SHORT_DATE_FORMAT" }}</td>
@@ -52,14 +55,14 @@
                                 {% button icon="glyphicon-list-alt" size="small" type="export" value="Docs" onclick="document_view(this.form);" %}
                             </form>
                         {% endif %}
-                        {% if perms.contracts.change_servicecontract %}
-                            {% url "service_contract_renew" service_contract.id as renew_service_contract_url %}
-                            {% button icon="glyphicon-refresh" size="small" type="default" value="Renew" url=renew_service_contract_url %}
-                        {% endif %}
                         {% admin_edit_url service_contract request.path as change_servicecontract_url %}
                         {% if change_servicecontract_url %}
                             {% button size="small" type="edit" value="Edit" url=change_servicecontract_url %}
                         {% endif %}
+                        {% if perms.contracts.change_servicecontract %}
+                            {% url "service_contract_renew" service_contract.id as renew_service_contract_url %}
+                            {% button icon="glyphicon-refresh" size="small" type="default" value="Renew" url=renew_service_contract_url %}
+                        {% endif %}
                     </td>
                 </tr>
             {% endfor %}
diff --git a/NEMO/apps/kiosk/templates/kiosk/choices.html b/NEMO/apps/kiosk/templates/kiosk/choices.html
index e9c3eeb8..32c785ef 100644
--- a/NEMO/apps/kiosk/templates/kiosk/choices.html
+++ b/NEMO/apps/kiosk/templates/kiosk/choices.html
@@ -22,7 +22,7 @@ <h1>Current {{ facility_name }} usage</h1>
 			<div class="col-xs-6">
 				<div class="alert alert-info" onclick="tool_information('{% url 'kiosk_tool_information' u.tool.tool_or_parent_id customer.id 'back_to_start' %}')">
 					<span style="font-size:x-large">{{ u.tool }}</span><br>
-					You are using this tool for the project named <strong>{{ u.project }}</strong> since <strong>{{ u.start|date:"l @ " }}{{ u.start|time }}</strong>
+					You are using this tool for the project named <strong>{{ u.project }}</strong> since <strong>{{ u.start|date:"MONTH_DAY_FORMAT" }} @ {{ u.start|time }}</strong>
 				</div>
 			</div>
 		{% endfor %}
diff --git a/NEMO/apps/kiosk/templates/kiosk/individual_reservation.html b/NEMO/apps/kiosk/templates/kiosk/individual_reservation.html
index 02ca1a83..19f57844 100644
--- a/NEMO/apps/kiosk/templates/kiosk/individual_reservation.html
+++ b/NEMO/apps/kiosk/templates/kiosk/individual_reservation.html
@@ -30,7 +30,7 @@
             Additional information:<br>
             {{ reservation.additional_information|linebreaksbr }}
         {% endif %}
-        {# Allow the user to cancel the reservation if they have that priviledge. #}
+        {# Allow the user to cancel the reservation if they have that privilege. #}
         {% if not reservation.missed and not reservation.cancelled %}
             {% if reservation.user.id == customer.id and reservation.has_not_ended %}
                 <div style="height:10px">{# Vertical spacer #}</div>
diff --git a/NEMO/apps/kiosk/templates/kiosk/kiosk.html b/NEMO/apps/kiosk/templates/kiosk/kiosk.html
index cf63d60a..6ce0af34 100644
--- a/NEMO/apps/kiosk/templates/kiosk/kiosk.html
+++ b/NEMO/apps/kiosk/templates/kiosk/kiosk.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
 	<title>Kiosk</title>
 	{% load static %}
diff --git a/NEMO/apps/kiosk/views.py b/NEMO/apps/kiosk/views.py
index aa2e46d8..52bd7135 100644
--- a/NEMO/apps/kiosk/views.py
+++ b/NEMO/apps/kiosk/views.py
@@ -118,13 +118,7 @@ def do_disable_tool(request, tool_id):
 			return render(request, "kiosk/acknowledgement.html", dictionary)
 
 	try:
-		dynamic_form.charge_for_consumables(
-			current_usage_event.user,
-			current_usage_event.operator,
-			current_usage_event.project,
-			current_usage_event.run_data,
-			request,
-		)
+		dynamic_form.charge_for_consumables(current_usage_event, request)
 	except Exception as e:
 		dictionary = {"message": str(e), "delay": 10}
 		return render(request, "kiosk/acknowledgement.html", dictionary)
diff --git a/NEMO/apps/sensors/admin.py b/NEMO/apps/sensors/admin.py
index 28259cc6..5d545d51 100644
--- a/NEMO/apps/sensors/admin.py
+++ b/NEMO/apps/sensors/admin.py
@@ -17,9 +17,10 @@
 	SensorCategory,
 	SensorData,
 )
+from NEMO.typing import QuerySetType
 
 
-def duplicate_sensor_configuration(model_admin, request, queryset):
+def duplicate_sensor_configuration(model_admin, request, queryset: QuerySetType[Sensor]):
 	for sensor in queryset:
 		original_name = sensor.name
 		new_name = "Copy of " + sensor.name
@@ -52,7 +53,7 @@ def duplicate_sensor_configuration(model_admin, request, queryset):
 			)
 
 
-def read_selected_sensors(model_admin, request, queryset):
+def read_selected_sensors(model_admin, request, queryset: QuerySetType[Sensor]):
 	for sensor in queryset:
 		try:
 			response = sensor.read_data(raise_exception=True)
@@ -64,6 +65,48 @@ def read_selected_sensors(model_admin, request, queryset):
 			messages.error(request, f"{sensor} data could not be read due to the following error: {str(error)}")
 
 
+@admin.action(description="Hide selected sensors")
+def hide_selected_sensors(model_admin, request, queryset: QuerySetType[Sensor]):
+	for sensor in queryset:
+		sensor.visible = False
+		sensor.save(update_fields=["visible"])
+
+
+@admin.action(description="Show selected sensors")
+def show_selected_sensors(model_admin, request, queryset: QuerySetType[Sensor]):
+	for sensor in queryset:
+		sensor.visible = True
+		sensor.save(update_fields=["visible"])
+
+
+@admin.action(description="Disable selected alerts")
+def disable_selected_alerts(model_admin, request, queryset: QuerySetType[SensorAlertEmail]):
+	for sensor_alert in queryset:
+		sensor_alert.enabled = False
+		sensor_alert.save(update_fields=["enabled"])
+
+
+@admin.action(description="Enable selected alerts")
+def enable_selected_alerts(model_admin, request, queryset: QuerySetType[SensorAlertEmail]):
+	for sensor_alert in queryset:
+		sensor_alert.enabled = True
+		sensor_alert.save(update_fields=["enabled"])
+
+
+@admin.action(description="Disable selected cards")
+def disable_selected_cards(model_admin, request, queryset: QuerySetType[SensorCard]):
+	for sensor_card in queryset:
+		sensor_card.enabled = False
+		sensor_card.save(update_fields=["enabled"])
+
+
+@admin.action(description="Enable selected cards")
+def enable_selected_cards(model_admin, request, queryset: QuerySetType[SensorCard]):
+	for sensor_card in queryset:
+		sensor_card.enabled = True
+		sensor_card.save(update_fields=["enabled"])
+
+
 class SensorCardAdminForm(forms.ModelForm):
 	class Meta:
 		model = SensorCard
@@ -85,6 +128,7 @@ def clean(self):
 class SensorCardAdmin(admin.ModelAdmin):
 	form = SensorCardAdminForm
 	list_display = ("name", "enabled", "server", "port", "category")
+	actions = [disable_selected_cards, enable_selected_cards]
 
 
 class SensorAdminForm(forms.ModelForm):
@@ -176,7 +220,7 @@ class SensorAdmin(admin.ModelAdmin):
 		("sensor_card", admin.RelatedOnlyFieldListFilter),
 		("sensor_category", admin.RelatedOnlyFieldListFilter),
 	)
-	actions = [duplicate_sensor_configuration, read_selected_sensors]
+	actions = [duplicate_sensor_configuration, read_selected_sensors, hide_selected_sensors, show_selected_sensors]
 
 	@display(boolean=True, ordering="sensor_card__enabled", description="Card Enabled")
 	def get_card_enabled(self, obj: Sensor):
@@ -226,6 +270,7 @@ def get_display_value(self, obj: SensorData):
 @register(SensorAlertEmail)
 class SensorAlertEmailAdmin(admin.ModelAdmin):
 	list_display = ("sensor", "enabled", "trigger_condition", "trigger_no_data", "additional_emails", "triggered_on")
+	actions = [disable_selected_alerts, enable_selected_alerts]
 
 
 @register(SensorAlertLog)
diff --git a/NEMO/apps/sensors/management/commands/manage_sensor_data.py b/NEMO/apps/sensors/management/commands/manage_sensor_data.py
index 5bdb44a7..ea91134d 100644
--- a/NEMO/apps/sensors/management/commands/manage_sensor_data.py
+++ b/NEMO/apps/sensors/management/commands/manage_sensor_data.py
@@ -7,4 +7,4 @@ class Command(BaseCommand):
 	help = "Run every minute to read and manage sensors data."
 
 	def handle(self, *args, **options):
-		do_manage_sensor_data(asynchronous=False)
+		do_manage_sensor_data()
diff --git a/NEMO/apps/sensors/models.py b/NEMO/apps/sensors/models.py
index 3e224657..f4b646a3 100644
--- a/NEMO/apps/sensors/models.py
+++ b/NEMO/apps/sensors/models.py
@@ -14,6 +14,7 @@
 
 from NEMO.apps.sensors.customizations import SensorCustomization
 from NEMO.apps.sensors.evaluators import evaluate_boolean_expression
+from NEMO.decorators import postpone
 from NEMO.fields import MultiEmailField
 from NEMO.models import BaseModel, InterlockCard
 from NEMO.typing import QuerySetType
@@ -124,6 +125,10 @@ class Sensor(BaseModel):
 	def card(self):
 		return self.sensor_card or self.interlock_card
 
+	@postpone
+	def read_data_async(self, raise_exception=False):
+		return self.read_data(raise_exception)
+
 	def read_data(self, raise_exception=False):
 		from NEMO.apps.sensors import sensors
 
diff --git a/NEMO/apps/sensors/views.py b/NEMO/apps/sensors/views.py
index 1f177f9a..16b4d590 100644
--- a/NEMO/apps/sensors/views.py
+++ b/NEMO/apps/sensors/views.py
@@ -6,12 +6,11 @@
 from django.http import HttpResponse, JsonResponse
 from django.shortcuts import get_object_or_404, render
 from django.utils import timezone
-from django.utils.text import slugify
 from django.views.decorators.http import require_GET
 
 from NEMO.apps.sensors.customizations import SensorCustomization
 from NEMO.apps.sensors.models import Sensor, SensorAlertLog, SensorCategory, SensorData
-from NEMO.decorators import disable_session_expiry_refresh, postpone, staff_member_required
+from NEMO.decorators import disable_session_expiry_refresh, staff_member_required
 from NEMO.typing import QuerySetType
 from NEMO.utilities import (
 	BasicDisplayTable,
@@ -19,6 +18,7 @@
 	export_format_datetime,
 	extract_times,
 	format_datetime,
+	slugify_underscore,
 )
 
 
@@ -77,7 +77,7 @@ def export_sensor_data(request, sensor_id):
 			}
 		)
 	response = table_result.to_csv()
-	sensor_name = slugify(sensor.name).replace("-", "_")
+	sensor_name = slugify_underscore(sensor.name)
 	filename = f"{sensor_name}_data_{export_format_datetime(start)}_to_{export_format_datetime(end)}.csv"
 	response["Content-Disposition"] = f'attachment; filename="{filename}"'
 	return response
@@ -142,10 +142,10 @@ def manage_sensor_data(request):
 	return do_manage_sensor_data()
 
 
-def do_manage_sensor_data(asynchronous=True):
+def do_manage_sensor_data():
 	minute_of_the_day = floor((timezone.now() - beginning_of_the_day(timezone.now())).total_seconds() / 60)
 	# Read data for each sensor at the minute interval set
 	for sensor in Sensor.objects.exclude(read_frequency=0):
 		if minute_of_the_day % sensor.read_frequency == 0:
-			postpone(sensor.read_data)() if asynchronous else sensor.read_data()
+			sensor.read_data_async()
 	return HttpResponse()
diff --git a/NEMO/decorators.py b/NEMO/decorators.py
index a5ce831d..2cc2c82c 100644
--- a/NEMO/decorators.py
+++ b/NEMO/decorators.py
@@ -1,11 +1,13 @@
 import importlib
 import inspect
+import sys
 from functools import wraps
 from threading import Lock, RLock, Thread
 
 from django.contrib.auth import REDIRECT_FIELD_NAME
 from django.contrib.auth.decorators import user_passes_test
-from django.utils.text import slugify
+
+from NEMO.utilities import slugify_underscore
 
 
 def disable_session_expiry_refresh(f):
@@ -25,11 +27,18 @@ def disable_session_expiry_refresh(f):
 
 # Use this decorator on a function to make a call to that function asynchronously
 # The function will be run in a separate thread, and the current execution will continue
+# The function will be run synchronously in the case of a management command (excluding runserver) since management
+# commands exit without waiting for threads to finish
 def postpone(function):
 	def decorator(*arguments, **named_arguments):
-		t = Thread(target=function, args=arguments, kwargs=named_arguments)
-		t.daemon = True
-		t.start()
+		is_management_command = ("django-admin" in sys.argv[0] or "manage" in sys.argv[0] or "django/__main__.py" in sys.argv[0])
+		is_runserver = "runserver" in sys.argv
+		if is_management_command and not is_runserver:
+			return function(*arguments, **named_arguments)
+		else:
+			t = Thread(target=function, args=arguments, kwargs=named_arguments)
+			t.daemon = True
+			t.start()
 
 	return decorator
 
@@ -42,7 +51,7 @@ def synchronized(method_argument=""):
 	def inner(function):
 		def decorator(*args, **kwargs):
 			func_args = inspect.signature(function).bind(*args, **kwargs).arguments
-			attribute_value = slugify(str(func_args.get(method_argument, ""))).replace("-", "_")
+			attribute_value = slugify_underscore(str(func_args.get(method_argument, "")))
 			lock_name = "__" + function.__name__ + "_lock_" + attribute_value + "__"
 			lock: RLock = vars(function).get(lock_name, None)
 			if lock is None:
diff --git a/NEMO/forms.py b/NEMO/forms.py
index 0d5f51f3..1139c462 100644
--- a/NEMO/forms.py
+++ b/NEMO/forms.py
@@ -51,7 +51,6 @@ class Meta:
 		model = User
 		exclude = [
 			"is_staff",
-			"is_service_personnel",
 			"is_technician",
 			"is_facility_manager",
 			"is_superuser",
@@ -344,7 +343,7 @@ class EmailBroadcastForm(Form):
 	copy_me = BooleanField(required=False, initial=True)
 
 	audience = ChoiceField(
-		choices=[("tool", "tool"), ("project", "project"), ("account", "account"), ("area", "area"), ("user", "user")]
+		choices=[("tool", "tool"), ("tool-reservation", "tool-reservation"), ("project", "project"), ("account", "account"), ("area", "area"), ("user", "user")]
 	)
 	selection = CharField(required=False)
 	no_type = BooleanField(initial=False, required=False)
@@ -387,8 +386,6 @@ class Meta:
 	def __init__(self, *args, **kwargs):
 		super().__init__(*args, **kwargs)
 		self.fields["tool_freed_time_notifications"].queryset = Tool.objects.filter(visible=True, parent_tool__isnull=True)
-		self.fields["tool_adjustment_notifications"].queryset = Tool.objects.filter(visible=True, parent_tool__isnull=True)
-		self.fields["area_adjustment_notifications"].queryset = Area.objects.filter(area_children_set__isnull=True)
 		self.fields["tool_task_notifications"].queryset = Tool.objects.filter(visible=True, parent_tool__isnull=True)
 
 	def clean_recurring_charges_reminder_days(self):
diff --git a/NEMO/interlocks.py b/NEMO/interlocks.py
index 29edd6e9..8af8cd2c 100644
--- a/NEMO/interlocks.py
+++ b/NEMO/interlocks.py
@@ -263,9 +263,12 @@ class ProXrInterlock(Interlock):
 	def clean_interlock(self, interlock_form: InterlockAdminForm):
 		"""Validates NEMO interlock configuration."""
 		channel = interlock_form.cleaned_data["channel"]
+		bank = interlock_form.cleaned_data["unit_id"]
 		error = {}
-		if channel not in range(1, 9):
-			error["channel"] = _("Relay must be 1-8.")
+		if bank is not None and bank not in range(0, 33):
+			error["unit_id"] = _("Bank must be 0-32. Use 0 to trigger all banks.")
+		if channel not in range(0, 9):
+			error["channel"] = _("Relay must be 0-8. Use 0 to trigger all relays.")
 		if error:
 			raise ValidationError(error)
 
@@ -283,13 +286,18 @@ def _send_bytes(self, relay_socket, proxrcmd):
 		# only the last byte of the response is important
 		return relay_socket.recv(64)[-1]
 
-	def _get_state(self, relay_socket, interlock_channel):
+	def _get_state(self, relay_socket, interlock_channel, interlock_bank):
 		"""
 		Returns current NEMO state of the relay.
 		Argument relay_socket is a connected socket object.
 		Argument interlock_channel is the NEMO interlock.channel.
+		Argument interlock_bank is the NEMO interlock.unit_id.
 		"""
-		state = self._send_bytes(relay_socket, (254, 115 + interlock_channel, 1))
+		# We cannot read bank 0 since it means all banks, so check the first one
+		read_bank = interlock_bank if interlock_bank != 0 else 1
+		# We cannot read relay 0 since it means all relays, so check the first one
+		read_channel = interlock_channel if interlock_channel != 0 else 1
+		state = self._send_bytes(relay_socket, (254, 115 + read_channel, read_bank))
 		if state == self.PXR_RELAY_OFF:
 			return Interlock_model.State.LOCKED
 		elif state == self.PXR_RELAY_ON:
@@ -300,16 +308,20 @@ def _get_state(self, relay_socket, interlock_channel):
 	def _send_command(self, interlock: Interlock_model, command_type: Interlock_model.State) -> Interlock_model.State:
 		"""Returns and sets NEMO locked/unlocked state."""
 		state = Interlock_model.State.UNKNOWN
+		# Backward compatibility, no bank means bank 1
+		bank = interlock.unit_id if interlock.unit_id is not None else 1
 		try:
 			with socket.create_connection((interlock.card.server, interlock.card.port), 10) as relay_socket:
 				if command_type == Interlock_model.State.LOCKED:
 					# turn the interlock channel off
-					self._send_bytes(relay_socket, (254, 99 + interlock.channel, 1))
-					state = self._get_state(relay_socket, interlock.channel)
+					off_command = (99 + interlock.channel) if interlock.channel != 0 else 129
+					self._send_bytes(relay_socket, (254, off_command, bank))
+					state = self._get_state(relay_socket, interlock.channel, bank)
 				elif command_type == Interlock_model.State.UNLOCKED:
 					# turn the interlock channel on
-					self._send_bytes(relay_socket, (254, 107 + interlock.channel, 1))
-					state = self._get_state(relay_socket, interlock.channel)
+					on_command = (107 + interlock.channel) if interlock.channel != 0 else 130
+					self._send_bytes(relay_socket, (254, on_command, bank))
+					state = self._get_state(relay_socket, interlock.channel, bank)
 		except Exception as error:
 			raise InterlockError(interlock=interlock, msg="Communication error: " + str(error))
 		return state
@@ -335,15 +347,15 @@ def _send_command(self, interlock: Interlock_model, command_type: Interlock_mode
 		state = Interlock_model.State.UNKNOWN
 		try:
 			if command_type == Interlock_model.State.LOCKED:
-				state = self.setRelayState(interlock, self.WEB_RELAY_OFF)
+				state = self.set_relay_state(interlock, self.WEB_RELAY_OFF)
 			elif command_type == Interlock_model.State.UNLOCKED:
-				state = self.setRelayState(interlock, self.WEB_RELAY_ON)
+				state = self.set_relay_state(interlock, self.WEB_RELAY_ON)
 		except Exception as error:
 			raise InterlockError(interlock=interlock, msg="General exception: " + str(error))
 		return state
 
 	@classmethod
-	def setRelayState(cls, interlock: Interlock_model, state: {0, 1}) -> Interlock_model.State:
+	def set_relay_state(cls, interlock: Interlock_model, state: {0, 1}) -> Interlock_model.State:
 		response, auth, response_error = None, None, None
 		if interlock.card.username and interlock.card.password:
 			auth = (interlock.card.username, interlock.card.password)
@@ -359,11 +371,11 @@ def setRelayState(cls, interlock: Interlock_model, state: {0, 1}) -> Interlock_m
 		if response_error:
 			raise Exception(f"Communication error: {response_error}")
 		# No errors, continue and read relay state
-		responseXML = ElementTree.fromstring(response.content)
+		response_xml = ElementTree.fromstring(response.content)
 		state = None
 		# Try with a few different lookups here since depending on the relay model, it could be relayX or relayXstate
 		for state_suffix in cls.state_response_suffixes:
-			element = responseXML.find(cls.state_parameter_template.format(interlock.channel or '') + state_suffix)
+			element = response_xml.find(cls.state_parameter_template.format(interlock.channel or '') + state_suffix)
 			# Explicitly check for None since 0 is a valid state to return
 			if element is not None:
 				state = int(element.text)
@@ -405,16 +417,16 @@ def _send_command(self, interlock: Interlock_model, command_type: Interlock_mode
 		state = Interlock_model.State.UNKNOWN
 		try:
 			if command_type == Interlock_model.State.LOCKED:
-				state = self.setRelayState(interlock, self.MODBUS_OFF)
+				state = self.set_relay_state(interlock, self.MODBUS_OFF)
 			elif command_type == Interlock_model.State.UNLOCKED:
-				state = self.setRelayState(interlock, self.MODBUS_ON)
+				state = self.set_relay_state(interlock, self.MODBUS_ON)
 		except Exception as error:
 			interlocks_logger.exception(error)
 			raise Exception("General exception: " + str(error))
 		return state
 
 	@classmethod
-	def setRelayState(cls, interlock: Interlock_model, state: {0, 1}) -> Interlock_model.State:
+	def set_relay_state(cls, interlock: Interlock_model, state: {0, 1}) -> Interlock_model.State:
 		coil = interlock.channel
 		client = ModbusTcpClient(interlock.card.server, port=interlock.card.port)
 		try:
diff --git a/NEMO/management/commands/area_auto_logout_users.py b/NEMO/management/commands/area_auto_logout_users.py
new file mode 100644
index 00000000..6539ac4a
--- /dev/null
+++ b/NEMO/management/commands/area_auto_logout_users.py
@@ -0,0 +1,13 @@
+from django.core.management import BaseCommand
+
+from NEMO.views.timed_services import do_auto_logout_users
+
+
+class Command(BaseCommand):
+	help = (
+		"Run every minute to trigger the automatic area logout."
+		"The field 'auto_logout_time' has to be set on an Area object for this to work."
+	)
+
+	def handle(self, *args, **options):
+		do_auto_logout_users()
diff --git a/NEMO/management/commands/cancel_unused_reservations.py b/NEMO/management/commands/cancel_unused_reservations.py
index ac89cf02..7c4236f1 100644
--- a/NEMO/management/commands/cancel_unused_reservations.py
+++ b/NEMO/management/commands/cancel_unused_reservations.py
@@ -1,6 +1,6 @@
 from django.core.management import BaseCommand
 
-from NEMO.views.calendar import do_cancel_unused_reservations
+from NEMO.views.timed_services import do_cancel_unused_reservations
 
 
 class Command(BaseCommand):
diff --git a/NEMO/management/commands/create_closure_alerts.py b/NEMO/management/commands/create_closure_alerts.py
index a0f9728b..b9d39b06 100644
--- a/NEMO/management/commands/create_closure_alerts.py
+++ b/NEMO/management/commands/create_closure_alerts.py
@@ -1,6 +1,6 @@
 from django.core.management import BaseCommand
 
-from NEMO.views.calendar import do_create_closure_alerts
+from NEMO.views.timed_services import do_create_closure_alerts
 
 
 class Command(BaseCommand):
diff --git a/NEMO/management/commands/manage_recurring_charges.py b/NEMO/management/commands/manage_recurring_charges.py
index d3ad8bcc..df76a626 100644
--- a/NEMO/management/commands/manage_recurring_charges.py
+++ b/NEMO/management/commands/manage_recurring_charges.py
@@ -1,6 +1,6 @@
 from django.core.management import BaseCommand
 
-from NEMO.views.calendar import do_manage_recurring_charges
+from NEMO.views.timed_services import do_manage_recurring_charges
 
 
 class Command(BaseCommand):
diff --git a/NEMO/management/commands/manage_tool_qualifications.py b/NEMO/management/commands/manage_tool_qualifications.py
index c4670c1d..1319fa15 100644
--- a/NEMO/management/commands/manage_tool_qualifications.py
+++ b/NEMO/management/commands/manage_tool_qualifications.py
@@ -1,6 +1,6 @@
 from django.core.management import BaseCommand
 
-from NEMO.views.calendar import do_manage_tool_qualifications
+from NEMO.views.timed_services import do_manage_tool_qualifications
 
 
 class Command(BaseCommand):
diff --git a/NEMO/management/commands/send_email_out_of_time_reservation_notification.py b/NEMO/management/commands/send_email_out_of_time_reservation_notification.py
index 91149bed..ff5cebd5 100644
--- a/NEMO/management/commands/send_email_out_of_time_reservation_notification.py
+++ b/NEMO/management/commands/send_email_out_of_time_reservation_notification.py
@@ -1,6 +1,6 @@
 from django.core.management import BaseCommand
 
-from NEMO.views.calendar import send_email_out_of_time_reservation_notification
+from NEMO.views.timed_services import send_email_out_of_time_reservation_notification
 
 
 class Command(BaseCommand):
diff --git a/NEMO/management/commands/send_email_reservation_ending_reminders.py b/NEMO/management/commands/send_email_reservation_ending_reminders.py
index 869e34ee..ec912b72 100644
--- a/NEMO/management/commands/send_email_reservation_ending_reminders.py
+++ b/NEMO/management/commands/send_email_reservation_ending_reminders.py
@@ -1,6 +1,6 @@
 from django.core.management import BaseCommand
 
-from NEMO.views.calendar import send_email_reservation_ending_reminders
+from NEMO.views.timed_services import send_email_reservation_ending_reminders
 
 
 class Command(BaseCommand):
diff --git a/NEMO/management/commands/send_email_reservation_reminders.py b/NEMO/management/commands/send_email_reservation_reminders.py
index a1219e8e..f9c67a72 100644
--- a/NEMO/management/commands/send_email_reservation_reminders.py
+++ b/NEMO/management/commands/send_email_reservation_reminders.py
@@ -1,6 +1,6 @@
 from django.core.management import BaseCommand
 
-from NEMO.views.calendar import send_email_reservation_reminders
+from NEMO.views.timed_services import send_email_reservation_reminders
 
 
 class Command(BaseCommand):
diff --git a/NEMO/management/commands/send_email_usage_reminders.py b/NEMO/management/commands/send_email_usage_reminders.py
index 5a2b5de5..4d90d6e1 100644
--- a/NEMO/management/commands/send_email_usage_reminders.py
+++ b/NEMO/management/commands/send_email_usage_reminders.py
@@ -1,6 +1,6 @@
 from django.core.management import BaseCommand
 
-from NEMO.views.calendar import send_email_usage_reminders
+from NEMO.views.timed_services import send_email_usage_reminders
 
 
 class Command(BaseCommand):
diff --git a/NEMO/management/commands/send_email_user_access_expiration_reminders.py b/NEMO/management/commands/send_email_user_access_expiration_reminders.py
index dfc7d2a5..c99af963 100644
--- a/NEMO/management/commands/send_email_user_access_expiration_reminders.py
+++ b/NEMO/management/commands/send_email_user_access_expiration_reminders.py
@@ -1,6 +1,6 @@
 from django.core.management import BaseCommand
 
-from NEMO.views.calendar import send_email_user_access_expiration_reminders
+from NEMO.views.timed_services import send_email_user_access_expiration_reminders
 
 
 class Command(BaseCommand):
diff --git a/NEMO/management/commands/send_email_weekend_access_notification.py b/NEMO/management/commands/send_email_weekend_access_notification.py
index 296e5c61..a8261819 100644
--- a/NEMO/management/commands/send_email_weekend_access_notification.py
+++ b/NEMO/management/commands/send_email_weekend_access_notification.py
@@ -1,6 +1,6 @@
 from django.core.management import BaseCommand
 
-from NEMO.views.access_requests import send_email_weekend_access_notification
+from NEMO.views.timed_services import send_email_weekend_access_notification
 
 
 class Command(BaseCommand):
diff --git a/NEMO/migrations/0049_version_4_7_0.py b/NEMO/migrations/0049_version_4_7_0.py
new file mode 100644
index 00000000..77244905
--- /dev/null
+++ b/NEMO/migrations/0049_version_4_7_0.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.20 on 2023-09-14 01:54
+
+from django.db import migrations, models
+
+from NEMO.migrations_utils import create_news_for_version
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('NEMO', '0048_version_4_6_3'),
+    ]
+
+    def new_version_news(apps, schema_editor):
+        create_news_for_version(apps, "4.7.0", "The new version of the calendar allows users to view multiple tool feeds at once by checking the box next to each tool you would like to see.")
+
+    operations = [
+        migrations.RunPython(new_version_news),
+        migrations.AlterField(
+            model_name='interlock',
+            name='unit_id',
+            field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Multiplier/Unit id/Bank'),
+        ),
+    ]
diff --git a/NEMO/migrations/0050_consumable_add_self_checkout_and_notes.py b/NEMO/migrations/0050_consumable_add_self_checkout_and_notes.py
new file mode 100644
index 00000000..9d69bed6
--- /dev/null
+++ b/NEMO/migrations/0050_consumable_add_self_checkout_and_notes.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.20 on 2023-09-14 01:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('NEMO', '0049_version_4_7_0'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='consumable',
+            name='allow_self_checkout',
+            field=models.BooleanField(default=True, help_text='Allow users to self checkout this consumable, only applicable when self checkout customization is enabled.'),
+        ),
+        migrations.AddField(
+            model_name='consumable',
+            name='notes',
+            field=models.TextField(blank=True, help_text='Notes about the consumable.', null=True),
+        ),
+    ]
diff --git a/NEMO/migrations/0051_request_reviewers_in_tool_and_area.py b/NEMO/migrations/0051_request_reviewers_in_tool_and_area.py
new file mode 100644
index 00000000..cbde8f75
--- /dev/null
+++ b/NEMO/migrations/0051_request_reviewers_in_tool_and_area.py
@@ -0,0 +1,86 @@
+# Generated by Django 3.2.21 on 2023-09-28 20:48
+from collections import defaultdict
+from typing import Dict, Set
+
+from django.conf import settings
+from django.db import migrations, models
+from django.db.models import Q
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('NEMO', '0050_consumable_add_self_checkout_and_notes'),
+    ]
+
+    def migrate_adjustment_request_reviewers_forward(apps, schema_editor):
+        Tool = apps.get_model('NEMO', 'Tool')
+        Area = apps.get_model('NEMO', 'Area')
+        User = apps.get_model('NEMO', 'User')
+        UserPreferences = apps.get_model('NEMO', 'UserPreferences')
+        tool_adjustments: Dict[int, Set] = defaultdict(set)
+        area_adjustments: Dict[int, Set] = defaultdict(set)
+        managers_all_tool_adjustments = User.objects.filter(is_active=True, is_facility_manager=True, preferences__tool_adjustment_notifications__isnull=True)
+        managers_all_area_adjustments = User.objects.filter(is_active=True, is_facility_manager=True, preferences__area_adjustment_notifications__isnull=True)
+        for user_preference in UserPreferences.objects.filter(Q(tool_adjustment_notifications__isnull=False) | Q(area_adjustment_notifications__isnull=False)):
+            for tool in user_preference.tool_adjustment_notifications.all():
+                tool_adjustments[tool.id].add(user_preference.user)
+                # Add managers who want to receive all
+                tool_adjustments[tool.id].update(managers_all_tool_adjustments)
+            for area in user_preference.area_adjustment_notifications.all():
+                area_adjustments[area.id].add(user_preference.user)
+                # Add managers who want to receive all
+                area_adjustments[area.id].update(managers_all_area_adjustments)
+        for tool in Tool.objects.in_bulk(tool_adjustments.keys()).values():
+            tool._adjustment_request_reviewers.set(tool_adjustments[tool.id])
+        for area in Area.objects.in_bulk(area_adjustments.keys()).values():
+            area.adjustment_request_reviewers.set(area_adjustments[area.id])
+
+    def migrate_adjustment_request_reviewers_reverse(apps, schema_editor):
+        Tool = apps.get_model('NEMO', 'Tool')
+        Area = apps.get_model('NEMO', 'Area')
+        for tool in Tool.objects.all():
+            for user in tool._adjustment_request_reviewers.all():
+                if user.preferences:
+                    user.preferences.tool_adjustment_notifications.add(tool)
+        for area in Area.objects.all():
+            for user in area.adjustment_request_reviewers.all():
+                if user.preferences:
+                    user.preferences.area_adjustment_notifications.add(area)
+
+    operations = [
+        migrations.AddField(
+            model_name='area',
+            name='access_request_reviewers',
+            field=models.ManyToManyField(blank=True, help_text='Users who can approve/deny access requests for this area. Defaults to facility managers if left blank.', related_name='access_request_reviewer_on_areas', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='area',
+            name='adjustment_request_reviewers',
+            field=models.ManyToManyField(blank=True, help_text='Users who can approve/deny adjustment requests for this area. Defaults to facility managers if left blank.', related_name='adjustment_request_reviewer_on_areas', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='tool',
+            name='_adjustment_request_reviewers',
+            field=models.ManyToManyField(blank=True, db_table='NEMO_tool_adjustment_request_reviewers', help_text='Users who can approve/deny adjustment requests for this tool. Defaults to facility managers if left blank.', related_name='adjustment_request_reviewer_on_tools', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.RunPython(migrate_adjustment_request_reviewers_forward, migrate_adjustment_request_reviewers_reverse),
+        migrations.RemoveField(
+            model_name='userpreferences',
+            name='area_adjustment_notifications',
+        ),
+        migrations.RemoveField(
+            model_name='userpreferences',
+            name='tool_adjustment_notifications',
+        ),
+        migrations.AlterField(
+            model_name='landingpagechoice',
+            name='notifications',
+            field=models.CharField(blank=True, choices=[('news', 'News creation and updates - notifies all users'), ('safetyissue', 'New safety issues - notifies staff only'), ('buddyrequest', 'New buddy request - notifies all users'), ('buddyrequestmessage', 'New buddy request reply - notifies request creator and users who have replied'), ('adjustmentrequest', 'New adjustment request - notifies reviewers only'), ('adjustmentrequestmessage', 'New adjustment request reply - notifies request creator and users who have replied'), ('temporaryphysicalaccessrequest', 'New access request - notifies other users on request and reviewers')], help_text="Displays a the number of new notifications for the user. For example, if the user has two unread news notifications then the number '2' would appear for the news icon on the landing page.", max_length=100, null=True),
+        ),
+        migrations.AlterField(
+            model_name='notification',
+            name='notification_type',
+            field=models.CharField(choices=[('news', 'News creation and updates - notifies all users'), ('safetyissue', 'New safety issues - notifies staff only'), ('buddyrequest', 'New buddy request - notifies all users'), ('buddyrequestmessage', 'New buddy request reply - notifies request creator and users who have replied'), ('adjustmentrequest', 'New adjustment request - notifies reviewers only'), ('adjustmentrequestmessage', 'New adjustment request reply - notifies request creator and users who have replied'), ('temporaryphysicalaccessrequest', 'New access request - notifies other users on request and reviewers')], max_length=100),
+        ),
+    ]
diff --git a/NEMO/migrations/0052_area_add_auto_logout_time.py b/NEMO/migrations/0052_area_add_auto_logout_time.py
new file mode 100644
index 00000000..2c335992
--- /dev/null
+++ b/NEMO/migrations/0052_area_add_auto_logout_time.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.20 on 2023-08-22 13:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('NEMO', '0051_request_reviewers_in_tool_and_area'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='area',
+            name='auto_logout_time',
+            field=models.PositiveIntegerField(blank=True, help_text='Number of minutes after which users will be automatically logged out of this area.', null=True),
+        ),
+        migrations.AlterField(
+            model_name='closure',
+            name='staff_absent',
+            field=models.BooleanField(default=True, help_text='Check this box and all staff members will be marked absent during this closure in staff status.', verbose_name='Staff absent entire day'),
+        ),
+    ]
diff --git a/NEMO/migrations/0053_consumable_withdraw_add_usage_event_link.py b/NEMO/migrations/0053_consumable_withdraw_add_usage_event_link.py
new file mode 100644
index 00000000..8107d123
--- /dev/null
+++ b/NEMO/migrations/0053_consumable_withdraw_add_usage_event_link.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.20 on 2023-08-28 17:07
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('NEMO', '0052_area_add_auto_logout_time'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='consumablewithdraw',
+            name='tool_usage',
+        ),
+        migrations.AddField(
+            model_name='consumablewithdraw',
+            name='usage_event',
+            field=models.ForeignKey(blank=True, help_text='Whether this withdraw is from tool usage', null=True, on_delete=django.db.models.deletion.CASCADE, to='NEMO.usageevent'),
+        ),
+    ]
diff --git a/NEMO/migrations/0054_documents_update_max_length.py b/NEMO/migrations/0054_documents_update_max_length.py
new file mode 100644
index 00000000..1c5cf7c4
--- /dev/null
+++ b/NEMO/migrations/0054_documents_update_max_length.py
@@ -0,0 +1,35 @@
+# Generated by Django 3.2.20 on 2023-08-29 17:28
+
+from django.db import migrations, models
+
+import NEMO.utilities
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('NEMO', '0053_consumable_withdraw_add_usage_event_link'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='projectdocuments',
+            name='document',
+            field=models.FileField(blank=True, max_length=255, null=True, upload_to=NEMO.utilities.document_filename_upload, verbose_name='Document'),
+        ),
+        migrations.AlterField(
+            model_name='safetyitemdocuments',
+            name='document',
+            field=models.FileField(blank=True, max_length=255, null=True, upload_to=NEMO.utilities.document_filename_upload, verbose_name='Document'),
+        ),
+        migrations.AlterField(
+            model_name='tooldocuments',
+            name='document',
+            field=models.FileField(blank=True, max_length=255, null=True, upload_to=NEMO.utilities.document_filename_upload, verbose_name='Document'),
+        ),
+        migrations.AlterField(
+            model_name='userdocuments',
+            name='document',
+            field=models.FileField(blank=True, max_length=255, null=True, upload_to=NEMO.utilities.document_filename_upload, verbose_name='Document'),
+        ),
+    ]
diff --git a/NEMO/migrations_utils.py b/NEMO/migrations_utils.py
index 79bc0908..0d43113c 100644
--- a/NEMO/migrations_utils.py
+++ b/NEMO/migrations_utils.py
@@ -20,10 +20,10 @@ def create_news_for_version(apps, version, extra_content=None):
         story.title = f"What's new in NEMO {version}?"
         content = f"Thank you for updating to NEMO {version}.\n"
         if extra_content:
-            content += extra_content + "\n"
+            content += "\n" + extra_content + "\n"
         content += (
             f"\nClick on the following links to consult the <a href='https://github.com/usnistgov/NEMO/releases/tag/{version}' target='_blank'>NEMO {version} Release Notes</a> "
-            f"and the <a href='https://www.nist.gov/document/nemofeaturemanualpdf' target='_blank'>NEMO {version} Feature manual</a>"
+            f"and the <a href='https://nemo.nist.gov/public/NEMO_Feature_Manual.pdf' target='_blank'>NEMO {version} Feature manual</a>"
         )
         content = f"Originally published on {migration_format_datetime(now)} by NEMO:\n" + content.strip()
         story.original_content = content
diff --git a/NEMO/mixins.py b/NEMO/mixins.py
index 30ebd53e..7dc0fdd0 100644
--- a/NEMO/mixins.py
+++ b/NEMO/mixins.py
@@ -5,10 +5,13 @@
 from typing import Dict, Optional, TYPE_CHECKING
 
 from dateutil import rrule
+from django.contrib.auth import get_permission_codename
 from django.core.exceptions import NON_FIELD_ERRORS
+from django.shortcuts import redirect
 from django.utils import timezone
 
 from NEMO.utilities import beginning_of_the_day, format_datetime, get_recurring_rule, RecurrenceFrequency
+from NEMO.views.constants import NEXT_PARAMETER_NAME
 
 if TYPE_CHECKING:
 	from NEMO.models import User
@@ -225,3 +228,73 @@ def clean_recurrence(self) -> Dict:
 		if self.rec_until and self.rec_count:
 			errors[NON_FIELD_ERRORS] = "'count' and 'until' cannot be used at the same time."
 		return errors
+
+
+# Admin mixin to allow redirect after add or change
+class ModelAdminRedirectMixin:
+
+	def response_post_save_add(self, request, obj):
+		return self.response_redirect(request, super().response_post_save_add(request, obj))
+
+	def response_post_save_change(self, request, obj):
+		return self.response_redirect(request, super().response_post_save_change(request, obj))
+
+	def response_delete(self, request, obj_display, obj_id):
+		return self.response_redirect(request, super().response_delete(request, obj_display, obj_id))
+
+	def response_redirect(self, request, original_response):
+		if NEXT_PARAMETER_NAME in request.GET:
+			return redirect(request.GET[NEXT_PARAMETER_NAME])
+		return original_response
+
+
+# Mixin to use the obj in permissions. By default, admin classes ignore the actual obj
+class ObjPermissionAdminMixin:
+	def has_change_permission(self, request, obj=None):
+		"""
+		Return True if the given request has permission to change the given
+		Django model instance, the default implementation doesn't examine the
+		`obj` parameter.
+
+		Can be overridden by the user in subclasses. In such case it should
+		return True if the given request has permission to change the `obj`
+		model instance. If `obj` is None, this should return True if the given
+		request has permission to change *any* object of the given type.
+		"""
+		opts = self.opts
+		codename = get_permission_codename('change', opts)
+		return request.user.has_perm("%s.%s" % (opts.app_label, codename), obj)
+
+	def has_delete_permission(self, request, obj=None):
+		"""
+		Return True if the given request has permission to change the given
+		Django model instance, the default implementation doesn't examine the
+		`obj` parameter.
+
+		Can be overridden by the user in subclasses. In such case it should
+		return True if the given request has permission to delete the `obj`
+		model instance. If `obj` is None, this should return True if the given
+		request has permission to delete *any* object of the given type.
+		"""
+		opts = self.opts
+		codename = get_permission_codename('delete', opts)
+		return request.user.has_perm("%s.%s" % (opts.app_label, codename), obj)
+
+	def has_view_permission(self, request, obj=None):
+		"""
+		Return True if the given request has permission to view the given
+		Django model instance. The default implementation doesn't examine the
+		`obj` parameter.
+
+		If overridden by the user in subclasses, it should return True if the
+		given request has permission to view the `obj` model instance. If `obj`
+		is None, it should return True if the request has permission to view
+		any object of the given type.
+		"""
+		opts = self.opts
+		codename_view = get_permission_codename('view', opts)
+		codename_change = get_permission_codename('change', opts)
+		return (
+				request.user.has_perm('%s.%s' % (opts.app_label, codename_view), obj) or
+				request.user.has_perm('%s.%s' % (opts.app_label, codename_change), obj)
+		)
diff --git a/NEMO/models.py b/NEMO/models.py
index 95236c5c..d99045d9 100644
--- a/NEMO/models.py
+++ b/NEMO/models.py
@@ -13,14 +13,13 @@
 from typing import List, Optional, Set, Union
 
 from django.conf import settings
-from django.contrib import auth
 from django.contrib.auth.models import BaseUserManager, Group, Permission, PermissionsMixin
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
 from django.core.validators import MinValueValidator
 from django.db import connections, models, transaction
-from django.db.models import Q, QuerySet
+from django.db.models import Q
 from django.db.models.manager import Manager
 from django.db.models.signals import pre_delete
 from django.dispatch import receiver
@@ -144,7 +143,7 @@ def __str__(self):
 
 
 class BaseDocumentModel(BaseModel):
-	document = models.FileField(null=True, blank=True, upload_to=document_filename_upload, verbose_name='Document')
+	document = models.FileField(max_length=CHAR_FIELD_MAXIMUM_LENGTH, null=True, blank=True, upload_to=document_filename_upload, verbose_name='Document')
 	url = models.CharField(null=True, blank=True, max_length=200, verbose_name='URL')
 	name = models.CharField(null=True, blank=True, max_length=200, help_text="The optional name to display for this document")
 	uploaded_at = models.DateTimeField(auto_now_add=True)
@@ -298,8 +297,6 @@ class UserPreferences(BaseModel):
 	tool_freed_time_notifications = models.ManyToManyField("Tool", blank=True, help_text="Tools to receive notification when reservation time is freed.")
 	tool_freed_time_notifications_min_time = models.PositiveIntegerField(default=120, help_text="Minimum amount of minutes freed to receive a notification.")
 	tool_freed_time_notifications_max_future_days = models.PositiveIntegerField(default=7, help_text="Maximum number of days in the future to receive a notification for.")
-	tool_adjustment_notifications = models.ManyToManyField("Tool", related_name="+", blank=True, help_text="Tools to see/receive adjustment notifications for. If empty all notifications will be received.")
-	area_adjustment_notifications = models.ManyToManyField("Area", related_name="+", blank=True, help_text="Areas to see/receive adjustment notifications for. If empty all notifications will be received.")
 	tool_task_notifications = models.ManyToManyField("Tool", related_name="+", blank=True, help_text="Tools to see maintenance records and receive task notifications for. If empty all notifications will be received.")
 
 	def get_recurring_charges_days(self) -> List[int]:
@@ -446,11 +443,18 @@ class TemporaryPhysicalAccessRequest(BaseModel):
 	reviewer = models.ForeignKey("User", null=True, blank=True, related_name='access_requests_reviewed', on_delete=models.CASCADE)
 	deleted = models.BooleanField(default=False, help_text="Indicates the request has been deleted and won't be shown anymore.")
 
-	def creator_and_other_users(self) -> Set:
+	def creator_and_other_users(self) -> Set[User]:
 		result = {self.creator}
 		result.update(self.other_users.all())
 		return result
 
+	def reviewers(self) -> QuerySetType[User]:
+		# Create the list of users to notify/show request to. If the physical access request area's
+		# list of reviewers is empty, send/show to all facility managers
+		facility_managers = User.objects.filter(is_active=True, is_facility_manager=True)
+		area_reviewers = self.physical_access_level.area.access_request_reviewers.filter(is_active=True)
+		return area_reviewers or facility_managers
+
 	def clean(self):
 		if self.start_time and self.end_time and self.end_time <= self.start_time:
 			raise ValidationError({"end_time": "The end time must be later than the start time"})
@@ -464,7 +468,7 @@ class Closure(BaseModel):
 	alert_days_before = models.PositiveIntegerField(null=True, blank=True, help_text="Enter the number of days before the closure when an alert should automatically be created. Leave blank for no alert.")
 	alert_template = models.TextField(null=True, blank=True, help_text=mark_safe("The template to create the alert with. The following variables are provided (when applicable): <b>name</b>, <b>start_time</b>, <b>end_time</b>, <b>areas</b>."))
 	notify_managers_last_occurrence = models.BooleanField(default=True, help_text="Check this box to notify facility managers on the last occurrence of this closure.")
-	staff_absent = models.BooleanField(default=True, help_text="Check this box and all staff members will be marked absent during this closure in staff status.")
+	staff_absent = models.BooleanField(verbose_name="Staff absent entire day", default=True, help_text="Check this box and all staff members will be marked absent during this closure in staff status.")
 	physical_access_levels = models.ManyToManyField('PhysicalAccessLevel', blank=True, help_text="Select access levels this closure applies to.")
 
 	def __str__(self):
@@ -600,48 +604,6 @@ def clean(self):
 				}
 			)
 
-	def has_perm(self, perm, obj=None):
-		"""
-		Returns True if the user has each of the specified permissions. If
-		object is passed, it checks if the user has all required perms for this object.
-		"""
-
-		# Active administrators have all permissions.
-		if self.is_active and self.is_superuser:
-			return True
-
-		# Otherwise we need to check the backends.
-		for backend in auth.get_backends():
-			if hasattr(backend, "has_perm"):
-				if obj is not None:
-					if backend.has_perm(self, perm, obj):
-						return True
-				else:
-					if backend.has_perm(self, perm):
-						return True
-		return False
-
-	def has_perms(self, perm_list, obj=None):
-		for perm in perm_list:
-			if not self.has_perm(perm, obj):
-				return False
-		return True
-
-	def has_module_perms(self, app_label):
-		"""
-		Returns True if the user has any permissions in the given app label.
-		Uses pretty much the same logic as has_perm, above.
-		"""
-		# Active administrators have all permissions.
-		if self.is_active and self.is_superuser:
-			return True
-
-		for backend in auth.get_backends():
-			if hasattr(backend, "has_module_perms"):
-				if backend.has_module_perms(self, app_label):
-					return True
-		return False
-
 	def check_password(self, raw_password):
 		return False
 
@@ -695,6 +657,11 @@ def get_short_name(self):
 	def get_name(self):
 		return self.first_name + ' ' + self.last_name
 
+	def get_initials(self):
+		first_name_initial = self.first_name[0] if self.first_name else ""
+		last_name_initial = self.last_name[0] if self.last_name else ""
+		return first_name_initial + last_name_initial
+
 	def accessible_access_levels(self):
 		if not self.is_staff and not self.is_user_office:
 			return self.physical_access_levels.all()
@@ -768,6 +735,19 @@ def get_contact_info_html(self):
 			content = escape(f'<h4 style="margin-top:0; text-align: center">{self.get_name()}</h4>Email: <a href="{email_url}" target="_blank">{self.email}</a><br>')
 			return f'<a href="javascript:;" data-title="{content}" data-placement="bottom" class="contact-info-tooltip info-tooltip-container"><span class="glyphicon glyphicon-send small-icon"></span>{self.get_name()}</a>'
 
+	def has_perm(self, perm, obj=None):
+		# By default we don't use the actual object, similar to django admin
+		general_permission = super().has_perm(perm)
+		if general_permission:
+			return True
+		# For charges, the reviewer of an adjustment request for that charge can also edit it
+		if obj and issubclass(type(obj), BillableItemMixin):
+			# If there is an approved adjustment request for this charge, check if the user is a reviewer
+			adjustment_request: AdjustmentRequest = AdjustmentRequest.objects.filter(status=RequestStatus.APPROVED, deleted=False, item_id=obj.id, item_type=ContentType.objects.get_for_model(obj)).first()
+			if adjustment_request:
+				return self in adjustment_request.reviewers()
+		return general_permission
+
 	@classmethod
 	def get_email_field_name(cls):
 		return 'email'
@@ -807,9 +787,12 @@ class Tool(SerializationByNameModel):
 	_tool_calendar_color = models.CharField(db_column="tool_calendar_color", max_length=9, default="#33ad33", help_text="Color for tool reservations in calendar overviews")
 	_category = models.CharField(db_column="category", null=True, blank=True, max_length=1000, help_text="Create sub-categories using slashes. For example \"Category 1/Sub-category 1\".")
 	_operational = models.BooleanField(db_column="operational", default=False, help_text="Marking the tool non-operational will prevent users from using the tool.")
+	# Tool permissions
 	_primary_owner = models.ForeignKey(User, db_column="primary_owner_id", null=True, blank=True, related_name="primary_tool_owner", help_text="The staff member who is responsible for administration of this tool.", on_delete=models.PROTECT)
 	_backup_owners = models.ManyToManyField(User, db_table='NEMO_tool_backup_owners', blank=True, related_name="backup_for_tools", help_text="Alternate staff members who are responsible for administration of this tool when the primary owner is unavailable.")
 	_superusers = models.ManyToManyField(User, db_table='NEMO_tool_superusers', blank=True, related_name="superuser_for_tools", help_text="Superusers who can train users on this tool.")
+	_adjustment_request_reviewers = models.ManyToManyField(User, db_table='NEMO_tool_adjustment_request_reviewers', blank=True, related_name="adjustment_request_reviewer_on_tools", help_text="Users who can approve/deny adjustment requests for this tool. Defaults to facility managers if left blank.")
+	# Extra info
 	_location = models.CharField(db_column="location", null=True, blank=True, max_length=100)
 	_phone_number = models.CharField(db_column="phone_number", null=True, blank=True, max_length=100)
 	_notification_email_address = models.EmailField(db_column="notification_email_address", blank=True, null=True, help_text="Messages that relate to this tool (such as comments, problems, and shutdowns) will be forwarded to this email address. This can be a normal email address or a mailing list address.")
@@ -917,6 +900,15 @@ def superusers(self, value):
 		self.raise_setter_error_if_child_tool("superusers")
 		self._superusers = value
 
+	@property
+	def adjustment_request_reviewers(self) -> QuerySetType[User]:
+		return self.parent_tool.adjustment_request_reviewers if self.is_child_tool() else self._adjustment_request_reviewers
+
+	@adjustment_request_reviewers.setter
+	def adjustment_request_reviewers(self, value):
+		self.raise_setter_error_if_child_tool("_adjustment_request_reviewers")
+		self._adjustment_request_reviewers = value
+
 	@property
 	def location(self):
 		return self.parent_tool.location if self.is_child_tool() else self._location
@@ -1304,7 +1296,7 @@ def clean(self):
 				errors["parent_tool"] = "You cannot select the parent to be the tool itself."
 		else:
 			from NEMO.views.customization import ToolCustomization
-			from NEMO.widgets.dynamic_form import DynamicForm
+			from NEMO.widgets.dynamic_form import validate_dynamic_form_model
 			if not self._category:
 				errors["_category"] = "This field is required."
 			if not self._location and ToolCustomization.get_bool("tool_location_required"):
@@ -1316,15 +1308,9 @@ def clean(self):
 
 			# Validate _post_usage_questions JSON format
 			if self._post_usage_questions:
-				try:
-					loads(self._post_usage_questions)
-				except ValueError:
-					errors["_post_usage_questions"] = "This field needs to be a valid JSON string"
-				try:
-					DynamicForm(self._post_usage_questions).validate("tool_usage_group_question", self.id)
-				except Exception:
-					error_info = sys.exc_info()
-					errors["_post_usage_questions"] = error_info[0].__name__ + ": " + str(error_info[1])
+				dynamic_form_errors = validate_dynamic_form_model(self._post_usage_questions, "tool_usage_group_question", self.id)
+				if dynamic_form_errors:
+					errors["_post_usage_questions"] = dynamic_form_errors
 
 			if self._policy_off_between_times and (not self._policy_off_start_time or not self._policy_off_end_time):
 				if not self._policy_off_start_time:
@@ -1478,6 +1464,10 @@ class Area(MPTTModel):
 	abuse_email: List[str] = fields.MultiEmailField(null=True, blank=True, help_text="An email will be sent to this address when users overstay in the area or in children areas (logged in with expired reservation). A comma-separated list can be used.")
 	reservation_email: List[str] = fields.MultiEmailField(null=True, 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.")
 
+	# Area permissions
+	adjustment_request_reviewers = models.ManyToManyField(User, blank=True, related_name="adjustment_request_reviewer_on_areas", help_text="Users who can approve/deny adjustment requests for this area. Defaults to facility managers if left blank.")
+	access_request_reviewers = models.ManyToManyField(User, blank=True, related_name="access_request_reviewer_on_areas", help_text="Users who can approve/deny access requests for this area. Defaults to facility managers if left blank.")
+
 	# Additional information
 	area_calendar_color = models.CharField(max_length=9, default="#88B7CD", help_text="Color for tool reservations in calendar overviews")
 
@@ -1485,6 +1475,7 @@ class Area(MPTTModel):
 	welcome_message = models.TextField(null=True, blank=True, help_text='The welcome message will be displayed on the tablet login page. You can use HTML and JavaScript.')
 	requires_reservation = models.BooleanField(default=False, help_text="Check this box to require a reservation for this area before a user can login.")
 	logout_grace_period = models.PositiveIntegerField(null=True, blank=True, help_text="Number of minutes users have to logout of this area after their reservation expired before being flagged and abuse email is sent.")
+	auto_logout_time = models.PositiveIntegerField(null=True, blank=True, help_text="Number of minutes after which users will be automatically logged out of this area.")
 	buddy_system_allowed = models.BooleanField(default=False, help_text="Check this box if the buddy system is allowed in this area.")
 
 	# Capacity
@@ -1663,6 +1654,11 @@ class Project(SerializationByNameModel):
 	class Meta:
 		ordering = ["name"]
 
+	def display_with_pis(self):
+		pis = ", ".join([pi.get_name() for pi in self.manager_set.all()])
+		pis = f" (PI{'s' if self.manager_set.count() > 1 else ''}: {pis})" if pis else ""
+		return f"{self.name}{pis}"
+
 	def __str__(self):
 		return str(self.name)
 
@@ -1829,6 +1825,8 @@ class Consumable(BaseModel):
 	quantity = models.IntegerField(help_text="The number of items currently in stock.")
 	reusable = models.BooleanField(default=False, help_text="Check this box if this item is reusable. The quantity of reusable items will not decrease when orders are made (storage bins for example).")
 	visible = models.BooleanField(default=True)
+	allow_self_checkout = models.BooleanField(default=True, help_text="Allow users to self checkout this consumable, only applicable when self checkout customization is enabled.")
+	notes = models.TextField(null=True, blank=True, help_text="Notes about the consumable.")
 	reminder_threshold = models.IntegerField(null=True, blank=True, help_text="More of this item should be ordered when the quantity falls below this threshold.")
 	reminder_email = models.EmailField(null=True, blank=True, help_text="An email will be sent to this address when the quantity of this item falls below the reminder threshold.")
 	reminder_threshold_reached = models.BooleanField(default=False)
@@ -1886,13 +1884,17 @@ class ConsumableWithdraw(BaseModel, BillableItemMixin):
 	quantity = models.PositiveIntegerField()
 	project = models.ForeignKey(Project, help_text="The withdraw will be billed to this project.", on_delete=models.CASCADE)
 	date = models.DateTimeField(default=timezone.now, help_text="The date and time when the user withdrew the consumable.")
-	tool_usage = models.BooleanField(default=False, help_text="Whether this withdraw is from tool usage")
+	usage_event = models.ForeignKey(UsageEvent, null=True, blank=True, help_text="Whether this withdraw is from tool usage", on_delete=models.CASCADE)
 	validated = models.BooleanField(default=False)
 	validated_by = models.ForeignKey(User, null=True, blank=True, related_name="consumable_withdrawal_validated_set", on_delete=models.CASCADE)
 
 	class Meta:
 		ordering = ['-date']
 
+	@property
+	def tool_usage(self) -> bool:
+		return bool(self.usage_event)
+
 	def clean(self):
 		errors = {}
 		if self.customer_id:
@@ -2064,7 +2066,7 @@ class State(object):
 
 	card = models.ForeignKey(InterlockCard, on_delete=models.CASCADE)
 	channel = models.PositiveIntegerField(blank=True, null=True, verbose_name="Channel/Relay/Coil")
-	unit_id = models.PositiveIntegerField(null=True, blank=True, verbose_name="Multiplier/Unit id")
+	unit_id = models.PositiveIntegerField(null=True, blank=True, verbose_name="Multiplier/Unit id/Bank")
 	state = models.IntegerField(choices=State.Choices, default=State.UNKNOWN)
 	most_recent_reply = models.TextField(default="None")
 	most_recent_reply_time = models.DateTimeField(null=True, blank=True)
@@ -2534,7 +2536,7 @@ class Types:
 			(SAFETY, 'New safety issues - notifies staff only'),
 			(BUDDY_REQUEST, 'New buddy request - notifies all users'),
 			(BUDDY_REQUEST_REPLY, 'New buddy request reply - notifies request creator and users who have replied'),
-			(ADJUSTMENT_REQUEST, 'New adjustment request - notifies facility managers only'),
+			(ADJUSTMENT_REQUEST, 'New adjustment request - notifies reviewers only'),
 			(ADJUSTMENT_REQUEST_REPLY, 'New adjustment request reply - notifies request creator and users who have replied'),
 			(TEMPORARY_ACCESS_REQUEST, 'New access request - notifies other users on request and reviewers')
 		)
@@ -2787,10 +2789,23 @@ def creator_and_reply_users(self) -> List[User]:
 		result = {self.creator}
 		for reply in self.replies:
 			result.add(reply.author)
-		for manager in User.objects.filter(is_active=True, is_facility_manager=True):
-			result.add(manager)
+		result.update(self.reviewers())
 		return list(result)
 
+	def reviewers(self) -> QuerySetType[User]:
+		# Create the list of users to notify/show request to. If the adjustment request has a tool/area and their
+		# list of reviewers is empty, send/show to all facility managers
+		tool: Tool = getattr(self.item, "tool", None) if self.item else None
+		area: Area = getattr(self.item, "area", None) if self.item else None
+		facility_managers = User.objects.filter(is_active=True, is_facility_manager=True)
+		if tool:
+			tool_reviewers = tool._adjustment_request_reviewers.filter(is_active=True)
+			return tool_reviewers or facility_managers
+		if area:
+			area_reviewers = area.adjustment_request_reviewers.filter(is_active=True)
+			return area_reviewers or facility_managers
+		return facility_managers
+
 	def clean(self):
 		if not self.description:
 			raise ValidationError({"description": _("This field is required.")})
diff --git a/NEMO/policy.py b/NEMO/policy.py
index d9ef826c..3cc0913d 100644
--- a/NEMO/policy.py
+++ b/NEMO/policy.py
@@ -45,7 +45,12 @@
     render_email_template,
     send_mail,
 )
-from NEMO.views.customization import ApplicationCustomization, EmailsCustomization, get_media_file_contents
+from NEMO.views.customization import (
+    ApplicationCustomization,
+    EmailsCustomization,
+    ToolCustomization,
+    get_media_file_contents,
+)
 
 
 class NEMOPolicy:
@@ -401,11 +406,18 @@ def check_to_save_reservation(
             policy_problems, user_creating_reservation, cancelled_reservation, new_reservation
         )
 
+        # Tool superusers can be exempt from reservation policy rules if customization is set
+        user_exempt = (
+            item_type == ReservationItemType.TOOL
+            and user in new_reservation.tool.superusers.all()
+            and ToolCustomization.get_bool("tool_reservation_policy_superusers_bypass")
+        )
+
         # The reservation start time may not exceed the item's reservation horizon.
-        # Staff may break this rule.
+        # Staff or tool superusers may break this rule.
         # An explicit policy override allows this rule to be broken.
         item = new_reservation.reservation_item
-        if item.reservation_horizon is not None:
+        if not user_exempt and item.reservation_horizon is not None:
             reservation_horizon = timedelta(days=item.reservation_horizon)
             if new_reservation.start > timezone.now() + reservation_horizon:
                 policy_problems.append(
@@ -414,9 +426,9 @@ def check_to_save_reservation(
                     + f" days from now for this {item_type.value}."
                 )
 
-        # Check item policy rules
+        # Check item policy rules, tool superusers may be exempt
         item_policy_problems = []
-        if self.should_enforce_reservation_policy(new_reservation):
+        if not user_exempt and self.should_enforce_reservation_policy(new_reservation):
             item_policy_problems = self.check_reservation_policy_for_item(
                 user_creating_reservation, new_reservation, cancelled_reservation
             )
diff --git a/NEMO/rest_pagination.py b/NEMO/rest_pagination.py
new file mode 100644
index 00000000..c00ad3aa
--- /dev/null
+++ b/NEMO/rest_pagination.py
@@ -0,0 +1,5 @@
+from rest_framework.pagination import PageNumberPagination
+
+
+class NEMOPageNumberPagination(PageNumberPagination):
+	page_size_query_param = "page_size"
diff --git a/NEMO/serializers.py b/NEMO/serializers.py
index 95308c78..c77eeb7a 100644
--- a/NEMO/serializers.py
+++ b/NEMO/serializers.py
@@ -6,6 +6,7 @@
 from rest_flex_fields.serializers import FlexFieldsSerializerMixin
 from rest_framework import serializers
 from rest_framework.fields import CharField, ChoiceField, DateTimeField, DecimalField, IntegerField
+from rest_framework.relations import PrimaryKeyRelatedField
 from rest_framework.utils import model_meta
 
 from NEMO.models import (
@@ -114,12 +115,15 @@ class Meta:
 
 
 class ProjectSerializer(FlexFieldsSerializerMixin, ModelSerializer):
+	principal_investigators = PrimaryKeyRelatedField(source="manager_set.all", many=True, read_only=True)
+
 	class Meta:
 		model = Project
 		fields = "__all__"
 		expandable_fields = {
 			"account": "NEMO.serializers.AccountSerializer",
 			"only_allow_tools": ("NEMO.serializers.ToolSerializer", {"many": True}),
+			"principal_investigators": ("NEMO.serializers.UserSerializer", {"source": "manager_set", "many": True}),
 		}
 
 
diff --git a/NEMO/static/admin/questions_preview/questions_preview.css b/NEMO/static/admin/dynamic_form_preview/dynamic_form_preview.css
similarity index 66%
rename from NEMO/static/admin/questions_preview/questions_preview.css
rename to NEMO/static/admin/dynamic_form_preview/dynamic_form_preview.css
index 07867ca9..11326e9e 100644
--- a/NEMO/static/admin/questions_preview/questions_preview.css
+++ b/NEMO/static/admin/dynamic_form_preview/dynamic_form_preview.css
@@ -3,12 +3,12 @@ label
     text-transform:capitalize
 }
 
-.questions_preview label
+.dynamic_form_preview label
 {
     text-transform: none;
 }
 
-.questions_preview
+.dynamic_form_preview
 {
     font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
     font-size: 14px;
@@ -18,30 +18,30 @@ label
     padding: 20px 30px;
 }
 
-div.questions_preview:empty
+div.dynamic_form_preview:empty
 {
     display: none;
 }
 
-.questions_preview_help
+.dynamic_form_preview_help
 {
     margin-left: 0 !important;
     padding-left: 0 !important;
 }
 
-.questions_preview *
+.dynamic_form_preview *
 {
     -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
     box-sizing: border-box;
 }
 
-.questions_preview .form-group
+.dynamic_form_preview .form-group
 {
     margin-bottom: 15px;
 }
 
-.questions_preview .form-group label
+.dynamic_form_preview .form-group label
 {
     display: inline-block;
     max-width: 100%;
@@ -55,19 +55,19 @@ div.questions_preview:empty
     line-height: inherit;
 }
 
-.questions_preview .form-group label:after
+.dynamic_form_preview .form-group label:after
 {
     height: initial !important;
 }
 
-.questions_preview .form-group .input-group
+.dynamic_form_preview .form-group .input-group
 {
     position: relative;
     display: table;
     border-collapse: separate;
 }
 
-.questions_preview .form-group .input-group .input-group-addon
+.dynamic_form_preview .form-group .input-group .input-group-addon
 {
     padding: 6px 12px;
     font-size: 14px;
@@ -84,14 +84,14 @@ div.questions_preview:empty
     display: table-cell;
 }
 
-.questions_preview .form-group .input-group .input-group-addon:last-child
+.dynamic_form_preview .form-group .input-group .input-group-addon:last-child
 {
     border-left: 0;
     border-top-left-radius: 0;
     border-bottom-left-radius: 0;
 }
 
-.questions_preview .form-group .form-control
+.dynamic_form_preview .form-group .form-control
 {
     display: block;
     width: 100%;
@@ -111,7 +111,7 @@ div.questions_preview:empty
     transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
 }
 
-.questions_preview .form-group .input-group .form-control
+.dynamic_form_preview .form-group .input-group .form-control
 {
     display: table-cell;
     position: relative;
@@ -121,13 +121,13 @@ div.questions_preview:empty
     margin-bottom: 0;
 }
 
-.questions_preview .form-group .input-group .form-control:first-child
+.dynamic_form_preview .form-group .input-group .form-control:first-child
 {
     border-top-right-radius: 0;
     border-bottom-right-radius: 0;
 }
 
-.questions_preview .form-group .radio, .questions_preview .form-group .checkbox
+.dynamic_form_preview .form-group .radio, .dynamic_form_preview .form-group .checkbox
 {
     position: relative;
     display: block;
@@ -135,7 +135,7 @@ div.questions_preview:empty
     margin-bottom: 10px;
 }
 
-.questions_preview .form-group .radio label, .questions_preview .form-group .checkbox label
+.dynamic_form_preview .form-group .radio label, .dynamic_form_preview .form-group .checkbox label
 {
     min-height: 20px;
     padding-left: 20px;
@@ -144,21 +144,21 @@ div.questions_preview:empty
     cursor: pointer;
 }
 
-.questions_preview .form-group .radio input[type="radio"], .questions_preview .form-group .radio-inline input[type="radio"], .questions_preview .form-group .checkbox input[type="checkbox"], .questions_preview .form-group .checkbox-inline input[type="checkbox"]
+.dynamic_form_preview .form-group .radio input[type="radio"], .dynamic_form_preview .form-group .radio-inline input[type="radio"], .dynamic_form_preview .form-group .checkbox input[type="checkbox"], .dynamic_form_preview .form-group .checkbox-inline input[type="checkbox"]
 {
     position: absolute;
     margin-top: 4px \9;
     margin-left: -20px;
 }
 
-.questions_preview .form-group input[type="radio"], .questions_preview .form-group input[type="checkbox"]
+.dynamic_form_preview .form-group input[type="radio"], .dynamic_form_preview .form-group input[type="checkbox"]
 {
     margin: 4px 0 0;
     margin-top: 1px \9;
     line-height: normal;
 }
 
-.questions_preview .form-group input[type="checkbox"], .questions_preview .form-group input[type="radio"]
+.dynamic_form_preview .form-group input[type="checkbox"], .dynamic_form_preview .form-group input[type="radio"]
 {
     -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
@@ -181,7 +181,7 @@ div.questions_preview:empty
     background-color: #ac2925;
 }
 
-.questions_preview input:invalid, .questions_preview select:invalid, .questions_preview textarea:invalid
+.dynamic_form_preview input:invalid, .dynamic_form_preview select:invalid, .dynamic_form_preview textarea:invalid
 {
     border-color: #dc3545!important;
 }
\ No newline at end of file
diff --git a/NEMO/static/admin/questions_preview/questions_preview.js b/NEMO/static/admin/dynamic_form_preview/dynamic_form_preview.js
similarity index 57%
rename from NEMO/static/admin/questions_preview/questions_preview.js
rename to NEMO/static/admin/dynamic_form_preview/dynamic_form_preview.js
index 4ee2be37..7c050485 100644
--- a/NEMO/static/admin/questions_preview/questions_preview.js
+++ b/NEMO/static/admin/dynamic_form_preview/dynamic_form_preview.js
@@ -2,10 +2,13 @@ window.addEventListener("load", function()
 {
     (function ($)
     {
+        /* set a separate form for testing post_usage data */
+        $('form').last().after('<form id="dynamic_form_preview_form"></form>');
+
         function update_input_form()
         {
-            $('.questions_preview input, .questions_preview textarea, .questions_preview select').each(function (index, element) {
-                $(element).attr('form', 'questions_preview_form')
+            $('.dynamic_form_preview input, .dynamic_form_preview textarea, .dynamic_form_preview select').each(function (index, element) {
+                $(element).attr('form', 'dynamic_form_preview_form')
             })
         }
         function update_validation_button()
@@ -13,7 +16,7 @@ window.addEventListener("load", function()
             let valid_message = $("#form_validity")
             if (valid_message)
             {
-                if(document.querySelector('#questions_preview_form').checkValidity())
+                if(document.querySelector('#dynamic_form_preview_form').checkValidity())
                 {
                     valid_message.removeClass("invalid");
                     valid_message.text("The form is valid!");
@@ -25,7 +28,7 @@ window.addEventListener("load", function()
                 }
             }
         }
-        $('.questions_preview').on('change keyup', "input[form='questions_preview_form'], textarea[form='questions_preview_form'], select[form='questions_preview_form']", update_validation_button);
+        $('.dynamic_form_preview').on('change keyup', "input[form='dynamic_form_preview_form'], textarea[form='dynamic_form_preview_form'], select[form='dynamic_form_preview_form']", update_validation_button);
         $('body').on('question-group-changed', function()
         {
             update_input_form();
diff --git a/NEMO/static/admin/reservation_questions/reservation_questions.js b/NEMO/static/admin/reservation_questions/reservation_questions.js
deleted file mode 100644
index 6464836d..00000000
--- a/NEMO/static/admin/reservation_questions/reservation_questions.js
+++ /dev/null
@@ -1,8 +0,0 @@
-window.addEventListener("load", function()
-{
-    (function ($)
-    {
-        /* set a separate form for testing questions data */
-        $('#reservationquestions_form').after('<form id="questions_preview_form"></form>')
-    })(django.jQuery);
-});
\ No newline at end of file
diff --git a/NEMO/static/admin/tool/tool.js b/NEMO/static/admin/tool/tool.js
index bc4ee050..4bccf68b 100644
--- a/NEMO/static/admin/tool/tool.js
+++ b/NEMO/static/admin/tool/tool.js
@@ -30,8 +30,6 @@ window.addEventListener("load", function()
         selector.change(hide_fields);
         hide_fields()
 
-        /* set a separate form for testing post_usage data */
-        $('#tool_form').after('<form id="questions_preview_form"></form>')
         add_required_class();
     })(django.jQuery);
 });
\ No newline at end of file
diff --git a/NEMO/static/fullcalendar/fullcalendar.js b/NEMO/static/fullcalendar/fullcalendar.js
index f8e06fb2..18cb5af1 100644
--- a/NEMO/static/fullcalendar/fullcalendar.js
+++ b/NEMO/static/fullcalendar/fullcalendar.js
@@ -10567,6 +10567,7 @@ function EventManager(options) { // assumed to be a calendar
 	t.isFetchNeeded = isFetchNeeded;
 	t.fetchEvents = fetchEvents;
 	t.addEventSource = addEventSource;
+	t.addEventSourceList = addEventSourceList;
 	t.removeEventSource = removeEventSource;
 	t.updateEvent = updateEvent;
 	t.renderEvent = renderEvent;
@@ -10788,6 +10789,20 @@ function EventManager(options) { // assumed to be a calendar
 	}
 
 
+	// Adds a list of event sources and re-fetches events
+	function addEventSourceList(sourceList) {
+		if(sourceList && $.isArray(sourceList)) {
+			for(const sourceInput of sourceList) {
+				let source = buildEventSource(sourceInput);
+				if (source) {
+					sources.push(source);
+				}
+			}
+			this.refetchEvents();
+		}
+	}
+
+
 	function buildEventSource(sourceInput) { // will return undefined if invalid source
 		var normalizers = FC.sourceNormalizers;
 		var source;
diff --git a/NEMO/static/fullcalendar/fullcalendar.min.js b/NEMO/static/fullcalendar/fullcalendar.min.js
index 79e04baf..1b974d2a 100644
--- a/NEMO/static/fullcalendar/fullcalendar.min.js
+++ b/NEMO/static/fullcalendar/fullcalendar.min.js
@@ -1,750 +1,1 @@
-(function(factory){if(typeof define==='function'&&define.amd){define(['jquery','moment'],factory);}
-else if(typeof exports==='object'){module.exports=factory(require('jquery'),require('moment'));}
-else{factory(jQuery,moment);}})(function($,moment){;;var FC=$.fullCalendar={version:"2.7.3",internalApiVersion:4};var fcViews=FC.views={};$.fn.fullCalendar=function(options){var args=Array.prototype.slice.call(arguments,1);var res=this;this.each(function(i,_element){var element=$(_element);var calendar=element.data('fullCalendar');var singleRes;if(typeof options==='string'){if(calendar&&$.isFunction(calendar[options])){singleRes=calendar[options].apply(calendar,args);if(!i){res=singleRes;}
-if(options==='destroy'){element.removeData('fullCalendar');}}}
-else if(!calendar){calendar=new Calendar(element,options);element.data('fullCalendar',calendar);calendar.render();}});return res;};var complexOptions=['header','buttonText','buttonIcons','themeButtonIcons'];function mergeOptions(optionObjs){return mergeProps(optionObjs,complexOptions);}
-function massageOverrides(input){var overrides={views:input.views||{}};var subObj;$.each(input,function(name,val){if(name!='views'){if($.isPlainObject(val)&&!/(time|duration|interval)$/i.test(name)&&$.inArray(name,complexOptions)==-1){subObj=null;$.each(val,function(subName,subVal){if(/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)){if(!overrides.views[subName]){overrides.views[subName]={};}
-overrides.views[subName][name]=subVal;}
-else{if(!subObj){subObj={};}
-subObj[subName]=subVal;}});if(subObj){overrides[name]=subObj;}}
-else{overrides[name]=val;}}});return overrides;};;FC.intersectRanges=intersectRanges;FC.applyAll=applyAll;FC.debounce=debounce;FC.isInt=isInt;FC.htmlEscape=htmlEscape;FC.cssToStr=cssToStr;FC.proxy=proxy;FC.capitaliseFirstLetter=capitaliseFirstLetter;function compensateScroll(rowEls,scrollbarWidths){if(scrollbarWidths.left){rowEls.css({'border-left-width':1,'margin-left':scrollbarWidths.left-1});}
-if(scrollbarWidths.right){rowEls.css({'border-right-width':1,'margin-right':scrollbarWidths.right-1});}}
-function uncompensateScroll(rowEls){rowEls.css({'margin-left':'','margin-right':'','border-left-width':'','border-right-width':''});}
-function disableCursor(){$('body').addClass('fc-not-allowed');}
-function enableCursor(){$('body').removeClass('fc-not-allowed');}
-function distributeHeight(els,availableHeight,shouldRedistribute){var minOffset1=Math.floor(availableHeight/els.length);var minOffset2=Math.floor(availableHeight-minOffset1*(els.length-1));var flexEls=[];var flexOffsets=[];var flexHeights=[];var usedHeight=0;undistributeHeight(els);els.each(function(i,el){var minOffset=i===els.length-1?minOffset2:minOffset1;var naturalOffset=$(el).outerHeight(true);if(naturalOffset<minOffset){flexEls.push(el);flexOffsets.push(naturalOffset);flexHeights.push($(el).height());}
-else{usedHeight+=naturalOffset;}});if(shouldRedistribute){availableHeight-=usedHeight;minOffset1=Math.floor(availableHeight/flexEls.length);minOffset2=Math.floor(availableHeight-minOffset1*(flexEls.length-1));}
-$(flexEls).each(function(i,el){var minOffset=i===flexEls.length-1?minOffset2:minOffset1;var naturalOffset=flexOffsets[i];var naturalHeight=flexHeights[i];var newHeight=minOffset-(naturalOffset-naturalHeight);if(naturalOffset<minOffset){$(el).height(newHeight);}});}
-function undistributeHeight(els){els.height('');}
-function matchCellWidths(els){var maxInnerWidth=0;els.find('> span').each(function(i,innerEl){var innerWidth=$(innerEl).outerWidth();if(innerWidth>maxInnerWidth){maxInnerWidth=innerWidth;}});maxInnerWidth++;els.width(maxInnerWidth);return maxInnerWidth;}
-function subtractInnerElHeight(outerEl,innerEl){var both=outerEl.add(innerEl);var diff;both.css({position:'relative',left:-1});diff=outerEl.outerHeight()-innerEl.outerHeight();both.css({position:'',left:''});return diff;}
-FC.getOuterRect=getOuterRect;FC.getClientRect=getClientRect;FC.getContentRect=getContentRect;FC.getScrollbarWidths=getScrollbarWidths;function getScrollParent(el){var position=el.css('position'),scrollParent=el.parents().filter(function(){var parent=$(this);return(/(auto|scroll)/).test(parent.css('overflow')+parent.css('overflow-y')+parent.css('overflow-x'));}).eq(0);return position==='fixed'||!scrollParent.length?$(el[0].ownerDocument||document):scrollParent;}
-function getOuterRect(el,origin){var offset=el.offset();var left=offset.left-(origin?origin.left:0);var top=offset.top-(origin?origin.top:0);return{left:left,right:left+el.outerWidth(),top:top,bottom:top+el.outerHeight()};}
-function getClientRect(el,origin){var offset=el.offset();var scrollbarWidths=getScrollbarWidths(el);var left=offset.left+getCssFloat(el,'border-left-width')+scrollbarWidths.left-(origin?origin.left:0);var top=offset.top+getCssFloat(el,'border-top-width')+scrollbarWidths.top-(origin?origin.top:0);return{left:left,right:left+el[0].clientWidth,top:top,bottom:top+el[0].clientHeight};}
-function getContentRect(el,origin){var offset=el.offset();var left=offset.left+getCssFloat(el,'border-left-width')+getCssFloat(el,'padding-left')-
-(origin?origin.left:0);var top=offset.top+getCssFloat(el,'border-top-width')+getCssFloat(el,'padding-top')-
-(origin?origin.top:0);return{left:left,right:left+el.width(),top:top,bottom:top+el.height()};}
-function getScrollbarWidths(el){var leftRightWidth=el.innerWidth()-el[0].clientWidth;var widths={left:0,right:0,top:0,bottom:el.innerHeight()-el[0].clientHeight};if(getIsLeftRtlScrollbars()&&el.css('direction')=='rtl'){widths.left=leftRightWidth;}
-else{widths.right=leftRightWidth;}
-return widths;}
-var _isLeftRtlScrollbars=null;function getIsLeftRtlScrollbars(){if(_isLeftRtlScrollbars===null){_isLeftRtlScrollbars=computeIsLeftRtlScrollbars();}
-return _isLeftRtlScrollbars;}
-function computeIsLeftRtlScrollbars(){var el=$('<div><div/></div>').css({position:'absolute',top:-1000,left:0,border:0,padding:0,overflow:'scroll',direction:'rtl'}).appendTo('body');var innerEl=el.children();var res=innerEl.offset().left>el.offset().left;el.remove();return res;}
-function getCssFloat(el,prop){return parseFloat(el.css(prop))||0;}
-FC.preventDefault=preventDefault;function isPrimaryMouseButton(ev){return ev.which==1&&!ev.ctrlKey;}
-function getEvX(ev){if(ev.pageX!==undefined){return ev.pageX;}
-var touches=ev.originalEvent.touches;if(touches){return touches[0].pageX;}}
-function getEvY(ev){if(ev.pageY!==undefined){return ev.pageY;}
-var touches=ev.originalEvent.touches;if(touches){return touches[0].pageY;}}
-function getEvIsTouch(ev){return/^touch/.test(ev.type);}
-function preventSelection(el){el.addClass('fc-unselectable').on('selectstart',preventDefault);}
-function preventDefault(ev){ev.preventDefault();}
-function bindAnyScroll(handler){if(window.addEventListener){window.addEventListener('scroll',handler,true);return true;}
-return false;}
-function unbindAnyScroll(handler){if(window.removeEventListener){window.removeEventListener('scroll',handler,true);return true;}
-return false;}
-FC.intersectRects=intersectRects;function intersectRects(rect1,rect2){var res={left:Math.max(rect1.left,rect2.left),right:Math.min(rect1.right,rect2.right),top:Math.max(rect1.top,rect2.top),bottom:Math.min(rect1.bottom,rect2.bottom)};if(res.left<res.right&&res.top<res.bottom){return res;}
-return false;}
-function constrainPoint(point,rect){return{left:Math.min(Math.max(point.left,rect.left),rect.right),top:Math.min(Math.max(point.top,rect.top),rect.bottom)};}
-function getRectCenter(rect){return{left:(rect.left+rect.right)/2,top:(rect.top+rect.bottom)/2};}
-function diffPoints(point1,point2){return{left:point1.left-point2.left,top:point1.top-point2.top};}
-FC.parseFieldSpecs=parseFieldSpecs;FC.compareByFieldSpecs=compareByFieldSpecs;FC.compareByFieldSpec=compareByFieldSpec;FC.flexibleCompare=flexibleCompare;function parseFieldSpecs(input){var specs=[];var tokens=[];var i,token;if(typeof input==='string'){tokens=input.split(/\s*,\s*/);}
-else if(typeof input==='function'){tokens=[input];}
-else if($.isArray(input)){tokens=input;}
-for(i=0;i<tokens.length;i++){token=tokens[i];if(typeof token==='string'){specs.push(token.charAt(0)=='-'?{field:token.substring(1),order:-1}:{field:token,order:1});}
-else if(typeof token==='function'){specs.push({func:token});}}
-return specs;}
-function compareByFieldSpecs(obj1,obj2,fieldSpecs){var i;var cmp;for(i=0;i<fieldSpecs.length;i++){cmp=compareByFieldSpec(obj1,obj2,fieldSpecs[i]);if(cmp){return cmp;}}
-return 0;}
-function compareByFieldSpec(obj1,obj2,fieldSpec){if(fieldSpec.func){return fieldSpec.func(obj1,obj2);}
-return flexibleCompare(obj1[fieldSpec.field],obj2[fieldSpec.field])*(fieldSpec.order||1);}
-function flexibleCompare(a,b){if(!a&&!b){return 0;}
-if(b==null){return-1;}
-if(a==null){return 1;}
-if($.type(a)==='string'||$.type(b)==='string'){return String(a).localeCompare(String(b));}
-return a-b;}
-function intersectRanges(subjectRange,constraintRange){var subjectStart=subjectRange.start;var subjectEnd=subjectRange.end;var constraintStart=constraintRange.start;var constraintEnd=constraintRange.end;var segStart,segEnd;var isStart,isEnd;if(subjectEnd>constraintStart&&subjectStart<constraintEnd){if(subjectStart>=constraintStart){segStart=subjectStart.clone();isStart=true;}
-else{segStart=constraintStart.clone();isStart=false;}
-if(subjectEnd<=constraintEnd){segEnd=subjectEnd.clone();isEnd=true;}
-else{segEnd=constraintEnd.clone();isEnd=false;}
-return{start:segStart,end:segEnd,isStart:isStart,isEnd:isEnd};}}
-FC.computeIntervalUnit=computeIntervalUnit;FC.divideRangeByDuration=divideRangeByDuration;FC.divideDurationByDuration=divideDurationByDuration;FC.multiplyDuration=multiplyDuration;FC.durationHasTime=durationHasTime;var dayIDs=['sun','mon','tue','wed','thu','fri','sat'];var intervalUnits=['year','month','week','day','hour','minute','second','millisecond'];function diffDayTime(a,b){return moment.duration({days:a.clone().stripTime().diff(b.clone().stripTime(),'days'),ms:a.time()-b.time()});}
-function diffDay(a,b){return moment.duration({days:a.clone().stripTime().diff(b.clone().stripTime(),'days')});}
-function diffByUnit(a,b,unit){return moment.duration(Math.round(a.diff(b,unit,true)),unit);}
-function computeIntervalUnit(start,end){var i,unit;var val;for(i=0;i<intervalUnits.length;i++){unit=intervalUnits[i];val=computeRangeAs(unit,start,end);if(val>=1&&isInt(val)){break;}}
-return unit;}
-function computeRangeAs(unit,start,end){if(end!=null){return end.diff(start,unit,true);}
-else if(moment.isDuration(start)){return start.as(unit);}
-else{return start.end.diff(start.start,unit,true);}}
-function divideRangeByDuration(start,end,dur){var months;if(durationHasTime(dur)){return(end-start)/dur;}
-months=dur.asMonths();if(Math.abs(months)>=1&&isInt(months)){return end.diff(start,'months',true)/months;}
-return end.diff(start,'days',true)/dur.asDays();}
-function divideDurationByDuration(dur1,dur2){var months1,months2;if(durationHasTime(dur1)||durationHasTime(dur2)){return dur1/dur2;}
-months1=dur1.asMonths();months2=dur2.asMonths();if(Math.abs(months1)>=1&&isInt(months1)&&Math.abs(months2)>=1&&isInt(months2)){return months1/months2;}
-return dur1.asDays()/dur2.asDays();}
-function multiplyDuration(dur,n){var months;if(durationHasTime(dur)){return moment.duration(dur*n);}
-months=dur.asMonths();if(Math.abs(months)>=1&&isInt(months)){return moment.duration({months:months*n});}
-return moment.duration({days:dur.asDays()*n});}
-function durationHasTime(dur){return Boolean(dur.hours()||dur.minutes()||dur.seconds()||dur.milliseconds());}
-function isNativeDate(input){return Object.prototype.toString.call(input)==='[object Date]'||input instanceof Date;}
-function isTimeString(str){return/^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);}
-FC.log=function(){var console=window.console;if(console&&console.log){return console.log.apply(console,arguments);}};FC.warn=function(){var console=window.console;if(console&&console.warn){return console.warn.apply(console,arguments);}
-else{return FC.log.apply(FC,arguments);}};var hasOwnPropMethod={}.hasOwnProperty;function mergeProps(propObjs,complexProps){var dest={};var i,name;var complexObjs;var j,val;var props;if(complexProps){for(i=0;i<complexProps.length;i++){name=complexProps[i];complexObjs=[];for(j=propObjs.length-1;j>=0;j--){val=propObjs[j][name];if(typeof val==='object'){complexObjs.unshift(val);}
-else if(val!==undefined){dest[name]=val;break;}}
-if(complexObjs.length){dest[name]=mergeProps(complexObjs);}}}
-for(i=propObjs.length-1;i>=0;i--){props=propObjs[i];for(name in props){if(!(name in dest)){dest[name]=props[name];}}}
-return dest;}
-function createObject(proto){var f=function(){};f.prototype=proto;return new f();}
-function copyOwnProps(src,dest){for(var name in src){if(hasOwnProp(src,name)){dest[name]=src[name];}}}
-function copyNativeMethods(src,dest){var names=['constructor','toString','valueOf'];var i,name;for(i=0;i<names.length;i++){name=names[i];if(src[name]!==Object.prototype[name]){dest[name]=src[name];}}}
-function hasOwnProp(obj,name){return hasOwnPropMethod.call(obj,name);}
-function isAtomic(val){return/undefined|null|boolean|number|string/.test($.type(val));}
-function applyAll(functions,thisObj,args){if($.isFunction(functions)){functions=[functions];}
-if(functions){var i;var ret;for(i=0;i<functions.length;i++){ret=functions[i].apply(thisObj,args)||ret;}
-return ret;}}
-function firstDefined(){for(var i=0;i<arguments.length;i++){if(arguments[i]!==undefined){return arguments[i];}}}
-function htmlEscape(s){return(s+'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/'/g,'&#039;').replace(/"/g,'&quot;').replace(/\n/g,'<br />');}
-function stripHtmlEntities(text){return text.replace(/&.*?;/g,'');}
-function cssToStr(cssProps){var statements=[];$.each(cssProps,function(name,val){if(val!=null){statements.push(name+':'+val);}});return statements.join(';');}
-function capitaliseFirstLetter(str){return str.charAt(0).toUpperCase()+str.slice(1);}
-function compareNumbers(a,b){return a-b;}
-function isInt(n){return n%1===0;}
-function proxy(obj,methodName){var method=obj[methodName];return function(){return method.apply(obj,arguments);};}
-function debounce(func,wait,immediate){var timeout,args,context,timestamp,result;var later=function(){var last=+new Date()-timestamp;if(last<wait){timeout=setTimeout(later,wait-last);}
-else{timeout=null;if(!immediate){result=func.apply(context,args);context=args=null;}}};return function(){context=this;args=arguments;timestamp=+new Date();var callNow=immediate&&!timeout;if(!timeout){timeout=setTimeout(later,wait);}
-if(callNow){result=func.apply(context,args);context=args=null;}
-return result;};};;var ambigDateOfMonthRegex=/^\s*\d{4}-\d\d$/;var ambigTimeOrZoneRegex=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;var newMomentProto=moment.fn;var oldMomentProto=$.extend({},newMomentProto);var allowValueOptimization;var setUTCValues;var setLocalValues;FC.moment=function(){return makeMoment(arguments);};FC.moment.utc=function(){var mom=makeMoment(arguments,true);if(mom.hasTime()){mom.utc();}
-return mom;};FC.moment.parseZone=function(){return makeMoment(arguments,true,true);};function makeMoment(args,parseAsUTC,parseZone){var input=args[0];var isSingleString=args.length==1&&typeof input==='string';var isAmbigTime;var isAmbigZone;var ambigMatch;var mom;if(moment.isMoment(input)){mom=moment.apply(null,args);transferAmbigs(input,mom);}
-else if(isNativeDate(input)||input===undefined){mom=moment.apply(null,args);}
-else{isAmbigTime=false;isAmbigZone=false;if(isSingleString){if(ambigDateOfMonthRegex.test(input)){input+='-01';args=[input];isAmbigTime=true;isAmbigZone=true;}
-else if((ambigMatch=ambigTimeOrZoneRegex.exec(input))){isAmbigTime=!ambigMatch[5];isAmbigZone=true;}}
-else if($.isArray(input)){isAmbigZone=true;}
-if(parseAsUTC||isAmbigTime){mom=moment.utc.apply(moment,args);}
-else{mom=moment.apply(null,args);}
-if(isAmbigTime){mom._ambigTime=true;mom._ambigZone=true;}
-else if(parseZone){if(isAmbigZone){mom._ambigZone=true;}
-else if(isSingleString){if(mom.utcOffset){mom.utcOffset(input);}
-else{mom.zone(input);}}}}
-mom._fullCalendar=true;return mom;}
-newMomentProto.clone=function(){var mom=oldMomentProto.clone.apply(this,arguments);transferAmbigs(this,mom);if(this._fullCalendar){mom._fullCalendar=true;}
-return mom;};newMomentProto.week=newMomentProto.weeks=function(input){var weekCalc=(this._locale||this._lang)._fullCalendar_weekCalc;if(input==null&&typeof weekCalc==='function'){return weekCalc(this);}
-else if(weekCalc==='ISO'){return oldMomentProto.isoWeek.apply(this,arguments);}
-return oldMomentProto.week.apply(this,arguments);};newMomentProto.time=function(time){if(!this._fullCalendar){return oldMomentProto.time.apply(this,arguments);}
-if(time==null){return moment.duration({hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()});}
-else{this._ambigTime=false;if(!moment.isDuration(time)&&!moment.isMoment(time)){time=moment.duration(time);}
-var dayHours=0;if(moment.isDuration(time)){dayHours=Math.floor(time.asDays())*24;}
-return this.hours(dayHours+time.hours()).minutes(time.minutes()).seconds(time.seconds()).milliseconds(time.milliseconds());}};newMomentProto.stripTime=function(){var a;if(!this._ambigTime){a=this.toArray();this.utc();setUTCValues(this,a.slice(0,3));this._ambigTime=true;this._ambigZone=true;}
-return this;};newMomentProto.hasTime=function(){return!this._ambigTime;};newMomentProto.stripZone=function(){var a,wasAmbigTime;if(!this._ambigZone){a=this.toArray();wasAmbigTime=this._ambigTime;this.utc();setUTCValues(this,a);this._ambigTime=wasAmbigTime||false;this._ambigZone=true;}
-return this;};newMomentProto.hasZone=function(){return!this._ambigZone;};newMomentProto.local=function(){var a=this.toArray();var wasAmbigZone=this._ambigZone;oldMomentProto.local.apply(this,arguments);this._ambigTime=false;this._ambigZone=false;if(wasAmbigZone){setLocalValues(this,a);}
-return this;};newMomentProto.utc=function(){oldMomentProto.utc.apply(this,arguments);this._ambigTime=false;this._ambigZone=false;return this;};$.each(['zone','utcOffset'],function(i,name){if(oldMomentProto[name]){newMomentProto[name]=function(tzo){if(tzo!=null){this._ambigTime=false;this._ambigZone=false;}
-return oldMomentProto[name].apply(this,arguments);};}});newMomentProto.format=function(){if(this._fullCalendar&&arguments[0]){return formatDate(this,arguments[0]);}
-if(this._ambigTime){return oldMomentFormat(this,'YYYY-MM-DD');}
-if(this._ambigZone){return oldMomentFormat(this,'YYYY-MM-DD[T]HH:mm:ss');}
-return oldMomentProto.format.apply(this,arguments);};newMomentProto.toISOString=function(){if(this._ambigTime){return oldMomentFormat(this,'YYYY-MM-DD');}
-if(this._ambigZone){return oldMomentFormat(this,'YYYY-MM-DD[T]HH:mm:ss');}
-return oldMomentProto.toISOString.apply(this,arguments);};newMomentProto.isWithin=function(start,end){var a=commonlyAmbiguate([this,start,end]);return a[0]>=a[1]&&a[0]<a[2];};newMomentProto.isSame=function(input,units){var a;if(!this._fullCalendar){return oldMomentProto.isSame.apply(this,arguments);}
-if(units){a=commonlyAmbiguate([this,input],true);return oldMomentProto.isSame.call(a[0],a[1],units);}
-else{input=FC.moment.parseZone(input);return oldMomentProto.isSame.call(this,input)&&Boolean(this._ambigTime)===Boolean(input._ambigTime)&&Boolean(this._ambigZone)===Boolean(input._ambigZone);}};$.each(['isBefore','isAfter'],function(i,methodName){newMomentProto[methodName]=function(input,units){var a;if(!this._fullCalendar){return oldMomentProto[methodName].apply(this,arguments);}
-a=commonlyAmbiguate([this,input]);return oldMomentProto[methodName].call(a[0],a[1],units);};});function commonlyAmbiguate(inputs,preserveTime){var anyAmbigTime=false;var anyAmbigZone=false;var len=inputs.length;var moms=[];var i,mom;for(i=0;i<len;i++){mom=inputs[i];if(!moment.isMoment(mom)){mom=FC.moment.parseZone(mom);}
-anyAmbigTime=anyAmbigTime||mom._ambigTime;anyAmbigZone=anyAmbigZone||mom._ambigZone;moms.push(mom);}
-for(i=0;i<len;i++){mom=moms[i];if(!preserveTime&&anyAmbigTime&&!mom._ambigTime){moms[i]=mom.clone().stripTime();}
-else if(anyAmbigZone&&!mom._ambigZone){moms[i]=mom.clone().stripZone();}}
-return moms;}
-function transferAmbigs(src,dest){if(src._ambigTime){dest._ambigTime=true;}
-else if(dest._ambigTime){dest._ambigTime=false;}
-if(src._ambigZone){dest._ambigZone=true;}
-else if(dest._ambigZone){dest._ambigZone=false;}}
-function setMomentValues(mom,a){mom.year(a[0]||0).month(a[1]||0).date(a[2]||0).hours(a[3]||0).minutes(a[4]||0).seconds(a[5]||0).milliseconds(a[6]||0);}
-allowValueOptimization='_d'in moment()&&'updateOffset'in moment;setUTCValues=allowValueOptimization?function(mom,a){mom._d.setTime(Date.UTC.apply(Date,a));moment.updateOffset(mom,false);}:setMomentValues;setLocalValues=allowValueOptimization?function(mom,a){mom._d.setTime(+new Date(a[0]||0,a[1]||0,a[2]||0,a[3]||0,a[4]||0,a[5]||0,a[6]||0));moment.updateOffset(mom,false);}:setMomentValues;;;function oldMomentFormat(mom,formatStr){return oldMomentProto.format.call(mom,formatStr);}
-function formatDate(date,formatStr){return formatDateWithChunks(date,getFormatStringChunks(formatStr));}
-function formatDateWithChunks(date,chunks){var s='';var i;for(i=0;i<chunks.length;i++){s+=formatDateWithChunk(date,chunks[i]);}
-return s;}
-var tokenOverrides={t:function(date){return oldMomentFormat(date,'a').charAt(0);},T:function(date){return oldMomentFormat(date,'A').charAt(0);}};function formatDateWithChunk(date,chunk){var token;var maybeStr;if(typeof chunk==='string'){return chunk;}
-else if((token=chunk.token)){if(tokenOverrides[token]){return tokenOverrides[token](date);}
-return oldMomentFormat(date,token);}
-else if(chunk.maybe){maybeStr=formatDateWithChunks(date,chunk.maybe);if(maybeStr.match(/[1-9]/)){return maybeStr;}}
-return'';}
-function formatRange(date1,date2,formatStr,separator,isRTL){var localeData;date1=FC.moment.parseZone(date1);date2=FC.moment.parseZone(date2);localeData=(date1.localeData||date1.lang).call(date1);formatStr=localeData.longDateFormat(formatStr)||formatStr;separator=separator||' - ';return formatRangeWithChunks(date1,date2,getFormatStringChunks(formatStr),separator,isRTL);}
-FC.formatRange=formatRange;function formatRangeWithChunks(date1,date2,chunks,separator,isRTL){var unzonedDate1=date1.clone().stripZone();var unzonedDate2=date2.clone().stripZone();var chunkStr;var leftI;var leftStr='';var rightI;var rightStr='';var middleI;var middleStr1='';var middleStr2='';var middleStr='';for(leftI=0;leftI<chunks.length;leftI++){chunkStr=formatSimilarChunk(date1,date2,unzonedDate1,unzonedDate2,chunks[leftI]);if(chunkStr===false){break;}
-leftStr+=chunkStr;}
-for(rightI=chunks.length-1;rightI>leftI;rightI--){chunkStr=formatSimilarChunk(date1,date2,unzonedDate1,unzonedDate2,chunks[rightI]);if(chunkStr===false){break;}
-rightStr=chunkStr+rightStr;}
-for(middleI=leftI;middleI<=rightI;middleI++){middleStr1+=formatDateWithChunk(date1,chunks[middleI]);middleStr2+=formatDateWithChunk(date2,chunks[middleI]);}
-if(middleStr1||middleStr2){if(isRTL){middleStr=middleStr2+separator+middleStr1;}
-else{middleStr=middleStr1+separator+middleStr2;}}
-return leftStr+middleStr+rightStr;}
-var similarUnitMap={Y:'year',M:'month',D:'day',d:'day',A:'second',a:'second',T:'second',t:'second',H:'second',h:'second',m:'second',s:'second'};function formatSimilarChunk(date1,date2,unzonedDate1,unzonedDate2,chunk){var token;var unit;if(typeof chunk==='string'){return chunk;}
-else if((token=chunk.token)){unit=similarUnitMap[token.charAt(0)];if(unit&&unzonedDate1.isSame(unzonedDate2,unit)){return oldMomentFormat(date1,token);}}
-return false;}
-var formatStringChunkCache={};function getFormatStringChunks(formatStr){if(formatStr in formatStringChunkCache){return formatStringChunkCache[formatStr];}
-return(formatStringChunkCache[formatStr]=chunkFormatString(formatStr));}
-function chunkFormatString(formatStr){var chunks=[];var chunker=/\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;var match;while((match=chunker.exec(formatStr))){if(match[1]){chunks.push(match[1]);}
-else if(match[2]){chunks.push({maybe:chunkFormatString(match[2])});}
-else if(match[3]){chunks.push({token:match[3]});}
-else if(match[5]){chunks.push(match[5]);}}
-return chunks;};;FC.Class=Class;function Class(){}
-Class.extend=function(){var len=arguments.length;var i;var members;for(i=0;i<len;i++){members=arguments[i];if(i<len-1){mixIntoClass(this,members);}}
-return extendClass(this,members||{});};Class.mixin=function(members){mixIntoClass(this,members);};function extendClass(superClass,members){var subClass;if(hasOwnProp(members,'constructor')){subClass=members.constructor;}
-if(typeof subClass!=='function'){subClass=members.constructor=function(){superClass.apply(this,arguments);};}
-subClass.prototype=createObject(superClass.prototype);copyOwnProps(members,subClass.prototype);copyNativeMethods(members,subClass.prototype);copyOwnProps(superClass,subClass);return subClass;}
-function mixIntoClass(theClass,members){copyOwnProps(members,theClass.prototype);};;var EmitterMixin=FC.EmitterMixin={on:function(types,handler){var intercept=function(ev,extra){return handler.apply(extra.context||this,extra.args||[]);};if(!handler.guid){handler.guid=$.guid++;}
-intercept.guid=handler.guid;$(this).on(types,intercept);return this;},off:function(types,handler){$(this).off(types,handler);return this;},trigger:function(types){var args=Array.prototype.slice.call(arguments,1);$(this).triggerHandler(types,{args:args});return this;},triggerWith:function(types,context,args){$(this).triggerHandler(types,{context:context,args:args});return this;}};;;var ListenerMixin=FC.ListenerMixin=(function(){var guid=0;var ListenerMixin={listenerId:null,listenTo:function(other,arg,callback){if(typeof arg==='object'){for(var eventName in arg){if(arg.hasOwnProperty(eventName)){this.listenTo(other,eventName,arg[eventName]);}}}
-else if(typeof arg==='string'){other.on(arg+'.'+this.getListenerNamespace(),$.proxy(callback,this));}},stopListeningTo:function(other,eventName){other.off((eventName||'')+'.'+this.getListenerNamespace());},getListenerNamespace:function(){if(this.listenerId==null){this.listenerId=guid++;}
-return'_listener'+this.listenerId;}};return ListenerMixin;})();;;var MouseIgnorerMixin={isIgnoringMouse:false,delayUnignoreMouse:null,initMouseIgnoring:function(delay){this.delayUnignoreMouse=debounce(proxy(this,'unignoreMouse'),delay||1000);},tempIgnoreMouse:function(){this.isIgnoringMouse=true;this.delayUnignoreMouse();},unignoreMouse:function(){this.isIgnoringMouse=false;}};;;var Popover=Class.extend(ListenerMixin,{isHidden:true,options:null,el:null,margin:10,constructor:function(options){this.options=options||{};},show:function(){if(this.isHidden){if(!this.el){this.render();}
-this.el.show();this.position();this.isHidden=false;this.trigger('show');}},hide:function(){if(!this.isHidden){this.el.hide();this.isHidden=true;this.trigger('hide');}},render:function(){var _this=this;var options=this.options;this.el=$('<div class="fc-popover"/>').addClass(options.className||'').css({top:0,left:0}).append(options.content).appendTo(options.parentEl);this.el.on('click','.fc-close',function(){_this.hide();});if(options.autoHide){this.listenTo($(document),'mousedown',this.documentMousedown);}},documentMousedown:function(ev){if(this.el&&!$(ev.target).closest(this.el).length){this.hide();}},removeElement:function(){this.hide();if(this.el){this.el.remove();this.el=null;}
-this.stopListeningTo($(document),'mousedown');},position:function(){var options=this.options;var origin=this.el.offsetParent().offset();var width=this.el.outerWidth();var height=this.el.outerHeight();var windowEl=$(window);var viewportEl=getScrollParent(this.el);var viewportTop;var viewportLeft;var viewportOffset;var top;var left;top=options.top||0;if(options.left!==undefined){left=options.left;}
-else if(options.right!==undefined){left=options.right-width;}
-else{left=0;}
-if(viewportEl.is(window)||viewportEl.is(document)){viewportEl=windowEl;viewportTop=0;viewportLeft=0;}
-else{viewportOffset=viewportEl.offset();viewportTop=viewportOffset.top;viewportLeft=viewportOffset.left;}
-viewportTop+=windowEl.scrollTop();viewportLeft+=windowEl.scrollLeft();if(options.viewportConstrain!==false){top=Math.min(top,viewportTop+viewportEl.outerHeight()-height-this.margin);top=Math.max(top,viewportTop+this.margin);left=Math.min(left,viewportLeft+viewportEl.outerWidth()-width-this.margin);left=Math.max(left,viewportLeft+this.margin);}
-this.el.css({top:top-origin.top,left:left-origin.left});},trigger:function(name){if(this.options[name]){this.options[name].apply(this,Array.prototype.slice.call(arguments,1));}}});;;var CoordCache=FC.CoordCache=Class.extend({els:null,forcedOffsetParentEl:null,origin:null,boundingRect:null,isHorizontal:false,isVertical:false,lefts:null,rights:null,tops:null,bottoms:null,constructor:function(options){this.els=$(options.els);this.isHorizontal=options.isHorizontal;this.isVertical=options.isVertical;this.forcedOffsetParentEl=options.offsetParent?$(options.offsetParent):null;},build:function(){var offsetParentEl=this.forcedOffsetParentEl||this.els.eq(0).offsetParent();this.origin=offsetParentEl.offset();this.boundingRect=this.queryBoundingRect();if(this.isHorizontal){this.buildElHorizontals();}
-if(this.isVertical){this.buildElVerticals();}},clear:function(){this.origin=null;this.boundingRect=null;this.lefts=null;this.rights=null;this.tops=null;this.bottoms=null;},ensureBuilt:function(){if(!this.origin){this.build();}},queryBoundingRect:function(){var scrollParentEl=getScrollParent(this.els.eq(0));if(!scrollParentEl.is(document)){return getClientRect(scrollParentEl);}},buildElHorizontals:function(){var lefts=[];var rights=[];this.els.each(function(i,node){var el=$(node);var left=el.offset().left;var width=el.outerWidth();lefts.push(left);rights.push(left+width);});this.lefts=lefts;this.rights=rights;},buildElVerticals:function(){var tops=[];var bottoms=[];this.els.each(function(i,node){var el=$(node);var top=el.offset().top;var height=el.outerHeight();tops.push(top);bottoms.push(top+height);});this.tops=tops;this.bottoms=bottoms;},getHorizontalIndex:function(leftOffset){this.ensureBuilt();var boundingRect=this.boundingRect;var lefts=this.lefts;var rights=this.rights;var len=lefts.length;var i;if(!boundingRect||(leftOffset>=boundingRect.left&&leftOffset<boundingRect.right)){for(i=0;i<len;i++){if(leftOffset>=lefts[i]&&leftOffset<rights[i]){return i;}}}},getVerticalIndex:function(topOffset){this.ensureBuilt();var boundingRect=this.boundingRect;var tops=this.tops;var bottoms=this.bottoms;var len=tops.length;var i;if(!boundingRect||(topOffset>=boundingRect.top&&topOffset<boundingRect.bottom)){for(i=0;i<len;i++){if(topOffset>=tops[i]&&topOffset<bottoms[i]){return i;}}}},getLeftOffset:function(leftIndex){this.ensureBuilt();return this.lefts[leftIndex];},getLeftPosition:function(leftIndex){this.ensureBuilt();return this.lefts[leftIndex]-this.origin.left;},getRightOffset:function(leftIndex){this.ensureBuilt();return this.rights[leftIndex];},getRightPosition:function(leftIndex){this.ensureBuilt();return this.rights[leftIndex]-this.origin.left;},getWidth:function(leftIndex){this.ensureBuilt();return this.rights[leftIndex]-this.lefts[leftIndex];},getTopOffset:function(topIndex){this.ensureBuilt();return this.tops[topIndex];},getTopPosition:function(topIndex){this.ensureBuilt();return this.tops[topIndex]-this.origin.top;},getBottomOffset:function(topIndex){this.ensureBuilt();return this.bottoms[topIndex];},getBottomPosition:function(topIndex){this.ensureBuilt();return this.bottoms[topIndex]-this.origin.top;},getHeight:function(topIndex){this.ensureBuilt();return this.bottoms[topIndex]-this.tops[topIndex];}});;;var DragListener=FC.DragListener=Class.extend(ListenerMixin,MouseIgnorerMixin,{options:null,subjectEl:null,subjectHref:null,originX:null,originY:null,scrollEl:null,isInteracting:false,isDistanceSurpassed:false,isDelayEnded:false,isDragging:false,isTouch:false,delay:null,delayTimeoutId:null,minDistance:null,handleTouchScrollProxy:null,constructor:function(options){this.options=options||{};this.handleTouchScrollProxy=proxy(this,'handleTouchScroll');this.initMouseIgnoring(500);},startInteraction:function(ev,extraOptions){var isTouch=getEvIsTouch(ev);if(ev.type==='mousedown'){if(this.isIgnoringMouse){return;}
-else if(!isPrimaryMouseButton(ev)){return;}
-else{ev.preventDefault();}}
-if(!this.isInteracting){extraOptions=extraOptions||{};this.delay=firstDefined(extraOptions.delay,this.options.delay,0);this.minDistance=firstDefined(extraOptions.distance,this.options.distance,0);this.subjectEl=this.options.subjectEl;this.isInteracting=true;this.isTouch=isTouch;this.isDelayEnded=false;this.isDistanceSurpassed=false;this.originX=getEvX(ev);this.originY=getEvY(ev);this.scrollEl=getScrollParent($(ev.target));this.bindHandlers();this.initAutoScroll();this.handleInteractionStart(ev);this.startDelay(ev);if(!this.minDistance){this.handleDistanceSurpassed(ev);}}},handleInteractionStart:function(ev){this.trigger('interactionStart',ev);},endInteraction:function(ev,isCancelled){if(this.isInteracting){this.endDrag(ev);if(this.delayTimeoutId){clearTimeout(this.delayTimeoutId);this.delayTimeoutId=null;}
-this.destroyAutoScroll();this.unbindHandlers();this.isInteracting=false;this.handleInteractionEnd(ev,isCancelled);if(this.isTouch){this.tempIgnoreMouse();}}},handleInteractionEnd:function(ev,isCancelled){this.trigger('interactionEnd',ev,isCancelled||false);},bindHandlers:function(){var _this=this;var touchStartIgnores=1;if(this.isTouch){this.listenTo($(document),{touchmove:this.handleTouchMove,touchend:this.endInteraction,touchcancel:this.endInteraction,touchstart:function(ev){if(touchStartIgnores){touchStartIgnores--;}
-else{_this.endInteraction(ev,true);}}});if(!bindAnyScroll(this.handleTouchScrollProxy)&&this.scrollEl){this.listenTo(this.scrollEl,'scroll',this.handleTouchScroll);}}
-else{this.listenTo($(document),{mousemove:this.handleMouseMove,mouseup:this.endInteraction});}
-this.listenTo($(document),{selectstart:preventDefault,contextmenu:preventDefault});},unbindHandlers:function(){this.stopListeningTo($(document));unbindAnyScroll(this.handleTouchScrollProxy);if(this.scrollEl){this.stopListeningTo(this.scrollEl,'scroll');}},startDrag:function(ev,extraOptions){this.startInteraction(ev,extraOptions);if(!this.isDragging){this.isDragging=true;this.handleDragStart(ev);}},handleDragStart:function(ev){this.trigger('dragStart',ev);this.initHrefHack();},handleMove:function(ev){var dx=getEvX(ev)-this.originX;var dy=getEvY(ev)-this.originY;var minDistance=this.minDistance;var distanceSq;if(!this.isDistanceSurpassed){distanceSq=dx*dx+dy*dy;if(distanceSq>=minDistance*minDistance){this.handleDistanceSurpassed(ev);}}
-if(this.isDragging){this.handleDrag(dx,dy,ev);}},handleDrag:function(dx,dy,ev){this.trigger('drag',dx,dy,ev);this.updateAutoScroll(ev);},endDrag:function(ev){if(this.isDragging){this.isDragging=false;this.handleDragEnd(ev);}},handleDragEnd:function(ev){this.trigger('dragEnd',ev);this.destroyHrefHack();},startDelay:function(initialEv){var _this=this;if(this.delay){this.delayTimeoutId=setTimeout(function(){_this.handleDelayEnd(initialEv);},this.delay);}
-else{this.handleDelayEnd(initialEv);}},handleDelayEnd:function(initialEv){this.isDelayEnded=true;if(this.isDistanceSurpassed){this.startDrag(initialEv);}},handleDistanceSurpassed:function(ev){this.isDistanceSurpassed=true;if(this.isDelayEnded){this.startDrag(ev);}},handleTouchMove:function(ev){if(this.isDragging){ev.preventDefault();}
-this.handleMove(ev);},handleMouseMove:function(ev){this.handleMove(ev);},handleTouchScroll:function(ev){if(!this.isDragging){this.endInteraction(ev,true);}},initHrefHack:function(){var subjectEl=this.subjectEl;if((this.subjectHref=subjectEl?subjectEl.attr('href'):null)){subjectEl.removeAttr('href');}},destroyHrefHack:function(){var subjectEl=this.subjectEl;var subjectHref=this.subjectHref;setTimeout(function(){if(subjectHref){subjectEl.attr('href',subjectHref);}},0);},trigger:function(name){if(this.options[name]){this.options[name].apply(this,Array.prototype.slice.call(arguments,1));}
-if(this['_'+name]){this['_'+name].apply(this,Array.prototype.slice.call(arguments,1));}}});;;DragListener.mixin({isAutoScroll:false,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,initAutoScroll:function(){var scrollEl=this.scrollEl;this.isAutoScroll=this.options.scroll&&scrollEl&&!scrollEl.is(window)&&!scrollEl.is(document);if(this.isAutoScroll){this.listenTo(scrollEl,'scroll',debounce(this.handleDebouncedScroll,100));}},destroyAutoScroll:function(){this.endAutoScroll();if(this.isAutoScroll){this.stopListeningTo(this.scrollEl,'scroll');}},computeScrollBounds:function(){if(this.isAutoScroll){this.scrollBounds=getOuterRect(this.scrollEl);}},updateAutoScroll:function(ev){var sensitivity=this.scrollSensitivity;var bounds=this.scrollBounds;var topCloseness,bottomCloseness;var leftCloseness,rightCloseness;var topVel=0;var leftVel=0;if(bounds){topCloseness=(sensitivity-(getEvY(ev)-bounds.top))/sensitivity;bottomCloseness=(sensitivity-(bounds.bottom-getEvY(ev)))/sensitivity;leftCloseness=(sensitivity-(getEvX(ev)-bounds.left))/sensitivity;rightCloseness=(sensitivity-(bounds.right-getEvX(ev)))/sensitivity;if(topCloseness>=0&&topCloseness<=1){topVel=topCloseness*this.scrollSpeed*-1;}
-else if(bottomCloseness>=0&&bottomCloseness<=1){topVel=bottomCloseness*this.scrollSpeed;}
-if(leftCloseness>=0&&leftCloseness<=1){leftVel=leftCloseness*this.scrollSpeed*-1;}
-else if(rightCloseness>=0&&rightCloseness<=1){leftVel=rightCloseness*this.scrollSpeed;}}
-this.setScrollVel(topVel,leftVel);},setScrollVel:function(topVel,leftVel){this.scrollTopVel=topVel;this.scrollLeftVel=leftVel;this.constrainScrollVel();if((this.scrollTopVel||this.scrollLeftVel)&&!this.scrollIntervalId){this.scrollIntervalId=setInterval(proxy(this,'scrollIntervalFunc'),this.scrollIntervalMs);}},constrainScrollVel:function(){var el=this.scrollEl;if(this.scrollTopVel<0){if(el.scrollTop()<=0){this.scrollTopVel=0;}}
-else if(this.scrollTopVel>0){if(el.scrollTop()+el[0].clientHeight>=el[0].scrollHeight){this.scrollTopVel=0;}}
-if(this.scrollLeftVel<0){if(el.scrollLeft()<=0){this.scrollLeftVel=0;}}
-else if(this.scrollLeftVel>0){if(el.scrollLeft()+el[0].clientWidth>=el[0].scrollWidth){this.scrollLeftVel=0;}}},scrollIntervalFunc:function(){var el=this.scrollEl;var frac=this.scrollIntervalMs/1000;if(this.scrollTopVel){el.scrollTop(el.scrollTop()+this.scrollTopVel*frac);}
-if(this.scrollLeftVel){el.scrollLeft(el.scrollLeft()+this.scrollLeftVel*frac);}
-this.constrainScrollVel();if(!this.scrollTopVel&&!this.scrollLeftVel){this.endAutoScroll();}},endAutoScroll:function(){if(this.scrollIntervalId){clearInterval(this.scrollIntervalId);this.scrollIntervalId=null;this.handleScrollEnd();}},handleDebouncedScroll:function(){if(!this.scrollIntervalId){this.handleScrollEnd();}},handleScrollEnd:function(){}});;;var HitDragListener=DragListener.extend({component:null,origHit:null,hit:null,coordAdjust:null,constructor:function(component,options){DragListener.call(this,options);this.component=component;},handleInteractionStart:function(ev){var subjectEl=this.subjectEl;var subjectRect;var origPoint;var point;this.computeCoords();if(ev){origPoint={left:getEvX(ev),top:getEvY(ev)};point=origPoint;if(subjectEl){subjectRect=getOuterRect(subjectEl);point=constrainPoint(point,subjectRect);}
-this.origHit=this.queryHit(point.left,point.top);if(subjectEl&&this.options.subjectCenter){if(this.origHit){subjectRect=intersectRects(this.origHit,subjectRect)||subjectRect;}
-point=getRectCenter(subjectRect);}
-this.coordAdjust=diffPoints(point,origPoint);}
-else{this.origHit=null;this.coordAdjust=null;}
-DragListener.prototype.handleInteractionStart.apply(this,arguments);},computeCoords:function(){this.component.prepareHits();this.computeScrollBounds();},handleDragStart:function(ev){var hit;DragListener.prototype.handleDragStart.apply(this,arguments);hit=this.queryHit(getEvX(ev),getEvY(ev));if(hit){this.handleHitOver(hit);}},handleDrag:function(dx,dy,ev){var hit;DragListener.prototype.handleDrag.apply(this,arguments);hit=this.queryHit(getEvX(ev),getEvY(ev));if(!isHitsEqual(hit,this.hit)){if(this.hit){this.handleHitOut();}
-if(hit){this.handleHitOver(hit);}}},handleDragEnd:function(){this.handleHitDone();DragListener.prototype.handleDragEnd.apply(this,arguments);},handleHitOver:function(hit){var isOrig=isHitsEqual(hit,this.origHit);this.hit=hit;this.trigger('hitOver',this.hit,isOrig,this.origHit);},handleHitOut:function(){if(this.hit){this.trigger('hitOut',this.hit);this.handleHitDone();this.hit=null;}},handleHitDone:function(){if(this.hit){this.trigger('hitDone',this.hit);}},handleInteractionEnd:function(){DragListener.prototype.handleInteractionEnd.apply(this,arguments);this.origHit=null;this.hit=null;this.component.releaseHits();},handleScrollEnd:function(){DragListener.prototype.handleScrollEnd.apply(this,arguments);this.computeCoords();},queryHit:function(left,top){if(this.coordAdjust){left+=this.coordAdjust.left;top+=this.coordAdjust.top;}
-return this.component.queryHit(left,top);}});function isHitsEqual(hit0,hit1){if(!hit0&&!hit1){return true;}
-if(hit0&&hit1){return hit0.component===hit1.component&&isHitPropsWithin(hit0,hit1)&&isHitPropsWithin(hit1,hit0);}
-return false;}
-function isHitPropsWithin(subHit,superHit){for(var propName in subHit){if(!/^(component|left|right|top|bottom)$/.test(propName)){if(subHit[propName]!==superHit[propName]){return false;}}}
-return true;};;var MouseFollower=Class.extend(ListenerMixin,{options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,y0:null,x0:null,topDelta:null,leftDelta:null,isFollowing:false,isHidden:false,isAnimating:false,constructor:function(sourceEl,options){this.options=options=options||{};this.sourceEl=sourceEl;this.parentEl=options.parentEl?$(options.parentEl):sourceEl.parent();},start:function(ev){if(!this.isFollowing){this.isFollowing=true;this.y0=getEvY(ev);this.x0=getEvX(ev);this.topDelta=0;this.leftDelta=0;if(!this.isHidden){this.updatePosition();}
-if(getEvIsTouch(ev)){this.listenTo($(document),'touchmove',this.handleMove);}
-else{this.listenTo($(document),'mousemove',this.handleMove);}}},stop:function(shouldRevert,callback){var _this=this;var revertDuration=this.options.revertDuration;function complete(){this.isAnimating=false;_this.removeElement();this.top0=this.left0=null;if(callback){callback();}}
-if(this.isFollowing&&!this.isAnimating){this.isFollowing=false;this.stopListeningTo($(document));if(shouldRevert&&revertDuration&&!this.isHidden){this.isAnimating=true;this.el.animate({top:this.top0,left:this.left0},{duration:revertDuration,complete:complete});}
-else{complete();}}},getEl:function(){var el=this.el;if(!el){this.sourceEl.width();el=this.el=this.sourceEl.clone().addClass(this.options.additionalClass||'').css({position:'absolute',visibility:'',display:this.isHidden?'none':'',margin:0,right:'auto',bottom:'auto',width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||'',zIndex:this.options.zIndex});el.addClass('fc-unselectable');el.appendTo(this.parentEl);}
-return el;},removeElement:function(){if(this.el){this.el.remove();this.el=null;}},updatePosition:function(){var sourceOffset;var origin;this.getEl();if(this.top0===null){this.sourceEl.width();sourceOffset=this.sourceEl.offset();origin=this.el.offsetParent().offset();this.top0=sourceOffset.top-origin.top;this.left0=sourceOffset.left-origin.left;}
-this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta});},handleMove:function(ev){this.topDelta=getEvY(ev)-this.y0;this.leftDelta=getEvX(ev)-this.x0;if(!this.isHidden){this.updatePosition();}},hide:function(){if(!this.isHidden){this.isHidden=true;if(this.el){this.el.hide();}}},show:function(){if(this.isHidden){this.isHidden=false;this.updatePosition();this.getEl().show();}}});;;var Grid=FC.Grid=Class.extend(ListenerMixin,MouseIgnorerMixin,{view:null,isRTL:null,start:null,end:null,el:null,elsByFill:null,eventTimeFormat:null,displayEventTime:null,displayEventEnd:null,minResizeDuration:null,largeUnit:null,dayDragListener:null,segDragListener:null,segResizeListener:null,externalDragListener:null,constructor:function(view){this.view=view;this.isRTL=view.opt('isRTL');this.elsByFill={};this.dayDragListener=this.buildDayDragListener();this.initMouseIgnoring();},computeEventTimeFormat:function(){return this.view.opt('smallTimeFormat');},computeDisplayEventTime:function(){return true;},computeDisplayEventEnd:function(){return true;},setRange:function(range){this.start=range.start.clone();this.end=range.end.clone();this.rangeUpdated();this.processRangeOptions();},rangeUpdated:function(){},processRangeOptions:function(){var view=this.view;var displayEventTime;var displayEventEnd;this.eventTimeFormat=view.opt('eventTimeFormat')||view.opt('timeFormat')||this.computeEventTimeFormat();displayEventTime=view.opt('displayEventTime');if(displayEventTime==null){displayEventTime=this.computeDisplayEventTime();}
-displayEventEnd=view.opt('displayEventEnd');if(displayEventEnd==null){displayEventEnd=this.computeDisplayEventEnd();}
-this.displayEventTime=displayEventTime;this.displayEventEnd=displayEventEnd;},spanToSegs:function(span){},diffDates:function(a,b){if(this.largeUnit){return diffByUnit(a,b,this.largeUnit);}
-else{return diffDayTime(a,b);}},prepareHits:function(){},releaseHits:function(){},queryHit:function(leftOffset,topOffset){},getHitSpan:function(hit){},getHitEl:function(hit){},setElement:function(el){this.el=el;preventSelection(el);this.bindDayHandler('touchstart',this.dayTouchStart);this.bindDayHandler('mousedown',this.dayMousedown);this.bindSegHandlers();this.bindGlobalHandlers();},bindDayHandler:function(name,handler){var _this=this;this.el.on(name,function(ev){if(!$(ev.target).is('.fc-event-container *, .fc-more')&&!$(ev.target).closest('.fc-popover').length){return handler.call(_this,ev);}});},removeElement:function(){this.unbindGlobalHandlers();this.clearDragListeners();this.el.remove();},renderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},bindGlobalHandlers:function(){this.listenTo($(document),{dragstart:this.externalDragStart,sortstart:this.externalDragStart});},unbindGlobalHandlers:function(){this.stopListeningTo($(document));},dayMousedown:function(ev){if(!this.isIgnoringMouse){this.dayDragListener.startInteraction(ev,{});}},dayTouchStart:function(ev){var view=this.view;if(view.isSelected||view.selectedEvent){this.tempIgnoreMouse();}
-this.dayDragListener.startInteraction(ev,{delay:this.view.opt('longPressDelay')});},buildDayDragListener:function(){var _this=this;var view=this.view;var isSelectable=view.opt('selectable');var dayClickHit;var selectionSpan;var dragListener=new HitDragListener(this,{scroll:view.opt('dragScroll'),interactionStart:function(){dayClickHit=dragListener.origHit;},dragStart:function(){view.unselect();},hitOver:function(hit,isOrig,origHit){if(origHit){if(!isOrig){dayClickHit=null;}
-if(isSelectable){selectionSpan=_this.computeSelection(_this.getHitSpan(origHit),_this.getHitSpan(hit));if(selectionSpan){_this.renderSelection(selectionSpan);}
-else if(selectionSpan===false){disableCursor();}}}},hitOut:function(){dayClickHit=null;selectionSpan=null;_this.unrenderSelection();enableCursor();},interactionEnd:function(ev,isCancelled){if(!isCancelled){if(dayClickHit&&!_this.isIgnoringMouse){view.triggerDayClick(_this.getHitSpan(dayClickHit),_this.getHitEl(dayClickHit),ev);}
-if(selectionSpan){view.reportSelection(selectionSpan,ev);}
-enableCursor();}}});return dragListener;},clearDragListeners:function(){this.dayDragListener.endInteraction();if(this.segDragListener){this.segDragListener.endInteraction();}
-if(this.segResizeListener){this.segResizeListener.endInteraction();}
-if(this.externalDragListener){this.externalDragListener.endInteraction();}},renderEventLocationHelper:function(eventLocation,sourceSeg){var fakeEvent=this.fabricateHelperEvent(eventLocation,sourceSeg);return this.renderHelper(fakeEvent,sourceSeg);},fabricateHelperEvent:function(eventLocation,sourceSeg){var fakeEvent=sourceSeg?createObject(sourceSeg.event):{};fakeEvent.start=eventLocation.start.clone();fakeEvent.end=eventLocation.end?eventLocation.end.clone():null;fakeEvent.allDay=null;this.view.calendar.normalizeEventDates(fakeEvent);fakeEvent.className=(fakeEvent.className||[]).concat('fc-helper');if(!sourceSeg){fakeEvent.editable=false;}
-return fakeEvent;},renderHelper:function(eventLocation,sourceSeg){},unrenderHelper:function(){},renderSelection:function(span){this.renderHighlight(span);},unrenderSelection:function(){this.unrenderHighlight();},computeSelection:function(span0,span1){var span=this.computeSelectionSpan(span0,span1);if(span&&!this.view.calendar.isSelectionSpanAllowed(span)){return false;}
-return span;},computeSelectionSpan:function(span0,span1){var dates=[span0.start,span0.end,span1.start,span1.end];dates.sort(compareNumbers);return{start:dates[0].clone(),end:dates[3].clone()};},renderHighlight:function(span){this.renderFill('highlight',this.spanToSegs(span));},unrenderHighlight:function(){this.unrenderFill('highlight');},highlightSegClasses:function(){return['fc-highlight'];},renderBusinessHours:function(){},unrenderBusinessHours:function(){},getNowIndicatorUnit:function(){},renderNowIndicator:function(date){},unrenderNowIndicator:function(){},renderFill:function(type,segs){},unrenderFill:function(type){var el=this.elsByFill[type];if(el){el.remove();delete this.elsByFill[type];}},renderFillSegEls:function(type,segs){var _this=this;var segElMethod=this[type+'SegEl'];var html='';var renderedSegs=[];var i;if(segs.length){for(i=0;i<segs.length;i++){html+=this.fillSegHtml(type,segs[i]);}
-$(html).each(function(i,node){var seg=segs[i];var el=$(node);if(segElMethod){el=segElMethod.call(_this,seg,el);}
-if(el){el=$(el);if(el.is(_this.fillSegTag)){seg.el=el;renderedSegs.push(seg);}}});}
-return renderedSegs;},fillSegTag:'div',fillSegHtml:function(type,seg){var classesMethod=this[type+'SegClasses'];var cssMethod=this[type+'SegCss'];var classes=classesMethod?classesMethod.call(this,seg):[];var css=cssToStr(cssMethod?cssMethod.call(this,seg):{});return'<'+this.fillSegTag+
-(classes.length?' class="'+classes.join(' ')+'"':'')+
-(css?' style="'+css+'"':'')+' />';},getDayClasses:function(date){var view=this.view;var today=view.calendar.getNow();var classes=['fc-'+dayIDs[date.day()]];if(view.intervalDuration.as('months')==1&&date.month()!=view.intervalStart.month()){classes.push('fc-other-month');}
-if(date.isSame(today,'day')){classes.push('fc-today',view.highlightStateClass);}
-else if(date<today){classes.push('fc-past');}
-else{classes.push('fc-future');}
-return classes;}});;;Grid.mixin({mousedOverSeg:null,isDraggingSeg:false,isResizingSeg:false,isDraggingExternal:false,segs:null,renderEvents:function(events){var bgEvents=[];var fgEvents=[];var i;for(i=0;i<events.length;i++){(isBgEvent(events[i])?bgEvents:fgEvents).push(events[i]);}
-this.segs=[].concat(this.renderBgEvents(bgEvents),this.renderFgEvents(fgEvents));},renderBgEvents:function(events){var segs=this.eventsToSegs(events);return this.renderBgSegs(segs)||segs;},renderFgEvents:function(events){var segs=this.eventsToSegs(events);return this.renderFgSegs(segs)||segs;},unrenderEvents:function(){this.handleSegMouseout();this.clearDragListeners();this.unrenderFgSegs();this.unrenderBgSegs();this.segs=null;},getEventSegs:function(){return this.segs||[];},renderFgSegs:function(segs){},unrenderFgSegs:function(){},renderFgSegEls:function(segs,disableResizing){var view=this.view;var html='';var renderedSegs=[];var i;if(segs.length){for(i=0;i<segs.length;i++){html+=this.fgSegHtml(segs[i],disableResizing);}
-$(html).each(function(i,node){var seg=segs[i];var el=view.resolveEventEl(seg.event,$(node));if(el){el.data('fc-seg',seg);seg.el=el;renderedSegs.push(seg);}});}
-return renderedSegs;},fgSegHtml:function(seg,disableResizing){},renderBgSegs:function(segs){return this.renderFill('bgEvent',segs);},unrenderBgSegs:function(){this.unrenderFill('bgEvent');},bgEventSegEl:function(seg,el){return this.view.resolveEventEl(seg.event,el);},bgEventSegClasses:function(seg){var event=seg.event;var source=event.source||{};return['fc-bgevent'].concat(event.className,source.className||[]);},bgEventSegCss:function(seg){return{'background-color':this.getSegSkinCss(seg)['background-color']};},businessHoursSegClasses:function(seg){return['fc-nonbusiness','fc-bgevent'];},bindSegHandlers:function(){this.bindSegHandler('touchstart',this.handleSegTouchStart);this.bindSegHandler('touchend',this.handleSegTouchEnd);this.bindSegHandler('mouseenter',this.handleSegMouseover);this.bindSegHandler('mouseleave',this.handleSegMouseout);this.bindSegHandler('mousedown',this.handleSegMousedown);this.bindSegHandler('click',this.handleSegClick);},bindSegHandler:function(name,handler){var _this=this;this.el.on(name,'.fc-event-container > *',function(ev){var seg=$(this).data('fc-seg');if(seg&&!_this.isDraggingSeg&&!_this.isResizingSeg){return handler.call(_this,seg,ev);}});},handleSegClick:function(seg,ev){return this.view.trigger('eventClick',seg.el[0],seg.event,ev);},handleSegMouseover:function(seg,ev){if(!this.isIgnoringMouse&&!this.mousedOverSeg){this.mousedOverSeg=seg;seg.el.addClass('fc-allow-mouse-resize');this.view.trigger('eventMouseover',seg.el[0],seg.event,ev);}},handleSegMouseout:function(seg,ev){ev=ev||{};if(this.mousedOverSeg){seg=seg||this.mousedOverSeg;this.mousedOverSeg=null;seg.el.removeClass('fc-allow-mouse-resize');this.view.trigger('eventMouseout',seg.el[0],seg.event,ev);}},handleSegMousedown:function(seg,ev){var isResizing=this.startSegResize(seg,ev,{distance:5});if(!isResizing&&this.view.isEventDraggable(seg.event)){this.buildSegDragListener(seg).startInteraction(ev,{distance:5});}},handleSegTouchStart:function(seg,ev){var view=this.view;var event=seg.event;var isSelected=view.isEventSelected(event);var isDraggable=view.isEventDraggable(event);var isResizable=view.isEventResizable(event);var isResizing=false;var dragListener;if(isSelected&&isResizable){isResizing=this.startSegResize(seg,ev);}
-if(!isResizing&&(isDraggable||isResizable)){dragListener=isDraggable?this.buildSegDragListener(seg):this.buildSegSelectListener(seg);dragListener.startInteraction(ev,{delay:isSelected?0:this.view.opt('longPressDelay')});}
-this.tempIgnoreMouse();},handleSegTouchEnd:function(seg,ev){this.tempIgnoreMouse();},startSegResize:function(seg,ev,dragOptions){if($(ev.target).is('.fc-resizer')){this.buildSegResizeListener(seg,$(ev.target).is('.fc-start-resizer')).startInteraction(ev,dragOptions);return true;}
-return false;},buildSegDragListener:function(seg){var _this=this;var view=this.view;var calendar=view.calendar;var el=seg.el;var event=seg.event;var isDragging;var mouseFollower;var dropLocation;if(this.segDragListener){return this.segDragListener;}
-var dragListener=this.segDragListener=new HitDragListener(view,{scroll:view.opt('dragScroll'),subjectEl:el,subjectCenter:true,interactionStart:function(ev){isDragging=false;mouseFollower=new MouseFollower(seg.el,{additionalClass:'fc-dragging',parentEl:view.el,opacity:dragListener.isTouch?null:view.opt('dragOpacity'),revertDuration:view.opt('dragRevertDuration'),zIndex:2});mouseFollower.hide();mouseFollower.start(ev);},dragStart:function(ev){if(dragListener.isTouch&&!view.isEventSelected(event)){view.selectEvent(event);}
-isDragging=true;_this.handleSegMouseout(seg,ev);_this.segDragStart(seg,ev);view.hideEvent(event);},hitOver:function(hit,isOrig,origHit){var dragHelperEls;if(seg.hit){origHit=seg.hit;}
-dropLocation=_this.computeEventDrop(origHit.component.getHitSpan(origHit),hit.component.getHitSpan(hit),event);if(dropLocation&&!calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation),event)){disableCursor();dropLocation=null;}
-if(dropLocation&&(dragHelperEls=view.renderDrag(dropLocation,seg))){dragHelperEls.addClass('fc-dragging');if(!dragListener.isTouch){_this.applyDragOpacity(dragHelperEls);}
-mouseFollower.hide();}
-else{mouseFollower.show();}
-if(isOrig){dropLocation=null;}},hitOut:function(){view.unrenderDrag();mouseFollower.show();dropLocation=null;},hitDone:function(){enableCursor();},interactionEnd:function(ev){mouseFollower.stop(!dropLocation,function(){if(isDragging){view.unrenderDrag();view.showEvent(event);_this.segDragStop(seg,ev);}
-if(dropLocation){view.reportEventDrop(event,dropLocation,this.largeUnit,el,ev);}});_this.segDragListener=null;}});return dragListener;},buildSegSelectListener:function(seg){var _this=this;var view=this.view;var event=seg.event;if(this.segDragListener){return this.segDragListener;}
-var dragListener=this.segDragListener=new DragListener({dragStart:function(ev){if(dragListener.isTouch&&!view.isEventSelected(event)){view.selectEvent(event);}},interactionEnd:function(ev){_this.segDragListener=null;}});return dragListener;},segDragStart:function(seg,ev){this.isDraggingSeg=true;this.view.trigger('eventDragStart',seg.el[0],seg.event,ev,{});},segDragStop:function(seg,ev){this.isDraggingSeg=false;this.view.trigger('eventDragStop',seg.el[0],seg.event,ev,{});},computeEventDrop:function(startSpan,endSpan,event){var calendar=this.view.calendar;var dragStart=startSpan.start;var dragEnd=endSpan.start;var delta;var dropLocation;if(dragStart.hasTime()===dragEnd.hasTime()){delta=this.diffDates(dragEnd,dragStart);if(event.allDay&&durationHasTime(delta)){dropLocation={start:event.start.clone(),end:calendar.getEventEnd(event),allDay:false};calendar.normalizeEventTimes(dropLocation);}
-else{dropLocation={start:event.start.clone(),end:event.end?event.end.clone():null,allDay:event.allDay};}
-dropLocation.start.add(delta);if(dropLocation.end){dropLocation.end.add(delta);}}
-else{dropLocation={start:dragEnd.clone(),end:null,allDay:!dragEnd.hasTime()};}
-return dropLocation;},applyDragOpacity:function(els){var opacity=this.view.opt('dragOpacity');if(opacity!=null){els.each(function(i,node){node.style.opacity=opacity;});}},externalDragStart:function(ev,ui){var view=this.view;var el;var accept;if(view.opt('droppable')){el=$((ui?ui.item:null)||ev.target);accept=view.opt('dropAccept');if($.isFunction(accept)?accept.call(el[0],el):el.is(accept)){if(!this.isDraggingExternal){this.listenToExternalDrag(el,ev,ui);}}}},listenToExternalDrag:function(el,ev,ui){var _this=this;var calendar=this.view.calendar;var meta=getDraggedElMeta(el);var dropLocation;var dragListener=_this.externalDragListener=new HitDragListener(this,{interactionStart:function(){_this.isDraggingExternal=true;},hitOver:function(hit){dropLocation=_this.computeExternalDrop(hit.component.getHitSpan(hit),meta);if(dropLocation&&!calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation),dropLocation,meta.eventProps)){disableCursor();dropLocation=null;}
-if(dropLocation){_this.renderDrag(dropLocation);}},hitOut:function(){dropLocation=null;},hitDone:function(){enableCursor();_this.unrenderDrag();},interactionEnd:function(ev){if(dropLocation){_this.view.reportExternalDrop(meta,dropLocation,el,ev,ui);}
-_this.isDraggingExternal=false;_this.externalDragListener=null;}});dragListener.startDrag(ev);},computeExternalDrop:function(span,meta){var calendar=this.view.calendar;var dropLocation={start:calendar.applyTimezone(span.start),end:null};if(meta.startTime&&!dropLocation.start.hasTime()){dropLocation.start.time(meta.startTime);}
-if(meta.duration){dropLocation.end=dropLocation.start.clone().add(meta.duration);}
-return dropLocation;},renderDrag:function(dropLocation,seg){},unrenderDrag:function(){},buildSegResizeListener:function(seg,isStart){var _this=this;var view=this.view;var calendar=view.calendar;var el=seg.el;var event=seg.event;var eventEnd=calendar.getEventEnd(event);var isDragging;var resizeLocation;var dragListener=this.segResizeListener=new HitDragListener(this,{scroll:view.opt('dragScroll'),subjectEl:el,interactionStart:function(){isDragging=false;},dragStart:function(ev){isDragging=true;_this.handleSegMouseout(seg,ev);_this.segResizeStart(seg,ev);},hitOver:function(hit,isOrig,origHit){var origHitSpan=_this.getHitSpan(origHit);var hitSpan=_this.getHitSpan(hit);resizeLocation=isStart?_this.computeEventStartResize(origHitSpan,hitSpan,event):_this.computeEventEndResize(origHitSpan,hitSpan,event);if(resizeLocation){if(!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation),event)){disableCursor();resizeLocation=null;}
-else if(resizeLocation.start.isSame(event.start)&&resizeLocation.end.isSame(eventEnd)){resizeLocation=null;}}
-if(resizeLocation){view.hideEvent(event);_this.renderEventResize(resizeLocation,seg);}},hitOut:function(){resizeLocation=null;},hitDone:function(){_this.unrenderEventResize();view.showEvent(event);enableCursor();},interactionEnd:function(ev){if(isDragging){_this.segResizeStop(seg,ev);}
-if(resizeLocation){view.reportEventResize(event,resizeLocation,this.largeUnit,el,ev);}
-_this.segResizeListener=null;}});return dragListener;},segResizeStart:function(seg,ev){this.isResizingSeg=true;this.view.trigger('eventResizeStart',seg.el[0],seg.event,ev,{});},segResizeStop:function(seg,ev){this.isResizingSeg=false;this.view.trigger('eventResizeStop',seg.el[0],seg.event,ev,{});},computeEventStartResize:function(startSpan,endSpan,event){return this.computeEventResize('start',startSpan,endSpan,event);},computeEventEndResize:function(startSpan,endSpan,event){return this.computeEventResize('end',startSpan,endSpan,event);},computeEventResize:function(type,startSpan,endSpan,event){var calendar=this.view.calendar;var delta=this.diffDates(endSpan[type],startSpan[type]);var resizeLocation;var defaultDuration;resizeLocation={start:event.start.clone(),end:calendar.getEventEnd(event),allDay:event.allDay};if(resizeLocation.allDay&&durationHasTime(delta)){resizeLocation.allDay=false;calendar.normalizeEventTimes(resizeLocation);}
-resizeLocation[type].add(delta);if(!resizeLocation.start.isBefore(resizeLocation.end)){defaultDuration=this.minResizeDuration||(event.allDay?calendar.defaultAllDayEventDuration:calendar.defaultTimedEventDuration);if(type=='start'){resizeLocation.start=resizeLocation.end.clone().subtract(defaultDuration);}
-else{resizeLocation.end=resizeLocation.start.clone().add(defaultDuration);}}
-return resizeLocation;},renderEventResize:function(range,seg){},unrenderEventResize:function(){},getEventTimeText:function(range,formatStr,displayEnd){if(formatStr==null){formatStr=this.eventTimeFormat;}
-if(displayEnd==null){displayEnd=this.displayEventEnd;}
-if(this.displayEventTime&&range.start.hasTime()){if(displayEnd&&range.end){return this.view.formatRange(range,formatStr);}
-else{return range.start.format(formatStr);}}
-return'';},getSegClasses:function(seg,isDraggable,isResizable){var view=this.view;var event=seg.event;var classes=['fc-event',seg.isStart?'fc-start':'fc-not-start',seg.isEnd?'fc-end':'fc-not-end'].concat(event.className,event.source?event.source.className:[]);if(isDraggable){classes.push('fc-draggable');}
-if(isResizable){classes.push('fc-resizable');}
-if(view.isEventSelected(event)){classes.push('fc-selected');}
-return classes;},getSegSkinCss:function(seg){var event=seg.event;var view=this.view;var source=event.source||{};var eventColor=event.color;var sourceColor=source.color;var optionColor=view.opt('eventColor');return{'background-color':event.backgroundColor||eventColor||source.backgroundColor||sourceColor||view.opt('eventBackgroundColor')||optionColor,'border-color':event.borderColor||eventColor||source.borderColor||sourceColor||view.opt('eventBorderColor')||optionColor,color:event.textColor||source.textColor||view.opt('eventTextColor')};},eventToSegs:function(event){return this.eventsToSegs([event]);},eventToSpan:function(event){return this.eventToSpans(event)[0];},eventToSpans:function(event){var range=this.eventToRange(event);return this.eventRangeToSpans(range,event);},eventsToSegs:function(allEvents,segSliceFunc){var _this=this;var eventsById=groupEventsById(allEvents);var segs=[];$.each(eventsById,function(id,events){var ranges=[];var i;for(i=0;i<events.length;i++){ranges.push(_this.eventToRange(events[i]));}
-if(isInverseBgEvent(events[0])){ranges=_this.invertRanges(ranges);for(i=0;i<ranges.length;i++){segs.push.apply(segs,_this.eventRangeToSegs(ranges[i],events[0],segSliceFunc));}}
-else{for(i=0;i<ranges.length;i++){segs.push.apply(segs,_this.eventRangeToSegs(ranges[i],events[i],segSliceFunc));}}});return segs;},eventToRange:function(event){return{start:event.start.clone().stripZone(),end:(event.end?event.end.clone():this.view.calendar.getDefaultEventEnd(event.allDay!=null?event.allDay:!event.start.hasTime(),event.start)).stripZone()};},eventRangeToSegs:function(range,event,segSliceFunc){var spans=this.eventRangeToSpans(range,event);var segs=[];var i;for(i=0;i<spans.length;i++){segs.push.apply(segs,this.eventSpanToSegs(spans[i],event,segSliceFunc));}
-return segs;},eventRangeToSpans:function(range,event){return[$.extend({},range)];},eventSpanToSegs:function(span,event,segSliceFunc){var segs=segSliceFunc?segSliceFunc(span):this.spanToSegs(span);var i,seg;for(i=0;i<segs.length;i++){seg=segs[i];seg.event=event;seg.eventStartMS=+span.start;seg.eventDurationMS=span.end-span.start;}
-return segs;},invertRanges:function(ranges){var view=this.view;var viewStart=view.start.clone();var viewEnd=view.end.clone();var inverseRanges=[];var start=viewStart;var i,range;ranges.sort(compareRanges);for(i=0;i<ranges.length;i++){range=ranges[i];if(range.start>start){inverseRanges.push({start:start,end:range.start});}
-start=range.end;}
-if(start<viewEnd){inverseRanges.push({start:start,end:viewEnd});}
-return inverseRanges;},sortEventSegs:function(segs){segs.sort(proxy(this,'compareEventSegs'));},compareEventSegs:function(seg1,seg2){return seg1.eventStartMS-seg2.eventStartMS||seg2.eventDurationMS-seg1.eventDurationMS||seg2.event.allDay-seg1.event.allDay||compareByFieldSpecs(seg1.event,seg2.event,this.view.eventOrderSpecs);}});function isBgEvent(event){var rendering=getEventRendering(event);return rendering==='background'||rendering==='inverse-background';}
-FC.isBgEvent=isBgEvent;function isInverseBgEvent(event){return getEventRendering(event)==='inverse-background';}
-function getEventRendering(event){return firstDefined((event.source||{}).rendering,event.rendering);}
-function groupEventsById(events){var eventsById={};var i,event;for(i=0;i<events.length;i++){event=events[i];(eventsById[event._id]||(eventsById[event._id]=[])).push(event);}
-return eventsById;}
-function compareRanges(range1,range2){return range1.start-range2.start;}
-FC.dataAttrPrefix='';function getDraggedElMeta(el){var prefix=FC.dataAttrPrefix;var eventProps;var startTime;var duration;var stick;if(prefix){prefix+='-';}
-eventProps=el.data(prefix+'event')||null;if(eventProps){if(typeof eventProps==='object'){eventProps=$.extend({},eventProps);}
-else{eventProps={};}
-startTime=eventProps.start;if(startTime==null){startTime=eventProps.time;}
-duration=eventProps.duration;stick=eventProps.stick;delete eventProps.start;delete eventProps.time;delete eventProps.duration;delete eventProps.stick;}
-if(startTime==null){startTime=el.data(prefix+'start');}
-if(startTime==null){startTime=el.data(prefix+'time');}
-if(duration==null){duration=el.data(prefix+'duration');}
-if(stick==null){stick=el.data(prefix+'stick');}
-startTime=startTime!=null?moment.duration(startTime):null;duration=duration!=null?moment.duration(duration):null;stick=Boolean(stick);return{eventProps:eventProps,startTime:startTime,duration:duration,stick:stick};};;var DayTableMixin=FC.DayTableMixin={breakOnWeeks:false,dayDates:null,dayIndices:null,daysPerRow:null,rowCnt:null,colCnt:null,colHeadFormat:null,updateDayTable:function(){var view=this.view;var date=this.start.clone();var dayIndex=-1;var dayIndices=[];var dayDates=[];var daysPerRow;var firstDay;var rowCnt;while(date.isBefore(this.end)){if(view.isHiddenDay(date)){dayIndices.push(dayIndex+0.5);}
-else{dayIndex++;dayIndices.push(dayIndex);dayDates.push(date.clone());}
-date.add(1,'days');}
-if(this.breakOnWeeks){firstDay=dayDates[0].day();for(daysPerRow=1;daysPerRow<dayDates.length;daysPerRow++){if(dayDates[daysPerRow].day()==firstDay){break;}}
-rowCnt=Math.ceil(dayDates.length/daysPerRow);}
-else{rowCnt=1;daysPerRow=dayDates.length;}
-this.dayDates=dayDates;this.dayIndices=dayIndices;this.daysPerRow=daysPerRow;this.rowCnt=rowCnt;this.updateDayTableCols();},updateDayTableCols:function(){this.colCnt=this.computeColCnt();this.colHeadFormat=this.view.opt('columnFormat')||this.computeColHeadFormat();},computeColCnt:function(){return this.daysPerRow;},getCellDate:function(row,col){return this.dayDates[this.getCellDayIndex(row,col)].clone();},getCellRange:function(row,col){var start=this.getCellDate(row,col);var end=start.clone().add(1,'days');return{start:start,end:end};},getCellDayIndex:function(row,col){return row*this.daysPerRow+this.getColDayIndex(col);},getColDayIndex:function(col){if(this.isRTL){return this.colCnt-1-col;}
-else{return col;}},getDateDayIndex:function(date){var dayIndices=this.dayIndices;var dayOffset=date.diff(this.start,'days');if(dayOffset<0){return dayIndices[0]-1;}
-else if(dayOffset>=dayIndices.length){return dayIndices[dayIndices.length-1]+1;}
-else{return dayIndices[dayOffset];}},computeColHeadFormat:function(){if(this.rowCnt>1||this.colCnt>10){return'ddd';}
-else if(this.colCnt>1){return this.view.opt('dayOfMonthFormat');}
-else{return'dddd';}},sliceRangeByRow:function(range){var daysPerRow=this.daysPerRow;var normalRange=this.view.computeDayRange(range);var rangeFirst=this.getDateDayIndex(normalRange.start);var rangeLast=this.getDateDayIndex(normalRange.end.clone().subtract(1,'days'));var segs=[];var row;var rowFirst,rowLast;var segFirst,segLast;for(row=0;row<this.rowCnt;row++){rowFirst=row*daysPerRow;rowLast=rowFirst+daysPerRow-1;segFirst=Math.max(rangeFirst,rowFirst);segLast=Math.min(rangeLast,rowLast);segFirst=Math.ceil(segFirst);segLast=Math.floor(segLast);if(segFirst<=segLast){segs.push({row:row,firstRowDayIndex:segFirst-rowFirst,lastRowDayIndex:segLast-rowFirst,isStart:segFirst===rangeFirst,isEnd:segLast===rangeLast});}}
-return segs;},sliceRangeByDay:function(range){var daysPerRow=this.daysPerRow;var normalRange=this.view.computeDayRange(range);var rangeFirst=this.getDateDayIndex(normalRange.start);var rangeLast=this.getDateDayIndex(normalRange.end.clone().subtract(1,'days'));var segs=[];var row;var rowFirst,rowLast;var i;var segFirst,segLast;for(row=0;row<this.rowCnt;row++){rowFirst=row*daysPerRow;rowLast=rowFirst+daysPerRow-1;for(i=rowFirst;i<=rowLast;i++){segFirst=Math.max(rangeFirst,i);segLast=Math.min(rangeLast,i);segFirst=Math.ceil(segFirst);segLast=Math.floor(segLast);if(segFirst<=segLast){segs.push({row:row,firstRowDayIndex:segFirst-rowFirst,lastRowDayIndex:segLast-rowFirst,isStart:segFirst===rangeFirst,isEnd:segLast===rangeLast});}}}
-return segs;},renderHeadHtml:function(){var view=this.view;return''+'<div class="fc-row '+view.widgetHeaderClass+'">'+'<table>'+'<thead>'+
-this.renderHeadTrHtml()+'</thead>'+'</table>'+'</div>';},renderHeadIntroHtml:function(){return this.renderIntroHtml();},renderHeadTrHtml:function(){return''+'<tr>'+
-(this.isRTL?'':this.renderHeadIntroHtml())+
-this.renderHeadDateCellsHtml()+
-(this.isRTL?this.renderHeadIntroHtml():'')+'</tr>';},renderHeadDateCellsHtml:function(){var htmls=[];var col,date;for(col=0;col<this.colCnt;col++){date=this.getCellDate(0,col);htmls.push(this.renderHeadDateCellHtml(date));}
-return htmls.join('');},renderHeadDateCellHtml:function(date,colspan,otherAttrs){var view=this.view;return''+'<th class="fc-day-header '+view.widgetHeaderClass+' fc-'+dayIDs[date.day()]+'"'+
-(this.rowCnt==1?' data-date="'+date.format('YYYY-MM-DD')+'"':'')+
-(colspan>1?' colspan="'+colspan+'"':'')+
-(otherAttrs?' '+otherAttrs:'')+'>'+
-htmlEscape(date.format(this.colHeadFormat))+'</th>';},renderBgTrHtml:function(row){return''+'<tr>'+
-(this.isRTL?'':this.renderBgIntroHtml(row))+
-this.renderBgCellsHtml(row)+
-(this.isRTL?this.renderBgIntroHtml(row):'')+'</tr>';},renderBgIntroHtml:function(row){return this.renderIntroHtml();},renderBgCellsHtml:function(row){var htmls=[];var col,date;for(col=0;col<this.colCnt;col++){date=this.getCellDate(row,col);htmls.push(this.renderBgCellHtml(date));}
-return htmls.join('');},renderBgCellHtml:function(date,otherAttrs){var view=this.view;var classes=this.getDayClasses(date);classes.unshift('fc-day',view.widgetContentClass);return'<td class="'+classes.join(' ')+'"'+' data-date="'+date.format('YYYY-MM-DD')+'"'+
-(otherAttrs?' '+otherAttrs:'')+'></td>';},renderIntroHtml:function(){},bookendCells:function(trEl){var introHtml=this.renderIntroHtml();if(introHtml){if(this.isRTL){trEl.append(introHtml);}
-else{trEl.prepend(introHtml);}}}};;;var DayGrid=FC.DayGrid=Grid.extend(DayTableMixin,{numbersVisible:false,bottomCoordPadding:0,rowEls:null,cellEls:null,helperEls:null,rowCoordCache:null,colCoordCache:null,renderDates:function(isRigid){var view=this.view;var rowCnt=this.rowCnt;var colCnt=this.colCnt;var html='';var row;var col;for(row=0;row<rowCnt;row++){html+=this.renderDayRowHtml(row,isRigid);}
-this.el.html(html);this.rowEls=this.el.find('.fc-row');this.cellEls=this.el.find('.fc-day');this.rowCoordCache=new CoordCache({els:this.rowEls,isVertical:true});this.colCoordCache=new CoordCache({els:this.cellEls.slice(0,this.colCnt),isHorizontal:true});for(row=0;row<rowCnt;row++){for(col=0;col<colCnt;col++){view.trigger('dayRender',null,this.getCellDate(row,col),this.getCellEl(row,col));}}},unrenderDates:function(){this.removeSegPopover();},renderBusinessHours:function(){var events=this.view.calendar.getBusinessHoursEvents(true);var segs=this.eventsToSegs(events);this.renderFill('businessHours',segs,'bgevent');},renderDayRowHtml:function(row,isRigid){var view=this.view;var classes=['fc-row','fc-week',view.widgetContentClass];if(isRigid){classes.push('fc-rigid');}
-return''+'<div class="'+classes.join(' ')+'">'+'<div class="fc-bg">'+'<table>'+
-this.renderBgTrHtml(row)+'</table>'+'</div>'+'<div class="fc-content-skeleton">'+'<table>'+
-(this.numbersVisible?'<thead>'+
-this.renderNumberTrHtml(row)+'</thead>':'')+'</table>'+'</div>'+'</div>';},renderNumberTrHtml:function(row){return''+'<tr>'+
-(this.isRTL?'':this.renderNumberIntroHtml(row))+
-this.renderNumberCellsHtml(row)+
-(this.isRTL?this.renderNumberIntroHtml(row):'')+'</tr>';},renderNumberIntroHtml:function(row){return this.renderIntroHtml();},renderNumberCellsHtml:function(row){var htmls=[];var col,date;for(col=0;col<this.colCnt;col++){date=this.getCellDate(row,col);htmls.push(this.renderNumberCellHtml(date));}
-return htmls.join('');},renderNumberCellHtml:function(date){var classes;if(!this.view.dayNumbersVisible){return'<td/>';}
-classes=this.getDayClasses(date);classes.unshift('fc-day-number');return''+'<td class="'+classes.join(' ')+'" data-date="'+date.format()+'">'+
-date.date()+'</td>';},computeEventTimeFormat:function(){return this.view.opt('extraSmallTimeFormat');},computeDisplayEventEnd:function(){return this.colCnt==1;},rangeUpdated:function(){this.updateDayTable();},spanToSegs:function(span){var segs=this.sliceRangeByRow(span);var i,seg;for(i=0;i<segs.length;i++){seg=segs[i];if(this.isRTL){seg.leftCol=this.daysPerRow-1-seg.lastRowDayIndex;seg.rightCol=this.daysPerRow-1-seg.firstRowDayIndex;}
-else{seg.leftCol=seg.firstRowDayIndex;seg.rightCol=seg.lastRowDayIndex;}}
-return segs;},prepareHits:function(){this.colCoordCache.build();this.rowCoordCache.build();this.rowCoordCache.bottoms[this.rowCnt-1]+=this.bottomCoordPadding;},releaseHits:function(){this.colCoordCache.clear();this.rowCoordCache.clear();},queryHit:function(leftOffset,topOffset){var col=this.colCoordCache.getHorizontalIndex(leftOffset);var row=this.rowCoordCache.getVerticalIndex(topOffset);if(row!=null&&col!=null){return this.getCellHit(row,col);}},getHitSpan:function(hit){return this.getCellRange(hit.row,hit.col);},getHitEl:function(hit){return this.getCellEl(hit.row,hit.col);},getCellHit:function(row,col){return{row:row,col:col,component:this,left:this.colCoordCache.getLeftOffset(col),right:this.colCoordCache.getRightOffset(col),top:this.rowCoordCache.getTopOffset(row),bottom:this.rowCoordCache.getBottomOffset(row)};},getCellEl:function(row,col){return this.cellEls.eq(row*this.colCnt+col);},renderDrag:function(eventLocation,seg){this.renderHighlight(this.eventToSpan(eventLocation));if(seg&&!seg.el.closest(this.el).length){return this.renderEventLocationHelper(eventLocation,seg);}},unrenderDrag:function(){this.unrenderHighlight();this.unrenderHelper();},renderEventResize:function(eventLocation,seg){this.renderHighlight(this.eventToSpan(eventLocation));return this.renderEventLocationHelper(eventLocation,seg);},unrenderEventResize:function(){this.unrenderHighlight();this.unrenderHelper();},renderHelper:function(event,sourceSeg){var helperNodes=[];var segs=this.eventToSegs(event);var rowStructs;segs=this.renderFgSegEls(segs);rowStructs=this.renderSegRows(segs);this.rowEls.each(function(row,rowNode){var rowEl=$(rowNode);var skeletonEl=$('<div class="fc-helper-skeleton"><table/></div>');var skeletonTop;if(sourceSeg&&sourceSeg.row===row){skeletonTop=sourceSeg.el.position().top;}
-else{skeletonTop=rowEl.find('.fc-content-skeleton tbody').position().top;}
-skeletonEl.css('top',skeletonTop).find('table').append(rowStructs[row].tbodyEl);rowEl.append(skeletonEl);helperNodes.push(skeletonEl[0]);});return(this.helperEls=$(helperNodes));},unrenderHelper:function(){if(this.helperEls){this.helperEls.remove();this.helperEls=null;}},fillSegTag:'td',renderFill:function(type,segs,className){var nodes=[];var i,seg;var skeletonEl;segs=this.renderFillSegEls(type,segs);for(i=0;i<segs.length;i++){seg=segs[i];skeletonEl=this.renderFillRow(type,seg,className);this.rowEls.eq(seg.row).append(skeletonEl);nodes.push(skeletonEl[0]);}
-this.elsByFill[type]=$(nodes);return segs;},renderFillRow:function(type,seg,className){var colCnt=this.colCnt;var startCol=seg.leftCol;var endCol=seg.rightCol+1;var skeletonEl;var trEl;className=className||type.toLowerCase();skeletonEl=$('<div class="fc-'+className+'-skeleton">'+'<table><tr/></table>'+'</div>');trEl=skeletonEl.find('tr');if(startCol>0){trEl.append('<td colspan="'+startCol+'"/>');}
-trEl.append(seg.el.attr('colspan',endCol-startCol));if(endCol<colCnt){trEl.append('<td colspan="'+(colCnt-endCol)+'"/>');}
-this.bookendCells(trEl);return skeletonEl;}});;;DayGrid.mixin({rowStructs:null,unrenderEvents:function(){this.removeSegPopover();Grid.prototype.unrenderEvents.apply(this,arguments);},getEventSegs:function(){return Grid.prototype.getEventSegs.call(this).concat(this.popoverSegs||[]);},renderBgSegs:function(segs){var allDaySegs=$.grep(segs,function(seg){return seg.event.allDay;});return Grid.prototype.renderBgSegs.call(this,allDaySegs);},renderFgSegs:function(segs){var rowStructs;segs=this.renderFgSegEls(segs);rowStructs=this.rowStructs=this.renderSegRows(segs);this.rowEls.each(function(i,rowNode){$(rowNode).find('.fc-content-skeleton > table').append(rowStructs[i].tbodyEl);});return segs;},unrenderFgSegs:function(){var rowStructs=this.rowStructs||[];var rowStruct;while((rowStruct=rowStructs.pop())){rowStruct.tbodyEl.remove();}
-this.rowStructs=null;},renderSegRows:function(segs){var rowStructs=[];var segRows;var row;segRows=this.groupSegRows(segs);for(row=0;row<segRows.length;row++){rowStructs.push(this.renderSegRow(row,segRows[row]));}
-return rowStructs;},fgSegHtml:function(seg,disableResizing){var view=this.view;var event=seg.event;var isDraggable=view.isEventDraggable(event);var isResizableFromStart=!disableResizing&&event.allDay&&seg.isStart&&view.isEventResizableFromStart(event);var isResizableFromEnd=!disableResizing&&event.allDay&&seg.isEnd&&view.isEventResizableFromEnd(event);var classes=this.getSegClasses(seg,isDraggable,isResizableFromStart||isResizableFromEnd);var skinCss=cssToStr(this.getSegSkinCss(seg));var timeHtml='';var timeText;var titleHtml;classes.unshift('fc-day-grid-event','fc-h-event');if(seg.isStart){timeText=this.getEventTimeText(event);if(timeText){timeHtml='<span class="fc-time">'+htmlEscape(timeText)+'</span>';}}
-titleHtml='<span class="fc-title">'+
-(htmlEscape(event.title||'')||'&nbsp;')+'</span>';return'<a class="'+classes.join(' ')+'"'+
-(event.url?' href="'+htmlEscape(event.url)+'"':'')+
-(skinCss?' style="'+skinCss+'"':'')+'>'+'<div class="fc-content">'+
-(this.isRTL?titleHtml+' '+timeHtml:timeHtml+' '+titleHtml)+'</div>'+
-(isResizableFromStart?'<div class="fc-resizer fc-start-resizer" />':'')+
-(isResizableFromEnd?'<div class="fc-resizer fc-end-resizer" />':'')+'</a>';},renderSegRow:function(row,rowSegs){var colCnt=this.colCnt;var segLevels=this.buildSegLevels(rowSegs);var levelCnt=Math.max(1,segLevels.length);var tbody=$('<tbody/>');var segMatrix=[];var cellMatrix=[];var loneCellMatrix=[];var i,levelSegs;var col;var tr;var j,seg;var td;function emptyCellsUntil(endCol){while(col<endCol){td=(loneCellMatrix[i-1]||[])[col];if(td){td.attr('rowspan',parseInt(td.attr('rowspan')||1,10)+1);}
-else{td=$('<td/>');tr.append(td);}
-cellMatrix[i][col]=td;loneCellMatrix[i][col]=td;col++;}}
-for(i=0;i<levelCnt;i++){levelSegs=segLevels[i];col=0;tr=$('<tr/>');segMatrix.push([]);cellMatrix.push([]);loneCellMatrix.push([]);if(levelSegs){for(j=0;j<levelSegs.length;j++){seg=levelSegs[j];emptyCellsUntil(seg.leftCol);td=$('<td class="fc-event-container"/>').append(seg.el);if(seg.leftCol!=seg.rightCol){td.attr('colspan',seg.rightCol-seg.leftCol+1);}
-else{loneCellMatrix[i][col]=td;}
-while(col<=seg.rightCol){cellMatrix[i][col]=td;segMatrix[i][col]=seg;col++;}
-tr.append(td);}}
-emptyCellsUntil(colCnt);this.bookendCells(tr);tbody.append(tr);}
-return{row:row,tbodyEl:tbody,cellMatrix:cellMatrix,segMatrix:segMatrix,segLevels:segLevels,segs:rowSegs};},buildSegLevels:function(segs){var levels=[];var i,seg;var j;this.sortEventSegs(segs);for(i=0;i<segs.length;i++){seg=segs[i];for(j=0;j<levels.length;j++){if(!isDaySegCollision(seg,levels[j])){break;}}
-seg.level=j;(levels[j]||(levels[j]=[])).push(seg);}
-for(j=0;j<levels.length;j++){levels[j].sort(compareDaySegCols);}
-return levels;},groupSegRows:function(segs){var segRows=[];var i;for(i=0;i<this.rowCnt;i++){segRows.push([]);}
-for(i=0;i<segs.length;i++){segRows[segs[i].row].push(segs[i]);}
-return segRows;}});function isDaySegCollision(seg,otherSegs){var i,otherSeg;for(i=0;i<otherSegs.length;i++){otherSeg=otherSegs[i];if(otherSeg.leftCol<=seg.rightCol&&otherSeg.rightCol>=seg.leftCol){return true;}}
-return false;}
-function compareDaySegCols(a,b){return a.leftCol-b.leftCol;};;DayGrid.mixin({segPopover:null,popoverSegs:null,removeSegPopover:function(){if(this.segPopover){this.segPopover.hide();}},limitRows:function(levelLimit){var rowStructs=this.rowStructs||[];var row;var rowLevelLimit;for(row=0;row<rowStructs.length;row++){this.unlimitRow(row);if(!levelLimit){rowLevelLimit=false;}
-else if(typeof levelLimit==='number'){rowLevelLimit=levelLimit;}
-else{rowLevelLimit=this.computeRowLevelLimit(row);}
-if(rowLevelLimit!==false){this.limitRow(row,rowLevelLimit);}}},computeRowLevelLimit:function(row){var rowEl=this.rowEls.eq(row);var rowHeight=rowEl.height();var trEls=this.rowStructs[row].tbodyEl.children();var i,trEl;var trHeight;function iterInnerHeights(i,childNode){trHeight=Math.max(trHeight,$(childNode).outerHeight());}
-for(i=0;i<trEls.length;i++){trEl=trEls.eq(i).removeClass('fc-limited');trHeight=0;trEl.find('> td > :first-child').each(iterInnerHeights);if(trEl.position().top+trHeight>rowHeight){return i;}}
-return false;},limitRow:function(row,levelLimit){var _this=this;var rowStruct=this.rowStructs[row];var moreNodes=[];var col=0;var levelSegs;var cellMatrix;var limitedNodes;var i,seg;var segsBelow;var totalSegsBelow;var colSegsBelow;var td,rowspan;var segMoreNodes;var j;var moreTd,moreWrap,moreLink;function emptyCellsUntil(endCol){while(col<endCol){segsBelow=_this.getCellSegs(row,col,levelLimit);if(segsBelow.length){td=cellMatrix[levelLimit-1][col];moreLink=_this.renderMoreLink(row,col,segsBelow);moreWrap=$('<div/>').append(moreLink);td.append(moreWrap);moreNodes.push(moreWrap[0]);}
-col++;}}
-if(levelLimit&&levelLimit<rowStruct.segLevels.length){levelSegs=rowStruct.segLevels[levelLimit-1];cellMatrix=rowStruct.cellMatrix;limitedNodes=rowStruct.tbodyEl.children().slice(levelLimit).addClass('fc-limited').get();for(i=0;i<levelSegs.length;i++){seg=levelSegs[i];emptyCellsUntil(seg.leftCol);colSegsBelow=[];totalSegsBelow=0;while(col<=seg.rightCol){segsBelow=this.getCellSegs(row,col,levelLimit);colSegsBelow.push(segsBelow);totalSegsBelow+=segsBelow.length;col++;}
-if(totalSegsBelow){td=cellMatrix[levelLimit-1][seg.leftCol];rowspan=td.attr('rowspan')||1;segMoreNodes=[];for(j=0;j<colSegsBelow.length;j++){moreTd=$('<td class="fc-more-cell"/>').attr('rowspan',rowspan);segsBelow=colSegsBelow[j];moreLink=this.renderMoreLink(row,seg.leftCol+j,[seg].concat(segsBelow));moreWrap=$('<div/>').append(moreLink);moreTd.append(moreWrap);segMoreNodes.push(moreTd[0]);moreNodes.push(moreTd[0]);}
-td.addClass('fc-limited').after($(segMoreNodes));limitedNodes.push(td[0]);}}
-emptyCellsUntil(this.colCnt);rowStruct.moreEls=$(moreNodes);rowStruct.limitedEls=$(limitedNodes);}},unlimitRow:function(row){var rowStruct=this.rowStructs[row];if(rowStruct.moreEls){rowStruct.moreEls.remove();rowStruct.moreEls=null;}
-if(rowStruct.limitedEls){rowStruct.limitedEls.removeClass('fc-limited');rowStruct.limitedEls=null;}},renderMoreLink:function(row,col,hiddenSegs){var _this=this;var view=this.view;return $('<a class="fc-more"/>').text(this.getMoreLinkText(hiddenSegs.length)).on('click',function(ev){var clickOption=view.opt('eventLimitClick');var date=_this.getCellDate(row,col);var moreEl=$(this);var dayEl=_this.getCellEl(row,col);var allSegs=_this.getCellSegs(row,col);var reslicedAllSegs=_this.resliceDaySegs(allSegs,date);var reslicedHiddenSegs=_this.resliceDaySegs(hiddenSegs,date);if(typeof clickOption==='function'){clickOption=view.trigger('eventLimitClick',null,{date:date,dayEl:dayEl,moreEl:moreEl,segs:reslicedAllSegs,hiddenSegs:reslicedHiddenSegs},ev);}
-if(clickOption==='popover'){_this.showSegPopover(row,col,moreEl,reslicedAllSegs);}
-else if(typeof clickOption==='string'){view.calendar.zoomTo(date,clickOption);}});},showSegPopover:function(row,col,moreLink,segs){var _this=this;var view=this.view;var moreWrap=moreLink.parent();var topEl;var options;if(this.rowCnt==1){topEl=view.el;}
-else{topEl=this.rowEls.eq(row);}
-options={className:'fc-more-popover',content:this.renderSegPopoverContent(row,col,segs),parentEl:this.el,top:topEl.offset().top,autoHide:true,viewportConstrain:view.opt('popoverViewportConstrain'),hide:function(){_this.segPopover.removeElement();_this.segPopover=null;_this.popoverSegs=null;}};if(this.isRTL){options.right=moreWrap.offset().left+moreWrap.outerWidth()+1;}
-else{options.left=moreWrap.offset().left-1;}
-this.segPopover=new Popover(options);this.segPopover.show();},renderSegPopoverContent:function(row,col,segs){var view=this.view;var isTheme=view.opt('theme');var title=this.getCellDate(row,col).format(view.opt('dayPopoverFormat'));var content=$('<div class="fc-header '+view.widgetHeaderClass+'">'+'<span class="fc-close '+
-(isTheme?'ui-icon ui-icon-closethick':'fc-icon fc-icon-x')+'"></span>'+'<span class="fc-title">'+
-htmlEscape(title)+'</span>'+'<div class="fc-clear"/>'+'</div>'+'<div class="fc-body '+view.widgetContentClass+'">'+'<div class="fc-event-container"></div>'+'</div>');var segContainer=content.find('.fc-event-container');var i;segs=this.renderFgSegEls(segs,true);this.popoverSegs=segs;for(i=0;i<segs.length;i++){this.prepareHits();segs[i].hit=this.getCellHit(row,col);this.releaseHits();segContainer.append(segs[i].el);}
-return content;},resliceDaySegs:function(segs,dayDate){var events=$.map(segs,function(seg){return seg.event;});var dayStart=dayDate.clone();var dayEnd=dayStart.clone().add(1,'days');var dayRange={start:dayStart,end:dayEnd};segs=this.eventsToSegs(events,function(range){var seg=intersectRanges(range,dayRange);return seg?[seg]:[];});this.sortEventSegs(segs);return segs;},getMoreLinkText:function(num){var opt=this.view.opt('eventLimitText');if(typeof opt==='function'){return opt(num);}
-else{return'+'+num+' '+opt;}},getCellSegs:function(row,col,startLevel){var segMatrix=this.rowStructs[row].segMatrix;var level=startLevel||0;var segs=[];var seg;while(level<segMatrix.length){seg=segMatrix[level][col];if(seg){segs.push(seg);}
-level++;}
-return segs;}});;;var TimeGrid=FC.TimeGrid=Grid.extend(DayTableMixin,{slotDuration:null,snapDuration:null,snapsPerSlot:null,minTime:null,maxTime:null,labelFormat:null,labelInterval:null,colEls:null,slatContainerEl:null,slatEls:null,nowIndicatorEls:null,colCoordCache:null,slatCoordCache:null,constructor:function(){Grid.apply(this,arguments);this.processOptions();},renderDates:function(){this.el.html(this.renderHtml());this.colEls=this.el.find('.fc-day');this.slatContainerEl=this.el.find('.fc-slats');this.slatEls=this.slatContainerEl.find('tr');this.colCoordCache=new CoordCache({els:this.colEls,isHorizontal:true});this.slatCoordCache=new CoordCache({els:this.slatEls,isVertical:true});this.renderContentSkeleton();},renderHtml:function(){return''+'<div class="fc-bg">'+'<table>'+
-this.renderBgTrHtml(0)+'</table>'+'</div>'+'<div class="fc-slats">'+'<table>'+
-this.renderSlatRowHtml()+'</table>'+'</div>';},renderSlatRowHtml:function(){var view=this.view;var isRTL=this.isRTL;var html='';var slotTime=moment.duration(+this.minTime);var slotDate;var isLabeled;var axisHtml;while(slotTime<this.maxTime){slotDate=this.start.clone().time(slotTime);isLabeled=isInt(divideDurationByDuration(slotTime,this.labelInterval));axisHtml='<td class="fc-axis fc-time '+view.widgetContentClass+'" '+view.axisStyleAttr()+'>'+
-(isLabeled?'<span>'+
-htmlEscape(slotDate.format(this.labelFormat))+'</span>':'')+'</td>';html+='<tr data-time="'+slotDate.format('HH:mm:ss')+'"'+
-(isLabeled?'':' class="fc-minor"')+'>'+
-(!isRTL?axisHtml:'')+'<td class="'+view.widgetContentClass+'"/>'+
-(isRTL?axisHtml:'')+"</tr>";slotTime.add(this.slotDuration);}
-return html;},processOptions:function(){var view=this.view;var slotDuration=view.opt('slotDuration');var snapDuration=view.opt('snapDuration');var input;slotDuration=moment.duration(slotDuration);snapDuration=snapDuration?moment.duration(snapDuration):slotDuration;this.slotDuration=slotDuration;this.snapDuration=snapDuration;this.snapsPerSlot=slotDuration/snapDuration;this.minResizeDuration=snapDuration;this.minTime=moment.duration(view.opt('minTime'));this.maxTime=moment.duration(view.opt('maxTime'));input=view.opt('slotLabelFormat');if($.isArray(input)){input=input[input.length-1];}
-this.labelFormat=input||view.opt('axisFormat')||view.opt('smallTimeFormat');input=view.opt('slotLabelInterval');this.labelInterval=input?moment.duration(input):this.computeLabelInterval(slotDuration);},computeLabelInterval:function(slotDuration){var i;var labelInterval;var slotsPerLabel;for(i=AGENDA_STOCK_SUB_DURATIONS.length-1;i>=0;i--){labelInterval=moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]);slotsPerLabel=divideDurationByDuration(labelInterval,slotDuration);if(isInt(slotsPerLabel)&&slotsPerLabel>1){return labelInterval;}}
-return moment.duration(slotDuration);},computeEventTimeFormat:function(){return this.view.opt('noMeridiemTimeFormat');},computeDisplayEventEnd:function(){return true;},prepareHits:function(){this.colCoordCache.build();this.slatCoordCache.build();},releaseHits:function(){this.colCoordCache.clear();},queryHit:function(leftOffset,topOffset){var snapsPerSlot=this.snapsPerSlot;var colCoordCache=this.colCoordCache;var slatCoordCache=this.slatCoordCache;var colIndex=colCoordCache.getHorizontalIndex(leftOffset);var slatIndex=slatCoordCache.getVerticalIndex(topOffset);if(colIndex!=null&&slatIndex!=null){var slatTop=slatCoordCache.getTopOffset(slatIndex);var slatHeight=slatCoordCache.getHeight(slatIndex);var partial=(topOffset-slatTop)/slatHeight;var localSnapIndex=Math.floor(partial*snapsPerSlot);var snapIndex=slatIndex*snapsPerSlot+localSnapIndex;var snapTop=slatTop+(localSnapIndex/snapsPerSlot)*slatHeight;var snapBottom=slatTop+((localSnapIndex+1)/snapsPerSlot)*slatHeight;return{col:colIndex,snap:snapIndex,component:this,left:colCoordCache.getLeftOffset(colIndex),right:colCoordCache.getRightOffset(colIndex),top:snapTop,bottom:snapBottom};}},getHitSpan:function(hit){var start=this.getCellDate(0,hit.col);var time=this.computeSnapTime(hit.snap);var end;start.time(time);end=start.clone().add(this.snapDuration);return{start:start,end:end};},getHitEl:function(hit){return this.colEls.eq(hit.col);},rangeUpdated:function(){this.updateDayTable();},computeSnapTime:function(snapIndex){return moment.duration(this.minTime+this.snapDuration*snapIndex);},spanToSegs:function(span){var segs=this.sliceRangeByTimes(span);var i;for(i=0;i<segs.length;i++){if(this.isRTL){segs[i].col=this.daysPerRow-1-segs[i].dayIndex;}
-else{segs[i].col=segs[i].dayIndex;}}
-return segs;},sliceRangeByTimes:function(range){var segs=[];var seg;var dayIndex;var dayDate;var dayRange;for(dayIndex=0;dayIndex<this.daysPerRow;dayIndex++){dayDate=this.dayDates[dayIndex].clone();dayRange={start:dayDate.clone().time(this.minTime),end:dayDate.clone().time(this.maxTime)};seg=intersectRanges(range,dayRange);if(seg){seg.dayIndex=dayIndex;segs.push(seg);}}
-return segs;},updateSize:function(isResize){this.slatCoordCache.build();if(isResize){this.updateSegVerticals([].concat(this.fgSegs||[],this.bgSegs||[],this.businessSegs||[]));}},getTotalSlatHeight:function(){return this.slatContainerEl.outerHeight();},computeDateTop:function(date,startOfDayDate){return this.computeTimeTop(moment.duration(date-startOfDayDate.clone().stripTime()));},computeTimeTop:function(time){var len=this.slatEls.length;var slatCoverage=(time-this.minTime)/this.slotDuration;var slatIndex;var slatRemainder;slatCoverage=Math.max(0,slatCoverage);slatCoverage=Math.min(len,slatCoverage);slatIndex=Math.floor(slatCoverage);slatIndex=Math.min(slatIndex,len-1);slatRemainder=slatCoverage-slatIndex;return this.slatCoordCache.getTopPosition(slatIndex)+
-this.slatCoordCache.getHeight(slatIndex)*slatRemainder;},renderDrag:function(eventLocation,seg){if(seg){return this.renderEventLocationHelper(eventLocation,seg);}
-else{this.renderHighlight(this.eventToSpan(eventLocation));}},unrenderDrag:function(){this.unrenderHelper();this.unrenderHighlight();},renderEventResize:function(eventLocation,seg){return this.renderEventLocationHelper(eventLocation,seg);},unrenderEventResize:function(){this.unrenderHelper();},renderHelper:function(event,sourceSeg){return this.renderHelperSegs(this.eventToSegs(event),sourceSeg);},unrenderHelper:function(){this.unrenderHelperSegs();},renderBusinessHours:function(){var events=this.view.calendar.getBusinessHoursEvents();var segs=this.eventsToSegs(events);this.renderBusinessSegs(segs);},unrenderBusinessHours:function(){this.unrenderBusinessSegs();},getNowIndicatorUnit:function(){return'minute';},renderNowIndicator:function(date){var segs=this.spanToSegs({start:date,end:date});var top=this.computeDateTop(date,date);var nodes=[];var i;for(i=0;i<segs.length;i++){nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>').css('top',top).appendTo(this.colContainerEls.eq(segs[i].col))[0]);}
-if(segs.length>0){nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>').css('top',top).appendTo(this.el.find('.fc-content-skeleton'))[0]);}
-this.nowIndicatorEls=$(nodes);},unrenderNowIndicator:function(){if(this.nowIndicatorEls){this.nowIndicatorEls.remove();this.nowIndicatorEls=null;}},renderSelection:function(span){if(this.view.opt('selectHelper')){this.renderEventLocationHelper(span);}
-else{this.renderHighlight(span);}},unrenderSelection:function(){this.unrenderHelper();this.unrenderHighlight();},renderHighlight:function(span){this.renderHighlightSegs(this.spanToSegs(span));},unrenderHighlight:function(){this.unrenderHighlightSegs();}});;;TimeGrid.mixin({colContainerEls:null,fgContainerEls:null,bgContainerEls:null,helperContainerEls:null,highlightContainerEls:null,businessContainerEls:null,fgSegs:null,bgSegs:null,helperSegs:null,highlightSegs:null,businessSegs:null,renderContentSkeleton:function(){var cellHtml='';var i;var skeletonEl;for(i=0;i<this.colCnt;i++){cellHtml+='<td>'+'<div class="fc-content-col">'+'<div class="fc-event-container fc-helper-container"></div>'+'<div class="fc-event-container"></div>'+'<div class="fc-highlight-container"></div>'+'<div class="fc-bgevent-container"></div>'+'<div class="fc-business-container"></div>'+'</div>'+'</td>';}
-skeletonEl=$('<div class="fc-content-skeleton">'+'<table>'+'<tr>'+cellHtml+'</tr>'+'</table>'+'</div>');this.colContainerEls=skeletonEl.find('.fc-content-col');this.helperContainerEls=skeletonEl.find('.fc-helper-container');this.fgContainerEls=skeletonEl.find('.fc-event-container:not(.fc-helper-container)');this.bgContainerEls=skeletonEl.find('.fc-bgevent-container');this.highlightContainerEls=skeletonEl.find('.fc-highlight-container');this.businessContainerEls=skeletonEl.find('.fc-business-container');this.bookendCells(skeletonEl.find('tr'));this.el.append(skeletonEl);},renderFgSegs:function(segs){segs=this.renderFgSegsIntoContainers(segs,this.fgContainerEls);this.fgSegs=segs;return segs;},unrenderFgSegs:function(){this.unrenderNamedSegs('fgSegs');},renderHelperSegs:function(segs,sourceSeg){var helperEls=[];var i,seg;var sourceEl;segs=this.renderFgSegsIntoContainers(segs,this.helperContainerEls);for(i=0;i<segs.length;i++){seg=segs[i];if(sourceSeg&&sourceSeg.col===seg.col){sourceEl=sourceSeg.el;seg.el.css({left:sourceEl.css('left'),right:sourceEl.css('right'),'margin-left':sourceEl.css('margin-left'),'margin-right':sourceEl.css('margin-right')});}
-helperEls.push(seg.el[0]);}
-this.helperSegs=segs;return $(helperEls);},unrenderHelperSegs:function(){this.unrenderNamedSegs('helperSegs');},renderBgSegs:function(segs){segs=this.renderFillSegEls('bgEvent',segs);this.updateSegVerticals(segs);this.attachSegsByCol(this.groupSegsByCol(segs),this.bgContainerEls);this.bgSegs=segs;return segs;},unrenderBgSegs:function(){this.unrenderNamedSegs('bgSegs');},renderHighlightSegs:function(segs){segs=this.renderFillSegEls('highlight',segs);this.updateSegVerticals(segs);this.attachSegsByCol(this.groupSegsByCol(segs),this.highlightContainerEls);this.highlightSegs=segs;},unrenderHighlightSegs:function(){this.unrenderNamedSegs('highlightSegs');},renderBusinessSegs:function(segs){segs=this.renderFillSegEls('businessHours',segs);this.updateSegVerticals(segs);this.attachSegsByCol(this.groupSegsByCol(segs),this.businessContainerEls);this.businessSegs=segs;},unrenderBusinessSegs:function(){this.unrenderNamedSegs('businessSegs');},groupSegsByCol:function(segs){var segsByCol=[];var i;for(i=0;i<this.colCnt;i++){segsByCol.push([]);}
-for(i=0;i<segs.length;i++){segsByCol[segs[i].col].push(segs[i]);}
-return segsByCol;},attachSegsByCol:function(segsByCol,containerEls){var col;var segs;var i;for(col=0;col<this.colCnt;col++){segs=segsByCol[col];for(i=0;i<segs.length;i++){containerEls.eq(col).append(segs[i].el);}}},unrenderNamedSegs:function(propName){var segs=this[propName];var i;if(segs){for(i=0;i<segs.length;i++){segs[i].el.remove();}
-this[propName]=null;}},renderFgSegsIntoContainers:function(segs,containerEls){var segsByCol;var col;segs=this.renderFgSegEls(segs);segsByCol=this.groupSegsByCol(segs);for(col=0;col<this.colCnt;col++){this.updateFgSegCoords(segsByCol[col]);}
-this.attachSegsByCol(segsByCol,containerEls);return segs;},fgSegHtml:function(seg,disableResizing){var view=this.view;var event=seg.event;var isDraggable=view.isEventDraggable(event);var isResizableFromStart=!disableResizing&&seg.isStart&&view.isEventResizableFromStart(event);var isResizableFromEnd=!disableResizing&&seg.isEnd&&view.isEventResizableFromEnd(event);var classes=this.getSegClasses(seg,isDraggable,isResizableFromStart||isResizableFromEnd);var skinCss=cssToStr(this.getSegSkinCss(seg));var timeText;var fullTimeText;var startTimeText;classes.unshift('fc-time-grid-event','fc-v-event');if(view.isMultiDayEvent(event)){if(seg.isStart||seg.isEnd){timeText=this.getEventTimeText(seg);fullTimeText=this.getEventTimeText(seg,'LT');startTimeText=this.getEventTimeText(seg,null,false);}}else{timeText=this.getEventTimeText(event);fullTimeText=this.getEventTimeText(event,'LT');startTimeText=this.getEventTimeText(event,null,false);}
-return'<a class="'+classes.join(' ')+'"'+
-(event.url?' href="'+htmlEscape(event.url)+'"':'')+
-(skinCss?' style="'+skinCss+'"':'')+'>'+'<div class="fc-content">'+
-(timeText?'<div class="fc-time"'+' data-start="'+htmlEscape(startTimeText)+'"'+' data-full="'+htmlEscape(fullTimeText)+'"'+'>'+'<span>'+htmlEscape(timeText)+'</span>'+'</div>':'')+
-(event.title?'<div class="fc-title">'+
-htmlEscape(event.title)+'</div>':'')+'</div>'+'<div class="fc-bg"/>'+
-(isResizableFromEnd?'<div class="fc-resizer fc-end-resizer" />':'')+'</a>';},updateSegVerticals:function(segs){this.computeSegVerticals(segs);this.assignSegVerticals(segs);},computeSegVerticals:function(segs){var i,seg;for(i=0;i<segs.length;i++){seg=segs[i];seg.top=this.computeDateTop(seg.start,seg.start);seg.bottom=this.computeDateTop(seg.end,seg.start);}},assignSegVerticals:function(segs){var i,seg;for(i=0;i<segs.length;i++){seg=segs[i];seg.el.css(this.generateSegVerticalCss(seg));}},generateSegVerticalCss:function(seg){return{top:seg.top,bottom:-seg.bottom};},updateFgSegCoords:function(segs){this.computeSegVerticals(segs);this.computeFgSegHorizontals(segs);this.assignSegVerticals(segs);this.assignFgSegHorizontals(segs);},computeFgSegHorizontals:function(segs){var levels;var level0;var i;this.sortEventSegs(segs);levels=buildSlotSegLevels(segs);computeForwardSlotSegs(levels);if((level0=levels[0])){for(i=0;i<level0.length;i++){computeSlotSegPressures(level0[i]);}
-for(i=0;i<level0.length;i++){this.computeFgSegForwardBack(level0[i],0,0);}}},computeFgSegForwardBack:function(seg,seriesBackwardPressure,seriesBackwardCoord){var forwardSegs=seg.forwardSegs;var i;if(seg.forwardCoord===undefined){if(!forwardSegs.length){seg.forwardCoord=1;}
-else{this.sortForwardSegs(forwardSegs);this.computeFgSegForwardBack(forwardSegs[0],seriesBackwardPressure+1,seriesBackwardCoord);seg.forwardCoord=forwardSegs[0].backwardCoord;}
-seg.backwardCoord=seg.forwardCoord-
-(seg.forwardCoord-seriesBackwardCoord)/(seriesBackwardPressure+1);for(i=0;i<forwardSegs.length;i++){this.computeFgSegForwardBack(forwardSegs[i],0,seg.forwardCoord);}}},sortForwardSegs:function(forwardSegs){forwardSegs.sort(proxy(this,'compareForwardSegs'));},compareForwardSegs:function(seg1,seg2){return seg2.forwardPressure-seg1.forwardPressure||(seg1.backwardCoord||0)-(seg2.backwardCoord||0)||this.compareEventSegs(seg1,seg2);},assignFgSegHorizontals:function(segs){var i,seg;for(i=0;i<segs.length;i++){seg=segs[i];seg.el.css(this.generateFgSegHorizontalCss(seg));if(seg.bottom-seg.top<30){seg.el.addClass('fc-short');}}},generateFgSegHorizontalCss:function(seg){var shouldOverlap=this.view.opt('slotEventOverlap');var backwardCoord=seg.backwardCoord;var forwardCoord=seg.forwardCoord;var props=this.generateSegVerticalCss(seg);var left;var right;if(shouldOverlap){forwardCoord=Math.min(1,backwardCoord+(forwardCoord-backwardCoord)*2);}
-if(this.isRTL){left=1-forwardCoord;right=backwardCoord;}
-else{left=backwardCoord;right=1-forwardCoord;}
-props.zIndex=seg.level+1;props.left=left*100+'%';props.right=right*100+'%';if(shouldOverlap&&seg.forwardPressure){props[this.isRTL?'marginLeft':'marginRight']=10*2;}
-return props;}});function buildSlotSegLevels(segs){var levels=[];var i,seg;var j;for(i=0;i<segs.length;i++){seg=segs[i];for(j=0;j<levels.length;j++){if(!computeSlotSegCollisions(seg,levels[j]).length){break;}}
-seg.level=j;(levels[j]||(levels[j]=[])).push(seg);}
-return levels;}
-function computeForwardSlotSegs(levels){var i,level;var j,seg;var k;for(i=0;i<levels.length;i++){level=levels[i];for(j=0;j<level.length;j++){seg=level[j];seg.forwardSegs=[];for(k=i+1;k<levels.length;k++){computeSlotSegCollisions(seg,levels[k],seg.forwardSegs);}}}}
-function computeSlotSegPressures(seg){var forwardSegs=seg.forwardSegs;var forwardPressure=0;var i,forwardSeg;if(seg.forwardPressure===undefined){for(i=0;i<forwardSegs.length;i++){forwardSeg=forwardSegs[i];computeSlotSegPressures(forwardSeg);forwardPressure=Math.max(forwardPressure,1+forwardSeg.forwardPressure);}
-seg.forwardPressure=forwardPressure;}}
-function computeSlotSegCollisions(seg,otherSegs,results){results=results||[];for(var i=0;i<otherSegs.length;i++){if(isSlotSegCollision(seg,otherSegs[i])){results.push(otherSegs[i]);}}
-return results;}
-function isSlotSegCollision(seg1,seg2){return seg1.bottom>seg2.top&&seg1.top<seg2.bottom;};;var View=FC.View=Class.extend(EmitterMixin,ListenerMixin,{type:null,name:null,title:null,calendar:null,options:null,el:null,displaying:null,isSkeletonRendered:false,isEventsRendered:false,start:null,end:null,intervalStart:null,intervalEnd:null,intervalDuration:null,intervalUnit:null,isRTL:false,isSelected:false,selectedEvent:null,eventOrderSpecs:null,widgetHeaderClass:null,widgetContentClass:null,highlightStateClass:null,nextDayThreshold:null,isHiddenDayHash:null,isNowIndicatorRendered:null,initialNowDate:null,initialNowQueriedMs:null,nowIndicatorTimeoutID:null,nowIndicatorIntervalID:null,constructor:function(calendar,type,options,intervalDuration){this.calendar=calendar;this.type=this.name=type;this.options=options;this.intervalDuration=intervalDuration||moment.duration(1,'day');this.nextDayThreshold=moment.duration(this.opt('nextDayThreshold'));this.initThemingProps();this.initHiddenDays();this.isRTL=this.opt('isRTL');this.eventOrderSpecs=parseFieldSpecs(this.opt('eventOrder'));this.initialize();},initialize:function(){},opt:function(name){return this.options[name];},trigger:function(name,thisObj){var calendar=this.calendar;return calendar.trigger.apply(calendar,[name,thisObj||this].concat(Array.prototype.slice.call(arguments,2),[this]));},setDate:function(date){this.setRange(this.computeRange(date));},setRange:function(range){$.extend(this,range);this.updateTitle();},computeRange:function(date){var intervalUnit=computeIntervalUnit(this.intervalDuration);var intervalStart=date.clone().startOf(intervalUnit);var intervalEnd=intervalStart.clone().add(this.intervalDuration);var start,end;if(/year|month|week|day/.test(intervalUnit)){intervalStart.stripTime();intervalEnd.stripTime();}
-else{if(!intervalStart.hasTime()){intervalStart=this.calendar.time(0);}
-if(!intervalEnd.hasTime()){intervalEnd=this.calendar.time(0);}}
-start=intervalStart.clone();start=this.skipHiddenDays(start);end=intervalEnd.clone();end=this.skipHiddenDays(end,-1,true);return{intervalUnit:intervalUnit,intervalStart:intervalStart,intervalEnd:intervalEnd,start:start,end:end};},computePrevDate:function(date){return this.massageCurrentDate(date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration),-1);},computeNextDate:function(date){return this.massageCurrentDate(date.clone().startOf(this.intervalUnit).add(this.intervalDuration));},massageCurrentDate:function(date,direction){if(this.intervalDuration.as('days')<=1){if(this.isHiddenDay(date)){date=this.skipHiddenDays(date,direction);date.startOf('day');}}
-return date;},updateTitle:function(){this.title=this.computeTitle();},computeTitle:function(){return this.formatRange({start:this.calendar.applyTimezone(this.intervalStart),end:this.calendar.applyTimezone(this.intervalEnd)},this.opt('titleFormat')||this.computeTitleFormat(),this.opt('titleRangeSeparator'));},computeTitleFormat:function(){if(this.intervalUnit=='year'){return'YYYY';}
-else if(this.intervalUnit=='month'){return this.opt('monthYearFormat');}
-else if(this.intervalDuration.as('days')>1){return'll';}
-else{return'LL';}},formatRange:function(range,formatStr,separator){var end=range.end;if(!end.hasTime()){end=end.clone().subtract(1);}
-return formatRange(range.start,end,formatStr,separator,this.opt('isRTL'));},setElement:function(el){this.el=el;this.bindGlobalHandlers();},removeElement:function(){this.clear();if(this.isSkeletonRendered){this.unrenderSkeleton();this.isSkeletonRendered=false;}
-this.unbindGlobalHandlers();this.el.remove();},display:function(date){var _this=this;var scrollState=null;if(this.displaying){scrollState=this.queryScroll();}
-this.calendar.freezeContentHeight();return this.clear().then(function(){return(_this.displaying=$.when(_this.displayView(date)).then(function(){_this.forceScroll(_this.computeInitialScroll(scrollState));_this.calendar.unfreezeContentHeight();_this.triggerRender();}));});},clear:function(){var _this=this;var displaying=this.displaying;if(displaying){return displaying.then(function(){_this.displaying=null;_this.clearEvents();return _this.clearView();});}
-else{return $.when();}},displayView:function(date){if(!this.isSkeletonRendered){this.renderSkeleton();this.isSkeletonRendered=true;}
-if(date){this.setDate(date);}
-if(this.render){this.render();}
-this.renderDates();this.updateSize();this.renderBusinessHours();this.startNowIndicator();},clearView:function(){this.unselect();this.stopNowIndicator();this.triggerUnrender();this.unrenderBusinessHours();this.unrenderDates();if(this.destroy){this.destroy();}},renderSkeleton:function(){},unrenderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},triggerRender:function(){this.trigger('viewRender',this,this,this.el);},triggerUnrender:function(){this.trigger('viewDestroy',this,this,this.el);},bindGlobalHandlers:function(){this.listenTo($(document),'mousedown',this.handleDocumentMousedown);this.listenTo($(document),'touchstart',this.processUnselect);},unbindGlobalHandlers:function(){this.stopListeningTo($(document));},initThemingProps:function(){var tm=this.opt('theme')?'ui':'fc';this.widgetHeaderClass=tm+'-widget-header';this.widgetContentClass=tm+'-widget-content';this.highlightStateClass=tm+'-state-highlight';},renderBusinessHours:function(){},unrenderBusinessHours:function(){},startNowIndicator:function(){var _this=this;var unit;var update;var delay;if(this.opt('nowIndicator')){unit=this.getNowIndicatorUnit();if(unit){update=proxy(this,'updateNowIndicator');this.initialNowDate=this.calendar.getNow();this.initialNowQueriedMs=+new Date();this.renderNowIndicator(this.initialNowDate);this.isNowIndicatorRendered=true;delay=this.initialNowDate.clone().startOf(unit).add(1,unit)-this.initialNowDate;this.nowIndicatorTimeoutID=setTimeout(function(){_this.nowIndicatorTimeoutID=null;update();delay=+moment.duration(1,unit);delay=Math.max(100,delay);_this.nowIndicatorIntervalID=setInterval(update,delay);},delay);}}},updateNowIndicator:function(){if(this.isNowIndicatorRendered){this.unrenderNowIndicator();this.renderNowIndicator(this.initialNowDate.clone().add(new Date()-this.initialNowQueriedMs));}},stopNowIndicator:function(){if(this.isNowIndicatorRendered){if(this.nowIndicatorTimeoutID){clearTimeout(this.nowIndicatorTimeoutID);this.nowIndicatorTimeoutID=null;}
-if(this.nowIndicatorIntervalID){clearTimeout(this.nowIndicatorIntervalID);this.nowIndicatorIntervalID=null;}
-this.unrenderNowIndicator();this.isNowIndicatorRendered=false;}},getNowIndicatorUnit:function(){},renderNowIndicator:function(date){},unrenderNowIndicator:function(){},updateSize:function(isResize){var scrollState;if(isResize){scrollState=this.queryScroll();}
-this.updateHeight(isResize);this.updateWidth(isResize);this.updateNowIndicator();if(isResize){this.setScroll(scrollState);}},updateWidth:function(isResize){},updateHeight:function(isResize){var calendar=this.calendar;this.setHeight(calendar.getSuggestedViewHeight(),calendar.isHeightAuto());},setHeight:function(height,isAuto){},computeInitialScroll:function(previousScrollState){return 0;},queryScroll:function(){},setScroll:function(scrollState){},forceScroll:function(scrollState){var _this=this;this.setScroll(scrollState);setTimeout(function(){_this.setScroll(scrollState);},0);},displayEvents:function(events){var scrollState=this.queryScroll();this.clearEvents();this.renderEvents(events);this.isEventsRendered=true;this.setScroll(scrollState);this.triggerEventRender();},clearEvents:function(){var scrollState;if(this.isEventsRendered){scrollState=this.queryScroll();this.triggerEventUnrender();if(this.destroyEvents){this.destroyEvents();}
-this.unrenderEvents();this.setScroll(scrollState);this.isEventsRendered=false;}},renderEvents:function(events){},unrenderEvents:function(){},triggerEventRender:function(){this.renderedEventSegEach(function(seg){this.trigger('eventAfterRender',seg.event,seg.event,seg.el);});this.trigger('eventAfterAllRender');},triggerEventUnrender:function(){this.renderedEventSegEach(function(seg){this.trigger('eventDestroy',seg.event,seg.event,seg.el);});},resolveEventEl:function(event,el){var custom=this.trigger('eventRender',event,event,el);if(custom===false){el=null;}
-else if(custom&&custom!==true){el=$(custom);}
-return el;},showEvent:function(event){this.renderedEventSegEach(function(seg){seg.el.css('visibility','');},event);},hideEvent:function(event){this.renderedEventSegEach(function(seg){seg.el.css('visibility','hidden');},event);},renderedEventSegEach:function(func,event){var segs=this.getEventSegs();var i;for(i=0;i<segs.length;i++){if(!event||segs[i].event._id===event._id){if(segs[i].el){func.call(this,segs[i]);}}}},getEventSegs:function(){return[];},isEventDraggable:function(event){var source=event.source||{};return firstDefined(event.startEditable,source.startEditable,this.opt('eventStartEditable'),event.editable,source.editable,this.opt('editable'));},reportEventDrop:function(event,dropLocation,largeUnit,el,ev){var calendar=this.calendar;var mutateResult=calendar.mutateEvent(event,dropLocation,largeUnit);var undoFunc=function(){mutateResult.undo();calendar.reportEventChange();};this.triggerEventDrop(event,mutateResult.dateDelta,undoFunc,el,ev);calendar.reportEventChange();},triggerEventDrop:function(event,dateDelta,undoFunc,el,ev){this.trigger('eventDrop',el[0],event,dateDelta,undoFunc,ev,{});},reportExternalDrop:function(meta,dropLocation,el,ev,ui){var eventProps=meta.eventProps;var eventInput;var event;if(eventProps){eventInput=$.extend({},eventProps,dropLocation);event=this.calendar.renderEvent(eventInput,meta.stick)[0];}
-this.triggerExternalDrop(event,dropLocation,el,ev,ui);},triggerExternalDrop:function(event,dropLocation,el,ev,ui){this.trigger('drop',el[0],dropLocation.start,ev,ui);if(event){this.trigger('eventReceive',null,event);}},renderDrag:function(dropLocation,seg){},unrenderDrag:function(){},isEventResizableFromStart:function(event){return this.opt('eventResizableFromStart')&&this.isEventResizable(event);},isEventResizableFromEnd:function(event){return this.isEventResizable(event);},isEventResizable:function(event){var source=event.source||{};return firstDefined(event.durationEditable,source.durationEditable,this.opt('eventDurationEditable'),event.editable,source.editable,this.opt('editable'));},reportEventResize:function(event,resizeLocation,largeUnit,el,ev){var calendar=this.calendar;var mutateResult=calendar.mutateEvent(event,resizeLocation,largeUnit);var undoFunc=function(){mutateResult.undo();calendar.reportEventChange();};this.triggerEventResize(event,mutateResult.durationDelta,undoFunc,el,ev);calendar.reportEventChange();},triggerEventResize:function(event,durationDelta,undoFunc,el,ev){this.trigger('eventResize',el[0],event,durationDelta,undoFunc,ev,{});},select:function(span,ev){this.unselect(ev);this.renderSelection(span);this.reportSelection(span,ev);},renderSelection:function(span){},reportSelection:function(span,ev){this.isSelected=true;this.triggerSelect(span,ev);},triggerSelect:function(span,ev){this.trigger('select',null,this.calendar.applyTimezone(span.start),this.calendar.applyTimezone(span.end),ev);},unselect:function(ev){if(this.isSelected){this.isSelected=false;if(this.destroySelection){this.destroySelection();}
-this.unrenderSelection();this.trigger('unselect',null,ev);}},unrenderSelection:function(){},selectEvent:function(event){if(!this.selectedEvent||this.selectedEvent!==event){this.unselectEvent();this.renderedEventSegEach(function(seg){seg.el.addClass('fc-selected');},event);this.selectedEvent=event;}},unselectEvent:function(){if(this.selectedEvent){this.renderedEventSegEach(function(seg){seg.el.removeClass('fc-selected');},this.selectedEvent);this.selectedEvent=null;}},isEventSelected:function(event){return this.selectedEvent&&this.selectedEvent._id===event._id;},handleDocumentMousedown:function(ev){if(isPrimaryMouseButton(ev)){this.processUnselect(ev);}},processUnselect:function(ev){this.processRangeUnselect(ev);this.processEventUnselect(ev);},processRangeUnselect:function(ev){var ignore;if(this.isSelected&&this.opt('unselectAuto')){ignore=this.opt('unselectCancel');if(!ignore||!$(ev.target).closest(ignore).length){this.unselect(ev);}}},processEventUnselect:function(ev){if(this.selectedEvent){if(!$(ev.target).closest('.fc-selected').length){this.unselectEvent();}}},triggerDayClick:function(span,dayEl,ev){this.trigger('dayClick',dayEl,this.calendar.applyTimezone(span.start),ev);},initHiddenDays:function(){var hiddenDays=this.opt('hiddenDays')||[];var isHiddenDayHash=[];var dayCnt=0;var i;if(this.opt('weekends')===false){hiddenDays.push(0,6);}
-for(i=0;i<7;i++){if(!(isHiddenDayHash[i]=$.inArray(i,hiddenDays)!==-1)){dayCnt++;}}
-if(!dayCnt){throw'invalid hiddenDays';}
-this.isHiddenDayHash=isHiddenDayHash;},isHiddenDay:function(day){if(moment.isMoment(day)){day=day.day();}
-return this.isHiddenDayHash[day];},skipHiddenDays:function(date,inc,isExclusive){var out=date.clone();inc=inc||1;while(this.isHiddenDayHash[(out.day()+(isExclusive?inc:0)+7)%7]){out.add(inc,'days');}
-return out;},computeDayRange:function(range){var startDay=range.start.clone().stripTime();var end=range.end;var endDay=null;var endTimeMS;if(end){endDay=end.clone().stripTime();endTimeMS=+end.time();if(endTimeMS&&endTimeMS>=this.nextDayThreshold){endDay.add(1,'days');}}
-if(!end||endDay<=startDay){endDay=startDay.clone().add(1,'days');}
-return{start:startDay,end:endDay};},isMultiDayEvent:function(event){var range=this.computeDayRange(event);return range.end.diff(range.start,'days')>1;}});;;var Scroller=FC.Scroller=Class.extend({el:null,scrollEl:null,overflowX:null,overflowY:null,constructor:function(options){options=options||{};this.overflowX=options.overflowX||options.overflow||'auto';this.overflowY=options.overflowY||options.overflow||'auto';},render:function(){this.el=this.renderEl();this.applyOverflow();},renderEl:function(){return(this.scrollEl=$('<div class="fc-scroller"></div>'));},clear:function(){this.setHeight('auto');this.applyOverflow();},destroy:function(){this.el.remove();},applyOverflow:function(){this.scrollEl.css({'overflow-x':this.overflowX,'overflow-y':this.overflowY});},lockOverflow:function(scrollbarWidths){var overflowX=this.overflowX;var overflowY=this.overflowY;scrollbarWidths=scrollbarWidths||this.getScrollbarWidths();if(overflowX==='auto'){overflowX=(scrollbarWidths.top||scrollbarWidths.bottom||this.scrollEl[0].scrollWidth-1>this.scrollEl[0].clientWidth)?'scroll':'hidden';}
-if(overflowY==='auto'){overflowY=(scrollbarWidths.left||scrollbarWidths.right||this.scrollEl[0].scrollHeight-1>this.scrollEl[0].clientHeight)?'scroll':'hidden';}
-this.scrollEl.css({'overflow-x':overflowX,'overflow-y':overflowY});},setHeight:function(height){this.scrollEl.height(height);},getScrollTop:function(){return this.scrollEl.scrollTop();},setScrollTop:function(top){this.scrollEl.scrollTop(top);},getClientWidth:function(){return this.scrollEl[0].clientWidth;},getClientHeight:function(){return this.scrollEl[0].clientHeight;},getScrollbarWidths:function(){return getScrollbarWidths(this.scrollEl);}});;;var Calendar=FC.Calendar=Class.extend({dirDefaults:null,langDefaults:null,overrides:null,options:null,viewSpecCache:null,view:null,header:null,loadingLevel:0,constructor:Calendar_constructor,initialize:function(){},initOptions:function(overrides){var lang,langDefaults;var isRTL,dirDefaults;overrides=massageOverrides(overrides);lang=overrides.lang;langDefaults=langOptionHash[lang];if(!langDefaults){lang=Calendar.defaults.lang;langDefaults=langOptionHash[lang]||{};}
-isRTL=firstDefined(overrides.isRTL,langDefaults.isRTL,Calendar.defaults.isRTL);dirDefaults=isRTL?Calendar.rtlDefaults:{};this.dirDefaults=dirDefaults;this.langDefaults=langDefaults;this.overrides=overrides;this.options=mergeOptions([Calendar.defaults,dirDefaults,langDefaults,overrides]);populateInstanceComputableOptions(this.options);this.viewSpecCache={};},getViewSpec:function(viewType){var cache=this.viewSpecCache;return cache[viewType]||(cache[viewType]=this.buildViewSpec(viewType));},getUnitViewSpec:function(unit){var viewTypes;var i;var spec;if($.inArray(unit,intervalUnits)!=-1){viewTypes=this.header.getViewsWithButtons();$.each(FC.views,function(viewType){viewTypes.push(viewType);});for(i=0;i<viewTypes.length;i++){spec=this.getViewSpec(viewTypes[i]);if(spec){if(spec.singleUnit==unit){return spec;}}}}},buildViewSpec:function(requestedViewType){var viewOverrides=this.overrides.views||{};var specChain=[];var defaultsChain=[];var overridesChain=[];var viewType=requestedViewType;var spec;var overrides;var duration;var unit;while(viewType){spec=fcViews[viewType];overrides=viewOverrides[viewType];viewType=null;if(typeof spec==='function'){spec={'class':spec};}
-if(spec){specChain.unshift(spec);defaultsChain.unshift(spec.defaults||{});duration=duration||spec.duration;viewType=viewType||spec.type;}
-if(overrides){overridesChain.unshift(overrides);duration=duration||overrides.duration;viewType=viewType||overrides.type;}}
-spec=mergeProps(specChain);spec.type=requestedViewType;if(!spec['class']){return false;}
-if(duration){duration=moment.duration(duration);if(duration.valueOf()){spec.duration=duration;unit=computeIntervalUnit(duration);if(duration.as(unit)===1){spec.singleUnit=unit;overridesChain.unshift(viewOverrides[unit]||{});}}}
-spec.defaults=mergeOptions(defaultsChain);spec.overrides=mergeOptions(overridesChain);this.buildViewSpecOptions(spec);this.buildViewSpecButtonText(spec,requestedViewType);return spec;},buildViewSpecOptions:function(spec){spec.options=mergeOptions([Calendar.defaults,spec.defaults,this.dirDefaults,this.langDefaults,this.overrides,spec.overrides]);populateInstanceComputableOptions(spec.options);},buildViewSpecButtonText:function(spec,requestedViewType){function queryButtonText(options){var buttonText=options.buttonText||{};return buttonText[requestedViewType]||(spec.singleUnit?buttonText[spec.singleUnit]:null);}
-spec.buttonTextOverride=queryButtonText(this.overrides)||spec.overrides.buttonText;spec.buttonTextDefault=queryButtonText(this.langDefaults)||queryButtonText(this.dirDefaults)||spec.defaults.buttonText||queryButtonText(Calendar.defaults)||(spec.duration?this.humanizeDuration(spec.duration):null)||requestedViewType;},instantiateView:function(viewType){var spec=this.getViewSpec(viewType);return new spec['class'](this,viewType,spec.options,spec.duration);},isValidViewType:function(viewType){return Boolean(this.getViewSpec(viewType));},pushLoading:function(){if(!(this.loadingLevel++)){this.trigger('loading',null,true,this.view);}},popLoading:function(){if(!(--this.loadingLevel)){this.trigger('loading',null,false,this.view);}},buildSelectSpan:function(zonedStartInput,zonedEndInput){var start=this.moment(zonedStartInput).stripZone();var end;if(zonedEndInput){end=this.moment(zonedEndInput).stripZone();}
-else if(start.hasTime()){end=start.clone().add(this.defaultTimedEventDuration);}
-else{end=start.clone().add(this.defaultAllDayEventDuration);}
-return{start:start,end:end};}});Calendar.mixin(EmitterMixin);function Calendar_constructor(element,overrides){var t=this;t.initOptions(overrides||{});var options=this.options;t.render=render;t.destroy=destroy;t.refetchEvents=refetchEvents;t.reportEvents=reportEvents;t.reportEventChange=reportEventChange;t.rerenderEvents=renderEvents;t.changeView=renderView;t.select=select;t.unselect=unselect;t.prev=prev;t.next=next;t.prevYear=prevYear;t.nextYear=nextYear;t.today=today;t.gotoDate=gotoDate;t.incrementDate=incrementDate;t.zoomTo=zoomTo;t.getDate=getDate;t.getCalendar=getCalendar;t.getView=getView;t.option=option;t.trigger=trigger;var localeData=createObject(getMomentLocaleData(options.lang));if(options.monthNames){localeData._months=options.monthNames;}
-if(options.monthNamesShort){localeData._monthsShort=options.monthNamesShort;}
-if(options.dayNames){localeData._weekdays=options.dayNames;}
-if(options.dayNamesShort){localeData._weekdaysShort=options.dayNamesShort;}
-if(options.firstDay!=null){var _week=createObject(localeData._week);_week.dow=options.firstDay;localeData._week=_week;}
-localeData._fullCalendar_weekCalc=(function(weekCalc){if(typeof weekCalc==='function'){return weekCalc;}
-else if(weekCalc==='local'){return weekCalc;}
-else if(weekCalc==='iso'||weekCalc==='ISO'){return'ISO';}})(options.weekNumberCalculation);t.defaultAllDayEventDuration=moment.duration(options.defaultAllDayEventDuration);t.defaultTimedEventDuration=moment.duration(options.defaultTimedEventDuration);t.moment=function(){var mom;if(options.timezone==='local'){mom=FC.moment.apply(null,arguments);if(mom.hasTime()){mom.local();}}
-else if(options.timezone==='UTC'){mom=FC.moment.utc.apply(null,arguments);}
-else{mom=FC.moment.parseZone.apply(null,arguments);}
-if('_locale'in mom){mom._locale=localeData;}
-else{mom._lang=localeData;}
-return mom;};t.getIsAmbigTimezone=function(){return options.timezone!=='local'&&options.timezone!=='UTC';};t.applyTimezone=function(date){if(!date.hasTime()){return date.clone();}
-var zonedDate=t.moment(date.toArray());var timeAdjust=date.time()-zonedDate.time();var adjustedZonedDate;if(timeAdjust){adjustedZonedDate=zonedDate.clone().add(timeAdjust);if(date.time()-adjustedZonedDate.time()===0){zonedDate=adjustedZonedDate;}}
-return zonedDate;};t.getNow=function(){var now=options.now;if(typeof now==='function'){now=now();}
-return t.moment(now).stripZone();};t.getEventEnd=function(event){if(event.end){return event.end.clone();}
-else{return t.getDefaultEventEnd(event.allDay,event.start);}};t.getDefaultEventEnd=function(allDay,zonedStart){var end=zonedStart.clone();if(allDay){end.stripTime().add(t.defaultAllDayEventDuration);}
-else{end.add(t.defaultTimedEventDuration);}
-if(t.getIsAmbigTimezone()){end.stripZone();}
-return end;};t.humanizeDuration=function(duration){return(duration.locale||duration.lang).call(duration,options.lang).humanize();};EventManager.call(t,options);var isFetchNeeded=t.isFetchNeeded;var fetchEvents=t.fetchEvents;var _element=element[0];var header;var headerElement;var content;var tm;var currentView;var viewsByType={};var suggestedViewHeight;var windowResizeProxy;var ignoreWindowResize=0;var events=[];var date;if(options.defaultDate!=null){date=t.moment(options.defaultDate).stripZone();}
-else{date=t.getNow();}
-function render(){if(!content){initialRender();}
-else if(elementVisible()){calcSize();renderView();}}
-function initialRender(){tm=options.theme?'ui':'fc';element.addClass('fc');if(options.isRTL){element.addClass('fc-rtl');}
-else{element.addClass('fc-ltr');}
-if(options.theme){element.addClass('ui-widget');}
-else{element.addClass('fc-unthemed');}
-content=$("<div class='fc-view-container'/>").prependTo(element);header=t.header=new Header(t,options);headerElement=header.render();if(headerElement){element.prepend(headerElement);}
-renderView(options.defaultView);if(options.handleWindowResize){windowResizeProxy=debounce(windowResize,options.windowResizeDelay);$(window).resize(windowResizeProxy);}}
-function destroy(){if(currentView){currentView.removeElement();}
-header.removeElement();content.remove();element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');if(windowResizeProxy){$(window).unbind('resize',windowResizeProxy);}}
-function elementVisible(){return element.is(':visible');}
-function renderView(viewType){ignoreWindowResize++;if(currentView&&viewType&&currentView.type!==viewType){header.deactivateButton(currentView.type);freezeContentHeight();currentView.removeElement();currentView=t.view=null;}
-if(!currentView&&viewType){currentView=t.view=viewsByType[viewType]||(viewsByType[viewType]=t.instantiateView(viewType));currentView.setElement($("<div class='fc-view fc-"+viewType+"-view' />").appendTo(content));header.activateButton(viewType);}
-if(currentView){date=currentView.massageCurrentDate(date);if(!currentView.displaying||!date.isWithin(currentView.intervalStart,currentView.intervalEnd)){if(elementVisible()){currentView.display(date);unfreezeContentHeight();updateHeaderTitle();updateTodayButton();getAndRenderEvents();}}}
-unfreezeContentHeight();ignoreWindowResize--;}
-t.getSuggestedViewHeight=function(){if(suggestedViewHeight===undefined){calcSize();}
-return suggestedViewHeight;};t.isHeightAuto=function(){return options.contentHeight==='auto'||options.height==='auto';};function updateSize(shouldRecalc){if(elementVisible()){if(shouldRecalc){_calcSize();}
-ignoreWindowResize++;currentView.updateSize(true);ignoreWindowResize--;return true;}}
-function calcSize(){if(elementVisible()){_calcSize();}}
-function _calcSize(){if(typeof options.contentHeight==='number'){suggestedViewHeight=options.contentHeight;}
-else if(typeof options.height==='number'){suggestedViewHeight=options.height-(headerElement?headerElement.outerHeight(true):0);}
-else{suggestedViewHeight=Math.round(content.width()/Math.max(options.aspectRatio,.5));}}
-function windowResize(ev){if(!ignoreWindowResize&&ev.target===window&&currentView.start){if(updateSize(true)){currentView.trigger('windowResize',_element);}}}
-function refetchEvents(){fetchAndRenderEvents();}
-function renderEvents(){if(elementVisible()){freezeContentHeight();currentView.displayEvents(events);unfreezeContentHeight();}}
-function destroyEvents(){freezeContentHeight();currentView.clearEvents();unfreezeContentHeight();}
-function getAndRenderEvents(){if(!options.lazyFetching||isFetchNeeded(currentView.start,currentView.end)){fetchAndRenderEvents();}
-else{renderEvents();}}
-function fetchAndRenderEvents(){fetchEvents(currentView.start,currentView.end);}
-function reportEvents(_events){events=_events;renderEvents();}
-function reportEventChange(){renderEvents();}
-function updateHeaderTitle(){header.updateTitle(currentView.title);}
-function updateTodayButton(){var now=t.getNow();if(now.isWithin(currentView.intervalStart,currentView.intervalEnd)){header.disableButton('today');}
-else{header.enableButton('today');}}
-function select(zonedStartInput,zonedEndInput){currentView.select(t.buildSelectSpan.apply(t,arguments));}
-function unselect(){if(currentView){currentView.unselect();}}
-function prev(){date=currentView.computePrevDate(date);renderView();}
-function next(){date=currentView.computeNextDate(date);renderView();}
-function prevYear(){date.add(-1,'years');renderView();}
-function nextYear(){date.add(1,'years');renderView();}
-function today(){date=t.getNow();renderView();}
-function gotoDate(zonedDateInput){date=t.moment(zonedDateInput).stripZone();renderView();}
-function incrementDate(delta){date.add(moment.duration(delta));renderView();}
-function zoomTo(newDate,viewType){var spec;viewType=viewType||'day';spec=t.getViewSpec(viewType)||t.getUnitViewSpec(viewType);date=newDate.clone();renderView(spec?spec.type:null);}
-function getDate(){return t.applyTimezone(date);}
-t.freezeContentHeight=freezeContentHeight;t.unfreezeContentHeight=unfreezeContentHeight;function freezeContentHeight(){content.css({width:'100%',height:content.height(),overflow:'hidden'});}
-function unfreezeContentHeight(){content.css({width:'',height:'',overflow:''});}
-function getCalendar(){return t;}
-function getView(){return currentView;}
-function option(name,value){if(value===undefined){return options[name];}
-if(name=='height'||name=='contentHeight'||name=='aspectRatio'){options[name]=value;updateSize(true);}}
-function trigger(name,thisObj){var args=Array.prototype.slice.call(arguments,2);thisObj=thisObj||_element;this.triggerWith(name,thisObj,args);if(options[name]){return options[name].apply(thisObj,args);}}
-t.initialize();};;Calendar.defaults={titleRangeSeparator:' \u2014 ',monthYearFormat:'MMMM YYYY',defaultTimedEventDuration:'02:00:00',defaultAllDayEventDuration:{days:1},forceEventDuration:false,nextDayThreshold:'09:00:00',defaultView:'month',aspectRatio:1.35,header:{left:'title',center:'',right:'today prev,next'},weekends:true,weekNumbers:false,weekNumberTitle:'W',weekNumberCalculation:'local',scrollTime:'06:00:00',lazyFetching:true,startParam:'start',endParam:'end',timezoneParam:'timezone',timezone:false,isRTL:false,buttonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",year:'year',today:'today',month:'month',week:'week',day:'day'},buttonIcons:{prev:'left-single-arrow',next:'right-single-arrow',prevYear:'left-double-arrow',nextYear:'right-double-arrow'},theme:false,themeButtonIcons:{prev:'circle-triangle-w',next:'circle-triangle-e',prevYear:'seek-prev',nextYear:'seek-next'},dragOpacity:.75,dragRevertDuration:500,dragScroll:true,unselectAuto:true,dropAccept:'*',eventOrder:'title',eventLimit:false,eventLimitText:'more',eventLimitClick:'popover',dayPopoverFormat:'LL',handleWindowResize:true,windowResizeDelay:200,longPressDelay:1000};Calendar.englishDefaults={dayPopoverFormat:'dddd, MMMM D'};Calendar.rtlDefaults={header:{left:'next,prev today',center:'',right:'title'},buttonIcons:{prev:'right-single-arrow',next:'left-single-arrow',prevYear:'right-double-arrow',nextYear:'left-double-arrow'},themeButtonIcons:{prev:'circle-triangle-e',next:'circle-triangle-w',nextYear:'seek-prev',prevYear:'seek-next'}};;;var langOptionHash=FC.langs={};FC.datepickerLang=function(langCode,dpLangCode,dpOptions){var fcOptions=langOptionHash[langCode]||(langOptionHash[langCode]={});fcOptions.isRTL=dpOptions.isRTL;fcOptions.weekNumberTitle=dpOptions.weekHeader;$.each(dpComputableOptions,function(name,func){fcOptions[name]=func(dpOptions);});if($.datepicker){$.datepicker.regional[dpLangCode]=$.datepicker.regional[langCode]=dpOptions;$.datepicker.regional.en=$.datepicker.regional[''];$.datepicker.setDefaults(dpOptions);}};FC.lang=function(langCode,newFcOptions){var fcOptions;var momOptions;fcOptions=langOptionHash[langCode]||(langOptionHash[langCode]={});if(newFcOptions){fcOptions=langOptionHash[langCode]=mergeOptions([fcOptions,newFcOptions]);}
-momOptions=getMomentLocaleData(langCode);$.each(momComputableOptions,function(name,func){if(fcOptions[name]==null){fcOptions[name]=func(momOptions,fcOptions);}});Calendar.defaults.lang=langCode;};var dpComputableOptions={buttonText:function(dpOptions){return{prev:stripHtmlEntities(dpOptions.prevText),next:stripHtmlEntities(dpOptions.nextText),today:stripHtmlEntities(dpOptions.currentText)};},monthYearFormat:function(dpOptions){return dpOptions.showMonthAfterYear?'YYYY['+dpOptions.yearSuffix+'] MMMM':'MMMM YYYY['+dpOptions.yearSuffix+']';}};var momComputableOptions={dayOfMonthFormat:function(momOptions,fcOptions){var format=momOptions.longDateFormat('l');format=format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g,'');if(fcOptions.isRTL){format+=' ddd';}
-else{format='ddd '+format;}
-return format;},mediumTimeFormat:function(momOptions){return momOptions.longDateFormat('LT').replace(/\s*a$/i,'a');},smallTimeFormat:function(momOptions){return momOptions.longDateFormat('LT').replace(':mm','(:mm)').replace(/(\Wmm)$/,'($1)').replace(/\s*a$/i,'a');},extraSmallTimeFormat:function(momOptions){return momOptions.longDateFormat('LT').replace(':mm','(:mm)').replace(/(\Wmm)$/,'($1)').replace(/\s*a$/i,'t');},hourFormat:function(momOptions){return momOptions.longDateFormat('LT').replace(':mm','').replace(/(\Wmm)$/,'').replace(/\s*a$/i,'a');},noMeridiemTimeFormat:function(momOptions){return momOptions.longDateFormat('LT').replace(/\s*a$/i,'');}};var instanceComputableOptions={smallDayDateFormat:function(options){return options.isRTL?'D dd':'dd D';},weekFormat:function(options){return options.isRTL?'w[ '+options.weekNumberTitle+']':'['+options.weekNumberTitle+' ]w';},smallWeekFormat:function(options){return options.isRTL?'w['+options.weekNumberTitle+']':'['+options.weekNumberTitle+']w';}};function populateInstanceComputableOptions(options){$.each(instanceComputableOptions,function(name,func){if(options[name]==null){options[name]=func(options);}});}
-function getMomentLocaleData(langCode){var func=moment.localeData||moment.langData;return func.call(moment,langCode)||func.call(moment,'en');}
-FC.lang('en',Calendar.englishDefaults);;;function Header(calendar,options){var t=this;t.render=render;t.removeElement=removeElement;t.updateTitle=updateTitle;t.activateButton=activateButton;t.deactivateButton=deactivateButton;t.disableButton=disableButton;t.enableButton=enableButton;t.getViewsWithButtons=getViewsWithButtons;var el=$();var viewsWithButtons=[];var tm;function render(){var sections=options.header;tm=options.theme?'ui':'fc';if(sections){el=$("<div class='fc-toolbar'/>").append(renderSection('left')).append(renderSection('right')).append(renderSection('center')).append('<div class="fc-clear"/>');return el;}}
-function removeElement(){el.remove();el=$();}
-function renderSection(position){var sectionEl=$('<div class="fc-'+position+'"/>');var buttonStr=options.header[position];if(buttonStr){$.each(buttonStr.split(' '),function(i){var groupChildren=$();var isOnlyButtons=true;var groupEl;$.each(this.split(','),function(j,buttonName){var customButtonProps;var viewSpec;var buttonClick;var overrideText;var defaultText;var themeIcon;var normalIcon;var innerHtml;var classes;var button;if(buttonName=='title'){groupChildren=groupChildren.add($('<h2>&nbsp;</h2>'));isOnlyButtons=false;}
-else{if((customButtonProps=(calendar.options.customButtons||{})[buttonName])){buttonClick=function(ev){if(customButtonProps.click){customButtonProps.click.call(button[0],ev);}};overrideText='';defaultText=customButtonProps.text;}
-else if((viewSpec=calendar.getViewSpec(buttonName))){buttonClick=function(){calendar.changeView(buttonName);};viewsWithButtons.push(buttonName);overrideText=viewSpec.buttonTextOverride;defaultText=viewSpec.buttonTextDefault;}
-else if(calendar[buttonName]){buttonClick=function(){calendar[buttonName]();};overrideText=(calendar.overrides.buttonText||{})[buttonName];defaultText=options.buttonText[buttonName];}
-if(buttonClick){themeIcon=customButtonProps?customButtonProps.themeIcon:options.themeButtonIcons[buttonName];normalIcon=customButtonProps?customButtonProps.icon:options.buttonIcons[buttonName];if(overrideText){innerHtml=htmlEscape(overrideText);}
-else if(themeIcon&&options.theme){innerHtml="<span class='ui-icon ui-icon-"+themeIcon+"'></span>";}
-else if(normalIcon&&!options.theme){innerHtml="<span class='fc-icon fc-icon-"+normalIcon+"'></span>";}
-else{innerHtml=htmlEscape(defaultText);}
-classes=['fc-'+buttonName+'-button',tm+'-button',tm+'-state-default'];button=$('<button type="button" class="'+classes.join(' ')+'">'+
-innerHtml+'</button>').click(function(ev){if(!button.hasClass(tm+'-state-disabled')){buttonClick(ev);if(button.hasClass(tm+'-state-active')||button.hasClass(tm+'-state-disabled')){button.removeClass(tm+'-state-hover');}}}).mousedown(function(){button.not('.'+tm+'-state-active').not('.'+tm+'-state-disabled').addClass(tm+'-state-down');}).mouseup(function(){button.removeClass(tm+'-state-down');}).hover(function(){button.not('.'+tm+'-state-active').not('.'+tm+'-state-disabled').addClass(tm+'-state-hover');},function(){button.removeClass(tm+'-state-hover').removeClass(tm+'-state-down');});groupChildren=groupChildren.add(button);}}});if(isOnlyButtons){groupChildren.first().addClass(tm+'-corner-left').end().last().addClass(tm+'-corner-right').end();}
-if(groupChildren.length>1){groupEl=$('<div/>');if(isOnlyButtons){groupEl.addClass('fc-button-group');}
-groupEl.append(groupChildren);sectionEl.append(groupEl);}
-else{sectionEl.append(groupChildren);}});}
-return sectionEl;}
-function updateTitle(text){el.find('h2').text(text);}
-function activateButton(buttonName){el.find('.fc-'+buttonName+'-button').addClass(tm+'-state-active');}
-function deactivateButton(buttonName){el.find('.fc-'+buttonName+'-button').removeClass(tm+'-state-active');}
-function disableButton(buttonName){el.find('.fc-'+buttonName+'-button').attr('disabled','disabled').addClass(tm+'-state-disabled');}
-function enableButton(buttonName){el.find('.fc-'+buttonName+'-button').removeAttr('disabled').removeClass(tm+'-state-disabled');}
-function getViewsWithButtons(){return viewsWithButtons;}};;FC.sourceNormalizers=[];FC.sourceFetchers=[];var ajaxDefaults={dataType:'json',cache:false};var eventGUID=1;function EventManager(options){var t=this;t.isFetchNeeded=isFetchNeeded;t.fetchEvents=fetchEvents;t.addEventSource=addEventSource;t.removeEventSource=removeEventSource;t.updateEvent=updateEvent;t.renderEvent=renderEvent;t.removeEvents=removeEvents;t.clientEvents=clientEvents;t.mutateEvent=mutateEvent;t.normalizeEventDates=normalizeEventDates;t.normalizeEventTimes=normalizeEventTimes;var reportEvents=t.reportEvents;var stickySource={events:[]};var sources=[stickySource];var rangeStart,rangeEnd;var currentFetchID=0;var pendingSourceCnt=0;var cache=[];$.each((options.events?[options.events]:[]).concat(options.eventSources||[]),function(i,sourceInput){var source=buildEventSource(sourceInput);if(source){sources.push(source);}});function isFetchNeeded(start,end){return!rangeStart||start<rangeStart||end>rangeEnd;}
-function fetchEvents(start,end){rangeStart=start;rangeEnd=end;cache=[];var fetchID=++currentFetchID;var len=sources.length;pendingSourceCnt=len;for(var i=0;i<len;i++){fetchEventSource(sources[i],fetchID);}}
-function fetchEventSource(source,fetchID){_fetchEventSource(source,function(eventInputs){var isArraySource=$.isArray(source.events);var i,eventInput;var abstractEvent;if(fetchID==currentFetchID){if(eventInputs){for(i=0;i<eventInputs.length;i++){eventInput=eventInputs[i];if(isArraySource){abstractEvent=eventInput;}
-else{abstractEvent=buildEventFromInput(eventInput,source);}
-if(abstractEvent){cache.push.apply(cache,expandEvent(abstractEvent));}}}
-pendingSourceCnt--;if(!pendingSourceCnt){reportEvents(cache);}}});}
-function _fetchEventSource(source,callback){var i;var fetchers=FC.sourceFetchers;var res;for(i=0;i<fetchers.length;i++){res=fetchers[i].call(t,source,rangeStart.clone(),rangeEnd.clone(),options.timezone,callback);if(res===true){return;}
-else if(typeof res=='object'){_fetchEventSource(res,callback);return;}}
-var events=source.events;if(events){if($.isFunction(events)){t.pushLoading();events.call(t,rangeStart.clone(),rangeEnd.clone(),options.timezone,function(events){callback(events);t.popLoading();});}
-else if($.isArray(events)){callback(events);}
-else{callback();}}else{var url=source.url;if(url){var success=source.success;var error=source.error;var complete=source.complete;var customData;if($.isFunction(source.data)){customData=source.data();}
-else{customData=source.data;}
-var data=$.extend({},customData||{});var startParam=firstDefined(source.startParam,options.startParam);var endParam=firstDefined(source.endParam,options.endParam);var timezoneParam=firstDefined(source.timezoneParam,options.timezoneParam);if(startParam){data[startParam]=rangeStart.format();}
-if(endParam){data[endParam]=rangeEnd.format();}
-if(options.timezone&&options.timezone!='local'){data[timezoneParam]=options.timezone;}
-t.pushLoading();$.ajax($.extend({},ajaxDefaults,source,{data:data,success:function(events){events=events||[];var res=applyAll(success,this,arguments);if($.isArray(res)){events=res;}
-callback(events);},error:function(){applyAll(error,this,arguments);callback();},complete:function(){applyAll(complete,this,arguments);t.popLoading();}}));}else{callback();}}}
-function addEventSource(sourceInput){var source=buildEventSource(sourceInput);if(source){sources.push(source);pendingSourceCnt++;fetchEventSource(source,currentFetchID);}}
-function buildEventSource(sourceInput){var normalizers=FC.sourceNormalizers;var source;var i;if($.isFunction(sourceInput)||$.isArray(sourceInput)){source={events:sourceInput};}
-else if(typeof sourceInput==='string'){source={url:sourceInput};}
-else if(typeof sourceInput==='object'){source=$.extend({},sourceInput);}
-if(source){if(source.className){if(typeof source.className==='string'){source.className=source.className.split(/\s+/);}}
-else{source.className=[];}
-if($.isArray(source.events)){source.origArray=source.events;source.events=$.map(source.events,function(eventInput){return buildEventFromInput(eventInput,source);});}
-for(i=0;i<normalizers.length;i++){normalizers[i].call(t,source);}
-return source;}}
-function removeEventSource(source){sources=$.grep(sources,function(src){return!isSourcesEqual(src,source);});cache=$.grep(cache,function(e){return!isSourcesEqual(e.source,source);});reportEvents(cache);}
-function isSourcesEqual(source1,source2){return source1&&source2&&getSourcePrimitive(source1)==getSourcePrimitive(source2);}
-function getSourcePrimitive(source){return((typeof source==='object')?(source.origArray||source.googleCalendarId||source.url||source.events):null)||source;}
-function updateEvent(event){event.start=t.moment(event.start);if(event.end){event.end=t.moment(event.end);}
-else{event.end=null;}
-mutateEvent(event,getMiscEventProps(event));reportEvents(cache);}
-function getMiscEventProps(event){var props={};$.each(event,function(name,val){if(isMiscEventPropName(name)){if(val!==undefined&&isAtomic(val)){props[name]=val;}}});return props;}
-function isMiscEventPropName(name){return!/^_|^(id|allDay|start|end)$/.test(name);}
-function renderEvent(eventInput,stick){var abstractEvent=buildEventFromInput(eventInput);var events;var i,event;if(abstractEvent){events=expandEvent(abstractEvent);for(i=0;i<events.length;i++){event=events[i];if(!event.source){if(stick){stickySource.events.push(event);event.source=stickySource;}
-cache.push(event);}}
-reportEvents(cache);return events;}
-return[];}
-function removeEvents(filter){var eventID;var i;if(filter==null){filter=function(){return true;};}
-else if(!$.isFunction(filter)){eventID=filter+'';filter=function(event){return event._id==eventID;};}
-cache=$.grep(cache,filter,true);for(i=0;i<sources.length;i++){if($.isArray(sources[i].events)){sources[i].events=$.grep(sources[i].events,filter,true);}}
-reportEvents(cache);}
-function clientEvents(filter){if($.isFunction(filter)){return $.grep(cache,filter);}
-else if(filter!=null){filter+='';return $.grep(cache,function(e){return e._id==filter;});}
-return cache;}
-function buildEventFromInput(input,source){var out={};var start,end;var allDay;if(options.eventDataTransform){input=options.eventDataTransform(input);}
-if(source&&source.eventDataTransform){input=source.eventDataTransform(input);}
-$.extend(out,input);if(source){out.source=source;}
-out._id=input._id||(input.id===undefined?'_fc'+eventGUID++:input.id+'');if(input.className){if(typeof input.className=='string'){out.className=input.className.split(/\s+/);}
-else{out.className=input.className;}}
-else{out.className=[];}
-start=input.start||input.date;end=input.end;if(isTimeString(start)){start=moment.duration(start);}
-if(isTimeString(end)){end=moment.duration(end);}
-if(input.dow||moment.isDuration(start)||moment.isDuration(end)){out.start=start?moment.duration(start):null;out.end=end?moment.duration(end):null;out._recurring=true;}
-else{if(start){start=t.moment(start);if(!start.isValid()){return false;}}
-if(end){end=t.moment(end);if(!end.isValid()){end=null;}}
-allDay=input.allDay;if(allDay===undefined){allDay=firstDefined(source?source.allDayDefault:undefined,options.allDayDefault);}
-assignDatesToEvent(start,end,allDay,out);}
-t.normalizeEvent(out);return out;}
-function assignDatesToEvent(start,end,allDay,event){event.start=start;event.end=end;event.allDay=allDay;normalizeEventDates(event);backupEventDates(event);}
-function normalizeEventDates(eventProps){normalizeEventTimes(eventProps);if(eventProps.end&&!eventProps.end.isAfter(eventProps.start)){eventProps.end=null;}
-if(!eventProps.end){if(options.forceEventDuration){eventProps.end=t.getDefaultEventEnd(eventProps.allDay,eventProps.start);}
-else{eventProps.end=null;}}}
-function normalizeEventTimes(eventProps){if(eventProps.allDay==null){eventProps.allDay=!(eventProps.start.hasTime()||(eventProps.end&&eventProps.end.hasTime()));}
-if(eventProps.allDay){eventProps.start.stripTime();if(eventProps.end){eventProps.end.stripTime();}}
-else{if(!eventProps.start.hasTime()){eventProps.start=t.applyTimezone(eventProps.start.time(0));}
-if(eventProps.end&&!eventProps.end.hasTime()){eventProps.end=t.applyTimezone(eventProps.end.time(0));}}}
-function expandEvent(abstractEvent,_rangeStart,_rangeEnd){var events=[];var dowHash;var dow;var i;var date;var startTime,endTime;var start,end;var event;_rangeStart=_rangeStart||rangeStart;_rangeEnd=_rangeEnd||rangeEnd;if(abstractEvent){if(abstractEvent._recurring){if((dow=abstractEvent.dow)){dowHash={};for(i=0;i<dow.length;i++){dowHash[dow[i]]=true;}}
-date=_rangeStart.clone().stripTime();while(date.isBefore(_rangeEnd)){if(!dowHash||dowHash[date.day()]){startTime=abstractEvent.start;endTime=abstractEvent.end;start=date.clone();end=null;if(startTime){start=start.time(startTime);}
-if(endTime){end=date.clone().time(endTime);}
-event=$.extend({},abstractEvent);assignDatesToEvent(start,end,!startTime&&!endTime,event);events.push(event);}
-date.add(1,'days');}}
-else{events.push(abstractEvent);}}
-return events;}
-function mutateEvent(event,newProps,largeUnit){var miscProps={};var oldProps;var clearEnd;var startDelta;var endDelta;var durationDelta;var undoFunc;function diffDates(date1,date0){if(largeUnit){return diffByUnit(date1,date0,largeUnit);}
-else if(newProps.allDay){return diffDay(date1,date0);}
-else{return diffDayTime(date1,date0);}}
-newProps=newProps||{};if(!newProps.start){newProps.start=event.start.clone();}
-if(newProps.end===undefined){newProps.end=event.end?event.end.clone():null;}
-if(newProps.allDay==null){newProps.allDay=event.allDay;}
-normalizeEventDates(newProps);oldProps={start:event._start.clone(),end:event._end?event._end.clone():t.getDefaultEventEnd(event._allDay,event._start),allDay:newProps.allDay};normalizeEventDates(oldProps);clearEnd=event._end!==null&&newProps.end===null;startDelta=diffDates(newProps.start,oldProps.start);if(newProps.end){endDelta=diffDates(newProps.end,oldProps.end);durationDelta=endDelta.subtract(startDelta);}
-else{durationDelta=null;}
-$.each(newProps,function(name,val){if(isMiscEventPropName(name)){if(val!==undefined){miscProps[name]=val;}}});undoFunc=mutateEvents(clientEvents(event._id),clearEnd,newProps.allDay,startDelta,durationDelta,miscProps);return{dateDelta:startDelta,durationDelta:durationDelta,undo:undoFunc};}
-function mutateEvents(events,clearEnd,allDay,dateDelta,durationDelta,miscProps){var isAmbigTimezone=t.getIsAmbigTimezone();var undoFunctions=[];if(dateDelta&&!dateDelta.valueOf()){dateDelta=null;}
-if(durationDelta&&!durationDelta.valueOf()){durationDelta=null;}
-$.each(events,function(i,event){var oldProps;var newProps;oldProps={start:event.start.clone(),end:event.end?event.end.clone():null,allDay:event.allDay};$.each(miscProps,function(name){oldProps[name]=event[name];});newProps={start:event._start,end:event._end,allDay:allDay};normalizeEventDates(newProps);if(clearEnd){newProps.end=null;}
-else if(durationDelta&&!newProps.end){newProps.end=t.getDefaultEventEnd(newProps.allDay,newProps.start);}
-if(dateDelta){newProps.start.add(dateDelta);if(newProps.end){newProps.end.add(dateDelta);}}
-if(durationDelta){newProps.end.add(durationDelta);}
-if(isAmbigTimezone&&!newProps.allDay&&(dateDelta||durationDelta)){newProps.start.stripZone();if(newProps.end){newProps.end.stripZone();}}
-$.extend(event,miscProps,newProps);backupEventDates(event);undoFunctions.push(function(){$.extend(event,oldProps);backupEventDates(event);});});return function(){for(var i=0;i<undoFunctions.length;i++){undoFunctions[i]();}};}
-t.getBusinessHoursEvents=getBusinessHoursEvents;function getBusinessHoursEvents(wholeDay){var optionVal=options.businessHours;var defaultVal={className:'fc-nonbusiness',start:'09:00',end:'17:00',dow:[1,2,3,4,5],rendering:'inverse-background'};var view=t.getView();var eventInput;if(optionVal){eventInput=$.extend({},defaultVal,typeof optionVal==='object'?optionVal:{});}
-if(eventInput){if(wholeDay){eventInput.start=null;eventInput.end=null;}
-return expandEvent(buildEventFromInput(eventInput),view.start,view.end);}
-return[];}
-t.isEventSpanAllowed=isEventSpanAllowed;t.isExternalSpanAllowed=isExternalSpanAllowed;t.isSelectionSpanAllowed=isSelectionSpanAllowed;function isEventSpanAllowed(span,event){var source=event.source||{};var constraint=firstDefined(event.constraint,source.constraint,options.eventConstraint);var overlap=firstDefined(event.overlap,source.overlap,options.eventOverlap);return isSpanAllowed(span,constraint,overlap,event);}
-function isExternalSpanAllowed(eventSpan,eventLocation,eventProps){var eventInput;var event;if(eventProps){eventInput=$.extend({},eventProps,eventLocation);event=expandEvent(buildEventFromInput(eventInput))[0];}
-if(event){return isEventSpanAllowed(eventSpan,event);}
-else{return isSelectionSpanAllowed(eventSpan);}}
-function isSelectionSpanAllowed(span){return isSpanAllowed(span,options.selectConstraint,options.selectOverlap);}
-function isSpanAllowed(span,constraint,overlap,event){var constraintEvents;var anyContainment;var peerEvents;var i,peerEvent;var peerOverlap;if(constraint!=null){constraintEvents=constraintToEvents(constraint);anyContainment=false;for(i=0;i<constraintEvents.length;i++){if(eventContainsRange(constraintEvents[i],span)){anyContainment=true;break;}}
-if(!anyContainment){return false;}}
-peerEvents=t.getPeerEvents(span,event);for(i=0;i<peerEvents.length;i++){peerEvent=peerEvents[i];if(eventIntersectsRange(peerEvent,span)){if(overlap===false){return false;}
-else if(typeof overlap==='function'&&!overlap(peerEvent,event)){return false;}
-if(event){peerOverlap=firstDefined(peerEvent.overlap,(peerEvent.source||{}).overlap);if(peerOverlap===false){return false;}
-if(typeof peerOverlap==='function'&&!peerOverlap(event,peerEvent)){return false;}}}}
-return true;}
-function constraintToEvents(constraintInput){if(constraintInput==='businessHours'){return getBusinessHoursEvents();}
-if(typeof constraintInput==='object'){return expandEvent(buildEventFromInput(constraintInput));}
-return clientEvents(constraintInput);}
-function eventContainsRange(event,range){var eventStart=event.start.clone().stripZone();var eventEnd=t.getEventEnd(event).stripZone();return range.start>=eventStart&&range.end<=eventEnd;}
-function eventIntersectsRange(event,range){var eventStart=event.start.clone().stripZone();var eventEnd=t.getEventEnd(event).stripZone();return range.start<eventEnd&&range.end>eventStart;}
-t.getEventCache=function(){return cache;};}
-Calendar.prototype.normalizeEvent=function(event){};Calendar.prototype.getPeerEvents=function(span,event){var cache=this.getEventCache();var peerEvents=[];var i,otherEvent;for(i=0;i<cache.length;i++){otherEvent=cache[i];if(!event||event._id!==otherEvent._id){peerEvents.push(otherEvent);}}
-return peerEvents;};function backupEventDates(event){event._allDay=event.allDay;event._start=event.start.clone();event._end=event.end?event.end.clone():null;};;var BasicView=FC.BasicView=View.extend({scroller:null,dayGridClass:DayGrid,dayGrid:null,dayNumbersVisible:false,weekNumbersVisible:false,weekNumberWidth:null,headContainerEl:null,headRowEl:null,initialize:function(){this.dayGrid=this.instantiateDayGrid();this.scroller=new Scroller({overflowX:'hidden',overflowY:'auto'});},instantiateDayGrid:function(){var subclass=this.dayGridClass.extend(basicDayGridMethods);return new subclass(this);},setRange:function(range){View.prototype.setRange.call(this,range);this.dayGrid.breakOnWeeks=/year|month|week/.test(this.intervalUnit);this.dayGrid.setRange(range);},computeRange:function(date){var range=View.prototype.computeRange.call(this,date);if(/year|month/.test(range.intervalUnit)){range.start.startOf('week');range.start=this.skipHiddenDays(range.start);if(range.end.weekday()){range.end.add(1,'week').startOf('week');range.end=this.skipHiddenDays(range.end,-1,true);}}
-return range;},renderDates:function(){this.dayNumbersVisible=this.dayGrid.rowCnt>1;this.weekNumbersVisible=this.opt('weekNumbers');this.dayGrid.numbersVisible=this.dayNumbersVisible||this.weekNumbersVisible;this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());this.renderHead();this.scroller.render();var dayGridContainerEl=this.scroller.el.addClass('fc-day-grid-container');var dayGridEl=$('<div class="fc-day-grid" />').appendTo(dayGridContainerEl);this.el.find('.fc-body > tr > td').append(dayGridContainerEl);this.dayGrid.setElement(dayGridEl);this.dayGrid.renderDates(this.hasRigidRows());},renderHead:function(){this.headContainerEl=this.el.find('.fc-head-container').html(this.dayGrid.renderHeadHtml());this.headRowEl=this.headContainerEl.find('.fc-row');},unrenderDates:function(){this.dayGrid.unrenderDates();this.dayGrid.removeElement();this.scroller.destroy();},renderBusinessHours:function(){this.dayGrid.renderBusinessHours();},renderSkeletonHtml:function(){return''+'<table>'+'<thead class="fc-head">'+'<tr>'+'<td class="fc-head-container '+this.widgetHeaderClass+'"></td>'+'</tr>'+'</thead>'+'<tbody class="fc-body">'+'<tr>'+'<td class="'+this.widgetContentClass+'"></td>'+'</tr>'+'</tbody>'+'</table>';},weekNumberStyleAttr:function(){if(this.weekNumberWidth!==null){return'style="width:'+this.weekNumberWidth+'px"';}
-return'';},hasRigidRows:function(){var eventLimit=this.opt('eventLimit');return eventLimit&&typeof eventLimit!=='number';},updateWidth:function(){if(this.weekNumbersVisible){this.weekNumberWidth=matchCellWidths(this.el.find('.fc-week-number'));}},setHeight:function(totalHeight,isAuto){var eventLimit=this.opt('eventLimit');var scrollerHeight;var scrollbarWidths;this.scroller.clear();uncompensateScroll(this.headRowEl);this.dayGrid.removeSegPopover();if(eventLimit&&typeof eventLimit==='number'){this.dayGrid.limitRows(eventLimit);}
-scrollerHeight=this.computeScrollerHeight(totalHeight);this.setGridHeight(scrollerHeight,isAuto);if(eventLimit&&typeof eventLimit!=='number'){this.dayGrid.limitRows(eventLimit);}
-if(!isAuto){this.scroller.setHeight(scrollerHeight);scrollbarWidths=this.scroller.getScrollbarWidths();if(scrollbarWidths.left||scrollbarWidths.right){compensateScroll(this.headRowEl,scrollbarWidths);scrollerHeight=this.computeScrollerHeight(totalHeight);this.scroller.setHeight(scrollerHeight);}
-this.scroller.lockOverflow(scrollbarWidths);}},computeScrollerHeight:function(totalHeight){return totalHeight-
-subtractInnerElHeight(this.el,this.scroller.el);},setGridHeight:function(height,isAuto){if(isAuto){undistributeHeight(this.dayGrid.rowEls);}
-else{distributeHeight(this.dayGrid.rowEls,height,true);}},queryScroll:function(){return this.scroller.getScrollTop();},setScroll:function(top){this.scroller.setScrollTop(top);},prepareHits:function(){this.dayGrid.prepareHits();},releaseHits:function(){this.dayGrid.releaseHits();},queryHit:function(left,top){return this.dayGrid.queryHit(left,top);},getHitSpan:function(hit){return this.dayGrid.getHitSpan(hit);},getHitEl:function(hit){return this.dayGrid.getHitEl(hit);},renderEvents:function(events){this.dayGrid.renderEvents(events);this.updateHeight();},getEventSegs:function(){return this.dayGrid.getEventSegs();},unrenderEvents:function(){this.dayGrid.unrenderEvents();},renderDrag:function(dropLocation,seg){return this.dayGrid.renderDrag(dropLocation,seg);},unrenderDrag:function(){this.dayGrid.unrenderDrag();},renderSelection:function(span){this.dayGrid.renderSelection(span);},unrenderSelection:function(){this.dayGrid.unrenderSelection();}});var basicDayGridMethods={renderHeadIntroHtml:function(){var view=this.view;if(view.weekNumbersVisible){return''+'<th class="fc-week-number '+view.widgetHeaderClass+'" '+view.weekNumberStyleAttr()+'>'+'<span>'+
-htmlEscape(view.opt('weekNumberTitle'))+'</span>'+'</th>';}
-return'';},renderNumberIntroHtml:function(row){var view=this.view;if(view.weekNumbersVisible){return''+'<td class="fc-week-number" '+view.weekNumberStyleAttr()+'>'+'<span>'+
-this.getCellDate(row,0).format('w')+'</span>'+'</td>';}
-return'';},renderBgIntroHtml:function(){var view=this.view;if(view.weekNumbersVisible){return'<td class="fc-week-number '+view.widgetContentClass+'" '+
-view.weekNumberStyleAttr()+'></td>';}
-return'';},renderIntroHtml:function(){var view=this.view;if(view.weekNumbersVisible){return'<td class="fc-week-number" '+view.weekNumberStyleAttr()+'></td>';}
-return'';}};;;var MonthView=FC.MonthView=BasicView.extend({computeRange:function(date){var range=BasicView.prototype.computeRange.call(this,date);var rowCnt;if(this.isFixedWeeks()){rowCnt=Math.ceil(range.end.diff(range.start,'weeks',true));range.end.add(6-rowCnt,'weeks');}
-return range;},setGridHeight:function(height,isAuto){isAuto=isAuto||this.opt('weekMode')==='variable';if(isAuto){height*=this.rowCnt/6;}
-distributeHeight(this.dayGrid.rowEls,height,!isAuto);},isFixedWeeks:function(){var weekMode=this.opt('weekMode');if(weekMode){return weekMode==='fixed';}
-return this.opt('fixedWeekCount');}});;;fcViews.basic={'class':BasicView};fcViews.basicDay={type:'basic',duration:{days:1}};fcViews.basicWeek={type:'basic',duration:{weeks:1}};fcViews.month={'class':MonthView,duration:{months:1},defaults:{fixedWeekCount:true}};;;var AgendaView=FC.AgendaView=View.extend({scroller:null,timeGridClass:TimeGrid,timeGrid:null,dayGridClass:DayGrid,dayGrid:null,axisWidth:null,headContainerEl:null,noScrollRowEls:null,bottomRuleEl:null,initialize:function(){this.timeGrid=this.instantiateTimeGrid();if(this.opt('allDaySlot')){this.dayGrid=this.instantiateDayGrid();}
-this.scroller=new Scroller({overflowX:'hidden',overflowY:'auto'});},instantiateTimeGrid:function(){var subclass=this.timeGridClass.extend(agendaTimeGridMethods);return new subclass(this);},instantiateDayGrid:function(){var subclass=this.dayGridClass.extend(agendaDayGridMethods);return new subclass(this);},setRange:function(range){View.prototype.setRange.call(this,range);this.timeGrid.setRange(range);if(this.dayGrid){this.dayGrid.setRange(range);}},renderDates:function(){this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());this.renderHead();this.scroller.render();var timeGridWrapEl=this.scroller.el.addClass('fc-time-grid-container');var timeGridEl=$('<div class="fc-time-grid" />').appendTo(timeGridWrapEl);this.el.find('.fc-body > tr > td').append(timeGridWrapEl);this.timeGrid.setElement(timeGridEl);this.timeGrid.renderDates();this.bottomRuleEl=$('<hr class="fc-divider '+this.widgetHeaderClass+'"/>').appendTo(this.timeGrid.el);if(this.dayGrid){this.dayGrid.setElement(this.el.find('.fc-day-grid'));this.dayGrid.renderDates();this.dayGrid.bottomCoordPadding=this.dayGrid.el.next('hr').outerHeight();}
-this.noScrollRowEls=this.el.find('.fc-row:not(.fc-scroller *)');},renderHead:function(){this.headContainerEl=this.el.find('.fc-head-container').html(this.timeGrid.renderHeadHtml());},unrenderDates:function(){this.timeGrid.unrenderDates();this.timeGrid.removeElement();if(this.dayGrid){this.dayGrid.unrenderDates();this.dayGrid.removeElement();}
-this.scroller.destroy();},renderSkeletonHtml:function(){return''+'<table>'+'<thead class="fc-head">'+'<tr>'+'<td class="fc-head-container '+this.widgetHeaderClass+'"></td>'+'</tr>'+'</thead>'+'<tbody class="fc-body">'+'<tr>'+'<td class="'+this.widgetContentClass+'">'+
-(this.dayGrid?'<div class="fc-day-grid"/>'+'<hr class="fc-divider '+this.widgetHeaderClass+'"/>':'')+'</td>'+'</tr>'+'</tbody>'+'</table>';},axisStyleAttr:function(){if(this.axisWidth!==null){return'style="width:'+this.axisWidth+'px"';}
-return'';},renderBusinessHours:function(){this.timeGrid.renderBusinessHours();if(this.dayGrid){this.dayGrid.renderBusinessHours();}},unrenderBusinessHours:function(){this.timeGrid.unrenderBusinessHours();if(this.dayGrid){this.dayGrid.unrenderBusinessHours();}},getNowIndicatorUnit:function(){return this.timeGrid.getNowIndicatorUnit();},renderNowIndicator:function(date){this.timeGrid.renderNowIndicator(date);},unrenderNowIndicator:function(){this.timeGrid.unrenderNowIndicator();},updateSize:function(isResize){this.timeGrid.updateSize(isResize);View.prototype.updateSize.call(this,isResize);},updateWidth:function(){this.axisWidth=matchCellWidths(this.el.find('.fc-axis'));},setHeight:function(totalHeight,isAuto){var eventLimit;var scrollerHeight;var scrollbarWidths;this.bottomRuleEl.hide();this.scroller.clear();uncompensateScroll(this.noScrollRowEls);if(this.dayGrid){this.dayGrid.removeSegPopover();eventLimit=this.opt('eventLimit');if(eventLimit&&typeof eventLimit!=='number'){eventLimit=AGENDA_ALL_DAY_EVENT_LIMIT;}
-if(eventLimit){this.dayGrid.limitRows(eventLimit);}}
-if(!isAuto){scrollerHeight=this.computeScrollerHeight(totalHeight);this.scroller.setHeight(scrollerHeight);scrollbarWidths=this.scroller.getScrollbarWidths();if(scrollbarWidths.left||scrollbarWidths.right){compensateScroll(this.noScrollRowEls,scrollbarWidths);scrollerHeight=this.computeScrollerHeight(totalHeight);this.scroller.setHeight(scrollerHeight);}
-this.scroller.lockOverflow(scrollbarWidths);if(this.timeGrid.getTotalSlatHeight()<scrollerHeight){this.bottomRuleEl.show();}}},computeScrollerHeight:function(totalHeight){return totalHeight-
-subtractInnerElHeight(this.el,this.scroller.el);},computeInitialScroll:function(){var scrollTime=moment.duration(this.opt('scrollTime'));var top=this.timeGrid.computeTimeTop(scrollTime);top=Math.ceil(top);if(top){top++;}
-return top;},queryScroll:function(){return this.scroller.getScrollTop();},setScroll:function(top){this.scroller.setScrollTop(top);},prepareHits:function(){this.timeGrid.prepareHits();if(this.dayGrid){this.dayGrid.prepareHits();}},releaseHits:function(){this.timeGrid.releaseHits();if(this.dayGrid){this.dayGrid.releaseHits();}},queryHit:function(left,top){var hit=this.timeGrid.queryHit(left,top);if(!hit&&this.dayGrid){hit=this.dayGrid.queryHit(left,top);}
-return hit;},getHitSpan:function(hit){return hit.component.getHitSpan(hit);},getHitEl:function(hit){return hit.component.getHitEl(hit);},renderEvents:function(events){var dayEvents=[];var timedEvents=[];var daySegs=[];var timedSegs;var i;for(i=0;i<events.length;i++){if(events[i].allDay){dayEvents.push(events[i]);}
-else{timedEvents.push(events[i]);}}
-timedSegs=this.timeGrid.renderEvents(timedEvents);if(this.dayGrid){daySegs=this.dayGrid.renderEvents(dayEvents);}
-this.updateHeight();},getEventSegs:function(){return this.timeGrid.getEventSegs().concat(this.dayGrid?this.dayGrid.getEventSegs():[]);},unrenderEvents:function(){this.timeGrid.unrenderEvents();if(this.dayGrid){this.dayGrid.unrenderEvents();}},renderDrag:function(dropLocation,seg){if(dropLocation.start.hasTime()){return this.timeGrid.renderDrag(dropLocation,seg);}
-else if(this.dayGrid){return this.dayGrid.renderDrag(dropLocation,seg);}},unrenderDrag:function(){this.timeGrid.unrenderDrag();if(this.dayGrid){this.dayGrid.unrenderDrag();}},renderSelection:function(span){if(span.start.hasTime()||span.end.hasTime()){this.timeGrid.renderSelection(span);}
-else if(this.dayGrid){this.dayGrid.renderSelection(span);}},unrenderSelection:function(){this.timeGrid.unrenderSelection();if(this.dayGrid){this.dayGrid.unrenderSelection();}}});var agendaTimeGridMethods={renderHeadIntroHtml:function(){var view=this.view;var weekText;if(view.opt('weekNumbers')){weekText=this.start.format(view.opt('smallWeekFormat'));return''+'<th class="fc-axis fc-week-number '+view.widgetHeaderClass+'" '+view.axisStyleAttr()+'>'+'<span>'+
-htmlEscape(weekText)+'</span>'+'</th>';}
-else{return'<th class="fc-axis '+view.widgetHeaderClass+'" '+view.axisStyleAttr()+'></th>';}},renderBgIntroHtml:function(){var view=this.view;return'<td class="fc-axis '+view.widgetContentClass+'" '+view.axisStyleAttr()+'></td>';},renderIntroHtml:function(){var view=this.view;return'<td class="fc-axis" '+view.axisStyleAttr()+'></td>';}};var agendaDayGridMethods={renderBgIntroHtml:function(){var view=this.view;return''+'<td class="fc-axis '+view.widgetContentClass+'" '+view.axisStyleAttr()+'>'+'<span>'+
-(view.opt('allDayHtml')||htmlEscape(view.opt('allDayText')))+'</span>'+'</td>';},renderIntroHtml:function(){var view=this.view;return'<td class="fc-axis" '+view.axisStyleAttr()+'></td>';}};;;var AGENDA_ALL_DAY_EVENT_LIMIT=5;var AGENDA_STOCK_SUB_DURATIONS=[{hours:1},{minutes:30},{minutes:15},{seconds:30},{seconds:15}];fcViews.agenda={'class':AgendaView,defaults:{allDaySlot:true,allDayText:'all-day',slotDuration:'00:30:00',minTime:'00:00:00',maxTime:'24:00:00',slotEventOverlap:true}};fcViews.agendaDay={type:'agenda',duration:{days:1}};fcViews.agendaWeek={type:'agenda',duration:{weeks:1}};;;return FC;});
\ No newline at end of file
+(function(factory){if(typeof define==="function"&&define.amd){define(["jquery","moment"],factory)}else if(typeof exports==="object"){module.exports=factory(require("jquery"),require("moment"))}else{factory(jQuery,moment)}})(function($,moment){var FC=$.fullCalendar={version:"2.7.3",internalApiVersion:4};var fcViews=FC.views={};$.fn.fullCalendar=function(options){var args=Array.prototype.slice.call(arguments,1);var res=this;this.each(function(i,_element){var element=$(_element);var calendar=element.data("fullCalendar");var singleRes;if(typeof options==="string"){if(calendar&&$.isFunction(calendar[options])){singleRes=calendar[options].apply(calendar,args);if(!i){res=singleRes}if(options==="destroy"){element.removeData("fullCalendar")}}}else if(!calendar){calendar=new Calendar(element,options);element.data("fullCalendar",calendar);calendar.render()}});return res};var complexOptions=["header","buttonText","buttonIcons","themeButtonIcons"];function mergeOptions(optionObjs){return mergeProps(optionObjs,complexOptions)}function massageOverrides(input){var overrides={views:input.views||{}};var subObj;$.each(input,function(name,val){if(name!="views"){if($.isPlainObject(val)&&!/(time|duration|interval)$/i.test(name)&&$.inArray(name,complexOptions)==-1){subObj=null;$.each(val,function(subName,subVal){if(/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)){if(!overrides.views[subName]){overrides.views[subName]={}}overrides.views[subName][name]=subVal}else{if(!subObj){subObj={}}subObj[subName]=subVal}});if(subObj){overrides[name]=subObj}}else{overrides[name]=val}}});return overrides}FC.intersectRanges=intersectRanges;FC.applyAll=applyAll;FC.debounce=debounce;FC.isInt=isInt;FC.htmlEscape=htmlEscape;FC.cssToStr=cssToStr;FC.proxy=proxy;FC.capitaliseFirstLetter=capitaliseFirstLetter;function compensateScroll(rowEls,scrollbarWidths){if(scrollbarWidths.left){rowEls.css({"border-left-width":1,"margin-left":scrollbarWidths.left-1})}if(scrollbarWidths.right){rowEls.css({"border-right-width":1,"margin-right":scrollbarWidths.right-1})}}function uncompensateScroll(rowEls){rowEls.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function disableCursor(){$("body").addClass("fc-not-allowed")}function enableCursor(){$("body").removeClass("fc-not-allowed")}function distributeHeight(els,availableHeight,shouldRedistribute){var minOffset1=Math.floor(availableHeight/els.length);var minOffset2=Math.floor(availableHeight-minOffset1*(els.length-1));var flexEls=[];var flexOffsets=[];var flexHeights=[];var usedHeight=0;undistributeHeight(els);els.each(function(i,el){var minOffset=i===els.length-1?minOffset2:minOffset1;var naturalOffset=$(el).outerHeight(true);if(naturalOffset<minOffset){flexEls.push(el);flexOffsets.push(naturalOffset);flexHeights.push($(el).height())}else{usedHeight+=naturalOffset}});if(shouldRedistribute){availableHeight-=usedHeight;minOffset1=Math.floor(availableHeight/flexEls.length);minOffset2=Math.floor(availableHeight-minOffset1*(flexEls.length-1))}$(flexEls).each(function(i,el){var minOffset=i===flexEls.length-1?minOffset2:minOffset1;var naturalOffset=flexOffsets[i];var naturalHeight=flexHeights[i];var newHeight=minOffset-(naturalOffset-naturalHeight);if(naturalOffset<minOffset){$(el).height(newHeight)}})}function undistributeHeight(els){els.height("")}function matchCellWidths(els){var maxInnerWidth=0;els.find("> span").each(function(i,innerEl){var innerWidth=$(innerEl).outerWidth();if(innerWidth>maxInnerWidth){maxInnerWidth=innerWidth}});maxInnerWidth++;els.width(maxInnerWidth);return maxInnerWidth}function subtractInnerElHeight(outerEl,innerEl){var both=outerEl.add(innerEl);var diff;both.css({position:"relative",left:-1});diff=outerEl.outerHeight()-innerEl.outerHeight();both.css({position:"",left:""});return diff}FC.getOuterRect=getOuterRect;FC.getClientRect=getClientRect;FC.getContentRect=getContentRect;FC.getScrollbarWidths=getScrollbarWidths;function getScrollParent(el){var position=el.css("position"),scrollParent=el.parents().filter(function(){var parent=$(this);return/(auto|scroll)/.test(parent.css("overflow")+parent.css("overflow-y")+parent.css("overflow-x"))}).eq(0);return position==="fixed"||!scrollParent.length?$(el[0].ownerDocument||document):scrollParent}function getOuterRect(el,origin){var offset=el.offset();var left=offset.left-(origin?origin.left:0);var top=offset.top-(origin?origin.top:0);return{left:left,right:left+el.outerWidth(),top:top,bottom:top+el.outerHeight()}}function getClientRect(el,origin){var offset=el.offset();var scrollbarWidths=getScrollbarWidths(el);var left=offset.left+getCssFloat(el,"border-left-width")+scrollbarWidths.left-(origin?origin.left:0);var top=offset.top+getCssFloat(el,"border-top-width")+scrollbarWidths.top-(origin?origin.top:0);return{left:left,right:left+el[0].clientWidth,top:top,bottom:top+el[0].clientHeight}}function getContentRect(el,origin){var offset=el.offset();var left=offset.left+getCssFloat(el,"border-left-width")+getCssFloat(el,"padding-left")-(origin?origin.left:0);var top=offset.top+getCssFloat(el,"border-top-width")+getCssFloat(el,"padding-top")-(origin?origin.top:0);return{left:left,right:left+el.width(),top:top,bottom:top+el.height()}}function getScrollbarWidths(el){var leftRightWidth=el.innerWidth()-el[0].clientWidth;var widths={left:0,right:0,top:0,bottom:el.innerHeight()-el[0].clientHeight};if(getIsLeftRtlScrollbars()&&el.css("direction")=="rtl"){widths.left=leftRightWidth}else{widths.right=leftRightWidth}return widths}var _isLeftRtlScrollbars=null;function getIsLeftRtlScrollbars(){if(_isLeftRtlScrollbars===null){_isLeftRtlScrollbars=computeIsLeftRtlScrollbars()}return _isLeftRtlScrollbars}function computeIsLeftRtlScrollbars(){var el=$("<div><div/></div>").css({position:"absolute",top:-1e3,left:0,border:0,padding:0,overflow:"scroll",direction:"rtl"}).appendTo("body");var innerEl=el.children();var res=innerEl.offset().left>el.offset().left;el.remove();return res}function getCssFloat(el,prop){return parseFloat(el.css(prop))||0}FC.preventDefault=preventDefault;function isPrimaryMouseButton(ev){return ev.which==1&&!ev.ctrlKey}function getEvX(ev){if(ev.pageX!==undefined){return ev.pageX}var touches=ev.originalEvent.touches;if(touches){return touches[0].pageX}}function getEvY(ev){if(ev.pageY!==undefined){return ev.pageY}var touches=ev.originalEvent.touches;if(touches){return touches[0].pageY}}function getEvIsTouch(ev){return/^touch/.test(ev.type)}function preventSelection(el){el.addClass("fc-unselectable").on("selectstart",preventDefault)}function preventDefault(ev){ev.preventDefault()}function bindAnyScroll(handler){if(window.addEventListener){window.addEventListener("scroll",handler,true);return true}return false}function unbindAnyScroll(handler){if(window.removeEventListener){window.removeEventListener("scroll",handler,true);return true}return false}FC.intersectRects=intersectRects;function intersectRects(rect1,rect2){var res={left:Math.max(rect1.left,rect2.left),right:Math.min(rect1.right,rect2.right),top:Math.max(rect1.top,rect2.top),bottom:Math.min(rect1.bottom,rect2.bottom)};if(res.left<res.right&&res.top<res.bottom){return res}return false}function constrainPoint(point,rect){return{left:Math.min(Math.max(point.left,rect.left),rect.right),top:Math.min(Math.max(point.top,rect.top),rect.bottom)}}function getRectCenter(rect){return{left:(rect.left+rect.right)/2,top:(rect.top+rect.bottom)/2}}function diffPoints(point1,point2){return{left:point1.left-point2.left,top:point1.top-point2.top}}FC.parseFieldSpecs=parseFieldSpecs;FC.compareByFieldSpecs=compareByFieldSpecs;FC.compareByFieldSpec=compareByFieldSpec;FC.flexibleCompare=flexibleCompare;function parseFieldSpecs(input){var specs=[];var tokens=[];var i,token;if(typeof input==="string"){tokens=input.split(/\s*,\s*/)}else if(typeof input==="function"){tokens=[input]}else if($.isArray(input)){tokens=input}for(i=0;i<tokens.length;i++){token=tokens[i];if(typeof token==="string"){specs.push(token.charAt(0)=="-"?{field:token.substring(1),order:-1}:{field:token,order:1})}else if(typeof token==="function"){specs.push({func:token})}}return specs}function compareByFieldSpecs(obj1,obj2,fieldSpecs){var i;var cmp;for(i=0;i<fieldSpecs.length;i++){cmp=compareByFieldSpec(obj1,obj2,fieldSpecs[i]);if(cmp){return cmp}}return 0}function compareByFieldSpec(obj1,obj2,fieldSpec){if(fieldSpec.func){return fieldSpec.func(obj1,obj2)}return flexibleCompare(obj1[fieldSpec.field],obj2[fieldSpec.field])*(fieldSpec.order||1)}function flexibleCompare(a,b){if(!a&&!b){return 0}if(b==null){return-1}if(a==null){return 1}if($.type(a)==="string"||$.type(b)==="string"){return String(a).localeCompare(String(b))}return a-b}function intersectRanges(subjectRange,constraintRange){var subjectStart=subjectRange.start;var subjectEnd=subjectRange.end;var constraintStart=constraintRange.start;var constraintEnd=constraintRange.end;var segStart,segEnd;var isStart,isEnd;if(subjectEnd>constraintStart&&subjectStart<constraintEnd){if(subjectStart>=constraintStart){segStart=subjectStart.clone();isStart=true}else{segStart=constraintStart.clone();isStart=false}if(subjectEnd<=constraintEnd){segEnd=subjectEnd.clone();isEnd=true}else{segEnd=constraintEnd.clone();isEnd=false}return{start:segStart,end:segEnd,isStart:isStart,isEnd:isEnd}}}FC.computeIntervalUnit=computeIntervalUnit;FC.divideRangeByDuration=divideRangeByDuration;FC.divideDurationByDuration=divideDurationByDuration;FC.multiplyDuration=multiplyDuration;FC.durationHasTime=durationHasTime;var dayIDs=["sun","mon","tue","wed","thu","fri","sat"];var intervalUnits=["year","month","week","day","hour","minute","second","millisecond"];function diffDayTime(a,b){return moment.duration({days:a.clone().stripTime().diff(b.clone().stripTime(),"days"),ms:a.time()-b.time()})}function diffDay(a,b){return moment.duration({days:a.clone().stripTime().diff(b.clone().stripTime(),"days")})}function diffByUnit(a,b,unit){return moment.duration(Math.round(a.diff(b,unit,true)),unit)}function computeIntervalUnit(start,end){var i,unit;var val;for(i=0;i<intervalUnits.length;i++){unit=intervalUnits[i];val=computeRangeAs(unit,start,end);if(val>=1&&isInt(val)){break}}return unit}function computeRangeAs(unit,start,end){if(end!=null){return end.diff(start,unit,true)}else if(moment.isDuration(start)){return start.as(unit)}else{return start.end.diff(start.start,unit,true)}}function divideRangeByDuration(start,end,dur){var months;if(durationHasTime(dur)){return(end-start)/dur}months=dur.asMonths();if(Math.abs(months)>=1&&isInt(months)){return end.diff(start,"months",true)/months}return end.diff(start,"days",true)/dur.asDays()}function divideDurationByDuration(dur1,dur2){var months1,months2;if(durationHasTime(dur1)||durationHasTime(dur2)){return dur1/dur2}months1=dur1.asMonths();months2=dur2.asMonths();if(Math.abs(months1)>=1&&isInt(months1)&&Math.abs(months2)>=1&&isInt(months2)){return months1/months2}return dur1.asDays()/dur2.asDays()}function multiplyDuration(dur,n){var months;if(durationHasTime(dur)){return moment.duration(dur*n)}months=dur.asMonths();if(Math.abs(months)>=1&&isInt(months)){return moment.duration({months:months*n})}return moment.duration({days:dur.asDays()*n})}function durationHasTime(dur){return Boolean(dur.hours()||dur.minutes()||dur.seconds()||dur.milliseconds())}function isNativeDate(input){return Object.prototype.toString.call(input)==="[object Date]"||input instanceof Date}function isTimeString(str){return/^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str)}FC.log=function(){var console=window.console;if(console&&console.log){return console.log.apply(console,arguments)}};FC.warn=function(){var console=window.console;if(console&&console.warn){return console.warn.apply(console,arguments)}else{return FC.log.apply(FC,arguments)}};var hasOwnPropMethod={}.hasOwnProperty;function mergeProps(propObjs,complexProps){var dest={};var i,name;var complexObjs;var j,val;var props;if(complexProps){for(i=0;i<complexProps.length;i++){name=complexProps[i];complexObjs=[];for(j=propObjs.length-1;j>=0;j--){val=propObjs[j][name];if(typeof val==="object"){complexObjs.unshift(val)}else if(val!==undefined){dest[name]=val;break}}if(complexObjs.length){dest[name]=mergeProps(complexObjs)}}}for(i=propObjs.length-1;i>=0;i--){props=propObjs[i];for(name in props){if(!(name in dest)){dest[name]=props[name]}}}return dest}function createObject(proto){var f=function(){};f.prototype=proto;return new f}function copyOwnProps(src,dest){for(var name in src){if(hasOwnProp(src,name)){dest[name]=src[name]}}}function copyNativeMethods(src,dest){var names=["constructor","toString","valueOf"];var i,name;for(i=0;i<names.length;i++){name=names[i];if(src[name]!==Object.prototype[name]){dest[name]=src[name]}}}function hasOwnProp(obj,name){return hasOwnPropMethod.call(obj,name)}function isAtomic(val){return/undefined|null|boolean|number|string/.test($.type(val))}function applyAll(functions,thisObj,args){if($.isFunction(functions)){functions=[functions]}if(functions){var i;var ret;for(i=0;i<functions.length;i++){ret=functions[i].apply(thisObj,args)||ret}return ret}}function firstDefined(){for(var i=0;i<arguments.length;i++){if(arguments[i]!==undefined){return arguments[i]}}}function htmlEscape(s){return(s+"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/'/g,"&#039;").replace(/"/g,"&quot;").replace(/\n/g,"<br />")}function stripHtmlEntities(text){return text.replace(/&.*?;/g,"")}function cssToStr(cssProps){var statements=[];$.each(cssProps,function(name,val){if(val!=null){statements.push(name+":"+val)}});return statements.join(";")}function capitaliseFirstLetter(str){return str.charAt(0).toUpperCase()+str.slice(1)}function compareNumbers(a,b){return a-b}function isInt(n){return n%1===0}function proxy(obj,methodName){var method=obj[methodName];return function(){return method.apply(obj,arguments)}}function debounce(func,wait,immediate){var timeout,args,context,timestamp,result;var later=function(){var last=+new Date-timestamp;if(last<wait){timeout=setTimeout(later,wait-last)}else{timeout=null;if(!immediate){result=func.apply(context,args);context=args=null}}};return function(){context=this;args=arguments;timestamp=+new Date;var callNow=immediate&&!timeout;if(!timeout){timeout=setTimeout(later,wait)}if(callNow){result=func.apply(context,args);context=args=null}return result}}var ambigDateOfMonthRegex=/^\s*\d{4}-\d\d$/;var ambigTimeOrZoneRegex=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;var newMomentProto=moment.fn;var oldMomentProto=$.extend({},newMomentProto);var allowValueOptimization;var setUTCValues;var setLocalValues;FC.moment=function(){return makeMoment(arguments)};FC.moment.utc=function(){var mom=makeMoment(arguments,true);if(mom.hasTime()){mom.utc()}return mom};FC.moment.parseZone=function(){return makeMoment(arguments,true,true)};function makeMoment(args,parseAsUTC,parseZone){var input=args[0];var isSingleString=args.length==1&&typeof input==="string";var isAmbigTime;var isAmbigZone;var ambigMatch;var mom;if(moment.isMoment(input)){mom=moment.apply(null,args);transferAmbigs(input,mom)}else if(isNativeDate(input)||input===undefined){mom=moment.apply(null,args)}else{isAmbigTime=false;isAmbigZone=false;if(isSingleString){if(ambigDateOfMonthRegex.test(input)){input+="-01";args=[input];isAmbigTime=true;isAmbigZone=true}else if(ambigMatch=ambigTimeOrZoneRegex.exec(input)){isAmbigTime=!ambigMatch[5];isAmbigZone=true}}else if($.isArray(input)){isAmbigZone=true}if(parseAsUTC||isAmbigTime){mom=moment.utc.apply(moment,args)}else{mom=moment.apply(null,args)}if(isAmbigTime){mom._ambigTime=true;mom._ambigZone=true}else if(parseZone){if(isAmbigZone){mom._ambigZone=true}else if(isSingleString){if(mom.utcOffset){mom.utcOffset(input)}else{mom.zone(input)}}}}mom._fullCalendar=true;return mom}newMomentProto.clone=function(){var mom=oldMomentProto.clone.apply(this,arguments);transferAmbigs(this,mom);if(this._fullCalendar){mom._fullCalendar=true}return mom};newMomentProto.week=newMomentProto.weeks=function(input){var weekCalc=(this._locale||this._lang)._fullCalendar_weekCalc;if(input==null&&typeof weekCalc==="function"){return weekCalc(this)}else if(weekCalc==="ISO"){return oldMomentProto.isoWeek.apply(this,arguments)}return oldMomentProto.week.apply(this,arguments)};newMomentProto.time=function(time){if(!this._fullCalendar){return oldMomentProto.time.apply(this,arguments)}if(time==null){return moment.duration({hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()})}else{this._ambigTime=false;if(!moment.isDuration(time)&&!moment.isMoment(time)){time=moment.duration(time)}var dayHours=0;if(moment.isDuration(time)){dayHours=Math.floor(time.asDays())*24}return this.hours(dayHours+time.hours()).minutes(time.minutes()).seconds(time.seconds()).milliseconds(time.milliseconds())}};newMomentProto.stripTime=function(){var a;if(!this._ambigTime){a=this.toArray();this.utc();setUTCValues(this,a.slice(0,3));this._ambigTime=true;this._ambigZone=true}return this};newMomentProto.hasTime=function(){return!this._ambigTime};newMomentProto.stripZone=function(){var a,wasAmbigTime;if(!this._ambigZone){a=this.toArray();wasAmbigTime=this._ambigTime;this.utc();setUTCValues(this,a);this._ambigTime=wasAmbigTime||false;this._ambigZone=true}return this};newMomentProto.hasZone=function(){return!this._ambigZone};newMomentProto.local=function(){var a=this.toArray();var wasAmbigZone=this._ambigZone;oldMomentProto.local.apply(this,arguments);this._ambigTime=false;this._ambigZone=false;if(wasAmbigZone){setLocalValues(this,a)}return this};newMomentProto.utc=function(){oldMomentProto.utc.apply(this,arguments);this._ambigTime=false;this._ambigZone=false;return this};$.each(["zone","utcOffset"],function(i,name){if(oldMomentProto[name]){newMomentProto[name]=function(tzo){if(tzo!=null){this._ambigTime=false;this._ambigZone=false}return oldMomentProto[name].apply(this,arguments)}}});newMomentProto.format=function(){if(this._fullCalendar&&arguments[0]){return formatDate(this,arguments[0])}if(this._ambigTime){return oldMomentFormat(this,"YYYY-MM-DD")}if(this._ambigZone){return oldMomentFormat(this,"YYYY-MM-DD[T]HH:mm:ss")}return oldMomentProto.format.apply(this,arguments)};newMomentProto.toISOString=function(){if(this._ambigTime){return oldMomentFormat(this,"YYYY-MM-DD")}if(this._ambigZone){return oldMomentFormat(this,"YYYY-MM-DD[T]HH:mm:ss")}return oldMomentProto.toISOString.apply(this,arguments)};newMomentProto.isWithin=function(start,end){var a=commonlyAmbiguate([this,start,end]);return a[0]>=a[1]&&a[0]<a[2]};newMomentProto.isSame=function(input,units){var a;if(!this._fullCalendar){return oldMomentProto.isSame.apply(this,arguments)}if(units){a=commonlyAmbiguate([this,input],true);return oldMomentProto.isSame.call(a[0],a[1],units)}else{input=FC.moment.parseZone(input);return oldMomentProto.isSame.call(this,input)&&Boolean(this._ambigTime)===Boolean(input._ambigTime)&&Boolean(this._ambigZone)===Boolean(input._ambigZone)}};$.each(["isBefore","isAfter"],function(i,methodName){newMomentProto[methodName]=function(input,units){var a;if(!this._fullCalendar){return oldMomentProto[methodName].apply(this,arguments)}a=commonlyAmbiguate([this,input]);return oldMomentProto[methodName].call(a[0],a[1],units)}});function commonlyAmbiguate(inputs,preserveTime){var anyAmbigTime=false;var anyAmbigZone=false;var len=inputs.length;var moms=[];var i,mom;for(i=0;i<len;i++){mom=inputs[i];if(!moment.isMoment(mom)){mom=FC.moment.parseZone(mom)}anyAmbigTime=anyAmbigTime||mom._ambigTime;anyAmbigZone=anyAmbigZone||mom._ambigZone;moms.push(mom)}for(i=0;i<len;i++){mom=moms[i];if(!preserveTime&&anyAmbigTime&&!mom._ambigTime){moms[i]=mom.clone().stripTime()}else if(anyAmbigZone&&!mom._ambigZone){moms[i]=mom.clone().stripZone()}}return moms}function transferAmbigs(src,dest){if(src._ambigTime){dest._ambigTime=true}else if(dest._ambigTime){dest._ambigTime=false}if(src._ambigZone){dest._ambigZone=true}else if(dest._ambigZone){dest._ambigZone=false}}function setMomentValues(mom,a){mom.year(a[0]||0).month(a[1]||0).date(a[2]||0).hours(a[3]||0).minutes(a[4]||0).seconds(a[5]||0).milliseconds(a[6]||0)}allowValueOptimization="_d"in moment()&&"updateOffset"in moment;setUTCValues=allowValueOptimization?function(mom,a){mom._d.setTime(Date.UTC.apply(Date,a));moment.updateOffset(mom,false)}:setMomentValues;setLocalValues=allowValueOptimization?function(mom,a){mom._d.setTime(+new Date(a[0]||0,a[1]||0,a[2]||0,a[3]||0,a[4]||0,a[5]||0,a[6]||0));moment.updateOffset(mom,false)}:setMomentValues;function oldMomentFormat(mom,formatStr){return oldMomentProto.format.call(mom,formatStr)}function formatDate(date,formatStr){return formatDateWithChunks(date,getFormatStringChunks(formatStr))}function formatDateWithChunks(date,chunks){var s="";var i;for(i=0;i<chunks.length;i++){s+=formatDateWithChunk(date,chunks[i])}return s}var tokenOverrides={t:function(date){return oldMomentFormat(date,"a").charAt(0)},T:function(date){return oldMomentFormat(date,"A").charAt(0)}};function formatDateWithChunk(date,chunk){var token;var maybeStr;if(typeof chunk==="string"){return chunk}else if(token=chunk.token){if(tokenOverrides[token]){return tokenOverrides[token](date)}return oldMomentFormat(date,token)}else if(chunk.maybe){maybeStr=formatDateWithChunks(date,chunk.maybe);if(maybeStr.match(/[1-9]/)){return maybeStr}}return""}function formatRange(date1,date2,formatStr,separator,isRTL){var localeData;date1=FC.moment.parseZone(date1);date2=FC.moment.parseZone(date2);localeData=(date1.localeData||date1.lang).call(date1);formatStr=localeData.longDateFormat(formatStr)||formatStr;separator=separator||" - ";return formatRangeWithChunks(date1,date2,getFormatStringChunks(formatStr),separator,isRTL)}FC.formatRange=formatRange;function formatRangeWithChunks(date1,date2,chunks,separator,isRTL){var unzonedDate1=date1.clone().stripZone();var unzonedDate2=date2.clone().stripZone();var chunkStr;var leftI;var leftStr="";var rightI;var rightStr="";var middleI;var middleStr1="";var middleStr2="";var middleStr="";for(leftI=0;leftI<chunks.length;leftI++){chunkStr=formatSimilarChunk(date1,date2,unzonedDate1,unzonedDate2,chunks[leftI]);if(chunkStr===false){break}leftStr+=chunkStr}for(rightI=chunks.length-1;rightI>leftI;rightI--){chunkStr=formatSimilarChunk(date1,date2,unzonedDate1,unzonedDate2,chunks[rightI]);if(chunkStr===false){break}rightStr=chunkStr+rightStr}for(middleI=leftI;middleI<=rightI;middleI++){middleStr1+=formatDateWithChunk(date1,chunks[middleI]);middleStr2+=formatDateWithChunk(date2,chunks[middleI])}if(middleStr1||middleStr2){if(isRTL){middleStr=middleStr2+separator+middleStr1}else{middleStr=middleStr1+separator+middleStr2}}return leftStr+middleStr+rightStr}var similarUnitMap={Y:"year",M:"month",D:"day",d:"day",A:"second",a:"second",T:"second",t:"second",H:"second",h:"second",m:"second",s:"second"};function formatSimilarChunk(date1,date2,unzonedDate1,unzonedDate2,chunk){var token;var unit;if(typeof chunk==="string"){return chunk}else if(token=chunk.token){unit=similarUnitMap[token.charAt(0)];if(unit&&unzonedDate1.isSame(unzonedDate2,unit)){return oldMomentFormat(date1,token)}}return false}var formatStringChunkCache={};function getFormatStringChunks(formatStr){if(formatStr in formatStringChunkCache){return formatStringChunkCache[formatStr]}return formatStringChunkCache[formatStr]=chunkFormatString(formatStr)}function chunkFormatString(formatStr){var chunks=[];var chunker=/\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;var match;while(match=chunker.exec(formatStr)){if(match[1]){chunks.push(match[1])}else if(match[2]){chunks.push({maybe:chunkFormatString(match[2])})}else if(match[3]){chunks.push({token:match[3]})}else if(match[5]){chunks.push(match[5])}}return chunks}FC.Class=Class;function Class(){}Class.extend=function(){var len=arguments.length;var i;var members;for(i=0;i<len;i++){members=arguments[i];if(i<len-1){mixIntoClass(this,members)}}return extendClass(this,members||{})};Class.mixin=function(members){mixIntoClass(this,members)};function extendClass(superClass,members){var subClass;if(hasOwnProp(members,"constructor")){subClass=members.constructor}if(typeof subClass!=="function"){subClass=members.constructor=function(){superClass.apply(this,arguments)}}subClass.prototype=createObject(superClass.prototype);copyOwnProps(members,subClass.prototype);copyNativeMethods(members,subClass.prototype);copyOwnProps(superClass,subClass);return subClass}function mixIntoClass(theClass,members){copyOwnProps(members,theClass.prototype)}var EmitterMixin=FC.EmitterMixin={on:function(types,handler){var intercept=function(ev,extra){return handler.apply(extra.context||this,extra.args||[])};if(!handler.guid){handler.guid=$.guid++}intercept.guid=handler.guid;$(this).on(types,intercept);return this},off:function(types,handler){$(this).off(types,handler);return this},trigger:function(types){var args=Array.prototype.slice.call(arguments,1);$(this).triggerHandler(types,{args:args});return this},triggerWith:function(types,context,args){$(this).triggerHandler(types,{context:context,args:args});return this}};var ListenerMixin=FC.ListenerMixin=function(){var guid=0;var ListenerMixin={listenerId:null,listenTo:function(other,arg,callback){if(typeof arg==="object"){for(var eventName in arg){if(arg.hasOwnProperty(eventName)){this.listenTo(other,eventName,arg[eventName])}}}else if(typeof arg==="string"){other.on(arg+"."+this.getListenerNamespace(),$.proxy(callback,this))}},stopListeningTo:function(other,eventName){other.off((eventName||"")+"."+this.getListenerNamespace())},getListenerNamespace:function(){if(this.listenerId==null){this.listenerId=guid++}return"_listener"+this.listenerId}};return ListenerMixin}();var MouseIgnorerMixin={isIgnoringMouse:false,delayUnignoreMouse:null,initMouseIgnoring:function(delay){this.delayUnignoreMouse=debounce(proxy(this,"unignoreMouse"),delay||1e3)},tempIgnoreMouse:function(){this.isIgnoringMouse=true;this.delayUnignoreMouse()},unignoreMouse:function(){this.isIgnoringMouse=false}};var Popover=Class.extend(ListenerMixin,{isHidden:true,options:null,el:null,margin:10,constructor:function(options){this.options=options||{}},show:function(){if(this.isHidden){if(!this.el){this.render()}this.el.show();this.position();this.isHidden=false;this.trigger("show")}},hide:function(){if(!this.isHidden){this.el.hide();this.isHidden=true;this.trigger("hide")}},render:function(){var _this=this;var options=this.options;this.el=$('<div class="fc-popover"/>').addClass(options.className||"").css({top:0,left:0}).append(options.content).appendTo(options.parentEl);this.el.on("click",".fc-close",function(){_this.hide()});if(options.autoHide){this.listenTo($(document),"mousedown",this.documentMousedown)}},documentMousedown:function(ev){if(this.el&&!$(ev.target).closest(this.el).length){this.hide()}},removeElement:function(){this.hide();if(this.el){this.el.remove();this.el=null}this.stopListeningTo($(document),"mousedown")},position:function(){var options=this.options;var origin=this.el.offsetParent().offset();var width=this.el.outerWidth();var height=this.el.outerHeight();var windowEl=$(window);var viewportEl=getScrollParent(this.el);var viewportTop;var viewportLeft;var viewportOffset;var top;var left;top=options.top||0;if(options.left!==undefined){left=options.left}else if(options.right!==undefined){left=options.right-width}else{left=0}if(viewportEl.is(window)||viewportEl.is(document)){viewportEl=windowEl;viewportTop=0;viewportLeft=0}else{viewportOffset=viewportEl.offset();viewportTop=viewportOffset.top;viewportLeft=viewportOffset.left}viewportTop+=windowEl.scrollTop();viewportLeft+=windowEl.scrollLeft();if(options.viewportConstrain!==false){top=Math.min(top,viewportTop+viewportEl.outerHeight()-height-this.margin);top=Math.max(top,viewportTop+this.margin);left=Math.min(left,viewportLeft+viewportEl.outerWidth()-width-this.margin);left=Math.max(left,viewportLeft+this.margin)}this.el.css({top:top-origin.top,left:left-origin.left})},trigger:function(name){if(this.options[name]){this.options[name].apply(this,Array.prototype.slice.call(arguments,1))}}});var CoordCache=FC.CoordCache=Class.extend({els:null,forcedOffsetParentEl:null,origin:null,boundingRect:null,isHorizontal:false,isVertical:false,lefts:null,rights:null,tops:null,bottoms:null,constructor:function(options){this.els=$(options.els);this.isHorizontal=options.isHorizontal;this.isVertical=options.isVertical;this.forcedOffsetParentEl=options.offsetParent?$(options.offsetParent):null},build:function(){var offsetParentEl=this.forcedOffsetParentEl||this.els.eq(0).offsetParent();this.origin=offsetParentEl.offset();this.boundingRect=this.queryBoundingRect();if(this.isHorizontal){this.buildElHorizontals()}if(this.isVertical){this.buildElVerticals()}},clear:function(){this.origin=null;this.boundingRect=null;this.lefts=null;this.rights=null;this.tops=null;this.bottoms=null},ensureBuilt:function(){if(!this.origin){this.build()}},queryBoundingRect:function(){var scrollParentEl=getScrollParent(this.els.eq(0));if(!scrollParentEl.is(document)){return getClientRect(scrollParentEl)}},buildElHorizontals:function(){var lefts=[];var rights=[];this.els.each(function(i,node){var el=$(node);var left=el.offset().left;var width=el.outerWidth();lefts.push(left);rights.push(left+width)});this.lefts=lefts;this.rights=rights},buildElVerticals:function(){var tops=[];var bottoms=[];this.els.each(function(i,node){var el=$(node);var top=el.offset().top;var height=el.outerHeight();tops.push(top);bottoms.push(top+height)});this.tops=tops;this.bottoms=bottoms},getHorizontalIndex:function(leftOffset){this.ensureBuilt();var boundingRect=this.boundingRect;var lefts=this.lefts;var rights=this.rights;var len=lefts.length;var i;if(!boundingRect||leftOffset>=boundingRect.left&&leftOffset<boundingRect.right){for(i=0;i<len;i++){if(leftOffset>=lefts[i]&&leftOffset<rights[i]){return i}}}},getVerticalIndex:function(topOffset){this.ensureBuilt();var boundingRect=this.boundingRect;var tops=this.tops;var bottoms=this.bottoms;var len=tops.length;var i;if(!boundingRect||topOffset>=boundingRect.top&&topOffset<boundingRect.bottom){for(i=0;i<len;i++){if(topOffset>=tops[i]&&topOffset<bottoms[i]){return i}}}},getLeftOffset:function(leftIndex){this.ensureBuilt();return this.lefts[leftIndex]},getLeftPosition:function(leftIndex){this.ensureBuilt();return this.lefts[leftIndex]-this.origin.left},getRightOffset:function(leftIndex){this.ensureBuilt();return this.rights[leftIndex]},getRightPosition:function(leftIndex){this.ensureBuilt();return this.rights[leftIndex]-this.origin.left},getWidth:function(leftIndex){this.ensureBuilt();return this.rights[leftIndex]-this.lefts[leftIndex]},getTopOffset:function(topIndex){this.ensureBuilt();return this.tops[topIndex]},getTopPosition:function(topIndex){this.ensureBuilt();return this.tops[topIndex]-this.origin.top},getBottomOffset:function(topIndex){this.ensureBuilt();return this.bottoms[topIndex]},getBottomPosition:function(topIndex){this.ensureBuilt();return this.bottoms[topIndex]-this.origin.top},getHeight:function(topIndex){this.ensureBuilt();return this.bottoms[topIndex]-this.tops[topIndex]}});var DragListener=FC.DragListener=Class.extend(ListenerMixin,MouseIgnorerMixin,{options:null,subjectEl:null,subjectHref:null,originX:null,originY:null,scrollEl:null,isInteracting:false,isDistanceSurpassed:false,isDelayEnded:false,isDragging:false,isTouch:false,delay:null,delayTimeoutId:null,minDistance:null,handleTouchScrollProxy:null,constructor:function(options){this.options=options||{};this.handleTouchScrollProxy=proxy(this,"handleTouchScroll");this.initMouseIgnoring(500)},startInteraction:function(ev,extraOptions){var isTouch=getEvIsTouch(ev);if(ev.type==="mousedown"){if(this.isIgnoringMouse){return}else if(!isPrimaryMouseButton(ev)){return}else{ev.preventDefault()}}if(!this.isInteracting){extraOptions=extraOptions||{};this.delay=firstDefined(extraOptions.delay,this.options.delay,0);this.minDistance=firstDefined(extraOptions.distance,this.options.distance,0);this.subjectEl=this.options.subjectEl;this.isInteracting=true;this.isTouch=isTouch;this.isDelayEnded=false;this.isDistanceSurpassed=false;this.originX=getEvX(ev);this.originY=getEvY(ev);this.scrollEl=getScrollParent($(ev.target));this.bindHandlers();this.initAutoScroll();this.handleInteractionStart(ev);this.startDelay(ev);if(!this.minDistance){this.handleDistanceSurpassed(ev)}}},handleInteractionStart:function(ev){this.trigger("interactionStart",ev)},endInteraction:function(ev,isCancelled){if(this.isInteracting){this.endDrag(ev);if(this.delayTimeoutId){clearTimeout(this.delayTimeoutId);this.delayTimeoutId=null}this.destroyAutoScroll();this.unbindHandlers();this.isInteracting=false;this.handleInteractionEnd(ev,isCancelled);if(this.isTouch){this.tempIgnoreMouse()}}},handleInteractionEnd:function(ev,isCancelled){this.trigger("interactionEnd",ev,isCancelled||false)},bindHandlers:function(){var _this=this;var touchStartIgnores=1;if(this.isTouch){this.listenTo($(document),{touchmove:this.handleTouchMove,touchend:this.endInteraction,touchcancel:this.endInteraction,touchstart:function(ev){if(touchStartIgnores){touchStartIgnores--}else{_this.endInteraction(ev,true)}}});if(!bindAnyScroll(this.handleTouchScrollProxy)&&this.scrollEl){this.listenTo(this.scrollEl,"scroll",this.handleTouchScroll)}}else{this.listenTo($(document),{mousemove:this.handleMouseMove,mouseup:this.endInteraction})}this.listenTo($(document),{selectstart:preventDefault,contextmenu:preventDefault})},unbindHandlers:function(){this.stopListeningTo($(document));unbindAnyScroll(this.handleTouchScrollProxy);if(this.scrollEl){this.stopListeningTo(this.scrollEl,"scroll")}},startDrag:function(ev,extraOptions){this.startInteraction(ev,extraOptions);if(!this.isDragging){this.isDragging=true;this.handleDragStart(ev)}},handleDragStart:function(ev){this.trigger("dragStart",ev);this.initHrefHack()},handleMove:function(ev){var dx=getEvX(ev)-this.originX;var dy=getEvY(ev)-this.originY;var minDistance=this.minDistance;var distanceSq;if(!this.isDistanceSurpassed){distanceSq=dx*dx+dy*dy;if(distanceSq>=minDistance*minDistance){this.handleDistanceSurpassed(ev)}}if(this.isDragging){this.handleDrag(dx,dy,ev)}},handleDrag:function(dx,dy,ev){this.trigger("drag",dx,dy,ev);this.updateAutoScroll(ev)},endDrag:function(ev){if(this.isDragging){this.isDragging=false;this.handleDragEnd(ev)}},handleDragEnd:function(ev){this.trigger("dragEnd",ev);this.destroyHrefHack()},startDelay:function(initialEv){var _this=this;if(this.delay){this.delayTimeoutId=setTimeout(function(){_this.handleDelayEnd(initialEv)},this.delay)}else{this.handleDelayEnd(initialEv)}},handleDelayEnd:function(initialEv){this.isDelayEnded=true;if(this.isDistanceSurpassed){this.startDrag(initialEv)}},handleDistanceSurpassed:function(ev){this.isDistanceSurpassed=true;if(this.isDelayEnded){this.startDrag(ev)}},handleTouchMove:function(ev){if(this.isDragging){ev.preventDefault()}this.handleMove(ev)},handleMouseMove:function(ev){this.handleMove(ev)},handleTouchScroll:function(ev){if(!this.isDragging){this.endInteraction(ev,true)}},initHrefHack:function(){var subjectEl=this.subjectEl;if(this.subjectHref=subjectEl?subjectEl.attr("href"):null){subjectEl.removeAttr("href")}},destroyHrefHack:function(){var subjectEl=this.subjectEl;var subjectHref=this.subjectHref;setTimeout(function(){if(subjectHref){subjectEl.attr("href",subjectHref)}},0)},trigger:function(name){if(this.options[name]){this.options[name].apply(this,Array.prototype.slice.call(arguments,1))}if(this["_"+name]){this["_"+name].apply(this,Array.prototype.slice.call(arguments,1))}}});DragListener.mixin({isAutoScroll:false,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,initAutoScroll:function(){var scrollEl=this.scrollEl;this.isAutoScroll=this.options.scroll&&scrollEl&&!scrollEl.is(window)&&!scrollEl.is(document);if(this.isAutoScroll){this.listenTo(scrollEl,"scroll",debounce(this.handleDebouncedScroll,100))}},destroyAutoScroll:function(){this.endAutoScroll();if(this.isAutoScroll){this.stopListeningTo(this.scrollEl,"scroll")}},computeScrollBounds:function(){if(this.isAutoScroll){this.scrollBounds=getOuterRect(this.scrollEl)}},updateAutoScroll:function(ev){var sensitivity=this.scrollSensitivity;var bounds=this.scrollBounds;var topCloseness,bottomCloseness;var leftCloseness,rightCloseness;var topVel=0;var leftVel=0;if(bounds){topCloseness=(sensitivity-(getEvY(ev)-bounds.top))/sensitivity;bottomCloseness=(sensitivity-(bounds.bottom-getEvY(ev)))/sensitivity;leftCloseness=(sensitivity-(getEvX(ev)-bounds.left))/sensitivity;rightCloseness=(sensitivity-(bounds.right-getEvX(ev)))/sensitivity;if(topCloseness>=0&&topCloseness<=1){topVel=topCloseness*this.scrollSpeed*-1}else if(bottomCloseness>=0&&bottomCloseness<=1){topVel=bottomCloseness*this.scrollSpeed}if(leftCloseness>=0&&leftCloseness<=1){leftVel=leftCloseness*this.scrollSpeed*-1}else if(rightCloseness>=0&&rightCloseness<=1){leftVel=rightCloseness*this.scrollSpeed}}this.setScrollVel(topVel,leftVel)},setScrollVel:function(topVel,leftVel){this.scrollTopVel=topVel;this.scrollLeftVel=leftVel;this.constrainScrollVel();if((this.scrollTopVel||this.scrollLeftVel)&&!this.scrollIntervalId){this.scrollIntervalId=setInterval(proxy(this,"scrollIntervalFunc"),this.scrollIntervalMs)}},constrainScrollVel:function(){var el=this.scrollEl;if(this.scrollTopVel<0){if(el.scrollTop()<=0){this.scrollTopVel=0}}else if(this.scrollTopVel>0){if(el.scrollTop()+el[0].clientHeight>=el[0].scrollHeight){this.scrollTopVel=0}}if(this.scrollLeftVel<0){if(el.scrollLeft()<=0){this.scrollLeftVel=0}}else if(this.scrollLeftVel>0){if(el.scrollLeft()+el[0].clientWidth>=el[0].scrollWidth){this.scrollLeftVel=0}}},scrollIntervalFunc:function(){var el=this.scrollEl;var frac=this.scrollIntervalMs/1e3;if(this.scrollTopVel){el.scrollTop(el.scrollTop()+this.scrollTopVel*frac)}if(this.scrollLeftVel){el.scrollLeft(el.scrollLeft()+this.scrollLeftVel*frac)}this.constrainScrollVel();if(!this.scrollTopVel&&!this.scrollLeftVel){this.endAutoScroll()}},endAutoScroll:function(){if(this.scrollIntervalId){clearInterval(this.scrollIntervalId);this.scrollIntervalId=null;this.handleScrollEnd()}},handleDebouncedScroll:function(){if(!this.scrollIntervalId){this.handleScrollEnd()}},handleScrollEnd:function(){}});var HitDragListener=DragListener.extend({component:null,origHit:null,hit:null,coordAdjust:null,constructor:function(component,options){DragListener.call(this,options);this.component=component},handleInteractionStart:function(ev){var subjectEl=this.subjectEl;var subjectRect;var origPoint;var point;this.computeCoords();if(ev){origPoint={left:getEvX(ev),top:getEvY(ev)};point=origPoint;if(subjectEl){subjectRect=getOuterRect(subjectEl);point=constrainPoint(point,subjectRect)}this.origHit=this.queryHit(point.left,point.top);if(subjectEl&&this.options.subjectCenter){if(this.origHit){subjectRect=intersectRects(this.origHit,subjectRect)||subjectRect}point=getRectCenter(subjectRect)}this.coordAdjust=diffPoints(point,origPoint)}else{this.origHit=null;this.coordAdjust=null}DragListener.prototype.handleInteractionStart.apply(this,arguments)},computeCoords:function(){this.component.prepareHits();this.computeScrollBounds()},handleDragStart:function(ev){var hit;DragListener.prototype.handleDragStart.apply(this,arguments);hit=this.queryHit(getEvX(ev),getEvY(ev));if(hit){this.handleHitOver(hit)}},handleDrag:function(dx,dy,ev){var hit;DragListener.prototype.handleDrag.apply(this,arguments);hit=this.queryHit(getEvX(ev),getEvY(ev));if(!isHitsEqual(hit,this.hit)){if(this.hit){this.handleHitOut()}if(hit){this.handleHitOver(hit)}}},handleDragEnd:function(){this.handleHitDone();DragListener.prototype.handleDragEnd.apply(this,arguments)},handleHitOver:function(hit){var isOrig=isHitsEqual(hit,this.origHit);this.hit=hit;this.trigger("hitOver",this.hit,isOrig,this.origHit)},handleHitOut:function(){if(this.hit){this.trigger("hitOut",this.hit);this.handleHitDone();this.hit=null}},handleHitDone:function(){if(this.hit){this.trigger("hitDone",this.hit)}},handleInteractionEnd:function(){DragListener.prototype.handleInteractionEnd.apply(this,arguments);this.origHit=null;this.hit=null;this.component.releaseHits()},handleScrollEnd:function(){DragListener.prototype.handleScrollEnd.apply(this,arguments);this.computeCoords()},queryHit:function(left,top){if(this.coordAdjust){left+=this.coordAdjust.left;top+=this.coordAdjust.top}return this.component.queryHit(left,top)}});function isHitsEqual(hit0,hit1){if(!hit0&&!hit1){return true}if(hit0&&hit1){return hit0.component===hit1.component&&isHitPropsWithin(hit0,hit1)&&isHitPropsWithin(hit1,hit0)}return false}function isHitPropsWithin(subHit,superHit){for(var propName in subHit){if(!/^(component|left|right|top|bottom)$/.test(propName)){if(subHit[propName]!==superHit[propName]){return false}}}return true}var MouseFollower=Class.extend(ListenerMixin,{options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,y0:null,x0:null,topDelta:null,leftDelta:null,isFollowing:false,isHidden:false,isAnimating:false,constructor:function(sourceEl,options){this.options=options=options||{};this.sourceEl=sourceEl;this.parentEl=options.parentEl?$(options.parentEl):sourceEl.parent()},start:function(ev){if(!this.isFollowing){this.isFollowing=true;this.y0=getEvY(ev);this.x0=getEvX(ev);this.topDelta=0;this.leftDelta=0;if(!this.isHidden){this.updatePosition()}if(getEvIsTouch(ev)){this.listenTo($(document),"touchmove",this.handleMove)}else{this.listenTo($(document),"mousemove",this.handleMove)}}},stop:function(shouldRevert,callback){var _this=this;var revertDuration=this.options.revertDuration;function complete(){this.isAnimating=false;_this.removeElement();this.top0=this.left0=null;if(callback){callback()}}if(this.isFollowing&&!this.isAnimating){this.isFollowing=false;this.stopListeningTo($(document));if(shouldRevert&&revertDuration&&!this.isHidden){this.isAnimating=true;this.el.animate({top:this.top0,left:this.left0},{duration:revertDuration,complete:complete})}else{complete()}}},getEl:function(){var el=this.el;if(!el){this.sourceEl.width();el=this.el=this.sourceEl.clone().addClass(this.options.additionalClass||"").css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex});el.addClass("fc-unselectable");el.appendTo(this.parentEl)}return el},removeElement:function(){if(this.el){this.el.remove();this.el=null}},updatePosition:function(){var sourceOffset;var origin;this.getEl();if(this.top0===null){this.sourceEl.width();sourceOffset=this.sourceEl.offset();origin=this.el.offsetParent().offset();this.top0=sourceOffset.top-origin.top;this.left0=sourceOffset.left-origin.left}this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},handleMove:function(ev){this.topDelta=getEvY(ev)-this.y0;this.leftDelta=getEvX(ev)-this.x0;if(!this.isHidden){this.updatePosition()}},hide:function(){if(!this.isHidden){this.isHidden=true;if(this.el){this.el.hide()}}},show:function(){if(this.isHidden){this.isHidden=false;this.updatePosition();this.getEl().show()}}});var Grid=FC.Grid=Class.extend(ListenerMixin,MouseIgnorerMixin,{view:null,isRTL:null,start:null,end:null,el:null,elsByFill:null,eventTimeFormat:null,displayEventTime:null,displayEventEnd:null,minResizeDuration:null,largeUnit:null,dayDragListener:null,segDragListener:null,segResizeListener:null,externalDragListener:null,constructor:function(view){this.view=view;this.isRTL=view.opt("isRTL");this.elsByFill={};this.dayDragListener=this.buildDayDragListener();this.initMouseIgnoring()},computeEventTimeFormat:function(){return this.view.opt("smallTimeFormat")},computeDisplayEventTime:function(){return true},computeDisplayEventEnd:function(){return true},setRange:function(range){this.start=range.start.clone();this.end=range.end.clone();this.rangeUpdated();this.processRangeOptions()},rangeUpdated:function(){},processRangeOptions:function(){var view=this.view;var displayEventTime;var displayEventEnd;this.eventTimeFormat=view.opt("eventTimeFormat")||view.opt("timeFormat")||this.computeEventTimeFormat();displayEventTime=view.opt("displayEventTime");if(displayEventTime==null){displayEventTime=this.computeDisplayEventTime()}displayEventEnd=view.opt("displayEventEnd");if(displayEventEnd==null){displayEventEnd=this.computeDisplayEventEnd()}this.displayEventTime=displayEventTime;this.displayEventEnd=displayEventEnd},spanToSegs:function(span){},diffDates:function(a,b){if(this.largeUnit){return diffByUnit(a,b,this.largeUnit)}else{return diffDayTime(a,b)}},prepareHits:function(){},releaseHits:function(){},queryHit:function(leftOffset,topOffset){},getHitSpan:function(hit){},getHitEl:function(hit){},setElement:function(el){this.el=el;preventSelection(el);this.bindDayHandler("touchstart",this.dayTouchStart);this.bindDayHandler("mousedown",this.dayMousedown);this.bindSegHandlers();this.bindGlobalHandlers()},bindDayHandler:function(name,handler){var _this=this;this.el.on(name,function(ev){if(!$(ev.target).is(".fc-event-container *, .fc-more")&&!$(ev.target).closest(".fc-popover").length){return handler.call(_this,ev)}})},removeElement:function(){this.unbindGlobalHandlers();this.clearDragListeners();this.el.remove()},renderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},bindGlobalHandlers:function(){this.listenTo($(document),{dragstart:this.externalDragStart,sortstart:this.externalDragStart})},unbindGlobalHandlers:function(){this.stopListeningTo($(document))},dayMousedown:function(ev){if(!this.isIgnoringMouse){this.dayDragListener.startInteraction(ev,{})}},dayTouchStart:function(ev){var view=this.view;if(view.isSelected||view.selectedEvent){this.tempIgnoreMouse()}this.dayDragListener.startInteraction(ev,{delay:this.view.opt("longPressDelay")})},buildDayDragListener:function(){var _this=this;var view=this.view;var isSelectable=view.opt("selectable");var dayClickHit;var selectionSpan;var dragListener=new HitDragListener(this,{scroll:view.opt("dragScroll"),interactionStart:function(){dayClickHit=dragListener.origHit},dragStart:function(){view.unselect()},hitOver:function(hit,isOrig,origHit){if(origHit){if(!isOrig){dayClickHit=null}if(isSelectable){selectionSpan=_this.computeSelection(_this.getHitSpan(origHit),_this.getHitSpan(hit));if(selectionSpan){_this.renderSelection(selectionSpan)}else if(selectionSpan===false){disableCursor()}}}},hitOut:function(){dayClickHit=null;selectionSpan=null;_this.unrenderSelection();enableCursor()},interactionEnd:function(ev,isCancelled){if(!isCancelled){if(dayClickHit&&!_this.isIgnoringMouse){view.triggerDayClick(_this.getHitSpan(dayClickHit),_this.getHitEl(dayClickHit),ev)}if(selectionSpan){view.reportSelection(selectionSpan,ev)}enableCursor()}}});return dragListener},clearDragListeners:function(){this.dayDragListener.endInteraction();if(this.segDragListener){this.segDragListener.endInteraction()}if(this.segResizeListener){this.segResizeListener.endInteraction()}if(this.externalDragListener){this.externalDragListener.endInteraction()}},renderEventLocationHelper:function(eventLocation,sourceSeg){var fakeEvent=this.fabricateHelperEvent(eventLocation,sourceSeg);return this.renderHelper(fakeEvent,sourceSeg)},fabricateHelperEvent:function(eventLocation,sourceSeg){var fakeEvent=sourceSeg?createObject(sourceSeg.event):{};fakeEvent.start=eventLocation.start.clone();fakeEvent.end=eventLocation.end?eventLocation.end.clone():null;fakeEvent.allDay=null;this.view.calendar.normalizeEventDates(fakeEvent);fakeEvent.className=(fakeEvent.className||[]).concat("fc-helper");if(!sourceSeg){fakeEvent.editable=false}return fakeEvent},renderHelper:function(eventLocation,sourceSeg){},unrenderHelper:function(){},renderSelection:function(span){this.renderHighlight(span)},unrenderSelection:function(){this.unrenderHighlight()},computeSelection:function(span0,span1){var span=this.computeSelectionSpan(span0,span1);if(span&&!this.view.calendar.isSelectionSpanAllowed(span)){return false}return span},computeSelectionSpan:function(span0,span1){var dates=[span0.start,span0.end,span1.start,span1.end];dates.sort(compareNumbers);return{start:dates[0].clone(),end:dates[3].clone()}},renderHighlight:function(span){this.renderFill("highlight",this.spanToSegs(span))},unrenderHighlight:function(){this.unrenderFill("highlight")},highlightSegClasses:function(){return["fc-highlight"]},renderBusinessHours:function(){},unrenderBusinessHours:function(){},getNowIndicatorUnit:function(){},renderNowIndicator:function(date){},unrenderNowIndicator:function(){},renderFill:function(type,segs){},unrenderFill:function(type){var el=this.elsByFill[type];if(el){el.remove();delete this.elsByFill[type]}},renderFillSegEls:function(type,segs){var _this=this;var segElMethod=this[type+"SegEl"];var html="";var renderedSegs=[];var i;if(segs.length){for(i=0;i<segs.length;i++){html+=this.fillSegHtml(type,segs[i])}$(html).each(function(i,node){var seg=segs[i];var el=$(node);if(segElMethod){el=segElMethod.call(_this,seg,el)}if(el){el=$(el);if(el.is(_this.fillSegTag)){seg.el=el;renderedSegs.push(seg)}}})}return renderedSegs},fillSegTag:"div",fillSegHtml:function(type,seg){var classesMethod=this[type+"SegClasses"];var cssMethod=this[type+"SegCss"];var classes=classesMethod?classesMethod.call(this,seg):[];var css=cssToStr(cssMethod?cssMethod.call(this,seg):{});return"<"+this.fillSegTag+(classes.length?' class="'+classes.join(" ")+'"':"")+(css?' style="'+css+'"':"")+" />"},getDayClasses:function(date){var view=this.view;var today=view.calendar.getNow();var classes=["fc-"+dayIDs[date.day()]];if(view.intervalDuration.as("months")==1&&date.month()!=view.intervalStart.month()){classes.push("fc-other-month")}if(date.isSame(today,"day")){classes.push("fc-today",view.highlightStateClass)}else if(date<today){classes.push("fc-past")}else{classes.push("fc-future")}return classes}});Grid.mixin({mousedOverSeg:null,isDraggingSeg:false,isResizingSeg:false,isDraggingExternal:false,segs:null,renderEvents:function(events){var bgEvents=[];var fgEvents=[];var i;for(i=0;i<events.length;i++){(isBgEvent(events[i])?bgEvents:fgEvents).push(events[i])}this.segs=[].concat(this.renderBgEvents(bgEvents),this.renderFgEvents(fgEvents))},renderBgEvents:function(events){var segs=this.eventsToSegs(events);return this.renderBgSegs(segs)||segs},renderFgEvents:function(events){var segs=this.eventsToSegs(events);return this.renderFgSegs(segs)||segs},unrenderEvents:function(){this.handleSegMouseout();this.clearDragListeners();this.unrenderFgSegs();this.unrenderBgSegs();this.segs=null},getEventSegs:function(){return this.segs||[]},renderFgSegs:function(segs){},unrenderFgSegs:function(){},renderFgSegEls:function(segs,disableResizing){var view=this.view;var html="";var renderedSegs=[];var i;if(segs.length){for(i=0;i<segs.length;i++){html+=this.fgSegHtml(segs[i],disableResizing)}$(html).each(function(i,node){var seg=segs[i];var el=view.resolveEventEl(seg.event,$(node));if(el){el.data("fc-seg",seg);seg.el=el;renderedSegs.push(seg)}})}return renderedSegs},fgSegHtml:function(seg,disableResizing){},renderBgSegs:function(segs){return this.renderFill("bgEvent",segs)},unrenderBgSegs:function(){this.unrenderFill("bgEvent")},bgEventSegEl:function(seg,el){return this.view.resolveEventEl(seg.event,el)},bgEventSegClasses:function(seg){var event=seg.event;var source=event.source||{};return["fc-bgevent"].concat(event.className,source.className||[])},bgEventSegCss:function(seg){return{"background-color":this.getSegSkinCss(seg)["background-color"]}},businessHoursSegClasses:function(seg){return["fc-nonbusiness","fc-bgevent"]},bindSegHandlers:function(){this.bindSegHandler("touchstart",this.handleSegTouchStart);this.bindSegHandler("touchend",this.handleSegTouchEnd);this.bindSegHandler("mouseenter",this.handleSegMouseover);this.bindSegHandler("mouseleave",this.handleSegMouseout);this.bindSegHandler("mousedown",this.handleSegMousedown);this.bindSegHandler("click",this.handleSegClick)},bindSegHandler:function(name,handler){var _this=this;this.el.on(name,".fc-event-container > *",function(ev){var seg=$(this).data("fc-seg");if(seg&&!_this.isDraggingSeg&&!_this.isResizingSeg){return handler.call(_this,seg,ev)}})},handleSegClick:function(seg,ev){return this.view.trigger("eventClick",seg.el[0],seg.event,ev)},handleSegMouseover:function(seg,ev){if(!this.isIgnoringMouse&&!this.mousedOverSeg){this.mousedOverSeg=seg;seg.el.addClass("fc-allow-mouse-resize");this.view.trigger("eventMouseover",seg.el[0],seg.event,ev)}},handleSegMouseout:function(seg,ev){ev=ev||{};if(this.mousedOverSeg){seg=seg||this.mousedOverSeg;this.mousedOverSeg=null;seg.el.removeClass("fc-allow-mouse-resize");this.view.trigger("eventMouseout",seg.el[0],seg.event,ev)}},handleSegMousedown:function(seg,ev){var isResizing=this.startSegResize(seg,ev,{distance:5});if(!isResizing&&this.view.isEventDraggable(seg.event)){this.buildSegDragListener(seg).startInteraction(ev,{distance:5})}},handleSegTouchStart:function(seg,ev){var view=this.view;var event=seg.event;var isSelected=view.isEventSelected(event);var isDraggable=view.isEventDraggable(event);var isResizable=view.isEventResizable(event);var isResizing=false;var dragListener;if(isSelected&&isResizable){isResizing=this.startSegResize(seg,ev)}if(!isResizing&&(isDraggable||isResizable)){dragListener=isDraggable?this.buildSegDragListener(seg):this.buildSegSelectListener(seg);dragListener.startInteraction(ev,{delay:isSelected?0:this.view.opt("longPressDelay")})}this.tempIgnoreMouse()},handleSegTouchEnd:function(seg,ev){this.tempIgnoreMouse()},startSegResize:function(seg,ev,dragOptions){if($(ev.target).is(".fc-resizer")){this.buildSegResizeListener(seg,$(ev.target).is(".fc-start-resizer")).startInteraction(ev,dragOptions);return true}return false},buildSegDragListener:function(seg){var _this=this;var view=this.view;var calendar=view.calendar;var el=seg.el;var event=seg.event;var isDragging;var mouseFollower;var dropLocation;if(this.segDragListener){return this.segDragListener}var dragListener=this.segDragListener=new HitDragListener(view,{scroll:view.opt("dragScroll"),subjectEl:el,subjectCenter:true,interactionStart:function(ev){isDragging=false;mouseFollower=new MouseFollower(seg.el,{additionalClass:"fc-dragging",parentEl:view.el,opacity:dragListener.isTouch?null:view.opt("dragOpacity"),revertDuration:view.opt("dragRevertDuration"),zIndex:2});mouseFollower.hide();mouseFollower.start(ev)},dragStart:function(ev){if(dragListener.isTouch&&!view.isEventSelected(event)){view.selectEvent(event)}isDragging=true;_this.handleSegMouseout(seg,ev);_this.segDragStart(seg,ev);view.hideEvent(event)},hitOver:function(hit,isOrig,origHit){var dragHelperEls;if(seg.hit){origHit=seg.hit}dropLocation=_this.computeEventDrop(origHit.component.getHitSpan(origHit),hit.component.getHitSpan(hit),event);if(dropLocation&&!calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation),event)){disableCursor();dropLocation=null}if(dropLocation&&(dragHelperEls=view.renderDrag(dropLocation,seg))){dragHelperEls.addClass("fc-dragging");if(!dragListener.isTouch){_this.applyDragOpacity(dragHelperEls)}mouseFollower.hide()}else{mouseFollower.show()}if(isOrig){dropLocation=null}},hitOut:function(){view.unrenderDrag();mouseFollower.show();dropLocation=null},hitDone:function(){enableCursor()},interactionEnd:function(ev){mouseFollower.stop(!dropLocation,function(){if(isDragging){view.unrenderDrag();view.showEvent(event);_this.segDragStop(seg,ev)}if(dropLocation){view.reportEventDrop(event,dropLocation,this.largeUnit,el,ev)}});_this.segDragListener=null}});return dragListener},buildSegSelectListener:function(seg){var _this=this;var view=this.view;var event=seg.event;if(this.segDragListener){return this.segDragListener}var dragListener=this.segDragListener=new DragListener({dragStart:function(ev){if(dragListener.isTouch&&!view.isEventSelected(event)){view.selectEvent(event)}},interactionEnd:function(ev){_this.segDragListener=null}});return dragListener},segDragStart:function(seg,ev){this.isDraggingSeg=true;this.view.trigger("eventDragStart",seg.el[0],seg.event,ev,{})},segDragStop:function(seg,ev){this.isDraggingSeg=false;this.view.trigger("eventDragStop",seg.el[0],seg.event,ev,{})},computeEventDrop:function(startSpan,endSpan,event){var calendar=this.view.calendar;var dragStart=startSpan.start;var dragEnd=endSpan.start;var delta;var dropLocation;if(dragStart.hasTime()===dragEnd.hasTime()){delta=this.diffDates(dragEnd,dragStart);if(event.allDay&&durationHasTime(delta)){dropLocation={start:event.start.clone(),end:calendar.getEventEnd(event),allDay:false};calendar.normalizeEventTimes(dropLocation)}else{dropLocation={start:event.start.clone(),end:event.end?event.end.clone():null,allDay:event.allDay}}dropLocation.start.add(delta);if(dropLocation.end){dropLocation.end.add(delta)}}else{dropLocation={start:dragEnd.clone(),end:null,allDay:!dragEnd.hasTime()}}return dropLocation},applyDragOpacity:function(els){var opacity=this.view.opt("dragOpacity");if(opacity!=null){els.each(function(i,node){node.style.opacity=opacity})}},externalDragStart:function(ev,ui){var view=this.view;var el;var accept;if(view.opt("droppable")){el=$((ui?ui.item:null)||ev.target);accept=view.opt("dropAccept");if($.isFunction(accept)?accept.call(el[0],el):el.is(accept)){if(!this.isDraggingExternal){this.listenToExternalDrag(el,ev,ui)}}}},listenToExternalDrag:function(el,ev,ui){var _this=this;var calendar=this.view.calendar;var meta=getDraggedElMeta(el);var dropLocation;var dragListener=_this.externalDragListener=new HitDragListener(this,{interactionStart:function(){_this.isDraggingExternal=true},hitOver:function(hit){dropLocation=_this.computeExternalDrop(hit.component.getHitSpan(hit),meta);if(dropLocation&&!calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation),dropLocation,meta.eventProps)){disableCursor();dropLocation=null}if(dropLocation){_this.renderDrag(dropLocation)}},hitOut:function(){dropLocation=null},hitDone:function(){enableCursor();_this.unrenderDrag()},interactionEnd:function(ev){if(dropLocation){_this.view.reportExternalDrop(meta,dropLocation,el,ev,ui)}_this.isDraggingExternal=false;_this.externalDragListener=null}});dragListener.startDrag(ev)},computeExternalDrop:function(span,meta){var calendar=this.view.calendar;var dropLocation={start:calendar.applyTimezone(span.start),end:null};if(meta.startTime&&!dropLocation.start.hasTime()){dropLocation.start.time(meta.startTime)}if(meta.duration){dropLocation.end=dropLocation.start.clone().add(meta.duration)}return dropLocation},renderDrag:function(dropLocation,seg){},unrenderDrag:function(){},buildSegResizeListener:function(seg,isStart){var _this=this;var view=this.view;var calendar=view.calendar;var el=seg.el;var event=seg.event;var eventEnd=calendar.getEventEnd(event);var isDragging;var resizeLocation;var dragListener=this.segResizeListener=new HitDragListener(this,{scroll:view.opt("dragScroll"),subjectEl:el,interactionStart:function(){isDragging=false},dragStart:function(ev){isDragging=true;_this.handleSegMouseout(seg,ev);_this.segResizeStart(seg,ev)},hitOver:function(hit,isOrig,origHit){var origHitSpan=_this.getHitSpan(origHit);var hitSpan=_this.getHitSpan(hit);resizeLocation=isStart?_this.computeEventStartResize(origHitSpan,hitSpan,event):_this.computeEventEndResize(origHitSpan,hitSpan,event);if(resizeLocation){if(!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation),event)){disableCursor();resizeLocation=null}else if(resizeLocation.start.isSame(event.start)&&resizeLocation.end.isSame(eventEnd)){resizeLocation=null}}if(resizeLocation){view.hideEvent(event);_this.renderEventResize(resizeLocation,seg)}},hitOut:function(){resizeLocation=null},hitDone:function(){_this.unrenderEventResize();view.showEvent(event);enableCursor()},interactionEnd:function(ev){if(isDragging){_this.segResizeStop(seg,ev)}if(resizeLocation){view.reportEventResize(event,resizeLocation,this.largeUnit,el,ev)}_this.segResizeListener=null}});return dragListener},segResizeStart:function(seg,ev){this.isResizingSeg=true;this.view.trigger("eventResizeStart",seg.el[0],seg.event,ev,{})},segResizeStop:function(seg,ev){this.isResizingSeg=false;this.view.trigger("eventResizeStop",seg.el[0],seg.event,ev,{})},computeEventStartResize:function(startSpan,endSpan,event){return this.computeEventResize("start",startSpan,endSpan,event)},computeEventEndResize:function(startSpan,endSpan,event){return this.computeEventResize("end",startSpan,endSpan,event)},computeEventResize:function(type,startSpan,endSpan,event){var calendar=this.view.calendar;var delta=this.diffDates(endSpan[type],startSpan[type]);var resizeLocation;var defaultDuration;resizeLocation={start:event.start.clone(),end:calendar.getEventEnd(event),allDay:event.allDay};if(resizeLocation.allDay&&durationHasTime(delta)){resizeLocation.allDay=false;calendar.normalizeEventTimes(resizeLocation)}resizeLocation[type].add(delta);if(!resizeLocation.start.isBefore(resizeLocation.end)){defaultDuration=this.minResizeDuration||(event.allDay?calendar.defaultAllDayEventDuration:calendar.defaultTimedEventDuration);if(type=="start"){resizeLocation.start=resizeLocation.end.clone().subtract(defaultDuration)}else{resizeLocation.end=resizeLocation.start.clone().add(defaultDuration)}}return resizeLocation},renderEventResize:function(range,seg){},unrenderEventResize:function(){},getEventTimeText:function(range,formatStr,displayEnd){if(formatStr==null){formatStr=this.eventTimeFormat}if(displayEnd==null){displayEnd=this.displayEventEnd}if(this.displayEventTime&&range.start.hasTime()){if(displayEnd&&range.end){return this.view.formatRange(range,formatStr)}else{return range.start.format(formatStr)}}return""},getSegClasses:function(seg,isDraggable,isResizable){var view=this.view;var event=seg.event;var classes=["fc-event",seg.isStart?"fc-start":"fc-not-start",seg.isEnd?"fc-end":"fc-not-end"].concat(event.className,event.source?event.source.className:[]);if(isDraggable){classes.push("fc-draggable")}if(isResizable){classes.push("fc-resizable")}if(view.isEventSelected(event)){classes.push("fc-selected")}return classes},getSegSkinCss:function(seg){var event=seg.event;var view=this.view;var source=event.source||{};var eventColor=event.color;var sourceColor=source.color;var optionColor=view.opt("eventColor");return{"background-color":event.backgroundColor||eventColor||source.backgroundColor||sourceColor||view.opt("eventBackgroundColor")||optionColor,"border-color":event.borderColor||eventColor||source.borderColor||sourceColor||view.opt("eventBorderColor")||optionColor,color:event.textColor||source.textColor||view.opt("eventTextColor")}},eventToSegs:function(event){return this.eventsToSegs([event])},eventToSpan:function(event){return this.eventToSpans(event)[0]},eventToSpans:function(event){var range=this.eventToRange(event);return this.eventRangeToSpans(range,event)},eventsToSegs:function(allEvents,segSliceFunc){var _this=this;var eventsById=groupEventsById(allEvents);var segs=[];$.each(eventsById,function(id,events){var ranges=[];var i;for(i=0;i<events.length;i++){ranges.push(_this.eventToRange(events[i]))}if(isInverseBgEvent(events[0])){ranges=_this.invertRanges(ranges);for(i=0;i<ranges.length;i++){segs.push.apply(segs,_this.eventRangeToSegs(ranges[i],events[0],segSliceFunc))}}else{for(i=0;i<ranges.length;i++){segs.push.apply(segs,_this.eventRangeToSegs(ranges[i],events[i],segSliceFunc))}}});return segs},eventToRange:function(event){return{start:event.start.clone().stripZone(),end:(event.end?event.end.clone():this.view.calendar.getDefaultEventEnd(event.allDay!=null?event.allDay:!event.start.hasTime(),event.start)).stripZone()}},eventRangeToSegs:function(range,event,segSliceFunc){var spans=this.eventRangeToSpans(range,event);var segs=[];var i;for(i=0;i<spans.length;i++){segs.push.apply(segs,this.eventSpanToSegs(spans[i],event,segSliceFunc))}return segs},eventRangeToSpans:function(range,event){return[$.extend({},range)]},eventSpanToSegs:function(span,event,segSliceFunc){var segs=segSliceFunc?segSliceFunc(span):this.spanToSegs(span);var i,seg;for(i=0;i<segs.length;i++){seg=segs[i];seg.event=event;seg.eventStartMS=+span.start;seg.eventDurationMS=span.end-span.start}return segs},invertRanges:function(ranges){var view=this.view;var viewStart=view.start.clone();var viewEnd=view.end.clone();var inverseRanges=[];var start=viewStart;var i,range;ranges.sort(compareRanges);for(i=0;i<ranges.length;i++){range=ranges[i];if(range.start>start){inverseRanges.push({start:start,end:range.start})}start=range.end}if(start<viewEnd){inverseRanges.push({start:start,end:viewEnd})}return inverseRanges},sortEventSegs:function(segs){segs.sort(proxy(this,"compareEventSegs"))},compareEventSegs:function(seg1,seg2){return seg1.eventStartMS-seg2.eventStartMS||seg2.eventDurationMS-seg1.eventDurationMS||seg2.event.allDay-seg1.event.allDay||compareByFieldSpecs(seg1.event,seg2.event,this.view.eventOrderSpecs)}});function isBgEvent(event){var rendering=getEventRendering(event);return rendering==="background"||rendering==="inverse-background"}FC.isBgEvent=isBgEvent;function isInverseBgEvent(event){return getEventRendering(event)==="inverse-background"}function getEventRendering(event){return firstDefined((event.source||{}).rendering,event.rendering)}function groupEventsById(events){var eventsById={};var i,event;for(i=0;i<events.length;i++){event=events[i];(eventsById[event._id]||(eventsById[event._id]=[])).push(event)}return eventsById}function compareRanges(range1,range2){return range1.start-range2.start}FC.dataAttrPrefix="";function getDraggedElMeta(el){var prefix=FC.dataAttrPrefix;var eventProps;var startTime;var duration;var stick;if(prefix){prefix+="-"}eventProps=el.data(prefix+"event")||null;if(eventProps){if(typeof eventProps==="object"){eventProps=$.extend({},eventProps)}else{eventProps={}}startTime=eventProps.start;if(startTime==null){startTime=eventProps.time}duration=eventProps.duration;stick=eventProps.stick;delete eventProps.start;delete eventProps.time;delete eventProps.duration;delete eventProps.stick}if(startTime==null){startTime=el.data(prefix+"start")}if(startTime==null){startTime=el.data(prefix+"time")}if(duration==null){duration=el.data(prefix+"duration")}if(stick==null){stick=el.data(prefix+"stick")}startTime=startTime!=null?moment.duration(startTime):null;duration=duration!=null?moment.duration(duration):null;stick=Boolean(stick);return{eventProps:eventProps,startTime:startTime,duration:duration,stick:stick}}var DayTableMixin=FC.DayTableMixin={breakOnWeeks:false,dayDates:null,dayIndices:null,daysPerRow:null,rowCnt:null,colCnt:null,colHeadFormat:null,updateDayTable:function(){var view=this.view;var date=this.start.clone();var dayIndex=-1;var dayIndices=[];var dayDates=[];var daysPerRow;var firstDay;var rowCnt;while(date.isBefore(this.end)){if(view.isHiddenDay(date)){dayIndices.push(dayIndex+.5)}else{dayIndex++;dayIndices.push(dayIndex);dayDates.push(date.clone())}date.add(1,"days")}if(this.breakOnWeeks){firstDay=dayDates[0].day();for(daysPerRow=1;daysPerRow<dayDates.length;daysPerRow++){if(dayDates[daysPerRow].day()==firstDay){break}}rowCnt=Math.ceil(dayDates.length/daysPerRow)}else{rowCnt=1;daysPerRow=dayDates.length}this.dayDates=dayDates;this.dayIndices=dayIndices;this.daysPerRow=daysPerRow;this.rowCnt=rowCnt;this.updateDayTableCols()},updateDayTableCols:function(){this.colCnt=this.computeColCnt();this.colHeadFormat=this.view.opt("columnFormat")||this.computeColHeadFormat()},computeColCnt:function(){return this.daysPerRow},getCellDate:function(row,col){return this.dayDates[this.getCellDayIndex(row,col)].clone()},getCellRange:function(row,col){var start=this.getCellDate(row,col);var end=start.clone().add(1,"days");return{start:start,end:end}},getCellDayIndex:function(row,col){return row*this.daysPerRow+this.getColDayIndex(col)},getColDayIndex:function(col){if(this.isRTL){return this.colCnt-1-col}else{return col}},getDateDayIndex:function(date){var dayIndices=this.dayIndices;var dayOffset=date.diff(this.start,"days");if(dayOffset<0){return dayIndices[0]-1}else if(dayOffset>=dayIndices.length){return dayIndices[dayIndices.length-1]+1}else{return dayIndices[dayOffset]}},computeColHeadFormat:function(){if(this.rowCnt>1||this.colCnt>10){return"ddd"}else if(this.colCnt>1){return this.view.opt("dayOfMonthFormat")}else{return"dddd"}},sliceRangeByRow:function(range){var daysPerRow=this.daysPerRow;var normalRange=this.view.computeDayRange(range);var rangeFirst=this.getDateDayIndex(normalRange.start);var rangeLast=this.getDateDayIndex(normalRange.end.clone().subtract(1,"days"));var segs=[];var row;var rowFirst,rowLast;var segFirst,segLast;for(row=0;row<this.rowCnt;row++){rowFirst=row*daysPerRow;rowLast=rowFirst+daysPerRow-1;segFirst=Math.max(rangeFirst,rowFirst);segLast=Math.min(rangeLast,rowLast);segFirst=Math.ceil(segFirst);segLast=Math.floor(segLast);if(segFirst<=segLast){segs.push({row:row,firstRowDayIndex:segFirst-rowFirst,lastRowDayIndex:segLast-rowFirst,isStart:segFirst===rangeFirst,isEnd:segLast===rangeLast})}}return segs},sliceRangeByDay:function(range){var daysPerRow=this.daysPerRow;var normalRange=this.view.computeDayRange(range);var rangeFirst=this.getDateDayIndex(normalRange.start);var rangeLast=this.getDateDayIndex(normalRange.end.clone().subtract(1,"days"));var segs=[];var row;var rowFirst,rowLast;var i;var segFirst,segLast;for(row=0;row<this.rowCnt;row++){rowFirst=row*daysPerRow;rowLast=rowFirst+daysPerRow-1;for(i=rowFirst;i<=rowLast;i++){segFirst=Math.max(rangeFirst,i);segLast=Math.min(rangeLast,i);segFirst=Math.ceil(segFirst);segLast=Math.floor(segLast);if(segFirst<=segLast){segs.push({row:row,firstRowDayIndex:segFirst-rowFirst,lastRowDayIndex:segLast-rowFirst,isStart:segFirst===rangeFirst,isEnd:segLast===rangeLast})}}}return segs},renderHeadHtml:function(){var view=this.view;return""+'<div class="fc-row '+view.widgetHeaderClass+'">'+"<table>"+"<thead>"+this.renderHeadTrHtml()+"</thead>"+"</table>"+"</div>"},renderHeadIntroHtml:function(){return this.renderIntroHtml()},renderHeadTrHtml:function(){return""+"<tr>"+(this.isRTL?"":this.renderHeadIntroHtml())+this.renderHeadDateCellsHtml()+(this.isRTL?this.renderHeadIntroHtml():"")+"</tr>"},renderHeadDateCellsHtml:function(){var htmls=[];var col,date;for(col=0;col<this.colCnt;col++){date=this.getCellDate(0,col);htmls.push(this.renderHeadDateCellHtml(date))}return htmls.join("")},renderHeadDateCellHtml:function(date,colspan,otherAttrs){var view=this.view;return""+'<th class="fc-day-header '+view.widgetHeaderClass+" fc-"+dayIDs[date.day()]+'"'+(this.rowCnt==1?' data-date="'+date.format("YYYY-MM-DD")+'"':"")+(colspan>1?' colspan="'+colspan+'"':"")+(otherAttrs?" "+otherAttrs:"")+">"+htmlEscape(date.format(this.colHeadFormat))+"</th>"},renderBgTrHtml:function(row){return""+"<tr>"+(this.isRTL?"":this.renderBgIntroHtml(row))+this.renderBgCellsHtml(row)+(this.isRTL?this.renderBgIntroHtml(row):"")+"</tr>"},renderBgIntroHtml:function(row){return this.renderIntroHtml()},renderBgCellsHtml:function(row){var htmls=[];var col,date;for(col=0;col<this.colCnt;col++){date=this.getCellDate(row,col);htmls.push(this.renderBgCellHtml(date))}return htmls.join("")},renderBgCellHtml:function(date,otherAttrs){var view=this.view;var classes=this.getDayClasses(date);classes.unshift("fc-day",view.widgetContentClass);return'<td class="'+classes.join(" ")+'"'+' data-date="'+date.format("YYYY-MM-DD")+'"'+(otherAttrs?" "+otherAttrs:"")+"></td>"},renderIntroHtml:function(){},bookendCells:function(trEl){var introHtml=this.renderIntroHtml();if(introHtml){if(this.isRTL){trEl.append(introHtml)}else{trEl.prepend(introHtml)}}}};var DayGrid=FC.DayGrid=Grid.extend(DayTableMixin,{numbersVisible:false,bottomCoordPadding:0,rowEls:null,cellEls:null,helperEls:null,rowCoordCache:null,colCoordCache:null,renderDates:function(isRigid){var view=this.view;var rowCnt=this.rowCnt;var colCnt=this.colCnt;var html="";var row;var col;for(row=0;row<rowCnt;row++){html+=this.renderDayRowHtml(row,isRigid)}this.el.html(html);this.rowEls=this.el.find(".fc-row");this.cellEls=this.el.find(".fc-day");this.rowCoordCache=new CoordCache({els:this.rowEls,isVertical:true});this.colCoordCache=new CoordCache({els:this.cellEls.slice(0,this.colCnt),isHorizontal:true});for(row=0;row<rowCnt;row++){for(col=0;col<colCnt;col++){view.trigger("dayRender",null,this.getCellDate(row,col),this.getCellEl(row,col))}}},unrenderDates:function(){this.removeSegPopover()},renderBusinessHours:function(){var events=this.view.calendar.getBusinessHoursEvents(true);var segs=this.eventsToSegs(events);this.renderFill("businessHours",segs,"bgevent")},renderDayRowHtml:function(row,isRigid){var view=this.view;var classes=["fc-row","fc-week",view.widgetContentClass];if(isRigid){classes.push("fc-rigid")}return""+'<div class="'+classes.join(" ")+'">'+'<div class="fc-bg">'+"<table>"+this.renderBgTrHtml(row)+"</table>"+"</div>"+'<div class="fc-content-skeleton">'+"<table>"+(this.numbersVisible?"<thead>"+this.renderNumberTrHtml(row)+"</thead>":"")+"</table>"+"</div>"+"</div>"},renderNumberTrHtml:function(row){return""+"<tr>"+(this.isRTL?"":this.renderNumberIntroHtml(row))+this.renderNumberCellsHtml(row)+(this.isRTL?this.renderNumberIntroHtml(row):"")+"</tr>"},renderNumberIntroHtml:function(row){return this.renderIntroHtml()},renderNumberCellsHtml:function(row){var htmls=[];var col,date;for(col=0;col<this.colCnt;col++){date=this.getCellDate(row,col);htmls.push(this.renderNumberCellHtml(date))}return htmls.join("")},renderNumberCellHtml:function(date){var classes;if(!this.view.dayNumbersVisible){return"<td/>"}classes=this.getDayClasses(date);classes.unshift("fc-day-number");return""+'<td class="'+classes.join(" ")+'" data-date="'+date.format()+'">'+date.date()+"</td>"},computeEventTimeFormat:function(){return this.view.opt("extraSmallTimeFormat")},computeDisplayEventEnd:function(){return this.colCnt==1},rangeUpdated:function(){this.updateDayTable()},spanToSegs:function(span){var segs=this.sliceRangeByRow(span);var i,seg;for(i=0;i<segs.length;i++){seg=segs[i];if(this.isRTL){seg.leftCol=this.daysPerRow-1-seg.lastRowDayIndex;seg.rightCol=this.daysPerRow-1-seg.firstRowDayIndex}else{seg.leftCol=seg.firstRowDayIndex;seg.rightCol=seg.lastRowDayIndex}}return segs},prepareHits:function(){this.colCoordCache.build();this.rowCoordCache.build();this.rowCoordCache.bottoms[this.rowCnt-1]+=this.bottomCoordPadding},releaseHits:function(){this.colCoordCache.clear();this.rowCoordCache.clear()},queryHit:function(leftOffset,topOffset){var col=this.colCoordCache.getHorizontalIndex(leftOffset);var row=this.rowCoordCache.getVerticalIndex(topOffset);if(row!=null&&col!=null){return this.getCellHit(row,col)}},getHitSpan:function(hit){return this.getCellRange(hit.row,hit.col)},getHitEl:function(hit){return this.getCellEl(hit.row,hit.col)},getCellHit:function(row,col){return{row:row,col:col,component:this,left:this.colCoordCache.getLeftOffset(col),right:this.colCoordCache.getRightOffset(col),top:this.rowCoordCache.getTopOffset(row),bottom:this.rowCoordCache.getBottomOffset(row)}},getCellEl:function(row,col){return this.cellEls.eq(row*this.colCnt+col)},renderDrag:function(eventLocation,seg){this.renderHighlight(this.eventToSpan(eventLocation));if(seg&&!seg.el.closest(this.el).length){return this.renderEventLocationHelper(eventLocation,seg)}},unrenderDrag:function(){this.unrenderHighlight();this.unrenderHelper()},renderEventResize:function(eventLocation,seg){this.renderHighlight(this.eventToSpan(eventLocation));return this.renderEventLocationHelper(eventLocation,seg)},unrenderEventResize:function(){this.unrenderHighlight();this.unrenderHelper()},renderHelper:function(event,sourceSeg){var helperNodes=[];var segs=this.eventToSegs(event);var rowStructs;segs=this.renderFgSegEls(segs);rowStructs=this.renderSegRows(segs);this.rowEls.each(function(row,rowNode){var rowEl=$(rowNode);var skeletonEl=$('<div class="fc-helper-skeleton"><table/></div>');var skeletonTop;if(sourceSeg&&sourceSeg.row===row){skeletonTop=sourceSeg.el.position().top}else{skeletonTop=rowEl.find(".fc-content-skeleton tbody").position().top}skeletonEl.css("top",skeletonTop).find("table").append(rowStructs[row].tbodyEl);rowEl.append(skeletonEl);helperNodes.push(skeletonEl[0])});return this.helperEls=$(helperNodes)},unrenderHelper:function(){if(this.helperEls){this.helperEls.remove();this.helperEls=null}},fillSegTag:"td",renderFill:function(type,segs,className){var nodes=[];var i,seg;var skeletonEl;segs=this.renderFillSegEls(type,segs);for(i=0;i<segs.length;i++){seg=segs[i];skeletonEl=this.renderFillRow(type,seg,className);this.rowEls.eq(seg.row).append(skeletonEl);nodes.push(skeletonEl[0])}this.elsByFill[type]=$(nodes);return segs},renderFillRow:function(type,seg,className){var colCnt=this.colCnt;var startCol=seg.leftCol;var endCol=seg.rightCol+1;var skeletonEl;var trEl;className=className||type.toLowerCase();skeletonEl=$('<div class="fc-'+className+'-skeleton">'+"<table><tr/></table>"+"</div>");trEl=skeletonEl.find("tr");if(startCol>0){trEl.append('<td colspan="'+startCol+'"/>')}trEl.append(seg.el.attr("colspan",endCol-startCol));if(endCol<colCnt){trEl.append('<td colspan="'+(colCnt-endCol)+'"/>')}this.bookendCells(trEl);return skeletonEl}});DayGrid.mixin({rowStructs:null,unrenderEvents:function(){this.removeSegPopover();Grid.prototype.unrenderEvents.apply(this,arguments)},getEventSegs:function(){return Grid.prototype.getEventSegs.call(this).concat(this.popoverSegs||[])},renderBgSegs:function(segs){var allDaySegs=$.grep(segs,function(seg){return seg.event.allDay});return Grid.prototype.renderBgSegs.call(this,allDaySegs)},renderFgSegs:function(segs){var rowStructs;segs=this.renderFgSegEls(segs);rowStructs=this.rowStructs=this.renderSegRows(segs);this.rowEls.each(function(i,rowNode){$(rowNode).find(".fc-content-skeleton > table").append(rowStructs[i].tbodyEl)});return segs},unrenderFgSegs:function(){var rowStructs=this.rowStructs||[];var rowStruct;while(rowStruct=rowStructs.pop()){rowStruct.tbodyEl.remove()}this.rowStructs=null},renderSegRows:function(segs){var rowStructs=[];var segRows;var row;segRows=this.groupSegRows(segs);for(row=0;row<segRows.length;row++){rowStructs.push(this.renderSegRow(row,segRows[row]))}return rowStructs},fgSegHtml:function(seg,disableResizing){var view=this.view;var event=seg.event;var isDraggable=view.isEventDraggable(event);var isResizableFromStart=!disableResizing&&event.allDay&&seg.isStart&&view.isEventResizableFromStart(event);var isResizableFromEnd=!disableResizing&&event.allDay&&seg.isEnd&&view.isEventResizableFromEnd(event);var classes=this.getSegClasses(seg,isDraggable,isResizableFromStart||isResizableFromEnd);var skinCss=cssToStr(this.getSegSkinCss(seg));var timeHtml="";var timeText;var titleHtml;classes.unshift("fc-day-grid-event","fc-h-event");if(seg.isStart){timeText=this.getEventTimeText(event);if(timeText){timeHtml='<span class="fc-time">'+htmlEscape(timeText)+"</span>"}}titleHtml='<span class="fc-title">'+(htmlEscape(event.title||"")||"&nbsp;")+"</span>";return'<a class="'+classes.join(" ")+'"'+(event.url?' href="'+htmlEscape(event.url)+'"':"")+(skinCss?' style="'+skinCss+'"':"")+">"+'<div class="fc-content">'+(this.isRTL?titleHtml+" "+timeHtml:timeHtml+" "+titleHtml)+"</div>"+(isResizableFromStart?'<div class="fc-resizer fc-start-resizer" />':"")+(isResizableFromEnd?'<div class="fc-resizer fc-end-resizer" />':"")+"</a>"},renderSegRow:function(row,rowSegs){var colCnt=this.colCnt;var segLevels=this.buildSegLevels(rowSegs);var levelCnt=Math.max(1,segLevels.length);var tbody=$("<tbody/>");var segMatrix=[];var cellMatrix=[];var loneCellMatrix=[];var i,levelSegs;var col;var tr;var j,seg;var td;function emptyCellsUntil(endCol){while(col<endCol){td=(loneCellMatrix[i-1]||[])[col];if(td){td.attr("rowspan",parseInt(td.attr("rowspan")||1,10)+1)}else{td=$("<td/>");tr.append(td)}cellMatrix[i][col]=td;loneCellMatrix[i][col]=td;col++}}for(i=0;i<levelCnt;i++){levelSegs=segLevels[i];col=0;tr=$("<tr/>");segMatrix.push([]);cellMatrix.push([]);loneCellMatrix.push([]);if(levelSegs){for(j=0;j<levelSegs.length;j++){seg=levelSegs[j];emptyCellsUntil(seg.leftCol);td=$('<td class="fc-event-container"/>').append(seg.el);if(seg.leftCol!=seg.rightCol){td.attr("colspan",seg.rightCol-seg.leftCol+1)}else{loneCellMatrix[i][col]=td}while(col<=seg.rightCol){cellMatrix[i][col]=td;segMatrix[i][col]=seg;col++}tr.append(td)}}emptyCellsUntil(colCnt);this.bookendCells(tr);tbody.append(tr)}return{row:row,tbodyEl:tbody,cellMatrix:cellMatrix,segMatrix:segMatrix,segLevels:segLevels,segs:rowSegs}},buildSegLevels:function(segs){var levels=[];var i,seg;var j;this.sortEventSegs(segs);for(i=0;i<segs.length;i++){seg=segs[i];for(j=0;j<levels.length;j++){if(!isDaySegCollision(seg,levels[j])){break}}seg.level=j;(levels[j]||(levels[j]=[])).push(seg)}for(j=0;j<levels.length;j++){levels[j].sort(compareDaySegCols)}return levels},groupSegRows:function(segs){var segRows=[];var i;for(i=0;i<this.rowCnt;i++){segRows.push([])}for(i=0;i<segs.length;i++){segRows[segs[i].row].push(segs[i])}return segRows}});function isDaySegCollision(seg,otherSegs){var i,otherSeg;for(i=0;i<otherSegs.length;i++){otherSeg=otherSegs[i];if(otherSeg.leftCol<=seg.rightCol&&otherSeg.rightCol>=seg.leftCol){return true}}return false}function compareDaySegCols(a,b){return a.leftCol-b.leftCol}DayGrid.mixin({segPopover:null,popoverSegs:null,removeSegPopover:function(){if(this.segPopover){this.segPopover.hide()}},limitRows:function(levelLimit){var rowStructs=this.rowStructs||[];var row;var rowLevelLimit;for(row=0;row<rowStructs.length;row++){this.unlimitRow(row);if(!levelLimit){rowLevelLimit=false}else if(typeof levelLimit==="number"){rowLevelLimit=levelLimit}else{rowLevelLimit=this.computeRowLevelLimit(row)}if(rowLevelLimit!==false){this.limitRow(row,rowLevelLimit)}}},computeRowLevelLimit:function(row){var rowEl=this.rowEls.eq(row);var rowHeight=rowEl.height();var trEls=this.rowStructs[row].tbodyEl.children();var i,trEl;var trHeight;function iterInnerHeights(i,childNode){trHeight=Math.max(trHeight,$(childNode).outerHeight())}for(i=0;i<trEls.length;i++){trEl=trEls.eq(i).removeClass("fc-limited");trHeight=0;trEl.find("> td > :first-child").each(iterInnerHeights);if(trEl.position().top+trHeight>rowHeight){return i}}return false},limitRow:function(row,levelLimit){var _this=this;var rowStruct=this.rowStructs[row];var moreNodes=[];var col=0;var levelSegs;var cellMatrix;var limitedNodes;var i,seg;var segsBelow;var totalSegsBelow;var colSegsBelow;var td,rowspan;var segMoreNodes;var j;var moreTd,moreWrap,moreLink;function emptyCellsUntil(endCol){while(col<endCol){segsBelow=_this.getCellSegs(row,col,levelLimit);if(segsBelow.length){td=cellMatrix[levelLimit-1][col];moreLink=_this.renderMoreLink(row,col,segsBelow);moreWrap=$("<div/>").append(moreLink);td.append(moreWrap);moreNodes.push(moreWrap[0])}col++}}if(levelLimit&&levelLimit<rowStruct.segLevels.length){levelSegs=rowStruct.segLevels[levelLimit-1];cellMatrix=rowStruct.cellMatrix;limitedNodes=rowStruct.tbodyEl.children().slice(levelLimit).addClass("fc-limited").get();for(i=0;i<levelSegs.length;i++){seg=levelSegs[i];emptyCellsUntil(seg.leftCol);colSegsBelow=[];totalSegsBelow=0;while(col<=seg.rightCol){segsBelow=this.getCellSegs(row,col,levelLimit);colSegsBelow.push(segsBelow);totalSegsBelow+=segsBelow.length;col++}if(totalSegsBelow){td=cellMatrix[levelLimit-1][seg.leftCol];rowspan=td.attr("rowspan")||1;segMoreNodes=[];for(j=0;j<colSegsBelow.length;j++){moreTd=$('<td class="fc-more-cell"/>').attr("rowspan",rowspan);segsBelow=colSegsBelow[j];moreLink=this.renderMoreLink(row,seg.leftCol+j,[seg].concat(segsBelow));moreWrap=$("<div/>").append(moreLink);moreTd.append(moreWrap);segMoreNodes.push(moreTd[0]);moreNodes.push(moreTd[0])}td.addClass("fc-limited").after($(segMoreNodes));limitedNodes.push(td[0])}}emptyCellsUntil(this.colCnt);rowStruct.moreEls=$(moreNodes);rowStruct.limitedEls=$(limitedNodes)}},unlimitRow:function(row){var rowStruct=this.rowStructs[row];if(rowStruct.moreEls){rowStruct.moreEls.remove();rowStruct.moreEls=null}if(rowStruct.limitedEls){rowStruct.limitedEls.removeClass("fc-limited");rowStruct.limitedEls=null}},renderMoreLink:function(row,col,hiddenSegs){var _this=this;var view=this.view;return $('<a class="fc-more"/>').text(this.getMoreLinkText(hiddenSegs.length)).on("click",function(ev){var clickOption=view.opt("eventLimitClick");var date=_this.getCellDate(row,col);var moreEl=$(this);var dayEl=_this.getCellEl(row,col);var allSegs=_this.getCellSegs(row,col);var reslicedAllSegs=_this.resliceDaySegs(allSegs,date);var reslicedHiddenSegs=_this.resliceDaySegs(hiddenSegs,date);if(typeof clickOption==="function"){clickOption=view.trigger("eventLimitClick",null,{date:date,dayEl:dayEl,moreEl:moreEl,segs:reslicedAllSegs,hiddenSegs:reslicedHiddenSegs},ev)}if(clickOption==="popover"){_this.showSegPopover(row,col,moreEl,reslicedAllSegs)}else if(typeof clickOption==="string"){view.calendar.zoomTo(date,clickOption)}})},showSegPopover:function(row,col,moreLink,segs){var _this=this;var view=this.view;var moreWrap=moreLink.parent();var topEl;var options;if(this.rowCnt==1){topEl=view.el}else{topEl=this.rowEls.eq(row)}options={className:"fc-more-popover",content:this.renderSegPopoverContent(row,col,segs),parentEl:this.el,top:topEl.offset().top,autoHide:true,viewportConstrain:view.opt("popoverViewportConstrain"),hide:function(){_this.segPopover.removeElement();_this.segPopover=null;_this.popoverSegs=null}};if(this.isRTL){options.right=moreWrap.offset().left+moreWrap.outerWidth()+1}else{options.left=moreWrap.offset().left-1}this.segPopover=new Popover(options);this.segPopover.show()},renderSegPopoverContent:function(row,col,segs){var view=this.view;var isTheme=view.opt("theme");var title=this.getCellDate(row,col).format(view.opt("dayPopoverFormat"));var content=$('<div class="fc-header '+view.widgetHeaderClass+'">'+'<span class="fc-close '+(isTheme?"ui-icon ui-icon-closethick":"fc-icon fc-icon-x")+'"></span>'+'<span class="fc-title">'+htmlEscape(title)+"</span>"+'<div class="fc-clear"/>'+"</div>"+'<div class="fc-body '+view.widgetContentClass+'">'+'<div class="fc-event-container"></div>'+"</div>");var segContainer=content.find(".fc-event-container");var i;segs=this.renderFgSegEls(segs,true);this.popoverSegs=segs;for(i=0;i<segs.length;i++){this.prepareHits();segs[i].hit=this.getCellHit(row,col);this.releaseHits();segContainer.append(segs[i].el)}return content},resliceDaySegs:function(segs,dayDate){var events=$.map(segs,function(seg){return seg.event});var dayStart=dayDate.clone();var dayEnd=dayStart.clone().add(1,"days");var dayRange={start:dayStart,end:dayEnd};segs=this.eventsToSegs(events,function(range){var seg=intersectRanges(range,dayRange);return seg?[seg]:[]});this.sortEventSegs(segs);return segs},getMoreLinkText:function(num){var opt=this.view.opt("eventLimitText");if(typeof opt==="function"){return opt(num)}else{return"+"+num+" "+opt}},getCellSegs:function(row,col,startLevel){var segMatrix=this.rowStructs[row].segMatrix;var level=startLevel||0;var segs=[];var seg;while(level<segMatrix.length){seg=segMatrix[level][col];if(seg){segs.push(seg)}level++}return segs}});var TimeGrid=FC.TimeGrid=Grid.extend(DayTableMixin,{slotDuration:null,snapDuration:null,snapsPerSlot:null,minTime:null,maxTime:null,labelFormat:null,labelInterval:null,colEls:null,slatContainerEl:null,slatEls:null,nowIndicatorEls:null,colCoordCache:null,slatCoordCache:null,constructor:function(){Grid.apply(this,arguments);this.processOptions()},renderDates:function(){this.el.html(this.renderHtml());this.colEls=this.el.find(".fc-day");this.slatContainerEl=this.el.find(".fc-slats");this.slatEls=this.slatContainerEl.find("tr");this.colCoordCache=new CoordCache({els:this.colEls,isHorizontal:true});this.slatCoordCache=new CoordCache({els:this.slatEls,isVertical:true});this.renderContentSkeleton()},renderHtml:function(){return""+'<div class="fc-bg">'+"<table>"+this.renderBgTrHtml(0)+"</table>"+"</div>"+'<div class="fc-slats">'+"<table>"+this.renderSlatRowHtml()+"</table>"+"</div>"},renderSlatRowHtml:function(){var view=this.view;var isRTL=this.isRTL;var html="";var slotTime=moment.duration(+this.minTime);var slotDate;var isLabeled;var axisHtml;while(slotTime<this.maxTime){slotDate=this.start.clone().time(slotTime);isLabeled=isInt(divideDurationByDuration(slotTime,this.labelInterval));axisHtml='<td class="fc-axis fc-time '+view.widgetContentClass+'" '+view.axisStyleAttr()+">"+(isLabeled?"<span>"+htmlEscape(slotDate.format(this.labelFormat))+"</span>":"")+"</td>";html+='<tr data-time="'+slotDate.format("HH:mm:ss")+'"'+(isLabeled?"":' class="fc-minor"')+">"+(!isRTL?axisHtml:"")+'<td class="'+view.widgetContentClass+'"/>'+(isRTL?axisHtml:"")+"</tr>";slotTime.add(this.slotDuration)}return html},processOptions:function(){var view=this.view;var slotDuration=view.opt("slotDuration");var snapDuration=view.opt("snapDuration");var input;slotDuration=moment.duration(slotDuration);snapDuration=snapDuration?moment.duration(snapDuration):slotDuration;this.slotDuration=slotDuration;this.snapDuration=snapDuration;this.snapsPerSlot=slotDuration/snapDuration;this.minResizeDuration=snapDuration;this.minTime=moment.duration(view.opt("minTime"));this.maxTime=moment.duration(view.opt("maxTime"));input=view.opt("slotLabelFormat");if($.isArray(input)){input=input[input.length-1]}this.labelFormat=input||view.opt("axisFormat")||view.opt("smallTimeFormat");input=view.opt("slotLabelInterval");this.labelInterval=input?moment.duration(input):this.computeLabelInterval(slotDuration)},computeLabelInterval:function(slotDuration){var i;var labelInterval;var slotsPerLabel;for(i=AGENDA_STOCK_SUB_DURATIONS.length-1;i>=0;i--){labelInterval=moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]);slotsPerLabel=divideDurationByDuration(labelInterval,slotDuration);if(isInt(slotsPerLabel)&&slotsPerLabel>1){return labelInterval}}return moment.duration(slotDuration)},computeEventTimeFormat:function(){return this.view.opt("noMeridiemTimeFormat")},computeDisplayEventEnd:function(){return true},prepareHits:function(){this.colCoordCache.build();this.slatCoordCache.build()},releaseHits:function(){this.colCoordCache.clear()},queryHit:function(leftOffset,topOffset){var snapsPerSlot=this.snapsPerSlot;var colCoordCache=this.colCoordCache;var slatCoordCache=this.slatCoordCache;var colIndex=colCoordCache.getHorizontalIndex(leftOffset);var slatIndex=slatCoordCache.getVerticalIndex(topOffset);if(colIndex!=null&&slatIndex!=null){var slatTop=slatCoordCache.getTopOffset(slatIndex);var slatHeight=slatCoordCache.getHeight(slatIndex);var partial=(topOffset-slatTop)/slatHeight;var localSnapIndex=Math.floor(partial*snapsPerSlot);var snapIndex=slatIndex*snapsPerSlot+localSnapIndex;var snapTop=slatTop+localSnapIndex/snapsPerSlot*slatHeight;var snapBottom=slatTop+(localSnapIndex+1)/snapsPerSlot*slatHeight;return{col:colIndex,snap:snapIndex,component:this,left:colCoordCache.getLeftOffset(colIndex),right:colCoordCache.getRightOffset(colIndex),top:snapTop,bottom:snapBottom}}},getHitSpan:function(hit){var start=this.getCellDate(0,hit.col);var time=this.computeSnapTime(hit.snap);var end;start.time(time);end=start.clone().add(this.snapDuration);return{start:start,end:end}},getHitEl:function(hit){return this.colEls.eq(hit.col)},rangeUpdated:function(){this.updateDayTable()},computeSnapTime:function(snapIndex){return moment.duration(this.minTime+this.snapDuration*snapIndex)},spanToSegs:function(span){var segs=this.sliceRangeByTimes(span);var i;for(i=0;i<segs.length;i++){if(this.isRTL){segs[i].col=this.daysPerRow-1-segs[i].dayIndex}else{segs[i].col=segs[i].dayIndex}}return segs},sliceRangeByTimes:function(range){var segs=[];var seg;var dayIndex;var dayDate;var dayRange;for(dayIndex=0;dayIndex<this.daysPerRow;dayIndex++){dayDate=this.dayDates[dayIndex].clone();dayRange={start:dayDate.clone().time(this.minTime),end:dayDate.clone().time(this.maxTime)};seg=intersectRanges(range,dayRange);if(seg){seg.dayIndex=dayIndex;segs.push(seg)}}return segs},updateSize:function(isResize){this.slatCoordCache.build();if(isResize){this.updateSegVerticals([].concat(this.fgSegs||[],this.bgSegs||[],this.businessSegs||[]))}},getTotalSlatHeight:function(){return this.slatContainerEl.outerHeight()},computeDateTop:function(date,startOfDayDate){return this.computeTimeTop(moment.duration(date-startOfDayDate.clone().stripTime()))},computeTimeTop:function(time){var len=this.slatEls.length;var slatCoverage=(time-this.minTime)/this.slotDuration;var slatIndex;var slatRemainder;slatCoverage=Math.max(0,slatCoverage);slatCoverage=Math.min(len,slatCoverage);slatIndex=Math.floor(slatCoverage);slatIndex=Math.min(slatIndex,len-1);slatRemainder=slatCoverage-slatIndex;return this.slatCoordCache.getTopPosition(slatIndex)+this.slatCoordCache.getHeight(slatIndex)*slatRemainder},renderDrag:function(eventLocation,seg){if(seg){return this.renderEventLocationHelper(eventLocation,seg)}else{this.renderHighlight(this.eventToSpan(eventLocation))}},unrenderDrag:function(){this.unrenderHelper();this.unrenderHighlight()},renderEventResize:function(eventLocation,seg){return this.renderEventLocationHelper(eventLocation,seg)},unrenderEventResize:function(){this.unrenderHelper()},renderHelper:function(event,sourceSeg){return this.renderHelperSegs(this.eventToSegs(event),sourceSeg)},unrenderHelper:function(){this.unrenderHelperSegs()},renderBusinessHours:function(){var events=this.view.calendar.getBusinessHoursEvents();var segs=this.eventsToSegs(events);this.renderBusinessSegs(segs)},unrenderBusinessHours:function(){this.unrenderBusinessSegs()},getNowIndicatorUnit:function(){return"minute"},renderNowIndicator:function(date){var segs=this.spanToSegs({start:date,end:date});var top=this.computeDateTop(date,date);var nodes=[];var i;for(i=0;i<segs.length;i++){nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>').css("top",top).appendTo(this.colContainerEls.eq(segs[i].col))[0])}if(segs.length>0){nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>').css("top",top).appendTo(this.el.find(".fc-content-skeleton"))[0])}this.nowIndicatorEls=$(nodes)},unrenderNowIndicator:function(){if(this.nowIndicatorEls){this.nowIndicatorEls.remove();this.nowIndicatorEls=null}},renderSelection:function(span){if(this.view.opt("selectHelper")){this.renderEventLocationHelper(span)}else{this.renderHighlight(span)}},unrenderSelection:function(){this.unrenderHelper();this.unrenderHighlight()},renderHighlight:function(span){this.renderHighlightSegs(this.spanToSegs(span))},unrenderHighlight:function(){this.unrenderHighlightSegs()}});TimeGrid.mixin({colContainerEls:null,fgContainerEls:null,bgContainerEls:null,helperContainerEls:null,highlightContainerEls:null,businessContainerEls:null,fgSegs:null,bgSegs:null,helperSegs:null,highlightSegs:null,businessSegs:null,renderContentSkeleton:function(){var cellHtml="";var i;var skeletonEl;for(i=0;i<this.colCnt;i++){cellHtml+="<td>"+'<div class="fc-content-col">'+'<div class="fc-event-container fc-helper-container"></div>'+'<div class="fc-event-container"></div>'+'<div class="fc-highlight-container"></div>'+'<div class="fc-bgevent-container"></div>'+'<div class="fc-business-container"></div>'+"</div>"+"</td>"}skeletonEl=$('<div class="fc-content-skeleton">'+"<table>"+"<tr>"+cellHtml+"</tr>"+"</table>"+"</div>");this.colContainerEls=skeletonEl.find(".fc-content-col");this.helperContainerEls=skeletonEl.find(".fc-helper-container");this.fgContainerEls=skeletonEl.find(".fc-event-container:not(.fc-helper-container)");this.bgContainerEls=skeletonEl.find(".fc-bgevent-container");this.highlightContainerEls=skeletonEl.find(".fc-highlight-container");this.businessContainerEls=skeletonEl.find(".fc-business-container");this.bookendCells(skeletonEl.find("tr"));this.el.append(skeletonEl)},renderFgSegs:function(segs){segs=this.renderFgSegsIntoContainers(segs,this.fgContainerEls);this.fgSegs=segs;return segs},unrenderFgSegs:function(){this.unrenderNamedSegs("fgSegs")},renderHelperSegs:function(segs,sourceSeg){var helperEls=[];var i,seg;var sourceEl;segs=this.renderFgSegsIntoContainers(segs,this.helperContainerEls);for(i=0;i<segs.length;i++){seg=segs[i];if(sourceSeg&&sourceSeg.col===seg.col){sourceEl=sourceSeg.el;seg.el.css({left:sourceEl.css("left"),right:sourceEl.css("right"),"margin-left":sourceEl.css("margin-left"),"margin-right":sourceEl.css("margin-right")})}helperEls.push(seg.el[0])}this.helperSegs=segs;return $(helperEls)},unrenderHelperSegs:function(){this.unrenderNamedSegs("helperSegs")},renderBgSegs:function(segs){segs=this.renderFillSegEls("bgEvent",segs);this.updateSegVerticals(segs);this.attachSegsByCol(this.groupSegsByCol(segs),this.bgContainerEls);this.bgSegs=segs;return segs},unrenderBgSegs:function(){this.unrenderNamedSegs("bgSegs")},renderHighlightSegs:function(segs){segs=this.renderFillSegEls("highlight",segs);this.updateSegVerticals(segs);this.attachSegsByCol(this.groupSegsByCol(segs),this.highlightContainerEls);this.highlightSegs=segs},unrenderHighlightSegs:function(){this.unrenderNamedSegs("highlightSegs")},renderBusinessSegs:function(segs){segs=this.renderFillSegEls("businessHours",segs);this.updateSegVerticals(segs);this.attachSegsByCol(this.groupSegsByCol(segs),this.businessContainerEls);this.businessSegs=segs},unrenderBusinessSegs:function(){this.unrenderNamedSegs("businessSegs")},groupSegsByCol:function(segs){var segsByCol=[];var i;for(i=0;i<this.colCnt;i++){segsByCol.push([])}for(i=0;i<segs.length;i++){segsByCol[segs[i].col].push(segs[i])}return segsByCol},attachSegsByCol:function(segsByCol,containerEls){var col;var segs;var i;for(col=0;col<this.colCnt;col++){segs=segsByCol[col];for(i=0;i<segs.length;i++){containerEls.eq(col).append(segs[i].el)}}},unrenderNamedSegs:function(propName){var segs=this[propName];var i;if(segs){for(i=0;i<segs.length;i++){segs[i].el.remove()}this[propName]=null}},renderFgSegsIntoContainers:function(segs,containerEls){var segsByCol;var col;segs=this.renderFgSegEls(segs);segsByCol=this.groupSegsByCol(segs);for(col=0;col<this.colCnt;col++){this.updateFgSegCoords(segsByCol[col])}this.attachSegsByCol(segsByCol,containerEls);return segs},fgSegHtml:function(seg,disableResizing){var view=this.view;var event=seg.event;var isDraggable=view.isEventDraggable(event);var isResizableFromStart=!disableResizing&&seg.isStart&&view.isEventResizableFromStart(event);var isResizableFromEnd=!disableResizing&&seg.isEnd&&view.isEventResizableFromEnd(event);var classes=this.getSegClasses(seg,isDraggable,isResizableFromStart||isResizableFromEnd);var skinCss=cssToStr(this.getSegSkinCss(seg));var timeText;var fullTimeText;var startTimeText;classes.unshift("fc-time-grid-event","fc-v-event");if(view.isMultiDayEvent(event)){if(seg.isStart||seg.isEnd){timeText=this.getEventTimeText(seg);fullTimeText=this.getEventTimeText(seg,"LT");startTimeText=this.getEventTimeText(seg,null,false)}}else{timeText=this.getEventTimeText(event);fullTimeText=this.getEventTimeText(event,"LT");startTimeText=this.getEventTimeText(event,null,false)}return'<a class="'+classes.join(" ")+'"'+(event.url?' href="'+htmlEscape(event.url)+'"':"")+(skinCss?' style="'+skinCss+'"':"")+">"+'<div class="fc-content">'+(timeText?'<div class="fc-time"'+' data-start="'+htmlEscape(startTimeText)+'"'+' data-full="'+htmlEscape(fullTimeText)+'"'+">"+"<span>"+htmlEscape(timeText)+"</span>"+"</div>":"")+(event.title?'<div class="fc-title">'+htmlEscape(event.title)+"</div>":"")+"</div>"+'<div class="fc-bg"/>'+(isResizableFromEnd?'<div class="fc-resizer fc-end-resizer" />':"")+"</a>"},updateSegVerticals:function(segs){this.computeSegVerticals(segs);this.assignSegVerticals(segs)},computeSegVerticals:function(segs){var i,seg;for(i=0;i<segs.length;i++){seg=segs[i];seg.top=this.computeDateTop(seg.start,seg.start);seg.bottom=this.computeDateTop(seg.end,seg.start)}},assignSegVerticals:function(segs){var i,seg;for(i=0;i<segs.length;i++){seg=segs[i];seg.el.css(this.generateSegVerticalCss(seg))}},generateSegVerticalCss:function(seg){return{top:seg.top,bottom:-seg.bottom}},updateFgSegCoords:function(segs){this.computeSegVerticals(segs);this.computeFgSegHorizontals(segs);this.assignSegVerticals(segs);this.assignFgSegHorizontals(segs)},computeFgSegHorizontals:function(segs){var levels;var level0;var i;this.sortEventSegs(segs);levels=buildSlotSegLevels(segs);computeForwardSlotSegs(levels);if(level0=levels[0]){for(i=0;i<level0.length;i++){computeSlotSegPressures(level0[i])}for(i=0;i<level0.length;i++){this.computeFgSegForwardBack(level0[i],0,0)}}},computeFgSegForwardBack:function(seg,seriesBackwardPressure,seriesBackwardCoord){var forwardSegs=seg.forwardSegs;var i;if(seg.forwardCoord===undefined){if(!forwardSegs.length){seg.forwardCoord=1}else{this.sortForwardSegs(forwardSegs);this.computeFgSegForwardBack(forwardSegs[0],seriesBackwardPressure+1,seriesBackwardCoord);seg.forwardCoord=forwardSegs[0].backwardCoord}seg.backwardCoord=seg.forwardCoord-(seg.forwardCoord-seriesBackwardCoord)/(seriesBackwardPressure+1);for(i=0;i<forwardSegs.length;i++){this.computeFgSegForwardBack(forwardSegs[i],0,seg.forwardCoord)}}},sortForwardSegs:function(forwardSegs){forwardSegs.sort(proxy(this,"compareForwardSegs"))},compareForwardSegs:function(seg1,seg2){return seg2.forwardPressure-seg1.forwardPressure||(seg1.backwardCoord||0)-(seg2.backwardCoord||0)||this.compareEventSegs(seg1,seg2)},assignFgSegHorizontals:function(segs){var i,seg;for(i=0;i<segs.length;i++){seg=segs[i];seg.el.css(this.generateFgSegHorizontalCss(seg));if(seg.bottom-seg.top<30){seg.el.addClass("fc-short")}}},generateFgSegHorizontalCss:function(seg){var shouldOverlap=this.view.opt("slotEventOverlap");var backwardCoord=seg.backwardCoord;var forwardCoord=seg.forwardCoord;var props=this.generateSegVerticalCss(seg);var left;var right;if(shouldOverlap){forwardCoord=Math.min(1,backwardCoord+(forwardCoord-backwardCoord)*2)}if(this.isRTL){left=1-forwardCoord;right=backwardCoord}else{left=backwardCoord;right=1-forwardCoord}props.zIndex=seg.level+1;props.left=left*100+"%";props.right=right*100+"%";if(shouldOverlap&&seg.forwardPressure){props[this.isRTL?"marginLeft":"marginRight"]=10*2}return props}});function buildSlotSegLevels(segs){var levels=[];var i,seg;var j;for(i=0;i<segs.length;i++){seg=segs[i];for(j=0;j<levels.length;j++){if(!computeSlotSegCollisions(seg,levels[j]).length){break}}seg.level=j;(levels[j]||(levels[j]=[])).push(seg)}return levels}function computeForwardSlotSegs(levels){var i,level;var j,seg;var k;for(i=0;i<levels.length;i++){level=levels[i];for(j=0;j<level.length;j++){seg=level[j];seg.forwardSegs=[];for(k=i+1;k<levels.length;k++){computeSlotSegCollisions(seg,levels[k],seg.forwardSegs)}}}}function computeSlotSegPressures(seg){var forwardSegs=seg.forwardSegs;var forwardPressure=0;var i,forwardSeg;if(seg.forwardPressure===undefined){for(i=0;i<forwardSegs.length;i++){forwardSeg=forwardSegs[i];computeSlotSegPressures(forwardSeg);forwardPressure=Math.max(forwardPressure,1+forwardSeg.forwardPressure)}seg.forwardPressure=forwardPressure}}function computeSlotSegCollisions(seg,otherSegs,results){results=results||[];for(var i=0;i<otherSegs.length;i++){if(isSlotSegCollision(seg,otherSegs[i])){results.push(otherSegs[i])}}return results}function isSlotSegCollision(seg1,seg2){return seg1.bottom>seg2.top&&seg1.top<seg2.bottom}var View=FC.View=Class.extend(EmitterMixin,ListenerMixin,{type:null,name:null,title:null,calendar:null,options:null,el:null,displaying:null,isSkeletonRendered:false,isEventsRendered:false,start:null,end:null,intervalStart:null,intervalEnd:null,intervalDuration:null,intervalUnit:null,isRTL:false,isSelected:false,selectedEvent:null,eventOrderSpecs:null,widgetHeaderClass:null,widgetContentClass:null,highlightStateClass:null,nextDayThreshold:null,isHiddenDayHash:null,isNowIndicatorRendered:null,initialNowDate:null,initialNowQueriedMs:null,nowIndicatorTimeoutID:null,nowIndicatorIntervalID:null,constructor:function(calendar,type,options,intervalDuration){this.calendar=calendar;this.type=this.name=type;this.options=options;this.intervalDuration=intervalDuration||moment.duration(1,"day");this.nextDayThreshold=moment.duration(this.opt("nextDayThreshold"));this.initThemingProps();this.initHiddenDays();this.isRTL=this.opt("isRTL");this.eventOrderSpecs=parseFieldSpecs(this.opt("eventOrder"));this.initialize()},initialize:function(){},opt:function(name){return this.options[name]},trigger:function(name,thisObj){var calendar=this.calendar;return calendar.trigger.apply(calendar,[name,thisObj||this].concat(Array.prototype.slice.call(arguments,2),[this]))},setDate:function(date){this.setRange(this.computeRange(date))},setRange:function(range){$.extend(this,range);this.updateTitle()},computeRange:function(date){var intervalUnit=computeIntervalUnit(this.intervalDuration);var intervalStart=date.clone().startOf(intervalUnit);var intervalEnd=intervalStart.clone().add(this.intervalDuration);var start,end;if(/year|month|week|day/.test(intervalUnit)){intervalStart.stripTime();intervalEnd.stripTime()}else{if(!intervalStart.hasTime()){intervalStart=this.calendar.time(0)}if(!intervalEnd.hasTime()){intervalEnd=this.calendar.time(0)}}start=intervalStart.clone();start=this.skipHiddenDays(start);end=intervalEnd.clone();end=this.skipHiddenDays(end,-1,true);return{intervalUnit:intervalUnit,intervalStart:intervalStart,intervalEnd:intervalEnd,start:start,end:end}},computePrevDate:function(date){return this.massageCurrentDate(date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration),-1)},computeNextDate:function(date){return this.massageCurrentDate(date.clone().startOf(this.intervalUnit).add(this.intervalDuration))},massageCurrentDate:function(date,direction){if(this.intervalDuration.as("days")<=1){if(this.isHiddenDay(date)){date=this.skipHiddenDays(date,direction);date.startOf("day")}}return date},updateTitle:function(){this.title=this.computeTitle()},computeTitle:function(){return this.formatRange({start:this.calendar.applyTimezone(this.intervalStart),end:this.calendar.applyTimezone(this.intervalEnd)},this.opt("titleFormat")||this.computeTitleFormat(),this.opt("titleRangeSeparator"))},computeTitleFormat:function(){if(this.intervalUnit=="year"){return"YYYY"}else if(this.intervalUnit=="month"){return this.opt("monthYearFormat")}else if(this.intervalDuration.as("days")>1){return"ll"}else{return"LL"}},formatRange:function(range,formatStr,separator){var end=range.end;if(!end.hasTime()){end=end.clone().subtract(1)}return formatRange(range.start,end,formatStr,separator,this.opt("isRTL"))},setElement:function(el){this.el=el;this.bindGlobalHandlers()},removeElement:function(){this.clear();if(this.isSkeletonRendered){this.unrenderSkeleton();this.isSkeletonRendered=false}this.unbindGlobalHandlers();this.el.remove()},display:function(date){var _this=this;var scrollState=null;if(this.displaying){scrollState=this.queryScroll()}this.calendar.freezeContentHeight();return this.clear().then(function(){return _this.displaying=$.when(_this.displayView(date)).then(function(){_this.forceScroll(_this.computeInitialScroll(scrollState));_this.calendar.unfreezeContentHeight();_this.triggerRender()})})},clear:function(){var _this=this;var displaying=this.displaying;if(displaying){return displaying.then(function(){_this.displaying=null;_this.clearEvents();return _this.clearView()})}else{return $.when()}},displayView:function(date){if(!this.isSkeletonRendered){this.renderSkeleton();this.isSkeletonRendered=true}if(date){this.setDate(date)}if(this.render){this.render()}this.renderDates();this.updateSize();this.renderBusinessHours();this.startNowIndicator()},clearView:function(){this.unselect();this.stopNowIndicator();this.triggerUnrender();this.unrenderBusinessHours();this.unrenderDates();if(this.destroy){this.destroy()}},renderSkeleton:function(){},unrenderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},triggerRender:function(){this.trigger("viewRender",this,this,this.el)},triggerUnrender:function(){this.trigger("viewDestroy",this,this,this.el)},bindGlobalHandlers:function(){this.listenTo($(document),"mousedown",this.handleDocumentMousedown);this.listenTo($(document),"touchstart",this.processUnselect)},unbindGlobalHandlers:function(){this.stopListeningTo($(document))},initThemingProps:function(){var tm=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=tm+"-widget-header";this.widgetContentClass=tm+"-widget-content";this.highlightStateClass=tm+"-state-highlight"},renderBusinessHours:function(){},unrenderBusinessHours:function(){},startNowIndicator:function(){var _this=this;var unit;var update;var delay;if(this.opt("nowIndicator")){unit=this.getNowIndicatorUnit();if(unit){update=proxy(this,"updateNowIndicator");this.initialNowDate=this.calendar.getNow();this.initialNowQueriedMs=+new Date;this.renderNowIndicator(this.initialNowDate);this.isNowIndicatorRendered=true;delay=this.initialNowDate.clone().startOf(unit).add(1,unit)-this.initialNowDate;this.nowIndicatorTimeoutID=setTimeout(function(){_this.nowIndicatorTimeoutID=null;update();delay=+moment.duration(1,unit);delay=Math.max(100,delay);_this.nowIndicatorIntervalID=setInterval(update,delay)},delay)}}},updateNowIndicator:function(){if(this.isNowIndicatorRendered){this.unrenderNowIndicator();this.renderNowIndicator(this.initialNowDate.clone().add(new Date-this.initialNowQueriedMs))}},stopNowIndicator:function(){if(this.isNowIndicatorRendered){if(this.nowIndicatorTimeoutID){clearTimeout(this.nowIndicatorTimeoutID);this.nowIndicatorTimeoutID=null}if(this.nowIndicatorIntervalID){clearTimeout(this.nowIndicatorIntervalID);this.nowIndicatorIntervalID=null}this.unrenderNowIndicator();this.isNowIndicatorRendered=false}},getNowIndicatorUnit:function(){},renderNowIndicator:function(date){},unrenderNowIndicator:function(){},updateSize:function(isResize){var scrollState;if(isResize){scrollState=this.queryScroll()}this.updateHeight(isResize);this.updateWidth(isResize);this.updateNowIndicator();if(isResize){this.setScroll(scrollState)}},updateWidth:function(isResize){},updateHeight:function(isResize){var calendar=this.calendar;this.setHeight(calendar.getSuggestedViewHeight(),calendar.isHeightAuto())},setHeight:function(height,isAuto){},computeInitialScroll:function(previousScrollState){return 0},queryScroll:function(){},setScroll:function(scrollState){},forceScroll:function(scrollState){var _this=this;this.setScroll(scrollState);setTimeout(function(){_this.setScroll(scrollState)},0)},displayEvents:function(events){var scrollState=this.queryScroll();this.clearEvents();this.renderEvents(events);this.isEventsRendered=true;this.setScroll(scrollState);this.triggerEventRender()},clearEvents:function(){var scrollState;if(this.isEventsRendered){scrollState=this.queryScroll();this.triggerEventUnrender();if(this.destroyEvents){this.destroyEvents()}this.unrenderEvents();this.setScroll(scrollState);this.isEventsRendered=false}},renderEvents:function(events){},unrenderEvents:function(){},triggerEventRender:function(){this.renderedEventSegEach(function(seg){this.trigger("eventAfterRender",seg.event,seg.event,seg.el)});this.trigger("eventAfterAllRender")},triggerEventUnrender:function(){this.renderedEventSegEach(function(seg){this.trigger("eventDestroy",seg.event,seg.event,seg.el)})},resolveEventEl:function(event,el){var custom=this.trigger("eventRender",event,event,el);if(custom===false){el=null}else if(custom&&custom!==true){el=$(custom)}return el},showEvent:function(event){this.renderedEventSegEach(function(seg){seg.el.css("visibility","")},event)},hideEvent:function(event){this.renderedEventSegEach(function(seg){seg.el.css("visibility","hidden")},event)},renderedEventSegEach:function(func,event){var segs=this.getEventSegs();var i;for(i=0;i<segs.length;i++){if(!event||segs[i].event._id===event._id){if(segs[i].el){func.call(this,segs[i])}}}},getEventSegs:function(){return[]},isEventDraggable:function(event){var source=event.source||{};return firstDefined(event.startEditable,source.startEditable,this.opt("eventStartEditable"),event.editable,source.editable,this.opt("editable"))},reportEventDrop:function(event,dropLocation,largeUnit,el,ev){var calendar=this.calendar;var mutateResult=calendar.mutateEvent(event,dropLocation,largeUnit);var undoFunc=function(){mutateResult.undo();calendar.reportEventChange()};this.triggerEventDrop(event,mutateResult.dateDelta,undoFunc,el,ev);calendar.reportEventChange()},triggerEventDrop:function(event,dateDelta,undoFunc,el,ev){this.trigger("eventDrop",el[0],event,dateDelta,undoFunc,ev,{})},reportExternalDrop:function(meta,dropLocation,el,ev,ui){var eventProps=meta.eventProps;var eventInput;var event;if(eventProps){eventInput=$.extend({},eventProps,dropLocation);event=this.calendar.renderEvent(eventInput,meta.stick)[0]}this.triggerExternalDrop(event,dropLocation,el,ev,ui)},triggerExternalDrop:function(event,dropLocation,el,ev,ui){this.trigger("drop",el[0],dropLocation.start,ev,ui);if(event){this.trigger("eventReceive",null,event)}},renderDrag:function(dropLocation,seg){},unrenderDrag:function(){},isEventResizableFromStart:function(event){return this.opt("eventResizableFromStart")&&this.isEventResizable(event)},isEventResizableFromEnd:function(event){return this.isEventResizable(event)},isEventResizable:function(event){var source=event.source||{};return firstDefined(event.durationEditable,source.durationEditable,this.opt("eventDurationEditable"),event.editable,source.editable,this.opt("editable"))},reportEventResize:function(event,resizeLocation,largeUnit,el,ev){var calendar=this.calendar;var mutateResult=calendar.mutateEvent(event,resizeLocation,largeUnit);var undoFunc=function(){mutateResult.undo();calendar.reportEventChange()};this.triggerEventResize(event,mutateResult.durationDelta,undoFunc,el,ev);calendar.reportEventChange()},triggerEventResize:function(event,durationDelta,undoFunc,el,ev){this.trigger("eventResize",el[0],event,durationDelta,undoFunc,ev,{})},select:function(span,ev){this.unselect(ev);this.renderSelection(span);this.reportSelection(span,ev)},renderSelection:function(span){},reportSelection:function(span,ev){this.isSelected=true;this.triggerSelect(span,ev)},triggerSelect:function(span,ev){this.trigger("select",null,this.calendar.applyTimezone(span.start),this.calendar.applyTimezone(span.end),ev)},unselect:function(ev){if(this.isSelected){this.isSelected=false;if(this.destroySelection){this.destroySelection()}this.unrenderSelection();this.trigger("unselect",null,ev)}},unrenderSelection:function(){},selectEvent:function(event){if(!this.selectedEvent||this.selectedEvent!==event){this.unselectEvent();this.renderedEventSegEach(function(seg){seg.el.addClass("fc-selected")},event);this.selectedEvent=event}},unselectEvent:function(){if(this.selectedEvent){this.renderedEventSegEach(function(seg){seg.el.removeClass("fc-selected")},this.selectedEvent);this.selectedEvent=null}},isEventSelected:function(event){return this.selectedEvent&&this.selectedEvent._id===event._id},handleDocumentMousedown:function(ev){if(isPrimaryMouseButton(ev)){this.processUnselect(ev)}},processUnselect:function(ev){this.processRangeUnselect(ev);this.processEventUnselect(ev)},processRangeUnselect:function(ev){var ignore;if(this.isSelected&&this.opt("unselectAuto")){ignore=this.opt("unselectCancel");if(!ignore||!$(ev.target).closest(ignore).length){this.unselect(ev)}}},processEventUnselect:function(ev){if(this.selectedEvent){if(!$(ev.target).closest(".fc-selected").length){this.unselectEvent()}}},triggerDayClick:function(span,dayEl,ev){this.trigger("dayClick",dayEl,this.calendar.applyTimezone(span.start),ev)},initHiddenDays:function(){var hiddenDays=this.opt("hiddenDays")||[];var isHiddenDayHash=[];var dayCnt=0;var i;if(this.opt("weekends")===false){hiddenDays.push(0,6)}for(i=0;i<7;i++){if(!(isHiddenDayHash[i]=$.inArray(i,hiddenDays)!==-1)){dayCnt++}}if(!dayCnt){throw"invalid hiddenDays"}this.isHiddenDayHash=isHiddenDayHash},isHiddenDay:function(day){if(moment.isMoment(day)){day=day.day()}return this.isHiddenDayHash[day]},skipHiddenDays:function(date,inc,isExclusive){var out=date.clone();inc=inc||1;while(this.isHiddenDayHash[(out.day()+(isExclusive?inc:0)+7)%7]){out.add(inc,"days")}return out},computeDayRange:function(range){var startDay=range.start.clone().stripTime();var end=range.end;var endDay=null;var endTimeMS;if(end){endDay=end.clone().stripTime();endTimeMS=+end.time();if(endTimeMS&&endTimeMS>=this.nextDayThreshold){endDay.add(1,"days")}}if(!end||endDay<=startDay){endDay=startDay.clone().add(1,"days")}return{start:startDay,end:endDay}},isMultiDayEvent:function(event){var range=this.computeDayRange(event);return range.end.diff(range.start,"days")>1}});var Scroller=FC.Scroller=Class.extend({el:null,scrollEl:null,overflowX:null,overflowY:null,constructor:function(options){options=options||{};this.overflowX=options.overflowX||options.overflow||"auto";this.overflowY=options.overflowY||options.overflow||"auto"},render:function(){this.el=this.renderEl();this.applyOverflow()},renderEl:function(){return this.scrollEl=$('<div class="fc-scroller"></div>')},clear:function(){this.setHeight("auto");this.applyOverflow()},destroy:function(){this.el.remove()},applyOverflow:function(){this.scrollEl.css({"overflow-x":this.overflowX,"overflow-y":this.overflowY})},lockOverflow:function(scrollbarWidths){var overflowX=this.overflowX;var overflowY=this.overflowY;scrollbarWidths=scrollbarWidths||this.getScrollbarWidths();if(overflowX==="auto"){overflowX=scrollbarWidths.top||scrollbarWidths.bottom||this.scrollEl[0].scrollWidth-1>this.scrollEl[0].clientWidth?"scroll":"hidden"}if(overflowY==="auto"){overflowY=scrollbarWidths.left||scrollbarWidths.right||this.scrollEl[0].scrollHeight-1>this.scrollEl[0].clientHeight?"scroll":"hidden"}this.scrollEl.css({"overflow-x":overflowX,"overflow-y":overflowY})},setHeight:function(height){this.scrollEl.height(height)},getScrollTop:function(){return this.scrollEl.scrollTop()},setScrollTop:function(top){this.scrollEl.scrollTop(top)},getClientWidth:function(){return this.scrollEl[0].clientWidth},getClientHeight:function(){return this.scrollEl[0].clientHeight},getScrollbarWidths:function(){return getScrollbarWidths(this.scrollEl)}});var Calendar=FC.Calendar=Class.extend({dirDefaults:null,langDefaults:null,overrides:null,options:null,viewSpecCache:null,view:null,header:null,loadingLevel:0,constructor:Calendar_constructor,initialize:function(){},initOptions:function(overrides){var lang,langDefaults;var isRTL,dirDefaults;overrides=massageOverrides(overrides);lang=overrides.lang;langDefaults=langOptionHash[lang];if(!langDefaults){lang=Calendar.defaults.lang;langDefaults=langOptionHash[lang]||{}}isRTL=firstDefined(overrides.isRTL,langDefaults.isRTL,Calendar.defaults.isRTL);dirDefaults=isRTL?Calendar.rtlDefaults:{};this.dirDefaults=dirDefaults;this.langDefaults=langDefaults;this.overrides=overrides;this.options=mergeOptions([Calendar.defaults,dirDefaults,langDefaults,overrides]);populateInstanceComputableOptions(this.options);this.viewSpecCache={}},getViewSpec:function(viewType){var cache=this.viewSpecCache;return cache[viewType]||(cache[viewType]=this.buildViewSpec(viewType))},getUnitViewSpec:function(unit){var viewTypes;var i;var spec;if($.inArray(unit,intervalUnits)!=-1){viewTypes=this.header.getViewsWithButtons();$.each(FC.views,function(viewType){viewTypes.push(viewType)});for(i=0;i<viewTypes.length;i++){spec=this.getViewSpec(viewTypes[i]);if(spec){if(spec.singleUnit==unit){return spec}}}}},buildViewSpec:function(requestedViewType){var viewOverrides=this.overrides.views||{};var specChain=[];var defaultsChain=[];var overridesChain=[];var viewType=requestedViewType;var spec;var overrides;var duration;var unit;while(viewType){spec=fcViews[viewType];overrides=viewOverrides[viewType];viewType=null;if(typeof spec==="function"){spec={class:spec}}if(spec){specChain.unshift(spec);defaultsChain.unshift(spec.defaults||{});duration=duration||spec.duration;viewType=viewType||spec.type}if(overrides){overridesChain.unshift(overrides);duration=duration||overrides.duration;viewType=viewType||overrides.type}}spec=mergeProps(specChain);spec.type=requestedViewType;if(!spec["class"]){return false}if(duration){duration=moment.duration(duration);if(duration.valueOf()){spec.duration=duration;unit=computeIntervalUnit(duration);if(duration.as(unit)===1){spec.singleUnit=unit;overridesChain.unshift(viewOverrides[unit]||{})}}}spec.defaults=mergeOptions(defaultsChain);spec.overrides=mergeOptions(overridesChain);this.buildViewSpecOptions(spec);this.buildViewSpecButtonText(spec,requestedViewType);return spec},buildViewSpecOptions:function(spec){spec.options=mergeOptions([Calendar.defaults,spec.defaults,this.dirDefaults,this.langDefaults,this.overrides,spec.overrides]);populateInstanceComputableOptions(spec.options)},buildViewSpecButtonText:function(spec,requestedViewType){function queryButtonText(options){var buttonText=options.buttonText||{};return buttonText[requestedViewType]||(spec.singleUnit?buttonText[spec.singleUnit]:null)}spec.buttonTextOverride=queryButtonText(this.overrides)||spec.overrides.buttonText;spec.buttonTextDefault=queryButtonText(this.langDefaults)||queryButtonText(this.dirDefaults)||spec.defaults.buttonText||queryButtonText(Calendar.defaults)||(spec.duration?this.humanizeDuration(spec.duration):null)||requestedViewType},instantiateView:function(viewType){var spec=this.getViewSpec(viewType);return new spec["class"](this,viewType,spec.options,spec.duration)},isValidViewType:function(viewType){return Boolean(this.getViewSpec(viewType))},pushLoading:function(){if(!this.loadingLevel++){this.trigger("loading",null,true,this.view)}},popLoading:function(){if(!--this.loadingLevel){this.trigger("loading",null,false,this.view)}},buildSelectSpan:function(zonedStartInput,zonedEndInput){var start=this.moment(zonedStartInput).stripZone();var end;if(zonedEndInput){end=this.moment(zonedEndInput).stripZone()}else if(start.hasTime()){end=start.clone().add(this.defaultTimedEventDuration)}else{end=start.clone().add(this.defaultAllDayEventDuration)}return{start:start,end:end}}});Calendar.mixin(EmitterMixin);function Calendar_constructor(element,overrides){var t=this;t.initOptions(overrides||{});var options=this.options;t.render=render;t.destroy=destroy;t.refetchEvents=refetchEvents;t.reportEvents=reportEvents;t.reportEventChange=reportEventChange;t.rerenderEvents=renderEvents;t.changeView=renderView;t.select=select;t.unselect=unselect;t.prev=prev;t.next=next;t.prevYear=prevYear;t.nextYear=nextYear;t.today=today;t.gotoDate=gotoDate;t.incrementDate=incrementDate;t.zoomTo=zoomTo;t.getDate=getDate;t.getCalendar=getCalendar;t.getView=getView;t.option=option;t.trigger=trigger;var localeData=createObject(getMomentLocaleData(options.lang));if(options.monthNames){localeData._months=options.monthNames}if(options.monthNamesShort){localeData._monthsShort=options.monthNamesShort}if(options.dayNames){localeData._weekdays=options.dayNames}if(options.dayNamesShort){localeData._weekdaysShort=options.dayNamesShort}if(options.firstDay!=null){var _week=createObject(localeData._week);_week.dow=options.firstDay;localeData._week=_week}localeData._fullCalendar_weekCalc=function(weekCalc){if(typeof weekCalc==="function"){return weekCalc}else if(weekCalc==="local"){return weekCalc}else if(weekCalc==="iso"||weekCalc==="ISO"){return"ISO"}}(options.weekNumberCalculation);t.defaultAllDayEventDuration=moment.duration(options.defaultAllDayEventDuration);t.defaultTimedEventDuration=moment.duration(options.defaultTimedEventDuration);t.moment=function(){var mom;if(options.timezone==="local"){mom=FC.moment.apply(null,arguments);if(mom.hasTime()){mom.local()}}else if(options.timezone==="UTC"){mom=FC.moment.utc.apply(null,arguments)}else{mom=FC.moment.parseZone.apply(null,arguments)}if("_locale"in mom){mom._locale=localeData}else{mom._lang=localeData}return mom};t.getIsAmbigTimezone=function(){return options.timezone!=="local"&&options.timezone!=="UTC"};t.applyTimezone=function(date){if(!date.hasTime()){return date.clone()}var zonedDate=t.moment(date.toArray());var timeAdjust=date.time()-zonedDate.time();var adjustedZonedDate;if(timeAdjust){adjustedZonedDate=zonedDate.clone().add(timeAdjust);if(date.time()-adjustedZonedDate.time()===0){zonedDate=adjustedZonedDate}}return zonedDate};t.getNow=function(){var now=options.now;if(typeof now==="function"){now=now()}return t.moment(now).stripZone()};t.getEventEnd=function(event){if(event.end){return event.end.clone()}else{return t.getDefaultEventEnd(event.allDay,event.start)}};t.getDefaultEventEnd=function(allDay,zonedStart){var end=zonedStart.clone();if(allDay){end.stripTime().add(t.defaultAllDayEventDuration)}else{end.add(t.defaultTimedEventDuration)}if(t.getIsAmbigTimezone()){end.stripZone()}return end};t.humanizeDuration=function(duration){return(duration.locale||duration.lang).call(duration,options.lang).humanize()};EventManager.call(t,options);var isFetchNeeded=t.isFetchNeeded;var fetchEvents=t.fetchEvents;var _element=element[0];var header;var headerElement;var content;var tm;var currentView;var viewsByType={};var suggestedViewHeight;var windowResizeProxy;var ignoreWindowResize=0;var events=[];var date;if(options.defaultDate!=null){date=t.moment(options.defaultDate).stripZone()}else{date=t.getNow()}function render(){if(!content){initialRender()}else if(elementVisible()){calcSize();renderView()}}function initialRender(){tm=options.theme?"ui":"fc";element.addClass("fc");if(options.isRTL){element.addClass("fc-rtl")}else{element.addClass("fc-ltr")}if(options.theme){element.addClass("ui-widget")}else{element.addClass("fc-unthemed")}content=$("<div class='fc-view-container'/>").prependTo(element);header=t.header=new Header(t,options);headerElement=header.render();if(headerElement){element.prepend(headerElement)}renderView(options.defaultView);if(options.handleWindowResize){windowResizeProxy=debounce(windowResize,options.windowResizeDelay);$(window).resize(windowResizeProxy)}}function destroy(){if(currentView){currentView.removeElement()}header.removeElement();content.remove();element.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget");if(windowResizeProxy){$(window).unbind("resize",windowResizeProxy)}}function elementVisible(){return element.is(":visible")}function renderView(viewType){ignoreWindowResize++;if(currentView&&viewType&&currentView.type!==viewType){header.deactivateButton(currentView.type);freezeContentHeight();currentView.removeElement();currentView=t.view=null}if(!currentView&&viewType){currentView=t.view=viewsByType[viewType]||(viewsByType[viewType]=t.instantiateView(viewType));currentView.setElement($("<div class='fc-view fc-"+viewType+"-view' />").appendTo(content));header.activateButton(viewType)}if(currentView){date=currentView.massageCurrentDate(date);if(!currentView.displaying||!date.isWithin(currentView.intervalStart,currentView.intervalEnd)){if(elementVisible()){currentView.display(date);unfreezeContentHeight();updateHeaderTitle();updateTodayButton();getAndRenderEvents()}}}unfreezeContentHeight();ignoreWindowResize--}t.getSuggestedViewHeight=function(){if(suggestedViewHeight===undefined){calcSize()}return suggestedViewHeight};t.isHeightAuto=function(){return options.contentHeight==="auto"||options.height==="auto"};function updateSize(shouldRecalc){if(elementVisible()){if(shouldRecalc){_calcSize()}ignoreWindowResize++;currentView.updateSize(true);ignoreWindowResize--;return true}}function calcSize(){if(elementVisible()){_calcSize()}}function _calcSize(){if(typeof options.contentHeight==="number"){suggestedViewHeight=options.contentHeight}else if(typeof options.height==="number"){suggestedViewHeight=options.height-(headerElement?headerElement.outerHeight(true):0)}else{suggestedViewHeight=Math.round(content.width()/Math.max(options.aspectRatio,.5))}}function windowResize(ev){if(!ignoreWindowResize&&ev.target===window&&currentView.start){if(updateSize(true)){currentView.trigger("windowResize",_element)}}}function refetchEvents(){fetchAndRenderEvents()}function renderEvents(){if(elementVisible()){freezeContentHeight();currentView.displayEvents(events);unfreezeContentHeight()}}function destroyEvents(){freezeContentHeight();currentView.clearEvents();unfreezeContentHeight()}function getAndRenderEvents(){if(!options.lazyFetching||isFetchNeeded(currentView.start,currentView.end)){fetchAndRenderEvents()}else{renderEvents()}}function fetchAndRenderEvents(){fetchEvents(currentView.start,currentView.end)}function reportEvents(_events){events=_events;renderEvents()}function reportEventChange(){renderEvents()}function updateHeaderTitle(){header.updateTitle(currentView.title)}function updateTodayButton(){var now=t.getNow();if(now.isWithin(currentView.intervalStart,currentView.intervalEnd)){header.disableButton("today")}else{header.enableButton("today")}}function select(zonedStartInput,zonedEndInput){currentView.select(t.buildSelectSpan.apply(t,arguments))}function unselect(){if(currentView){currentView.unselect()}}function prev(){date=currentView.computePrevDate(date);renderView()}function next(){date=currentView.computeNextDate(date);renderView()}function prevYear(){date.add(-1,"years");renderView()}function nextYear(){date.add(1,"years");renderView()}function today(){date=t.getNow();renderView()}function gotoDate(zonedDateInput){date=t.moment(zonedDateInput).stripZone();renderView()}function incrementDate(delta){date.add(moment.duration(delta));renderView()}function zoomTo(newDate,viewType){var spec;viewType=viewType||"day";spec=t.getViewSpec(viewType)||t.getUnitViewSpec(viewType);date=newDate.clone();renderView(spec?spec.type:null)}function getDate(){return t.applyTimezone(date)}t.freezeContentHeight=freezeContentHeight;t.unfreezeContentHeight=unfreezeContentHeight;function freezeContentHeight(){content.css({width:"100%",height:content.height(),overflow:"hidden"})}function unfreezeContentHeight(){content.css({width:"",height:"",overflow:""})}function getCalendar(){return t}function getView(){return currentView}function option(name,value){if(value===undefined){return options[name]}if(name=="height"||name=="contentHeight"||name=="aspectRatio"){options[name]=value;updateSize(true)}}function trigger(name,thisObj){var args=Array.prototype.slice.call(arguments,2);thisObj=thisObj||_element;this.triggerWith(name,thisObj,args);if(options[name]){return options[name].apply(thisObj,args)}}t.initialize()}Calendar.defaults={titleRangeSeparator:" — ",monthYearFormat:"MMMM YYYY",defaultTimedEventDuration:"02:00:00",defaultAllDayEventDuration:{days:1},forceEventDuration:false,nextDayThreshold:"09:00:00",defaultView:"month",aspectRatio:1.35,header:{left:"title",center:"",right:"today prev,next"},weekends:true,weekNumbers:false,weekNumberTitle:"W",weekNumberCalculation:"local",scrollTime:"06:00:00",lazyFetching:true,startParam:"start",endParam:"end",timezoneParam:"timezone",timezone:false,isRTL:false,buttonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",year:"year",today:"today",month:"month",week:"week",day:"day"},buttonIcons:{prev:"left-single-arrow",next:"right-single-arrow",prevYear:"left-double-arrow",nextYear:"right-double-arrow"},theme:false,themeButtonIcons:{prev:"circle-triangle-w",next:"circle-triangle-e",prevYear:"seek-prev",nextYear:"seek-next"},dragOpacity:.75,dragRevertDuration:500,dragScroll:true,unselectAuto:true,dropAccept:"*",eventOrder:"title",eventLimit:false,eventLimitText:"more",eventLimitClick:"popover",dayPopoverFormat:"LL",handleWindowResize:true,windowResizeDelay:200,longPressDelay:1e3};Calendar.englishDefaults={dayPopoverFormat:"dddd, MMMM D"};Calendar.rtlDefaults={header:{left:"next,prev today",center:"",right:"title"},buttonIcons:{prev:"right-single-arrow",next:"left-single-arrow",prevYear:"right-double-arrow",nextYear:"left-double-arrow"},themeButtonIcons:{prev:"circle-triangle-e",next:"circle-triangle-w",nextYear:"seek-prev",prevYear:"seek-next"}};var langOptionHash=FC.langs={};FC.datepickerLang=function(langCode,dpLangCode,dpOptions){var fcOptions=langOptionHash[langCode]||(langOptionHash[langCode]={});fcOptions.isRTL=dpOptions.isRTL;fcOptions.weekNumberTitle=dpOptions.weekHeader;$.each(dpComputableOptions,function(name,func){fcOptions[name]=func(dpOptions)});if($.datepicker){$.datepicker.regional[dpLangCode]=$.datepicker.regional[langCode]=dpOptions;$.datepicker.regional.en=$.datepicker.regional[""];$.datepicker.setDefaults(dpOptions)}};FC.lang=function(langCode,newFcOptions){var fcOptions;var momOptions;fcOptions=langOptionHash[langCode]||(langOptionHash[langCode]={});if(newFcOptions){fcOptions=langOptionHash[langCode]=mergeOptions([fcOptions,newFcOptions])}momOptions=getMomentLocaleData(langCode);$.each(momComputableOptions,function(name,func){if(fcOptions[name]==null){fcOptions[name]=func(momOptions,fcOptions)}});Calendar.defaults.lang=langCode};var dpComputableOptions={buttonText:function(dpOptions){return{prev:stripHtmlEntities(dpOptions.prevText),next:stripHtmlEntities(dpOptions.nextText),today:stripHtmlEntities(dpOptions.currentText)}},monthYearFormat:function(dpOptions){return dpOptions.showMonthAfterYear?"YYYY["+dpOptions.yearSuffix+"] MMMM":"MMMM YYYY["+dpOptions.yearSuffix+"]"}};var momComputableOptions={dayOfMonthFormat:function(momOptions,fcOptions){var format=momOptions.longDateFormat("l");format=format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g,"");if(fcOptions.isRTL){format+=" ddd"}else{format="ddd "+format}return format},mediumTimeFormat:function(momOptions){return momOptions.longDateFormat("LT").replace(/\s*a$/i,"a")},smallTimeFormat:function(momOptions){return momOptions.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"a")},extraSmallTimeFormat:function(momOptions){return momOptions.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"t")},hourFormat:function(momOptions){return momOptions.longDateFormat("LT").replace(":mm","").replace(/(\Wmm)$/,"").replace(/\s*a$/i,"a")},noMeridiemTimeFormat:function(momOptions){return momOptions.longDateFormat("LT").replace(/\s*a$/i,"")}};var instanceComputableOptions={smallDayDateFormat:function(options){return options.isRTL?"D dd":"dd D"},weekFormat:function(options){return options.isRTL?"w[ "+options.weekNumberTitle+"]":"["+options.weekNumberTitle+" ]w"},smallWeekFormat:function(options){return options.isRTL?"w["+options.weekNumberTitle+"]":"["+options.weekNumberTitle+"]w"}};function populateInstanceComputableOptions(options){$.each(instanceComputableOptions,function(name,func){if(options[name]==null){options[name]=func(options)}})}function getMomentLocaleData(langCode){var func=moment.localeData||moment.langData;return func.call(moment,langCode)||func.call(moment,"en")}FC.lang("en",Calendar.englishDefaults);function Header(calendar,options){var t=this;t.render=render;t.removeElement=removeElement;t.updateTitle=updateTitle;t.activateButton=activateButton;t.deactivateButton=deactivateButton;t.disableButton=disableButton;t.enableButton=enableButton;t.getViewsWithButtons=getViewsWithButtons;var el=$();var viewsWithButtons=[];var tm;function render(){var sections=options.header;tm=options.theme?"ui":"fc";if(sections){el=$("<div class='fc-toolbar'/>").append(renderSection("left")).append(renderSection("right")).append(renderSection("center")).append('<div class="fc-clear"/>');return el}}function removeElement(){el.remove();el=$()}function renderSection(position){var sectionEl=$('<div class="fc-'+position+'"/>');var buttonStr=options.header[position];if(buttonStr){$.each(buttonStr.split(" "),function(i){var groupChildren=$();var isOnlyButtons=true;var groupEl;$.each(this.split(","),function(j,buttonName){var customButtonProps;var viewSpec;var buttonClick;var overrideText;var defaultText;var themeIcon;var normalIcon;var innerHtml;var classes;var button;if(buttonName=="title"){groupChildren=groupChildren.add($("<h2>&nbsp;</h2>"));isOnlyButtons=false}else{if(customButtonProps=(calendar.options.customButtons||{})[buttonName]){buttonClick=function(ev){if(customButtonProps.click){customButtonProps.click.call(button[0],ev)}};overrideText="";defaultText=customButtonProps.text}else if(viewSpec=calendar.getViewSpec(buttonName)){buttonClick=function(){calendar.changeView(buttonName)};viewsWithButtons.push(buttonName);overrideText=viewSpec.buttonTextOverride;defaultText=viewSpec.buttonTextDefault}else if(calendar[buttonName]){buttonClick=function(){calendar[buttonName]()};overrideText=(calendar.overrides.buttonText||{})[buttonName];defaultText=options.buttonText[buttonName]}if(buttonClick){themeIcon=customButtonProps?customButtonProps.themeIcon:options.themeButtonIcons[buttonName];normalIcon=customButtonProps?customButtonProps.icon:options.buttonIcons[buttonName];if(overrideText){innerHtml=htmlEscape(overrideText)}else if(themeIcon&&options.theme){innerHtml="<span class='ui-icon ui-icon-"+themeIcon+"'></span>"}else if(normalIcon&&!options.theme){innerHtml="<span class='fc-icon fc-icon-"+normalIcon+"'></span>"}else{innerHtml=htmlEscape(defaultText)}classes=["fc-"+buttonName+"-button",tm+"-button",tm+"-state-default"];button=$('<button type="button" class="'+classes.join(" ")+'">'+innerHtml+"</button>").click(function(ev){if(!button.hasClass(tm+"-state-disabled")){buttonClick(ev);if(button.hasClass(tm+"-state-active")||button.hasClass(tm+"-state-disabled")){button.removeClass(tm+"-state-hover")}}}).mousedown(function(){button.not("."+tm+"-state-active").not("."+tm+"-state-disabled").addClass(tm+"-state-down")}).mouseup(function(){button.removeClass(tm+"-state-down")}).hover(function(){button.not("."+tm+"-state-active").not("."+tm+"-state-disabled").addClass(tm+"-state-hover")},function(){button.removeClass(tm+"-state-hover").removeClass(tm+"-state-down")});groupChildren=groupChildren.add(button)}}});if(isOnlyButtons){groupChildren.first().addClass(tm+"-corner-left").end().last().addClass(tm+"-corner-right").end()}if(groupChildren.length>1){groupEl=$("<div/>");if(isOnlyButtons){groupEl.addClass("fc-button-group")}groupEl.append(groupChildren);sectionEl.append(groupEl)}else{sectionEl.append(groupChildren)}})}return sectionEl}function updateTitle(text){el.find("h2").text(text)}function activateButton(buttonName){el.find(".fc-"+buttonName+"-button").addClass(tm+"-state-active")}function deactivateButton(buttonName){el.find(".fc-"+buttonName+"-button").removeClass(tm+"-state-active")}function disableButton(buttonName){el.find(".fc-"+buttonName+"-button").attr("disabled","disabled").addClass(tm+"-state-disabled")}function enableButton(buttonName){el.find(".fc-"+buttonName+"-button").removeAttr("disabled").removeClass(tm+"-state-disabled")}function getViewsWithButtons(){return viewsWithButtons}}FC.sourceNormalizers=[];FC.sourceFetchers=[];var ajaxDefaults={dataType:"json",cache:false};var eventGUID=1;function EventManager(options){var t=this;t.isFetchNeeded=isFetchNeeded;t.fetchEvents=fetchEvents;t.addEventSource=addEventSource;t.addEventSourceList=addEventSourceList;t.removeEventSource=removeEventSource;t.updateEvent=updateEvent;t.renderEvent=renderEvent;t.removeEvents=removeEvents;t.clientEvents=clientEvents;t.mutateEvent=mutateEvent;t.normalizeEventDates=normalizeEventDates;t.normalizeEventTimes=normalizeEventTimes;var reportEvents=t.reportEvents;var stickySource={events:[]};var sources=[stickySource];var rangeStart,rangeEnd;var currentFetchID=0;var pendingSourceCnt=0;var cache=[];$.each((options.events?[options.events]:[]).concat(options.eventSources||[]),function(i,sourceInput){var source=buildEventSource(sourceInput);if(source){sources.push(source)}});function isFetchNeeded(start,end){return!rangeStart||start<rangeStart||end>rangeEnd}function fetchEvents(start,end){rangeStart=start;rangeEnd=end;cache=[];var fetchID=++currentFetchID;var len=sources.length;pendingSourceCnt=len;for(var i=0;i<len;i++){fetchEventSource(sources[i],fetchID)}}function fetchEventSource(source,fetchID){_fetchEventSource(source,function(eventInputs){var isArraySource=$.isArray(source.events);var i,eventInput;var abstractEvent;if(fetchID==currentFetchID){if(eventInputs){for(i=0;i<eventInputs.length;i++){eventInput=eventInputs[i];if(isArraySource){abstractEvent=eventInput}else{abstractEvent=buildEventFromInput(eventInput,source)}if(abstractEvent){cache.push.apply(cache,expandEvent(abstractEvent))}}}pendingSourceCnt--;if(!pendingSourceCnt){reportEvents(cache)}}})}function _fetchEventSource(source,callback){var i;var fetchers=FC.sourceFetchers;var res;for(i=0;i<fetchers.length;i++){res=fetchers[i].call(t,source,rangeStart.clone(),rangeEnd.clone(),options.timezone,callback);if(res===true){return}else if(typeof res=="object"){_fetchEventSource(res,callback);return}}var events=source.events;if(events){if($.isFunction(events)){t.pushLoading();events.call(t,rangeStart.clone(),rangeEnd.clone(),options.timezone,function(events){callback(events);t.popLoading()})}else if($.isArray(events)){callback(events)}else{callback()}}else{var url=source.url;if(url){var success=source.success;var error=source.error;var complete=source.complete;var customData;if($.isFunction(source.data)){customData=source.data()}else{customData=source.data}var data=$.extend({},customData||{});var startParam=firstDefined(source.startParam,options.startParam);var endParam=firstDefined(source.endParam,options.endParam);var timezoneParam=firstDefined(source.timezoneParam,options.timezoneParam);if(startParam){data[startParam]=rangeStart.format()}if(endParam){data[endParam]=rangeEnd.format()}if(options.timezone&&options.timezone!="local"){data[timezoneParam]=options.timezone}t.pushLoading();$.ajax($.extend({},ajaxDefaults,source,{data:data,success:function(events){events=events||[];var res=applyAll(success,this,arguments);if($.isArray(res)){events=res}callback(events)},error:function(){applyAll(error,this,arguments);callback()},complete:function(){applyAll(complete,this,arguments);t.popLoading()}}))}else{callback()}}}function addEventSource(sourceInput){var source=buildEventSource(sourceInput);if(source){sources.push(source);pendingSourceCnt++;fetchEventSource(source,currentFetchID)}}function addEventSourceList(sourceList){if(sourceList&&$.isArray(sourceList)){for(const sourceInput of sourceList){let source=buildEventSource(sourceInput);if(source){sources.push(source)}}this.refetchEvents()}}function buildEventSource(sourceInput){var normalizers=FC.sourceNormalizers;var source;var i;if($.isFunction(sourceInput)||$.isArray(sourceInput)){source={events:sourceInput}}else if(typeof sourceInput==="string"){source={url:sourceInput}}else if(typeof sourceInput==="object"){source=$.extend({},sourceInput)}if(source){if(source.className){if(typeof source.className==="string"){source.className=source.className.split(/\s+/)}}else{source.className=[]}if($.isArray(source.events)){source.origArray=source.events;source.events=$.map(source.events,function(eventInput){return buildEventFromInput(eventInput,source)})}for(i=0;i<normalizers.length;i++){normalizers[i].call(t,source)}return source}}function removeEventSource(source){sources=$.grep(sources,function(src){return!isSourcesEqual(src,source)});cache=$.grep(cache,function(e){return!isSourcesEqual(e.source,source)});reportEvents(cache)}function isSourcesEqual(source1,source2){return source1&&source2&&getSourcePrimitive(source1)==getSourcePrimitive(source2)}function getSourcePrimitive(source){return(typeof source==="object"?source.origArray||source.googleCalendarId||source.url||source.events:null)||source}function updateEvent(event){event.start=t.moment(event.start);if(event.end){event.end=t.moment(event.end)}else{event.end=null}mutateEvent(event,getMiscEventProps(event));reportEvents(cache)}function getMiscEventProps(event){var props={};$.each(event,function(name,val){if(isMiscEventPropName(name)){if(val!==undefined&&isAtomic(val)){props[name]=val}}});return props}function isMiscEventPropName(name){return!/^_|^(id|allDay|start|end)$/.test(name)}function renderEvent(eventInput,stick){var abstractEvent=buildEventFromInput(eventInput);var events;var i,event;if(abstractEvent){events=expandEvent(abstractEvent);for(i=0;i<events.length;i++){event=events[i];if(!event.source){if(stick){stickySource.events.push(event);event.source=stickySource}cache.push(event)}}reportEvents(cache);return events}return[]}function removeEvents(filter){var eventID;var i;if(filter==null){filter=function(){return true}}else if(!$.isFunction(filter)){eventID=filter+"";filter=function(event){return event._id==eventID}}cache=$.grep(cache,filter,true);for(i=0;i<sources.length;i++){if($.isArray(sources[i].events)){sources[i].events=$.grep(sources[i].events,filter,true)}}reportEvents(cache)}function clientEvents(filter){if($.isFunction(filter)){return $.grep(cache,filter)}else if(filter!=null){filter+="";return $.grep(cache,function(e){return e._id==filter})}return cache}function buildEventFromInput(input,source){var out={};var start,end;var allDay;if(options.eventDataTransform){input=options.eventDataTransform(input)}if(source&&source.eventDataTransform){input=source.eventDataTransform(input)}$.extend(out,input);if(source){out.source=source}out._id=input._id||(input.id===undefined?"_fc"+eventGUID++:input.id+"");if(input.className){if(typeof input.className=="string"){out.className=input.className.split(/\s+/)}else{out.className=input.className}}else{out.className=[]}start=input.start||input.date;end=input.end;if(isTimeString(start)){start=moment.duration(start)}if(isTimeString(end)){end=moment.duration(end)}if(input.dow||moment.isDuration(start)||moment.isDuration(end)){out.start=start?moment.duration(start):null;out.end=end?moment.duration(end):null;out._recurring=true}else{if(start){start=t.moment(start);if(!start.isValid()){return false}}if(end){end=t.moment(end);if(!end.isValid()){end=null}}allDay=input.allDay;if(allDay===undefined){allDay=firstDefined(source?source.allDayDefault:undefined,options.allDayDefault)}assignDatesToEvent(start,end,allDay,out)}t.normalizeEvent(out);return out}function assignDatesToEvent(start,end,allDay,event){event.start=start;event.end=end;event.allDay=allDay;normalizeEventDates(event);backupEventDates(event)}function normalizeEventDates(eventProps){normalizeEventTimes(eventProps);if(eventProps.end&&!eventProps.end.isAfter(eventProps.start)){eventProps.end=null}if(!eventProps.end){if(options.forceEventDuration){eventProps.end=t.getDefaultEventEnd(eventProps.allDay,eventProps.start)}else{eventProps.end=null}}}function normalizeEventTimes(eventProps){if(eventProps.allDay==null){eventProps.allDay=!(eventProps.start.hasTime()||eventProps.end&&eventProps.end.hasTime())}if(eventProps.allDay){eventProps.start.stripTime();if(eventProps.end){eventProps.end.stripTime()}}else{if(!eventProps.start.hasTime()){eventProps.start=t.applyTimezone(eventProps.start.time(0))}if(eventProps.end&&!eventProps.end.hasTime()){eventProps.end=t.applyTimezone(eventProps.end.time(0))}}}function expandEvent(abstractEvent,_rangeStart,_rangeEnd){var events=[];var dowHash;var dow;var i;var date;var startTime,endTime;var start,end;var event;_rangeStart=_rangeStart||rangeStart;_rangeEnd=_rangeEnd||rangeEnd;if(abstractEvent){if(abstractEvent._recurring){if(dow=abstractEvent.dow){dowHash={};for(i=0;i<dow.length;i++){dowHash[dow[i]]=true}}date=_rangeStart.clone().stripTime();while(date.isBefore(_rangeEnd)){if(!dowHash||dowHash[date.day()]){startTime=abstractEvent.start;endTime=abstractEvent.end;start=date.clone();end=null;if(startTime){start=start.time(startTime)}if(endTime){end=date.clone().time(endTime)}event=$.extend({},abstractEvent);assignDatesToEvent(start,end,!startTime&&!endTime,event);events.push(event)}date.add(1,"days")}}else{events.push(abstractEvent)}}return events}function mutateEvent(event,newProps,largeUnit){var miscProps={};var oldProps;var clearEnd;var startDelta;var endDelta;var durationDelta;var undoFunc;function diffDates(date1,date0){if(largeUnit){return diffByUnit(date1,date0,largeUnit)}else if(newProps.allDay){return diffDay(date1,date0)}else{return diffDayTime(date1,date0)}}newProps=newProps||{};if(!newProps.start){newProps.start=event.start.clone()}if(newProps.end===undefined){newProps.end=event.end?event.end.clone():null}if(newProps.allDay==null){newProps.allDay=event.allDay}normalizeEventDates(newProps);oldProps={start:event._start.clone(),end:event._end?event._end.clone():t.getDefaultEventEnd(event._allDay,event._start),allDay:newProps.allDay};normalizeEventDates(oldProps);clearEnd=event._end!==null&&newProps.end===null;startDelta=diffDates(newProps.start,oldProps.start);if(newProps.end){endDelta=diffDates(newProps.end,oldProps.end);durationDelta=endDelta.subtract(startDelta)}else{durationDelta=null}$.each(newProps,function(name,val){if(isMiscEventPropName(name)){if(val!==undefined){miscProps[name]=val}}});undoFunc=mutateEvents(clientEvents(event._id),clearEnd,newProps.allDay,startDelta,durationDelta,miscProps);return{dateDelta:startDelta,durationDelta:durationDelta,undo:undoFunc}}function mutateEvents(events,clearEnd,allDay,dateDelta,durationDelta,miscProps){var isAmbigTimezone=t.getIsAmbigTimezone();var undoFunctions=[];if(dateDelta&&!dateDelta.valueOf()){dateDelta=null}if(durationDelta&&!durationDelta.valueOf()){durationDelta=null}$.each(events,function(i,event){var oldProps;var newProps;oldProps={start:event.start.clone(),end:event.end?event.end.clone():null,allDay:event.allDay};$.each(miscProps,function(name){oldProps[name]=event[name]});newProps={start:event._start,end:event._end,allDay:allDay};normalizeEventDates(newProps);if(clearEnd){newProps.end=null}else if(durationDelta&&!newProps.end){newProps.end=t.getDefaultEventEnd(newProps.allDay,newProps.start)}if(dateDelta){newProps.start.add(dateDelta);if(newProps.end){newProps.end.add(dateDelta)}}if(durationDelta){newProps.end.add(durationDelta)}if(isAmbigTimezone&&!newProps.allDay&&(dateDelta||durationDelta)){newProps.start.stripZone();if(newProps.end){newProps.end.stripZone()}}$.extend(event,miscProps,newProps);backupEventDates(event);undoFunctions.push(function(){$.extend(event,oldProps);backupEventDates(event)})});return function(){for(var i=0;i<undoFunctions.length;i++){undoFunctions[i]()}}}t.getBusinessHoursEvents=getBusinessHoursEvents;function getBusinessHoursEvents(wholeDay){var optionVal=options.businessHours;var defaultVal={className:"fc-nonbusiness",start:"09:00",end:"17:00",dow:[1,2,3,4,5],rendering:"inverse-background"};var view=t.getView();var eventInput;if(optionVal){eventInput=$.extend({},defaultVal,typeof optionVal==="object"?optionVal:{})}if(eventInput){if(wholeDay){eventInput.start=null;eventInput.end=null}return expandEvent(buildEventFromInput(eventInput),view.start,view.end)}return[]}t.isEventSpanAllowed=isEventSpanAllowed;t.isExternalSpanAllowed=isExternalSpanAllowed;t.isSelectionSpanAllowed=isSelectionSpanAllowed;function isEventSpanAllowed(span,event){var source=event.source||{};var constraint=firstDefined(event.constraint,source.constraint,options.eventConstraint);var overlap=firstDefined(event.overlap,source.overlap,options.eventOverlap);return isSpanAllowed(span,constraint,overlap,event)}function isExternalSpanAllowed(eventSpan,eventLocation,eventProps){var eventInput;var event;if(eventProps){eventInput=$.extend({},eventProps,eventLocation);event=expandEvent(buildEventFromInput(eventInput))[0]}if(event){return isEventSpanAllowed(eventSpan,event)}else{return isSelectionSpanAllowed(eventSpan)}}function isSelectionSpanAllowed(span){return isSpanAllowed(span,options.selectConstraint,options.selectOverlap)}function isSpanAllowed(span,constraint,overlap,event){var constraintEvents;var anyContainment;var peerEvents;var i,peerEvent;var peerOverlap;if(constraint!=null){constraintEvents=constraintToEvents(constraint);anyContainment=false;for(i=0;i<constraintEvents.length;i++){if(eventContainsRange(constraintEvents[i],span)){anyContainment=true;break}}if(!anyContainment){return false}}peerEvents=t.getPeerEvents(span,event);for(i=0;i<peerEvents.length;i++){peerEvent=peerEvents[i];if(eventIntersectsRange(peerEvent,span)){if(overlap===false){return false}else if(typeof overlap==="function"&&!overlap(peerEvent,event)){return false}if(event){peerOverlap=firstDefined(peerEvent.overlap,(peerEvent.source||{}).overlap);if(peerOverlap===false){return false}if(typeof peerOverlap==="function"&&!peerOverlap(event,peerEvent)){return false}}}}return true}function constraintToEvents(constraintInput){if(constraintInput==="businessHours"){return getBusinessHoursEvents()}if(typeof constraintInput==="object"){return expandEvent(buildEventFromInput(constraintInput))}return clientEvents(constraintInput)}function eventContainsRange(event,range){var eventStart=event.start.clone().stripZone();var eventEnd=t.getEventEnd(event).stripZone();return range.start>=eventStart&&range.end<=eventEnd}function eventIntersectsRange(event,range){var eventStart=event.start.clone().stripZone();var eventEnd=t.getEventEnd(event).stripZone();return range.start<eventEnd&&range.end>eventStart}t.getEventCache=function(){return cache}}Calendar.prototype.normalizeEvent=function(event){};Calendar.prototype.getPeerEvents=function(span,event){var cache=this.getEventCache();var peerEvents=[];var i,otherEvent;for(i=0;i<cache.length;i++){otherEvent=cache[i];if(!event||event._id!==otherEvent._id){peerEvents.push(otherEvent)}}return peerEvents};function backupEventDates(event){event._allDay=event.allDay;event._start=event.start.clone();event._end=event.end?event.end.clone():null}var BasicView=FC.BasicView=View.extend({scroller:null,dayGridClass:DayGrid,dayGrid:null,dayNumbersVisible:false,weekNumbersVisible:false,weekNumberWidth:null,headContainerEl:null,headRowEl:null,initialize:function(){this.dayGrid=this.instantiateDayGrid();this.scroller=new Scroller({overflowX:"hidden",overflowY:"auto"})},instantiateDayGrid:function(){var subclass=this.dayGridClass.extend(basicDayGridMethods);return new subclass(this)},setRange:function(range){View.prototype.setRange.call(this,range);this.dayGrid.breakOnWeeks=/year|month|week/.test(this.intervalUnit);this.dayGrid.setRange(range)},computeRange:function(date){var range=View.prototype.computeRange.call(this,date);if(/year|month/.test(range.intervalUnit)){range.start.startOf("week");range.start=this.skipHiddenDays(range.start);if(range.end.weekday()){range.end.add(1,"week").startOf("week");range.end=this.skipHiddenDays(range.end,-1,true)}}return range},renderDates:function(){this.dayNumbersVisible=this.dayGrid.rowCnt>1;this.weekNumbersVisible=this.opt("weekNumbers");this.dayGrid.numbersVisible=this.dayNumbersVisible||this.weekNumbersVisible;this.el.addClass("fc-basic-view").html(this.renderSkeletonHtml());this.renderHead();this.scroller.render();var dayGridContainerEl=this.scroller.el.addClass("fc-day-grid-container");var dayGridEl=$('<div class="fc-day-grid" />').appendTo(dayGridContainerEl);this.el.find(".fc-body > tr > td").append(dayGridContainerEl);this.dayGrid.setElement(dayGridEl);this.dayGrid.renderDates(this.hasRigidRows())},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.dayGrid.renderHeadHtml());this.headRowEl=this.headContainerEl.find(".fc-row")},unrenderDates:function(){this.dayGrid.unrenderDates();this.dayGrid.removeElement();this.scroller.destroy()},renderBusinessHours:function(){this.dayGrid.renderBusinessHours()},renderSkeletonHtml:function(){return""+"<table>"+'<thead class="fc-head">'+"<tr>"+'<td class="fc-head-container '+this.widgetHeaderClass+'"></td>'+"</tr>"+"</thead>"+'<tbody class="fc-body">'+"<tr>"+'<td class="'+this.widgetContentClass+'"></td>'+"</tr>"+"</tbody>"+"</table>"},weekNumberStyleAttr:function(){if(this.weekNumberWidth!==null){return'style="width:'+this.weekNumberWidth+'px"'}return""},hasRigidRows:function(){var eventLimit=this.opt("eventLimit");return eventLimit&&typeof eventLimit!=="number"},updateWidth:function(){if(this.weekNumbersVisible){this.weekNumberWidth=matchCellWidths(this.el.find(".fc-week-number"))}},setHeight:function(totalHeight,isAuto){var eventLimit=this.opt("eventLimit");var scrollerHeight;var scrollbarWidths;this.scroller.clear();uncompensateScroll(this.headRowEl);this.dayGrid.removeSegPopover();if(eventLimit&&typeof eventLimit==="number"){this.dayGrid.limitRows(eventLimit)}scrollerHeight=this.computeScrollerHeight(totalHeight);this.setGridHeight(scrollerHeight,isAuto);if(eventLimit&&typeof eventLimit!=="number"){this.dayGrid.limitRows(eventLimit)}if(!isAuto){this.scroller.setHeight(scrollerHeight);scrollbarWidths=this.scroller.getScrollbarWidths();if(scrollbarWidths.left||scrollbarWidths.right){compensateScroll(this.headRowEl,scrollbarWidths);scrollerHeight=this.computeScrollerHeight(totalHeight);this.scroller.setHeight(scrollerHeight)}this.scroller.lockOverflow(scrollbarWidths)}},computeScrollerHeight:function(totalHeight){return totalHeight-subtractInnerElHeight(this.el,this.scroller.el)},setGridHeight:function(height,isAuto){if(isAuto){undistributeHeight(this.dayGrid.rowEls)}else{distributeHeight(this.dayGrid.rowEls,height,true)}},queryScroll:function(){return this.scroller.getScrollTop()},setScroll:function(top){this.scroller.setScrollTop(top)},prepareHits:function(){this.dayGrid.prepareHits()},releaseHits:function(){this.dayGrid.releaseHits()},queryHit:function(left,top){return this.dayGrid.queryHit(left,top)},getHitSpan:function(hit){return this.dayGrid.getHitSpan(hit)},getHitEl:function(hit){return this.dayGrid.getHitEl(hit)},renderEvents:function(events){this.dayGrid.renderEvents(events);this.updateHeight()},getEventSegs:function(){return this.dayGrid.getEventSegs()},unrenderEvents:function(){this.dayGrid.unrenderEvents()},renderDrag:function(dropLocation,seg){return this.dayGrid.renderDrag(dropLocation,seg)},unrenderDrag:function(){this.dayGrid.unrenderDrag()},renderSelection:function(span){this.dayGrid.renderSelection(span)},unrenderSelection:function(){this.dayGrid.unrenderSelection()}});var basicDayGridMethods={renderHeadIntroHtml:function(){var view=this.view;if(view.weekNumbersVisible){return""+'<th class="fc-week-number '+view.widgetHeaderClass+'" '+view.weekNumberStyleAttr()+">"+"<span>"+htmlEscape(view.opt("weekNumberTitle"))+"</span>"+"</th>"}return""},renderNumberIntroHtml:function(row){var view=this.view;if(view.weekNumbersVisible){return""+'<td class="fc-week-number" '+view.weekNumberStyleAttr()+">"+"<span>"+this.getCellDate(row,0).format("w")+"</span>"+"</td>"}return""},renderBgIntroHtml:function(){var view=this.view;if(view.weekNumbersVisible){return'<td class="fc-week-number '+view.widgetContentClass+'" '+view.weekNumberStyleAttr()+"></td>"}return""},renderIntroHtml:function(){var view=this.view;if(view.weekNumbersVisible){return'<td class="fc-week-number" '+view.weekNumberStyleAttr()+"></td>"}return""}};var MonthView=FC.MonthView=BasicView.extend({computeRange:function(date){var range=BasicView.prototype.computeRange.call(this,date);var rowCnt;if(this.isFixedWeeks()){rowCnt=Math.ceil(range.end.diff(range.start,"weeks",true));range.end.add(6-rowCnt,"weeks")}return range},setGridHeight:function(height,isAuto){isAuto=isAuto||this.opt("weekMode")==="variable";if(isAuto){height*=this.rowCnt/6}distributeHeight(this.dayGrid.rowEls,height,!isAuto)},isFixedWeeks:function(){var weekMode=this.opt("weekMode");if(weekMode){return weekMode==="fixed"}return this.opt("fixedWeekCount")}});fcViews.basic={class:BasicView};fcViews.basicDay={type:"basic",duration:{days:1}};fcViews.basicWeek={type:"basic",duration:{weeks:1}};fcViews.month={class:MonthView,duration:{months:1},defaults:{fixedWeekCount:true}};var AgendaView=FC.AgendaView=View.extend({scroller:null,timeGridClass:TimeGrid,timeGrid:null,dayGridClass:DayGrid,dayGrid:null,axisWidth:null,headContainerEl:null,noScrollRowEls:null,bottomRuleEl:null,initialize:function(){this.timeGrid=this.instantiateTimeGrid();if(this.opt("allDaySlot")){this.dayGrid=this.instantiateDayGrid()}this.scroller=new Scroller({overflowX:"hidden",overflowY:"auto"})},instantiateTimeGrid:function(){var subclass=this.timeGridClass.extend(agendaTimeGridMethods);return new subclass(this)},instantiateDayGrid:function(){var subclass=this.dayGridClass.extend(agendaDayGridMethods);return new subclass(this)},setRange:function(range){View.prototype.setRange.call(this,range);this.timeGrid.setRange(range);if(this.dayGrid){this.dayGrid.setRange(range)}},renderDates:function(){this.el.addClass("fc-agenda-view").html(this.renderSkeletonHtml());this.renderHead();this.scroller.render();var timeGridWrapEl=this.scroller.el.addClass("fc-time-grid-container");var timeGridEl=$('<div class="fc-time-grid" />').appendTo(timeGridWrapEl);this.el.find(".fc-body > tr > td").append(timeGridWrapEl);this.timeGrid.setElement(timeGridEl);this.timeGrid.renderDates();this.bottomRuleEl=$('<hr class="fc-divider '+this.widgetHeaderClass+'"/>').appendTo(this.timeGrid.el);if(this.dayGrid){this.dayGrid.setElement(this.el.find(".fc-day-grid"));this.dayGrid.renderDates();this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()}this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)")},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.timeGrid.renderHeadHtml())},unrenderDates:function(){this.timeGrid.unrenderDates();this.timeGrid.removeElement();if(this.dayGrid){this.dayGrid.unrenderDates();this.dayGrid.removeElement()}this.scroller.destroy()},renderSkeletonHtml:function(){return""+"<table>"+'<thead class="fc-head">'+"<tr>"+'<td class="fc-head-container '+this.widgetHeaderClass+'"></td>'+"</tr>"+"</thead>"+'<tbody class="fc-body">'+"<tr>"+'<td class="'+this.widgetContentClass+'">'+(this.dayGrid?'<div class="fc-day-grid"/>'+'<hr class="fc-divider '+this.widgetHeaderClass+'"/>':"")+"</td>"+"</tr>"+"</tbody>"+"</table>"},axisStyleAttr:function(){if(this.axisWidth!==null){return'style="width:'+this.axisWidth+'px"'}return""},renderBusinessHours:function(){this.timeGrid.renderBusinessHours();if(this.dayGrid){this.dayGrid.renderBusinessHours()}},unrenderBusinessHours:function(){this.timeGrid.unrenderBusinessHours();if(this.dayGrid){this.dayGrid.unrenderBusinessHours()}},getNowIndicatorUnit:function(){return this.timeGrid.getNowIndicatorUnit()},renderNowIndicator:function(date){this.timeGrid.renderNowIndicator(date)},unrenderNowIndicator:function(){this.timeGrid.unrenderNowIndicator()},updateSize:function(isResize){this.timeGrid.updateSize(isResize);View.prototype.updateSize.call(this,isResize)},updateWidth:function(){this.axisWidth=matchCellWidths(this.el.find(".fc-axis"))},setHeight:function(totalHeight,isAuto){var eventLimit;var scrollerHeight;var scrollbarWidths;this.bottomRuleEl.hide();this.scroller.clear();uncompensateScroll(this.noScrollRowEls);if(this.dayGrid){this.dayGrid.removeSegPopover();eventLimit=this.opt("eventLimit");if(eventLimit&&typeof eventLimit!=="number"){eventLimit=AGENDA_ALL_DAY_EVENT_LIMIT}if(eventLimit){this.dayGrid.limitRows(eventLimit)}}if(!isAuto){scrollerHeight=this.computeScrollerHeight(totalHeight);this.scroller.setHeight(scrollerHeight);scrollbarWidths=this.scroller.getScrollbarWidths();if(scrollbarWidths.left||scrollbarWidths.right){compensateScroll(this.noScrollRowEls,scrollbarWidths);scrollerHeight=this.computeScrollerHeight(totalHeight);this.scroller.setHeight(scrollerHeight)}this.scroller.lockOverflow(scrollbarWidths);if(this.timeGrid.getTotalSlatHeight()<scrollerHeight){this.bottomRuleEl.show()}}},computeScrollerHeight:function(totalHeight){return totalHeight-subtractInnerElHeight(this.el,this.scroller.el)},computeInitialScroll:function(){var scrollTime=moment.duration(this.opt("scrollTime"));var top=this.timeGrid.computeTimeTop(scrollTime);top=Math.ceil(top);if(top){top++}return top},queryScroll:function(){return this.scroller.getScrollTop()},setScroll:function(top){this.scroller.setScrollTop(top)},prepareHits:function(){this.timeGrid.prepareHits();if(this.dayGrid){this.dayGrid.prepareHits()}},releaseHits:function(){this.timeGrid.releaseHits();if(this.dayGrid){this.dayGrid.releaseHits()}},queryHit:function(left,top){var hit=this.timeGrid.queryHit(left,top);if(!hit&&this.dayGrid){hit=this.dayGrid.queryHit(left,top)}return hit},getHitSpan:function(hit){return hit.component.getHitSpan(hit)},getHitEl:function(hit){return hit.component.getHitEl(hit)},renderEvents:function(events){var dayEvents=[];var timedEvents=[];var daySegs=[];var timedSegs;var i;for(i=0;i<events.length;i++){if(events[i].allDay){dayEvents.push(events[i])}else{timedEvents.push(events[i])}}timedSegs=this.timeGrid.renderEvents(timedEvents);if(this.dayGrid){daySegs=this.dayGrid.renderEvents(dayEvents)}this.updateHeight()},getEventSegs:function(){return this.timeGrid.getEventSegs().concat(this.dayGrid?this.dayGrid.getEventSegs():[])},unrenderEvents:function(){this.timeGrid.unrenderEvents();if(this.dayGrid){this.dayGrid.unrenderEvents()}},renderDrag:function(dropLocation,seg){if(dropLocation.start.hasTime()){return this.timeGrid.renderDrag(dropLocation,seg)}else if(this.dayGrid){return this.dayGrid.renderDrag(dropLocation,seg)}},unrenderDrag:function(){this.timeGrid.unrenderDrag();if(this.dayGrid){this.dayGrid.unrenderDrag()}},renderSelection:function(span){if(span.start.hasTime()||span.end.hasTime()){this.timeGrid.renderSelection(span)}else if(this.dayGrid){this.dayGrid.renderSelection(span)}},unrenderSelection:function(){this.timeGrid.unrenderSelection();if(this.dayGrid){this.dayGrid.unrenderSelection()}}});var agendaTimeGridMethods={renderHeadIntroHtml:function(){var view=this.view;var weekText;if(view.opt("weekNumbers")){weekText=this.start.format(view.opt("smallWeekFormat"));return""+'<th class="fc-axis fc-week-number '+view.widgetHeaderClass+'" '+view.axisStyleAttr()+">"+"<span>"+htmlEscape(weekText)+"</span>"+"</th>"}else{return'<th class="fc-axis '+view.widgetHeaderClass+'" '+view.axisStyleAttr()+"></th>"}},renderBgIntroHtml:function(){var view=this.view;return'<td class="fc-axis '+view.widgetContentClass+'" '+view.axisStyleAttr()+"></td>"},renderIntroHtml:function(){var view=this.view;return'<td class="fc-axis" '+view.axisStyleAttr()+"></td>"}};var agendaDayGridMethods={renderBgIntroHtml:function(){var view=this.view;return""+'<td class="fc-axis '+view.widgetContentClass+'" '+view.axisStyleAttr()+">"+"<span>"+(view.opt("allDayHtml")||htmlEscape(view.opt("allDayText")))+"</span>"+"</td>"},renderIntroHtml:function(){var view=this.view;return'<td class="fc-axis" '+view.axisStyleAttr()+"></td>"}};var AGENDA_ALL_DAY_EVENT_LIMIT=5;var AGENDA_STOCK_SUB_DURATIONS=[{hours:1},{minutes:30},{minutes:15},{seconds:30},{seconds:15}];fcViews.agenda={class:AgendaView,defaults:{allDaySlot:true,allDayText:"all-day",slotDuration:"00:30:00",minTime:"00:00:00",maxTime:"24:00:00",slotEventOverlap:true}};fcViews.agendaDay={type:"agenda",duration:{days:1}};fcViews.agendaWeek={type:"agenda",duration:{weeks:1}};return FC});
diff --git a/NEMO/static/nemo.css b/NEMO/static/nemo.css
index e052db29..ff2617f7 100644
--- a/NEMO/static/nemo.css
+++ b/NEMO/static/nemo.css
@@ -77,9 +77,16 @@ body.impersonating .application-sidebar, body.impersonating .application-content
     padding-right: 4px;
 }
 
+#sidebar #tool_tree input[type=checkbox], #sidebar #area_tree input[type=checkbox]
+{
+	margin-right: 5px;
+	margin-top: 0;
+}
+
 #sidebar #tool_tree a, #sidebar #area_tree a
 {
 	padding-left: 0;
+	display: inline;
 }
 
 #extra-links a
@@ -1173,3 +1180,8 @@ li.area-tree-node a, li.area-tree-node a:hover, li.area-tree-node a:focus
 	display: inline-block;
 	float: right;
 }
+
+.consumable-row .tooltip-inner
+{
+	text-align: left!important;
+}
\ No newline at end of file
diff --git a/NEMO/static/nemo.js b/NEMO/static/nemo.js
index 6c578978..5277e825 100644
--- a/NEMO/static/nemo.js
+++ b/NEMO/static/nemo.js
@@ -58,6 +58,14 @@ 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()
+	{
+		$(this).click({"callback": callback}, callback);
+	});
+}
+
 // This function allows categories in the tool tree sidebar to be expanded and collapsed.
 // It must be called upon loading any page that uses the tool tree.
 function enable_item_tree_toggling()
@@ -211,6 +219,33 @@ function toggle_item_categories(item_type)
 	save_sidebar_state();
 }
 
+function get_checked_items()
+{
+	const items = [];
+	$('.item-checkbox:checkbox:checked').each(function()
+	{
+		items.push({
+			id: $(this).data('item-id'),
+			type: $(this).data('item-type'),
+		});
+	});
+	return items;
+}
+
+function set_checked_item_by_id(item_id, item_type)
+{
+	let item = $("#checkbox-" + item_type + "-" + item_id);
+	if(item.length === 1)
+	{
+		item.prop('checked', true)
+	}
+}
+
+function clear_checked_items()
+{
+	$('.item-checkbox:checkbox').removeAttr('checked');
+}
+
 function get_selected_item()
 {
 	let selected_item = $(".selected");
@@ -288,6 +323,11 @@ function save_sidebar_state()
 		localStorage['Selected item ID'] = selected_item;
 	}
 
+	let checked_items = get_checked_items();
+	if(checked_items && checked_items.length > 0) {
+		localStorage['Checked Items'] = JSON.stringify(checked_items);
+	}
+
 	if(showQualifiedTools !== null) {
 		localStorage.setItem("showQualifiedTools", showQualifiedTools)
 	}
@@ -329,6 +369,13 @@ function load_sidebar_state()
 		let selected_item = JSON.parse(selected)
 		set_selected_item_by_id(selected_item.id, selected_item.type);
 	}
+
+	let checked_items = localStorage['Checked Items'];
+	if(checked_items) {
+		JSON.parse(checked_items).forEach(function (item) {
+			set_checked_item_by_id(item.id, item.type);
+		});
+	}
 }
 
 function load_qualified_tools(user_qualifications)
@@ -697,11 +744,11 @@ function submit_and_disable(input_submit)
 	}
 }
 
-function auto_size_textarea(textarea)
+function auto_size_textarea(textarea, rows)
 {
 	if (textarea)
 	{
-		textarea.rows = 1;
+		textarea.rows = rows || 1;
 		textarea.style.height = '';
 		textarea.style.height = textarea.scrollHeight + 3 + 'px';
 	}
diff --git a/NEMO/templates/authorization_failed.html b/NEMO/templates/authorization_failed.html
index 908e2e9d..3b67f232 100644
--- a/NEMO/templates/authorization_failed.html
+++ b/NEMO/templates/authorization_failed.html
@@ -2,7 +2,7 @@
 {% load custom_tags_and_filters %}
 
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
 	<title>Authorization failed</title>
 	{# jQuery #}
diff --git a/NEMO/templates/base.html b/NEMO/templates/base.html
index 6a89203d..1eccfba0 100644
--- a/NEMO/templates/base.html
+++ b/NEMO/templates/base.html
@@ -56,9 +56,9 @@
 
 	<title>{% block title %}{% endblock %}</title>
 </head>
-<body class="{% if request.session.impersonate_id %}impersonating{% endif %}">
+<body class="{% if "impersonate_id" in request.session and request.session.impersonate_id %}impersonating{% endif %}">
 <a class="sr-only sr-only-focusable" href="#main-content">Skip to main content</a>
-{% if request.session.impersonate_id %}
+{% if "impersonate_id" in request.session and request.session.impersonate_id %}
 	{% include 'base/impersonate_header.html' %}
 {% endif %}
 {% if no_header %}
diff --git a/NEMO/templates/calendar/calendar.html b/NEMO/templates/calendar/calendar.html
index 22e1a619..e85deb07 100644
--- a/NEMO/templates/calendar/calendar.html
+++ b/NEMO/templates/calendar/calendar.html
@@ -37,17 +37,20 @@
 
 	{# 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><a href="javascript:void(0)" class="selected personal_schedule" data-item-id="personal_schedule" onclick="set_selected_item(this); update_event_sources()">Personal schedule</a></li>
+		<li><a href="javascript:void(0)" class="selected personal_schedule" data-item-id="personal_schedule" onclick="on_extra_link_click(this)">Personal schedule</a></li>
 		{% if calendar_all_tools %}
-		<li><a href="javascript:void(0)" class="all_tools" data-item-id="all_tools" onclick="set_selected_item(this); update_event_sources()">All tools</a></li>
+		<li><a href="javascript:void(0)" class="all_tools" data-item-id="all_tools" onclick="on_extra_link_click(this)">All tools</a></li>
 		{% endif %}
 		{% if calendar_all_areas %}
-		<li><a href="javascript:void(0)" class="all_areas" data-item-id="all_areas" onclick="set_selected_item(this); update_event_sources()">All areas</a></li>
+		<li><a href="javascript:void(0)" class="all_areas" data-item-id="all_areas" onclick="on_extra_link_click(this)">All areas</a></li>
 		{% endif %}
 		{% if calendar_all_areastools %}
-		<li><a href="javascript:void(0)" class="all_areastools" data-item-id="all_areastools" onclick="set_selected_item(this); update_event_sources()">All areas and tools</a></li>
+		<li><a href="javascript:void(0)" class="all_areastools" data-item-id="all_areastools" onclick="on_extra_link_click(this)">All areas and tools</a></li>
 		{% endif %}
 	</ul>
+    <button style="width: 100%" id="clear-selection-btn" class="btn btn-default" onclick="on_clear_selection_click();">
+        Clear Selection
+    </button>
 	{# The item tree has the HTML class tag "item_tree". See the widgets/item_tree.py file for more information. #}
 	{{ rendered_item_tree_html }}
 	<div style="height:50px"></div>
@@ -82,14 +85,8 @@
 	calendar_jq.fullCalendar("removeEventSource", "{% url 'event_feed' %}");
 
 	let event_type = get_event_type();
-	let item_event_source =
-	{
-		url: "{% url 'event_feed' %}",
-		data:
-		{
-			event_type: event_type
-		}
-	};
+    let item_event_source_list = [];
+    let url = "{% url 'event_feed' %}";
 
 	if(event_type === 'reservations' || event_type === '{{ facility_name|lower }} usage')
 	{
@@ -98,43 +95,82 @@
 		$(scheduled_outage_button).attr("disabled", "true").addClass('fc-state-disabled');
 		if(item === 'personal_schedule')
 		{
-			item_event_source.data.personal_schedule = true;
+            add_item_to_event_feed_list(item_event_source_list, url, event_type, { personal_schedule: true });
 		}
 		else if(item === 'all_tools')
 		{
-			item_event_source.data.all_tools = true;
+            add_item_to_event_feed_list(item_event_source_list, url, event_type, { all_tools: true });
 		}
 		else if(item === 'all_areas')
 		{
-			item_event_source.data.all_areas = true;
+            add_item_to_event_feed_list(item_event_source_list, url, event_type, { all_areas: true });
 		}
 		else if(item === 'all_areastools')
 		{
-			item_event_source.data.all_areastools = true;
-		}
-		else if(item)
-		{
-			item = JSON.parse(item)
-			item_event_source.data.item_id = item.id;
-			item_event_source.data.item_type = item.type;
-			$(scheduled_outage_button).removeAttr("disabled").removeClass('fc-state-disabled');
+            add_item_to_event_feed_list(item_event_source_list, url, event_type, { all_areastools: true });
 		}
+        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;
+            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;
+
+            if(selected_tree_item)
+		    {
+                add_item_to_event_feed_list(item_event_source_list, url, event_type, {
+                    item_id: selected_tree_item.id,
+                    item_type: selected_tree_item.type,
+                    display_name,
+                });
+                $(scheduled_outage_button).removeAttr("disabled").removeClass('fc-state-disabled');
+		    }
+
+            {# For each checked item, add item event source feed to list #}
+            checked_items.forEach(function(checked_item)
+            {
+                {# 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, {
+                        item_id: checked_item.id,
+                        item_type: checked_item.type,
+                        display_name,
+                    });
+                }
+            });
+        }
 	}
 	else if(event_type === 'specific user')
 	{
 		let user_search = $("#user_search");
 		if(user_search.typeahead('val'))
-			item_event_source.data.user = user_search.typeahead('val');
+        {
+            add_item_to_event_feed_list(item_event_source_list, url, event_type, { user: user_search.typeahead('val') });
+        }
 		else
 			return;
 	}
 
-	calendar_jq.fullCalendar("addEventSource", item_event_source);
+    {# Add event source feeds to calendar and re-fetch events #}
+    calendar_jq.fullCalendar("addEventSourceList", item_event_source_list);
 
 	refresh_sidebar_icons();
 	refresh_calendar_login();
 }
 
+{# Adds an event source feed to an event source list #}
+function add_item_to_event_feed_list(item_event_source_list, url, event_type, data)
+{
+    item_event_source_list.push({
+        url,
+        data: {
+            event_type,
+            ...data,
+        },
+    });
+}
+
 {# This function is a callback for when a reservation is successfully created by the user. #}
 {# It renders the event to the calendar by refetching the event feed. #}
 function event_creation_success_callback(response, status, xml_http_request, ajax_post, success_callback)
@@ -356,6 +392,18 @@
 	ajax_post(url, {'title': title}, refresh_calendar_and_sidebar, [failure_dialog, refresh_calendar_and_sidebar]);
 }
 
+function change_outage_title(url, title)
+{
+    let failure_dialog = ajax_failure_callback("Unable to change outage title");
+    ajax_post(url, {'title': title}, refresh_calendar_and_sidebar, [failure_dialog, refresh_calendar_and_sidebar]);
+}
+
+function change_outage_details(url, details)
+{
+    let failure_dialog = ajax_failure_callback("Unable to change outage details");
+    ajax_post(url, {'details': details}, refresh_calendar_and_sidebar, [failure_dialog, refresh_calendar_and_sidebar]);
+}
+
 function change_reservation_date(url, reservation_id, param, value)
 {
     let data = {'id': reservation_id};
@@ -370,6 +418,20 @@
     ajax_post(url, data, function(response, status, xml_http_request) { return event_creation_success_callback(response, status, xml_http_request, post_data) }, [ajax_failure_callback("Reservation date changed failed")], refresh_calendar_and_sidebar)
 }
 
+function change_outage_date(url, outage_id, param, value)
+{
+    let data = {'id': outage_id};
+    data[param] = value;
+    let post_data =
+    {
+        "url": url,
+        "data": data,
+        "type": "POST",
+        "dataType": "html"
+    };
+    ajax_post(url, data, function(response, status, xml_http_request) { return event_creation_success_callback(response, status, xml_http_request, post_data) }, [ajax_failure_callback("Outage date changed failed")], refresh_calendar_and_sidebar)
+}
+
 function change_reservation_project(url, project_id)
 {
 	let failure_dialog = ajax_failure_callback("Unable to change reservation project");
@@ -633,6 +695,7 @@
 		$(scheduled_outage_button).hide();
 		clear_specific_user();
 	}
+    update_clear_selection_btn_display();
 }
 
 function get_specific_user_activity(jquery_event, search_selection, dataset_name)
@@ -656,20 +719,68 @@
     );
 }
 
+{# Clear selection button - on click handler #}
+function on_clear_selection_click()
+{
+    clear_checked_items();
+    save_sidebar_state();
+    update_clear_selection_btn_display();
+    update_event_sources();
+}
+
+{# Item Checkbox - on click handler #}
+function on_item_checkbox_click()
+{
+    {# When the current selected item is not a tool or area, change selection to the checked item #}
+    let selected_item = get_selected_item();
+    if (selected_item === "personal_schedule" || selected_item === "all_tools" || selected_item === "all_areas" || selected_item === "all_areastools") {
+        set_selected_item_by_id($(this).data('item-id'), $(this).data('item-type'));
+    }
+
+    update_event_sources();
+    save_sidebar_state();
+    update_clear_selection_btn_display();
+}
+
+{# Extra Link - on click handler #}
+function on_extra_link_click(extra_link)
+{
+    set_selected_item(extra_link);
+    clear_checked_items();
+    save_sidebar_state();
+    update_clear_selection_btn_display();
+    update_event_sources();
+}
+
+
+{# Show / Hide Clear Selection button #}
+function update_clear_selection_btn_display()
+{
+    const event_type = get_event_type();
+    if(get_checked_items().length > 0 && event_type !== "specific user") {
+        $('#clear-selection-btn').show();
+    } else {
+        $('#clear-selection-btn').hide();
+    }
+}
+
 function on_load()
 {
     $("html").css("overflow", "hidden");
 	create_calendar();
 	set_interval_when_visible(document, refresh_calendar_and_sidebar, 30000);
 	set_item_link_callback(update_event_sources);
+    set_item_checkbox_callback(on_item_checkbox_click);
 	enable_item_tree_toggling();
 	$('#item_search').autocomplete('items', on_item_search_selection, {% json_search_base_with_extra_fields tools|add:areas %}, {% if tools and areas %}false{% else %}true{% endif %});
 	$("#item_search").focus();
 	load_sidebar_state();
+    update_clear_selection_btn_display();
     {% if calendar_qualified_tools and not user.is_staff  %}
         {# Filter qualified tools depending on the status of the button. #}
         load_qualified_tools(retrieve_user_qualifications());
     {% endif %}
+	$('input[type=checkbox].item-checkbox').show();
 	$(".item_tree").show(); {# Item tree is initially hidden so that previous expand/collapse state of categories is configured before it's visible. #}
 	{% if auto_select_item_id %}
 		expand_to_item("{{ auto_select_item_id }}", "{{ auto_select_item_type }}");
diff --git a/NEMO/templates/calendar/reservation_event_feed.html b/NEMO/templates/calendar/reservation_event_feed.html
index c8fb9bec..b1d110c4 100644
--- a/NEMO/templates/calendar/reservation_event_feed.html
+++ b/NEMO/templates/calendar/reservation_event_feed.html
@@ -2,19 +2,23 @@
 	{% for x in events %}
 	{
 		{% if personal_schedule and x.tool %}
-			"title": "{{ x.title|default:x.reservation_item.name|escapejs }}",
+			"title": "{{x.reservation_item.name|escapejs}} ({{x.user.username|escapejs}})\n{{ x.title|escapejs }}",
 			"color": "#33ad33",
 		{% elif personal_schedule and x.area %}
-			"title": "{{ x.title|default:x.reservation_item.name|escapejs }}",
+			"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 }}\n{{ x.title|escapejs }}",
+			"title": "{{x.reservation_item.name|escapejs}} ({{x.user.username|escapejs}})\n{{ x.title|escapejs }}",
 			"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 }}\n{{ x.title|escapejs }}",
+			"title": "{{x.reservation_item.name|escapejs}} ({{x.user.username|escapejs}})\n{{ x.title|escapejs }}",
 			"color": "{{ x.reservation_item.area_calendar_color|default:'#84CD84'|escapejs }}",
 		{% else %}
-			"title": "{{ x.title|default:x.user|escapejs }}",
+            {% if display_name %}
+			    "title": "{{x.reservation_item.name|escapejs}} ({{x.user.username|escapejs}})\n{{ x.title|escapejs }}",
+            {% else %}
+                "title": "{{ x.title|default:x.user|escapejs }}",
+            {% endif %}
 			"color": {% if x.user.id == user.id %}"#3a87ad"{% else %}"#88B7CD"{% endif %},
 		{% endif %}
 		{% if x.creator.id == x.user.id %}
diff --git a/NEMO/templates/calendar/usage_event_feed.html b/NEMO/templates/calendar/usage_event_feed.html
index 100fa704..8684eed7 100644
--- a/NEMO/templates/calendar/usage_event_feed.html
+++ b/NEMO/templates/calendar/usage_event_feed.html
@@ -2,9 +2,9 @@
 	{% for x in usage_events %}
 	{
 		{% if personal_schedule %}
-			"title": "{{ x.tool.name }} \n {{ x.project }}",
+			"title": "{{ x.tool.name }} ({{ x.user.username }})\n {{ x.project }}",
 		{% else %}
-			"title": "{% if x.operator.id != x.user.id %}{{ x.operator }} on behalf of {% endif %}{{ x.user }} \n {{ x.project }}",
+			"title": "{{ x.tool.name }} ({% if x.operator.id != x.user.id %}{{ x.operator }} on behalf of {% endif %}{{ x.user.username }})\n {{ x.project }}",
 		{% endif %}
 		{% if x.operator.id == x.user.id %}
 			"tooltip": "{{ x.tool.name }} usage for {{ x.user }} billed to project {{ x.project }}",
diff --git a/NEMO/templates/consumables/consumables.html b/NEMO/templates/consumables/consumables.html
index e3fb0371..d70d5e7e 100644
--- a/NEMO/templates/consumables/consumables.html
+++ b/NEMO/templates/consumables/consumables.html
@@ -53,7 +53,21 @@ <h1>Withdraw consumables</h1>
                             {% endif %}
                             {% for item in category.list %}
                                 <tr><td {% if categories|length > 1 or category.grouper %}style="padding-left: 15px;"{% endif %}>
-                                    <span class="consumable_trigger" data-item-id="{{ item.id }}">{{ item.name }}{% if rates and rates|get_item:item.name %} ({{ rates|get_item:item.name|safe }}){% endif %} <span class="glyphicon glyphicon-plus-sign success-highlight" style="vertical-align: text-top"></span></span>
+                                    <div class="consumable-row">
+                                        {% if item.notes %}<span data-toggle="notes-tooltip" title="{{ item.notes|linebreaksbr }}" class="glyphicon glyphicon-info-sign primary-highlight"></span>{% endif %}
+                                        <span class="consumable_trigger" data-item-id="{{ item.id }}">{{ item.name }}{% if rates and rates|get_item:item.name %} ({{ rates|get_item:item.name|safe }}){% endif %}
+                                            {% if not item.reusable %}
+                                                {% if item.quantity > 0 %}
+                                                    <span>({{ item.quantity }} left)</span>
+                                                {% else %}
+                                                    <span class="danger-highlight">(sold-out)</span>
+                                                {% endif %}
+                                            {% endif %}
+                                            {% if item.reusable or item.quantity > 0 %}
+                                                <span class="glyphicon glyphicon-plus-sign success-highlight" style="vertical-align: text-top"></span>
+                                            {% endif %}
+                                        </span>
+                                    </div>
                                 </td></tr>
                             {% endfor %}
                         {% endfor %}
@@ -123,6 +137,7 @@ <h1>Withdraw consumables</h1>
 		}
         window.addEventListener("load", function ()
         {
+			$('[data-toggle="notes-tooltip"]').tooltip({"html": true});
             $('#customer_search').autocomplete('users', fetch_projects, {{ users|json_search_base }}).focus();
             let withdrawal_order_form = $('#withdrawal_form_order');
             withdrawal_order_form.on('shown.bs.tooltip', function(e)
diff --git a/NEMO/templates/customizations/customizations_templates.html b/NEMO/templates/customizations/customizations_templates.html
index fd0a0e60..a9ccf7bc 100644
--- a/NEMO/templates/customizations/customizations_templates.html
+++ b/NEMO/templates/customizations/customizations_templates.html
@@ -48,7 +48,7 @@ <h3 class="customization-section-title" id="jumbotron_watermark_id">&quot;<a hre
 <div id="panel-sort">
     <div class="panel-body">
         <h3 class="customization-section-title" id="access_request_notification_email_id">Access request notification email</h3>
-        <p>This email is sent to the person who created the request when an access request is created or updated. The other users present on the request as well as the facility managers are cc'ed.</p>
+        <p>This email is sent to the person who created the request when an access request is created or updated. The other users present on the request as well as the reviewers are cc'ed.</p>
         <p>The following context variables are provided when the email is rendered:</p>
         <ul>
             <li><b>template_color</b> - the color: green if approved, red if denied, blue otherwise</li>
@@ -62,7 +62,7 @@ <h3 class="customization-section-title" id="access_request_notification_email_id
 
     <div class="panel-body">
         <h3 class="customization-section-title" id="adjustment_request_notification_email_id">Adjustment request notification email</h3>
-        <p>This email is sent to the person who created the request when an adjustment request is created or updated. Facility managers are cc'ed.</p>
+        <p>This email is sent to the person who created the request when an adjustment request is created or updated. Reviewers are cc'ed.</p>
         <p>When an adjustment request is approved it is also sent to the User Office</p>
         <p>The following context variables are provided when the email is rendered:</p>
         <ul>
diff --git a/NEMO/templates/customizations/customizations_tool.html b/NEMO/templates/customizations/customizations_tool.html
index e3e6af5a..bde278f2 100644
--- a/NEMO/templates/customizations/customizations_tool.html
+++ b/NEMO/templates/customizations/customizations_tool.html
@@ -22,16 +22,15 @@ <h3 class="customization-section-title">Tool settings</h3>
             </div>
         </div>
         <div class="form-group">
-            <label class="control-label col-sm-4 col-md-3">Tool problem updates</label>
+            <label class="control-label col-sm-4 col-md-3">Reservation policy</label>
             <div class="col-sm-8 col-md-9">
                 <div class="checkbox">
-                    <input type="hidden" name="tool_task_updates_facility_managers" value="off"/>
-                    <label><input type="checkbox" name="tool_task_updates_facility_managers" {% if tool_task_updates_facility_managers != 'off' %}checked{% endif %} value="">Send all tool problem updates to facility managers</label>
+                    <label><input type="checkbox" name="tool_reservation_policy_superusers_bypass" {% if tool_reservation_policy_superusers_bypass %}checked{% endif %} value="enabled"> Tool superusers are exempt from reservation policy rules</label>
                 </div>
             </div>
         </div>
         <div class="customization-separation" style="margin-bottom: 15px"></div>
-        <h3 class="customization-section-title">Tool control</h3>
+        <h3 class="customization-section-title">Tool configuration</h3>
         <div class="form-group {% if errors.tool_control_configuration_setting_template %}has-error{% endif %}">
             <label class="control-label col-md-3" for="tool_control_configuration_setting_template">Configuration template</label>
             <div class="col-md-7">
@@ -45,6 +44,52 @@ <h3 class="customization-section-title">Tool control</h3>
                 {% endif %}
             </div>
         </div>
+        <div class="form-group {% if errors.tool_configuration_near_future_days %}has-error{% endif %}">
+            <label class="control-label col-sm-4 col-md-3" for="tool_configuration_near_future_days">Near future</label>
+            <div class="col-md-3">
+                <div class="input-group">
+                    <input type="number" min="1" id="tool_configuration_near_future_days" name="tool_configuration_near_future_days" class="form-control text-right" value="{% if errors.tool_configuration_near_future_days %}{{ errors.tool_configuration_near_future_days.value }}{% else %}{{ tool_configuration_near_future_days }}{% endif %}"/>
+                    <span class="input-group-addon">day(s)</span>
+                </div>
+            </div>
+            <div class="col-md-offset-3 col-md-9 help-block light-grey">
+                {% if errors.tool_configuration_near_future_days %}
+                    {{ errors.tool_configuration_near_future_days.error }}
+                {% else %}
+                    The number of days for the "near future" configuration agenda (Weekend days are automatically included if the "near future" falls on the weekend).
+                {% endif %}
+            </div>
+        </div>
+        <div class="customization-separation" style="margin-bottom: 15px"></div>
+        <h3 class="customization-section-title">Tool problems</h3>
+        <div class="form-group">
+            <label class="control-label col-sm-4 col-md-3">New problems</label>
+            <div class="col-sm-8 col-md-9">
+                <div class="checkbox">
+                    <label><input type="checkbox" name="tool_problem_send_to_all_qualified_users" {% if tool_problem_send_to_all_qualified_users %}checked{% endif %} value="enabled">Send new tool problems to all qualified users</label>
+                </div>
+            </div>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-sm-4 col-md-3">Updates</label>
+            <div class="col-sm-8 col-md-9">
+                <div class="checkbox">
+                    <input type="hidden" name="tool_task_updates_facility_managers" value="off"/>
+                    <label><input type="checkbox" name="tool_task_updates_facility_managers" {% if tool_task_updates_facility_managers != 'off' %}checked{% endif %} value="">Send all tool problem updates to facility managers</label>
+                </div>
+            </div>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-sm-4 col-md-3" for="tool_problem_max_image_size_pixels">Limit task images size</label>
+            <div class="col-md-3">
+                <div class="input-group">
+                    <input type="number" min="0" id="tool_problem_max_image_size_pixels" name="tool_problem_max_image_size_pixels" class="form-control text-right" value="{% if errors.tool_problem_max_image_size_pixels %}{{ errors.tool_problem_max_image_size_pixels.value }}{% else %}{{ tool_problem_max_image_size_pixels }}{% endif %}"/>
+                    <span class="input-group-addon">pixels</span>
+                </div>
+            </div>
+        </div>
+        <div class="customization-separation" style="margin-bottom: 15px"></div>
+        <h3 class="customization-section-title">Tool control</h3>
         <div class="form-group">
             <label class="control-label col-sm-4 col-md-3">Usage data history</label>
             <div class="col-sm-8 col-md-9">
@@ -53,6 +98,14 @@ <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">Email users with upcoming reservation</label>
+            <div class="col-sm-8 col-md-9">
+                <div class="checkbox">
+                    <label><input type="checkbox" id="tool_control_broadcast_upcoming_reservation" name="tool_control_broadcast_upcoming_reservation" {% if tool_control_broadcast_upcoming_reservation %}checked{% endif %} value="enabled"> Allow qualified users on a tool to send a broadcast email to users with an upcoming reservation</label>
+                </div>
+            </div>
+        </div>
         <div class="customization-separation" style="margin-bottom: 15px"></div>
         <h3 class="customization-section-title">Tool qualification expiration</h3>
         <p>If active, this feature will remove tool qualification from a user if the user has not used the tool after a while or never used it since qualified (configured separately).
diff --git a/NEMO/templates/email/compose_email.html b/NEMO/templates/email/compose_email.html
index 8d9955eb..b3da1a81 100644
--- a/NEMO/templates/email/compose_email.html
+++ b/NEMO/templates/email/compose_email.html
@@ -7,24 +7,26 @@ <h1>Compose an email</h1>
 		{{ error }}
 	{% elif generic_email_sample %}
 		<div class="row">
-			<form action="{% url 'send_broadcast_email' %}" id="email_form" method="post">
+			<form action="{% url 'send_broadcast_email' %}" id="email_form" method="post" enctype="multipart/form-data">
 				{% csrf_token %}
-				<div class="col-sm-6">
-                    <div class="row">
-                        <div class="col-sm-6 h3">Recipients</div>
-                        <div class="col-sm-6 h3">{% if user_emails %}{% button type="export" size="small" value="Export user list" onclick="export_user_emails();" %}{% endif %}</div>
-                    </div>
-					Active users are listed below in black. Inactive users are <span class="light-grey strikethrough">crossed out</span>.
-					<div class="checkbox">
-						<label><input type="checkbox" name="only_active_users" onclick="updated_email_link()" checked> Only send this email to active users</label>
-					</div>
-					<div class="checkbox">
-						<label><input type="checkbox" name="copy_me" onclick="updated_email_link()" checked> Send a copy of this email to myself</label>
+				{% if user.is_any_part_of_staff %}
+					<div class="col-sm-6">
+						<div class="row">
+							<div class="col-sm-6 h3">Recipients</div>
+							<div class="col-sm-6 h3">{% if user_emails %}{% button type="export" size="small" value="Export user list" onclick="export_user_emails();" %}{% endif %}</div>
+						</div>
+						Active users are listed below in black. Inactive users are <span class="light-grey strikethrough">crossed out</span>.
+						<div class="checkbox">
+							<label><input type="checkbox" name="only_active_users" onclick="updated_email_link()" checked> Only send this email to active users</label>
+						</div>
+						<div class="checkbox">
+							<label><input type="checkbox" name="copy_me" onclick="updated_email_link()" checked> Send a copy of this email to myself</label>
+						</div>
+						{% for u in users %}
+							<span {% if not u.is_active %}class="light-grey strikethrough"{% endif %}>{{ u }} - {{ u.email }}</span><br>
+						{% endfor %}
 					</div>
-					{% for u in users %}
-						<span {% if not u.is_active %}class="light-grey strikethrough"{% endif %}>{{ u }} - {{ u.email }}</span><br>
-					{% endfor %}
-				</div>
+				{% endif %}
 				<div class="col-sm-6">
 					<h3>Email content</h3>
 					<input type="hidden" name="audience" value="{{ audience }}">
@@ -33,7 +35,14 @@ <h3>Email content</h3>
 					    <input type="hidden" name="selection" value="{{ selection_value }}" oninput="updated_email_link()">
                     {% endfor %}
 					<div class="form-group">
+                        {% if topic %}
+                            <div class="input-group">
+                                <span class="input-group-addon">[{{ topic }}]</span>
+                        {% endif %}
 						<input type="text" name="subject" class="form-control" aria-label="Email subject" placeholder="Subject line" oninput="updated_email_link()" autofocus required>
+                        {% if topic %}
+                            </div>
+                        {% endif %}
 					</div>
 					<div class="form-group">
 						Title background color:
@@ -71,9 +80,12 @@ <h3>Email content</h3>
 					<div class="form-group">
 						<textarea class="form-control" name="contents" aria-label="Email contents" rows="6" placeholder="What would you like to say?" oninput="updated_email_link()" required></textarea>
 					</div>
+					<div class="form-group">
+						<input name="attachments" type="file" multiple>
+					</div>
                     {% button type="view" onclick="update_preview()" value="Preview" %}
 					{% button type="save" value="Send" icon="glyphicon-send" %}
-                    {% if user_emails %}
+                    {% if user.is_any_part_of_staff and user_emails %}
                         {% if active_user_emails|length > 2000 %}
                             <div class="pull-right" style="display: inline-block" id="default_client_email_wrapper" data-title="There are too many email addresses to use the default email client. Use the export function instead.">
                                 {% button type="info" value="Use default email client" icon="glyphicon-envelope" disabled="disabled" %}
@@ -101,6 +113,7 @@ <h3>Your email will look like this...</h3>
             $('#default_client_email_wrapper').tooltip();
             updated_email_link();
         });
+        {% if user.is_any_part_of_staff %}
         function export_user_emails()
         {
             let export_url = "{% url 'export_email_addresses' %}?"
@@ -113,9 +126,13 @@ <h3>Your email will look like this...</h3>
             {% endfor %}
             window.open(export_url);
         }
+        {%  endif %}
         function updated_email_link()
         {
-            email_using_client('{{ user.email }}', '{{ user_emails }}', '{{ active_user_emails }}')
+            {# If the user is not staff member, the email link is not visible, this function will be a NO OP #}
+            {% if user.is_any_part_of_staff %}
+                email_using_client('{{ user.email }}', '{{ user_emails }}', '{{ active_user_emails }}')
+            {%  endif %}
         }
 		function update_preview()
         {
@@ -127,6 +144,7 @@ <h3>Your email will look like this...</h3>
 			    $('#email_preview').html(response);
 			}
         }
+        {% if user.is_any_part_of_staff %}
         function email_using_client(user_email, user_emails, active_user_emails)
         {
             let only_active = $('input[name=only_active_users]')[0].checked;
@@ -172,5 +190,6 @@ <h3>Your email will look like this...</h3>
             let mailto_link = $('#email_client_link')[0];
             mailto_link.href = mailto;
         }
+        {%  endif %}
 	</script>
 {% endblock %}
\ No newline at end of file
diff --git a/NEMO/templates/email/email_broadcast.html b/NEMO/templates/email/email_broadcast.html
index 601e566a..2014af16 100644
--- a/NEMO/templates/email/email_broadcast.html
+++ b/NEMO/templates/email/email_broadcast.html
@@ -14,6 +14,7 @@ <h1>Email broadcast</h1>
 	<ul>
 		<li><a href="{% url 'email_broadcast' 'user' %}">have an account in {{ site_title }}</a></li>
 		{% if tools_exist %}<li><a href="{% url 'email_broadcast' 'tool' %}">are qualified for a tool</a></li>{% endif %}
+		{% if tools_exist %}<li><a href="{% url 'email_broadcast' 'tool-reservation' %}">have an upcoming reservation on a tool</a></li>{% endif %}
 		{% if areas_exist %}<li><a href="{% url 'email_broadcast' 'area' %}">have access to an area</a></li>{% endif %}
 		<li><a href="{% url 'email_broadcast' 'project' %}">are working on a project</a></li>
 		<li><a href="{% url 'email_broadcast' 'account' %}">bill to an account</a></li>
@@ -22,7 +23,7 @@ <h1>Email broadcast</h1>
 	{% if search_base %}
 		<div class="row">
 			<div class="col-sm-4">
-				<input id="search" class="form-control" aria-label="Audience" placeholder="Which {{ audience }}?" autofocus />
+				<input id="search" class="form-control" aria-label="Audience" placeholder="Which {% if audience == 'tool-reservation' %}tool{% else %}{{ audience }}{% endif %}?" autofocus />
 			</div>
 		</div>
 
diff --git a/NEMO/templates/event_details/outage_details.html b/NEMO/templates/event_details/outage_details.html
index b89e0fa2..69077716 100644
--- a/NEMO/templates/event_details/outage_details.html
+++ b/NEMO/templates/event_details/outage_details.html
@@ -1,11 +1,67 @@
 {% extends popup_view|yesno:"base/popup.html,base.html" %}
 {% load custom_tags_and_filters %}
+
+{% if not popup_view %}
+{% block extrahead %}
+    <script type="text/javascript">
+    function callback_success(){
+        // if on a standalone page, reload the browser window to display the results of the change to the outage
+        location.reload()
+    }
+
+    function cancel_outage(url)
+    {
+        let failure_dialog = ajax_failure_callback("Unable to cancel this outage");
+        ajax_post(url, undefined, callback_success, [failure_dialog, callback_success]);
+    }
+
+    function change_outage_title(url, title)
+    {
+        let failure_dialog = ajax_failure_callback("Unable to change outage title");
+        ajax_post(url, {'title': title}, callback_success, [failure_dialog]);
+    }
+
+    function change_outage_details(url, details)
+    {
+        let failure_dialog = ajax_failure_callback("Unable to change outage details");
+        ajax_post(url, {'details': details}, callback_success, [failure_dialog]);
+    }
+
+    function change_outage_date(url, outage_id, param, value)
+    {
+        let failure_dialog = ajax_failure_callback("Unable to change outage date");
+        let data = {'id': outage_id};
+        data[param] = value;
+        ajax_post(url, data, callback_success, [failure_dialog]);
+    }
+
+    </script>
+{% endblock %}
+{% endif %}
+
 {% block title %}Scheduled outage details{% endblock %}
 {% block content %}
     {% if not popup_view %}
         <h1>Scheduled outage details</h1>
     {% endif %}
-    Title: {{ outage.title }}<br>
+    {% if user.is_staff %}
+        <label for="outage_title" style="margin-bottom: 0">Title: </label>
+        <div id="outage_display_title" style="display: inline">
+            {{ outage.title }}
+            <span class="glyphicon glyphicon-pencil pull-right" title="Change the outage title" onclick="$('#outage_display_title').hide();$('#outage_change_title').css('display', 'inline');"></span>
+        </div>
+        <div id="outage_change_title" style="display: none">
+            <div class="input-group" style="display: inline-flex; vertical-align: middle">
+                <input type="text" id="outage_title" class="form-control" value="{{ outage.title }}">
+                <span class="input-group-btn">
+                    <button type="button" class="btn btn-success" data-dismiss="modal" onclick="{% if popup_view %}$('#dialog').one('hidden.bs.modal', function() { {% endif %}change_outage_title('{% url 'change_outage_title' outage.id %}', $('#outage_title').val()) {% if popup_view %}}){% endif %}">Change title</button>
+                </span>
+            </div>
+        </div>
+        <br>
+    {% else %}
+        Title: {{ outage.title }}<br>
+    {% endif %}
     Created by: {{ outage.creator }}<br>
     {% if outage.tool %}Tool: {{ outage.tool }}<br>{% endif %}
     {% if outage.resource %}Resource: {{ outage.resource }} <span class="light-grey">({{ outage.resource.category }})</span><br>{% endif %}
@@ -13,16 +69,30 @@ <h1>Scheduled outage details</h1>
     Start: {{ outage.start }}<br>
     End: {{ outage.end }}<br>
     Identifier: {{ outage.id }}<br>
-    {% if outage.details %}
-        Details:<br>
-        {{ outage.details|linebreaksbr }}
+    {% if outage.details or user.is_staff %}
+        {% if user.is_staff %}
+            <label for="outage_details" style="margin-bottom: 0">Details: </label><br>
+            <div id="outage_display_details">
+                <span class="glyphicon glyphicon-pencil pull-right" title="Change the outage details" onclick="$('#outage_display_details').hide();$('#outage_change_details').show();auto_size_textarea(document.getElementById('outage_details'))"></span>
+                {{ outage.details|linebreaksbr }}
+            </div>
+            <div id="outage_change_details" style="display: none">
+                <div class="input-group" style="width: 100%;">
+                    <textarea id="outage_details" class="form-control">{{ outage.details }}</textarea>
+                    <span class="input-group-addon btn btn-success" style="color: #fff" data-dismiss="modal" onclick="{% if popup_view %}$('#dialog').one('hidden.bs.modal', function() { {% endif %}change_outage_details('{% url 'change_outage_details' outage.id %}', $('#outage_details').val()) {% if popup_view %}}){% endif %}">Change details</span>
+                </div>
+            </div>
+            <br>
+        {% else %}
+            Details:<br>
+            {{ outage.details|linebreaksbr }}<br>
+        {% endif %}
     {% endif %}
 
-    {# Allow the user to cancel the outage if they have that priviledge. only in popup view #}
-    {% if user.is_staff and popup_view %}
+    {# Allow the user to cancel the outage if they have that privilege. only in popup view #}
+    {% if user.is_staff %}
         <div class="modal-footer" style="padding: 15px 0 0 0;">
-            {% url 'cancel_outage' outage.id as cancel_outage_url %}
-            {% button type="delete" value="Cancel this outage" dismiss="modal" onclick="$('#dialog').one('hidden.bs.modal', function() { cancel_outage('"|concat:cancel_outage_url|concat:"', 'Outage "|concat:outage.id|concat:"'); });" %}
+            <button type="button" class="btn btn-danger" data-dismiss="modal" onclick="{% if popup_view %}$('#dialog').one('hidden.bs.modal', function() { {% endif %}cancel_outage('{% url 'cancel_outage' outage.id  %}', 'Outage {{ outage.id }}'); {% if popup_view %}}){% endif %}">Cancel this outage</button>
         </div>
     {% endif %}
 {% endblock %}
\ No newline at end of file
diff --git a/NEMO/templates/event_details/reservation_details.html b/NEMO/templates/event_details/reservation_details.html
index ff7e21af..7c3e625c 100644
--- a/NEMO/templates/event_details/reservation_details.html
+++ b/NEMO/templates/event_details/reservation_details.html
@@ -154,8 +154,12 @@ <h1>{{ reservation.reservation_item }} reservation details</h1>
     </tr>
     <tr>
         {% if request.user == reservation.user and reservation.has_not_ended and reservation.has_not_started %}
-            <td style="vertical-align: middle"><label for="reservation_end">End:</label></td>
-            <td>
+            <td style="vertical-align: middle"><label for="reservation_end" style="margin-bottom: 0">End:</label></td>
+            <td id="reservation_display_end">
+                {{ reservation.end }}
+                <span class="glyphicon glyphicon-pencil pull-right" title="Change the end date" onclick="$('#reservation_display_end').hide();$('#reservation_change_end').show();"></span>
+            </td>
+            <td id="reservation_change_end" style="display: none">
                 <div class="input-group">
                     <input type="text" id="reservation_end" class="form-control" value="{{ reservation.end|localtime|input_date_format }}">
                     <span class="input-group-btn">
@@ -212,7 +216,7 @@ <h1>{{ reservation.reservation_item }} reservation details</h1>
     {% endif %}
     </table>
 
-    {# Allow the user to cancel the reservation if they have that priviledge. #}
+    {# 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 %}
             <div class="modal-footer" style="padding: 15px 0 0 0;">
diff --git a/NEMO/templates/jumbotron/jumbotron.html b/NEMO/templates/jumbotron/jumbotron.html
index 1e01b8f7..1d629a06 100644
--- a/NEMO/templates/jumbotron/jumbotron.html
+++ b/NEMO/templates/jumbotron/jumbotron.html
@@ -1,7 +1,7 @@
 {% load static %}
 {% load custom_tags_and_filters %}
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
 	{# This meta-tag forcefully disables 'compatibility mode' in Internet Explorer because it causes rendering problems. #}
 	<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
diff --git a/NEMO/templates/jumbotron/jumbotron_content.html b/NEMO/templates/jumbotron/jumbotron_content.html
index 2483c887..4b82066b 100644
--- a/NEMO/templates/jumbotron/jumbotron_content.html
+++ b/NEMO/templates/jumbotron/jumbotron_content.html
@@ -39,7 +39,7 @@ <h2>Tool usage</h2>
 							<tr {% if u.operator.is_any_part_of_staff %}class="success" {% elif u.operator.is_service_personnel %}class="warning" {% else %}style="background:transparent"{% endif %}>
 								<td>{{ u.tool }}</td>
 								<td><span>{{ u.operator.first_name }} {{ u.operator.last_name }}{% if u.user != u.operator %} on behalf of {{ u.user.first_name }} {{ u.user.last_name }}{% endif %}</span></td>
-								<td>{{ u.start|date:"l @ " }}{{ u.start|time }}</td>
+								<td>{{ u.start|date:"MONTH_DAY_FORMAT" }} @ {{ u.start|time }}</td>
 							</tr>
 						{% endfor %}
 						</tbody>
diff --git a/NEMO/templates/mobile/individual_reservation.html b/NEMO/templates/mobile/individual_reservation.html
index 99531f16..d173b2eb 100644
--- a/NEMO/templates/mobile/individual_reservation.html
+++ b/NEMO/templates/mobile/individual_reservation.html
@@ -24,7 +24,7 @@
 			Additional information:<br>
 			{{ reservation.additional_information|linebreaksbr }}
 		{% endif %}
-		{# Allow the user to cancel the reservation if they have that priviledge. #}
+		{# 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 %}
 				<div style="height:10px">{# Vertical spacer #}</div>
diff --git a/NEMO/templates/occupancy/occupancy_content.html b/NEMO/templates/occupancy/occupancy_content.html
index 3ba36ab9..b765c84c 100644
--- a/NEMO/templates/occupancy/occupancy_content.html
+++ b/NEMO/templates/occupancy/occupancy_content.html
@@ -25,7 +25,7 @@
 					{% endif %}
 					<span>{{ o.customer.first_name }} {{ o.customer.last_name }}</span>
 				</td>
-				<td style="white-space: nowrap;width:25%">{{ o.start|date:"l @ "}} {{ o.start|time }}</td>
+				<td style="white-space: nowrap;width:25%">{{ o.start|date:"MONTH_DAY_FORMAT" }} @  {{ o.start|time }}</td>
 				<td><div style="width: 0; min-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ o.project }}</div></td>
 			</tr>
 		{% endfor %}
diff --git a/NEMO/templates/pagination/pagination_column.html b/NEMO/templates/pagination/pagination_column.html
index 5543b6c9..2e373e27 100644
--- a/NEMO/templates/pagination/pagination_column.html
+++ b/NEMO/templates/pagination/pagination_column.html
@@ -1,8 +1,8 @@
 {% load custom_tags_and_filters %}
 <span class="pagination-column">
-{% with order_list=order_by|concat:',-'|concat:order_by %}
-	{% with order=page.paginator.order_by|cut:order_by|yesno:order_list %}
-		<a href="?o={{ order }}&p={{ page.paginator.page_number }}&pp={{ page.paginator.per_page }}">{{ name }}<span class="{% if order_by not in page.paginator.order_by %}invisible{% endif %} glyphicon glyphicon-triangle-{% if order == order_by %}bottom{% else %}top{% endif %}"></span></a>
+{% with order_list=order_by|concat:',-'|concat:order_by %} {# order_list = "name,-name #}
+	{% with order=page.paginator.order_by|cut:order_by|yesno:order_list %} {# order = -name if order_by is name and vice versa #}
+		<a href="?{{ extra_params }}&o={{ order }}&p={{ page.paginator.page_number }}&pp={{ page.paginator.per_page }}">{{ name }}<span class="{% if not page.paginator.order_by or order_by not in page.paginator.order_by or order_by|length|add:1 < page.paginator.order_by|length %}invisible{% endif %} glyphicon glyphicon-triangle-{% if order == order_by %}bottom{% else %}top{% endif %}"></span></a>
 	{% endwith %}
 {% endwith %}
 </span>
\ No newline at end of file
diff --git a/NEMO/templates/requests/access_requests/access_request.html b/NEMO/templates/requests/access_requests/access_request.html
index e013fb20..86d91bd4 100644
--- a/NEMO/templates/requests/access_requests/access_request.html
+++ b/NEMO/templates/requests/access_requests/access_request.html
@@ -47,7 +47,7 @@ <h1 class="form-group">{% if form.instance.id %}Modify access request{% else %}N
                     {% endif %}
                     </select>
                 </div>
-                {% if form.instance.id and user.is_facility_manager %}
+                {% if form.instance.id and user in form.instance.reviewers %}
                     <div class="col-sm-6">
                         <label for="creator_readonly" class="control-label">Creator</label>
                         <input id="creator_readonly" class="form-control" style="min-width: 250px;" disabled value="{{ form.instance.creator }}">
@@ -79,7 +79,7 @@ <h1 class="form-group">{% if form.instance.id %}Modify access request{% else %}N
 		</div>
 		<div class="text-right">
             {% if form.instance.get_status_display == "Pending" %}
-                {% if form.instance.id and user.is_facility_manager %}
+                {% if form.instance.id and user in form.instance.reviewers %}
                     {% button type="save" submit=False name="approve_request" title="Approve request" icon="glyphicon-ok-circle" value="Approve" onclick="if (confirm('Are you sure you want to approve this request?')) {submit_and_disable(this)}" %}
                     {% button type="delete" submit=False name="deny_request" title="Deny request" icon="glyphicon-ban-circle" value="Deny" onclick="if (confirm('Are you sure you want to deny this request?')) {submit_and_disable(this)}" %}
                 {% else %}
diff --git a/NEMO/templates/requests/access_requests/access_requests_table.html b/NEMO/templates/requests/access_requests/access_requests_table.html
index b521fb4a..842fb7e0 100644
--- a/NEMO/templates/requests/access_requests/access_requests_table.html
+++ b/NEMO/templates/requests/access_requests/access_requests_table.html
@@ -8,7 +8,7 @@
             <th class="text-center">Requested end</th>
             <th class="text-center">Users</th>
             <th class="text-center">
-                {% if request_status == 'pending' and user.is_facility_manager %}
+                {% if request_status == 'pending' and user_is_reviewer %}
                     Action
                 {% elif request_status == 'approved' %}
                     Approved by
@@ -34,7 +34,7 @@
                     </ul>
                 </td>
                 <td class="text-center text-nowrap" style="vertical-align: middle">
-                    {% if request_status == 'pending' and user.is_facility_manager %}
+                    {% if request_status == 'pending' and user in a_request.reviewers %}
                         <a class="btn btn-sm btn-default" title="Approve/Deny" href="{% url 'edit_access_request' a_request.id %}"><i class="glyphicon glyphicon-ok-circle success-highlight"></i>&nbsp;&nbsp;<i class="glyphicon glyphicon-ban-circle danger-highlight"></i> Review</a>
                     {% elif request_status == 'pending' and a_request.creator == user %}
                         {% url 'edit_access_request' a_request.id as edit_request_url %}
diff --git a/NEMO/templates/requests/adjustment_requests/adjustment_request.html b/NEMO/templates/requests/adjustment_requests/adjustment_request.html
index 877b5b61..a335a763 100644
--- a/NEMO/templates/requests/adjustment_requests/adjustment_request.html
+++ b/NEMO/templates/requests/adjustment_requests/adjustment_request.html
@@ -17,7 +17,7 @@ <h1 class="form-group">{% if instance_id %}Modify adjustment request{% else %}Ne
                     {{ form.non_field_errors }}
                 </div>
             {% endif %}
-            {% if instance_id and user.is_facility_manager %}
+            {% if instance_id and user in form.instance.reviewers %}
                 <div class="form-group">
                     <div class="row">
                         <div class="col-sm-6">
@@ -74,7 +74,7 @@ <h1 class="form-group">{% if instance_id %}Modify adjustment request{% else %}Ne
                 <label for="description" class="control-label">Description</label>{% if form.description.errors %} - <span style="color:red">{{ form.description.errors|striptags }}</span>{% endif %}
                 <textarea class="form-control" rows="3" name="description" id="description" placeholder="Explain the reason for this request" required>{{ form.description.value|default_if_none:"" }}</textarea>
             </div>
-            {% if form.instance.get_status_display == "Pending" and instance_id and user.is_facility_manager %}
+            {% 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 %}
                     <textarea class="form-control" rows="3" name="manager_note" id="manager_note" placeholder="Feedback to send to the user if the request is denied or instructions to the user office if it's approved">{{ form.manager_note.value|default_if_none:"" }}</textarea>
@@ -82,7 +82,7 @@ <h1 class="form-group">{% if instance_id %}Modify adjustment request{% else %}Ne
             {% endif %}
             <div class="text-right">
                 {% if form.instance.get_status_display == "Pending" %}
-                    {% if instance_id and user.is_facility_manager %}
+                    {% if instance_id and user in form.instance.reviewers %}
                         {% button type="save" submit=False name="approve_request" title="Approve request" icon="glyphicon-ok-circle" value="Approve" onclick="confirm_review_dialog(this);" %}
                         {% button type="delete" submit=False name="deny_request" title="Deny request" icon="glyphicon-ban-circle" value="Deny" onclick="confirm_review_dialog(this);" %}
                     {% endif %}
diff --git a/NEMO/templates/requests/adjustment_requests/adjustment_requests_table.html b/NEMO/templates/requests/adjustment_requests/adjustment_requests_table.html
index 5b156486..0c5df433 100644
--- a/NEMO/templates/requests/adjustment_requests/adjustment_requests_table.html
+++ b/NEMO/templates/requests/adjustment_requests/adjustment_requests_table.html
@@ -3,7 +3,7 @@
     <thead>
         <tr>
             <th>Created</th>
-            {% if user.is_any_part_of_staff %}
+            {% if user_is_reviewer or user.is_any_part_of_staff %}
                 <th>User</th>
             {% endif %}
             <th class="text-center">Charge</th>
@@ -11,7 +11,7 @@
             <th class="text-center text-nowrap">New end</th>
             <th class="text-center">Difference</th>
             <th class="text-right text-nowrap">
-                {% if request_status == 'pending' and user.is_facility_manager %}
+                {% if request_status == 'pending' and user_is_reviewer %}
                     Action
                 {% elif request_status == 'approved' %}
                     Approved by
@@ -25,7 +25,7 @@
         {% for a_request in adjustment_request_list %}
             <tr>
                 <td style="vertical-align: middle">{% if a_request.id in request_notifications %}<span class="label label-success">New</span> {% endif %}{{ a_request.creation_time|date:'SHORT_DATETIME_FORMAT' }}</td>
-                {% if user.is_any_part_of_staff %}
+                {% if user_is_reviewer or user.is_any_part_of_staff %}
                     <td style="vertical-align: middle">{{ a_request.creator.get_name }}</td>
                 {% endif %}
                 <td class="text-center" style="vertical-align: middle">{{ a_request.item.get_display }}</td>
@@ -43,7 +43,7 @@
                             {% endif %}
                         {% endif %}
                     {% else %}
-                        {% if request_status == 'pending' and user.is_facility_manager %}
+                        {% if request_status == 'pending' and user in a_request.reviewers %}
                             <a class="btn btn-sm btn-default" title="Approve/Deny" href="{% url 'edit_adjustment_request' a_request.id %}"><i class="glyphicon glyphicon-ok-circle success-highlight"></i>&nbsp;&nbsp;<i class="glyphicon glyphicon-ban-circle danger-highlight"></i> Review</a>
                         {% endif %}
                         {% if request_status == 'pending' and a_request.creator == user %}
@@ -56,8 +56,8 @@
                 </td>
             </tr>
             <tr>
-                <td colspan="{% if user.is_any_part_of_staff %}3{% else %}2{% endif %}" style="border: none">
-                    <div style="color: gray">
+                <td colspan="{% if user_is_reviewer or user.is_any_part_of_staff %}3{% else %}2{% endif %}" style="border: none">
+                    <div style="color: rgb(128,128,128)">
                         {% if a_request.description %}
                             <div>
                                 <div class="adjustment-request-message">
@@ -78,7 +78,7 @@
                             {% endfor %}
                         {% endif %}
                         {% if a_request.get_status_display == "Pending" %}
-                            {% if user == a_request.creator or user.is_facility_manager %}
+                            {% if user == a_request.creator or user in a_request.reviewers %}
                                 <div>
                                     <div class="adjustment-request-reply-textarea" style="display: none;">
                                         <form id="adjustment_request_reply_{{ a_request.id }}" action="{% url 'adjustment_request_reply' a_request.id %}" method="post">
diff --git a/NEMO/templates/status_dashboard/tools.html b/NEMO/templates/status_dashboard/tools.html
index eaf9f7ef..a1b9a442 100644
--- a/NEMO/templates/status_dashboard/tools.html
+++ b/NEMO/templates/status_dashboard/tools.html
@@ -45,7 +45,7 @@
 					{% endif %}
 					{{ tool.user }}
 				</td>
-				<td>{{ tool.in_use_since|date:"l @ "}} {{ tool.in_use_since|time }}</td>
+				<td>{{ tool.in_use_since|date:"MONTH_DAY_FORMAT" }} @ {{ tool.in_use_since|time }}</td>
 			{% else %}
 				<td aria-label="No user"></td>
 				<td aria-label="Not in use"></td>
diff --git a/NEMO/templates/tool_control/tool_status.html b/NEMO/templates/tool_control/tool_status.html
index 219fe448..087b21eb 100644
--- a/NEMO/templates/tool_control/tool_status.html
+++ b/NEMO/templates/tool_control/tool_status.html
@@ -7,7 +7,7 @@ <h1>{{ tool.name_or_child_in_use_name }}</h1>
                 <li style="width:48%; text-align:center" class="active"><a href="#summary" style="padding: 10px 5px">Summary</a></li>
                 <li style="width:48%; text-align:center"><a href="#details" style="padding: 10px 5px">Details</a></li>
                 {% if configs %}<li style="width:48%; text-align:center"><a href="#config" style="padding: 10px 5px">Config History</a></li>{% endif %}
-                {% if tool.post_usage_questions and not hide_data_history or user.is_any_part_of_staff %}<li style="width: 48%; text-align: center"><a href="#usage_data" style="padding: 10px 5px;" onclick="load_usage_data('{{ tool.id }}');">Usage Data History</a></li>{% endif %}
+                {% if tool.post_usage_questions and not hide_data_history or tool.post_usage_questions and user.is_any_part_of_staff %}<li style="width: 48%; text-align: center"><a href="#usage_data" style="padding: 10px 5px;" onclick="load_usage_data('{{ tool.id }}');">Usage Data History</a></li>{% endif %}
                 <li style="width:48%; text-align:center"><a href="#problem" style="padding: 10px 5px">Report a problem</a></li>
                 <li style="width:48%; text-align:center"><a href="#comment" style="padding: 10px 5px">Post a comment</a></li>
             </ul>
@@ -17,7 +17,7 @@ <h1 class="pull-left" style="margin-right:20px; margin-top:0; margin-bottom:10px
                 <li class="active"><a href="#summary">Summary</a></li>
                 <li><a href="#details">Details</a></li>
                 {% if configs %}<li><a href="#config">Config History</a></li>{% endif %}
-                {% if tool.post_usage_questions and not hide_data_history or user.is_any_part_of_staff %}<li><a href="#usage_data" onclick="load_usage_data('{{ tool.id }}');">Usage Data History</a></li>{% endif %}
+                {% if tool.post_usage_questions and not hide_data_history or tool.post_usage_questions and user.is_any_part_of_staff %}<li><a href="#usage_data" onclick="load_usage_data('{{ tool.id }}');">Usage Data History</a></li>{% endif %}
                 <li><a href="#problem">Report a problem</a></li>
                 <li><a href="#comment">Post a comment</a></li>
             </ul>
@@ -46,7 +46,7 @@ <h2>
 							{% if current_usage_event.operator.id != current_usage_event.user.id %}
 								on behalf of {{ current_usage_event.user }}
 							{% endif %}
-							for the project named {{ current_usage_event.project.name }} since {{ current_usage_event.start|date:"l @ " }}{{ current_usage_event.start|time }}.
+							for the project named {{ current_usage_event.project.name }} since {{ current_usage_event.start|date:"MONTH_DAY_FORMAT" }} @ {{ current_usage_event.start|time }}.
 						</h2>
 					{% endwith %}
 				</div>
@@ -443,6 +443,7 @@ <h4 id="what_to_do_label">What would you like to do?</h4>
 				{% endif %}
 				{% if tool.location %}<p>The {{ tool.name_or_child_in_use_name }} is located in room <span class="glyphicon glyphicon-map-marker small-icon"></span>{{ tool.location }}.</p>{% endif %}
 				{% if tool.phone_number %}<p>You may dial the phone that is closest to the {{ tool.name_or_child_in_use_name }} at extension <span class="glyphicon glyphicon-phone small-icon"></span>{{ tool.phone_number }}.</p>{% endif %}
+				{% if show_broadcast_upcoming_reservation %}<p>You can email <a href="{% url 'compose_email' %}?audience=tool-reservation&selection={{ tool.id }}"><span class="glyphicon glyphicon-send"></span> all users with an upcoming reservation</a>.</p>{% endif %}
 			</div>
 		</div>
 		{% if user.is_staff %}
@@ -563,8 +564,7 @@ <h4 class="media-heading">Task &amp; comment history for this tool</h4>
 			{% csrf_token %}
 			<input type="hidden" name="action" value="create">
 			<input type="hidden" name="tool" value="{{ tool.id }}">
-			<p>Use this form to report a problem relating to the currently selected tool. The {{ facility_name }} staff will be notified of the problem by email and the details of the problem will be visible to everyone on the website.</p>
-
+			<p>Use this form to report a problem relating to the currently selected tool. The {{ facility_name }} staff as well as anyone with an upcoming reservation will be notified of the problem by email and the details of the problem will be visible to everyone on the website.</p>
 			{% if task_categories %}
 				<div class="form-group">
 					<label for="problem_category">Please choose a category that best describes this problem. If there is no appropriate category then leave this field blank.</label>
diff --git a/NEMO/templates/usage/usage_base.html b/NEMO/templates/usage/usage_base.html
index e9ba1f60..f29d4348 100644
--- a/NEMO/templates/usage/usage_base.html
+++ b/NEMO/templates/usage/usage_base.html
@@ -46,13 +46,19 @@ <h1>Usage{% if billing_service %} and billing information{% endif %}
                         <label for="pi_project">Managed project</label>
                     </div>
                     <div class="col-sm-3 col-md-3 col-lg-2">
-                        <select name="pi_project" id="pi_project" class="form-control" style="text-align-last:center" title="Select a project you are the PI for to see all usage for that project">
+                        <select {% if show_only_my_usage %}disabled{% endif %} name="pi_project" id="pi_project" class="form-control" style="text-align-last:center" title="Select a project you are the PI for to see all usage for that project">
                             <option selected value="">All</option>
                             {% for project in pi_projects %}
                                 <option value="{{ project.id }}" {% if selected_project and selected_project.id == project.id %}selected{% endif %}>{{ project.name }}</option>
                             {% endfor %}
                         </select>
                     </div>
+                    <div class="col-sm-3 col-md-3 col-lg-2">
+                        <div class="checkbox">
+                            <input type="hidden" name="show_only_my_usage" value="off">
+                            <label><input type="checkbox" {% if show_only_my_usage %}checked{% endif %} name="show_only_my_usage" value="enabled" onclick="$('#pi_project').attr('disabled', this.checked)">Show only my usage</label>
+                        </div>
+                    </div>
             {% endif %}
 			{% if user.active_projects %}
                 </div>
diff --git a/NEMO/templates/users/create_or_modify_user.html b/NEMO/templates/users/create_or_modify_user.html
index 2dd87832..86070fde 100644
--- a/NEMO/templates/users/create_or_modify_user.html
+++ b/NEMO/templates/users/create_or_modify_user.html
@@ -138,6 +138,7 @@
 			{% endif %}
 			<div class="col-sm-offset-2 col-sm-4">
 				<div class="checkbox">
+					<label><input id="is_service_personnel" name="is_service_personnel" type="checkbox" {% if form.is_service_personnel.value %}checked{% endif %}> Service personnel</label><br>
 					<label><input id="training_required" name="training_required" type="checkbox" {% if form.training_required.value %}checked{% endif %}> Facility rules tutorial required</label>
 				</div>
 			</div>
@@ -409,11 +410,11 @@
             auto_size_textarea(document.getElementById('notes'));
 			$('[data-toggle="tooltip"]').tooltip();
 			$("#access_expiration").datetimepicker({format: '{{ date_input_js_format }}'});
-			$('#add_a_project').autocomplete('projects', on_search_selection, {% json_search_base_with_extra_fields projects 'application_identifier' %});
+			$('#add_a_project').autocomplete('projects', on_search_selection, {% json_search_base_with_extra_fields projects 'application_identifier' display="display_with_pis" %});
 			$('#add_a_tool_qualification').autocomplete('tools', on_search_selection, {{ tools|json_search_base }});
 			{% if form.instance.id %} {# Only look for projects and qualifications if this is an existing user (and not a new user). #}
                 {% for project in form.instance.projects.all %}
-					add_project('{{ project.name }}', {{ project.id }});
+					add_project('{{ project.display_with_pis }}', {{ project.id }});
 				{% endfor %}
 				{% for tool in form.instance.qualifications.all %}
 					add_tool_qualification('{{ tool.name }}', {{ tool.id }});
diff --git a/NEMO/templates/users/preferences.html b/NEMO/templates/users/preferences.html
index ea571653..33c635dc 100644
--- a/NEMO/templates/users/preferences.html
+++ b/NEMO/templates/users/preferences.html
@@ -75,7 +75,7 @@ <h3 id="buddy_system_label">Buddy System</h3>
 					</div>
 				</div>
 			</div>
-            <div class="panel panel-default">
+             <div class="panel panel-default">
 				<div class="panel-body">
 					<h3 id="adjustment_requests_label">Adjustment Requests</h3>
 					<div class="form-group col-sm-12">
@@ -83,32 +83,8 @@ <h3 id="adjustment_requests_label">Adjustment Requests</h3>
 							<label><input name="email_new_adjustment_request_reply" type="checkbox" {% if user_preferences.email_new_adjustment_request_reply %}checked{% endif %}>Check this box to receive an email notification for replies to your adjustment requests</label>
 						</div>
 					</div>
-                    {% if user.is_facility_manager %}
-                        <div class="form-group">
-                            <label class="col-sm-5 col-md-4 col-lg-3 form-control-static" for="tool_adjustment_notification_search">Only manage requests for tools</label>
-                            <div class="col-sm-6 col-lg-4">
-                                <input id="tool_adjustment_notification_search" type="text" autocomplete="off" class="form-control" placeholder="Add a tool">
-                            </div>
-                        </div>
-                        <div class="form-group">
-                            <div id="tool-adjustment-notification-list" class="col-sm-offset-5 col-md-offset-4 col-lg-offset-3 col-sm-7">
-                                You are currently receiving adjustment request notifications for all tools.
-                            </div>
-                        </div>
-                        <div class="form-group">
-                            <label class="col-sm-5 col-md-4 col-lg-3 form-control-static" for="area_adjustment_notification_search">Only manage requests for areas</label>
-                            <div class="col-sm-6 col-lg-4">
-                                <input id="area_adjustment_notification_search" type="text" autocomplete="off" class="form-control" placeholder="Add an area">
-                            </div>
-                        </div>
-                        <div class="form-group">
-                            <div id="area-adjustment-notification-list" class="col-sm-offset-5 col-md-offset-4 col-lg-offset-3 col-sm-7">
-                                You are currently receiving adjustment request notifications for all areas.
-                            </div>
-                        </div>
-                    {% endif %}
                 </div>
-            </div>
+             </div>
             {% if user.is_staff or user.is_facility_manager or user.is_service_personnel %}
                 <div class="panel panel-default">
                     <div class="panel-body">
@@ -214,24 +190,6 @@ <h3 id="email_notifications_label">Email notifications</h3>
             {
                 remove_from_list("#task-notification-list", "#tool_task_notifications_" + tool_id, "You are currently viewing/receiving maintenance notifications for all tools{% if not user.is_facility_manager or not 'tool'|customization:'tool_task_updates_facility_managers' == 'enabled' %} you are an owner or backup owners of{% endif %}.");
             }
-            function add_tool_adjustment_notification(jquery_event, search_selection, dataset_name)
-            {
-                add_to_list("#tool-adjustment-notification-list", "remove_tool_adjustment_notification", search_selection.id, search_selection.name, "Remove the " + search_selection.name, "tool_adjustment_notifications");
-                $("#tool_adjustment_notification_search").typeahead('val', '').focus();
-            }
-            function remove_tool_adjustment_notification(tool_id)
-            {
-                remove_from_list("#tool-adjustment-notification-list", "#tool_adjustment_notifications_" + tool_id, "You are currently receiving adjustment request notifications for all tools.");
-            }
-            function add_area_adjustment_notification(jquery_event, search_selection, dataset_name)
-            {
-                add_to_list("#area-adjustment-notification-list", "remove_area_adjustment_notification", search_selection.id, search_selection.name, "Remove the " + search_selection.name, "area_adjustment_notifications");
-                $("#area_adjustment_notification_search").typeahead('val', '').focus();
-            }
-            function remove_area_adjustment_notification(area_id)
-            {
-                remove_from_list("#area-adjustment-notification-list", "#area_adjustment_notifications_" + area_id, "You are currently receiving adjustment request notifications for all areas.");
-            }
             function add_tool_notification(jquery_event, search_selection, dataset_name)
             {
                 add_to_list("#notification-list", "remove_tool_notification", search_selection.id, search_selection.name, "Remove the " + search_selection.name, "tool_freed_time_notifications");
@@ -253,24 +211,8 @@ <h3 id="email_notifications_label">Email notifications</h3>
                         {% endfor %}
                     {% endfor %}
                 {% endif %}
-                $('#tool_adjustment_notification_search').autocomplete('tools', add_tool_adjustment_notification, {{ form.fields.tool_adjustment_notifications.queryset|json_search_base }});
-                $('#area_adjustment_notification_search').autocomplete('areas', add_area_adjustment_notification, {{ form.fields.area_adjustment_notifications.queryset|json_search_base }});
                 $('#tool_task_notification_search').autocomplete('tools', add_tool_task_notification, {{ form.fields.tool_task_notifications.queryset|json_search_base }});
                 {% if form.instance.id %}
-                    {% for tool_id in form.tool_adjustment_notifications.value %}
-                        {% for tool in form.fields.tool_adjustment_notifications.queryset %}
-                            {% if tool.id == tool_id|to_int %}
-                                add_tool_adjustment_notification(null, {'name': '{{ tool.name }}', 'id': {{ tool.id }} });
-                            {% endif %}
-                        {% endfor %}
-                    {% endfor %}
-                    {% for area_id in form.area_adjustment_notifications.value %}
-                        {% for area in form.fields.area_adjustment_notifications.queryset %}
-                            {% if area.id == area_id|to_int %}
-                                add_area_adjustment_notification(null, {'name': '{{ area.name }}', 'id': {{ area.id }} });
-                            {% endif %}
-                        {% endfor %}
-                    {% endfor %}
                     {% for tool_id in form.tool_task_notifications.value %}
                         {% for tool in form.fields.tool_task_notifications.queryset %}
                             {% if tool.id == tool_id|to_int %}
diff --git a/NEMO/templatetags/custom_tags_and_filters.py b/NEMO/templatetags/custom_tags_and_filters.py
index eb929dad..9fff0e9f 100644
--- a/NEMO/templatetags/custom_tags_and_filters.py
+++ b/NEMO/templatetags/custom_tags_and_filters.py
@@ -183,7 +183,7 @@ def admin_edit_url(context, obj, redirect_url=None):
 	user = context["request"].user
 	try:
 		obj_type = content_type(obj)
-		permission = user.has_perm(f"{obj_type.app_label}.change_{obj_type.model}")
+		permission = user.has_perm(f"{obj_type.app_label}.change_{obj_type.model}", obj)
 		if permission:
 			url = reverse(f"admin:{obj_type.app_label}_{obj_type.model}_change", args=[obj.id])
 			if redirect_url:
diff --git a/NEMO/urls.py b/NEMO/urls.py
index 4a979d99..17d3eb44 100644
--- a/NEMO/urls.py
+++ b/NEMO/urls.py
@@ -51,6 +51,7 @@
 	sidebar,
 	status_dashboard,
 	tasks,
+	timed_services,
 	tool_control,
 	training,
 	tutorials,
@@ -62,6 +63,11 @@
 
 logger = logging.getLogger(__name__)
 
+
+def sort_urls(url_path):
+	return url_path[0].count('/'), url_path[0]
+
+
 # REST API URLs
 router = routers.DefaultRouter()
 router.register(r"auth_groups", api.GroupViewSet)
@@ -87,6 +93,7 @@
 router.register(r"training_sessions", api.TrainingSessionViewSet)
 router.register(r"usage_events", api.UsageEventViewSet)
 router.register(r"users", api.UserViewSet)
+router.registry.sort(key=sort_urls)
 
 reservation_item_types = f'(?P<item_type>{"|".join(ReservationItemType.values())})'
 
@@ -194,6 +201,8 @@
 	path("cancel_reservation/<int:reservation_id>/", calendar.cancel_reservation, name="cancel_reservation"),
 	path("cancel_outage/<int:outage_id>/", calendar.cancel_outage, name="cancel_outage"),
 	path("set_reservation_title/<int:reservation_id>/", calendar.set_reservation_title, name="set_reservation_title"),
+	path("change_outage_title/<int:outage_id>/", calendar.change_outage_title, name="change_outage_title"),
+	path("change_outage_details/<int:outage_id>/", calendar.change_outage_details, name="change_outage_details"),
 	path("change_reservation_date/", calendar.change_reservation_date, name="change_reservation_date"),
 	path("change_reservation_project/<int:reservation_id>/", calendar.change_reservation_project, name="change_reservation_project"),
 	path("proxy_reservation/", calendar.proxy_reservation, name="proxy_reservation"),
@@ -254,7 +263,7 @@
 	path("get_email_form_for_user/<int:user_id>/", email.get_email_form_for_user, name="get_email_form_for_user"),
 	path("send_email/", email.send_email, name="send_email"),
 	path("email_broadcast/", email.email_broadcast, name="email_broadcast"),
-	re_path(r"^email_broadcast/(?P<audience>tool|area|account|project|user)/$", email.email_broadcast, name="email_broadcast"),
+	re_path(r"^email_broadcast/(?P<audience>tool|area|account|project|user|tool-reservation)/$", email.email_broadcast, name="email_broadcast"),
 	path("email_preview/", email.email_preview, name="email_preview"),
 	path("compose_email/", email.compose_email, name="compose_email"),
 	path("email_export_addresses/", email.export_email_addresses, name="export_email_addresses"),
@@ -371,16 +380,17 @@
 		path("new_area_access_record/", area_access.new_area_access_record, name="new_area_access_record"),
 
 		# Reminders and periodic events
-		path("cancel_unused_reservations/", calendar.cancel_unused_reservations, name="cancel_unused_reservations"),
-		path("create_closure_alerts/", calendar.create_closure_alerts, name="create_closure_alerts"),
-		path("email_out_of_time_reservation_notification/", calendar.email_out_of_time_reservation_notification, name="email_out_of_time_reservation_notification"),
-		path("email_reservation_ending_reminders/", calendar.email_reservation_ending_reminders, name="email_reservation_ending_reminders"),
-		path("email_reservation_reminders/", calendar.email_reservation_reminders, name="email_reservation_reminders"),
-		path("email_usage_reminders/", calendar.email_usage_reminders, name="email_usage_reminders"),
-		path("email_weekend_access_notification/", access_requests.email_weekend_access_notification, name="email_weekend_access_notification"),
-		path("email_user_access_expiration_reminders/", calendar.email_user_access_expiration_reminders, name="email_user_access_expiration_reminders"),
-		path("manage_tool_qualifications/", calendar.manage_tool_qualifications, name="manage_tool_qualifications"),
-		path("manage_recurring_charges/", calendar.manage_recurring_charges, name="manage_recurring_charges"),
+		path("cancel_unused_reservations/", timed_services.cancel_unused_reservations, name="cancel_unused_reservations"),
+		path("create_closure_alerts/", timed_services.create_closure_alerts, name="create_closure_alerts"),
+		path("email_out_of_time_reservation_notification/", timed_services.email_out_of_time_reservation_notification, name="email_out_of_time_reservation_notification"),
+		path("email_reservation_ending_reminders/", timed_services.email_reservation_ending_reminders, name="email_reservation_ending_reminders"),
+		path("email_reservation_reminders/", timed_services.email_reservation_reminders, name="email_reservation_reminders"),
+		path("email_usage_reminders/", timed_services.email_usage_reminders, name="email_usage_reminders"),
+		path("email_weekend_access_notification/", timed_services.email_weekend_access_notification, name="email_weekend_access_notification"),
+		path("email_user_access_expiration_reminders/", timed_services.email_user_access_expiration_reminders, name="email_user_access_expiration_reminders"),
+		path("manage_tool_qualifications/", timed_services.manage_tool_qualifications, name="manage_tool_qualifications"),
+		path("manage_recurring_charges/", timed_services.manage_recurring_charges, name="manage_recurring_charges"),
+		path("auto_logout_users/", timed_services.auto_logout_users, name="auto_logout_users"),
 
 		# Abuse:
 		path("abuse/", abuse.abuse, name="abuse"),
diff --git a/NEMO/utilities.py b/NEMO/utilities.py
index 6d195b25..ea8df53a 100644
--- a/NEMO/utilities.py
+++ b/NEMO/utilities.py
@@ -31,6 +31,7 @@
 from django.utils import timezone
 from django.utils.formats import date_format, get_format, time_format
 from django.utils.html import format_html
+from django.utils.text import slugify
 from django.utils.timezone import is_naive, localtime
 
 utilities_logger = getLogger(__name__)
@@ -710,3 +711,8 @@ def create_ics(identifier, event_name, start: datetime, end: datetime, user, org
 
 	attachment = create_email_attachment(ics, maintype="text", subtype="calendar", method=method_name)
 	return attachment
+
+
+def slugify_underscore(name: str):
+	# Slugify and replaces dashes by underscores
+	return slugify(name).replace("-", "_")
diff --git a/NEMO/views/access_requests.py b/NEMO/views/access_requests.py
index 0f5ff38d..9778fdfb 100644
--- a/NEMO/views/access_requests.py
+++ b/NEMO/views/access_requests.py
@@ -1,20 +1,17 @@
-from datetime import datetime, timedelta
 from logging import getLogger
-from typing import List
 
-from django.contrib.auth.decorators import login_required, permission_required
+from django.contrib.auth.decorators import login_required
 from django.db.models import Q
 from django.http import HttpResponse, HttpResponseBadRequest
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.utils import timezone
-from django.utils.dateparse import parse_datetime
-from django.utils.text import slugify
 from django.views.decorators.http import require_GET, require_http_methods
 
 from NEMO.decorators import user_office_or_manager_required
 from NEMO.forms import TemporaryPhysicalAccessRequestForm
 from NEMO.models import (
+	Area,
 	Notification,
 	PhysicalAccessLevel,
 	RequestStatus,
@@ -26,19 +23,15 @@
 from NEMO.utilities import (
 	BasicDisplayTable,
 	EmailCategory,
-	beginning_of_the_day,
 	bootstrap_primary_color,
 	export_format_datetime,
-	format_datetime,
 	get_full_url,
 	quiet_int,
 	render_email_template,
 	send_mail,
+	slugify_underscore,
 )
 from NEMO.views.customization import (
-	ApplicationCustomization,
-	CustomizationBase,
-	EmailsCustomization,
 	UserRequestsCustomization,
 	get_media_file_contents,
 )
@@ -55,22 +48,34 @@ def access_requests(request):
 	max_requests = quiet_int(UserRequestsCustomization.get("access_requests_display_max"), None)
 	physical_access_requests = TemporaryPhysicalAccessRequest.objects.filter(deleted=False)
 	physical_access_requests = physical_access_requests.order_by("-end_time")
-	if not user.is_facility_manager and not user.is_staff and not user.is_user_office:
-		# For some reason doing an "or" filtering with manytomany field returns duplicates, and using distinct() returns nothing...
-		other_user_physical_access_requests = physical_access_requests.filter(other_users__in=[user]).distinct()
-		physical_access_requests = physical_access_requests.filter(
-			Q(creator=user) | Q(id__in=other_user_physical_access_requests)
-		)
+
+	# For some reason doing an "or" filtering with manytomany field returns duplicates, and using distinct() returns nothing...
+	other_user_physical_access_requests = physical_access_requests.filter(other_users__in=[user]).distinct()
+	my_requests = physical_access_requests.filter(Q(creator=user) | Q(id__in=other_user_physical_access_requests))
+
+	user_is_reviewer = is_user_a_reviewer(user)
+	user_is_staff = user.is_facility_manager or user.is_user_office or user.is_staff
+	if not user_is_reviewer and not user_is_staff:
+		physical_access_requests = my_requests
+	elif user_is_reviewer:
+		# show all requests the user can review (+ his requests), exclude the rest
+		exclude = []
+		for access_request in physical_access_requests:
+			if user not in access_request.creator_and_other_users() and user not in access_request.reviewers():
+				exclude.append(access_request.pk)
+		physical_access_requests = physical_access_requests.exclude(pk__in=exclude)
 	dictionary = {
 		"pending_access_requests": physical_access_requests.filter(status=RequestStatus.PENDING).order_by("start_time"),
 		"approved_access_requests": physical_access_requests.filter(status=RequestStatus.APPROVED)[:max_requests],
 		"denied_access_requests": physical_access_requests.filter(status=RequestStatus.DENIED)[:max_requests],
 		"expired_access_requests": physical_access_requests.filter(status=RequestStatus.EXPIRED)[:max_requests],
 		"access_requests_description": UserRequestsCustomization.get("access_requests_description"),
-		"access_request_notifications": get_notifications(
-			request.user, Notification.Types.TEMPORARY_ACCESS_REQUEST, delete=not user.is_facility_manager
-		),
+		"access_request_notifications": get_notifications(request.user, Notification.Types.TEMPORARY_ACCESS_REQUEST, delete=False),
+		"user_is_reviewer": user_is_reviewer
 	}
+
+	# Delete notifications for seen requests
+	Notification.objects.filter(user=request.user, notification_type=Notification.Types.TEMPORARY_ACCESS_REQUEST, object_id__in=my_requests).delete()
 	return render(request, "requests/access_requests/access_requests.html", dictionary)
 
 
@@ -97,8 +102,8 @@ def create_access_request(request, request_id=None):
 				errors.append("You are not allowed to edit expired or deleted requests.")
 			if access_request.status != RequestStatus.PENDING:
 				errors.append("Only pending requests can be modified.")
-			if access_request.creator != user and not user.is_facility_manager:
-				errors.append("You are not allowed to edit a request you didn't create.")
+			if access_request.creator != user and not user in access_request.reviewers():
+				errors.append("You are not allowed to edit this request.")
 
 		form = TemporaryPhysicalAccessRequestForm(
 			request.POST, instance=access_request, initial={"creator": access_request.creator if edit else user}
@@ -109,13 +114,13 @@ def create_access_request(request, request_id=None):
 			form.add_error(None, error)
 
 		cleaned_data = form.clean()
-		if cleaned_data and not user.is_facility_manager and cleaned_data.get("start_time") < timezone.now():
+		if cleaned_data and (not edit or user not in access_request.reviewers()) and cleaned_data.get("start_time") < timezone.now():
 			form.add_error("start_time", "The start time must be later than the current time")
 
 		if form.is_valid():
 			if not edit:
 				form.instance.creator = user
-			if edit and user.is_facility_manager:
+			if edit and user in access_request.reviewers():
 				decision = [state for state in ["approve_request", "deny_request"] if state in request.POST]
 				if decision:
 					if next(iter(decision)) == "approve_request":
@@ -129,11 +134,10 @@ def create_access_request(request, request_id=None):
 			new_access_request = form.save()
 			create_access_request_notification(new_access_request)
 			if edit:
-				# remove notification for current user and other facility managers
+				# remove notification for current user and other reviewers
 				delete_notification(Notification.Types.TEMPORARY_ACCESS_REQUEST, new_access_request.id, [user])
-				if user.is_facility_manager:
-					managers = User.objects.filter(is_active=True, is_facility_manager=True)
-					delete_notification(Notification.Types.TEMPORARY_ACCESS_REQUEST, new_access_request.id, managers)
+				if user in access_request.reviewers():
+					delete_notification(Notification.Types.TEMPORARY_ACCESS_REQUEST, new_access_request.id, access_request.reviewers())
 			send_request_received_email(request, new_access_request, edit)
 			return redirect("user_requests", "access")
 		else:
@@ -181,19 +185,21 @@ def mark_requests_expired():
 
 
 def send_request_received_email(request, access_request: TemporaryPhysicalAccessRequest, edit):
-	user_office_email = EmailsCustomization.get("user_office_email_address")
 	access_request_notification_email = get_media_file_contents("access_request_notification_email.html")
-	if user_office_email and access_request_notification_email:
-		# cc facility managers
-		cc_users: List[User] = list(User.objects.filter(is_active=True, is_facility_manager=True))
-		# and other users
-		cc_users.extend(access_request.other_users.all())
+	if access_request_notification_email:
+		# reviewers
+		reviewer_emails = [
+			email
+			for user in access_request.reviewers()
+			for email in user.get_emails(user.get_preferences().email_send_adjustment_request_updates)
+		]
+		# cc creator + other users
+		cc_users = access_request.creator_and_other_users()
 		ccs = [
 			email
 			for user in cc_users
 			for email in user.get_emails(user.get_preferences().email_send_access_request_updates)
 		]
-		ccs.append(user_office_email)
 		status = (
 			"approved"
 			if access_request.status == RequestStatus.APPROVED
@@ -214,101 +220,24 @@ def send_request_received_email(request, access_request: TemporaryPhysicalAccess
 				"access_requests_url": absolute_url,
 			},
 		)
-		email_notification = access_request.creator.get_preferences().email_send_access_request_updates
-		send_mail(
-			subject=f"Your access request for the {access_request.physical_access_level.area} has been {status}",
-			content=message,
-			from_email=user_office_email,
-			to=access_request.creator.get_emails(email_notification),
-			cc=ccs,
-			email_category=EmailCategory.ACCESS_REQUESTS,
-		)
-
-
-@login_required
-@permission_required("NEMO.trigger_timed_services", raise_exception=True)
-@require_GET
-def email_weekend_access_notification(request):
-	return send_email_weekend_access_notification()
-
-
-def send_email_weekend_access_notification():
-	"""
-	Sends a weekend access email to the addresses set in customization with the template provided.
-	The email is sent when the first request (each week) that includes weekend access is approved.
-	If no weekend access requests are made by the given time on the cutoff day (if set), a no access email is sent.
-	"""
-	try:
-		user_office_email = EmailsCustomization.get("user_office_email_address")
-		email_to = UserRequestsCustomization.get("weekend_access_notification_emails")
-		access_contents = get_media_file_contents("weekend_access_email.html")
-		if user_office_email and email_to and access_contents:
-			process_weekend_access_notification(user_office_email, email_to, access_contents)
-	except Exception as error:
-		access_request_logger.error(error)
-	return HttpResponse()
-
-
-def process_weekend_access_notification(user_office_email, email_to, access_contents):
-	today = datetime.today()
-	beginning_of_the_week = beginning_of_the_day(today - timedelta(days=today.weekday()))
-	cutoff_day = UserRequestsCustomization.get("weekend_access_notification_cutoff_day")
-	cutoff_hour = UserRequestsCustomization.get("weekend_access_notification_cutoff_hour")
-	# Set the cutoff in actual datetime format
-	cutoff_datetime = None
-	if cutoff_hour.isdigit() and cutoff_day and cutoff_day.isdigit():
-		cutoff_datetime = (beginning_of_the_week + timedelta(days=int(cutoff_day))).replace(hour=int(cutoff_hour))
-
-	end_of_the_week = beginning_of_the_week + timedelta(weeks=1)
-	beginning_of_the_weekend = beginning_of_the_week + timedelta(days=5)
-
-	# Approved access request that include weekend time do overlap with weekend date interval.
-	approved_weekend_access_requests = TemporaryPhysicalAccessRequest.objects.filter(
-		deleted=False, status=RequestStatus.APPROVED
-	)
-	approved_weekend_access_requests = approved_weekend_access_requests.exclude(start_time__gte=end_of_the_week)
-	approved_weekend_access_requests = approved_weekend_access_requests.exclude(end_time__lte=beginning_of_the_weekend)
-
-	cutoff_time_passed = cutoff_datetime and timezone.now() >= cutoff_datetime
-	last_sent = CustomizationBase.get("weekend_access_notification_last_sent")
-	last_sent_datetime = parse_datetime(last_sent) if last_sent else None
-	if (
-			(not last_sent_datetime or last_sent_datetime < beginning_of_the_week)
-			and access_contents
-			and approved_weekend_access_requests.exists()
-			and not cutoff_time_passed
-	):
-		send_weekend_email_access(True, user_office_email, email_to, access_contents, beginning_of_the_week)
-		CustomizationBase.set("weekend_access_notification_last_sent", str(timezone.now()))
-	if access_contents and cutoff_datetime and not approved_weekend_access_requests.exists():
-		is_cutoff = today.weekday() == int(cutoff_day) and cutoff_datetime.hour == timezone.localtime().hour
-		if is_cutoff:
-			send_weekend_email_access(False, user_office_email, email_to, access_contents, beginning_of_the_week)
-
-
-def send_weekend_email_access(access, user_office_email, email_to, contents, beginning_of_the_week):
-	facility_name = ApplicationCustomization.get("facility_name")
-	recipients = tuple([e for e in email_to.split(",") if e])
-	ccs = [
-		email
-		for manager in User.objects.filter(is_active=True, is_facility_manager=True)
-		for email in manager.get_emails(manager.get_preferences().email_send_access_request_updates)
-	]
-	ccs.append(user_office_email)
-
-	sat = format_datetime(beginning_of_the_week + timedelta(days=5), "SHORT_DATE_FORMAT", as_current_timezone=False)
-	sun = format_datetime(beginning_of_the_week + timedelta(days=6), "SHORT_DATE_FORMAT", as_current_timezone=False)
-
-	subject = f"{'NO w' if not access else 'W'}eekend access for the {facility_name} {sat} - {sun}"
-	message = render_email_template(contents, {"weekend_access": access})
-	send_mail(
-		subject=subject,
-		content=message,
-		from_email=user_office_email,
-		to=recipients,
-		cc=ccs,
-		email_category=EmailCategory.ACCESS_REQUESTS,
-	)
+		if status in ["received", "updated"]:
+			send_mail(
+				subject=f"Access request for the {access_request.physical_access_level.area} {status}",
+				content=message,
+				from_email=access_request.creator.email,
+				to=reviewer_emails,
+				cc=ccs,
+				email_category=EmailCategory.ACCESS_REQUESTS,
+			)
+		else:
+			send_mail(
+				subject=f"Your access request for the {access_request.physical_access_level.area} has been {status}",
+				content=message,
+				from_email=access_request.reviewer.email,
+				to=ccs,
+				cc=reviewer_emails,
+				email_category=EmailCategory.ACCESS_REQUESTS,
+			)
 
 
 @user_office_or_manager_required
@@ -345,8 +274,13 @@ def access_csv_export(request_qs: QuerySetType[TemporaryPhysicalAccessRequest])
 			}
 		)
 
-	name = slugify(UserRequestsCustomization.get("access_requests_title")).replace("-", "_")
+	name = slugify_underscore(UserRequestsCustomization.get("access_requests_title"))
 	filename = f"{name}_{export_format_datetime()}.csv"
 	response = table_result.to_csv()
 	response["Content-Disposition"] = f'attachment; filename="{filename}"'
 	return response
+
+
+def is_user_a_reviewer(user: User) -> bool:
+	is_reviewer_on_any_area = Area.objects.filter(access_request_reviewers__in=[user]).exists()
+	return user.is_facility_manager or is_reviewer_on_any_area
diff --git a/NEMO/views/adjustment_requests.py b/NEMO/views/adjustment_requests.py
index 418e3450..56fa1138 100644
--- a/NEMO/views/adjustment_requests.py
+++ b/NEMO/views/adjustment_requests.py
@@ -3,40 +3,40 @@
 
 from django.contrib.auth.decorators import login_required
 from django.contrib.contenttypes.models import ContentType
-from django.db.models import Q, QuerySet
 from django.http import HttpResponse, HttpResponseBadRequest
 from django.shortcuts import get_object_or_404, redirect, render
 from django.template.defaultfilters import linebreaksbr
 from django.urls import reverse
 from django.utils import timezone
-from django.views.decorators.http import require_GET, require_http_methods, require_POST
+from django.views.decorators.http import require_GET, require_POST, require_http_methods
 
 from NEMO.decorators import accounting_or_user_office_or_manager_required
 from NEMO.forms import AdjustmentRequestForm
 from NEMO.mixins import BillableItemMixin
 from NEMO.models import (
     AdjustmentRequest,
+    Area,
     AreaAccessRecord,
     Notification,
     RequestMessage,
     RequestStatus,
     Reservation,
     StaffCharge,
+    Tool,
     UsageEvent,
     User,
 )
-from NEMO.typing import QuerySetType
 from NEMO.utilities import (
     BasicDisplayTable,
-    bootstrap_primary_color,
     EmailCategory,
+    bootstrap_primary_color,
     export_format_datetime,
     get_full_url,
     quiet_int,
     render_email_template,
     send_mail,
 )
-from NEMO.views.customization import (EmailsCustomization, get_media_file_contents, UserRequestsCustomization)
+from NEMO.views.customization import (EmailsCustomization, UserRequestsCustomization, get_media_file_contents)
 from NEMO.views.notifications import (
     create_adjustment_request_notification,
     create_request_message_notification,
@@ -54,13 +54,18 @@ def adjustment_requests(request):
     user: User = request.user
     max_requests = quiet_int(UserRequestsCustomization.get("adjustment_requests_display_max"), None)
     adj_requests = AdjustmentRequest.objects.filter(deleted=False)
-    if not user.is_facility_manager and not user.is_user_office and not user.is_accounting_officer:
-        adj_requests = adj_requests.filter(creator=user)
-    if user.is_facility_manager and (user.get_preferences().tool_adjustment_notifications.exists() or user.get_preferences().area_adjustment_notifications.exists()):
+    my_requests = adj_requests.filter(creator=user)
+
+    user_is_reviewer = is_user_a_reviewer(user)
+    user_is_staff = user.is_facility_manager or user.is_user_office or user.is_accounting_officer
+    if not user_is_reviewer and not user_is_staff:
+        # only show own requests
+        adj_requests = my_requests
+    elif user_is_reviewer:
+        # show all requests the user can review, exclude the rest
         exclude = []
         for adj in adj_requests:
-            managers = managers_for_adjustment_request(adj)
-            if user not in managers:
+            if user != adj.creator and user not in adj.reviewers():
                 exclude.append(adj.pk)
         adj_requests = adj_requests.exclude(pk__in=exclude)
 
@@ -69,11 +74,13 @@ def adjustment_requests(request):
         "approved_adjustment_requests": adj_requests.filter(status=RequestStatus.APPROVED)[:max_requests],
         "denied_adjustment_requests": adj_requests.filter(status=RequestStatus.DENIED)[:max_requests],
         "adjustment_requests_description": UserRequestsCustomization.get("adjustment_requests_description"),
-        "request_notifications": get_notifications(
-            request.user, Notification.Types.ADJUSTMENT_REQUEST, delete=not user.is_facility_manager
-        ),
+        "request_notifications": get_notifications(request.user, Notification.Types.ADJUSTMENT_REQUEST, delete=False),
         "reply_notifications": get_notifications(request.user, Notification.Types.ADJUSTMENT_REQUEST_REPLY),
+        "user_is_reviewer": user_is_reviewer
     }
+
+    # Delete notifications for seen requests
+    Notification.objects.filter(user=request.user, notification_type=Notification.Types.ADJUSTMENT_REQUEST, object_id__in=my_requests).delete()
     return render(request, "requests/adjustment_requests/adjustment_requests.html", dictionary)
 
 
@@ -118,8 +125,8 @@ def create_adjustment_request(request, request_id=None, item_type_id=None, item_
                 errors.append("You are not allowed to edit deleted requests.")
             if adjustment_request.status != RequestStatus.PENDING:
                 errors.append("Only pending requests can be modified.")
-            if adjustment_request.creator != user and not user.is_facility_manager:
-                errors.append("You are not allowed to edit a request you didn't create.")
+            if adjustment_request.creator != user and user not in adjustment_request.reviewers():
+                errors.append("You are not allowed to edit this request.")
 
         form = AdjustmentRequestForm(
             request.POST,
@@ -134,7 +141,7 @@ def create_adjustment_request(request, request_id=None, item_type_id=None, item_
         if form.is_valid():
             if not edit:
                 form.instance.creator = user
-            if edit and user.is_facility_manager:
+            if edit and user in adjustment_request.reviewers():
                 decision = [state for state in ["approve_request", "deny_request"] if state in request.POST]
                 if decision:
                     if next(iter(decision)) == "approve_request":
@@ -146,16 +153,15 @@ def create_adjustment_request(request, request_id=None, item_type_id=None, item_
             form.instance.last_updated_by = user
             new_adjustment_request = form.save()
 
-            managers_to_notify: Set[User] = set(list(managers_for_adjustment_request(adjustment_request)))
+            reviewers: Set[User] = set(list(adjustment_request.reviewers()))
 
-            create_adjustment_request_notification(new_adjustment_request, managers_to_notify)
+            create_adjustment_request_notification(new_adjustment_request)
             if edit:
-                # remove notification for current user and other facility managers
+                # remove notification for current user and other reviewers
                 delete_notification(Notification.Types.ADJUSTMENT_REQUEST, adjustment_request.id, [user])
-                if user.is_facility_manager:
-                    managers = User.objects.filter(is_active=True, is_facility_manager=True)
-                    delete_notification(Notification.Types.ADJUSTMENT_REQUEST, adjustment_request.id, managers)
-            send_request_received_email(request, new_adjustment_request, edit, managers_to_notify)
+                if user in reviewers:
+                    delete_notification(Notification.Types.ADJUSTMENT_REQUEST, adjustment_request.id, reviewers)
+            send_request_received_email(request, new_adjustment_request, edit, reviewers)
             return redirect("user_requests", "adjustment")
         else:
             item_type = form.cleaned_data.get("item_type")
@@ -183,8 +189,8 @@ def adjustment_request_reply(request, request_id):
 
     if adjustment_request.status != RequestStatus.PENDING:
         return HttpResponseBadRequest("Replies are only allowed on PENDING requests")
-    elif user != adjustment_request.creator and not user.is_facility_manager:
-        return HttpResponseBadRequest("Only the creator and managers can reply to adjustment requests")
+    elif user != adjustment_request.creator and user not in adjustment_request.reviewers():
+        return HttpResponseBadRequest("Only the creator and reviewers can reply to adjustment requests")
     elif message_content:
         reply = RequestMessage()
         reply.content_object = adjustment_request
@@ -217,14 +223,14 @@ def delete_adjustment_request(request, request_id):
     return redirect("user_requests", "adjustment")
 
 
-def send_request_received_email(request, adjustment_request: AdjustmentRequest, edit, facility_managers: Set[User]):
+def send_request_received_email(request, adjustment_request: AdjustmentRequest, edit, reviewers: Set[User]):
     user_office_email = EmailsCustomization.get("user_office_email_address")
     adjustment_request_notification_email = get_media_file_contents("adjustment_request_notification_email.html")
     if user_office_email and adjustment_request_notification_email:
-        # cc facility managers
-        manager_emails = [
+        # reviewers
+        reviewer_emails = [
             email
-            for user in facility_managers
+            for user in reviewers
             for email in user.get_emails(user.get_preferences().email_send_adjustment_request_updates)
         ]
         status = (
@@ -253,7 +259,7 @@ def send_request_received_email(request, adjustment_request: AdjustmentRequest,
                 subject=f"Adjustment request {status}",
                 content=message,
                 from_email=adjustment_request.creator.email,
-                to=manager_emails,
+                to=reviewer_emails,
                 cc=adjustment_request.creator.get_emails(creator_notification),
                 email_category=EmailCategory.ADJUSTMENT_REQUESTS,
             )
@@ -263,7 +269,7 @@ def send_request_received_email(request, adjustment_request: AdjustmentRequest,
                 content=message,
                 from_email=adjustment_request.reviewer.email,
                 to=adjustment_request.creator.get_emails(creator_notification),
-                cc=manager_emails,
+                cc=reviewer_emails,
                 email_category=EmailCategory.ADJUSTMENT_REQUESTS,
             )
 
@@ -277,7 +283,7 @@ def send_request_received_email(request, adjustment_request: AdjustmentRequest,
                 content=message,
                 from_email=adjustment_request.reviewer.email,
                 to=[user_office_email],
-                cc=manager_emails,
+                cc=reviewer_emails,
                 email_category=EmailCategory.ADJUSTMENT_REQUESTS,
             )
 
@@ -393,22 +399,7 @@ def adjustments_csv_export(request_list: List[AdjustmentRequest]) -> HttpRespons
     return response
 
 
-def managers_for_adjustment_request(adjustment_request: AdjustmentRequest) -> QuerySetType[User]:
-    # Create the list of facility managers to notify/show request to. If the adjustment request has a tool/area and no
-    # one added that tool/area to their adjustment request list, send/show to everyone (otherwise nobody would see it)
-    tool = getattr(adjustment_request.item, "tool", None) if adjustment_request.item else None
-    area = getattr(adjustment_request.item, "area", None) if adjustment_request.item else None
-    reviewers = User.objects.filter(is_active=True, is_facility_manager=True)
-    if tool:
-        reviewers_filter = reviewers.filter(Q(preferences__tool_adjustment_notifications__isnull=True) | Q(
-            preferences__tool_adjustment_notifications__in=[tool]))
-        if reviewers_filter.exists():
-            # Only limit managers if at least one person will receive the notification.
-            reviewers = reviewers_filter
-    if area:
-        reviewers_filter = reviewers.filter(Q(preferences__area_adjustment_notifications__isnull=True) | Q(
-            preferences__area_adjustment_notifications__in=[area]))
-        if reviewers_filter.exists():
-            # Only limit managers if at least one person will receive the notification.
-            reviewers = reviewers_filter
-    return reviewers
+def is_user_a_reviewer(user: User) -> bool:
+    is_reviewer_on_any_tool = Tool.objects.filter(_adjustment_request_reviewers__in=[user]).exists()
+    is_reviewer_on_any_area = Area.objects.filter(adjustment_request_reviewers__in=[user]).exists()
+    return user.is_facility_manager or is_reviewer_on_any_tool or is_reviewer_on_any_area
diff --git a/NEMO/views/api.py b/NEMO/views/api.py
index 10f628d0..985883bc 100644
--- a/NEMO/views/api.py
+++ b/NEMO/views/api.py
@@ -2,9 +2,11 @@
 
 from django.contrib.auth.models import Group, Permission
 from django.contrib.contenttypes.models import ContentType
+from django.db import transaction
 from django.utils.safestring import mark_safe
 from drf_excel.mixins import XLSXFileMixin
 from rest_framework import status, viewsets
+from rest_framework.exceptions import ValidationError
 from rest_framework.renderers import BrowsableAPIRenderer
 from rest_framework.response import Response
 from rest_framework.serializers import ListSerializer
@@ -30,6 +32,7 @@
 	UsageEvent,
 	User,
 )
+from NEMO.rest_pagination import NEMOPageNumberPagination
 from NEMO.serializers import (
 	AccountSerializer,
 	AccountTypeSerializer,
@@ -88,11 +91,23 @@ class ModelViewSet(XLSXFileMixin, viewsets.ModelViewSet):
 	Also allows XLSX retrieval
 	"""
 
+	# Bypass pagination when exporting into any format that's not the browsable API
+	def paginate_queryset(self, queryset):
+		page_size_override = self.request and self.request.GET.get(NEMOPageNumberPagination.page_size_query_param, None)
+		renderer = self.request.accepted_renderer if self.request and hasattr(self.request, "accepted_renderer") else None
+		if page_size_override is not None or not renderer or isinstance(renderer, BrowsableAPIRenderer):
+			return super().paginate_queryset(queryset)
+		return None
+
+	@transaction.atomic
 	def create(self, request, *args, **kwargs):
 		many = isinstance(request.data, list)
 		serializer = self.get_serializer(data=request.data, many=many)
 		serializer.is_valid(raise_exception=True)
-		self.perform_create(serializer)
+		try:
+			self.perform_create(serializer)
+		except Exception as e:
+			raise ValidationError({"error": str(e)})
 		headers = self.get_success_headers(serializer.data)
 		return Response(serializer.data, headers=headers)
 
@@ -118,11 +133,11 @@ class UserViewSet(ModelViewSet):
 		"id": ["exact", "in"],
 		"type": ["exact", "in"],
 		"domain": ["exact", "in", "isempty"],
-		"username": ["iexact", "in"],
-		"first_name": ["iexact", "icontains"],
-		"last_name": ["iexact", "icontains"],
-		"email": ["iexact", "icontains"],
-		"badge_number": ["iexact", "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"],
@@ -141,7 +156,7 @@ class ProjectDisciplineViewSet(ModelViewSet):
 	filename = "project_disciplines"
 	queryset = ProjectDiscipline.objects.all()
 	serializer_class = ProjectDisciplineSerializer
-	filterset_fields = {"id": ["exact", "in"], "name": ["iexact"], "display_order": ["exact"]}
+	filterset_fields = {"id": ["exact", "in"], "name": ["exact", "iexact"], "display_order": ["exact"]}
 
 
 class ProjectViewSet(ModelViewSet):
@@ -150,7 +165,7 @@ class ProjectViewSet(ModelViewSet):
 	serializer_class = ProjectSerializer
 	filterset_fields = {
 		"id": ["exact", "in"],
-		"name": ["iexact"],
+		"name": ["exact", "iexact"],
 		"application_identifier": ["exact"],
 		"active": ["exact"],
 		"account_id": ["exact", "in"],
@@ -177,11 +192,11 @@ class ToolViewSet(ModelViewSet):
 	serializer_class = ToolSerializer
 	filterset_fields = {
 		"id": ["exact", "in"],
-		"name": ["exact"],
+		"name": ["exact", "iexact"],
 		"visible": ["exact"],
-		"_category": ["exact"],
+		"_category": ["exact", "iexact"],
 		"_operational": ["exact"],
-		"_location": ["exact"],
+		"_location": ["exact", "iexact"],
 		"_requires_area_access": ["exact", "isnull"],
 		"_post_usage_questions": ["isempty"],
 	}
@@ -205,9 +220,9 @@ class AreaViewSet(ModelViewSet):
 	serializer_class = AreaSerializer
 	filterset_fields = {
 		"id": ["exact", "in"],
-		"name": ["exact"],
+		"name": ["exact", "iexact"],
 		"parent_area": ["exact", "in"],
-		"category": ["exact"],
+		"category": ["exact", "iexact"],
 		"requires_reservation": ["exact"],
 		"buddy_system_allowed": ["exact"],
 		"maximum_capacity": ["exact", "gte", "gt", "lte", "lt", "isnull"],
@@ -222,7 +237,7 @@ class ResourceViewSet(ModelViewSet):
 	serializer_class = ResourceSerializer
 	filterset_fields = {
 		"id": ["exact", "in"],
-		"name": ["exact"],
+		"name": ["exact", "iexact"],
 		"available": ["exact"],
 		"fully_dependent_tools": ["in"],
 		"partially_dependent_tools": ["in"],
@@ -246,6 +261,7 @@ class ReservationViewSet(ModelViewSet):
 		"cancelled": ["exact"],
 		"missed": ["exact"],
 		"validated": ["exact"],
+		"validated_by": ["exact", "in", "isnull"],
 		"question_data": ["isempty"],
 	}
 
@@ -263,6 +279,7 @@ class UsageEventViewSet(ModelViewSet):
 		"operator_id": ["exact", "in"],
 		"tool_id": ["exact", "in"],
 		"validated": ["exact"],
+		"validated_by": ["exact", "in", "isnull"],
 	}
 
 
@@ -279,6 +296,7 @@ class AreaAccessRecordViewSet(ModelViewSet):
 		"area_id": ["exact", "in"],
 		"staff_charge_id": ["exact", "isnull", "in"],
 		"validated": ["exact"],
+		"validated_by": ["exact", "in", "isnull"],
 	}
 
 
@@ -332,6 +350,7 @@ class StaffChargeViewSet(ModelViewSet):
 		"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"],
 	}
 
@@ -351,6 +370,7 @@ class TrainingSessionViewSet(ModelViewSet):
 		"date": ["month", "year", "day", "gte", "gt", "lte", "lt"],
 		"qualified": ["exact"],
 		"validated": ["exact"],
+		"validated_by": ["exact", "in", "isnull"],
 	}
 
 
@@ -358,7 +378,7 @@ class ConsumableCategoryViewSet(ModelViewSet):
 	filename = "consumable_categories"
 	queryset = ConsumableCategory.objects.all()
 	serializer_class = ConsumableCategorySerializer
-	filterset_fields = {"id": ["exact", "in"], "name": ["iexact"]}
+	filterset_fields = {"id": ["exact", "in"], "name": ["exact", "iexact"]}
 
 
 class ConsumableViewSet(ModelViewSet):
@@ -389,6 +409,7 @@ class ConsumableWithdrawViewSet(ModelViewSet):
 		"quantity": ["exact", "gte", "lte", "gt", "lt"],
 		"date": ["month", "year", "day", "gte", "gt", "lte", "lt"],
 		"validated": ["exact"],
+		"validated_by": ["exact", "in", "isnull"],
 	}
 
 
@@ -410,7 +431,7 @@ class GroupViewSet(ModelViewSet):
 	queryset = Group.objects.all()
 	serializer_class = GroupSerializer
 	filterset_fields = {
-		"name": ["exact", "in"],
+		"name": ["exact", "iexact", "in"],
 		"permissions": ["exact"],
 	}
 
@@ -421,8 +442,8 @@ class PermissionViewSet(XLSXFileMixin, viewsets.ReadOnlyModelViewSet):
 	queryset = Permission.objects.all()
 	serializer_class = PermissionSerializer
 	filterset_fields = {
-		"name": ["exact", "in"],
-		"codename": ["exact", "in"],
+		"name": ["exact", "iexact", "in"],
+		"codename": ["exact", "iexact", "in"],
 		"content_type_id": ["exact", "in"],
 	}
 
diff --git a/NEMO/views/calendar.py b/NEMO/views/calendar.py
index 26d2c868..a07684ab 100644
--- a/NEMO/views/calendar.py
+++ b/NEMO/views/calendar.py
@@ -1,12 +1,11 @@
-from datetime import date, datetime, timedelta
+from datetime import datetime, timedelta
 from http import HTTPStatus
 from json import dumps, loads
 from logging import getLogger
 from re import match
-from typing import Dict, Iterable, List, Optional, Union
+from typing import List, Optional, Union
 
-from django.contrib.auth.decorators import login_required, permission_required
-from django.core.exceptions import ValidationError
+from django.contrib.auth.decorators import login_required
 from django.db.models import Q
 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, redirect, render
@@ -18,24 +17,16 @@
 
 from NEMO.decorators import disable_session_expiry_refresh, postpone, staff_member_required, synchronized
 from NEMO.exceptions import ProjectChargeException, RequiredUnansweredQuestionsException
-from NEMO.forms import nice_errors
 from NEMO.models import (
-	Alert,
 	Area,
 	AreaAccessRecord,
-	Closure,
-	ClosureTime,
 	Configuration,
-	EmailNotificationType,
 	Project,
-	Qualification,
-	RecurringConsumableCharge,
 	Reservation,
 	ReservationItemType,
 	ReservationQuestions,
 	ScheduledOutage,
 	ScheduledOutageCategory,
-	StaffCharge,
 	Tool,
 	UsageEvent,
 	User,
@@ -43,10 +34,7 @@
 )
 from NEMO.policy import policy_class as policy
 from NEMO.utilities import (
-	EmailCategory,
 	RecurrenceFrequency,
-	as_timezone,
-	beginning_of_the_day,
 	bootstrap_primary_color,
 	create_ics,
 	date_input_format,
@@ -68,9 +56,6 @@
 	ApplicationCustomization,
 	CalendarCustomization,
 	EmailsCustomization,
-	RecurringChargesCustomization,
-	ToolCustomization,
-	UserCustomization,
 	get_media_file_contents,
 )
 from NEMO.widgets.dynamic_form import DynamicForm, render_group_questions
@@ -218,6 +203,7 @@ def reservation_event_feed(request, start, end):
 	all_tools = request.GET.get('all_tools')
 	all_areas = request.GET.get('all_areas')
 	all_areastools = request.GET.get('all_areastools')
+	display_name = request.GET.get("display_name") == "true"
 
 	# Filter events that only have to do with the relevant tool/area.
 	item_type = request.GET.get('item_type')
@@ -253,6 +239,7 @@ def reservation_event_feed(request, start, end):
 		'all_tools': all_tools,
 		'all_areas': all_areas,
 		'all_areastools': all_areastools,
+		'display_name': display_name,
 	}
 	return render(request, 'calendar/reservation_event_feed.html', dictionary)
 
@@ -742,6 +729,24 @@ def set_reservation_title(request, reservation_id):
 	return HttpResponse()
 
 
+@staff_member_required
+@require_POST
+def change_outage_title(request, outage_id):
+	outage = get_object_or_404(ScheduledOutage, id=outage_id)
+	outage.title = request.POST.get("title", "")[:outage._meta.get_field("title").max_length]
+	outage.save(update_fields=["title"])
+	return HttpResponse()
+
+
+@staff_member_required
+@require_POST
+def change_outage_details(request, outage_id):
+	outage = get_object_or_404(ScheduledOutage, id=outage_id)
+	outage.details = request.POST.get("details", "")
+	outage.save(update_fields=["details"])
+	return HttpResponse()
+
+
 @login_required
 @require_POST
 def change_reservation_date(request):
@@ -801,259 +806,6 @@ def change_reservation_project(request, reservation_id):
 	return HttpResponse()
 
 
-@login_required
-@permission_required('NEMO.trigger_timed_services', raise_exception=True)
-@require_GET
-def email_reservation_reminders(request):
-	return send_email_reservation_reminders(request)
-
-
-def send_email_reservation_reminders(request=None):
-	# Exit early if the reservation reminder email template has not been customized for the organization yet.
-	reservation_reminder_message = get_media_file_contents('reservation_reminder_email.html')
-	reservation_warning_message = get_media_file_contents('reservation_warning_email.html')
-	if not reservation_reminder_message or not reservation_warning_message:
-		calendar_logger.error("Reservation reminder email couldn't be send because either reservation_reminder_email.html or reservation_warning_email.html is not defined")
-		return HttpResponseNotFound('The reservation reminder and/or warning email templates have not been customized for your organization yet. Please visit the customization page to upload both templates, then reservation reminder email notifications can be sent.')
-
-	# Find all reservations that are two hours from now, plus or minus 5 minutes to allow for time skew.
-	preparation_time = 120
-	tolerance = 5
-	earliest_start = timezone.now() + timedelta(minutes=preparation_time) - timedelta(minutes=tolerance)
-	latest_start = timezone.now() + timedelta(minutes=preparation_time) + timedelta(minutes=tolerance)
-	upcoming_reservations = Reservation.objects.filter(cancelled=False, start__gt=earliest_start, start__lt=latest_start)
-	# Email a reminder to each user with an upcoming reservation.
-	for reservation in upcoming_reservations:
-		item = reservation.reservation_item
-		item_type = reservation.reservation_item_type
-		if item_type == ReservationItemType.TOOL and item.operational and not item.problematic() and item.all_resources_available()\
-				or item_type == ReservationItemType.AREA and not item.required_resource_is_unavailable():
-			subject = item.name + " reservation reminder"
-			rendered_message = render_email_template(reservation_reminder_message, {'reservation': reservation, 'template_color': bootstrap_primary_color('success')}, request)
-		elif (item_type == ReservationItemType.TOOL and not item.operational) or item.required_resource_is_unavailable():
-			subject = item.name + " reservation problem"
-			rendered_message = render_email_template(reservation_warning_message, {'reservation': reservation, 'template_color': bootstrap_primary_color('danger'), 'fatal_error': True}, request)
-		else:
-			subject = item.name + " reservation warning"
-			rendered_message = render_email_template(reservation_warning_message, {'reservation': reservation, 'template_color': bootstrap_primary_color('warning'), 'fatal_error': False}, request)
-		user_office_email = EmailsCustomization.get('user_office_email_address')
-		email_notification = reservation.user.get_preferences().email_send_reservation_reminders
-		reservation.user.email_user(subject=subject, message=rendered_message, from_email=user_office_email, email_category=EmailCategory.TIMED_SERVICES, email_notification=email_notification)
-	return HttpResponse()
-
-
-@login_required
-@permission_required('NEMO.trigger_timed_services', raise_exception=True)
-@require_GET
-def email_reservation_ending_reminders(request):
-	return send_email_reservation_ending_reminders(request)
-
-
-def send_email_reservation_ending_reminders(request=None):
-	# Exit early if the reservation ending reminder email template has not been customized for the organization yet.
-	reservation_ending_reminder_message = get_media_file_contents('reservation_ending_reminder_email.html')
-	user_office_email = EmailsCustomization.get('user_office_email_address')
-	if not reservation_ending_reminder_message:
-		calendar_logger.error("Reservation ending reminder email couldn't be send because either reservation_ending_reminder_email.html is not defined")
-		return HttpResponseNotFound('The reservation ending reminder template has not been customized for your organization yet. Please visit the customization page to upload one, then reservation ending reminder email notifications can be sent.')
-
-	# We only send ending reservation reminders to users that are currently logged in
-	current_logged_in_user = AreaAccessRecord.objects.filter(end=None, staff_charge=None, customer__is_staff=False)
-	valid_reservations = Reservation.objects.filter(cancelled=False, missed=False, shortened=False)
-	user_area_reservations = valid_reservations.filter(area__isnull=False, user__in=current_logged_in_user.values_list('customer', flat=True))
-
-	# Find all reservations that end 30 or 15 min from now, plus or minus 3 minutes to allow for time skew.
-	reminder_times = [30, 15]
-	tolerance = 3
-	time_filter = Q()
-	for reminder_time in reminder_times:
-		earliest_end = timezone.now() + timedelta(minutes=reminder_time) - timedelta(minutes=tolerance)
-		latest_end = timezone.now() + timedelta(minutes=reminder_time) + timedelta(minutes=tolerance)
-		new_filter = Q(end__gt=earliest_end, end__lt=latest_end)
-		time_filter = time_filter | new_filter
-	ending_reservations = user_area_reservations.filter(time_filter)
-	# Email a reminder to each user with a reservation ending soon.
-	for reservation in ending_reservations:
-		subject = reservation.reservation_item.name + " reservation ending soon"
-		rendered_message = render_email_template(reservation_ending_reminder_message, {'reservation': reservation}, request)
-		email_notification = reservation.user.get_preferences().email_send_reservation_ending_reminders
-		reservation.user.email_user(subject=subject, message=rendered_message, from_email=user_office_email, email_category=EmailCategory.TIMED_SERVICES, email_notification=email_notification)
-	return HttpResponse()
-
-
-@login_required
-@permission_required('NEMO.trigger_timed_services', raise_exception=True)
-@require_GET
-def email_usage_reminders(request):
-	projects_to_exclude = request.GET.getlist("projects_to_exclude[]")
-	return send_email_usage_reminders(projects_to_exclude, request)
-
-
-def send_email_usage_reminders(projects_to_exclude=None, request=None):
-	if projects_to_exclude is None:
-		projects_to_exclude = []
-	busy_users = AreaAccessRecord.objects.none()
-	if ApplicationCustomization.get_bool("area_in_usage_reminders"):
-		busy_users = AreaAccessRecord.objects.filter(end=None, staff_charge=None).exclude(project__id__in=projects_to_exclude)
-	busy_tools = UsageEvent.objects.filter(end=None).exclude(project__id__in=projects_to_exclude)
-
-	# Make lists of all the things a user is logged in to.
-	# We don't want to send 3 separate emails if a user is logged into three things.
-	# Just send one email for all the things!
-	aggregate = {}
-	for access_record in busy_users:
-		key = access_record.customer_id
-		aggregate[key] = {
-			'user': access_record.customer,
-			'resources_in_use': [access_record.area],
-		}
-	for usage_event in busy_tools:
-		key = usage_event.operator_id
-		if key in aggregate:
-			aggregate[key]['resources_in_use'].append(usage_event.tool.name)
-		else:
-			aggregate[key] = {
-				'user': usage_event.operator,
-				'resources_in_use': [usage_event.tool],
-			}
-
-	user_office_email = EmailsCustomization.get('user_office_email_address')
-
-	message = get_media_file_contents('usage_reminder_email.html')
-	facility_name = ApplicationCustomization.get('facility_name')
-	if message:
-		subject = f"{facility_name} usage"
-		for value in aggregate.values():
-			user: User = value["user"]
-			resources_in_use = value["resources_in_use"]
-			# for backwards compatibility, add it to the user object (that's how it was defined and used in the template)
-			user.resources_in_use = resources_in_use
-			rendered_message = render_email_template(message, {"user": user, "resources_in_use": resources_in_use}, request)
-			email_notification = user.get_preferences().email_send_usage_reminders
-			user.email_user(subject=subject, message=rendered_message, from_email=user_office_email, email_category=EmailCategory.TIMED_SERVICES, email_notification=email_notification)
-
-	message = get_media_file_contents('staff_charge_reminder_email.html')
-	if message:
-		busy_staff = StaffCharge.objects.filter(end=None)
-		for staff_charge in busy_staff:
-			subject = "Active staff charge since " + format_datetime(staff_charge.start)
-			rendered_message = render_email_template(message, {'staff_charge': staff_charge}, request)
-			email_notification = staff_charge.staff_member.get_preferences().email_send_usage_reminders
-			staff_charge.staff_member.email_user(subject=subject, message=rendered_message, from_email=user_office_email, email_category=EmailCategory.TIMED_SERVICES, email_notification=email_notification)
-
-	return HttpResponse()
-
-
-@login_required
-@require_GET
-@permission_required('NEMO.trigger_timed_services', raise_exception=True)
-def cancel_unused_reservations(request):
-	return do_cancel_unused_reservations(request)
-
-
-def do_cancel_unused_reservations(request=None):
-	"""
-	Missed reservation for tools is when there is no tool activity during the reservation time + missed reservation threshold.
-	Any tool usage will count, since we don't want to charge for missed reservation when users swap reservation or somebody else gets to use the tool.
-
-	Missed reservation for areas is when there is no area access login during the reservation time + missed reservation threshold
-	"""
-
-	# Missed Tool Reservations
-	tools = Tool.objects.filter(visible=True, _operational=True, _missed_reservation_threshold__isnull=False)
-	missed_reservations = []
-	for tool in tools:
-		# If a tool is in use then there's no need to look for unused reservation time.
-		if tool.in_use() or tool.required_resource_is_unavailable() or tool.scheduled_outage_in_progress():
-			continue
-		# Calculate the timestamp of how long a user can be late for a reservation.
-		threshold = (timezone.now() - timedelta(minutes=tool.missed_reservation_threshold))
-		threshold = datetime.replace(threshold, second=0, microsecond=0)  # Round down to the nearest minute.
-		# Find the reservations that began exactly at the threshold.
-		reservation = Reservation.objects.filter(cancelled=False, missed=False, shortened=False, tool=tool, user__is_staff=False, start=threshold, end__gt=timezone.now())
-		for r in reservation:
-			# Staff may abandon reservations.
-			if r.user.is_staff:
-				continue
-			# If there was no tool enable or disable event since the threshold timestamp then we assume the reservation has been missed.
-			if not (UsageEvent.objects.filter(tool_id__in=tool.get_family_tool_ids(), start__gte=threshold).exists() or UsageEvent.objects.filter(tool_id__in=tool.get_family_tool_ids(), end__gte=threshold).exists()):
-				# Mark the reservation as missed and notify the user & staff.
-				r.missed = True
-				r.save()
-				missed_reservations.append(r)
-
-	# Missed Area Reservations
-	areas = Area.objects.filter(missed_reservation_threshold__isnull=False)
-	for area in areas:
-		# if area has outage or required resource is unavailable, no need to look
-		if area.required_resource_is_unavailable() or area.scheduled_outage_in_progress():
-			continue
-
-		# Calculate the timestamp of how long a user can be late for a reservation.
-		threshold = (timezone.now() - timedelta(minutes=area.missed_reservation_threshold))
-		threshold = datetime.replace(threshold, second=0, microsecond=0)  # Round down to the nearest minute.
-		# Find the reservations that began exactly at the threshold.
-		reservation = Reservation.objects.filter(cancelled=False, missed=False, shortened=False, area=area, user__is_staff=False, start=threshold, end__gt=timezone.now())
-		for r in reservation:
-			# Staff may abandon reservations.
-			if r.user.is_staff:
-				continue
-			# if the user is not already logged in or if there was no area access starting or ending since the threshold timestamp then we assume the reservation was missed
-			if not (AreaAccessRecord.objects.filter(area__id=area.id, customer=r.user, staff_charge=None, end=None).exists() or AreaAccessRecord.objects.filter(area__id=area.id, customer=r.user, start__gte=threshold).exists() or AreaAccessRecord.objects.filter(area__id=area.id, customer=r.user, end__gte=threshold).exists()):
-				# Mark the reservation as missed and notify the user & staff.
-				r.missed = True
-				r.save()
-				missed_reservations.append(r)
-
-	for r in missed_reservations:
-		send_missed_reservation_notification(r, request)
-
-	return HttpResponse()
-
-
-@login_required
-@require_GET
-@permission_required('NEMO.trigger_timed_services', raise_exception=True)
-def email_out_of_time_reservation_notification(request):
-	return send_email_out_of_time_reservation_notification(request)
-
-
-def send_email_out_of_time_reservation_notification(request=None):
-	"""
-	Out of time reservation notification for areas is when a user is still logged in an area but his reservation expired.
-	"""
-	# Exit early if the out of time reservation email template has not been customized for the organization yet.
-	# This feature only sends emails, so if the template is not defined there nothing to do.
-	if not get_media_file_contents('out_of_time_reservation_email.html'):
-		return HttpResponseNotFound('The out of time reservation email template has not been customized for your organization yet. Please visit the customization page to upload a template, then out of time email notifications can be sent.')
-
-	out_of_time_user_area = []
-
-	# Find all logged users
-	access_records: List[AreaAccessRecord] = AreaAccessRecord.objects.filter(end=None, staff_charge=None).prefetch_related('customer', 'area').only('customer', 'area')
-	for access_record in access_records:
-		# staff and service personnel are exempt from out of time notification
-		customer = access_record.customer
-		area = access_record.area
-		if customer.is_staff or customer.is_service_personnel:
-			continue
-
-		if area.requires_reservation:
-			# Calculate the timestamp of how late a user can be logged in after a reservation ended.
-			threshold = timezone.now() if not area.logout_grace_period else timezone.now() - timedelta(minutes=area.logout_grace_period)
-			threshold = datetime.replace(threshold, second=0, microsecond=0)  # Round down to the nearest minute.
-			ending_reservations = Reservation.objects.filter(cancelled=False, missed=False, shortened=False, area=area, user=customer, start__lte=timezone.now(), end=threshold)
-			# find out if a reservation is starting right at the same time (in case of back to back reservations, in which case customer is good)
-			starting_reservations = Reservation.objects.filter(cancelled=False, missed=False, shortened=False, area=area, user=customer, start=threshold)
-			if ending_reservations.exists() and not starting_reservations.exists():
-				out_of_time_user_area.append(ending_reservations[0])
-
-	for reservation in out_of_time_user_area:
-		send_out_of_time_reservation_notification(reservation, request)
-
-	return HttpResponse()
-
-
 @staff_member_required
 @require_GET
 def proxy_reservation(request):
@@ -1112,6 +864,7 @@ def shorten_reservation(user: User, item: Union[Area, Tool], new_end: datetime =
 			current_reservation.shortened = True
 			current_reservation.descendant = new_reservation
 			current_reservation.save()
+			send_tool_free_time_notification(None, current_reservation, new_reservation, missed_or_shortened=True)
 	except Reservation.DoesNotExist:
 		pass
 
@@ -1167,34 +920,6 @@ def cancel_the_reservation(reservation: Reservation, user_cancelling_reservation
 	return response
 
 
-def send_missed_reservation_notification(reservation, request=None):
-	message = get_media_file_contents('missed_reservation_email.html')
-	user_office_email = EmailsCustomization.get('user_office_email_address')
-	abuse_email = EmailsCustomization.get('abuse_email_address')
-	if message and user_office_email:
-		subject = "Missed reservation for the " + str(reservation.reservation_item)
-		message = render_email_template(message, {'reservation': reservation}, request)
-		recipients = reservation.user.get_emails(reservation.user.get_preferences().email_send_reservation_emails)
-		recipients.append(abuse_email)
-		recipients.append(user_office_email)
-		send_mail(subject=subject, content=message, from_email=user_office_email, to=recipients, email_category=EmailCategory.TIMED_SERVICES)
-	else:
-		calendar_logger.error("Missed reservation email couldn't be send because missed_reservation_email.html or user_office_email are not defined")
-
-
-def send_out_of_time_reservation_notification(reservation: Reservation, request=None):
-	message = get_media_file_contents('out_of_time_reservation_email.html')
-	user_office_email = EmailsCustomization.get('user_office_email_address')
-	if message and user_office_email:
-		subject = "Out of time in the " + str(reservation.area.name)
-		message = render_email_template(message, {'reservation': reservation}, request)
-		recipients = reservation.user.get_emails(reservation.user.get_preferences().email_send_reservation_ending_reminders)
-		recipients.extend(reservation.area.abuse_email_list())
-		send_mail(subject=subject, content=message, from_email=user_office_email, to=recipients, email_category=EmailCategory.TIMED_SERVICES)
-	else:
-		calendar_logger.error("Out of time reservation email couldn't be send because out_of_time_reservation_email.html or user_office_email are not defined")
-
-
 def send_user_created_reservation_notification(reservation: Reservation):
 	user = reservation.user
 	recipients = user.get_emails(user.get_preferences().email_send_reservation_emails) if user.get_preferences().attach_created_reservation else []
@@ -1234,223 +959,10 @@ def send_user_cancelled_reservation_notification(reservation: Reservation):
 			calendar_logger.error("User cancelled reservation notification could not be send because user_office_email_address is not defined")
 
 
-@login_required
-@require_GET
-@permission_required("NEMO.trigger_timed_services", raise_exception=True)
-def create_closure_alerts(request):
-	return do_create_closure_alerts()
-
-
-def do_create_closure_alerts():
-	future_times = ClosureTime.objects.filter(closure__alert_days_before__isnull=False, end_time__gt=timezone.now())
-	for closure_time in future_times:
-		create_alert_for_closure_time(closure_time)
-	for closure in Closure.objects.filter(notify_managers_last_occurrence=True):
-		closure_time_ending = ClosureTime.objects.filter(closure=closure).latest("end_time")
-		if as_timezone(closure_time_ending.end_time).date() == date.today():
-			email_last_closure_occurrence(closure_time_ending)
-	return HttpResponse()
-
-
-def create_alert_for_closure_time(closure_time: ClosureTime):
-	# Create alerts a week before their debut time (closure start - days before) at the latest
-	now = timezone.now()
-	next_week = now + timedelta(weeks=1)
-	closure = closure_time.closure
-	alert_start = closure_time.start_time - timedelta(days=closure.alert_days_before)
-	time_ready = alert_start <= next_week
-	if time_ready:
-		# Check if there is already an alert with the same title ending at the closure end time
-		alert_already_exist = Alert.objects.filter(title=closure.name, expiration_time=closure_time.end_time).exists()
-		if not alert_already_exist:
-			Alert.objects.create(
-				title=closure.name,
-				contents=closure_time.alert_contents(),
-				category="Closure",
-				debut_time=alert_start,
-				expiration_time=closure_time.end_time,
-			)
-
-
-def email_last_closure_occurrence(closure_time):
-	facility_manager_emails = [email for manager in User.objects.filter(is_active=True, is_facility_manager=True) for email in manager.get_emails(EmailNotificationType.BOTH_EMAILS)]
-	message = f"""
-Dear facility manager,<br>
-This email is to inform you that today was the last occurrence for the {closure_time.closure.name} facility closure.
-<br><br>Go to NEMO -> Detailed administration -> Closures to add more times if needed.
-"""
-	send_mail(
-		subject=f"Last {closure_time.closure.name} occurrence",
-		content=message,
-		from_email=get_email_from_settings(),
-		to=facility_manager_emails,
-		email_category=EmailCategory.SYSTEM,
-	)
-
-
-@login_required
-@require_GET
-@permission_required("NEMO.trigger_timed_services", raise_exception=True)
-def email_user_access_expiration_reminders(request):
-	return send_email_user_access_expiration_reminders(request)
-
-
-def send_email_user_access_expiration_reminders(request=None):
-	facility_name = ApplicationCustomization.get("facility_name")
-	user_office_email = EmailsCustomization.get("user_office_email_address")
-	access_expiration_reminder_days = UserCustomization.get("user_access_expiration_reminder_days")
-	template = get_media_file_contents("user_access_expiration_reminder_email.html")
-	if user_office_email and template and access_expiration_reminder_days:
-		user_expiration_reminder_cc = UserCustomization.get("user_access_expiration_reminder_cc")
-		ccs = [e for e in user_expiration_reminder_cc.split(",") if e]
-		for remaining_days in [int(days) for days in access_expiration_reminder_days.split(",")]:
-			expiration_date = date.today() + timedelta(days=remaining_days)
-			for user in User.objects.filter(is_active=True, access_expiration=expiration_date):
-				subject = f"Your {facility_name} access expires in {remaining_days} days ({format_datetime(user.access_expiration)})"
-				message = render_email_template(template, {"user": user, "remaining_days": remaining_days}, request)
-				email_notification = user.get_preferences().email_send_access_expiration_emails
-				user.email_user(
-					subject=subject,
-					message=message,
-					from_email=user_office_email,
-					cc=ccs,
-					email_notification=email_notification
-				)
-	return HttpResponse()
-
-
-@login_required
-@require_GET
-@permission_required("NEMO.trigger_timed_services", raise_exception=True)
-def manage_tool_qualifications(request):
-	return do_manage_tool_qualifications(request)
-
-
-def do_manage_tool_qualifications(request=None):
-	user_office_email = EmailsCustomization.get("user_office_email_address")
-	qualification_reminder_days = ToolCustomization.get("tool_qualification_reminder_days")
-	qualification_expiration_days = ToolCustomization.get("tool_qualification_expiration_days")
-	qualification_expiration_never_used = ToolCustomization.get("tool_qualification_expiration_never_used_days")
-	template = get_media_file_contents("tool_qualification_expiration_email.html")
-	if user_office_email and template:
-		if qualification_expiration_days or qualification_expiration_never_used:
-			qualification_expiration_days = quiet_int(qualification_expiration_days, None)
-			qualification_expiration_never_used = quiet_int(qualification_expiration_never_used, None)
-			for qualification in Qualification.objects.filter(user__is_active=True, user__is_staff=False, tool___qualifications_never_expire=False).prefetch_related("tool", "user"):
-				user = qualification.user
-				tool = qualification.tool
-				last_tool_use = None
-				try:
-					# Last tool use cannot be before the last time they qualified
-					last_tool_use = max(as_timezone(UsageEvent.objects.filter(user=user, tool=tool).latest("start").start).date(), qualification.qualified_on)
-					expiration_date: date = last_tool_use + timedelta(days=qualification_expiration_days) if qualification_expiration_days else None
-				except UsageEvent.DoesNotExist:
-					# User never used the tool, use the qualification date
-					expiration_date: date = qualification.qualified_on + timedelta(days=qualification_expiration_never_used) if qualification_expiration_never_used else None
-				if expiration_date:
-					if expiration_date <= date.today():
-						qualification.delete()
-						send_tool_qualification_expiring_email(qualification, last_tool_use, expiration_date, request=request)
-					if qualification_reminder_days:
-						for remaining_days in [int(days) for days in qualification_reminder_days.split(",")]:
-							if expiration_date - timedelta(days=remaining_days) == date.today():
-								send_tool_qualification_expiring_email(qualification, last_tool_use, expiration_date, remaining_days, request=request)
-	return HttpResponse()
-
-
-def send_tool_qualification_expiring_email(qualification: Qualification, last_tool_use: date, expiration_date: date, remaining_days: int = None, request=None):
-	user_office_email = EmailsCustomization.get("user_office_email_address")
-	template = get_media_file_contents("tool_qualification_expiration_email.html")
-	# Add extra cc emails
-	tool_qualification_cc = ToolCustomization.get("tool_qualification_cc")
-	ccs = [e for e in tool_qualification_cc.split(",") if e]
-	if remaining_days:
-		subject_expiration = f" expires in {remaining_days} days!"
-	else:
-		subject_expiration = " has expired"
-	subject = f"Your {qualification.tool.name} qualification {subject_expiration}"
-	dictionary = {
-		"user": qualification.user,
-		"tool": qualification.tool,
-		"last_tool_use": last_tool_use,
-		"expiration_date": expiration_date,
-		"qualification_date": qualification.qualified_on,
-		"remaining_days": remaining_days
-	}
-	message = render_email_template(template, dictionary, request)
-	email_notification = qualification.user.get_preferences().email_send_tool_qualification_expiration_emails
-	qualification.user.email_user(
-		subject=subject,
-		message=message,
-		from_email=user_office_email,
-		cc=ccs,
-		email_notification=email_notification
-	)
-
-
-@login_required
-@require_GET
-@permission_required("NEMO.trigger_timed_services", raise_exception=True)
-def manage_recurring_charges(request):
-	return do_manage_recurring_charges(request)
-
-
-def do_manage_recurring_charges(request=None):
-	# Dictionary of user ids and list of recurring charges they need to be reminded of
-	user_reminders = {}
-	for recurring_charge in RecurringConsumableCharge.objects.filter(customer__isnull=False):
-		today = beginning_of_the_day(datetime.now(), in_local_timezone=False)
-		next_match_including_today = recurring_charge.next_charge(inc=True)
-		if not next_match_including_today:
-			continue
-		if next_match_including_today == today or next_match_including_today.date() == today.date():
-			try:
-				recurring_charge.charge()
-			except ValidationError as e:
-				url = get_full_url(reverse("edit_recurring_charge", args=[recurring_charge.id]), request)
-				recurring_charge_name = RecurringChargesCustomization.get("recurring_charges_name")
-				user_office_email = EmailsCustomization.get("user_office_email_address")
-				content = f"The item \"{recurring_charge.name}\" <b>could not be charged</b> for the following reason(s):"
-				content += f"{nice_errors(e).as_ul()}"
-				content += f"You can fix the issue by going to the <a href=\"{url}\">{recurring_charge.name}</a> page."
-				send_mail(subject=f"Error processing {recurring_charge_name.lower()}", content=content, from_email=None, to=[user_office_email])
-			except Exception:
-				calendar_logger.exception("Error trying to charge for %s", recurring_charge.name)
-		customer: User = recurring_charge.customer
-		next_charge = recurring_charge.next_charge()
-		if customer.get_preferences().get_recurring_charges_days():
-			reminder_days = (next_charge - today).days
-			if reminder_days in customer.get_preferences().get_recurring_charges_days():
-				key = customer.id
-				if key in user_reminders:
-					user_reminders[key]["charges"].append(recurring_charge)
-				else:
-					user_reminders[key] = {
-						"user": customer,
-						"reminder_days": reminder_days,
-						"charges": [recurring_charge],
-					}
-	send_recurring_charge_reminders(request, user_reminders.values())
-	return HttpResponse()
-
-
-def send_recurring_charge_reminders(request, reminders: Iterable[Dict]):
-	message = get_media_file_contents("recurring_charges_reminder_email.html")
-	user_office_email = EmailsCustomization.get("user_office_email_address")
-	recurring_charges_name = RecurringChargesCustomization.get("recurring_charges_name")
-	if message and user_office_email:
-		for user_reminders in reminders:
-			subject = f"{recurring_charges_name} will be charged in {user_reminders['reminder_days']} day(s)"
-			user_instance: User = user_reminders["user"]
-			rendered_message = render_email_template(message, user_reminders, request)
-			email_notification = user_instance.get_preferences().email_send_recurring_charges_reminder_emails
-			user_instance.email_user(subject=subject, message=rendered_message, from_email=user_office_email, email_category=EmailCategory.TIMED_SERVICES, email_notification=email_notification)
-
-
 @postpone
-def send_tool_free_time_notification(request, cancelled_reservation: Reservation, new_reservation: Optional[Reservation] = None):
+def send_tool_free_time_notification(request, cancelled_reservation: Reservation, new_reservation: Optional[Reservation] = None, missed_or_shortened=False):
 	tool = cancelled_reservation.tool
-	if tool and cancelled_reservation.start > timezone.now():
+	if tool and (cancelled_reservation.start > timezone.now() or missed_or_shortened):
 		max_duration = cancelled_reservation.duration().total_seconds() / 60
 		freed_time = None
 		start_time = None
diff --git a/NEMO/views/configuration_agenda.py b/NEMO/views/configuration_agenda.py
index b4e71886..6dc6967a 100644
--- a/NEMO/views/configuration_agenda.py
+++ b/NEMO/views/configuration_agenda.py
@@ -6,6 +6,7 @@
 from NEMO.decorators import staff_member_required
 from NEMO.models import Configuration, Reservation, Tool
 from NEMO.utilities import distinct_qs_value_list, localize, naive_local_current_datetime
+from NEMO.views.customization import ToolCustomization
 
 
 @staff_member_required
@@ -18,11 +19,18 @@ def configuration_agenda(request, time_period='today'):
 		start = naive_local_current_datetime().replace(tzinfo=None)
 		end = naive_local_current_datetime().replace(hour=23, minute=59, second=59, microsecond=999, tzinfo=None)
 	elif time_period == 'near_future':
+		days_near_future = ToolCustomization.get_int("tool_configuration_near_future_days", 1)
 		start = naive_local_current_datetime().replace(hour=23, minute=59, second=59, microsecond=999, tzinfo=None)
-		if start.weekday() == 4:  # If it's Friday, then the 'near future' is Saturday, Sunday, and Monday
-			end = start + timedelta(days=3)
-		else:  # If it's not Friday, then the 'near future' is the next day
-			end = start + timedelta(days=1)
+		if start.weekday() == 5:
+			# If it's Saturday, then the 'near future' is always "customization days" + 1 (for Sunday)
+			end = start + timedelta(days=days_near_future + 1)
+		elif start.weekday() < 5 <= days_near_future + start.weekday() - 1:
+			# For weekdays, if the sum of today plus "customization days" falls on the weekend
+			# Then the 'near future' is "customization days" + 2 (Saturday and Sunday)
+			end = start + timedelta(days=days_near_future + 2)
+		else:
+			# Other cases, i.e. Sundays and weekdays with the sum not falling on a weekend
+			end = start + timedelta(days=days_near_future)
 	start = localize(start)
 	end = localize(end)
 	reservations = Reservation.objects.filter(start__gt=start, start__lt=end, tool__id__in=tool_ids, self_configuration=False, cancelled=False, missed=False, shortened=False).exclude(additional_information='').order_by('start')
diff --git a/NEMO/views/consumables.py b/NEMO/views/consumables.py
index eec2ae3a..9483bea6 100644
--- a/NEMO/views/consumables.py
+++ b/NEMO/views/consumables.py
@@ -6,7 +6,6 @@
 from django.contrib.auth.decorators import login_required, user_passes_test
 from django.http import HttpResponseBadRequest
 from django.shortcuts import get_object_or_404, redirect, render
-from django.utils.text import slugify
 from django.views.decorators.http import require_GET, require_POST, require_http_methods
 
 from NEMO.decorators import user_office_or_manager_required
@@ -20,8 +19,10 @@
 	as_timezone,
 	export_format_datetime,
 	format_datetime,
-	queryset_search_filter, render_email_template,
+	queryset_search_filter,
+	render_email_template,
 	send_mail,
+	slugify_underscore,
 )
 from NEMO.views.customization import (
 	ApplicationCustomization,
@@ -48,27 +49,33 @@ def self_checkout(user) -> bool:
 @require_http_methods(["GET", "POST"])
 def consumables(request):
 	user: User = request.user
+	is_self_checkout = self_checkout(user)
 	if request.method == "GET":
 		from NEMO.rates import rate_class
 
 		rate_dict = rate_class.get_consumable_rates(Consumable.objects.all())
+		consumable_list = Consumable.objects.filter(visible=True).order_by("category", "name")
+		if is_self_checkout:
+			consumable_list = consumable_list.filter(allow_self_checkout=True)
 
 		dictionary = {
 			"users": User.objects.filter(is_active=True),
-			"consumables": Consumable.objects.filter(visible=True).order_by("category", "name"),
+			"consumables": consumable_list,
 			"rates": rate_dict,
-			"self_checkout": self_checkout(user),
+			"self_checkout": is_self_checkout,
 		}
-		if self_checkout(user):
+		if is_self_checkout:
 			dictionary["projects"] = user.active_projects()
 		return render(request, "consumables/consumables.html", dictionary)
 	elif request.method == "POST":
 		updated_post_data = request.POST.copy()
-		if self_checkout(user):
+		if is_self_checkout:
 			updated_post_data.update({'customer': user.id})
 		form = ConsumableWithdrawForm(updated_post_data)
 		if form.is_valid():
 			withdraw = form.save(commit=False)
+			if is_self_checkout and not withdraw.consumable.allow_self_checkout:
+				return HttpResponseBadRequest("You can not self checkout this consumable")
 			try:
 				policy.check_billing_to_project(withdraw.project, withdraw.customer, withdraw.consumable, withdraw)
 			except ProjectChargeException as e:
@@ -195,7 +202,7 @@ def export_recurring_charges(request):
 
 	response = table.to_csv()
 	feature_name = RecurringChargesCustomization.get("recurring_charges_name")
-	filename = f"{slugify(feature_name.lower()).replace('-', '_')}_{export_format_datetime()}.csv"
+	filename = f"{slugify_underscore(feature_name.lower())}_{export_format_datetime()}.csv"
 	response["Content-Disposition"] = f'attachment; filename="{filename}"'
 	return response
 
@@ -258,14 +265,14 @@ def extended_permissions(request) -> bool:
 	return not lock_charges or user.is_facility_manager or user.is_superuser
 
 
-def make_withdrawal(consumable_id: int, quantity: int, project_id: int, merchant: User, customer_id: int, tool_usage=False, request=None):
+def make_withdrawal(consumable_id: int, quantity: int, project_id: int, merchant: User, customer_id: int, usage_event=None, request=None):
 	withdraw = ConsumableWithdraw(
 		consumable_id=consumable_id,
 		quantity=quantity,
 		merchant=merchant,
 		customer_id=customer_id,
 		project_id=project_id,
-		tool_usage=tool_usage
+		usage_event=usage_event
 	)
 	withdraw.full_clean()
 	withdraw.save()
diff --git a/NEMO/views/customization.py b/NEMO/views/customization.py
index b2f82ff0..8133ef0f 100644
--- a/NEMO/views/customization.py
+++ b/NEMO/views/customization.py
@@ -20,7 +20,7 @@
 from NEMO import init_admin_site
 from NEMO.decorators import administrator_required, customization
 from NEMO.exceptions import InvalidCustomizationException
-from NEMO.models import BadgeReader, ConsumableCategory, Customization, Project, RecurringConsumableCharge
+from NEMO.models import BadgeReader, ConsumableCategory, Customization, Notification, Project, RecurringConsumableCharge
 from NEMO.utilities import RecurrenceFrequency, date_input_format, datetime_input_format, quiet_int
 
 
@@ -326,6 +326,17 @@ def get_date_limit(cls) -> datetime:
 		except:
 			pass
 
+	@classmethod
+	def set(cls, name: str, value):
+		if name == "adjustment_requests_enabled" and value != "enabled":
+			# If adjustment requests are being disabled, remove all notifications
+			previously_enabled = cls.get_bool("adjustment_requests_enabled")
+			if previously_enabled:
+				Notification.objects.filter(
+					notification_type__in=[Notification.Types.ADJUSTMENT_REQUEST, Notification.Types.ADJUSTMENT_REQUEST_REPLY]
+				).delete()
+		super().set(name, value)
+
 	def context(self) -> Dict:
 		context_dict = super().context()
 		context_dict["frequency_choices"] = [(freq.index, freq.display_value) for freq in self.frequencies]
@@ -387,14 +398,19 @@ class ToolCustomization(CustomizationBase):
 		"tool_task_updates_facility_managers": "enabled",
 		"tool_control_hide_data_history_users": "",
 		"tool_control_configuration_setting_template": "{{ current_setting }}",
+		"tool_control_broadcast_upcoming_reservation": "",
 		"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_configuration_near_future_days": "1",
+		"tool_reservation_policy_superusers_bypass": "",
 	}
 
 	def validate(self, name, value):
-		if name == "tool_qualification_expiration_days" and value:
+		if name in ["tool_qualification_expiration_days", "tool_problem_max_image_size_pixels", "tool_configuration_near_future_days"] and value:
 			validate_integer(value)
 		if name == "tool_qualification_reminder_days" and value:
 			# Check that we have an integer or a list of integers
diff --git a/NEMO/views/email.py b/NEMO/views/email.py
index c27f3ecc..d1df6894 100644
--- a/NEMO/views/email.py
+++ b/NEMO/views/email.py
@@ -1,7 +1,7 @@
 import csv
 from logging import getLogger
 from smtplib import SMTPException
-from typing import List
+from typing import List, Optional
 
 from django.contrib import messages
 from django.contrib.auth.decorators import login_required
@@ -10,15 +10,26 @@
 from django.http import HttpResponse, HttpResponseBadRequest
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
+from django.utils import timezone
 from django.utils.safestring import mark_safe
 from django.views.decorators.http import require_GET, require_POST
 
 from NEMO.decorators import any_staff_required
 from NEMO.forms import EmailBroadcastForm
-from NEMO.models import Account, Area, Project, Tool, User, UserType
+from NEMO.models import Account, Area, Project, Reservation, Tool, User, UserType
 from NEMO.typing import QuerySetType
-from NEMO.utilities import EmailCategory, export_format_datetime, render_email_template, send_mail
-from NEMO.views.customization import ApplicationCustomization, get_media_file_contents
+from NEMO.utilities import (
+	EmailCategory,
+	create_email_attachment,
+	export_format_datetime,
+	render_email_template,
+	send_mail,
+)
+from NEMO.views.customization import (
+	ApplicationCustomization,
+	ToolCustomization,
+	get_media_file_contents,
+)
 
 logger = getLogger(__name__)
 
@@ -84,7 +95,7 @@ def send_email(request):
 @require_GET
 def email_broadcast(request, audience=""):
 	dictionary = {}
-	if audience == "tool":
+	if audience == "tool" or audience == "tool-reservation":
 		dictionary["search_base"] = Tool.objects.filter(visible=True)
 	elif audience == "area":
 		dictionary["search_base"] = Area.objects.all()
@@ -101,23 +112,32 @@ def email_broadcast(request, audience=""):
 	return render(request, "email/email_broadcast.html", dictionary)
 
 
-@any_staff_required
+@login_required
 @require_GET
 def compose_email(request):
 	try:
 		audience = request.GET["audience"]
 		selection = request.GET.getlist("selection")
+		# Check if the user is allowed to broadcast email
+		error = check_user_allowed(request.user, audience, selection)
+		if error:
+			return HttpResponseBadRequest(error)
 		no_type = request.GET.get("no_type") == "on"
-		users = get_users_for_email(audience, selection, no_type)
+		users, topic = get_users_for_email(audience, selection, no_type)
 	except:
 		dictionary = {"error": "You specified an invalid audience parameter"}
-		return render(request, "email/email_broadcast.html", dictionary)
+		# Only render the email_broadcast page when user is staff member
+		if request.user.is_any_part_of_staff:
+			return render(request, "email/email_broadcast.html", dictionary)
+		else:
+			return HttpResponseBadRequest("You specified an invalid audience parameter")
 	generic_email_sample = get_media_file_contents("generic_email.html")
 	dictionary = {
 		"audience": audience,
 		"selection": selection,
 		"no_type": no_type,
 		"users": users,
+		"topic": topic,
 		"user_emails": ";".join([email for user in users for email in user.get_emails(user.get_preferences().email_send_broadcast_emails)]),
 		"active_user_emails": ";".join([email for user in users for email in user.get_emails(user.get_preferences().email_send_broadcast_emails) if user.is_active]),
 	}
@@ -140,7 +160,7 @@ def export_email_addresses(request):
 		selection = request.GET.getlist("selection")
 		no_type = request.GET.get("no_type") == "on"
 		only_active_users = request.GET.get("active") == "on"
-		users = get_users_for_email(audience, selection, no_type)
+		users, topic = get_users_for_email(audience, selection, no_type)
 		response = HttpResponse(content_type="text/csv")
 		writer = csv.writer(response)
 		writer.writerow(["First", "Last", "Username", "Email"])
@@ -157,7 +177,7 @@ def export_email_addresses(request):
 		return render(request, "email/email_broadcast.html", dictionary)
 
 
-@any_staff_required
+@login_required
 @require_POST
 def send_broadcast_email(request):
 	content = get_media_file_contents("generic_email.html")
@@ -168,6 +188,10 @@ def send_broadcast_email(request):
 	form = EmailBroadcastForm(request.POST)
 	if not form.is_valid():
 		return render(request, "email/compose_email.html", {"form": form})
+	# Check if the user is allowed to broadcast email
+	error = check_user_allowed(request.user, form.cleaned_data["audience"], form.cleaned_data["selection"])
+	if error:
+		return HttpResponseBadRequest(error)
 	dictionary = {
 		"title": form.cleaned_data["title"],
 		"greeting": form.cleaned_data["greeting"],
@@ -180,7 +204,7 @@ def send_broadcast_email(request):
 		audience = form.cleaned_data["audience"]
 		selection = form.cleaned_data["selection"]
 		no_type = form.cleaned_data["no_type"]
-		users = get_users_for_email(audience, selection, no_type)
+		users, topic = get_users_for_email(audience, selection, no_type)
 		if active_choice:
 			users = users.filter(is_active=True)
 	except Exception as error:
@@ -197,16 +221,20 @@ def send_broadcast_email(request):
 		dictionary = {"error": "The audience you specified is empty. You must send the email to at least one person."}
 		return render(request, "email/compose_email.html", dictionary)
 	subject = form.cleaned_data["subject"]
+	if topic:
+		subject = f"[{topic}] " + subject
 	users = [email for user in users for email in user.get_emails(user.get_preferences().email_send_broadcast_emails)]
 	sender: User = request.user
 	if form.cleaned_data["copy_me"]:
 		users += sender.get_emails(sender.get_preferences().email_send_broadcast_emails)
+	attachments = [create_email_attachment(attachment.file, attachment.name) for attachment in request.FILES.getlist('attachments', [])]
 	try:
 		send_mail(
 			subject=subject,
 			content=content,
 			from_email=sender.email,
 			bcc=set(users),
+			attachments=attachments,
 			email_category=EmailCategory.BROADCAST_EMAIL,
 		)
 	except SMTPException as error:
@@ -222,7 +250,7 @@ def send_broadcast_email(request):
 	return redirect("email_broadcast")
 
 
-@any_staff_required
+@login_required
 @require_POST
 def email_preview(request):
 	generic_email_template = get_media_file_contents("generic_email.html")
@@ -239,22 +267,38 @@ def email_preview(request):
 	return HttpResponse()
 
 
-def get_users_for_email(audience: str, selection: List, no_type: bool) -> QuerySetType[User]:
+# Returns users and an optional topic to add to the email subject
+def get_users_for_email(audience: str, selection: List, no_type: bool) -> (QuerySetType[User], str):
 	users = User.objects.none()
+	topic = None
 	if audience == "tool":
 		users = User.objects.filter(qualifications__id__in=selection).distinct()
+		if len(selection) == 1:
+			topic = Tool.objects.filter(pk=selection[0]).first().name
+	elif audience == "tool-reservation":
+		upcoming_reservations = Reservation.objects.filter(cancelled=False, missed=False, shortened=False, tool__in=selection, end__gte=timezone.now())
+		users = User.objects.filter(reservation_user__in=upcoming_reservations).distinct()
+		if len(selection) == 1:
+			topic = Tool.objects.filter(pk=selection[0]).first().name
 	elif audience == "area":
-		access_levels = [access_level for area in Area.objects.filter(pk__in=selection) for access_level in area.get_physical_access_levels()]
+		areas: QuerySetType[Area] = Area.objects.filter(pk__in=selection)
+		access_levels = [access_level for area in areas for access_level in area.get_physical_access_levels()]
 		user_filter = Q(physical_access_levels__in=access_levels)
 		# if one of the access levels allows staff, add all staff & user office
 		if any([access_level.allow_staff_access for access_level in access_levels]):
 			user_filter |= Q(is_staff=True)
 			user_filter |= Q(is_user_office=True)
 		users = User.objects.filter(user_filter).distinct()
+		if len(selection) == 1:
+			topic = areas.first().name
 	elif audience == "project":
 		users = User.objects.filter(projects__id__in=selection).distinct()
+		if len(selection) == 1:
+			topic = Project.objects.filter(pk=selection[0]).first().name
 	elif audience == "account":
 		users = User.objects.filter(projects__account__id__in=selection).distinct()
+		if len(selection) == 1:
+			topic = Account.objects.filter(pk=selection[0]).first().name
 	elif audience == "user":
 		users = User.objects.all().distinct()
 		if selection:
@@ -265,4 +309,14 @@ def get_users_for_email(audience: str, selection: List, no_type: bool) -> QueryS
 			)
 		elif no_type:
 			users = users.filter(type_id__isnull=True)
-	return users
+	return users, topic
+
+def check_user_allowed(user: User, audience: str, selection: str) -> Optional[str]:
+	if not user.is_any_part_of_staff:
+		allow_broadcast_upcoming_reservation = ToolCustomization.get_bool("tool_control_broadcast_upcoming_reservation")
+		if not allow_broadcast_upcoming_reservation or audience != "tool-reservation":
+			return "You may not broadcast email to this audience"
+		else:
+			tool = Tool.objects.filter(id__in=selection).first()
+			if not tool or user not in tool.user_set.all():
+				return "You can only send a broadcast email to users of a tool you are qualified to use"
\ No newline at end of file
diff --git a/NEMO/views/history.py b/NEMO/views/history.py
index 94ba2cb6..32770fec 100644
--- a/NEMO/views/history.py
+++ b/NEMO/views/history.py
@@ -4,12 +4,12 @@
 from django.contrib.contenttypes.models import ContentType
 from django.http import HttpResponseBadRequest
 from django.shortcuts import get_object_or_404, render
-from django.utils.text import capfirst, slugify
+from django.utils.text import capfirst
 from django.views.decorators.http import require_GET
 
 from NEMO.decorators import any_staff_required
 from NEMO.models import Account, ActivityHistory, MembershipHistory, Project, User
-from NEMO.utilities import BasicDisplayTable, export_format_datetime
+from NEMO.utilities import BasicDisplayTable, export_format_datetime, slugify_underscore
 
 
 @any_staff_required
@@ -69,7 +69,7 @@ def history(request, item_type, item_id):
 	action_list.rows.sort(key=lambda x: x["date"], reverse=True)
 	csv_export = bool(request.GET.get("csv", False))
 	if csv_export:
-		name = slugify(getattr(item, 'name', str(item))).replace("-", "_")
+		name = slugify_underscore(getattr(item, 'name', str(item)))
 		response = action_list.to_csv()
 		filename = f"{item_type}_history_{name}_{export_format_datetime()}.csv"
 		response["Content-Disposition"] = f'attachment; filename="{filename}"'
diff --git a/NEMO/views/notifications.py b/NEMO/views/notifications.py
index 472febfb..b8a4897a 100644
--- a/NEMO/views/notifications.py
+++ b/NEMO/views/notifications.py
@@ -1,5 +1,5 @@
 from datetime import datetime, timedelta
-from typing import List, Set
+from typing import Iterable, List, Set
 
 from django.contrib.contenttypes.models import ContentType
 from django.utils import timezone
@@ -19,7 +19,7 @@ def delete_expired_notifications():
 	Notification.objects.filter(expiration__lt=timezone.now()).delete()
 
 
-def get_notifications(user: User, notification_type: Notification.Types, delete=True):
+def get_notifications(user: User, notification_type: str, delete=True):
 	notifications = Notification.objects.filter(user=user, notification_type=notification_type)
 	if notifications:
 		notification_ids = list(notifications.values_list("object_id", flat=True))
@@ -38,7 +38,7 @@ def get_notification_counts(user: User):
 	return counts
 
 
-def delete_notification(notification_type: Notification.Types, instance_id, users: List[User] = None):
+def delete_notification(notification_type: str, instance_id, users: Iterable[User] = None):
 	notifications = Notification.objects.filter(notification_type=notification_type, object_id=instance_id)
 	if users:
 		notifications = notifications.filter(user__in=users)
@@ -85,7 +85,7 @@ def create_buddy_request_notification(buddy_request: BuddyRequest):
 			)
 
 
-def create_request_message_notification(reply: RequestMessage, notification_type: Notification.Types, expiration: datetime):
+def create_request_message_notification(reply: RequestMessage, notification_type: str, expiration: datetime):
 	for user in reply.content_object.creator_and_reply_users():
 		if user != reply.author:
 			Notification.objects.update_or_create(
@@ -101,10 +101,8 @@ def create_access_request_notification(access_request: TemporaryPhysicalAccessRe
 	request_end = access_request.end_time
 	expiration = end_of_the_day(datetime(request_end.year, request_end.month, request_end.day))
 
-	reviewers: List[User] = User.objects.filter(is_active=True, is_facility_manager=True)
-
 	users_to_notify: Set[User] = set(access_request.other_users.all())
-	users_to_notify.update(reviewers)
+	users_to_notify.update(access_request.reviewers())
 	if access_request.last_updated_by and access_request.last_updated_by != access_request.creator:
 		users_to_notify.add(access_request.creator)
 	for user in users_to_notify:
@@ -117,7 +115,8 @@ def create_access_request_notification(access_request: TemporaryPhysicalAccessRe
 		)
 
 
-def create_adjustment_request_notification(adjustment_request: AdjustmentRequest, users_to_notify: Set[User]):
+def create_adjustment_request_notification(adjustment_request: AdjustmentRequest):
+	users_to_notify = set(adjustment_request.reviewers())
 	users_to_notify.add(adjustment_request.creator)
 	expiration = timezone.now() + timedelta(days=30)  # 30 days for adjustment requests to expire
 	for user in users_to_notify:
diff --git a/NEMO/views/status_dashboard.py b/NEMO/views/status_dashboard.py
index e7fe65b2..30c074e3 100644
--- a/NEMO/views/status_dashboard.py
+++ b/NEMO/views/status_dashboard.py
@@ -33,9 +33,9 @@
 	as_timezone,
 	BasicDisplayTable,
 	beginning_of_the_day,
+	end_of_the_day,
 	export_format_datetime,
 	format_datetime,
-	localize,
 	quiet_int,
 )
 from NEMO.views.customization import StatusDashboardCustomization
@@ -226,7 +226,7 @@ def staff_absences_dict(staffs, days, start, end):
 
 def closures_dict(days, start, end):
 	dictionary = {}
-	l_start, l_end = localize(start), localize(end)
+	l_start, l_end = beginning_of_the_day(start), end_of_the_day(end)
 	closure_times = ClosureTime.objects.filter(start_time__lte=l_end, end_time__gte=l_start, closure__staff_absent=True)
 	for closure_time in closure_times:
 		for day in days:
diff --git a/NEMO/views/tasks.py b/NEMO/views/tasks.py
index 72ff1756..34a80be0 100644
--- a/NEMO/views/tasks.py
+++ b/NEMO/views/tasks.py
@@ -117,6 +117,11 @@ def send_new_task_emails(request, task: Task, task_images: List[TaskImages]):
 		subject = ('SAFETY HAZARD: ' if task.safety_hazard else '') + task.tool.name + (' shutdown' if task.force_shutdown else ' problem')
 		message = render_email_template(message, dictionary, request)
 		recipients = get_task_email_recipients(task)
+		if ToolCustomization.get_bool("tool_problem_send_to_all_qualified_users"):
+			recipients = set(recipients)
+			for user in task.tool.user_set.all():
+				if user.is_active:
+					recipients.update([email for email in user.get_emails(user.get_preferences().email_send_task_updates)])
 		send_mail(subject=subject, content=message, from_email=request.user.email, to=recipients, attachments=attachments, email_category=EmailCategory.TASKS)
 
 	# Email any user (excluding staff) with a future reservation on the tool:
@@ -301,9 +306,10 @@ def save_task_images(request, task: Task) -> List[TaskImages]:
 	task_images: List[TaskImages] = []
 	try:
 		images_form = TaskImagesForm(request.POST, request.FILES)
+		max_size_pixels = ToolCustomization.get_int("tool_problem_max_image_size_pixels")
 		if images_form.is_valid() and images_form.cleaned_data['image'] is not None:
 			for image_memory_file in request.FILES.getlist('image'):
-				resized_image = resize_image(image_memory_file, 350)
+				resized_image = resize_image(image_memory_file, max_size_pixels)
 				image = TaskImages(task=task)
 				image.image.save(resized_image.name, ContentFile(resized_image.read()), save=False)
 				image.save()
diff --git a/NEMO/views/timed_services.py b/NEMO/views/timed_services.py
new file mode 100644
index 00000000..9d2d7f99
--- /dev/null
+++ b/NEMO/views/timed_services.py
@@ -0,0 +1,666 @@
+from datetime import date, datetime, timedelta
+from logging import getLogger
+from typing import Dict, Iterable, List
+
+from django.contrib.auth.decorators import login_required, permission_required
+from django.core.exceptions import ValidationError
+from django.db.models import Q
+from django.http import HttpResponse, HttpResponseNotFound
+from django.urls import reverse
+from django.utils import timezone
+from django.utils.dateparse import parse_datetime
+from django.views.decorators.http import require_GET
+
+from NEMO.forms import nice_errors
+from NEMO.models import (
+	Alert,
+	Area,
+	AreaAccessRecord,
+	Closure,
+	ClosureTime,
+	EmailNotificationType,
+	Qualification,
+	RecurringConsumableCharge,
+	RequestStatus,
+	Reservation,
+	ReservationItemType,
+	StaffCharge,
+	TemporaryPhysicalAccessRequest,
+	Tool,
+	UsageEvent,
+	User,
+)
+from NEMO.typing import QuerySetType
+from NEMO.utilities import (
+	EmailCategory,
+	as_timezone,
+	beginning_of_the_day,
+	bootstrap_primary_color,
+	format_datetime,
+	get_email_from_settings,
+	get_full_url,
+	quiet_int,
+	render_email_template,
+	send_mail,
+)
+from NEMO.views.area_access import log_out_user
+from NEMO.views.calendar import send_tool_free_time_notification
+from NEMO.views.customization import (
+	ApplicationCustomization,
+	CustomizationBase,
+	EmailsCustomization,
+	RecurringChargesCustomization,
+	ToolCustomization,
+	UserCustomization,
+	UserRequestsCustomization,
+	get_media_file_contents,
+)
+
+timed_service_logger = getLogger(__name__)
+
+
+@login_required
+@require_GET
+@permission_required('NEMO.trigger_timed_services', raise_exception=True)
+def cancel_unused_reservations(request):
+	return do_cancel_unused_reservations(request)
+
+
+def do_cancel_unused_reservations(request=None):
+	"""
+	Missed reservation for tools is when there is no tool activity during the reservation time + missed reservation threshold.
+	Any tool usage will count, since we don't want to charge for missed reservation when users swap reservation or somebody else gets to use the tool.
+
+	Missed reservation for areas is when there is no area access login during the reservation time + missed reservation threshold
+	"""
+
+	# Missed Tool Reservations
+	tools = Tool.objects.filter(visible=True, _operational=True, _missed_reservation_threshold__isnull=False)
+	missed_reservations: List[Reservation] = []
+	for tool in tools:
+		# If a tool is in use then there's no need to look for unused reservation time.
+		if tool.in_use() or tool.required_resource_is_unavailable() or tool.scheduled_outage_in_progress():
+			continue
+		# Calculate the timestamp of how long a user can be late for a reservation.
+		threshold = (timezone.now() - timedelta(minutes=tool.missed_reservation_threshold))
+		threshold = datetime.replace(threshold, second=0, microsecond=0)  # Round down to the nearest minute.
+		# Find the reservations that began exactly at the threshold.
+		reservation = Reservation.objects.filter(cancelled=False, missed=False, shortened=False, tool=tool, user__is_staff=False, start=threshold, end__gt=timezone.now())
+		for r in reservation:
+			# Staff may abandon reservations.
+			if r.user.is_staff:
+				continue
+			# If there was no tool enable or disable event since the threshold timestamp then we assume the reservation has been missed.
+			if not (UsageEvent.objects.filter(tool_id__in=tool.get_family_tool_ids(), start__gte=threshold).exists() or UsageEvent.objects.filter(tool_id__in=tool.get_family_tool_ids(), end__gte=threshold).exists()):
+				# Mark the reservation as missed and notify the user & staff.
+				r.missed = True
+				r.save()
+				missed_reservations.append(r)
+
+	# Missed Area Reservations
+	areas = Area.objects.filter(missed_reservation_threshold__isnull=False)
+	for area in areas:
+		# if area has outage or required resource is unavailable, no need to look
+		if area.required_resource_is_unavailable() or area.scheduled_outage_in_progress():
+			continue
+
+		# Calculate the timestamp of how long a user can be late for a reservation.
+		threshold = (timezone.now() - timedelta(minutes=area.missed_reservation_threshold))
+		threshold = datetime.replace(threshold, second=0, microsecond=0)  # Round down to the nearest minute.
+		# Find the reservations that began exactly at the threshold.
+		reservation = Reservation.objects.filter(cancelled=False, missed=False, shortened=False, area=area, user__is_staff=False, start=threshold, end__gt=timezone.now())
+		for r in reservation:
+			# Staff may abandon reservations.
+			if r.user.is_staff:
+				continue
+			# if the user is not already logged in or if there was no area access starting or ending since the threshold timestamp then we assume the reservation was missed
+			if not (AreaAccessRecord.objects.filter(area__id=area.id, customer=r.user, staff_charge=None, end=None).exists() or AreaAccessRecord.objects.filter(area__id=area.id, customer=r.user, start__gte=threshold).exists() or AreaAccessRecord.objects.filter(area__id=area.id, customer=r.user, end__gte=threshold).exists()):
+				# Mark the reservation as missed and notify the user & staff.
+				r.missed = True
+				r.save()
+				missed_reservations.append(r)
+
+	for r in missed_reservations:
+		send_missed_reservation_notification(r, request)
+	# Deal with the missed reservation freed time in a separate loop just in case something raises an exception
+	for r in missed_reservations:
+		if r.tool:
+			# This is a fake reservation to free the time between now and the original end of the reservation
+			new_reservation = Reservation()
+			new_reservation.start = r.start
+			new_reservation.end = timezone.now()
+			send_tool_free_time_notification(request, r, new_reservation, missed_or_shortened=True)
+	return HttpResponse()
+
+
+def send_missed_reservation_notification(reservation, request=None):
+	message = get_media_file_contents('missed_reservation_email.html')
+	user_office_email = EmailsCustomization.get('user_office_email_address')
+	abuse_email = EmailsCustomization.get('abuse_email_address')
+	if message and user_office_email:
+		subject = "Missed reservation for the " + str(reservation.reservation_item)
+		message = render_email_template(message, {'reservation': reservation}, request)
+		recipients = reservation.user.get_emails(reservation.user.get_preferences().email_send_reservation_emails)
+		recipients.append(abuse_email)
+		recipients.append(user_office_email)
+		send_mail(subject=subject, content=message, from_email=user_office_email, to=recipients, email_category=EmailCategory.TIMED_SERVICES)
+	else:
+		timed_service_logger.error("Missed reservation email couldn't be send because missed_reservation_email.html or user_office_email are not defined")
+
+
+@login_required
+@require_GET
+@permission_required("NEMO.trigger_timed_services", raise_exception=True)
+def create_closure_alerts(request):
+	return do_create_closure_alerts()
+
+
+def do_create_closure_alerts():
+	future_times = ClosureTime.objects.filter(closure__alert_days_before__isnull=False, end_time__gt=timezone.now())
+	for closure_time in future_times:
+		create_alert_for_closure_time(closure_time)
+	for closure in Closure.objects.filter(notify_managers_last_occurrence=True):
+		closure_time_ending = ClosureTime.objects.filter(closure=closure).latest("end_time")
+		if as_timezone(closure_time_ending.end_time).date() == date.today():
+			email_last_closure_occurrence(closure_time_ending)
+	return HttpResponse()
+
+
+def create_alert_for_closure_time(closure_time: ClosureTime):
+	# Create alerts a week before their debut time (closure start - days before) at the latest
+	now = timezone.now()
+	next_week = now + timedelta(weeks=1)
+	closure = closure_time.closure
+	alert_start = closure_time.start_time - timedelta(days=closure.alert_days_before)
+	time_ready = alert_start <= next_week
+	if time_ready:
+		# Check if there is already an alert with the same title ending at the closure end time
+		alert_already_exist = Alert.objects.filter(title=closure.name, expiration_time=closure_time.end_time).exists()
+		if not alert_already_exist:
+			Alert.objects.create(
+				title=closure.name,
+				contents=closure_time.alert_contents(),
+				category="Closure",
+				debut_time=alert_start,
+				expiration_time=closure_time.end_time,
+			)
+
+
+def email_last_closure_occurrence(closure_time):
+	facility_manager_emails = [email for manager in User.objects.filter(is_active=True, is_facility_manager=True) for email in manager.get_emails(EmailNotificationType.BOTH_EMAILS)]
+	message = f"""
+Dear facility manager,<br>
+This email is to inform you that today was the last occurrence for the {closure_time.closure.name} facility closure.
+<br><br>Go to NEMO -> Detailed administration -> Closures to add more times if needed.
+"""
+	send_mail(
+		subject=f"Last {closure_time.closure.name} occurrence",
+		content=message,
+		from_email=get_email_from_settings(),
+		to=facility_manager_emails,
+		email_category=EmailCategory.SYSTEM,
+	)
+
+
+@login_required
+@require_GET
+@permission_required('NEMO.trigger_timed_services', raise_exception=True)
+def email_out_of_time_reservation_notification(request):
+	return send_email_out_of_time_reservation_notification(request)
+
+
+def send_email_out_of_time_reservation_notification(request=None):
+	"""
+	Out of time reservation notification for areas is when a user is still logged in an area but his reservation expired.
+	"""
+	# Exit early if the out of time reservation email template has not been customized for the organization yet.
+	# This feature only sends emails, so if the template is not defined there nothing to do.
+	if not get_media_file_contents('out_of_time_reservation_email.html'):
+		return HttpResponseNotFound('The out of time reservation email template has not been customized for your organization yet. Please visit the customization page to upload a template, then out of time email notifications can be sent.')
+
+	out_of_time_user_area = []
+
+	# Find all logged users
+	access_records: List[AreaAccessRecord] = AreaAccessRecord.objects.filter(end=None, staff_charge=None).prefetch_related('customer', 'area').only('customer', 'area')
+	for access_record in access_records:
+		# staff and service personnel are exempt from out of time notification
+		customer = access_record.customer
+		area = access_record.area
+		if customer.is_staff or customer.is_service_personnel:
+			continue
+
+		if area.requires_reservation:
+			# Calculate the timestamp of how late a user can be logged in after a reservation ended.
+			threshold = timezone.now() if not area.logout_grace_period else timezone.now() - timedelta(minutes=area.logout_grace_period)
+			threshold = datetime.replace(threshold, second=0, microsecond=0)  # Round down to the nearest minute.
+			ending_reservations = Reservation.objects.filter(cancelled=False, missed=False, shortened=False, area=area, user=customer, start__lte=timezone.now(), end=threshold)
+			# find out if a reservation is starting right at the same time (in case of back to back reservations, in which case customer is good)
+			starting_reservations = Reservation.objects.filter(cancelled=False, missed=False, shortened=False, area=area, user=customer, start=threshold)
+			if ending_reservations.exists() and not starting_reservations.exists():
+				out_of_time_user_area.append(ending_reservations[0])
+
+	for reservation in out_of_time_user_area:
+		send_out_of_time_reservation_notification(reservation, request)
+
+	return HttpResponse()
+
+
+def send_out_of_time_reservation_notification(reservation: Reservation, request=None):
+	message = get_media_file_contents('out_of_time_reservation_email.html')
+	user_office_email = EmailsCustomization.get('user_office_email_address')
+	if message and user_office_email:
+		subject = "Out of time in the " + str(reservation.area.name)
+		message = render_email_template(message, {'reservation': reservation}, request)
+		recipients = reservation.user.get_emails(reservation.user.get_preferences().email_send_reservation_ending_reminders)
+		recipients.extend(reservation.area.abuse_email_list())
+		send_mail(subject=subject, content=message, from_email=user_office_email, to=recipients, email_category=EmailCategory.TIMED_SERVICES)
+	else:
+		timed_service_logger.error("Out of time reservation email couldn't be send because out_of_time_reservation_email.html or user_office_email are not defined")
+
+
+@login_required
+@permission_required('NEMO.trigger_timed_services', raise_exception=True)
+@require_GET
+def email_reservation_ending_reminders(request):
+	return send_email_reservation_ending_reminders(request)
+
+
+def send_email_reservation_ending_reminders(request=None):
+	# Exit early if the reservation ending reminder email template has not been customized for the organization yet.
+	reservation_ending_reminder_message = get_media_file_contents('reservation_ending_reminder_email.html')
+	user_office_email = EmailsCustomization.get('user_office_email_address')
+	if not reservation_ending_reminder_message:
+		timed_service_logger.error("Reservation ending reminder email couldn't be send because either reservation_ending_reminder_email.html is not defined")
+		return HttpResponseNotFound('The reservation ending reminder template has not been customized for your organization yet. Please visit the customization page to upload one, then reservation ending reminder email notifications can be sent.')
+
+	# We only send ending reservation reminders to users that are currently logged in
+	current_logged_in_user = AreaAccessRecord.objects.filter(end=None, staff_charge=None, customer__is_staff=False)
+	valid_reservations = Reservation.objects.filter(cancelled=False, missed=False, shortened=False)
+	user_area_reservations = valid_reservations.filter(area__isnull=False, user__in=current_logged_in_user.values_list('customer', flat=True))
+
+	# Find all reservations that end 30 or 15 min from now, plus or minus 3 minutes to allow for time skew.
+	reminder_times = [30, 15]
+	tolerance = 3
+	time_filter = Q()
+	for reminder_time in reminder_times:
+		earliest_end = timezone.now() + timedelta(minutes=reminder_time) - timedelta(minutes=tolerance)
+		latest_end = timezone.now() + timedelta(minutes=reminder_time) + timedelta(minutes=tolerance)
+		new_filter = Q(end__gt=earliest_end, end__lt=latest_end)
+		time_filter = time_filter | new_filter
+	ending_reservations = user_area_reservations.filter(time_filter)
+	# Email a reminder to each user with a reservation ending soon.
+	for reservation in ending_reservations:
+		subject = reservation.reservation_item.name + " reservation ending soon"
+		rendered_message = render_email_template(reservation_ending_reminder_message, {'reservation': reservation}, request)
+		email_notification = reservation.user.get_preferences().email_send_reservation_ending_reminders
+		reservation.user.email_user(subject=subject, message=rendered_message, from_email=user_office_email, email_category=EmailCategory.TIMED_SERVICES, email_notification=email_notification)
+	return HttpResponse()
+
+
+@login_required
+@permission_required('NEMO.trigger_timed_services', raise_exception=True)
+@require_GET
+def email_usage_reminders(request):
+	projects_to_exclude = request.GET.getlist("projects_to_exclude[]")
+	return send_email_usage_reminders(projects_to_exclude, request)
+
+
+def send_email_usage_reminders(projects_to_exclude=None, request=None):
+	if projects_to_exclude is None:
+		projects_to_exclude = []
+	busy_users = AreaAccessRecord.objects.none()
+	if ApplicationCustomization.get_bool("area_in_usage_reminders"):
+		busy_users = AreaAccessRecord.objects.filter(end=None, staff_charge=None).exclude(project__id__in=projects_to_exclude)
+	busy_tools = UsageEvent.objects.filter(end=None).exclude(project__id__in=projects_to_exclude)
+
+	# Make lists of all the things a user is logged in to.
+	# We don't want to send 3 separate emails if a user is logged into three things.
+	# Just send one email for all the things!
+	aggregate = {}
+	for access_record in busy_users:
+		key = access_record.customer_id
+		aggregate[key] = {
+			'user': access_record.customer,
+			'resources_in_use': [access_record.area],
+		}
+	for usage_event in busy_tools:
+		key = usage_event.operator_id
+		if key in aggregate:
+			aggregate[key]['resources_in_use'].append(usage_event.tool.name)
+		else:
+			aggregate[key] = {
+				'user': usage_event.operator,
+				'resources_in_use': [usage_event.tool],
+			}
+
+	user_office_email = EmailsCustomization.get('user_office_email_address')
+
+	message = get_media_file_contents('usage_reminder_email.html')
+	facility_name = ApplicationCustomization.get('facility_name')
+	if message:
+		subject = f"{facility_name} usage"
+		for value in aggregate.values():
+			user: User = value["user"]
+			resources_in_use = value["resources_in_use"]
+			# for backwards compatibility, add it to the user object (that's how it was defined and used in the template)
+			user.resources_in_use = resources_in_use
+			rendered_message = render_email_template(message, {"user": user, "resources_in_use": resources_in_use}, request)
+			email_notification = user.get_preferences().email_send_usage_reminders
+			user.email_user(subject=subject, message=rendered_message, from_email=user_office_email, email_category=EmailCategory.TIMED_SERVICES, email_notification=email_notification)
+
+	message = get_media_file_contents('staff_charge_reminder_email.html')
+	if message:
+		busy_staff = StaffCharge.objects.filter(end=None)
+		for staff_charge in busy_staff:
+			subject = "Active staff charge since " + format_datetime(staff_charge.start)
+			rendered_message = render_email_template(message, {'staff_charge': staff_charge}, request)
+			email_notification = staff_charge.staff_member.get_preferences().email_send_usage_reminders
+			staff_charge.staff_member.email_user(subject=subject, message=rendered_message, from_email=user_office_email, email_category=EmailCategory.TIMED_SERVICES, email_notification=email_notification)
+
+	return HttpResponse()
+
+
+@login_required
+@permission_required('NEMO.trigger_timed_services', raise_exception=True)
+@require_GET
+def email_reservation_reminders(request):
+	return send_email_reservation_reminders(request)
+
+
+def send_email_reservation_reminders(request=None):
+	# Exit early if the reservation reminder email template has not been customized for the organization yet.
+	reservation_reminder_message = get_media_file_contents('reservation_reminder_email.html')
+	reservation_warning_message = get_media_file_contents('reservation_warning_email.html')
+	if not reservation_reminder_message or not reservation_warning_message:
+		timed_service_logger.error("Reservation reminder email couldn't be send because either reservation_reminder_email.html or reservation_warning_email.html is not defined")
+		return HttpResponseNotFound('The reservation reminder and/or warning email templates have not been customized for your organization yet. Please visit the customization page to upload both templates, then reservation reminder email notifications can be sent.')
+
+	# Find all reservations that are two hours from now, plus or minus 5 minutes to allow for time skew.
+	preparation_time = 120
+	tolerance = 5
+	earliest_start = timezone.now() + timedelta(minutes=preparation_time) - timedelta(minutes=tolerance)
+	latest_start = timezone.now() + timedelta(minutes=preparation_time) + timedelta(minutes=tolerance)
+	upcoming_reservations = Reservation.objects.filter(cancelled=False, start__gt=earliest_start, start__lt=latest_start)
+	# Email a reminder to each user with an upcoming reservation.
+	for reservation in upcoming_reservations:
+		item = reservation.reservation_item
+		item_type = reservation.reservation_item_type
+		if item_type == ReservationItemType.TOOL and item.operational and not item.problematic() and item.all_resources_available()\
+				or item_type == ReservationItemType.AREA and not item.required_resource_is_unavailable():
+			subject = item.name + " reservation reminder"
+			rendered_message = render_email_template(reservation_reminder_message, {'reservation': reservation, 'template_color': bootstrap_primary_color('success')}, request)
+		elif (item_type == ReservationItemType.TOOL and not item.operational) or item.required_resource_is_unavailable():
+			subject = item.name + " reservation problem"
+			rendered_message = render_email_template(reservation_warning_message, {'reservation': reservation, 'template_color': bootstrap_primary_color('danger'), 'fatal_error': True}, request)
+		else:
+			subject = item.name + " reservation warning"
+			rendered_message = render_email_template(reservation_warning_message, {'reservation': reservation, 'template_color': bootstrap_primary_color('warning'), 'fatal_error': False}, request)
+		user_office_email = EmailsCustomization.get('user_office_email_address')
+		email_notification = reservation.user.get_preferences().email_send_reservation_reminders
+		reservation.user.email_user(subject=subject, message=rendered_message, from_email=user_office_email, email_category=EmailCategory.TIMED_SERVICES, email_notification=email_notification)
+	return HttpResponse()
+
+
+@login_required
+@permission_required("NEMO.trigger_timed_services", raise_exception=True)
+@require_GET
+def email_weekend_access_notification(request):
+	return send_email_weekend_access_notification()
+
+
+def send_email_weekend_access_notification():
+	"""
+	Sends a weekend access email to the addresses set in customization with the template provided.
+	The email is sent when the first request (each week) that includes weekend access is approved.
+	If no weekend access requests are made by the given time on the cutoff day (if set), a no access email is sent.
+	"""
+	try:
+		user_office_email = EmailsCustomization.get("user_office_email_address")
+		email_to = UserRequestsCustomization.get("weekend_access_notification_emails")
+		access_contents = get_media_file_contents("weekend_access_email.html")
+		if user_office_email and email_to and access_contents:
+			process_weekend_access_notification(user_office_email, email_to, access_contents)
+	except Exception as error:
+		timed_service_logger.error(error)
+	return HttpResponse()
+
+
+def process_weekend_access_notification(user_office_email, email_to, access_contents):
+	today = datetime.today()
+	beginning_of_the_week = beginning_of_the_day(today - timedelta(days=today.weekday()))
+	cutoff_day = UserRequestsCustomization.get("weekend_access_notification_cutoff_day")
+	cutoff_hour = UserRequestsCustomization.get("weekend_access_notification_cutoff_hour")
+	# Set the cutoff in actual datetime format
+	cutoff_datetime = None
+	if cutoff_hour.isdigit() and cutoff_day and cutoff_day.isdigit():
+		cutoff_datetime = (beginning_of_the_week + timedelta(days=int(cutoff_day))).replace(hour=int(cutoff_hour))
+
+	end_of_the_week = beginning_of_the_week + timedelta(weeks=1)
+	beginning_of_the_weekend = beginning_of_the_week + timedelta(days=5)
+
+	# Approved access request that include weekend time do overlap with weekend date interval.
+	approved_weekend_access_requests = TemporaryPhysicalAccessRequest.objects.filter(
+		deleted=False, status=RequestStatus.APPROVED
+	)
+	approved_weekend_access_requests = approved_weekend_access_requests.exclude(start_time__gte=end_of_the_week)
+	approved_weekend_access_requests = approved_weekend_access_requests.exclude(end_time__lte=beginning_of_the_weekend)
+
+	cutoff_time_passed = cutoff_datetime and timezone.now() >= cutoff_datetime
+	last_sent = CustomizationBase.get("weekend_access_notification_last_sent")
+	last_sent_datetime = parse_datetime(last_sent) if last_sent else None
+	if (
+			(not last_sent_datetime or last_sent_datetime < beginning_of_the_week)
+			and access_contents
+			and approved_weekend_access_requests.exists()
+			and not cutoff_time_passed
+	):
+		send_weekend_email_access(True, user_office_email, email_to, access_contents, beginning_of_the_week)
+		CustomizationBase.set("weekend_access_notification_last_sent", str(timezone.now()))
+	if access_contents and cutoff_datetime and not approved_weekend_access_requests.exists():
+		is_cutoff = today.weekday() == int(cutoff_day) and cutoff_datetime.hour == timezone.localtime().hour
+		if is_cutoff:
+			send_weekend_email_access(False, user_office_email, email_to, access_contents, beginning_of_the_week)
+
+
+def send_weekend_email_access(access, user_office_email, email_to, contents, beginning_of_the_week):
+	facility_name = ApplicationCustomization.get("facility_name")
+	recipients = tuple([e for e in email_to.split(",") if e])
+	ccs = [
+		email
+		for manager in User.objects.filter(is_active=True, is_facility_manager=True)
+		for email in manager.get_emails(manager.get_preferences().email_send_access_request_updates)
+	]
+	ccs.append(user_office_email)
+
+	sat = format_datetime(beginning_of_the_week + timedelta(days=5), "SHORT_DATE_FORMAT", as_current_timezone=False)
+	sun = format_datetime(beginning_of_the_week + timedelta(days=6), "SHORT_DATE_FORMAT", as_current_timezone=False)
+
+	subject = f"{'NO w' if not access else 'W'}eekend access for the {facility_name} {sat} - {sun}"
+	message = render_email_template(contents, {"weekend_access": access})
+	send_mail(
+		subject=subject,
+		content=message,
+		from_email=user_office_email,
+		to=recipients,
+		cc=ccs,
+		email_category=EmailCategory.ACCESS_REQUESTS,
+	)
+
+
+@login_required
+@require_GET
+@permission_required("NEMO.trigger_timed_services", raise_exception=True)
+def email_user_access_expiration_reminders(request):
+	return send_email_user_access_expiration_reminders(request)
+
+
+def send_email_user_access_expiration_reminders(request=None):
+	facility_name = ApplicationCustomization.get("facility_name")
+	user_office_email = EmailsCustomization.get("user_office_email_address")
+	access_expiration_reminder_days = UserCustomization.get("user_access_expiration_reminder_days")
+	template = get_media_file_contents("user_access_expiration_reminder_email.html")
+	if user_office_email and template and access_expiration_reminder_days:
+		user_expiration_reminder_cc = UserCustomization.get("user_access_expiration_reminder_cc")
+		ccs = [e for e in user_expiration_reminder_cc.split(",") if e]
+		for remaining_days in [int(days) for days in access_expiration_reminder_days.split(",")]:
+			expiration_date = date.today() + timedelta(days=remaining_days)
+			for user in User.objects.filter(is_active=True, access_expiration=expiration_date):
+				subject = f"Your {facility_name} access expires in {remaining_days} days ({format_datetime(user.access_expiration)})"
+				message = render_email_template(template, {"user": user, "remaining_days": remaining_days}, request)
+				email_notification = user.get_preferences().email_send_access_expiration_emails
+				user.email_user(
+					subject=subject,
+					message=message,
+					from_email=user_office_email,
+					cc=ccs,
+					email_notification=email_notification
+				)
+	return HttpResponse()
+
+
+@login_required
+@require_GET
+@permission_required("NEMO.trigger_timed_services", raise_exception=True)
+def manage_tool_qualifications(request):
+	return do_manage_tool_qualifications(request)
+
+
+def do_manage_tool_qualifications(request=None):
+	user_office_email = EmailsCustomization.get("user_office_email_address")
+	qualification_reminder_days = ToolCustomization.get("tool_qualification_reminder_days")
+	qualification_expiration_days = ToolCustomization.get("tool_qualification_expiration_days")
+	qualification_expiration_never_used = ToolCustomization.get("tool_qualification_expiration_never_used_days")
+	template = get_media_file_contents("tool_qualification_expiration_email.html")
+	if user_office_email and template:
+		if qualification_expiration_days or qualification_expiration_never_used:
+			qualification_expiration_days = quiet_int(qualification_expiration_days, None)
+			qualification_expiration_never_used = quiet_int(qualification_expiration_never_used, None)
+			for qualification in Qualification.objects.filter(user__is_active=True, user__is_staff=False, tool___qualifications_never_expire=False).prefetch_related("tool", "user"):
+				user = qualification.user
+				tool = qualification.tool
+				last_tool_use = None
+				try:
+					# Last tool use cannot be before the last time they qualified
+					last_tool_use = max(as_timezone(UsageEvent.objects.filter(user=user, tool=tool).latest("start").start).date(), qualification.qualified_on)
+					expiration_date: date = last_tool_use + timedelta(days=qualification_expiration_days) if qualification_expiration_days else None
+				except UsageEvent.DoesNotExist:
+					# User never used the tool, use the qualification date
+					expiration_date: date = qualification.qualified_on + timedelta(days=qualification_expiration_never_used) if qualification_expiration_never_used else None
+				if expiration_date:
+					if expiration_date <= date.today():
+						qualification.delete()
+						send_tool_qualification_expiring_email(qualification, last_tool_use, expiration_date, request=request)
+					if qualification_reminder_days:
+						for remaining_days in [int(days) for days in qualification_reminder_days.split(",")]:
+							if expiration_date - timedelta(days=remaining_days) == date.today():
+								send_tool_qualification_expiring_email(qualification, last_tool_use, expiration_date, remaining_days, request=request)
+	return HttpResponse()
+
+
+def send_tool_qualification_expiring_email(qualification: Qualification, last_tool_use: date, expiration_date: date, remaining_days: int = None, request=None):
+	user_office_email = EmailsCustomization.get("user_office_email_address")
+	template = get_media_file_contents("tool_qualification_expiration_email.html")
+	# Add extra cc emails
+	tool_qualification_cc = ToolCustomization.get("tool_qualification_cc")
+	ccs = [e for e in tool_qualification_cc.split(",") if e]
+	if remaining_days:
+		subject_expiration = f" expires in {remaining_days} days!"
+	else:
+		subject_expiration = " has expired"
+	subject = f"Your {qualification.tool.name} qualification {subject_expiration}"
+	dictionary = {
+		"user": qualification.user,
+		"tool": qualification.tool,
+		"last_tool_use": last_tool_use,
+		"expiration_date": expiration_date,
+		"qualification_date": qualification.qualified_on,
+		"remaining_days": remaining_days
+	}
+	message = render_email_template(template, dictionary, request)
+	email_notification = qualification.user.get_preferences().email_send_tool_qualification_expiration_emails
+	qualification.user.email_user(
+		subject=subject,
+		message=message,
+		from_email=user_office_email,
+		cc=ccs,
+		email_notification=email_notification
+	)
+
+
+@login_required
+@require_GET
+@permission_required("NEMO.trigger_timed_services", raise_exception=True)
+def manage_recurring_charges(request):
+	return do_manage_recurring_charges(request)
+
+
+def do_manage_recurring_charges(request=None):
+	# Dictionary of user ids and list of recurring charges they need to be reminded of
+	user_reminders = {}
+	for recurring_charge in RecurringConsumableCharge.objects.filter(customer__isnull=False):
+		today = beginning_of_the_day(datetime.now(), in_local_timezone=False)
+		next_match_including_today = recurring_charge.next_charge(inc=True)
+		if not next_match_including_today:
+			continue
+		if next_match_including_today == today or next_match_including_today.date() == today.date():
+			try:
+				recurring_charge.charge()
+			except ValidationError as e:
+				url = get_full_url(reverse("edit_recurring_charge", args=[recurring_charge.id]), request)
+				recurring_charge_name = RecurringChargesCustomization.get("recurring_charges_name")
+				user_office_email = EmailsCustomization.get("user_office_email_address")
+				content = f"The item \"{recurring_charge.name}\" <b>could not be charged</b> for the following reason(s):"
+				content += f"{nice_errors(e).as_ul()}"
+				content += f"You can fix the issue by going to the <a href=\"{url}\">{recurring_charge.name}</a> page."
+				send_mail(subject=f"Error processing {recurring_charge_name.lower()}", content=content, from_email=None, to=[user_office_email])
+			except Exception:
+				timed_service_logger.exception("Error trying to charge for %s", recurring_charge.name)
+		customer: User = recurring_charge.customer
+		next_charge = recurring_charge.next_charge()
+		if customer.get_preferences().get_recurring_charges_days():
+			reminder_days = (next_charge - today).days
+			if reminder_days in customer.get_preferences().get_recurring_charges_days():
+				key = customer.id
+				if key in user_reminders:
+					user_reminders[key]["charges"].append(recurring_charge)
+				else:
+					user_reminders[key] = {
+						"user": customer,
+						"reminder_days": reminder_days,
+						"charges": [recurring_charge],
+					}
+	send_recurring_charge_reminders(request, user_reminders.values())
+	return HttpResponse()
+
+
+def send_recurring_charge_reminders(request, reminders: Iterable[Dict]):
+	message = get_media_file_contents("recurring_charges_reminder_email.html")
+	user_office_email = EmailsCustomization.get("user_office_email_address")
+	recurring_charges_name = RecurringChargesCustomization.get("recurring_charges_name")
+	if message and user_office_email:
+		for user_reminders in reminders:
+			subject = f"{recurring_charges_name} will be charged in {user_reminders['reminder_days']} day(s)"
+			user_instance: User = user_reminders["user"]
+			rendered_message = render_email_template(message, user_reminders, request)
+			email_notification = user_instance.get_preferences().email_send_recurring_charges_reminder_emails
+			user_instance.email_user(subject=subject, message=rendered_message, from_email=user_office_email, email_category=EmailCategory.TIMED_SERVICES, email_notification=email_notification)
+
+
+@login_required
+@require_GET
+@permission_required("NEMO.trigger_timed_services", raise_exception=True)
+def auto_logout_users(request):
+	return do_auto_logout_users()
+
+
+def do_auto_logout_users():
+	current_logged_in_user_to_logout: QuerySetType[AreaAccessRecord] = AreaAccessRecord.objects.filter(area__auto_logout_time__isnull=False, end__isnull=True, staff_charge__isnull=True).prefetch_related("customer", "area")
+	for record in current_logged_in_user_to_logout:
+		timeout = timedelta(minutes=record.area.auto_logout_time)
+		if record.start + timeout <= timezone.now():
+			# Using regular logout function for consistency
+			log_out_user(record.customer)
+			# Now adjust the time, so it's "auto_logout_time" minutes long max
+			record.end = record.start + timeout
+			record.save(update_fields=["end"])
+	return HttpResponse()
diff --git a/NEMO/views/tool_control.py b/NEMO/views/tool_control.py
index 424ae620..c1e9dead 100644
--- a/NEMO/views/tool_control.py
+++ b/NEMO/views/tool_control.py
@@ -54,6 +54,7 @@
 	EmailsCustomization,
 	InterlockCustomization,
 	RemoteWorkCustomization,
+	ToolCustomization,
 	get_media_file_contents,
 )
 from NEMO.widgets.configuration_editor import ConfigurationEditor
@@ -89,7 +90,9 @@ def tool_status(request, tool_id):
 	""" Gets the current status of the tool (that is, whether it is currently in use or not). """
 	from NEMO.rates import rate_class
 	tool = get_object_or_404(Tool, id=tool_id, visible=True)
-
+	user_is_qualified = tool.user_set.filter(id=request.user.id).exists()
+	user_is_staff = request.user.is_staff
+	tool_control_broadcast_upcoming_reservation_enabled = ToolCustomization.get_bool("tool_control_broadcast_upcoming_reservation")
 	dictionary = {
 		"tool": tool,
 		"tool_rate": rate_class.get_tool_rate(tool, request.user),
@@ -99,6 +102,7 @@ def tool_status(request, tool_id):
 		"task_statuses": TaskStatus.objects.all(),
 		"post_usage_questions": DynamicForm(tool.post_usage_questions).render("tool_usage_group_question", tool_id),
 		"configs": get_tool_full_config_history(tool),
+		"show_broadcast_upcoming_reservation": user_is_staff or (user_is_qualified and tool_control_broadcast_upcoming_reservation_enabled),
 	}
 
 	try:
@@ -426,13 +430,7 @@ def disable_tool(request, tool_id):
 			return HttpResponseBadRequest(str(e))
 
 	try:
-		dynamic_form.charge_for_consumables(
-			current_usage_event.user,
-			current_usage_event.operator,
-			current_usage_event.project,
-			current_usage_event.run_data,
-			request
-		)
+		dynamic_form.charge_for_consumables(current_usage_event, request)
 	except Exception as e:
 		return HttpResponseBadRequest(str(e))
 	dynamic_form.update_tool_counters(current_usage_event.run_data, tool.id)
diff --git a/NEMO/views/usage.py b/NEMO/views/usage.py
index 388a3d69..868d3d14 100644
--- a/NEMO/views/usage.py
+++ b/NEMO/views/usage.py
@@ -116,6 +116,12 @@ def usage(request):
 	trainee_filter = Q(trainee=user) | Q(project__in=user_managed_projects)
 	project_id = request.GET.get("project") or request.GET.get("pi_project")
 	csv_export = bool(request.GET.get("csv", False))
+	show_only_my_usage = user_managed_projects and request.GET.get("show_only_my_usage", "enabled") == "enabled"
+	if show_only_my_usage:
+		# Forcing to be user only
+		customer_filter &= Q(customer=user)
+		user_filter &= Q(user=user)
+		trainee_filter &= Q(trainee=user)
 	if user_managed_projects:
 		base_dictionary['selected_project'] = "all"
 	if project_id:
@@ -146,6 +152,7 @@ def usage(request):
 		}
 		if user_managed_projects:
 			dictionary['pi_projects'] = user_managed_projects
+			dictionary['show_only_my_usage'] = show_only_my_usage
 		dictionary['no_charges'] = not (dictionary['area_access'] or dictionary['consumables'] or dictionary['missed_reservations'] or dictionary['staff_charges'] or dictionary['training_sessions'] or dictionary['usage_events'])
 		return render(request, 'usage/usage.html', {**base_dictionary, **dictionary})
 
diff --git a/NEMO/views/user_requests.py b/NEMO/views/user_requests.py
index 79d6eed7..b1970723 100644
--- a/NEMO/views/user_requests.py
+++ b/NEMO/views/user_requests.py
@@ -2,7 +2,7 @@
 from django.shortcuts import render
 from django.views.decorators.http import require_GET
 
-from NEMO.models import PhysicalAccessLevel, User
+from NEMO.models import Area, PhysicalAccessLevel, User
 from NEMO.views.customization import UserRequestsCustomization
 
 
@@ -14,6 +14,8 @@ def user_requests(request, tab: str = None):
 		if PhysicalAccessLevel.objects.filter(allow_user_request=True).exists()
 		and User.objects.filter(is_active=True, is_facility_manager=True).exists()
 		else "buddy"
+		if Area.objects.filter(buddy_system_allowed=True).exists()
+		else "adjustment"
 	)
 	buddy_requests_title = UserRequestsCustomization.get("buddy_requests_title")
 	access_requests_title = UserRequestsCustomization.get("access_requests_title")
diff --git a/NEMO/widgets/dynamic_form.py b/NEMO/widgets/dynamic_form.py
index 9d59fa16..d3ccfb16 100644
--- a/NEMO/widgets/dynamic_form.py
+++ b/NEMO/widgets/dynamic_form.py
@@ -1,3 +1,4 @@
+import sys
 from collections import Counter
 from copy import copy
 from distutils.util import strtobool
@@ -7,16 +8,16 @@
 
 from django.urls import NoReverseMatch, reverse
 from django.utils.safestring import mark_safe
-from django.utils.text import slugify
 
 from NEMO.exceptions import RequiredUnansweredQuestionsException
 from NEMO.models import Consumable, ToolUsageCounter
+from NEMO.utilities import slugify_underscore
 from NEMO.views.consumables import make_withdrawal
 
 dynamic_form_logger = getLogger(__name__)
 
 
-GROUP_TYPE_QUESTION_KEY = "group"
+GROUP_TYPE_FIELD_KEY = "group"
 
 
 class PostUsageQuestion:
@@ -47,6 +48,7 @@ def __init__(self, properties: Dict, index: int = None):
 		# For backwards compatibility keep default choice
 		self.default_value = self._init_property("default_value") or self._init_property("default_choice")
 		self.choices = self._init_property("choices")
+		self.labels = self._init_property("labels")
 		self.group_add_button_name = self._init_property("group_add_button_name") or "Add"
 		self.index = index
 		if index and not isinstance(self, PostUsageGroupQuestion):
@@ -97,6 +99,12 @@ def validate_property_exists(self, prop: str):
 		except KeyError:
 			raise Exception(f"{self.question_type} requires property '{prop}' to be defined")
 
+	def validate_labels_and_choices(self):
+		self.validate_property_exists("choices")
+		if "labels" in self.properties:
+			if len(self.properties["labels"]) != len(self.properties["choices"]):
+				raise Exception("When using labels you need one for each choice")
+
 	@staticmethod
 	def load_questions(questions: Optional[List[Dict]], index: int = None):
 		questions_to_load = questions or []
@@ -111,16 +119,17 @@ class PostUsageRadioQuestion(PostUsageQuestion):
 
 	def validate(self):
 		super().validate()
-		self.validate_property_exists("choices")
+		self.validate_labels_and_choices()
 
 	def render_element(self, virtual_inputs: bool, group_question_url: str, group_item_id: int) -> str:
 		title = self.title_html or self.title
 		result = f'<div class="form-group"><div style="white-space: pre-wrap">{title}{self.required_span if self.required else ""}</div>'
-		for choice in self.choices:
+		for index, choice in enumerate(self.choices):
+			label = self.labels[index] if self.labels else choice
 			result += '<div class="radio">'
 			required = "required" if self.required else ""
 			is_default_choice = "checked" if self.default_value and self.default_value == choice else ""
-			result += f'<label><input type="radio" name="{self.form_name}" value="{choice}" {required} {is_default_choice}>{choice}</label>'
+			result += f'<label><input type="radio" name="{self.form_name}" value="{choice}" {required} {is_default_choice}>{label}</label>'
 			result += "</div>"
 		result += "</div>"
 		return result
@@ -131,17 +140,18 @@ class PostUsageCheckboxQuestion(PostUsageQuestion):
 
 	def validate(self):
 		super().validate()
-		self.validate_property_exists("choices")
+		self.validate_labels_and_choices()
 
 	def render_element(self, virtual_inputs: bool, group_question_url: str, group_item_id: int) -> str:
 		title = self.title_html or self.title
 		result = f'<div class="form-group"><div style="white-space: pre-wrap">{title}{self.required_span if self.required else ""}</div>'
 		result += f'<input aria-label="hidden field used for required answer" id="required_{ self.form_name }" type="checkbox" value="" style="display: none" { "required" if self.required else "" }/>'
-		for choice in self.choices:
+		for index, choice in enumerate(self.choices):
+			label = self.labels[index] if self.labels else choice
 			result += '<div class="checkbox">'
 			required = f"""onclick="checkbox_required('{self.form_name}')" """ if self.required else ""
 			is_default_choice = "checked" if self.default_value and self.default_value == choice else ""
-			result += f'<label><input type="checkbox" name="{self.form_name}" value="{choice}" {required} {is_default_choice}>{choice}</label>'
+			result += f'<label><input type="checkbox" name="{self.form_name}" value="{choice}" {required} {is_default_choice}>{label}</label>'
 			result += "</div>"
 		result += "</div>"
 		return result
@@ -171,7 +181,7 @@ class PostUsageDropdownQuestion(PostUsageQuestion):
 	def validate(self):
 		super().validate()
 		self.validate_property_exists("max-width")
-		self.validate_property_exists("choices")
+		self.validate_labels_and_choices()
 
 	def render_element(self, virtual_inputs: bool, group_question_url: str, group_item_id: int) -> str:
 		title = self.title_html or self.title
@@ -184,9 +194,10 @@ def render_element(self, virtual_inputs: bool, group_question_url: str, group_it
 		blank_disabled = 'disabled="disabled"' if required else ""
 		placeholder = self.placeholder if self.placeholder else "Select an option"
 		result += f'<option {blank_disabled} selected="selected" value="">{placeholder}</option>'
-		for choice in self.choices:
+		for index, choice in enumerate(self.choices):
+			label = self.labels[index] if self.labels else choice
 			is_default_choice = "selected" if self.default_value and self.default_value == choice else ""
-			result += f'<option value="{choice}" {is_default_choice}>{choice}</option>'
+			result += f'<option value="{choice}" {is_default_choice}>{label}</option>'
 		result += "</select>"
 		if self.help:
 			result += f'<div style="font-size:smaller;color:#999;{max_width}">{self.help}</div>'
@@ -301,7 +312,7 @@ def __init__(self, properties: Dict, index: int = None):
 		super().__init__(properties, index)
 		self.max_number = self._init_property("max_number")
 		# we need a safe group name to use in js function and variable names
-		self.group_name = slugify(self.name).replace("-", "_")
+		self.group_name = slugify_underscore(self.name)
 		self.sub_questions: List[PostUsageQuestion] = PostUsageQuestion.load_questions(
 			self._init_property("questions"), index
 		)
@@ -421,7 +432,7 @@ def validate(self, group_question_url: str, group_item_id: int):
 		for question in self.untreated_questions:
 			if question["type"] not in question_types.keys():
 				raise Exception(f"type has to be one of {', '.join(question_types.keys())}")
-			if question["type"] == GROUP_TYPE_QUESTION_KEY and "questions" in question:
+			if question["type"] == GROUP_TYPE_FIELD_KEY and "questions" in question:
 				for sub_question in question["questions"]:
 					if sub_question["type"] not in question_types.keys():
 						raise Exception(f"type has to be one of {', '.join(question_types.keys())}")
@@ -491,7 +502,11 @@ def filter_questions(self, function: Callable[[PostUsageQuestion], bool]) -> Lis
 						results.append(sub_question)
 		return results
 
-	def charge_for_consumables(self, customer, merchant, project, run_data: str, request=None):
+	def charge_for_consumables(self, usage_event, request=None):
+		customer = usage_event.user
+		merchant = usage_event.operator
+		project = usage_event.project
+		run_data = usage_event.run_data
 		try:
 			run_data_json = loads(run_data)
 		except Exception as error:
@@ -499,10 +514,10 @@ def charge_for_consumables(self, customer, merchant, project, run_data: str, req
 			return
 		for question in self.questions:
 			input_data = run_data_json[question.name] if question.name in run_data_json else None
-			withdraw_consumable_for_question(question, input_data, customer, merchant, project, request)
+			withdraw_consumable_for_question(question, input_data, customer, merchant, project, usage_event, request)
 			if isinstance(question, PostUsageGroupQuestion):
 				for sub_question in question.sub_questions:
-					withdraw_consumable_for_question(sub_question, input_data, customer, merchant, project, request)
+					withdraw_consumable_for_question(sub_question, input_data, customer, merchant, project, usage_event, request)
 
 	def update_tool_counters(self, run_data: str, tool_id: int):
 		# This function increments all counters associated with the given tool
@@ -525,6 +540,18 @@ def update_tool_counters(self, run_data: str, tool_id: int):
 				counter.save()
 
 
+def get_submitted_user_inputs(user_data: str) -> Dict:
+	""" Takes the user data as a string and returns a dictionary of inputs or a list of inputs for group fields """
+	user_input = {}
+	user_data_json = loads(user_data)
+	for field_name, data in user_data_json.items():
+		if data["type"] != 'group':
+			user_input[field_name] = data["user_input"]
+		else:
+			user_input[field_name] = data["user_input"].values()
+	return user_input
+
+
 def render_group_questions(request, questions, group_question_url, group_item_id, group_name) -> str:
 	question_index = request.GET["index"]
 	virtual_inputs = bool(strtobool((request.GET["virtual_inputs"])))
@@ -546,7 +573,7 @@ def validate_consumable_for_question(question: PostUsageQuestion):
 				raise Exception(f"Consumable with name '{question.consumable}' could not be found. Make sure the names match.")
 
 
-def withdraw_consumable_for_question(question, input_data, customer, merchant, project, request):
+def withdraw_consumable_for_question(question, input_data, customer, merchant, project, usage_event, request):
 	if isinstance(question, PostUsageNumberFieldQuestion):
 		if question.consumable:
 			consumable = Consumable.objects.get(name=question.consumable)
@@ -565,7 +592,7 @@ def withdraw_consumable_for_question(question, input_data, customer, merchant, p
 					merchant=merchant,
 					quantity=quantity,
 					project_id=project.id,
-					tool_usage=True,
+					usage_event=usage_event,
 					request=request,
 				)
 
@@ -583,6 +610,40 @@ def get_counter_increment_for_question(question, input_data, counter_question):
 	return additional_value
 
 
+def validate_dynamic_form_model(dynamic_form_json: str, group_url: str, item_id) -> List[str]:
+	errors = []
+	if dynamic_form_json:
+		try:
+			loads(dynamic_form_json)
+		except ValueError:
+			errors.append("This field needs to be a valid JSON string")
+		try:
+			dynamic_form = DynamicForm(dynamic_form_json)
+			dynamic_form.validate(group_url, item_id)
+		except KeyError as e:
+			errors.append(f"{e} property is required")
+		except Exception:
+			error_info = sys.exc_info()
+			errors.append(error_info[0].__name__ + ": " + str(error_info[1]))
+	return errors
+
+
+def admin_render_dynamic_form_preview(dynamic_form_json: str, group_url: str, item_id):
+	form_validity_div = ""
+	rendered_form = ""
+	try:
+		rendered_form = DynamicForm(dynamic_form_json).render(group_url, item_id)
+		if dynamic_form_json:
+			form_validity_div = '<div id="form_validity"></div>'
+	except:
+		pass
+	return mark_safe(
+		'<div class="dynamic_form_preview">{}{}</div><div class="help dynamic_form_preview_help">Save form to preview</div>'.format(
+			rendered_form, form_validity_div
+		)
+	)
+
+
 question_types: Dict[str, Type[PostUsageQuestion]] = {
 	"number": PostUsageNumberFieldQuestion,
 	"float": PostUsageFloatFieldQuestion,
diff --git a/NEMO/widgets/item_tree.py b/NEMO/widgets/item_tree.py
index aedd7d00..36458966 100644
--- a/NEMO/widgets/item_tree.py
+++ b/NEMO/widgets/item_tree.py
@@ -129,8 +129,9 @@ def __render_helper(self, node, result, category_items_lookup=None):
 		Each node is output as an HTML list item. If the node has children then those are also output.
 		"""
 		if node.__is_leaf():
-			result += '<li>'
+			result += '<li style="display: flex;">'
 			css_class = "" if node.is_user_qualified else 'class="disabled"'
+			result += f'<input type="checkbox" id="checkbox-{node.item_type.value}-{node.id}" name="item-checkbox" class="item-checkbox" data-item-id="{node.id}" data-item-type="{node.item_type.value}" style="display: none;">'
 			result += f'<a id="{node.item_type.value}-{node.id}" href="javascript:void(0);" onclick="set_selected_item(this)" data-item-id="{node.id}" data-item-type="{node.item_type.value}" data-item-name="{node.name}" {css_class}>{node.name}</a>'
 		if not node.__is_leaf():
 			node_li_class = "area-category" if node.item_type == ReservationItemType.AREA else 'tool-category'
diff --git a/resources/fixtures/splash_pad.json b/resources/fixtures/splash_pad.json
index e780084b..461dd40f 100644
--- a/resources/fixtures/splash_pad.json
+++ b/resources/fixtures/splash_pad.json
@@ -534,6 +534,20 @@
       "reminder_threshold_reached": false
     }
   },
+  {
+    "model": "NEMO.consumable",
+    "pk": 5,
+    "fields": {
+      "name": "Sputter gold",
+      "category": 2,
+      "quantity": 200,
+      "reusable": true,
+      "visible": true,
+      "reminder_threshold": null,
+      "reminder_email": null,
+      "reminder_threshold_reached": false
+    }
+  },
   {
     "model": "NEMO.consumablecategory",
     "pk": 1,
@@ -541,6 +555,13 @@
       "name": "Rental"
     }
   },
+  {
+    "model": "NEMO.consumablecategory",
+    "pk": 2,
+    "fields": {
+      "name": "Material"
+    }
+  },
   {
     "model": "NEMO.interlockcard",
     "pk": 1,
@@ -5256,7 +5277,7 @@
       "_maximum_future_reservation_time": null,
       "_missed_reservation_threshold": 30,
       "_allow_delayed_logoff": false,
-      "_post_usage_questions": "",
+      "_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]",
       "_policy_off_between_times": false,
       "_policy_off_start_time": null,
       "_policy_off_end_time": null,
@@ -8676,6 +8697,24 @@
       "_superusers": []
     }
   },
+  {
+    "model": "NEMO.toolusagecounter",
+    "pk": 1,
+    "fields": {
+      "name": "Total gold used",
+      "description": "",
+      "value": 203,
+      "tool": 63,
+      "tool_usage_question": "gold_used",
+      "last_reset_value": null,
+      "last_reset": null,
+      "last_reset_by": null,
+      "warning_threshold": null,
+      "warning_email": "",
+      "warning_threshold_reached": false,
+      "is_active": true
+    }
+  },
   {
     "model": "NEMO.toolqualificationgroup",
     "pk": 1,
diff --git a/resources/settings.py b/resources/settings.py
index 0a0767fa..4e6ec8dc 100644
--- a/resources/settings.py
+++ b/resources/settings.py
@@ -1,3 +1,8 @@
+import re
+
+from django.conf import global_settings
+from rest_framework.settings import DEFAULTS
+
 # ------------------------------------------------------------------
 # -------------------- Django settings for NEMO --------------------
 # ------------------------------------------------------------------
@@ -17,28 +22,34 @@
 SESSION_COOKIE_AGE = 2419200  # 2419200 seconds == 4 weeks
 # Whether to expire the session when the user closes their browser
 SESSION_EXPIRE_AT_BROWSER_CLOSE = True
-# Whether to use a secure cookie for the session cookie. If this is set to True, the cookie will be marked as “secure”, which means browsers may ensure that the cookie is only sent under an HTTPS connection.
-SESSION_COOKIE_SECURE = False  # Set to True if you have a HTTPS Certificate installed
+# Whether to use a secure cookie for the session cookie. If this is set to True, the cookie will be marked as “secure”,
+# which means browsers may ensure that the cookie is only sent under an HTTPS connection.
+SESSION_COOKIE_SECURE = False  # Set to True if you have an HTTPS Certificate installed
 
 # -------------------- CSRF --------------------
-# Whether to use a secure cookie for the CSRF cookie. If this is set to True, the cookie will be marked as “secure”, which means browsers may ensure that the cookie is only sent with an HTTPS connection.
-CSRF_COOKIE_SECURE = False  # Set to True if you have a HTTPS Certificate installed
+# Whether to use a secure cookie for the CSRF cookie. If this is set to True, the cookie will be marked as “secure”,
+# which means browsers may ensure that the cookie is only sent with an HTTPS connection.
+CSRF_COOKIE_SECURE = False  # Set to True if you have an HTTPS Certificate installed
 # Set to None to use session-based CSRF cookies, which keep the cookies in-memory instead of persistent storage
 CSRF_COOKIE_AGE = None  # Keeps the cookies in-memory
-# Whether to store the CSRF token in a the user's session vs in a cookie
+# Whether to store the CSRF token in the user's session vs in a cookie
 CSRF_USE_SESSIONS = False  # Using cookie
 
 # -------------------- Security --------------------
-# If True, the SecurityMiddleware sets the "X-XSS-Protection: 1; mode=block" header on all responses that do not already have it.
+# If True, the SecurityMiddleware sets the "X-XSS-Protection: 1; mode=block" header
+# on all responses that do not already have it.
 SECURE_BROWSER_XSS_FILTER = True
-# If True, the SecurityMiddleware sets the "X-Content-Type-Options: nosniff" header on all responses that do not already have it.
+# If True, the SecurityMiddleware sets the "X-Content-Type-Options: nosniff" header
+# on all responses that do not already have it.
 SECURE_CONTENT_TYPE_NOSNIFF = True
-# If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict Transport Security header on all responses that do not already have it.
+# If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict Transport Security header
+# on all responses that do not already have it.
 SECURE_HSTS_SECONDS = 15768000
 # If True, the SecurityMiddleware adds the includeSubDomains directive to the HTTP Strict Transport Security header.
 SECURE_HSTS_INCLUDE_SUBDOMAINS = True
-# If True, the SecurityMiddleware redirects all non-HTTPS requests to HTTPS (except for those URLs matching a regular expression listed in SECURE_REDIRECT_EXEMPT).
-SECURE_SSL_REDIRECT = False  # Set to True if you have a HTTPS Certificate installed
+# If True, the SecurityMiddleware redirects all non-HTTPS requests to HTTPS
+# (except for those URLs matching a regular expression listed in SECURE_REDIRECT_EXEMPT).
+SECURE_SSL_REDIRECT = False  # Set to True if you have an HTTPS Certificate installed
 # Set to "DENY" to prevent frames even from the same server
 X_FRAME_OPTIONS = "SAMEORIGIN"
 
@@ -54,6 +65,8 @@
 DATE_FORMAT = "l, F jS, Y"
 SHORT_DATE_FORMAT = "m/d/Y"
 TIME_FORMAT = "g:i A"
+# This format is used on the status dashboard and jumbotron when displaying since when the user has been logged in.
+MONTH_DAY_FORMAT = "l m/d"
 
 # Date and time formats, used in file names when exporting data
 EXPORT_DATE_FORMAT = "m_d_Y"
@@ -61,16 +74,20 @@
 
 # -------------------- Input date and time formats --------------------
 # See allowed formats at https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
-DATETIME_INPUT_FORMATS = ["%m/%d/%Y %I:%M %p"]
-DATE_INPUT_FORMATS = ["%m/%d/%Y"]
-TIME_INPUT_FORMATS = ["%I:%M %p"]
+DATETIME_INPUT_FORMATS = ["%m/%d/%Y %I:%M %p", *global_settings.DATE_INPUT_FORMATS]
+DATE_INPUT_FORMATS = ["%m/%d/%Y", *global_settings.DATE_INPUT_FORMATS]
+TIME_INPUT_FORMATS = ["%I:%M %p", *global_settings.TIME_INPUT_FORMATS]
 
 # -------------------- Internationalization and localization --------------------
-# A boolean that specifies whether Django’s translation system should be enabled. This provides an easy way to turn it off, for performance. If this is set to False, Django will make some optimizations so as not to load the translation machinery.
+# A boolean that specifies whether Django’s translation system should be enabled.
+# This provides an easy way to turn it off, for performance.
+# If this is set to False, Django will make some optimizations so as not to load the translation machinery.
 USE_I18N = False
-# A boolean that specifies if localized formatting of data will be enabled by default or not. If this is set to True, e.g. Django will display numbers and dates using the format of the current locale
+# A boolean that specifies if localized formatting of data will be enabled by default or not.
+# If this is set to True, e.g. Django will display numbers and dates using the format of the current locale
 USE_L10N = False
-# A boolean that specifies if datetimes will be timezone-aware by default or not. If this is set to True, Django will use timezone-aware datetimes internally. Otherwise, Django will use naive datetimes in local time.
+# A boolean that specifies if datetimes will be timezone-aware by default or not. If this is set to True,
+# Django will use timezone-aware datetimes internally. Otherwise, Django will use naive datetimes in local time.
 USE_TZ = True
 
 # -------------------- Installed Apps --------------------
@@ -105,14 +122,20 @@
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
     "django.middleware.common.BrokenLinkEmailsMiddleware",
     "NEMO.middleware.DeviceDetectionMiddleware",
-    "NEMO.middleware.SessionTimeout",  # Needed for LDAP and other non-SSO authentication. Adds a middleware to identify when the user's session has expired.
-    # "NEMO.middleware.HTTPHeaderAuthenticationMiddleware", # Needed for Nginx or other remote user Authentication. Adds a middleware to look for remote user in http header (which can be customized)
-    # "NEMO.middleware.RemoteUserAuthenticationMiddleware", # Needed for development setup with REMOTE_USER environment variable
-    "NEMO.middleware.ImpersonateMiddleware", # This needs to be added AFTER any kind of authentication middleware
-    "NEMO.middleware.NEMOAuditlogMiddleware", # This needs to be set AFTER ImpersonateMiddleware to correctly set the user in the audit log. If set before the ImpersonateMiddleware, the admin user will be the one recorded as making changes, not the impersonated user.
+    # Needed for LDAP and non-SSO authentication. Adds a middleware to identify when the user's session has expired.
+    "NEMO.middleware.SessionTimeout",
+    # Needed for Nginx and remote user Authentication. Adds a middleware to look for remote user in http header.
+    # "NEMO.middleware.HTTPHeaderAuthenticationMiddleware",
+    # Needed for development setup with REMOTE_USER environment variable
+    # "NEMO.middleware.RemoteUserAuthenticationMiddleware",
+    # This needs to be added AFTER any kind of authentication middleware
+    "NEMO.middleware.ImpersonateMiddleware",
+    # This needs to be set AFTER ImpersonateMiddleware to correctly set the user in the audit log. If set before
+    # the ImpersonateMiddleware, the admin user will be the one recorded as making changes, not the impersonated user.
+    "NEMO.middleware.NEMOAuditlogMiddleware",
 ]
 
-# By default HTTPHeaderAuthenticationMiddleware will look in the `AUTHORIZATION` HTTP header.
+# By default, HTTPHeaderAuthenticationMiddleware will look in the `AUTHORIZATION` HTTP header.
 AUTHENTICATION_HEADER = "AUTHORIZATION"
 
 # -------------------- Template Settings --------------------
@@ -143,7 +166,6 @@
 
 # Django REST framework:
 # http://www.django-rest-framework.org/
-from rest_framework.settings import DEFAULTS
 REST_FRAMEWORK = {
     "DEFAULT_PERMISSION_CLASSES": ("NEMO.permissions.DjangoModelPermissions",),
     "DEFAULT_FILTER_BACKENDS": ("NEMO.rest_filter_backend.NEMOFilterBackend",),
@@ -151,7 +173,7 @@
     # "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.TokenAuthentication", "rest_framework.authentication.SessionAuthentication"),
     "DEFAULT_RENDERER_CLASSES": DEFAULTS["DEFAULT_RENDERER_CLASSES"] + ["drf_excel.renderers.XLSXRenderer"],
     "DEFAULT_PARSER_CLASSES": DEFAULTS["DEFAULT_PARSER_CLASSES"] + ["NEMO.parsers.CSVParser"],
-    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
+    "DEFAULT_PAGINATION_CLASS": "NEMO.rest_pagination.NEMOPageNumberPagination",
     "PAGE_SIZE": 1000,
     # Formats used when exporting data in REST API (for export in json, excel, html etc.)
     # "DATETIME_FORMAT": "%m-%d-%Y %H:%M:%S",
@@ -164,10 +186,13 @@
 
 # ALLOWED_HOSTS = ["*"] # "*" allows any domain. Use at your own risk. Not recommended.
 ALLOWED_HOSTS = ["nemo.mydomain.com", "localhost", "192.168.1.2"]  # Preferred way of specifying allowed hosts.
-# Change this SERVER_DOMAIN setting to specify the server domain to use for building links when a request IS NOT available (for email reminders and other timed services).
+# Change this SERVER_DOMAIN setting to specify the server domain to use for building links when
+# a request IS NOT available (for email reminders and other timed services).
 SERVER_DOMAIN = "https://{}".format(ALLOWED_HOSTS[0])
-# When a request IS available, django will use the request server name, or the HTTP_HOST header if provided, or the X_FORWARDED_HOST header if the following setting is uncommented.
-# 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.
+# When a request IS available, django will use the request server name, or the HTTP_HOST header if provided,
+# or the X_FORWARDED_HOST header if the following setting is uncommented.
+# 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
 
 # -------------------- Elevated roles --------------------
@@ -182,13 +207,17 @@
 SERVER_EMAIL = "NEMO Administrator <admin@mydomain.com>"
 # Default email address to use for various automated correspondence from the site manager(s).
 DEFAULT_FROM_EMAIL = "NEMO Webmaster <webmaster@mydomain.com>"
-# Change this to True to ALWAYS use DEFAULT_FROM_EMAIL as sender for all emails and use REPLY_TO with the original sender
+# Change this to True to ALWAYS use DEFAULT_FROM_EMAIL as sender and use REPLY_TO with the original sender
 # Useful when using a single email address (gmail) that prevents "spoofing" (sending as someone else)
 EMAIL_USE_DEFAULT_AND_REPLY_TO = False
 # Name of the reservation calendar invite organizer.
 RESERVATION_ORGANIZER = "NEMO"
-# Email used as the reservation calendar invite organizer email. Defaults to "no_reply" which is an invalid email. Setting a real email address here will mean that email will receive all the responses from every user after they accept the invitation.
+# Email used as the reservation calendar invite organizer email. Defaults to "no_reply" which is an invalid email.
+# Setting a real email address here will mean that email will receive all the responses from every user after they
+# accept the invitation.
 RESERVATION_ORGANIZER_EMAIL = "no_reply"
+# Change this default value to True if you want new users to get ICS calendar invite for reservations by default.
+USER_RESERVATION_PREFERENCES_DEFAULT = False
 
 # -------------------- SMTP Server config --------------------
 # Uncomment the following if using an email SMTP server
@@ -211,7 +240,9 @@
     }
 }
 
-STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"  # Comment this line out for dev. This makes sure static files have a version to avoid having to clear browser cache between releases.
+# Comment this line out for dev. This makes sure static files have a version to avoid
+# having to clear browser cache between releases.
+STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
 STATIC_ROOT = "/nemo/static/"
 STATIC_URL = "/static/"
 MEDIA_ROOT = "/nemo/media/"
@@ -303,7 +334,8 @@
 # USERNAME_REGEX = "^[A-Za-z0-9_]+$"
 
 # Specify your list of LDAP authentication servers only if you choose to use LDAP authentication
-# Below is an example with a free ldap testing server. Usernames and passwords available at https://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/
+# Below is an example with a free ldap testing server. Usernames and passwords available at
+# https://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/
 # LDAP_SERVERS = [
 #     {
 #         "url": "ldap.forumsys.com",
@@ -323,7 +355,8 @@
 #     "domains": [],
 # }
 
-# Audit log. Update this list based on your audit needs. See supported fields at https://django-auditlog.readthedocs.io/en/latest/usage.html#settings
+# Audit log. Update this list based on your audit needs. See supported fields at
+# https://django-auditlog.readthedocs.io/en/latest/usage.html#settings
 AUDITLOG_INCLUDE_TRACKING_MODELS = (
     # Track changes to user access expiration, roles and managed projects
     {
@@ -336,3 +369,13 @@
     "NEMO.Account",
     "NEMO.Customization",
 )
+
+# List of compiled regular expression objects describing URLs that should be ignored
+# when reporting HTTP 404 errors via email
+IGNORABLE_404_URLS = [
+    re.compile(r'\.(php|cgi)$'),
+    re.compile(r'^/phpmyadmin/'),
+    re.compile(r'^/robots.txt$'),
+    re.compile(r"^/apple-touch-icon.*\.png$"),
+    re.compile(r"^/favicon\.ico$"),
+]
diff --git a/resources/splash_pad_settings.py b/resources/splash_pad_settings.py
index 1df4444b..748a9518 100644
--- a/resources/splash_pad_settings.py
+++ b/resources/splash_pad_settings.py
@@ -36,6 +36,7 @@
 	'NEMO.apps.sensors',
 	'NEMO.apps.contracts',
 	'rest_framework',
+	'rest_framework.authtoken',
 	'django_filters',
 	'mptt',
 	'auditlog',
@@ -76,11 +77,15 @@
 	},
 ]
 
+from rest_framework.settings import DEFAULTS
 REST_FRAMEWORK = {
-	'DEFAULT_PERMISSION_CLASSES': ('NEMO.permissions.DjangoModelPermissions',),
-	'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
-	'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
-	'PAGE_SIZE': 1000,
+	"DEFAULT_PERMISSION_CLASSES": ("NEMO.permissions.DjangoModelPermissions",),
+	"DEFAULT_FILTER_BACKENDS": ("NEMO.rest_filter_backend.NEMOFilterBackend",),
+	"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.TokenAuthentication"),
+	"DEFAULT_RENDERER_CLASSES": DEFAULTS["DEFAULT_RENDERER_CLASSES"] + ["drf_excel.renderers.XLSXRenderer"],
+	"DEFAULT_PARSER_CLASSES": DEFAULTS["DEFAULT_PARSER_CLASSES"] + ["NEMO.parsers.CSVParser"],
+	"DEFAULT_PAGINATION_CLASS": "NEMO.rest_pagination.NEMOPageNumberPagination",
+	"PAGE_SIZE": 1000,
 }
 
 SERVER_EMAIL = 'NEMO Server Administrator <nemo.admin@example.org>'
diff --git a/resources/systemd/nemo_auto_logout_users.service b/resources/systemd/nemo_auto_logout_users.service
new file mode 100644
index 00000000..66845ecb
--- /dev/null
+++ b/resources/systemd/nemo_auto_logout_users.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=Auto logout users in NEMO
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/docker exec nemo django-admin area_auto_logout_users
diff --git a/resources/systemd/nemo_auto_logout_users.timer b/resources/systemd/nemo_auto_logout_users.timer
new file mode 100644
index 00000000..e1e5f25a
--- /dev/null
+++ b/resources/systemd/nemo_auto_logout_users.timer
@@ -0,0 +1,8 @@
+[Unit]
+Description=Auto logout users every minute in NEMO
+
+[Timer]
+OnCalendar=minutely
+
+[Install]
+WantedBy=multi-user.target
diff --git a/setup.py b/setup.py
index 177200ca..802391b8 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 
 setup(
 	name='NEMO',
-	version='4.6.4',
+	version='4.7.0',
 	python_requires='>=3.8, <4',
 	packages=find_namespace_packages(exclude=['NEMO.tests', 'NEMO.tests.*']),
 	include_package_data=True,
@@ -24,16 +24,16 @@
 		'Programming Language :: Python :: 3.8',
 	],
 	install_requires=[
-		'cryptography==41.0.2',
-		'Django==3.2.21',
+		'cryptography==41.0.4',
+		'Django==3.2.22',
 		'django-auditlog==2.3.0',
-		'django-filter==23.2',
+		'django-filter==23.3',
 		'django-mptt==0.14.0',
 		'djangorestframework==3.14.0',
 		'drf-excel==2.4.0',
 		'drf-flex-fields==1.0.2',
 		'ldap3==2.9.1',
-		'Pillow==10.0.0',
+		'Pillow==10.0.1',
 		'pymodbus==3.3.2',
 		'python-dateutil==2.8.2',
 		'pytz==2023.3',


Test patch:
diff --git a/NEMO/tests/test_area_access.py b/NEMO/tests/test_area_access.py
index a2d08c6a..7e3f5a96 100644
--- a/NEMO/tests/test_area_access.py
+++ b/NEMO/tests/test_area_access.py
@@ -1,4 +1,7 @@
+import datetime
+
 from django.contrib.auth.models import Permission
+from django.core.management import call_command
 from django.test import TestCase
 from django.urls import reverse
 from django.utils import timezone
@@ -88,7 +91,6 @@ def test_farewell_screen_fails(self):
 		self.assertEqual(response.status_code, 200)  # All good now
 		self.assertTrue("farewell_screen" in response.request['PATH_INFO'])
 
-
 	def test_login_to_area(self):
 		response = self.client.post(reverse('login_to_area', kwargs={'door_id': door.id}), follow=True)
 		test_response_is_failed_login(self, response)
@@ -380,3 +382,24 @@ def test_door(self):
 		self.assertEqual(door.interlock.state, Interlock.State.UNKNOWN)
 		# unlocking door is an async action and creates problems when testing with SQLite (Database Table locked)
 
+
+class AreaAutoLogoutTestCase(TestCase):
+	def testAutoLogout(self):
+		user = login_as_user(self.client)
+		area = Area.objects.create(name="Cleanroom", welcome_message="Welcome to the cleanroom")
+		project = Project.objects.create(name="Project1", account=Account.objects.create(name="Account1"))
+		start = timezone.now() - datetime.timedelta(minutes=5)
+		record = AreaAccessRecord.objects.create(area=area, customer=user, project=project, start=start)
+		call_command("area_auto_logout_users")
+		# Nothing should happen
+		self.assertFalse(AreaAccessRecord.objects.get(id=record.id).end)
+		area.auto_logout_time = 10
+		area.save()
+		call_command("area_auto_logout_users")
+		# Nothing should happen
+		self.assertFalse(AreaAccessRecord.objects.get(id=record.id).end)
+		area.auto_logout_time = 3
+		area.save()
+		call_command("area_auto_logout_users")
+		new_record = AreaAccessRecord.objects.get(id=record.id)
+		self.assertEqual(new_record.end, new_record.start + datetime.timedelta(minutes=3))
diff --git a/NEMO/tests/test_calendar/test_area_reservation.py b/NEMO/tests/test_calendar/test_area_reservation.py
index 513402d6..21f31cb5 100644
--- a/NEMO/tests/test_calendar/test_area_reservation.py
+++ b/NEMO/tests/test_calendar/test_area_reservation.py
@@ -295,18 +295,18 @@ def test_resize_reservation(self):
 		end_outage = start_outage + timedelta(hours=1)
 		ScheduledOutage.objects.create(area=area, start=start_outage, end=end_outage, creator=staff)
 		response = self.client.post(reverse('resize_reservation'), {'delta': 61, 'id': reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		self.assertContains(response, "Your reservation coincides with a scheduled outage. Please choose a different time.")
 
 		# test reduce reservation time by 10 min
 		response = self.client.post(reverse('resize_reservation'), {'delta': -10, 'id': reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		old_reservation = Reservation.objects.get(pk=reservation.id)
 		self.assertTrue(old_reservation.cancelled)
-		self.assertEquals(old_reservation.cancelled_by, consumer)
-		self.assertEquals(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
+		self.assertEqual(old_reservation.cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
 		new_reservation = list(Reservation.objects.filter(area=area, cancelled=False))[0]
-		self.assertEquals(new_reservation.end, old_reservation.end - timedelta(minutes=10))
+		self.assertEqual(new_reservation.end, old_reservation.end - timedelta(minutes=10))
 
 		# test resize cancelled reservation
 		reservation = Reservation.objects.get(pk=reservation.id)
@@ -315,41 +315,41 @@ def test_resize_reservation(self):
 
 		# test increase reservation time by 10 min
 		response = self.client.post(reverse('resize_reservation'), {'delta': 10, 'id': new_reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		old_reservation = Reservation.objects.get(pk=new_reservation.id)
 		self.assertTrue(old_reservation.cancelled)
-		self.assertEquals(old_reservation.cancelled_by, consumer)
-		self.assertEquals(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
+		self.assertEqual(old_reservation.cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
 		new_reservation = list(Reservation.objects.filter(area=area, cancelled=False))[0]
-		self.assertEquals(new_reservation.end, old_reservation.end + timedelta(minutes=10))
+		self.assertEqual(new_reservation.end, old_reservation.end + timedelta(minutes=10))
 		new_reservation.delete()
 
 		# test increase reservation time by 10 min while logged in
 		older_resa_still_ongoing = Reservation.objects.create(area=area, start=timezone.now() - timedelta(hours=1), end=timezone.now() + timedelta(hours=1), user=consumer, creator=consumer, missed=False, short_notice=False)
 		AreaAccessRecord.objects.create(area=area, customer=consumer, start=timezone.now(), project=project)
 		response = self.client.post(reverse('resize_reservation'), {'delta': 10, 'id': older_resa_still_ongoing.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		old_reservation = Reservation.objects.get(pk=older_resa_still_ongoing.id)
 		self.assertTrue(old_reservation.cancelled)
-		self.assertEquals(old_reservation.cancelled_by, consumer)
-		self.assertEquals(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
+		self.assertEqual(old_reservation.cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
 		new_reservation = list(Reservation.objects.filter(area=area, cancelled=False))[0]
-		self.assertEquals(new_reservation.end, old_reservation.end + timedelta(minutes=10))
-		self.assertEquals(new_reservation.start, old_reservation.start)
+		self.assertEqual(new_reservation.end, old_reservation.end + timedelta(minutes=10))
+		self.assertEqual(new_reservation.start, old_reservation.start)
 		new_reservation.delete()
 
 		# test decrease reservation time by 10 min while logged in
 		older_resa_still_ongoing = Reservation.objects.create(area=area, start=timezone.now() - timedelta(hours=1), end=timezone.now() + timedelta(hours=1), user=consumer, creator=consumer, missed=False, short_notice=False)
 		AreaAccessRecord.objects.create(area=area, customer=consumer, start=timezone.now(), project=project)
 		response = self.client.post(reverse('resize_reservation'), {'delta': -10, 'id': older_resa_still_ongoing.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		old_reservation = Reservation.objects.get(pk=older_resa_still_ongoing.id)
 		self.assertTrue(old_reservation.cancelled)
-		self.assertEquals(old_reservation.cancelled_by, consumer)
-		self.assertEquals(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
+		self.assertEqual(old_reservation.cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
 		new_reservation = list(Reservation.objects.filter(area=area, cancelled=False))[0]
-		self.assertEquals(new_reservation.end, old_reservation.end - timedelta(minutes=10))
-		self.assertEquals(new_reservation.start, old_reservation.start)
+		self.assertEqual(new_reservation.end, old_reservation.end - timedelta(minutes=10))
+		self.assertEqual(new_reservation.start, old_reservation.start)
 
 	def test_move_reservation(self):
 		# create reservation
@@ -359,47 +359,47 @@ def test_move_reservation(self):
 
 		login_as(self.client, consumer)
 		response = self.client.post(reverse('create_reservation'), data, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		reservation = Reservation.objects.get(area=area)
 		self.assertTrue(reservation.id)
 
 		# test wrong delta
 		response = self.client.post(reverse('move_reservation'), {'delta': 'asd', 'id': reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals(response.content.decode(), "Invalid delta")
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual(response.content.decode(), "Invalid delta")
 
 		# test no reservation id
 		response = self.client.post(reverse('move_reservation'), {'delta': 10}, follow=True)
-		self.assertEquals(response.status_code, 404)
-		self.assertEquals(response.content.decode(), "The reservation that you wish to modify doesn't exist!")
+		self.assertEqual(response.status_code, 404)
+		self.assertEqual(response.content.decode(), "The reservation that you wish to modify doesn't exist!")
 
 		# create an outage and try to move reservation to overlap outage
 		start_outage = (end + timedelta(hours=1)).astimezone()
 		end_outage = start_outage + timedelta(hours=1)
 		ScheduledOutage.objects.create(area=area, start=start_outage, end=end_outage, creator=staff)
 		response = self.client.post(reverse('move_reservation'), {'delta': 61, 'id': reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		self.assertContains(response, "Your reservation coincides with a scheduled outage. Please choose a different time.")
 
 		# test move reservation while logged in area
 		older_resa_still_ongoing = Reservation.objects.create(area=area, start=timezone.now() - timedelta(hours=1), end=timezone.now() + timedelta(hours=1), user=consumer, creator=consumer, missed=False, short_notice=False)
 		area_access_record = AreaAccessRecord.objects.create(area=area, customer=consumer, start=timezone.now(), project=project)
 		response = self.client.post(reverse('move_reservation'), {'delta': -10, 'id': older_resa_still_ongoing.id}, follow=True)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals(response.content.decode(), "You may only resize an area reservation while logged in that area.")
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual(response.content.decode(), "You may only resize an area reservation while logged in that area.")
 		area_access_record.delete()
 		older_resa_still_ongoing.delete()
 
 		# test move reservation 10 min earlier
 		response = self.client.post(reverse('move_reservation'), {'delta': -10, 'id': reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		old_reservation = Reservation.objects.get(pk=reservation.id)
 		self.assertTrue(old_reservation.cancelled)
-		self.assertEquals(old_reservation.cancelled_by, consumer)
-		self.assertEquals(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
+		self.assertEqual(old_reservation.cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
 		new_reservation = list(Reservation.objects.filter(area=area, cancelled=False))[0]
-		self.assertEquals(new_reservation.end, old_reservation.end - timedelta(minutes=10))
-		self.assertEquals(new_reservation.start, old_reservation.start - timedelta(minutes=10))
+		self.assertEqual(new_reservation.end, old_reservation.end - timedelta(minutes=10))
+		self.assertEqual(new_reservation.start, old_reservation.start - timedelta(minutes=10))
 
 		# test move cancelled reservation
 		reservation = Reservation.objects.get(pk=reservation.id)
@@ -408,14 +408,14 @@ def test_move_reservation(self):
 
 		# test move new reservation 10 min later
 		response = self.client.post(reverse('move_reservation'), {'delta': 10, 'id': new_reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		old_reservation = Reservation.objects.get(pk=new_reservation.id)
 		self.assertTrue(old_reservation.cancelled)
-		self.assertEquals(old_reservation.cancelled_by, consumer)
-		self.assertEquals(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
+		self.assertEqual(old_reservation.cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.filter(area=area, cancelled=False).count(), 1)
 		new_reservation = list(Reservation.objects.filter(area=area, cancelled=False))[0]
-		self.assertEquals(new_reservation.end, old_reservation.end + timedelta(minutes=10))
-		self.assertEquals(new_reservation.start, old_reservation.start + timedelta(minutes=10))
+		self.assertEqual(new_reservation.end, old_reservation.end + timedelta(minutes=10))
+		self.assertEqual(new_reservation.start, old_reservation.start + timedelta(minutes=10))
 
 	def test_cancel_reservation(self):
 		# create reservation
@@ -425,54 +425,54 @@ def test_cancel_reservation(self):
 
 		login_as(self.client, consumer)
 		response = self.client.post(reverse('create_reservation'), data, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		reservation = Reservation.objects.get(area=area)
 		self.assertTrue(reservation.id)
 
 		# get should fail
 		response = self.client.get(reverse('cancel_reservation', kwargs={'reservation_id': 999}), {}, follow=True)
-		self.assertEquals(response.status_code, 405)
+		self.assertEqual(response.status_code, 405)
 
 		# test wrong id
 		response = self.client.post(reverse('cancel_reservation', kwargs={'reservation_id': 999}), {}, follow=True)
-		self.assertEquals(response.status_code, 404)
+		self.assertEqual(response.status_code, 404)
 
 		# test non staff user trying to cancel reservation
 		login_as_user(self.client)
 		response = self.client.post(reverse('cancel_reservation', kwargs={'reservation_id': reservation.id}), {}, follow=True)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals("You must provide a reason when cancelling someone else's reservation.", response.content.decode())
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual("You must provide a reason when cancelling someone else's reservation.", response.content.decode())
 
 		response = self.client.post(reverse('cancel_reservation', kwargs={'reservation_id': reservation.id}), {'reason':'reason'}, follow=True)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals("You may not cancel reservations that you do not own.", response.content.decode())
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual("You may not cancel reservations that you do not own.", response.content.decode())
 
 		login_as(self.client, consumer)
 
 		# test cancel missed reservation
 		missed_resa = Reservation.objects.create(area=area, start=(start+timedelta(days=1)).astimezone(), end=(end+timedelta(days=1)).astimezone(), user=consumer, creator=consumer, missed=True, short_notice=False)
 		response = self.client.post(reverse('cancel_reservation', kwargs={'reservation_id': missed_resa.id}), {}, follow=True)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals(response.content.decode(), "This reservation was missed and cannot be modified.")
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual(response.content.decode(), "This reservation was missed and cannot be modified.")
 
 		# test cancel already ended reservation
 		already_ended_resa = Reservation.objects.create(area=area, start=(start - timedelta(days=1)).astimezone(), end=(end - timedelta(days=1)).astimezone(), user=consumer, creator=consumer, missed=False, short_notice=False)
 		response = self.client.post(reverse('cancel_reservation', kwargs={'reservation_id': already_ended_resa.id}), {}, follow=True)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals(response.content.decode(), "You may not cancel reservations that have already ended.")
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual(response.content.decode(), "You may not cancel reservations that have already ended.")
 
 		# test cancel reservation while logged in area
 		older_resa_still_ongoing = Reservation.objects.create(area=area, start=timezone.now() - timedelta(hours=1), end=timezone.now() + timedelta(hours=1), user=consumer, creator=consumer, missed=False, short_notice=False)
 		AreaAccessRecord.objects.create(area=area, customer=consumer, start=timezone.now(), project=project)
 		response = self.client.post(reverse('cancel_reservation', kwargs={'reservation_id': older_resa_still_ongoing.id}), {}, follow=True)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals(response.content.decode(), "You may not cancel an area reservation while logged in that area.")
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual(response.content.decode(), "You may not cancel an area reservation while logged in that area.")
 
 		# cancel reservation
 		response = self.client.post(reverse('cancel_reservation', kwargs={'reservation_id': reservation.id}), {}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		self.assertTrue(Reservation.objects.get(pk=reservation.id).cancelled)
-		self.assertEquals(Reservation.objects.get(pk=reservation.id).cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.get(pk=reservation.id).cancelled_by, consumer)
 
 		# test cancel already cancelled reservation
 		response = self.client.post(reverse('cancel_reservation', kwargs={'reservation_id': reservation.id}), {}, follow=True)
@@ -485,9 +485,9 @@ def test_cancel_reservation(self):
 		self.assertContains(response, "You must provide a reason when cancelling someone else's reservation.", status_code=400)
 
 		response = self.client.post(reverse('cancel_reservation', kwargs={'reservation_id': other_resa.id}), {'reason': 'reason'}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		self.assertTrue(Reservation.objects.get(pk=other_resa.id).cancelled)
-		self.assertEquals(Reservation.objects.get(pk=other_resa.id).cancelled_by, staff)
+		self.assertEqual(Reservation.objects.get(pk=other_resa.id).cancelled_by, staff)
 
 	def test_reservation_details(self):
 		# create reservation
@@ -497,7 +497,7 @@ def test_reservation_details(self):
 
 		login_as(self.client, consumer)
 		response = self.client.post(reverse('create_reservation'), data, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		reservation = Reservation.objects.get(area=area)
 		self.assertTrue(reservation.id)
 
@@ -506,14 +506,14 @@ def test_reservation_details(self):
 
 		# post should fail
 		response = self.client.post(reverse('reservation_details', kwargs={'reservation_id': 999}), {}, follow=True)
-		self.assertEquals(response.status_code, 405)
+		self.assertEqual(response.status_code, 405)
 
 		# test wrong id
 		response = self.client.get(reverse('reservation_details', kwargs={'reservation_id': 999}), {}, follow=True)
-		self.assertEquals(response.status_code, 404)
+		self.assertEqual(response.status_code, 404)
 
 		response = self.client.get(reverse('reservation_details', kwargs={'reservation_id': reservation.id}), {}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 
 	def test_reservation_with_area_configuration(self):
 		# TODO: create those tests
diff --git a/NEMO/tests/test_calendar/test_tool_qualification_expiration.py b/NEMO/tests/test_calendar/test_tool_qualification_expiration.py
index 7873600a..6708ac74 100644
--- a/NEMO/tests/test_calendar/test_tool_qualification_expiration.py
+++ b/NEMO/tests/test_calendar/test_tool_qualification_expiration.py
@@ -6,8 +6,8 @@
 from django.utils import timezone
 
 from NEMO.models import Account, EmailLog, Project, Qualification, Tool, UsageEvent, User
-from NEMO.views.calendar import do_manage_tool_qualifications
 from NEMO.views.customization import EmailsCustomization, ToolCustomization
+from NEMO.views.timed_services import do_manage_tool_qualifications
 
 
 @patch("django.core.files.storage.FileSystemStorage.exists")
diff --git a/NEMO/tests/test_calendar/test_tool_reservation.py b/NEMO/tests/test_calendar/test_tool_reservation.py
index bb5f9489..1ffe2b17 100644
--- a/NEMO/tests/test_calendar/test_tool_reservation.py
+++ b/NEMO/tests/test_calendar/test_tool_reservation.py
@@ -351,20 +351,20 @@ def test_resize_reservation(self):
 		end_outage = start_outage + timedelta(hours=1)
 		ScheduledOutage.objects.create(tool=tool, start=start_outage, end=end_outage, creator=staff)
 		response = self.client.post(reverse("resize_reservation"), {"delta": 61, "id": reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		self.assertContains(
 			response, "Your reservation coincides with a scheduled outage. Please choose a different time."
 		)
 
 		# test reduce reservation time by 10 min
 		response = self.client.post(reverse("resize_reservation"), {"delta": -10, "id": reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		old_reservation = Reservation.objects.get(pk=reservation.id)
 		self.assertTrue(old_reservation.cancelled)
-		self.assertEquals(old_reservation.cancelled_by, consumer)
-		self.assertEquals(Reservation.objects.filter(tool=tool, cancelled=False).count(), 1)
+		self.assertEqual(old_reservation.cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.filter(tool=tool, cancelled=False).count(), 1)
 		new_reservation = list(Reservation.objects.filter(tool=tool, cancelled=False))[0]
-		self.assertEquals(new_reservation.end, old_reservation.end - timedelta(minutes=10))
+		self.assertEqual(new_reservation.end, old_reservation.end - timedelta(minutes=10))
 
 		# test resize cancelled reservation
 		reservation = Reservation.objects.get(pk=reservation.id)
@@ -373,13 +373,13 @@ def test_resize_reservation(self):
 
 		# test increase reservation time by 10 min
 		response = self.client.post(reverse("resize_reservation"), {"delta": 10, "id": new_reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		old_reservation = Reservation.objects.get(pk=new_reservation.id)
 		self.assertTrue(old_reservation.cancelled)
-		self.assertEquals(old_reservation.cancelled_by, consumer)
-		self.assertEquals(Reservation.objects.filter(tool=tool, cancelled=False).count(), 1)
+		self.assertEqual(old_reservation.cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.filter(tool=tool, cancelled=False).count(), 1)
 		new_reservation = list(Reservation.objects.filter(tool=tool, cancelled=False))[0]
-		self.assertEquals(new_reservation.end, old_reservation.end + timedelta(minutes=10))
+		self.assertEqual(new_reservation.end, old_reservation.end + timedelta(minutes=10))
 
 	def test_move_reservation(self):
 		# create reservation
@@ -389,40 +389,40 @@ def test_move_reservation(self):
 
 		login_as(self.client, consumer)
 		response = self.client.post(reverse("create_reservation"), data, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		reservation = Reservation.objects.get(tool=tool)
 		self.assertTrue(reservation.id)
 
 		# test wrong delta
 		response = self.client.post(reverse("move_reservation"), {"delta": "asd", "id": reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals(response.content.decode(), "Invalid delta")
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual(response.content.decode(), "Invalid delta")
 
 		# test no reservation id
 		response = self.client.post(reverse("move_reservation"), {"delta": 10}, follow=True)
-		self.assertEquals(response.status_code, 404)
-		self.assertEquals(response.content.decode(), "The reservation that you wish to modify doesn't exist!")
+		self.assertEqual(response.status_code, 404)
+		self.assertEqual(response.content.decode(), "The reservation that you wish to modify doesn't exist!")
 
 		# create an outage and try to move reservation to overlap outage
 		start_outage = (end + timedelta(hours=1)).astimezone()
 		end_outage = start_outage + timedelta(hours=1)
 		ScheduledOutage.objects.create(tool=tool, start=start_outage, end=end_outage, creator=staff)
 		response = self.client.post(reverse("move_reservation"), {"delta": 61, "id": reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		self.assertContains(
 			response, "Your reservation coincides with a scheduled outage. Please choose a different time."
 		)
 
 		# test move reservation 10 min earlier
 		response = self.client.post(reverse("move_reservation"), {"delta": -10, "id": reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		old_reservation = Reservation.objects.get(pk=reservation.id)
 		self.assertTrue(old_reservation.cancelled)
-		self.assertEquals(old_reservation.cancelled_by, consumer)
-		self.assertEquals(Reservation.objects.filter(tool=tool, cancelled=False).count(), 1)
+		self.assertEqual(old_reservation.cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.filter(tool=tool, cancelled=False).count(), 1)
 		new_reservation = list(Reservation.objects.filter(tool=tool, cancelled=False))[0]
-		self.assertEquals(new_reservation.end, old_reservation.end - timedelta(minutes=10))
-		self.assertEquals(new_reservation.start, old_reservation.start - timedelta(minutes=10))
+		self.assertEqual(new_reservation.end, old_reservation.end - timedelta(minutes=10))
+		self.assertEqual(new_reservation.start, old_reservation.start - timedelta(minutes=10))
 
 		# test move cancelled reservation
 		reservation = Reservation.objects.get(pk=reservation.id)
@@ -431,14 +431,14 @@ def test_move_reservation(self):
 
 		# test move new reservation 10 min later
 		response = self.client.post(reverse("move_reservation"), {"delta": 10, "id": new_reservation.id}, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		old_reservation = Reservation.objects.get(pk=new_reservation.id)
 		self.assertTrue(old_reservation.cancelled)
-		self.assertEquals(old_reservation.cancelled_by, consumer)
-		self.assertEquals(Reservation.objects.filter(tool=tool, cancelled=False).count(), 1)
+		self.assertEqual(old_reservation.cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.filter(tool=tool, cancelled=False).count(), 1)
 		new_reservation = list(Reservation.objects.filter(tool=tool, cancelled=False))[0]
-		self.assertEquals(new_reservation.end, old_reservation.end + timedelta(minutes=10))
-		self.assertEquals(new_reservation.start, old_reservation.start + timedelta(minutes=10))
+		self.assertEqual(new_reservation.end, old_reservation.end + timedelta(minutes=10))
+		self.assertEqual(new_reservation.start, old_reservation.start + timedelta(minutes=10))
 
 	def test_cancel_reservation(self):
 		# create reservation
@@ -448,33 +448,33 @@ def test_cancel_reservation(self):
 
 		login_as(self.client, consumer)
 		response = self.client.post(reverse("create_reservation"), data, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		reservation = Reservation.objects.get(tool=tool)
 		self.assertTrue(reservation.id)
 
 		# get should fail
 		response = self.client.get(reverse("cancel_reservation", kwargs={"reservation_id": 999}), {}, follow=True)
-		self.assertEquals(response.status_code, 405)
+		self.assertEqual(response.status_code, 405)
 
 		# test wrong id
 		response = self.client.post(reverse("cancel_reservation", kwargs={"reservation_id": 999}), {}, follow=True)
-		self.assertEquals(response.status_code, 404)
+		self.assertEqual(response.status_code, 404)
 
 		# test non staff user trying to cancel reservation
 		login_as_user(self.client)
 		response = self.client.post(
 			reverse("cancel_reservation", kwargs={"reservation_id": reservation.id}), {}, follow=True
 		)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals(
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual(
 			"You must provide a reason when cancelling someone else's reservation.", response.content.decode()
 		)
 
 		response = self.client.post(
 			reverse("cancel_reservation", kwargs={"reservation_id": reservation.id}), {"reason": "reason"}, follow=True
 		)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals("You may not cancel reservations that you do not own.", response.content.decode())
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual("You may not cancel reservations that you do not own.", response.content.decode())
 
 		login_as(self.client, consumer)
 
@@ -491,8 +491,8 @@ def test_cancel_reservation(self):
 		response = self.client.post(
 			reverse("cancel_reservation", kwargs={"reservation_id": missed_resa.id}), {}, follow=True
 		)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals(response.content.decode(), "This reservation was missed and cannot be modified.")
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual(response.content.decode(), "This reservation was missed and cannot be modified.")
 
 		# test cancel already ended reservation
 		already_ended_resa = Reservation.objects.create(
@@ -507,22 +507,22 @@ def test_cancel_reservation(self):
 		response = self.client.post(
 			reverse("cancel_reservation", kwargs={"reservation_id": already_ended_resa.id}), {}, follow=True
 		)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals(response.content.decode(), "You may not cancel reservations that have already ended.")
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual(response.content.decode(), "You may not cancel reservations that have already ended.")
 
 		# cancel reservation
 		response = self.client.post(
 			reverse("cancel_reservation", kwargs={"reservation_id": reservation.id}), {}, follow=True
 		)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		self.assertTrue(Reservation.objects.get(pk=reservation.id).cancelled)
-		self.assertEquals(Reservation.objects.get(pk=reservation.id).cancelled_by, consumer)
+		self.assertEqual(Reservation.objects.get(pk=reservation.id).cancelled_by, consumer)
 
 		# test cancel already cancelled reservation
 		response = self.client.post(
 			reverse("cancel_reservation", kwargs={"reservation_id": reservation.id}), {}, follow=True
 		)
-		self.assertEquals(response.status_code, 400)
+		self.assertEqual(response.status_code, 400)
 		self.assertContains(response, "This reservation has already been cancelled by ", status_code=400)
 
 		# test staff cancelling somebody else's reservation
@@ -538,17 +538,17 @@ def test_cancel_reservation(self):
 		response = self.client.post(
 			reverse("cancel_reservation", kwargs={"reservation_id": other_resa.id}), {}, follow=True
 		)
-		self.assertEquals(response.status_code, 400)
-		self.assertEquals(
+		self.assertEqual(response.status_code, 400)
+		self.assertEqual(
 			"You must provide a reason when cancelling someone else's reservation.", response.content.decode()
 		)
 
 		response = self.client.post(
 			reverse("cancel_reservation", kwargs={"reservation_id": other_resa.id}), {"reason": "reason"}, follow=True
 		)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		self.assertTrue(Reservation.objects.get(pk=other_resa.id).cancelled)
-		self.assertEquals(Reservation.objects.get(pk=other_resa.id).cancelled_by, staff)
+		self.assertEqual(Reservation.objects.get(pk=other_resa.id).cancelled_by, staff)
 
 	def test_reservation_details(self):
 		# create reservation
@@ -558,7 +558,7 @@ def test_reservation_details(self):
 
 		login_as(self.client, consumer)
 		response = self.client.post(reverse("create_reservation"), data, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		reservation = Reservation.objects.get(tool=tool)
 		self.assertTrue(reservation.id)
 
@@ -567,16 +567,16 @@ def test_reservation_details(self):
 
 		# post should fail
 		response = self.client.post(reverse("reservation_details", kwargs={"reservation_id": 999}), {}, follow=True)
-		self.assertEquals(response.status_code, 405)
+		self.assertEqual(response.status_code, 405)
 
 		# test wrong id
 		response = self.client.get(reverse("reservation_details", kwargs={"reservation_id": 999}), {}, follow=True)
-		self.assertEquals(response.status_code, 404)
+		self.assertEqual(response.status_code, 404)
 
 		response = self.client.get(
 			reverse("reservation_details", kwargs={"reservation_id": reservation.id}), {}, follow=True
 		)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 
 	def test_reservation_with_tool_configuration(self):
 		config = Configuration.objects.create(
@@ -598,14 +598,14 @@ def test_reservation_with_tool_configuration(self):
 
 		login_as(self.client, consumer)
 		response = self.client.post(reverse("create_reservation"), data, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		self.assertContains(response, "Choose tool configuration")
 		self.assertFalse(Reservation.objects.filter(tool=tool).exists())
 
 		data["configured"] = "true"
 		login_as(self.client, consumer)
 		response = self.client.post(reverse("create_reservation"), data, follow=True)
-		self.assertEquals(response.status_code, 200)
+		self.assertEqual(response.status_code, 200)
 		self.assertTrue(Reservation.objects.get(tool=tool))
 
 	def test_reservation_policy_off(self):
diff --git a/NEMO/tests/test_closures.py b/NEMO/tests/test_closures.py
index 3175adb2..8983e831 100644
--- a/NEMO/tests/test_closures.py
+++ b/NEMO/tests/test_closures.py
@@ -6,7 +6,7 @@
 from NEMO.admin import ClosureAdminForm
 from NEMO.models import Alert, Closure, ClosureTime, Customization, EmailLog, User
 from NEMO.utilities import EmailCategory
-from NEMO.views.calendar import do_create_closure_alerts
+from NEMO.views.timed_services import do_create_closure_alerts
 
 
 class ClosuresTestCase(TestCase):
diff --git a/NEMO/tests/test_consumables.py b/NEMO/tests/test_consumables.py
index 76cd368c..587d6f3e 100644
--- a/NEMO/tests/test_consumables.py
+++ b/NEMO/tests/test_consumables.py
@@ -8,7 +8,7 @@
 from django.utils import timezone
 
 from NEMO.forms import RecurringConsumableChargeForm
-from NEMO.models import Consumable, ConsumableWithdraw, RecurringConsumableCharge, User
+from NEMO.models import Consumable, ConsumableWithdraw, RecurringConsumableCharge, Tool, UsageEvent, User
 from NEMO.tests.test_utilities import (
 	create_user_and_project,
 	login_as,
@@ -19,7 +19,10 @@
 )
 from NEMO.utilities import RecurrenceFrequency, beginning_of_the_day, format_datetime, get_recurring_rule
 from NEMO.views.consumables import make_withdrawal
-from NEMO.views.customization import RecurringChargesCustomization
+from NEMO.views.customization import (
+	ApplicationCustomization,
+	RecurringChargesCustomization,
+)
 
 consumable: Optional[Consumable] = None
 supply: Optional[Consumable] = None
@@ -76,8 +79,38 @@ def test_withdrawal_not_allowed(self):
 		customer_project.allow_consumable_withdrawals = False
 		customer_project.save()
 		self.assertRaises(ValidationError, make_withdrawal, consumable.id, 1, customer_project.id, staff, customer.id)
+		tool = Tool.objects.create(name="test_tool1", primary_owner=staff)
+		start = timezone.now() - timedelta(hours=2)
+		usage_event = UsageEvent.objects.create(user=customer, operator=customer, tool=tool, project=customer_project, start=start, end=timezone.now())
 		# However, it should always work for tool usage (otherwise the user cannot disable the tool)
-		make_withdrawal(consumable.id, 1, customer_project.id, staff, customer.id, tool_usage=True)
+		make_withdrawal(consumable.id, 1, customer_project.id, staff, customer.id, usage_event=usage_event)
+
+	def test_post_self_checkout_not_allowed(self):
+		# save consumable_user_self_checkout value
+		consumable_user_self_checkout = ApplicationCustomization.get("consumable_user_self_checkout")
+		try:
+			ApplicationCustomization.set("consumable_user_self_checkout", "enabled")
+			consumable_no_self_checkout = Consumable.objects.create(
+				name="Consumable No Self Checkout",
+				quantity=10,
+				reminder_threshold=5,
+				reminder_email="test@test.com",
+				allow_self_checkout=False
+			)
+			customer, customer_project = create_user_and_project()
+			quantity = consumable_no_self_checkout.quantity
+			login_as(self.client, customer)
+			data = {"customer": customer.id, "project": customer_project.id, "consumable": consumable_no_self_checkout.id, "quantity": "1"}
+			response = self.client.post(reverse("consumables"), data, follow=True)
+			# Should fail because consumable is not tagged with allow_self_checkout = True
+			self.assertEqual(response.status_code, 400)
+			# Checkout, nothing should happen since the order did not go through
+			response = self.client.post(reverse("withdraw_consumables"), follow=True)
+			self.assertEqual(response.status_code, 200)
+			self.assertEqual(quantity, Consumable.objects.get(pk=consumable_no_self_checkout.id).quantity)
+		finally:
+			# restore consumable_user_self_checkout value
+			ApplicationCustomization.set("consumable_user_self_checkout", consumable_user_self_checkout)
 
 	def test_clean(self):
 		test_consumable = Consumable()
diff --git a/NEMO/tests/test_misc.py b/NEMO/tests/test_misc.py
index e00eb1d9..d954e71b 100644
--- a/NEMO/tests/test_misc.py
+++ b/NEMO/tests/test_misc.py
@@ -1,3 +1,5 @@
+import re
+
 from django.conf import settings
 from django.test import TestCase
 from PIL import Image
@@ -36,4 +38,13 @@ def test_resize_image(self):
 			self.assertEqual(resized_height, 40)
 		resized_image.close()
 
-
+	def test_regex_escaping(self):
+		# non escaped, no match
+		self.assertFalse(re.match("test?[]]", "test?[]]"))
+		# escaped, match
+		self.assertTrue(re.match(re.escape("test?[]]"), "test?[]]"))
+		# escaped + number
+		self.assertTrue(re.match(re.escape("test?[]]") + "\d+$", "test?[]]5"))
+		self.assertTrue(re.match(re.escape("test?[]]") + "\d+$", "test?[]]124"))
+		# number is mandatory
+		self.assertFalse(re.match(re.escape("test?[]]") + "\d+$", "test?[]]"))
diff --git a/NEMO/tests/test_requests/test_adjustment_requests.py b/NEMO/tests/test_requests/test_adjustment_requests.py
index fc97791e..ee7d0b2c 100644
--- a/NEMO/tests/test_requests/test_adjustment_requests.py
+++ b/NEMO/tests/test_requests/test_adjustment_requests.py
@@ -65,8 +65,8 @@ def test_create_request(self):
         self.assertEqual(adjustment_request.status, RequestStatus.PENDING)
 
     def test_notification_created(self):
-        # Test that new adjustment request => facility managers are notified
-        facility_manager = User.objects.create(
+        # Test that new adjustment request => reviewers are notified (here just managers)
+        reviewer = User.objects.create(
             username="test_manager", first_name="Managy", last_name="McManager", is_facility_manager=True
         )
         login_as_user(self.client)
@@ -75,9 +75,7 @@ def test_notification_created(self):
         }
         self.client.post(reverse("create_adjustment_request"), data=data)
         self.assertTrue(
-            Notification.objects.filter(
-                user=facility_manager, notification_type=Notification.Types.ADJUSTMENT_REQUEST
-            ).exists()
+            Notification.objects.filter(user=reviewer, notification_type=Notification.Types.ADJUSTMENT_REQUEST).exists()
         )
 
     def test_delete_request(self):
@@ -111,7 +109,7 @@ def test_deny_request(self):
         self.review_request(deny_request="Deny")
 
     def review_request(self, approve_request="", deny_request=""):
-        manager = User.objects.create(username="manager", first_name="", last_name="Manager", is_facility_manager=True)
+        reviewer = User.objects.create(username="manager", first_name="", last_name="Manager", is_facility_manager=True)
         user, project = create_user_and_project()
         login_as(self.client, user)
         data = {
@@ -133,24 +131,22 @@ def review_request(self, approve_request="", deny_request=""):
             data["deny_request"] = deny_request
         response = self.client.post(reverse("edit_adjustment_request", args=[adjustment_request.id]), data=data)
         # regular user cannot edit request they didn't create
-        self.assertContains(response, "You are not allowed to edit a request you")
+        self.assertContains(response, "You are not allowed to edit this request.")
         staff = login_as_staff(self.client)
         response = self.client.post(reverse("edit_adjustment_request", args=[adjustment_request.id]), data=data)
         # regular staff cannot either
-        self.assertContains(response, "You are not allowed to edit a request you")
-        facility_manager = staff
-        facility_manager.is_facility_manager = True
-        facility_manager.save()
-        login_as(self.client, facility_manager)
+        self.assertContains(response, "You are not allowed to edit this request.")
+        # reviewer can
+        login_as(self.client, reviewer)
         response = self.client.post(reverse("edit_adjustment_request", args=[adjustment_request.id]), data=data)
         self.assertRedirects(response, reverse("user_requests", args=["adjustment"]))
         adjustment_request = AdjustmentRequest.objects.get(id=adjustment_request.id)
         status = RequestStatus.APPROVED if approve_request else RequestStatus.DENIED
         self.assertEqual(adjustment_request.status, status)
-        # Notification doesn't exist anymore (for manager)
+        # Notification doesn't exist anymore (for reviewer)
         self.assertFalse(
-            Notification.objects.filter(user=manager,
-                notification_type=Notification.Types.ADJUSTMENT_REQUEST, object_id=adjustment_request.id
+            Notification.objects.filter(
+                user=reviewer, notification_type=Notification.Types.ADJUSTMENT_REQUEST, object_id=adjustment_request.id
             ).exists()
         )
 
@@ -163,10 +159,14 @@ def test_reply(self):
         login_as_user(self.client)
         data = {"reply_content": "this is a reply"}
         response = self.client.post(reverse("adjustment_request_reply", args=[adjustment_request.id]), data=data)
-        self.assertContains(response, "Only the creator and managers can reply to adjustment requests", status_code=400)
+        self.assertContains(
+            response, "Only the creator and reviewers can reply to adjustment requests", status_code=400
+        )
         staff = login_as_staff(self.client)
         response = self.client.post(reverse("adjustment_request_reply", args=[adjustment_request.id]), data=data)
-        self.assertContains(response, "Only the creator and managers can reply to adjustment requests", status_code=400)
+        self.assertContains(
+            response, "Only the creator and reviewers can reply to adjustment requests", status_code=400
+        )
         staff.is_facility_manager = True
         staff.save()
         response = self.client.post(reverse("adjustment_request_reply", args=[adjustment_request.id]), data=data)
diff --git a/NEMO/tests/test_tool.py b/NEMO/tests/test_tool.py
index a67eb9e5..f98532fa 100644
--- a/NEMO/tests/test_tool.py
+++ b/NEMO/tests/test_tool.py
@@ -65,44 +65,44 @@ def setUp(self):
 		alternate_tool = alternate_tool_form.save()
 
 	def test_tool_and_parent_properties(self):
-		self.assertNotEquals(tool.id, alternate_tool.id)
-		self.assertNotEquals(tool.name, alternate_tool.name)
+		self.assertNotEqual(tool.id, alternate_tool.id)
+		self.assertNotEqual(tool.name, alternate_tool.name)
 		self.assertFalse(alternate_tool.visible)
 		self.assertTrue(tool.visible)
 		self.assertTrue(tool.is_parent_tool())
 		self.assertTrue(alternate_tool.is_child_tool())
-		self.assertEquals(tool.category, alternate_tool.category)
-		self.assertEquals(tool.operational, alternate_tool.operational)
-		self.assertEquals(tool.primary_owner, alternate_tool.primary_owner)
-		self.assertEquals(tool.backup_owners, alternate_tool.backup_owners)
-		self.assertEquals(tool.location, alternate_tool.location)
-		self.assertEquals(tool.phone_number, alternate_tool.phone_number)
-		self.assertEquals(tool.notification_email_address, alternate_tool.notification_email_address)
-		self.assertEquals(tool.interlock, alternate_tool.interlock)
-		self.assertEquals(tool.requires_area_access, alternate_tool.requires_area_access)
-		self.assertEquals(
+		self.assertEqual(tool.category, alternate_tool.category)
+		self.assertEqual(tool.operational, alternate_tool.operational)
+		self.assertEqual(tool.primary_owner, alternate_tool.primary_owner)
+		self.assertEqual(tool.backup_owners, alternate_tool.backup_owners)
+		self.assertEqual(tool.location, alternate_tool.location)
+		self.assertEqual(tool.phone_number, alternate_tool.phone_number)
+		self.assertEqual(tool.notification_email_address, alternate_tool.notification_email_address)
+		self.assertEqual(tool.interlock, alternate_tool.interlock)
+		self.assertEqual(tool.requires_area_access, alternate_tool.requires_area_access)
+		self.assertEqual(
 			tool.grant_physical_access_level_upon_qualification,
 			alternate_tool.grant_physical_access_level_upon_qualification,
 		)
-		self.assertEquals(
+		self.assertEqual(
 			tool.grant_badge_reader_access_upon_qualification,
 			alternate_tool.grant_badge_reader_access_upon_qualification,
 		)
-		self.assertEquals(tool.reservation_horizon, alternate_tool.reservation_horizon)
-		self.assertEquals(tool.minimum_usage_block_time, alternate_tool.minimum_usage_block_time)
-		self.assertEquals(tool.maximum_usage_block_time, alternate_tool.maximum_usage_block_time)
-		self.assertEquals(tool.maximum_reservations_per_day, alternate_tool.maximum_reservations_per_day)
-		self.assertEquals(tool.minimum_time_between_reservations, alternate_tool.minimum_time_between_reservations)
-		self.assertEquals(tool.maximum_future_reservation_time, alternate_tool.maximum_future_reservation_time)
-		self.assertEquals(tool.missed_reservation_threshold, alternate_tool.missed_reservation_threshold)
-		self.assertEquals(tool.allow_delayed_logoff, alternate_tool.allow_delayed_logoff)
-		self.assertEquals(tool.post_usage_questions, alternate_tool.post_usage_questions)
-		self.assertEquals(tool.policy_off_between_times, alternate_tool.policy_off_between_times)
-		self.assertEquals(tool.policy_off_start_time, alternate_tool.policy_off_start_time)
-		self.assertEquals(tool.policy_off_end_time, alternate_tool.policy_off_end_time)
-		self.assertEquals(tool.policy_off_weekend, alternate_tool.policy_off_weekend)
+		self.assertEqual(tool.reservation_horizon, alternate_tool.reservation_horizon)
+		self.assertEqual(tool.minimum_usage_block_time, alternate_tool.minimum_usage_block_time)
+		self.assertEqual(tool.maximum_usage_block_time, alternate_tool.maximum_usage_block_time)
+		self.assertEqual(tool.maximum_reservations_per_day, alternate_tool.maximum_reservations_per_day)
+		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.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)
+		self.assertEqual(tool.policy_off_end_time, alternate_tool.policy_off_end_time)
+		self.assertEqual(tool.policy_off_weekend, alternate_tool.policy_off_weekend)
 
-		self.assertEquals(tool.get_absolute_url(), alternate_tool.get_absolute_url())
+		self.assertEqual(tool.get_absolute_url(), alternate_tool.get_absolute_url())
 
 	def test_tool_in_use(self):
 		user = login_as_user(self.client)
@@ -124,7 +124,7 @@ def test_tool_in_use(self):
 		response = self.client.post(
 			reverse("login_to_area", kwargs={"door_id": area_door.id}), {"badge_number": user.badge_number}, follow=True
 		)
-		self.assertEquals(response.status_code, 200, response.content.decode())
+		self.assertEqual(response.status_code, 200, response.content.decode())
 		login_as(self.client, user)
 		# start using tool
 		response = self.client.post(
@@ -134,9 +134,9 @@ def test_tool_in_use(self):
 			),
 			follow=True,
 		)
-		self.assertEquals(response.status_code, 200, response.content.decode())
+		self.assertEqual(response.status_code, 200, response.content.decode())
 		# make sure both tool and child tool are "in use"
 		self.assertTrue(tool.in_use())
 		self.assertTrue(alternate_tool.in_use())
 		# make sure both return the same usage event
-		self.assertEquals(tool.get_current_usage_event(), alternate_tool.get_current_usage_event())
+		self.assertEqual(tool.get_current_usage_event(), alternate_tool.get_current_usage_event())
diff --git a/NEMO/tests/test_urls.py b/NEMO/tests/test_urls.py
index 48c27eb7..4d03c0ac 100644
--- a/NEMO/tests/test_urls.py
+++ b/NEMO/tests/test_urls.py
@@ -102,6 +102,7 @@
 	"create_adjustment_request": {"kwargs": {}},
 	"edit_adjustment_request": {"kwargs": {"request_id": 1}},
 	"change_reservation_date": {"login_id": 1, "post": {"id": 2, "new_start": (start + datetime.timedelta(hours=12)).replace(minute=15).strftime(settings.DATETIME_INPUT_FORMATS[0]), "new_end": end_one_day.replace(minute=30).strftime(settings.DATETIME_INPUT_FORMATS[0]),}},
+	"compose_email": {"login_id": 1},
 }
 
 urls_to_skip = [