+{% endblock %}
\ No newline at end of file
diff --git a/NEMO/apps/sensors/urls.py b/NEMO/apps/sensors/urls.py
new file mode 100644
index 00000000..c9764770
--- /dev/null
+++ b/NEMO/apps/sensors/urls.py
@@ -0,0 +1,14 @@
+from django.urls import path, re_path
+
+from NEMO.apps.sensors import views
+
+urlpatterns = [
+ path("sensors/", views.sensors, name="sensors"),
+ path("sensors//", views.sensors, name="sensors"),
+ path("sensor_details//", views.sensor_details, name="sensor_details"),
+ re_path(r"sensor_details/(?P\d+)/(?Pchart|data|alert)/$", views.sensor_details,name="sensor_details"),
+ path("sensor_chart_data//", views.sensor_chart_data, name="sensor_chart_data"),
+ path("sensor_alert_log//", views.sensor_alert_log, name="sensor_alert_log"),
+ path("export_sensor_data//", views.export_sensor_data, name="export_sensor_data"),
+ path("manage_sensor_data/", views.manage_sensor_data, name="manage_sensor_data"),
+]
diff --git a/NEMO/apps/sensors/views.py b/NEMO/apps/sensors/views.py
new file mode 100644
index 00000000..059b9ff1
--- /dev/null
+++ b/NEMO/apps/sensors/views.py
@@ -0,0 +1,138 @@
+from datetime import datetime, timedelta
+from math import floor
+
+from django.contrib.auth.decorators import login_required, permission_required
+from django.db.models import QuerySet
+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.utilities import (
+ BasicDisplayTable,
+ beginning_of_the_day,
+ export_format_datetime,
+ extract_times,
+ format_datetime,
+)
+
+
+@staff_member_required
+@require_GET
+def sensors(request, category_id=None):
+ selected_category = None
+ if category_id:
+ selected_category = get_object_or_404(SensorCategory, pk=category_id)
+ categories = SensorCategory.objects.filter(parent=category_id)
+ sensor_list = Sensor.objects.filter(visible=True, sensor_category_id=category_id).order_by("name")
+ return render(
+ request,
+ "sensors/sensors.html",
+ {"selected_category": selected_category, "categories": categories, "sensors": sensor_list},
+ )
+
+
+@staff_member_required
+@require_GET
+def sensor_details(request, sensor_id, tab: str = None):
+ sensor = get_object_or_404(Sensor, pk=sensor_id)
+ chart_step = int(request.GET.get("chart_step", 1))
+ default_refresh_rate = int(SensorCustomization.get("sensor_default_refresh_rate"))
+ refresh_rate = int(request.GET.get("refresh_rate", default_refresh_rate))
+ sensor_data, start, end = get_sensor_data(request, sensor)
+ dictionary = {
+ "tab": tab or "chart",
+ "sensor": sensor,
+ "start": start,
+ "end": end,
+ "refresh_rate": refresh_rate,
+ "chart_step": chart_step,
+ }
+ return render(request, "sensors/sensor_data.html", dictionary)
+
+
+@staff_member_required
+@require_GET
+def export_sensor_data(request, sensor_id):
+ sensor = get_object_or_404(Sensor, pk=sensor_id)
+ sensor_data, start, end = get_sensor_data(request, sensor)
+ table_result = BasicDisplayTable()
+ table_result.add_header(("date", "Date"))
+ table_result.add_header(("value", "Value"))
+ table_result.add_header(("display_value", "Display value"))
+ for data_point in sensor_data:
+ table_result.add_row(
+ {
+ "date": format_datetime(data_point.created_date, "SHORT_DATETIME_FORMAT"),
+ "value": data_point.value,
+ "display_value": data_point.display_value(),
+ }
+ )
+ response = table_result.to_csv()
+ sensor_name = slugify(sensor.name).replace("-", "_")
+ filename = f"{sensor_name}_data_{export_format_datetime(start)}_to_{export_format_datetime(end)}.csv"
+ response["Content-Disposition"] = f'attachment; filename="{filename}"'
+ return response
+
+
+@staff_member_required
+@require_GET
+@disable_session_expiry_refresh
+def sensor_chart_data(request, sensor_id):
+ sensor = get_object_or_404(Sensor, pk=sensor_id)
+ labels = []
+ data = []
+ sensor_data = get_sensor_data(request, sensor)[0].order_by("created_date")
+ for data_point in sensor_data:
+ labels.append(format_datetime(data_point.created_date, "m/d/Y H:i:s"))
+ data.append(data_point.value)
+ return JsonResponse(data={"labels": labels, "data": data})
+
+
+@staff_member_required
+@require_GET
+@disable_session_expiry_refresh
+def sensor_alert_log(request, sensor_id):
+ sensor = get_object_or_404(Sensor, pk=sensor_id)
+ sensor_data, start, end = get_sensor_data(request, sensor)
+ alert_log_entries = SensorAlertLog.objects.filter(sensor=sensor, time__gte=start, time__lte=end or timezone.now())
+ return render(request, "sensors/sensor_alerts.html", {"alerts": alert_log_entries})
+
+
+def get_sensor_data(request, sensor) -> (QuerySet, datetime, datetime):
+ start, end = extract_times(request.GET, start_required=False, end_required=False)
+ sensor_data = SensorData.objects.filter(sensor=sensor)
+ now = timezone.now().replace(second=0, microsecond=0).astimezone()
+ sensor_default_daterange = SensorCustomization.get("sensor_default_daterange")
+ if not start:
+ if sensor_default_daterange == "last_year":
+ start = now - timedelta(days=365)
+ elif sensor_default_daterange == "last_month":
+ start = now - timedelta(days=30)
+ elif sensor_default_daterange == "last_week":
+ start = now - timedelta(weeks=1)
+ elif sensor_default_daterange == "last_72hrs":
+ start = now - timedelta(days=3)
+ else:
+ start = now - timedelta(days=1)
+ return sensor_data.filter(created_date__gte=start, created_date__lte=(end or now)), start, end
+
+
+@login_required
+@require_GET
+@permission_required("NEMO.trigger_timed_services", raise_exception=True)
+def manage_sensor_data(request):
+ return do_manage_sensor_data()
+
+
+def do_manage_sensor_data(asynchronous=True):
+ 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()
+ return HttpResponse()
diff --git a/NEMO/context_processors.py b/NEMO/context_processors.py
index 32437ba2..dcde2a52 100644
--- a/NEMO/context_processors.py
+++ b/NEMO/context_processors.py
@@ -1,5 +1,6 @@
from NEMO.models import Area, Notification, PhysicalAccessLevel, Tool, User
-from NEMO.views.customization import get_customization
+from NEMO.utilities import date_input_js_format, datetime_input_js_format, time_input_js_format
+from NEMO.views.customization import ApplicationCustomization
from NEMO.views.notifications import get_notification_counts
@@ -21,11 +22,11 @@ def base_context(request):
except:
request.session["no_header"] = False
try:
- facility_name = get_customization("facility_name")
+ facility_name = ApplicationCustomization.get("facility_name")
except:
facility_name = "Facility"
try:
- site_title = get_customization("site_title")
+ site_title = ApplicationCustomization.get("site_title")
except:
site_title = ""
try:
@@ -73,5 +74,8 @@ def base_context(request):
"buddy_notification_count": buddy_notification_count,
"temporary_access_notification_count": temporary_access_notification_count,
"facility_managers_exist": facility_managers_exist,
+ "time_input_js_format": time_input_js_format,
+ "date_input_js_format": date_input_js_format,
+ "datetime_input_js_format": datetime_input_js_format,
"no_header": request.session.get("no_header", False),
}
diff --git a/NEMO/decorators.py b/NEMO/decorators.py
index 5ea4c493..95de1158 100644
--- a/NEMO/decorators.py
+++ b/NEMO/decorators.py
@@ -21,7 +21,7 @@ def disable_session_expiry_refresh(f):
return f
-# Use this decorator on a function to make a call to said function asynchronous
+# 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
def postpone(function):
def decorator(*arguments, **named_arguments):
@@ -58,6 +58,21 @@ def decorator(*args, **kwargs):
return inner
+# Use this decorator annotation to register your own customizations which will be shown in the customization page
+# The key should be unique and if possible one word, the title will be shown on the customization tab
+def customization(key, title, order=999):
+ from NEMO.views.customization import CustomizationBase
+
+ def customization_wrapper(customization_class):
+ if not issubclass(customization_class, CustomizationBase):
+ raise ValueError("Wrapped class must subclass CustomizationBase.")
+ customization_instance = customization_class(key, title, order)
+ CustomizationBase.add_instance(customization_instance)
+ return customization_instance
+
+ return customization_wrapper
+
+
def staff_member_required(view_func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
"""
Decorator for views that checks that the user is logged in and is a staff member.
diff --git a/NEMO/exceptions.py b/NEMO/exceptions.py
index e93432f3..a636391d 100644
--- a/NEMO/exceptions.py
+++ b/NEMO/exceptions.py
@@ -10,8 +10,8 @@ class NEMOException(Exception):
def __init__(self, msg=None):
if msg is None:
- from NEMO.views.customization import get_customization
- site_title = get_customization('site_title')
+ from NEMO.views.customization import ApplicationCustomization
+ site_title = ApplicationCustomization.get('site_title')
msg = self.default_msg.format(site_title)
self.msg = msg
super().__init__(msg)
diff --git a/NEMO/forms.py b/NEMO/forms.py
index b3b5d161..fb79a972 100644
--- a/NEMO/forms.py
+++ b/NEMO/forms.py
@@ -37,7 +37,7 @@
UserPreferences,
)
from NEMO.utilities import bootstrap_primary_color, format_datetime, quiet_int
-from NEMO.views.customization import get_customization
+from NEMO.views.customization import UserRequestsCustomization
class UserForm(ModelForm):
@@ -386,6 +386,15 @@ class Meta:
"display_new_buddy_request_reply_notification",
"email_new_buddy_request_reply",
"staff_status_view",
+ "email_alternate",
+ "email_send_reservation_emails",
+ "email_send_usage_reminders",
+ "email_send_reservation_reminders",
+ "email_send_reservation_ending_reminders",
+ "email_send_buddy_request_replies",
+ "email_send_access_request_updates",
+ "email_send_task_updates",
+ "email_send_broadcast_emails"
]
@@ -415,7 +424,7 @@ def clean(self):
return
cleaned_data = super().clean()
other_users = len(cleaned_data.get("other_users")) if "other_users" in cleaned_data else 0
- minimum_total_users = quiet_int(get_customization("access_requests_minimum_users", 2))
+ minimum_total_users = quiet_int(UserRequestsCustomization.get("access_requests_minimum_users"), 2)
if other_users < minimum_total_users -1:
self.add_error("other_users", f"You need at least {minimum_total_users-1} other {'buddy' if minimum_total_users == 2 else 'buddies'} for this request")
return cleaned_data
diff --git a/NEMO/interlocks.py b/NEMO/interlocks.py
index 6573713e..02720636 100644
--- a/NEMO/interlocks.py
+++ b/NEMO/interlocks.py
@@ -381,11 +381,11 @@ def setRelayState(cls, interlock: Interlock_model, state: {0, 1}) -> Interlock_m
coil = interlock.channel
client = ModbusTcpClient(interlock.card.server, port=interlock.card.port)
client.connect()
- write_reply = client.write_coil(coil, state, unit=1)
+ write_reply = client.write_coil(coil, state)
if write_reply.isError():
raise Exception(str(write_reply))
sleep(0.3)
- read_reply = client.read_coils(coil, 1, unit=1)
+ read_reply = client.read_coils(coil, 1)
if read_reply.isError():
raise Exception(str(read_reply))
state = read_reply.bits[0]
diff --git a/NEMO/migrations/0038_version_4_0_0.py b/NEMO/migrations/0038_version_4_0_0.py
index 3ffb26bb..33fb2546 100644
--- a/NEMO/migrations/0038_version_4_0_0.py
+++ b/NEMO/migrations/0038_version_4_0_0.py
@@ -14,13 +14,13 @@ class Migration(migrations.Migration):
def new_version_news(apps, schema_editor):
create_news_for_version(apps, "4.0.0", "")
- def add_web_relay_x_series(apps, schema_editor):
+ def add_modbus_tcp_interlock_category(apps, schema_editor):
InterlockCardCategory = apps.get_model("NEMO", "InterlockCardCategory")
InterlockCardCategory.objects.create(name="ModbusTcp", key="modbus_tcp")
operations = [
migrations.RunPython(new_version_news),
- migrations.RunPython(add_web_relay_x_series),
+ migrations.RunPython(add_modbus_tcp_interlock_category),
migrations.AlterField(
model_name='interlock',
name='channel',
diff --git a/NEMO/migrations/0039_version_4_1_0.py b/NEMO/migrations/0039_version_4_1_0.py
new file mode 100644
index 00000000..2bd9e1f9
--- /dev/null
+++ b/NEMO/migrations/0039_version_4_1_0.py
@@ -0,0 +1,79 @@
+# Generated by Django 3.2.12 on 2022-04-07 14:17
+
+from django.db import migrations, models
+
+from NEMO.migrations_utils import create_news_for_version
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('NEMO', '0038_version_4_0_0'),
+ ]
+
+ def new_version_news(apps, schema_editor):
+ create_news_for_version(apps, "4.1.0", "")
+
+ operations = [
+ migrations.RunPython(new_version_news),
+ migrations.AddField(
+ model_name='landingpagechoice',
+ name='hide_from_staff',
+ field=models.BooleanField(default=False, help_text='Hides this choice from staff and technicians. When checked, only normal users, facility managers and super-users can see the choice'),
+ ),
+ migrations.AlterField(
+ model_name='landingpagechoice',
+ name='hide_from_users',
+ field=models.BooleanField(default=False, help_text='Hides this choice from normal users. When checked, only staff, technicians, facility managers and super-users can see the choice'),
+ ),
+ migrations.AddField(
+ model_name='userpreferences',
+ name='email_alternate',
+ field=models.EmailField(blank=True, max_length=254, null=True),
+ ),
+ migrations.AddField(
+ model_name='userpreferences',
+ name='email_send_access_request_updates',
+ field=models.BooleanField(default=True, help_text='Send access request updates to my alternate email'),
+ ),
+ migrations.AddField(
+ model_name='userpreferences',
+ name='email_send_buddy_request_replies',
+ field=models.BooleanField(default=True, help_text='Send buddy request replies to my alternate email'),
+ ),
+ migrations.AddField(
+ model_name='userpreferences',
+ name='email_send_reservation_emails',
+ field=models.BooleanField(default=True, help_text='Send reservation emails to my alternate email'),
+ ),
+ migrations.AddField(
+ model_name='userpreferences',
+ name='email_send_reservation_ending_reminders',
+ field=models.BooleanField(default=True, help_text='Send reservation ending reminders to my alternate email'),
+ ),
+ migrations.AddField(
+ model_name='userpreferences',
+ name='email_send_reservation_reminders',
+ field=models.BooleanField(default=True, help_text='Send reservation reminders to my alternate email'),
+ ),
+ migrations.AddField(
+ model_name='userpreferences',
+ name='email_send_task_updates',
+ field=models.BooleanField(default=True, help_text='Send task updates to my alternate email'),
+ ),
+ migrations.AddField(
+ model_name='userpreferences',
+ name='email_send_usage_reminders',
+ field=models.BooleanField(default=True, help_text='Send usage reminders to my alternate email'),
+ ),
+ migrations.AddField(
+ model_name='userpreferences',
+ name='email_send_broadcast_emails',
+ field=models.BooleanField(default=True, help_text='Send broadcast emails to my alternate email'),
+ ),
+ migrations.AlterField(
+ model_name='emaillog',
+ name='category',
+ field=models.IntegerField(choices=[(0, 'General'), (1, 'System'), (2, 'Direct Contact'), (3, 'Broadcast Email'), (4, 'Timed Services'), (5, 'Feedback'), (6, 'Abuse'), (7, 'Safety'), (8, 'Tasks'), (9, 'Access Requests'), (10, 'Sensors')], default=0),
+ ),
+ ]
diff --git a/NEMO/models.py b/NEMO/models.py
index 1fae9e6e..f7c29f34 100644
--- a/NEMO/models.py
+++ b/NEMO/models.py
@@ -100,6 +100,15 @@ class UserPreferences(models.Model):
display_new_buddy_request_reply_notification = models.BooleanField('new_buddy_request_reply_notification', default=True, help_text='Whether or not to notify the user of replies on buddy request he commented on (via unread badges)')
email_new_buddy_request_reply = models.BooleanField('email_new_buddy_request_reply', default=True, help_text='Whether or not to email the user of replies on buddy request he commented on')
staff_status_view = models.CharField('staff_status_view', max_length=100, default='day', choices=[('day', 'Day'), ('week', 'Week'), ('month', 'Month')], help_text='Preferred view for staff status page')
+ email_alternate = models.EmailField(null=True, blank=True)
+ email_send_reservation_emails = models.BooleanField(default=True, help_text="Send reservation emails to my alternate email")
+ email_send_usage_reminders = models.BooleanField(default=True, help_text="Send usage reminders to my alternate email")
+ email_send_reservation_reminders = models.BooleanField(default=True, help_text="Send reservation reminders to my alternate email")
+ email_send_reservation_ending_reminders = models.BooleanField(default=True, help_text="Send reservation ending reminders to my alternate email")
+ email_send_buddy_request_replies = models.BooleanField(default=True, help_text="Send buddy request replies to my alternate email")
+ email_send_access_request_updates = models.BooleanField(default=True, help_text="Send access request updates to my alternate email")
+ email_send_task_updates = models.BooleanField(default=True, help_text="Send task updates to my alternate email")
+ email_send_broadcast_emails = models.BooleanField(default=True, help_text="Send broadcast emails to my alternate email")
class Meta:
verbose_name = 'User preferences'
@@ -459,9 +468,15 @@ def has_usable_password(self):
def set_unusable_password(self):
pass
- def email_user(self, subject, content, from_email, attachments=None, email_category:EmailCategory = EmailCategory.GENERAL):
+ def get_emails(self, include_alternate=False) -> List[str]:
+ emails = [self.email]
+ if include_alternate and self.get_preferences().email_alternate:
+ emails.append(self.preferences.email_alternate)
+ return emails
+
+ def email_user(self, subject, message, from_email, attachments=None, send_to_alternate=False, email_category:EmailCategory = EmailCategory.GENERAL):
""" Sends an email to this user. """
- send_mail(subject=subject, content=content, from_email=from_email, to=[self.email], attachments=attachments, email_category=email_category)
+ send_mail(subject=subject, content=message, from_email=from_email, to=self.get_emails(send_to_alternate), attachments=attachments, email_category=email_category)
def get_full_name(self):
return self.get_name() + ' (' + self.username + ')'
@@ -2089,7 +2104,8 @@ class LandingPageChoice(models.Model):
secure_referral = models.BooleanField(default=True, help_text="Improves security by blocking HTTP referer [sic] information from the targeted page. Enabling this prevents the target page from manipulating the calling page's DOM with JavaScript. This should always be used for external links. It is safe to uncheck this when linking within the site. Leave this box checked if you don't know what this means")
hide_from_mobile_devices = models.BooleanField(default=False, help_text="Hides this choice when the landing page is viewed from a mobile device")
hide_from_desktop_computers = models.BooleanField(default=False, help_text="Hides this choice when the landing page is viewed from a desktop computer")
- hide_from_users = models.BooleanField(default=False, help_text="Hides this choice from normal users. When checked, only staff, technicians, and super-users can see the choice")
+ hide_from_users = models.BooleanField(default=False, help_text="Hides this choice from normal users. When checked, only staff, technicians, facility managers and super-users can see the choice")
+ hide_from_staff = models.BooleanField(default=False, help_text="Hides this choice from staff and technicians. When checked, only normal users, facility managers and super-users can see the choice")
notifications = models.CharField(max_length=100, blank=True, null=True, choices=Notification.Types.Choices, 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.")
class Meta:
diff --git a/NEMO/permissions.py b/NEMO/permissions.py
index cb99a69b..8629433f 100644
--- a/NEMO/permissions.py
+++ b/NEMO/permissions.py
@@ -1,9 +1,23 @@
from rest_framework import permissions
-
class BillingAPI(permissions.BasePermission):
- """ Checks that a user has permission to use the NEMO RESTful API for billing purposes. """
+ """ Checks that a user has permission to use the NEMO RESTful API for billing purposes.
+ This is a global permission and will give the user access to all API objects."""
+
def has_permission(self, request, view):
if request and request.user.has_perm('NEMO.use_billing_api'):
- return True
+ return True
return False
+
+
+class DjangoModelPermissions(permissions.DjangoModelPermissions):
+ """ Checks that a user has the correct model permission (including view) to use the NEMO RESTful API. """
+ perms_map = {
+ 'GET': ['%(app_label)s.view_%(model_name)s'],
+ 'OPTIONS': [],
+ 'HEAD': [],
+ 'POST': ['%(app_label)s.add_%(model_name)s'],
+ 'PUT': ['%(app_label)s.change_%(model_name)s'],
+ 'PATCH': ['%(app_label)s.change_%(model_name)s'],
+ 'DELETE': ['%(app_label)s.delete_%(model_name)s'],
+ }
diff --git a/NEMO/rates.py b/NEMO/rates.py
index 25ea9e4a..a5c9d709 100644
--- a/NEMO/rates.py
+++ b/NEMO/rates.py
@@ -119,4 +119,5 @@ def get_rate_class():
return ret()
+# ONLY import this LOCALLY to avoid potential issues
rate_class = get_rate_class()
\ No newline at end of file
diff --git a/NEMO/static/admin/questions_preview/questions_preview.css b/NEMO/static/admin/questions_preview/questions_preview.css
index 2d63af29..af9498ac 100644
--- a/NEMO/static/admin/questions_preview/questions_preview.css
+++ b/NEMO/static/admin/questions_preview/questions_preview.css
@@ -179,4 +179,9 @@ div.questions_preview:empty
{
color: whitesmoke;
background-color: #ac2925;
+}
+
+.questions_preview input:invalid, .questions_preview select:invalid, .questions_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/questions_preview/questions_preview.js
index 27e0da64..4ee2be37 100644
--- a/NEMO/static/admin/questions_preview/questions_preview.js
+++ b/NEMO/static/admin/questions_preview/questions_preview.js
@@ -25,7 +25,7 @@ window.addEventListener("load", function()
}
}
}
- $('.questions_preview').on('change keyup', "input[required][form='questions_preview_form'], textarea[required][form='questions_preview_form'], select[required][form='questions_preview_form']", update_validation_button);
+ $('.questions_preview').on('change keyup', "input[form='questions_preview_form'], textarea[form='questions_preview_form'], select[form='questions_preview_form']", update_validation_button);
$('body').on('question-group-changed', function()
{
update_input_form();
diff --git a/NEMO/static/datetimepicker/bootstrap-datepicker.js b/NEMO/static/datetimepicker/bootstrap-datepicker.js
deleted file mode 100644
index a94f79fc..00000000
--- a/NEMO/static/datetimepicker/bootstrap-datepicker.js
+++ /dev/null
@@ -1,2039 +0,0 @@
-/*!
- * Datepicker for Bootstrap v1.9.0 (https://github.com/uxsolutions/bootstrap-datepicker)
- *
- * Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0)
- */
-
-(function(factory){
- if (typeof define === 'function' && define.amd) {
- define(['jquery'], factory);
- } else if (typeof exports === 'object') {
- factory(require('jquery'));
- } else {
- factory(jQuery);
- }
-}(function($, undefined){
- function UTCDate(){
- return new Date(Date.UTC.apply(Date, arguments));
- }
- function UTCToday(){
- var today = new Date();
- return UTCDate(today.getFullYear(), today.getMonth(), today.getDate());
- }
- function isUTCEquals(date1, date2) {
- return (
- date1.getUTCFullYear() === date2.getUTCFullYear() &&
- date1.getUTCMonth() === date2.getUTCMonth() &&
- date1.getUTCDate() === date2.getUTCDate()
- );
- }
- function alias(method, deprecationMsg){
- return function(){
- if (deprecationMsg !== undefined) {
- $.fn.datepicker.deprecated(deprecationMsg);
- }
-
- return this[method].apply(this, arguments);
- };
- }
- function isValidDate(d) {
- return d && !isNaN(d.getTime());
- }
-
- var DateArray = (function(){
- var extras = {
- get: function(i){
- return this.slice(i)[0];
- },
- contains: function(d){
- // Array.indexOf is not cross-browser;
- // $.inArray doesn't work with Dates
- var val = d && d.valueOf();
- for (var i=0, l=this.length; i < l; i++)
- // Use date arithmetic to allow dates with different times to match
- if (0 <= this[i].valueOf() - val && this[i].valueOf() - val < 1000*60*60*24)
- return i;
- return -1;
- },
- remove: function(i){
- this.splice(i,1);
- },
- replace: function(new_array){
- if (!new_array)
- return;
- if (!$.isArray(new_array))
- new_array = [new_array];
- this.clear();
- this.push.apply(this, new_array);
- },
- clear: function(){
- this.length = 0;
- },
- copy: function(){
- var a = new DateArray();
- a.replace(this);
- return a;
- }
- };
-
- return function(){
- var a = [];
- a.push.apply(a, arguments);
- $.extend(a, extras);
- return a;
- };
- })();
-
-
- // Picker object
-
- var Datepicker = function(element, options){
- $.data(element, 'datepicker', this);
-
- this._events = [];
- this._secondaryEvents = [];
-
- this._process_options(options);
-
- this.dates = new DateArray();
- this.viewDate = this.o.defaultViewDate;
- this.focusDate = null;
-
- this.element = $(element);
- this.isInput = this.element.is('input');
- this.inputField = this.isInput ? this.element : this.element.find('input');
- this.component = this.element.hasClass('date') ? this.element.find('.add-on, .input-group-addon, .input-group-append, .input-group-prepend, .btn') : false;
- if (this.component && this.component.length === 0)
- this.component = false;
- this.isInline = !this.component && this.element.is('div');
-
- this.picker = $(DPGlobal.template);
-
- // Checking templates and inserting
- if (this._check_template(this.o.templates.leftArrow)) {
- this.picker.find('.prev').html(this.o.templates.leftArrow);
- }
-
- if (this._check_template(this.o.templates.rightArrow)) {
- this.picker.find('.next').html(this.o.templates.rightArrow);
- }
-
- this._buildEvents();
- this._attachEvents();
-
- if (this.isInline){
- this.picker.addClass('datepicker-inline').appendTo(this.element);
- }
- else {
- this.picker.addClass('datepicker-dropdown dropdown-menu');
- }
-
- if (this.o.rtl){
- this.picker.addClass('datepicker-rtl');
- }
-
- if (this.o.calendarWeeks) {
- this.picker.find('.datepicker-days .datepicker-switch, thead .datepicker-title, tfoot .today, tfoot .clear')
- .attr('colspan', function(i, val){
- return Number(val) + 1;
- });
- }
-
- this._process_options({
- startDate: this._o.startDate,
- endDate: this._o.endDate,
- daysOfWeekDisabled: this.o.daysOfWeekDisabled,
- daysOfWeekHighlighted: this.o.daysOfWeekHighlighted,
- datesDisabled: this.o.datesDisabled
- });
-
- this._allow_update = false;
- this.setViewMode(this.o.startView);
- this._allow_update = true;
-
- this.fillDow();
- this.fillMonths();
-
- this.update();
-
- if (this.isInline){
- this.show();
- }
- };
-
- Datepicker.prototype = {
- constructor: Datepicker,
-
- _resolveViewName: function(view){
- $.each(DPGlobal.viewModes, function(i, viewMode){
- if (view === i || $.inArray(view, viewMode.names) !== -1){
- view = i;
- return false;
- }
- });
-
- return view;
- },
-
- _resolveDaysOfWeek: function(daysOfWeek){
- if (!$.isArray(daysOfWeek))
- daysOfWeek = daysOfWeek.split(/[,\s]*/);
- return $.map(daysOfWeek, Number);
- },
-
- _check_template: function(tmp){
- try {
- // If empty
- if (tmp === undefined || tmp === "") {
- return false;
- }
- // If no html, everything ok
- if ((tmp.match(/[<>]/g) || []).length <= 0) {
- return true;
- }
- // Checking if html is fine
- var jDom = $(tmp);
- return jDom.length > 0;
- }
- catch (ex) {
- return false;
- }
- },
-
- _process_options: function(opts){
- // Store raw options for reference
- this._o = $.extend({}, this._o, opts);
- // Processed options
- var o = this.o = $.extend({}, this._o);
-
- // Check if "de-DE" style date is available, if not language should
- // fallback to 2 letter code eg "de"
- var lang = o.language;
- if (!dates[lang]){
- lang = lang.split('-')[0];
- if (!dates[lang])
- lang = defaults.language;
- }
- o.language = lang;
-
- // Retrieve view index from any aliases
- o.startView = this._resolveViewName(o.startView);
- o.minViewMode = this._resolveViewName(o.minViewMode);
- o.maxViewMode = this._resolveViewName(o.maxViewMode);
-
- // Check view is between min and max
- o.startView = Math.max(this.o.minViewMode, Math.min(this.o.maxViewMode, o.startView));
-
- // true, false, or Number > 0
- if (o.multidate !== true){
- o.multidate = Number(o.multidate) || false;
- if (o.multidate !== false)
- o.multidate = Math.max(0, o.multidate);
- }
- o.multidateSeparator = String(o.multidateSeparator);
-
- o.weekStart %= 7;
- o.weekEnd = (o.weekStart + 6) % 7;
-
- var format = DPGlobal.parseFormat(o.format);
- if (o.startDate !== -Infinity){
- if (!!o.startDate){
- if (o.startDate instanceof Date)
- o.startDate = this._local_to_utc(this._zero_time(o.startDate));
- else
- o.startDate = DPGlobal.parseDate(o.startDate, format, o.language, o.assumeNearbyYear);
- }
- else {
- o.startDate = -Infinity;
- }
- }
- if (o.endDate !== Infinity){
- if (!!o.endDate){
- if (o.endDate instanceof Date)
- o.endDate = this._local_to_utc(this._zero_time(o.endDate));
- else
- o.endDate = DPGlobal.parseDate(o.endDate, format, o.language, o.assumeNearbyYear);
- }
- else {
- o.endDate = Infinity;
- }
- }
-
- o.daysOfWeekDisabled = this._resolveDaysOfWeek(o.daysOfWeekDisabled||[]);
- o.daysOfWeekHighlighted = this._resolveDaysOfWeek(o.daysOfWeekHighlighted||[]);
-
- o.datesDisabled = o.datesDisabled||[];
- if (!$.isArray(o.datesDisabled)) {
- o.datesDisabled = o.datesDisabled.split(',');
- }
- o.datesDisabled = $.map(o.datesDisabled, function(d){
- return DPGlobal.parseDate(d, format, o.language, o.assumeNearbyYear);
- });
-
- var plc = String(o.orientation).toLowerCase().split(/\s+/g),
- _plc = o.orientation.toLowerCase();
- plc = $.grep(plc, function(word){
- return /^auto|left|right|top|bottom$/.test(word);
- });
- o.orientation = {x: 'auto', y: 'auto'};
- if (!_plc || _plc === 'auto')
- ; // no action
- else if (plc.length === 1){
- switch (plc[0]){
- case 'top':
- case 'bottom':
- o.orientation.y = plc[0];
- break;
- case 'left':
- case 'right':
- o.orientation.x = plc[0];
- break;
- }
- }
- else {
- _plc = $.grep(plc, function(word){
- return /^left|right$/.test(word);
- });
- o.orientation.x = _plc[0] || 'auto';
-
- _plc = $.grep(plc, function(word){
- return /^top|bottom$/.test(word);
- });
- o.orientation.y = _plc[0] || 'auto';
- }
- if (o.defaultViewDate instanceof Date || typeof o.defaultViewDate === 'string') {
- o.defaultViewDate = DPGlobal.parseDate(o.defaultViewDate, format, o.language, o.assumeNearbyYear);
- } else if (o.defaultViewDate) {
- var year = o.defaultViewDate.year || new Date().getFullYear();
- var month = o.defaultViewDate.month || 0;
- var day = o.defaultViewDate.day || 1;
- o.defaultViewDate = UTCDate(year, month, day);
- } else {
- o.defaultViewDate = UTCToday();
- }
- },
- _applyEvents: function(evs){
- for (var i=0, el, ch, ev; i < evs.length; i++){
- el = evs[i][0];
- if (evs[i].length === 2){
- ch = undefined;
- ev = evs[i][1];
- } else if (evs[i].length === 3){
- ch = evs[i][1];
- ev = evs[i][2];
- }
- el.on(ev, ch);
- }
- },
- _unapplyEvents: function(evs){
- for (var i=0, el, ev, ch; i < evs.length; i++){
- el = evs[i][0];
- if (evs[i].length === 2){
- ch = undefined;
- ev = evs[i][1];
- } else if (evs[i].length === 3){
- ch = evs[i][1];
- ev = evs[i][2];
- }
- el.off(ev, ch);
- }
- },
- _buildEvents: function(){
- var events = {
- keyup: $.proxy(function(e){
- if ($.inArray(e.keyCode, [27, 37, 39, 38, 40, 32, 13, 9]) === -1)
- this.update();
- }, this),
- keydown: $.proxy(this.keydown, this),
- paste: $.proxy(this.paste, this)
- };
-
- if (this.o.showOnFocus === true) {
- events.focus = $.proxy(this.show, this);
- }
-
- if (this.isInput) { // single input
- this._events = [
- [this.element, events]
- ];
- }
- // component: input + button
- else if (this.component && this.inputField.length) {
- this._events = [
- // For components that are not readonly, allow keyboard nav
- [this.inputField, events],
- [this.component, {
- click: $.proxy(this.show, this)
- }]
- ];
- }
- else {
- this._events = [
- [this.element, {
- click: $.proxy(this.show, this),
- keydown: $.proxy(this.keydown, this)
- }]
- ];
- }
- this._events.push(
- // Component: listen for blur on element descendants
- [this.element, '*', {
- blur: $.proxy(function(e){
- this._focused_from = e.target;
- }, this)
- }],
- // Input: listen for blur on element
- [this.element, {
- blur: $.proxy(function(e){
- this._focused_from = e.target;
- }, this)
- }]
- );
-
- if (this.o.immediateUpdates) {
- // Trigger input updates immediately on changed year/month
- this._events.push([this.element, {
- 'changeYear changeMonth': $.proxy(function(e){
- this.update(e.date);
- }, this)
- }]);
- }
-
- this._secondaryEvents = [
- [this.picker, {
- click: $.proxy(this.click, this)
- }],
- [this.picker, '.prev, .next', {
- click: $.proxy(this.navArrowsClick, this)
- }],
- [this.picker, '.day:not(.disabled)', {
- click: $.proxy(this.dayCellClick, this)
- }],
- [$(window), {
- resize: $.proxy(this.place, this)
- }],
- [$(document), {
- 'mousedown touchstart': $.proxy(function(e){
- // Clicked outside the datepicker, hide it
- if (!(
- this.element.is(e.target) ||
- this.element.find(e.target).length ||
- this.picker.is(e.target) ||
- this.picker.find(e.target).length ||
- this.isInline
- )){
- this.hide();
- }
- }, this)
- }]
- ];
- },
- _attachEvents: function(){
- this._detachEvents();
- this._applyEvents(this._events);
- },
- _detachEvents: function(){
- this._unapplyEvents(this._events);
- },
- _attachSecondaryEvents: function(){
- this._detachSecondaryEvents();
- this._applyEvents(this._secondaryEvents);
- },
- _detachSecondaryEvents: function(){
- this._unapplyEvents(this._secondaryEvents);
- },
- _trigger: function(event, altdate){
- var date = altdate || this.dates.get(-1),
- local_date = this._utc_to_local(date);
-
- this.element.trigger({
- type: event,
- date: local_date,
- viewMode: this.viewMode,
- dates: $.map(this.dates, this._utc_to_local),
- format: $.proxy(function(ix, format){
- if (arguments.length === 0){
- ix = this.dates.length - 1;
- format = this.o.format;
- } else if (typeof ix === 'string'){
- format = ix;
- ix = this.dates.length - 1;
- }
- format = format || this.o.format;
- var date = this.dates.get(ix);
- return DPGlobal.formatDate(date, format, this.o.language);
- }, this)
- });
- },
-
- show: function(){
- if (this.inputField.is(':disabled') || (this.inputField.prop('readonly') && this.o.enableOnReadonly === false))
- return;
- if (!this.isInline)
- this.picker.appendTo(this.o.container);
- this.place();
- this.picker.show();
- this._attachSecondaryEvents();
- this._trigger('show');
- if ((window.navigator.msMaxTouchPoints || 'ontouchstart' in document) && this.o.disableTouchKeyboard) {
- $(this.element).blur();
- }
- return this;
- },
-
- hide: function(){
- if (this.isInline || !this.picker.is(':visible'))
- return this;
- this.focusDate = null;
- this.picker.hide().detach();
- this._detachSecondaryEvents();
- this.setViewMode(this.o.startView);
-
- if (this.o.forceParse && this.inputField.val())
- this.setValue();
- this._trigger('hide');
- return this;
- },
-
- destroy: function(){
- this.hide();
- this._detachEvents();
- this._detachSecondaryEvents();
- this.picker.remove();
- delete this.element.data().datepicker;
- if (!this.isInput){
- delete this.element.data().date;
- }
- return this;
- },
-
- paste: function(e){
- var dateString;
- if (e.originalEvent.clipboardData && e.originalEvent.clipboardData.types
- && $.inArray('text/plain', e.originalEvent.clipboardData.types) !== -1) {
- dateString = e.originalEvent.clipboardData.getData('text/plain');
- } else if (window.clipboardData) {
- dateString = window.clipboardData.getData('Text');
- } else {
- return;
- }
- this.setDate(dateString);
- this.update();
- e.preventDefault();
- },
-
- _utc_to_local: function(utc){
- if (!utc) {
- return utc;
- }
-
- var local = new Date(utc.getTime() + (utc.getTimezoneOffset() * 60000));
-
- if (local.getTimezoneOffset() !== utc.getTimezoneOffset()) {
- local = new Date(utc.getTime() + (local.getTimezoneOffset() * 60000));
- }
-
- return local;
- },
- _local_to_utc: function(local){
- return local && new Date(local.getTime() - (local.getTimezoneOffset()*60000));
- },
- _zero_time: function(local){
- return local && new Date(local.getFullYear(), local.getMonth(), local.getDate());
- },
- _zero_utc_time: function(utc){
- return utc && UTCDate(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate());
- },
-
- getDates: function(){
- return $.map(this.dates, this._utc_to_local);
- },
-
- getUTCDates: function(){
- return $.map(this.dates, function(d){
- return new Date(d);
- });
- },
-
- getDate: function(){
- return this._utc_to_local(this.getUTCDate());
- },
-
- getUTCDate: function(){
- var selected_date = this.dates.get(-1);
- if (selected_date !== undefined) {
- return new Date(selected_date);
- } else {
- return null;
- }
- },
-
- clearDates: function(){
- this.inputField.val('');
- this.update();
- this._trigger('changeDate');
-
- if (this.o.autoclose) {
- this.hide();
- }
- },
-
- setDates: function(){
- var args = $.isArray(arguments[0]) ? arguments[0] : arguments;
- this.update.apply(this, args);
- this._trigger('changeDate');
- this.setValue();
- return this;
- },
-
- setUTCDates: function(){
- var args = $.isArray(arguments[0]) ? arguments[0] : arguments;
- this.setDates.apply(this, $.map(args, this._utc_to_local));
- return this;
- },
-
- setDate: alias('setDates'),
- setUTCDate: alias('setUTCDates'),
- remove: alias('destroy', 'Method `remove` is deprecated and will be removed in version 2.0. Use `destroy` instead'),
-
- setValue: function(){
- var formatted = this.getFormattedDate();
- this.inputField.val(formatted);
- return this;
- },
-
- getFormattedDate: function(format){
- if (format === undefined)
- format = this.o.format;
-
- var lang = this.o.language;
- return $.map(this.dates, function(d){
- return DPGlobal.formatDate(d, format, lang);
- }).join(this.o.multidateSeparator);
- },
-
- getStartDate: function(){
- return this.o.startDate;
- },
-
- setStartDate: function(startDate){
- this._process_options({startDate: startDate});
- this.update();
- this.updateNavArrows();
- return this;
- },
-
- getEndDate: function(){
- return this.o.endDate;
- },
-
- setEndDate: function(endDate){
- this._process_options({endDate: endDate});
- this.update();
- this.updateNavArrows();
- return this;
- },
-
- setDaysOfWeekDisabled: function(daysOfWeekDisabled){
- this._process_options({daysOfWeekDisabled: daysOfWeekDisabled});
- this.update();
- return this;
- },
-
- setDaysOfWeekHighlighted: function(daysOfWeekHighlighted){
- this._process_options({daysOfWeekHighlighted: daysOfWeekHighlighted});
- this.update();
- return this;
- },
-
- setDatesDisabled: function(datesDisabled){
- this._process_options({datesDisabled: datesDisabled});
- this.update();
- return this;
- },
-
- place: function(){
- if (this.isInline)
- return this;
- var calendarWidth = this.picker.outerWidth(),
- calendarHeight = this.picker.outerHeight(),
- visualPadding = 10,
- container = $(this.o.container),
- windowWidth = container.width(),
- scrollTop = this.o.container === 'body' ? $(document).scrollTop() : container.scrollTop(),
- appendOffset = container.offset();
-
- var parentsZindex = [0];
- this.element.parents().each(function(){
- var itemZIndex = $(this).css('z-index');
- if (itemZIndex !== 'auto' && Number(itemZIndex) !== 0) parentsZindex.push(Number(itemZIndex));
- });
- var zIndex = Math.max.apply(Math, parentsZindex) + this.o.zIndexOffset;
- var offset = this.component ? this.component.parent().offset() : this.element.offset();
- var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(false);
- var width = this.component ? this.component.outerWidth(true) : this.element.outerWidth(false);
- var left = offset.left - appendOffset.left;
- var top = offset.top - appendOffset.top;
-
- if (this.o.container !== 'body') {
- top += scrollTop;
- }
-
- this.picker.removeClass(
- 'datepicker-orient-top datepicker-orient-bottom '+
- 'datepicker-orient-right datepicker-orient-left'
- );
-
- if (this.o.orientation.x !== 'auto'){
- this.picker.addClass('datepicker-orient-' + this.o.orientation.x);
- if (this.o.orientation.x === 'right')
- left -= calendarWidth - width;
- }
- // auto x orientation is best-placement: if it crosses a window
- // edge, fudge it sideways
- else {
- if (offset.left < 0) {
- // component is outside the window on the left side. Move it into visible range
- this.picker.addClass('datepicker-orient-left');
- left -= offset.left - visualPadding;
- } else if (left + calendarWidth > windowWidth) {
- // the calendar passes the widow right edge. Align it to component right side
- this.picker.addClass('datepicker-orient-right');
- left += width - calendarWidth;
- } else {
- if (this.o.rtl) {
- // Default to right
- this.picker.addClass('datepicker-orient-right');
- } else {
- // Default to left
- this.picker.addClass('datepicker-orient-left');
- }
- }
- }
-
- // auto y orientation is best-situation: top or bottom, no fudging,
- // decision based on which shows more of the calendar
- var yorient = this.o.orientation.y,
- top_overflow;
- if (yorient === 'auto'){
- top_overflow = -scrollTop + top - calendarHeight;
- yorient = top_overflow < 0 ? 'bottom' : 'top';
- }
-
- this.picker.addClass('datepicker-orient-' + yorient);
- if (yorient === 'top')
- top -= calendarHeight + parseInt(this.picker.css('padding-top'));
- else
- top += height;
-
- if (this.o.rtl) {
- var right = windowWidth - (left + width);
- this.picker.css({
- top: top,
- right: right,
- zIndex: zIndex
- });
- } else {
- this.picker.css({
- top: top,
- left: left,
- zIndex: zIndex
- });
- }
- return this;
- },
-
- _allow_update: true,
- update: function(){
- if (!this._allow_update)
- return this;
-
- var oldDates = this.dates.copy(),
- dates = [],
- fromArgs = false;
- if (arguments.length){
- $.each(arguments, $.proxy(function(i, date){
- if (date instanceof Date)
- date = this._local_to_utc(date);
- dates.push(date);
- }, this));
- fromArgs = true;
- } else {
- dates = this.isInput
- ? this.element.val()
- : this.element.data('date') || this.inputField.val();
- if (dates && this.o.multidate)
- dates = dates.split(this.o.multidateSeparator);
- else
- dates = [dates];
- delete this.element.data().date;
- }
-
- dates = $.map(dates, $.proxy(function(date){
- return DPGlobal.parseDate(date, this.o.format, this.o.language, this.o.assumeNearbyYear);
- }, this));
- dates = $.grep(dates, $.proxy(function(date){
- return (
- !this.dateWithinRange(date) ||
- !date
- );
- }, this), true);
- this.dates.replace(dates);
-
- if (this.o.updateViewDate) {
- if (this.dates.length)
- this.viewDate = new Date(this.dates.get(-1));
- else if (this.viewDate < this.o.startDate)
- this.viewDate = new Date(this.o.startDate);
- else if (this.viewDate > this.o.endDate)
- this.viewDate = new Date(this.o.endDate);
- else
- this.viewDate = this.o.defaultViewDate;
- }
-
- if (fromArgs){
- // setting date by clicking
- this.setValue();
- this.element.change();
- }
- else if (this.dates.length){
- // setting date by typing
- if (String(oldDates) !== String(this.dates) && fromArgs) {
- this._trigger('changeDate');
- this.element.change();
- }
- }
- if (!this.dates.length && oldDates.length) {
- this._trigger('clearDate');
- this.element.change();
- }
-
- this.fill();
- return this;
- },
-
- fillDow: function(){
- if (this.o.showWeekDays) {
- var dowCnt = this.o.weekStart,
- html = '
';
- if (this.o.calendarWeeks){
- html += '
';
- }
- while (dowCnt < this.o.weekStart + 7){
- html += '
'+dates[this.o.language].daysMin[(dowCnt++)%7]+'
';
- }
- html += '
';
- this.picker.find('.datepicker-days thead').append(html);
- }
- },
-
- fillMonths: function(){
- var localDate = this._utc_to_local(this.viewDate);
- var html = '';
- var focused;
- for (var i = 0; i < 12; i++){
- focused = localDate && localDate.getMonth() === i ? ' focused' : '';
- html += '' + dates[this.o.language].monthsShort[i] + '';
- }
- this.picker.find('.datepicker-months td').html(html);
- },
-
- setRange: function(range){
- if (!range || !range.length)
- delete this.range;
- else
- this.range = $.map(range, function(d){
- return d.valueOf();
- });
- this.fill();
- },
-
- getClassNames: function(date){
- var cls = [],
- year = this.viewDate.getUTCFullYear(),
- month = this.viewDate.getUTCMonth(),
- today = UTCToday();
- if (date.getUTCFullYear() < year || (date.getUTCFullYear() === year && date.getUTCMonth() < month)){
- cls.push('old');
- } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() === year && date.getUTCMonth() > month)){
- cls.push('new');
- }
- if (this.focusDate && date.valueOf() === this.focusDate.valueOf())
- cls.push('focused');
- // Compare internal UTC date with UTC today, not local today
- if (this.o.todayHighlight && isUTCEquals(date, today)) {
- cls.push('today');
- }
- if (this.dates.contains(date) !== -1)
- cls.push('active');
- if (!this.dateWithinRange(date)){
- cls.push('disabled');
- }
- if (this.dateIsDisabled(date)){
- cls.push('disabled', 'disabled-date');
- }
- if ($.inArray(date.getUTCDay(), this.o.daysOfWeekHighlighted) !== -1){
- cls.push('highlighted');
- }
-
- if (this.range){
- if (date > this.range[0] && date < this.range[this.range.length-1]){
- cls.push('range');
- }
- if ($.inArray(date.valueOf(), this.range) !== -1){
- cls.push('selected');
- }
- if (date.valueOf() === this.range[0]){
- cls.push('range-start');
- }
- if (date.valueOf() === this.range[this.range.length-1]){
- cls.push('range-end');
- }
- }
- return cls;
- },
-
- _fill_yearsView: function(selector, cssClass, factor, year, startYear, endYear, beforeFn){
- var html = '';
- var step = factor / 10;
- var view = this.picker.find(selector);
- var startVal = Math.floor(year / factor) * factor;
- var endVal = startVal + step * 9;
- var focusedVal = Math.floor(this.viewDate.getFullYear() / step) * step;
- var selected = $.map(this.dates, function(d){
- return Math.floor(d.getUTCFullYear() / step) * step;
- });
-
- var classes, tooltip, before;
- for (var currVal = startVal - step; currVal <= endVal + step; currVal += step) {
- classes = [cssClass];
- tooltip = null;
-
- if (currVal === startVal - step) {
- classes.push('old');
- } else if (currVal === endVal + step) {
- classes.push('new');
- }
- if ($.inArray(currVal, selected) !== -1) {
- classes.push('active');
- }
- if (currVal < startYear || currVal > endYear) {
- classes.push('disabled');
- }
- if (currVal === focusedVal) {
- classes.push('focused');
- }
-
- if (beforeFn !== $.noop) {
- before = beforeFn(new Date(currVal, 0, 1));
- if (before === undefined) {
- before = {};
- } else if (typeof before === 'boolean') {
- before = {enabled: before};
- } else if (typeof before === 'string') {
- before = {classes: before};
- }
- if (before.enabled === false) {
- classes.push('disabled');
- }
- if (before.classes) {
- classes = classes.concat(before.classes.split(/\s+/));
- }
- if (before.tooltip) {
- tooltip = before.tooltip;
- }
- }
-
- html += '' + currVal + '';
- }
-
- view.find('.datepicker-switch').text(startVal + '-' + endVal);
- view.find('td').html(html);
- },
-
- fill: function(){
- var d = new Date(this.viewDate),
- year = d.getUTCFullYear(),
- month = d.getUTCMonth(),
- startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity,
- startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity,
- endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity,
- endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity,
- todaytxt = dates[this.o.language].today || dates['en'].today || '',
- cleartxt = dates[this.o.language].clear || dates['en'].clear || '',
- titleFormat = dates[this.o.language].titleFormat || dates['en'].titleFormat,
- todayDate = UTCToday(),
- titleBtnVisible = (this.o.todayBtn === true || this.o.todayBtn === 'linked') && todayDate >= this.o.startDate && todayDate <= this.o.endDate && !this.weekOfDateIsDisabled(todayDate),
- tooltip,
- before;
- if (isNaN(year) || isNaN(month))
- return;
- this.picker.find('.datepicker-days .datepicker-switch')
- .text(DPGlobal.formatDate(d, titleFormat, this.o.language));
- this.picker.find('tfoot .today')
- .text(todaytxt)
- .css('display', titleBtnVisible ? 'table-cell' : 'none');
- this.picker.find('tfoot .clear')
- .text(cleartxt)
- .css('display', this.o.clearBtn === true ? 'table-cell' : 'none');
- this.picker.find('thead .datepicker-title')
- .text(this.o.title)
- .css('display', typeof this.o.title === 'string' && this.o.title !== '' ? 'table-cell' : 'none');
- this.updateNavArrows();
- this.fillMonths();
- var prevMonth = UTCDate(year, month, 0),
- day = prevMonth.getUTCDate();
- prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7);
- var nextMonth = new Date(prevMonth);
- if (prevMonth.getUTCFullYear() < 100){
- nextMonth.setUTCFullYear(prevMonth.getUTCFullYear());
- }
- nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
- nextMonth = nextMonth.valueOf();
- var html = [];
- var weekDay, clsName;
- while (prevMonth.valueOf() < nextMonth){
- weekDay = prevMonth.getUTCDay();
- if (weekDay === this.o.weekStart){
- html.push('
');
- if (this.o.calendarWeeks){
- // ISO 8601: First week contains first thursday.
- // ISO also states week starts on Monday, but we can be more abstract here.
- var
- // Start of current week: based on weekstart/current date
- ws = new Date(+prevMonth + (this.o.weekStart - weekDay - 7) % 7 * 864e5),
- // Thursday of this week
- th = new Date(Number(ws) + (7 + 4 - ws.getUTCDay()) % 7 * 864e5),
- // First Thursday of year, year from thursday
- yth = new Date(Number(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay()) % 7 * 864e5),
- // Calendar week: ms between thursdays, div ms per day, div 7 days
- calWeek = (th - yth) / 864e5 / 7 + 1;
- html.push('
'+ calWeek +'
');
- }
- }
- clsName = this.getClassNames(prevMonth);
- clsName.push('day');
-
- var content = prevMonth.getUTCDate();
-
- if (this.o.beforeShowDay !== $.noop){
- before = this.o.beforeShowDay(this._utc_to_local(prevMonth));
- if (before === undefined)
- before = {};
- else if (typeof before === 'boolean')
- before = {enabled: before};
- else if (typeof before === 'string')
- before = {classes: before};
- if (before.enabled === false)
- clsName.push('disabled');
- if (before.classes)
- clsName = clsName.concat(before.classes.split(/\s+/));
- if (before.tooltip)
- tooltip = before.tooltip;
- if (before.content)
- content = before.content;
- }
-
- //Check if uniqueSort exists (supported by jquery >=1.12 and >=2.2)
- //Fallback to unique function for older jquery versions
- if ($.isFunction($.uniqueSort)) {
- clsName = $.uniqueSort(clsName);
- } else {
- clsName = $.unique(clsName);
- }
-
- html.push('
{% endblock %}
\ No newline at end of file
diff --git a/NEMO/templates/accounts_and_projects/create_project.html b/NEMO/templates/accounts_and_projects/create_project.html
index 062c5b4d..4b6f0231 100644
--- a/NEMO/templates/accounts_and_projects/create_project.html
+++ b/NEMO/templates/accounts_and_projects/create_project.html
@@ -47,7 +47,7 @@
{% else %}
{% if start or end %}
- No access records exist between {{ start }} and {{ end }}.
+ No access records exist {% if start and end %}between {{ start }} and {{ end }}{% elif start %}after {{ start }}{% else %}before {{ end }}{% endif %}.
{% endif %}
{% endif %}
\ No newline at end of file
diff --git a/NEMO/templates/customizations/customizations.html b/NEMO/templates/customizations/customizations.html
index 3c3b1619..3512977a 100644
--- a/NEMO/templates/customizations/customizations.html
+++ b/NEMO/templates/customizations/customizations.html
@@ -5,881 +5,21 @@
Customizations
You can customize portions of {{ site_title }} to suit your organization's needs.
-
-
-
Email addresses
-
-
-
-
-
-
-
Application settings
-
-
-
-
-
-
-
Calendar settings
-
-
-
-
-
-
-
Status dashboard settings
-
-
-
-
-
-
-
Interlock settings
-
-
-
-
-
-
-
User requests settings
-
-
-
-
-
-
-
Login banner
-
The login banner is an informational message displayed underneath the username and password text boxes on the login page. You can customize this to convey rules for {{ site_title }} users at your organization.
- {% include 'customizations/customizations_upload.html' with element=login_banner name='login banner' %}
-
What would you like everyone to know about safety policy and procedures? This introduction will be presented at the top of the safety page. You can use HTML to modify the look of the text.
- {% include 'customizations/customizations_upload.html' with element=safety_introduction name='safety introduction' %}
-
- The facility rules tutorial is an opportunity to provide new users with a tutorial to your lab operating procedures and rules.
- The HTML you upload is rendered with the Django template engine. You can use JavaScript (including jQuery) within the page.
-
-
-
-
The form should include {% templatetag openblock %} csrf_token {% templatetag closeblock %} inside the form tags.
-
Completion of the HTML form should be POSTed to "{% templatetag openblock %} url 'facility_rules' {% templatetag closeblock %}" (exactly as is).
-
-
- Upon completion, the user's "training required" attribute is set to false, and they are able to make reservations and control tools.
-
- {% include 'customizations/customizations_upload.html' with element=facility_rules_tutorial name='facility rules tutorial' hide_content=True %}
-
- This message is displayed after authentication when either no matching username could be found in the database or if that user has been deactivated.
- The HTML you upload is rendered with the Django template engine. You can use JavaScript (including jQuery) within the message.
-
- {% include 'customizations/customizations_upload.html' with element=authorization_failed name='authorization failed' button_name='Upload failed login page' %}
-
This image is displayed as background for the Jumbotron. It is set as a full background screen, so your image needs to take that into account.
- The image you upload should be a valid image file (recommended size is 1500x1000).
-
- {% include 'customizations/customizations_upload.html' with element=jumbotron_watermark name='jumbotron watermark' extension='png' %}
-
-
-
-
-
-
Access request notification email
-
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.
-
The following context variables are provided when the email is rendered:
-
-
template_color - the color: green if approved, red if denied, blue otherwise
-
access_request - the user's access request that was created or updated
-
status - the status of the request: received, updated, approved or denied
-
access_requests_url - the URL to the access requests page
-
- {% include 'customizations/customizations_upload.html' with element=access_request_notification_email name='access request notification email' %}
-
-
-
-
-
-
Cancellation email
-
This email is sent to a user when a staff member cancels the user's reservation. The following context variables are provided when the email is rendered:
-
-
reservation - the user's reservation that was cancelled
-
staff_member - the user object of the staff member who cancelled the reservation
-
reason - the reason the staff member provided for cancelling the reservation
-
- {% include 'customizations/customizations_upload.html' with element=cancellation_email name='cancellation email' %}
-
-
-
-
-
-
Counter threshold reached email
-
- This email is sent to the counter's warning email when the value of an counter reaches the warning threshold.
- The following context variables are provided when the email is rendered:
-
-
-
counter - the counter which value reached the warning threshold
-
- {% include 'customizations/customizations_upload.html' with element=counter_threshold_reached_email name='counter threshold reached email' %}
-
-
-
-
-
-
Feedback email
-
- This email is sent when a user submits feedback. The feedback email address (at the top of this page) must also be configured for users to be able to do this.
- The following context variables are provided when the email is rendered:
-
-
-
contents - the user's feedback
-
user - the user object of the user who submitted the feedback
-
- {% include 'customizations/customizations_upload.html' with element=feedback_email name='feedback email' %}
-
-
-
-
-
-
Generic email
-
A generic email that can be sent to qualified tool users, members of an account, or members of a project. Send these using the email broadcast page. The following context variables are provided when the email is rendered:
-
-
title - the user specified title of the email
-
greeting - a greeting to the recipients of the email
-
contents - the body of the email
-
template_color - the color to emphasize
-
- {% include 'customizations/customizations_upload.html' with element=generic_email name='generic email' %}
-
-
-
-
-
-
Missed reservation email
-
- This email is sent when a user misses a reservation. If a tool is not used for an amount
- of time after the user's reservation has begun, it is marked as missed and removed from the calendar.
- The following context variables are provided when the email is rendered:
-
-
-
reservation - the reservation that the user missed
-
- {% include 'customizations/customizations_upload.html' with element=missed_reservation_email name='missed reservation email' %}
-
-
-
-
-
-
Facility rules tutorial email
-
- This email is sent when a user completes the facility rules tutorial. It can contain a free-response answer (quiz question).
- If you do not upload a template then no notification email is sent to staff when a user completes the training tutorial.
- The following context variables are provided when the email is rendered:
-
-
-
user - the user's model instance
-
making_reservations_rule_summary - a free-response answer provided by the user. Normally, this is provided by the user to summarize their understanding of the {{ facility_name }} rules and proceedures.
-
- {% include 'customizations/customizations_upload.html' with element=facility_rules_tutorial_email name='facility rules tutorial email' %}
-
-
-
-
-
-
New task email
-
- This email is sent when a new maintenance task is created for a tool.
- The following context variables are provided when the email is rendered:
-
-
-
user - the user who created the task
-
task - the task information
-
tool - the tool that the task is associated with
-
tool_control_absolute_url - the URL of the tool control page for the tool
-
template_color - an HTML color code indicating the severity of the problem. Orange for warning, red for shutdown.
-
- {% include 'customizations/customizations_upload.html' with element=new_task_email name='new task email' %}
-
-
-
-
-
-
Out of time reservation email
-
- This email is sent when a user is still logged in an area but his reservation expired. A grace period can be set when configuring the area.
- The following context variables are provided when the email is rendered:
-
-
-
reservation - the reservation that the user is out of time on
-
- {% include 'customizations/customizations_upload.html' with element=out_of_time_reservation_email name='out of time reservation email' %}
-
-
-
-
-
-
Reorder supplies reminder email
-
- This email is sent to the item's reminder email when the quantity of an item falls below the reminder threshold and should be reordered.
- The following context variables are provided when the email is rendered:
-
-
-
item - the item which quantity fell below the reminder threshold
-
- {% include 'customizations/customizations_upload.html' with element=reorder_supplies_reminder_email name='reorder supplies reminder email' %}
-
-
-
-
-
-
Reservation ending reminder email
-
- This email is sent to a user that is logged in an area 30 and 15 minutes before their area reservation ends.
- The following context variables are provided when the email is rendered:
-
-
-
reservation - the user's reservation ending
-
- {% include 'customizations/customizations_upload.html' with element=reservation_ending_reminder_email name='reservation ending reminder email' %}
-
-
-
-
-
-
Reservation reminder email
-
- This email is sent to a user two hours before their tool/area reservation begins.
- The reservation warning email must also exist for this email to be sent.
- The following context variables are provided when the email is rendered:
-
-
-
reservation - the user's upcoming reservation
-
- {% include 'customizations/customizations_upload.html' with element=reservation_reminder_email name='reservation reminder email' %}
-
-
-
-
-
-
Reservation warning email
-
- This email is sent to a user two hours before their tool/area reservation begins and maintenance may interfere with the upcoming reservation.
- The reservation reminder email must also exist for this email to be sent.
- The following context variables are provided when the email is rendered:
-
-
-
reservation - the user's upcoming reservation
-
fatal_error - boolean value that, when true, indicates that it will be impossible for the user to use the tool/access the area during their reservation (due to maintenance, outages or a missing required dependency)
-
template_color - an HTML color code indicating the severity of the problem. Orange for warning, red for shutdown.
-
- {% include 'customizations/customizations_upload.html' with element=reservation_warning_email name='reservation warning email' %}
-
-
-
-
-
-
Safety issue email
-
- This email is sent when a new maintenance task is created for a tool.
- The following context variables are provided when the email is rendered:
-
-
-
issue - the issue information
-
issue_absolute_url - the URL for the detailed view of the issue
-
- {% include 'customizations/customizations_upload.html' with element=safety_issue_email name='safety issue email' %}
-
-
-
-
-
-
Staff charge reminder email
-
- This email is periodically sent to remind staff that they are charging a user for staff time.
- The following context variables are provided when the email is rendered:
-
-
-
staff_charge - the staff charge that is in progress
-
- {% include 'customizations/customizations_upload.html' with element=staff_charge_reminder_email name='staff charge reminder email' %}
-
-
-
-
-
-
Task status notification email
-
- This email is sent when a tool task has be updated and set to a particular state.
- The following context variables are provided when the email is rendered:
-
-
-
template_color - the color to emphasize
-
title - a title indicating that the message is a task status notification
-
task - the task that was updated
-
status_message - the current status message for the task
-
notification_message - the notification message that is configured (via the admin site) for the status
-
tool_control_absolute_url - the URL of the tool control page for the task
-
- {% include 'customizations/customizations_upload.html' with element=task_status_notification name='task status notification' %}
-
-
-
-
-
-
Unauthorized tool access email
-
- This email is sent when a user tries to access a tool:
-
-
-
without being logged in to the area in which the tool resides (type 'area').
-
without having a current area reservation (type 'reservation')
-
-
- The following context variables are provided when the email is rendered:
-
-
-
operator - the person who attempted to use the tool
-
tool - the tool that the user was denied access to
-
type - the type of abuse ('area' or 'reservation')
-
- {% include 'customizations/customizations_upload.html' with element=unauthorized_tool_access_email name='unauthorized tool access email' %}
-
-
-
-
-
-
Usage reminder email
-
- This email is periodically sent to remind a user that they have a tool enabled.
- The following context variables are provided when the email is rendered:
-
-
-
user - the user who is using a tool or logged in to an area
-
- {% include 'customizations/customizations_upload.html' with element=usage_reminder_email name='usage reminder email' %}
-
-
-
-
-
-
User reservation created email
-
- This email is sent to a user when the user creates a reservation and has opted to receive ics calendar notification in his preferences.
- This is optional, the email will still be sent with only the calendar attachment if this is left blank.
-
The following context variables are provided when the email is rendered:
-
-
-
reservation - the user's reservation that was created
-
- {% include 'customizations/customizations_upload.html' with element=reservation_created_user_email name='reservation created user email' %}
-
-
-
-
-
-
User reservation cancelled email
-
- This email is sent to a user when the user cancels his own reservation and has opted to receive ics calendar notification in his preferences.
- This is optional, the email will still be sent with only the calendar attachment if this is left blank.
-
The following context variables are provided when the email is rendered:
-
-
-
reservation - the user's reservation that was cancelled
-
- {% include 'customizations/customizations_upload.html' with element=reservation_cancelled_user_email name='reservation cancelled user email' %}
-
- there is at least one approved access request that includes weekend time during the current week.
- (The email is sent when the first weekend access request is approved).
-
-
- there are no approved access requests that include weekend time during the current week.
- (the email is sent on the cutoff day at the cutoff hour provided in the user requests settings above).
-
-
-
- The following context variables are provided when the email is rendered:
-
-
-
weekend_access - true/false, whether there are approved weekend access requests for the current week.
-
- {% include 'customizations/customizations_upload.html' with element=weekend_access_email name='weekend access email' %}
+
+ {% for customization_instance in customization.instances %}
+
- You can upload a json file to display tool rates. An example of a valid rates file can be found on Github.
-
It should be a JSON array of elements, and the following key/values are supported for each element:
-
-
-
item_id - the id of the tool or supply
-
table_id - the type of rate, one of "inventory_rate" (for supplies), "primetime_eq_hourly_rate" (for tool), "training_individual_hourly_rate" (for individual training rate), "training_group_hourly_rate" (for group training rate)
-
rate_class - the class, one of "full cost" or "cost shared"
-
rate - the rate amount
-
item - the name of the item (optional)
-
- {% include 'customizations/customizations_upload.html' with element=rates name='rates' extension="json" %}
-
-
+
\ No newline at end of file
diff --git a/NEMO/templates/customizations/customizations_calendar.html b/NEMO/templates/customizations/customizations_calendar.html
new file mode 100644
index 00000000..6740a866
--- /dev/null
+++ b/NEMO/templates/customizations/customizations_calendar.html
@@ -0,0 +1,107 @@
+
+
Calendar settings
+
+
\ No newline at end of file
diff --git a/NEMO/templates/customizations/customizations_dashboard.html b/NEMO/templates/customizations/customizations_dashboard.html
new file mode 100644
index 00000000..ce9a2400
--- /dev/null
+++ b/NEMO/templates/customizations/customizations_dashboard.html
@@ -0,0 +1,94 @@
+
+
Status dashboard settings
+
+
\ No newline at end of file
diff --git a/NEMO/templates/customizations/customizations_emails.html b/NEMO/templates/customizations/customizations_emails.html
new file mode 100644
index 00000000..be28a667
--- /dev/null
+++ b/NEMO/templates/customizations/customizations_emails.html
@@ -0,0 +1,61 @@
+
+
Email addresses
+
+
\ No newline at end of file
diff --git a/NEMO/templates/customizations/customizations_interlock.html b/NEMO/templates/customizations/customizations_interlock.html
new file mode 100644
index 00000000..37df81b8
--- /dev/null
+++ b/NEMO/templates/customizations/customizations_interlock.html
@@ -0,0 +1,44 @@
+
+
Interlock settings
+
+
+
\ No newline at end of file
diff --git a/NEMO/templates/customizations/customizations_rates.html b/NEMO/templates/customizations/customizations_rates.html
new file mode 100644
index 00000000..6fe86495
--- /dev/null
+++ b/NEMO/templates/customizations/customizations_rates.html
@@ -0,0 +1,15 @@
+
+
Tool Rates
+
+ You can upload a json file to display tool rates. An example of a valid rates file can be found on Github.
+
It should be a JSON array of elements, and the following key/values are supported for each element:
+
+
+
item_id - the id of the tool or supply
+
table_id - the type of rate, one of "inventory_rate" (for supplies), "primetime_eq_hourly_rate" (for tool), "training_individual_hourly_rate" (for individual training rate), "training_group_hourly_rate" (for group training rate)
+
rate_class - the class, one of "full cost" or "cost shared"
+
rate - the rate amount
+
item - the name of the item (optional)
+
+ {% include 'customizations/customizations_upload.html' with element=rates name='rates' extension='json' key='rates' %}
+
\ No newline at end of file
diff --git a/NEMO/templates/customizations/customizations_requests.html b/NEMO/templates/customizations/customizations_requests.html
new file mode 100644
index 00000000..57437fee
--- /dev/null
+++ b/NEMO/templates/customizations/customizations_requests.html
@@ -0,0 +1,116 @@
+
+
User requests settings
+
+
+
\ No newline at end of file
diff --git a/NEMO/templates/customizations/customizations_templates.html b/NEMO/templates/customizations/customizations_templates.html
new file mode 100644
index 00000000..ac1b434d
--- /dev/null
+++ b/NEMO/templates/customizations/customizations_templates.html
@@ -0,0 +1,357 @@
+
+
Login banner
+
The login banner is an informational message displayed underneath the username and password text boxes on the login page. You can customize this to convey rules for {{ site_title }} users at your organization.
+ {% include 'customizations/customizations_upload.html' with element=login_banner name='login banner' key='templates' %}
+
+
+ This message is displayed after authentication when either no matching username could be found in the database or if that user has been deactivated.
+ The HTML you upload is rendered with the Django template engine. You can use JavaScript (including jQuery) within the message.
+
+ {% include 'customizations/customizations_upload.html' with element=authorization_failed name='authorization failed' button_name='Upload failed login page' key='templates' %}
+
+
What would you like everyone to know about safety policy and procedures? This introduction will be presented at the top of the safety page. You can use HTML to modify the look of the text.
+ {% include 'customizations/customizations_upload.html' with element=safety_introduction name='safety introduction' key='templates' %}
+
+
+ The facility rules tutorial is an opportunity to provide new users with a tutorial to your lab operating procedures and rules.
+ The HTML you upload is rendered with the Django template engine. You can use JavaScript (including jQuery) within the page.
+
+
+
+
The form should include {% templatetag openblock %} csrf_token {% templatetag closeblock %} inside the form tags.
+
Completion of the HTML form should be POSTed to "{% templatetag openblock %} url 'facility_rules' {% templatetag closeblock %}" (exactly as is).
+
+
+ Upon completion, the user's "training required" attribute is set to false, and they are able to make reservations and control tools.
+
+ {% include 'customizations/customizations_upload.html' with element=facility_rules_tutorial name='facility rules tutorial' hide_content=True key='templates' %}
+
+
This image is displayed as background for the Jumbotron. It is set as a full background screen, so your image needs to take that into account.
+ The image you upload should be a valid image file (recommended size is 1500x1000).
+
+ {% include 'customizations/customizations_upload.html' with element=jumbotron_watermark name='jumbotron watermark' extension='png' key='templates' %}
+
+
+
+
Access request notification email
+
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.
+
The following context variables are provided when the email is rendered:
+
+
template_color - the color: green if approved, red if denied, blue otherwise
+
access_request - the user's access request that was created or updated
+
status - the status of the request: received, updated, approved or denied
+
access_requests_url - the URL to the access requests page
+
+ {% include 'customizations/customizations_upload.html' with element=access_request_notification_email name='access request notification email' key='templates' %}
+
+
+
+
+
Cancellation email
+
This email is sent to a user when a staff member cancels the user's reservation. The following context variables are provided when the email is rendered:
+
+
reservation - the user's reservation that was cancelled
+
staff_member - the user object of the staff member who cancelled the reservation
+
reason - the reason the staff member provided for cancelling the reservation
+
+ {% include 'customizations/customizations_upload.html' with element=cancellation_email name='cancellation email' key='templates' %}
+
+
+
+
+
Counter threshold reached email
+
+ This email is sent to the counter's warning email when the value of an counter reaches the warning threshold.
+ The following context variables are provided when the email is rendered:
+
+
+
counter - the counter which value reached the warning threshold
+
+ {% include 'customizations/customizations_upload.html' with element=counter_threshold_reached_email name='counter threshold reached email' key='templates' %}
+
+
+
+
+
Feedback email
+
+ This email is sent when a user submits feedback. The feedback email address (at the top of this page) must also be configured for users to be able to do this.
+ The following context variables are provided when the email is rendered:
+
+
+
contents - the user's feedback
+
user - the user object of the user who submitted the feedback
+
+ {% include 'customizations/customizations_upload.html' with element=feedback_email name='feedback email' key='templates' %}
+
+
+
+
+
Generic email
+
A generic email that can be sent to qualified tool users, members of an account, or members of a project. Send these using the email broadcast page. The following context variables are provided when the email is rendered:
+
+
title - the user specified title of the email
+
greeting - a greeting to the recipients of the email
+
contents - the body of the email
+
template_color - the color to emphasize
+
+ {% include 'customizations/customizations_upload.html' with element=generic_email name='generic email' key='templates' %}
+
+
+
+
+
Missed reservation email
+
+ This email is sent when a user misses a reservation. If a tool is not used for an amount
+ of time after the user's reservation has begun, it is marked as missed and removed from the calendar.
+ The following context variables are provided when the email is rendered:
+
+
+
reservation - the reservation that the user missed
+
+ {% include 'customizations/customizations_upload.html' with element=missed_reservation_email name='missed reservation email' key='templates' %}
+
+
+
+
+
Facility rules tutorial email
+
+ This email is sent when a user completes the facility rules tutorial. It can contain a free-response answer (quiz question).
+ If you do not upload a template then no notification email is sent to staff when a user completes the training tutorial.
+ The following context variables are provided when the email is rendered:
+
+
+
user - the user's model instance
+
making_reservations_rule_summary - a free-response answer provided by the user. Normally, this is provided by the user to summarize their understanding of the {{ facility_name }} rules and proceedures.
+
+ {% include 'customizations/customizations_upload.html' with element=facility_rules_tutorial_email name='facility rules tutorial email' key='templates' %}
+
+
+
+
+
New task email
+
+ This email is sent when a new maintenance task is created for a tool.
+ The following context variables are provided when the email is rendered:
+
+
+
user - the user who created the task
+
task - the task information
+
tool - the tool that the task is associated with
+
tool_control_absolute_url - the URL of the tool control page for the tool
+
template_color - an HTML color code indicating the severity of the problem. Orange for warning, red for shutdown.
+
+ {% include 'customizations/customizations_upload.html' with element=new_task_email name='new task email' key='templates' %}
+
+
+
+
+
Out of time reservation email
+
+ This email is sent when a user is still logged in an area but his reservation expired. A grace period can be set when configuring the area.
+ The following context variables are provided when the email is rendered:
+
+
+
reservation - the reservation that the user is out of time on
+
+ {% include 'customizations/customizations_upload.html' with element=out_of_time_reservation_email name='out of time reservation email' key='templates' %}
+
+
+
+
+
Reorder supplies reminder email
+
+ This email is sent to the item's reminder email when the quantity of an item falls below the reminder threshold and should be reordered.
+ The following context variables are provided when the email is rendered:
+
+
+
item - the item which quantity fell below the reminder threshold
+
+ {% include 'customizations/customizations_upload.html' with element=reorder_supplies_reminder_email name='reorder supplies reminder email' key='templates' %}
+
+
+
+
+
Reservation ending reminder email
+
+ This email is sent to a user that is logged in an area 30 and 15 minutes before their area reservation ends.
+ The following context variables are provided when the email is rendered:
+
+
+
reservation - the user's reservation ending
+
+ {% include 'customizations/customizations_upload.html' with element=reservation_ending_reminder_email name='reservation ending reminder email' key='templates' %}
+
+
+
+
+
Reservation reminder email
+
+ This email is sent to a user two hours before their tool/area reservation begins.
+ The reservation warning email must also exist for this email to be sent.
+ The following context variables are provided when the email is rendered:
+
+
+
reservation - the user's upcoming reservation
+
+ {% include 'customizations/customizations_upload.html' with element=reservation_reminder_email name='reservation reminder email' key='templates' %}
+
+
+
+
+
Reservation warning email
+
+ This email is sent to a user two hours before their tool/area reservation begins and maintenance may interfere with the upcoming reservation.
+ The reservation reminder email must also exist for this email to be sent.
+ The following context variables are provided when the email is rendered:
+
+
+
reservation - the user's upcoming reservation
+
fatal_error - boolean value that, when true, indicates that it will be impossible for the user to use the tool/access the area during their reservation (due to maintenance, outages or a missing required dependency)
+
template_color - an HTML color code indicating the severity of the problem. Orange for warning, red for shutdown.
+
+ {% include 'customizations/customizations_upload.html' with element=reservation_warning_email name='reservation warning email' key='templates' %}
+
+
+
+
+
Safety issue email
+
+ This email is sent when a new maintenance task is created for a tool.
+ The following context variables are provided when the email is rendered:
+
+
+
issue - the issue information
+
issue_absolute_url - the URL for the detailed view of the issue
+
+ {% include 'customizations/customizations_upload.html' with element=safety_issue_email name='safety issue email' key='templates' %}
+
+
+
+
+
Staff charge reminder email
+
+ This email is periodically sent to remind staff that they are charging a user for staff time.
+ The following context variables are provided when the email is rendered:
+
+
+
staff_charge - the staff charge that is in progress
+
+ {% include 'customizations/customizations_upload.html' with element=staff_charge_reminder_email name='staff charge reminder email' key='templates' %}
+
+
+
+
+
Task status notification email
+
+ This email is sent when a tool task has be updated and set to a particular state.
+ The following context variables are provided when the email is rendered:
+
+
+
template_color - the color to emphasize
+
title - a title indicating that the message is a task status notification
+
task - the task that was updated
+
status_message - the current status message for the task
+
notification_message - the notification message that is configured (via the admin site) for the status
+
tool_control_absolute_url - the URL of the tool control page for the task
+
+ {% include 'customizations/customizations_upload.html' with element=task_status_notification name='task status notification' key='templates' %}
+
+
+
+
+
Unauthorized tool access email
+
+ This email is sent when a user tries to access a tool:
+
+
+
without being logged in to the area in which the tool resides (type 'area').
+
without having a current area reservation (type 'reservation')
+
+
+ The following context variables are provided when the email is rendered:
+
+
+
operator - the person who attempted to use the tool
+
tool - the tool that the user was denied access to
+
type - the type of abuse ('area' or 'reservation')
+
+ {% include 'customizations/customizations_upload.html' with element=unauthorized_tool_access_email name='unauthorized tool access email' key='templates' %}
+
+
+
+
+
Usage reminder email
+
+ This email is periodically sent to remind a user that they have a tool enabled.
+ The following context variables are provided when the email is rendered:
+
+
+
user - the user who is using a tool or logged in to an area
+
+ {% include 'customizations/customizations_upload.html' with element=usage_reminder_email name='usage reminder email' key='templates' %}
+
+
+
+
+
User reservation created email
+
+ This email is sent to a user when the user creates a reservation and has opted to receive ics calendar notification in his preferences.
+ This is optional, the email will still be sent with only the calendar attachment if this is left blank.
+
The following context variables are provided when the email is rendered:
+
+
+
reservation - the user's reservation that was created
+
+ {% include 'customizations/customizations_upload.html' with element=reservation_created_user_email name='reservation created user email' key='templates' %}
+
+
+
+
+
User reservation cancelled email
+
+ This email is sent to a user when the user cancels his own reservation and has opted to receive ics calendar notification in his preferences.
+ This is optional, the email will still be sent with only the calendar attachment if this is left blank.
+
The following context variables are provided when the email is rendered:
+
+
+
reservation - the user's reservation that was cancelled
+
+ {% include 'customizations/customizations_upload.html' with element=reservation_cancelled_user_email name='reservation cancelled user email' key='templates' %}
+
+
+ there is at least one approved access request that includes weekend time during the current week.
+ (The email is sent when the first weekend access request is approved).
+
+
+ there are no approved access requests that include weekend time during the current week.
+ (the email is sent on the cutoff day at the cutoff hour provided in the user requests settings above).
+
+
+
+ The following context variables are provided when the email is rendered:
+
+
+
weekend_access - true/false, whether there are approved weekend access requests for the current week.
+
+ {% include 'customizations/customizations_upload.html' with element=weekend_access_email name='weekend access email' key='templates' %}
+
\ No newline at end of file
diff --git a/NEMO/templates/customizations/customizations_upload.html b/NEMO/templates/customizations/customizations_upload.html
index 90e3c246..00869b3a 100644
--- a/NEMO/templates/customizations/customizations_upload.html
+++ b/NEMO/templates/customizations/customizations_upload.html
@@ -1,19 +1,22 @@
{% load static %}
{% with element_name=name.split|join:"_"|lower extension=extension|default:"html" %}
-