Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.1.0.dev #114

Merged
merged 84 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
c179b34
- starting 4.1.0 dev
rptmat57 Apr 7, 2022
e24e96c
- added version number to login page
rptmat57 Apr 7, 2022
2f43446
- fixed closures not respecting staff_absent flag on staff status cal…
rptmat57 Apr 14, 2022
74c46d5
- fixed format_datetime not checking the right type of datetime
rptmat57 Apr 18, 2022
61dd8ad
- added default auto field to splash_pad_settings.py
rptmat57 Apr 20, 2022
299ce9f
- removing unit from interlock modbus calls
rptmat57 Apr 21, 2022
acc2a9f
- updated python version 3.6 -> 3.7 in setup.py
rptmat57 Apr 21, 2022
8f27f17
- updated set interval js method to return the interval handle
rptmat57 Apr 21, 2022
6baddd7
- initial commit of internal sensor plugin
rptmat57 Apr 21, 2022
4056ded
- added tabs in sensor data page
rptmat57 Apr 22, 2022
1daebc1
- fixed spelling error
rptmat57 Apr 22, 2022
e8f8ee7
Bump django from 3.2.12 to 3.2.13
dependabot[bot] Apr 22, 2022
d37d877
- split customizations into tabs and simplified the logic
rptmat57 Apr 24, 2022
86347cb
- removed commented out code
rptmat57 Apr 24, 2022
f71fa5c
- changed sensor data value from int to float
rptmat57 Apr 26, 2022
3077997
- consolidating sensors migrations
rptmat57 Apr 26, 2022
aedb603
- added default category for sensors
rptmat57 Apr 26, 2022
d1ad93b
- completely refactored customizations to allow plugins to easily add…
rptmat57 Apr 26, 2022
9a89fef
- updated sensor categories to allow multiple levels
rptmat57 May 2, 2022
9ff764d
- consolidating sensors migrations
rptmat57 May 2, 2022
e0a2b92
- moment.js 2.10.2 -> 2.29.3
rptmat57 May 2, 2022
779767c
- added minified versions of jquery and moment
rptmat57 May 4, 2022
3d9ecbd
- added new option for landing page choices to hide icon from staff (…
rptmat57 May 4, 2022
e8b0f6f
- consolidated migrations
rptmat57 May 4, 2022
73a9f12
- fixed landing page logic
rptmat57 May 4, 2022
378cd2a
- added minified versions of most of the js and css libraries
rptmat57 May 4, 2022
16ace97
- added moment.min.js.map
rptmat57 May 5, 2022
dcfaa37
- added customization for sensor default refresh rate
rptmat57 May 5, 2022
f7642fc
- fixed bug preventing more data to be read when an error occurred
rptmat57 May 5, 2022
96841b2
- when copying a sensor configuration, read frequency will be disable…
rptmat57 May 5, 2022
102d53e
- fixed set_interval_when_visible js function which would get called …
rptmat57 May 5, 2022
8539c01
- when called with no time parameter, set_interval_when_visible will …
rptmat57 May 5, 2022
fbb09d0
- added new variables to set js dateformat for date and datetime pickers
rptmat57 May 5, 2022
98dda12
- added daterange for sensor chart and data table
rptmat57 May 5, 2022
0425a4c
- added disable session expiry refresh on sensor data pull
rptmat57 May 5, 2022
c835e19
- removed hardcoded date formats for calendar feed and mobile calenda…
rptmat57 May 5, 2022
284af14
- now using date/time input formats already set in settings.py in sen…
rptmat57 May 9, 2022
b360603
- updated create account and create project pages to use date input f…
rptmat57 May 9, 2022
4dc3a47
- updated new user page to use date input format from settings
rptmat57 May 9, 2022
5775ee9
- updated staff absence form to use date input format from settings
rptmat57 May 9, 2022
50e81e1
- updated buddy request form to use date input format from settings
rptmat57 May 9, 2022
9a4debd
- updated input date format filter to return empty string when date i…
rptmat57 May 9, 2022
2676883
- updated access request form to use date input format from settings
rptmat57 May 9, 2022
9a76858
- fixed sensor end date type issue
rptmat57 May 9, 2022
5289579
- added option to extract times with beginning and end of the day
rptmat57 May 10, 2022
22bd797
- using UTC moment dates in sensor for consistency
rptmat57 May 10, 2022
d5aeb43
- updated comments and usage data forms to use date input format from…
rptmat57 May 10, 2022
caf09f5
- fixed cancel outage button not displayed
rptmat57 May 11, 2022
a170f7a
- made all date/time parsing consistent with input formats from setti…
rptmat57 May 11, 2022
a15f637
- added customization setting to make training required for new users
rptmat57 May 12, 2022
5d09d7d
- replaced all usages of get_customization() with the appropriate cla…
rptmat57 May 12, 2022
4f31650
- rename customization training_not_required to default_user_training…
rptmat57 May 12, 2022
65b89c3
- on post usage questions, reservation questions and additional event…
rptmat57 May 12, 2022
eaf7aa0
- fixed some wrong js conditions. evaluating if ('{{ var }}') will t…
rptmat57 May 13, 2022
5176bd4
Requires user to be logged in order to access media folder items.
r-xyz May 16, 2022
359ca3f
Merge pull request #112 from r-xyz/master
rptmat57 May 16, 2022
203cc7a
- Added alternate email user preferences, with options to send usage …
rptmat57 May 16, 2022
f5e9bfc
- consolidating 4.1.0 migrations
rptmat57 May 16, 2022
6b654a7
Merge pull request #111 from usnistgov/dependabot/pip/django-3.2.13
rptmat57 May 17, 2022
49e358d
- added sensor email alerts
rptmat57 May 19, 2022
c57258d
- consolidating 4.1.0 migrations and initial sensor migrations
rptmat57 May 19, 2022
506ff88
- added "enabled" flag in sensor alert admin display list
rptmat57 May 20, 2022
edbfb26
- made email address for sensor alert a list
rptmat57 May 20, 2022
40115f2
- consolidating sensor plugin migrations
rptmat57 May 20, 2022
43103bf
- updated Nginx authentication to strip domain from username if present
rptmat57 May 24, 2022
11b0a1a
- updated splash pad reservation question title
rptmat57 May 27, 2022
3b62e21
- added more supported functions in evaluators for sensors
rptmat57 May 27, 2022
ea15d29
- added sensor data url in administration menu
rptmat57 May 27, 2022
8daf581
- added confirmation dialog before forcing user out of a tool or area…
rptmat57 May 28, 2022
6386e8c
- for sensors, checking that modbus connection was established before…
rptmat57 May 31, 2022
5233d55
- cryptography 36.0.2 -> 37.0.2
rptmat57 May 31, 2022
e826b3e
- added DjangoModelPermissions as an alternative to BillingAPI permis…
rptmat57 Jun 4, 2022
c551eea
- updated ALLOWED_HOSTS on splash_pad_settings.py
rptmat57 Jun 9, 2022
b713870
- updated splash pad data and README.md to add new online demo info
rptmat57 Jun 9, 2022
a59ef8a
- fixed a bug with migrate and migrations breaking when application i…
rptmat57 Jun 11, 2022
1830583
- fixed a permission issue when accessing billing API
rptmat57 Jun 11, 2022
2513c8a
- added sensors app to splash pad
rptmat57 Jun 13, 2022
27b0931
- removed unnecessary odd/even ports on sensor card
rptmat57 Jun 13, 2022
681dd40
- added more options for default date range on sensors
rptmat57 Jun 13, 2022
dc253bb
- added some sensor data in the splash pad version
rptmat57 Jun 13, 2022
baeed12
- added more alert logs in splash pad data
rptmat57 Jun 14, 2022
f0cba16
- removed unused "number" from sensor card
rptmat57 Jun 15, 2022
a6468b3
- added many more tools to NEMO splash pad
rptmat57 Jun 15, 2022
ad825a2
- preparing for 4.1.0 release
rptmat57 Jun 15, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Dockerfile.splash_pad
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ RUN rm --recursive --force /nemo

RUN mkdir /nemo
RUN mkdir /nemo/media
RUN mkdir /nemo/media/tool_images
WORKDIR /nemo

COPY resources/icons/* /nemo/media/
COPY resources/people/* /nemo/media/
COPY resources/sounds/* /nemo/media/
COPY resources/images/tool_images/* /nemo/media/tool_images/
COPY resources/images/* /nemo/media/
COPY resources/emails/* /nemo/media/
COPY resources/splash_pad_rates.json /nemo/media/rates.json
Expand Down
8 changes: 6 additions & 2 deletions NEMO/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import sys

from django.apps import AppConfig


def init_admin_site():
from NEMO.views.customization import get_customization
from NEMO.views.customization import ApplicationCustomization
from django.contrib import admin
# customize the site
site_title = get_customization("site_title", raise_exception=False)
site_title = ApplicationCustomization.get("site_title", raise_exception=False)
admin.site.site_header = site_title
admin.site.site_title = site_title
admin.site.index_title = "Detailed administration"
Expand All @@ -20,6 +22,8 @@ class NEMOConfig(AppConfig):
name = "NEMO"

def ready(self):
if 'migrate' or 'makemigrations' in sys.argv:
return
from django.apps import apps
if apps.is_installed("django.contrib.admin"):
init_admin_site()
Expand Down
6 changes: 3 additions & 3 deletions NEMO/apps/area_access/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
)
from NEMO.models import (BadgeReader, Door, PhysicalAccessLog, PhysicalAccessType, Project, UsageEvent, User)
from NEMO.views.area_access import log_in_user_to_area, log_out_user
from NEMO.views.customization import get_customization
from NEMO.views.customization import ApplicationCustomization, InterlockCustomization
from NEMO.views.policy import check_billing_to_project, check_policy_to_enter_any_area, check_policy_to_enter_this_area
from NEMO.views.tool_control import interlock_bypass_allowed

Expand Down Expand Up @@ -73,7 +73,7 @@ def login_to_area(request, door_id):
log.time = timezone.now()
log.result = PhysicalAccessType.DENY # Assume the user does not have access

facility_name = get_customization("facility_name")
facility_name = ApplicationCustomization.get("facility_name")

# Check policy for entering an area
try:
Expand Down Expand Up @@ -289,7 +289,7 @@ def open_door(request, door_id):


def interlock_error(action: str = None, user: User = None, bypass_allowed: bool = None):
error_message = get_customization('door_interlock_failure_message')
error_message = InterlockCustomization.get('door_interlock_failure_message')
bypass_allowed = interlock_bypass_allowed(user) if bypass_allowed is None else bypass_allowed
dictionary = {
"message": linebreaksbr(error_message),
Expand Down
2 changes: 1 addition & 1 deletion NEMO/apps/kiosk/templates/kiosk/tool_information.html
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ <h2>

update_stop_button();
$('body').on('question-group-changed', update_stop_button);
$('body').on('change keyup', '#tool_control input[required], #tool_control select[required], #tool_control textarea[required], #downtime', update_stop_button);
$('body').on('change keyup', '#tool_control input, #tool_control select, #tool_control textarea, #downtime', update_stop_button);
$('#downtime').numpad({'readonly': false, 'hidePlusMinusButton': true, 'hideDecimalButton': true});
$('#delayed_logoff_help').popover();
revert(60);
Expand Down
24 changes: 12 additions & 12 deletions NEMO/apps/kiosk/templates/kiosk/tool_reservation.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ <h4>When would you like to reserve the {{ tool }}?</h4>
</form>
<div style="height:350px"></div>
<script>
var unavailable_times = [];
let unavailable_times = [];
{% for item in tool_reservation_times %}
unavailable_times.push([{{ item.start|date:"U" }},{{ item.end|date:"U" }}]);
{% endfor %}
var date_picker = $('#date').pickadate({format: "dddd, mmmm d", formatSubmit: "yyyy-mm-dd", firstDay: 1, hiddenName: true, onSet: refresh_times});
var start_time_picker = $('#start').pickatime({interval: 15, formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
var end_time_picker = $('#end').pickatime({interval: 15, formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
let date_picker = $('#date').pickadate({format: "dddd, mmmm d", formatSubmit: "yyyy-mm-dd", firstDay: 1, hiddenName: true, onSet: refresh_times});
let start_time_picker = $('#start').pickatime({interval: 15, formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
let end_time_picker = $('#end').pickatime({interval: 15, formatSubmit: "H:i", hiddenName: true, formatLabel: format_label});
// set initial date
if ('{{ date }}') {
if ('{{ date|default_if_none:'' }}') {
date_picker.pickadate('picker').set('select', '{{ date }}', {format: 'yyyy-mm-dd'})
}
function refresh_times() {
Expand All @@ -71,13 +71,13 @@ <h4>When would you like to reserve the {{ tool }}?</h4>
}
function format_label(time) {
if (date_picker.pickadate('picker').get('select') && unavailable_times.length > 0) {
var date_selected = date_picker.pickadate('picker').get('select').pick; // selected date in milliseconds
var time_selected = time.pick * 60 * 1000; // time in milliseconds
var date_time_selected = (date_selected + time_selected)/1000; // back to seconds to compare with python timestamp
for (var i=0 ; i < unavailable_times.length; i++) {
var times = unavailable_times[i];
var start = times[0];
var end = times[1];
let date_selected = date_picker.pickadate('picker').get('select').pick; // selected date in milliseconds
let time_selected = time.pick * 60 * 1000; // time in milliseconds
let date_time_selected = (date_selected + time_selected)/1000; // back to seconds to compare with python timestamp
for (let i=0 ; i < unavailable_times.length; i++) {
let times = unavailable_times[i];
let start = times[0];
let end = times[1];
if (date_time_selected >= start && date_time_selected < end) {
return '<sp!an>h:i A</sp!an> <sm!all> !alre!ad!y re!serve!d</sm!all>';
}
Expand Down
Empty file added NEMO/apps/sensors/__init__.py
Empty file.
229 changes: 229 additions & 0 deletions NEMO/apps/sensors/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
from copy import deepcopy

from django import forms
from django.contrib import admin, messages
from django.contrib.admin import register
from django.contrib.admin.decorators import display
from django.contrib.admin.utils import display_for_value
from django.urls import reverse
from django.utils.safestring import mark_safe

from NEMO.apps.sensors.models import (
Sensor,
SensorAlertEmail,
SensorAlertLog,
SensorCard,
SensorCardCategory,
SensorCategory,
SensorData,
)


def duplicate_sensor_configuration(model_admin, request, queryset):
for sensor in queryset:
original_name = sensor.name
new_name = "Copy of " + sensor.name
try:
existing_sensor = Sensor.objects.filter(name=new_name)
if existing_sensor.exists():
messages.error(
request,
mark_safe(
f'There is already a copy of {original_name} as <a href="{reverse("admin:sensors_sensor_change", args=[existing_sensor.first().id])}">{new_name}</a>. Change the copy\'s name and try again'
),
)
continue
else:
new_sensor: Sensor = deepcopy(sensor)
new_sensor.name = new_name
new_sensor.read_frequency = 0
new_sensor.id = None
new_sensor.pk = None
new_sensor.save()
messages.success(
request,
mark_safe(
f'A duplicate of {original_name} has been made as <a href="{reverse("admin:sensors_sensor_change", args=[new_sensor.id])}">{new_sensor.name}</a>'
),
)
except Exception as error:
messages.error(
request, f"{original_name} could not be duplicated because of the following error: {str(error)}"
)


def read_selected_sensors(model_admin, request, queryset):
for sensor in queryset:
try:
response = sensor.read_data(raise_exception=True)
if isinstance(response, SensorData):
messages.success(request, f"{sensor} data read: {response.value}")
elif isinstance(response, str):
messages.warning(request, response)
except Exception as error:
messages.error(request, f"{sensor} data could not be read due to the following error: {str(error)}")


class SensorCardAdminForm(forms.ModelForm):
class Meta:
model = SensorCard
widgets = {"password": forms.PasswordInput(render_value=True)}
fields = "__all__"

def clean(self):
if any(self.errors):
return
cleaned_data = super().clean()
category = cleaned_data["category"]
from NEMO.apps.sensors import sensors

sensors.get(category).clean_sensor_card(self)
return cleaned_data


@register(SensorCard)
class SensorCardAdmin(admin.ModelAdmin):
form = SensorCardAdminForm
list_display = ("name", "enabled", "server", "port", "category")


class SensorAdminForm(forms.ModelForm):
class Meta:
model = Sensor
fields = "__all__"

def clean(self):
if any(self.errors):
return
cleaned_data = super().clean()

card = (
self.cleaned_data["sensor_card"]
if "sensor_card" in self.cleaned_data
else self.cleaned_data["interlock_card"]
)
if card:
category = card.category
from NEMO.apps.sensors import sensors

sensors.get(category).clean_sensor(self)
return cleaned_data


class SensorCategoryAdminForm(forms.ModelForm):
class Meta:
model = SensorCategory
fields = "__all__"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
children_ids = [child.id for child in self.instance.all_children()]
self.fields["parent"].queryset = SensorCategory.objects.exclude(id__in=[self.instance.pk, *children_ids])


@register(SensorCategory)
class SensorCategoryAdmin(admin.ModelAdmin):
form = SensorCategoryAdminForm
list_display = ("name", "get_parent", "get_children")

@display(ordering="children", description="Children")
def get_children(self, category: SensorCategory) -> str:
return mark_safe(
", ".join(
[
f'<a href="{reverse("admin:sensors_sensorcategory_change", args=[child.id])}">{child.name}</a>'
for child in category.children.all()
]
)
)

@display(ordering="parent", description="Parent")
def get_parent(self, category: SensorCategory) -> str:
if not category.parent:
return ""
return mark_safe(
f'<a href="{reverse("admin:sensors_sensorcategory_change", args=[category.parent.id])}">{category.parent.name}</a>'
)

def formfield_for_foreignkey(self, db_field, request, **kwargs):
""" Filter list of potential parents """
if db_field.name == "parent":
kwargs["queryset"] = SensorCategory.objects.filter()
return super().formfield_for_foreignkey(db_field, request, **kwargs)


@register(Sensor)
class SensorAdmin(admin.ModelAdmin):
form = SensorAdminForm
list_display = (
"id",
"name",
"visible",
"card",
"get_card_enabled",
"sensor_category",
"unit_id",
"read_address",
"number_of_values",
"get_read_frequency",
"get_last_read",
"get_last_read_at",
)
actions = [duplicate_sensor_configuration, read_selected_sensors]

@display(boolean=True, ordering="sensor_card__enabled", description="Card Enabled")
def get_card_enabled(self, obj: Sensor):
return obj.card.enabled

@display(description="Last read")
def get_last_read(self, obj: Sensor):
last_data_point = obj.last_data_point()
return last_data_point.value if last_data_point else ""

@display(description="Last read at")
def get_last_read_at(self, obj: Sensor):
last_data_point = obj.last_data_point()
return last_data_point.created_date if last_data_point else ""

@display(ordering="read_frequency", description="Read frequency")
def get_read_frequency(self, obj: Sensor):
return obj.read_frequency if obj.read_frequency != 0 else display_for_value(False, "", boolean=True)


@register(SensorCardCategory)
class SensorCardCategoryAdmin(admin.ModelAdmin):
list_display = ("name", "key")


@register(SensorData)
class SensorDataAdmin(admin.ModelAdmin):
list_display = ("created_date", "sensor", "value", "get_display_value")
date_hierarchy = "created_date"
list_filter = ("sensor", "sensor__sensor_category")

@display(ordering="sensor__data_prefix", description="Display value")
def get_display_value(self, obj: SensorData):
return obj.display_value()


@register(SensorAlertEmail)
class SensorAlertEmailAdmin(admin.ModelAdmin):
list_display = ("sensor", "enabled", "trigger_condition", "trigger_no_data", "additional_emails", "triggered_on")
readonly_fields = ("triggered_on",)


@register(SensorAlertLog)
class SensorAlertLogAdmin(admin.ModelAdmin):
list_display = ["id", "time", "sensor", "reset", "value"]
list_filter = ["sensor", "value", "reset"]
date_hierarchy = "time"

def has_delete_permission(self, request, obj=None):
return False

def has_add_permission(self, request):
return False

def has_change_permission(self, request, obj=None):
return False
15 changes: 15 additions & 0 deletions NEMO/apps/sensors/customizations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.core.validators import validate_email

from NEMO.decorators import customization
from NEMO.views.customization import CustomizationBase


@customization(key="sensors", title="Sensor Data")
class SensorCustomization(CustomizationBase):
variables = {"sensor_default_daterange": "", "sensor_default_refresh_rate": "0", "sensor_alert_emails": ""}

def validate(self, name, value):
if name == "sensor_alert_emails":
recipients = tuple([e for e in value.split(",") if e])
for email in recipients:
validate_email(email)
Loading