Skip to content

Commit

Permalink
Merge pull request #119 from usnistgov/4.2.0.dev
Browse files Browse the repository at this point in the history
4.2.0.dev
  • Loading branch information
rptmat57 authored Sep 2, 2022
2 parents 97da0cb + c6c11dc commit 0607e64
Show file tree
Hide file tree
Showing 48 changed files with 877 additions and 145 deletions.
12 changes: 5 additions & 7 deletions Dockerfile.splash_pad
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +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/icons/ /nemo/media
COPY resources/people/ /nemo/media/
COPY resources/sounds/ /nemo/media/
COPY resources/images/ /nemo/media/
COPY resources/emails/ /nemo/media/
COPY resources/splash_pad_rates.json /nemo/media/rates.json
COPY resources/splash_pad_settings.py /nemo/
COPY resources/fixtures/splash_pad.json /nemo/
Expand Down
2 changes: 1 addition & 1 deletion NEMO/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class NEMOConfig(AppConfig):
name = "NEMO"

def ready(self):
if 'migrate' or 'makemigrations' in sys.argv:
if 'migrate' in sys.argv or 'makemigrations' in sys.argv:
return
from django.apps import apps
if apps.is_installed("django.contrib.admin"):
Expand Down
48 changes: 47 additions & 1 deletion NEMO/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
BadgeReader,
BuddyRequest,
BuddyRequestMessage,
Chemical,
ChemicalHazard,
Closure,
ClosureTime,
Comment,
Expand Down Expand Up @@ -155,7 +157,7 @@ def clean(self):
primary_owner = cleaned_data.get("_primary_owner")
image = cleaned_data.get("_image")

# only resize if an image is present and has changed
# only resize if an image is present and has changed
if image and not isinstance(image, FieldFile):
from NEMO.utilities import resize_image

Expand Down Expand Up @@ -1391,6 +1393,50 @@ class StaffAbsenceAdmin(admin.ModelAdmin):
list_filter = ("staff_member", "absence_type", "start_date", "end_date", "creation_time")


class ChemicalHazardAdminForm(forms.ModelForm):
class Meta:
model = ChemicalHazard
fields = "__all__"

chemicals = forms.ModelMultipleChoiceField(
queryset=Chemical.objects.all(),
required=False,
widget=FilteredSelectMultiple(verbose_name="Chemicals", is_stacked=False),
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.fields["chemicals"].initial = self.instance.chemical_set.all()

def clean(self):
cleaned_data = super().clean()
logo = cleaned_data.get("logo")

# only resize if a logo is present and has changed
if logo and not isinstance(logo, FieldFile):
from NEMO.utilities import resize_image

# resize image to 250x250 maximum
cleaned_data["logo"] = resize_image(logo, 250)


@register(ChemicalHazard)
class ChemicalHazardAdmin(admin.ModelAdmin):
form = ChemicalHazardAdminForm
list_display = ("name", "display_order")

def save_model(self, request, obj: ChemicalHazard, form, change):
super().save_model(request, obj, form, change)
if "chemicals" in form.changed_data:
obj.chemical_set.set(form.cleaned_data["chemicals"])


@register(Chemical)
class ChemicalAdmin(admin.ModelAdmin):
filter_horizontal = ("hazards",)


@register(EmailLog)
class EmailLogAdmin(admin.ModelAdmin):
list_display = ["id", "category", "sender", "to", "subject", "when", "ok"]
Expand Down
4 changes: 2 additions & 2 deletions NEMO/apps/kiosk/templates/kiosk/tool_information.html
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ <h2>This tool is <strong>operational</strong> and <strong>idle</strong>.</h2>
<div class="media">
<span class="glyphicon glyphicon-comment pull-left notification-icon primary-highlight"></span>
<div class="media-body">
<span class="media-middle">{{ c.content }}</span>
<span class="media-middle">{{ c.content|linebreaksbr }}</span>
<span class="media-bottom">{{ c.author }} wrote this comment on {{ c.creation_date }}</span>
</div>
</div>
Expand All @@ -188,7 +188,7 @@ <h2>This tool is <strong>operational</strong> and <strong>idle</strong>.</h2>
<div class="media">
<span class="glyphicon glyphicon-comment pull-left notification-icon primary-highlight"></span>
<div class="media-body">
<span class="media-middle">{{ c.content }}</span>
<span class="media-middle">{{ c.content|linebreaksbr }}</span>
<span class="media-bottom">{{ c.author }} wrote this comment on {{ c.creation_date }}</span>
</div>
</div>
Expand Down
25 changes: 12 additions & 13 deletions NEMO/apps/sensors/evaluators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ast
import operator
from _ast import BinOp, BoolOp, Call, Compare, Index, Name, NameConstant, Num, Slice, Subscript, UnaryOp
from math import ceil, floor, sqrt, trunc

from pymodbus.constants import Endian
Expand Down Expand Up @@ -38,27 +37,27 @@ class BasicEvaluatorVisitor(ast.NodeVisitor):
def __init__(self, **kwargs):
self._variables = kwargs

def visit_Name(self, node: Name):
def visit_Name(self, node: ast.Name):
if node.id in self._variables:
return self._variables[node.id]
else:
raise AttributeError(f"Variable not found: {node.id}")

def visit_Num(self, node: Num):
def visit_Num(self, node: ast.Num):
return node.n

def visit_NameConstant(self, node: NameConstant):
def visit_NameConstant(self, node: ast.NameConstant):
return node.value

def visit_UnaryOp(self, node: UnaryOp):
def visit_UnaryOp(self, node: ast.UnaryOp):
val = self.visit(node.operand)
op = type(node.op)
if op in self.operators:
return self.operators[op](val)
else:
raise TypeError(f"Unsupported operation: {op.__name__}")

def visit_BinOp(self, node: BinOp):
def visit_BinOp(self, node: ast.BinOp):
lhs = self.visit(node.left)
rhs = self.visit(node.right)
op = type(node.op)
Expand All @@ -67,19 +66,19 @@ def visit_BinOp(self, node: BinOp):
else:
raise TypeError(f"Unsupported operation: {op.__name__}")

def visit_Subscript(self, node: Subscript):
def visit_Subscript(self, node: ast.Subscript):
val = self.visit(node.value)
index = self.visit(node.slice)
try:
return val[index]
except AttributeError:
return self.generic_visit(node)

def visit_Index(self, node: Index, **kwargs):
def visit_Index(self, node: ast.Index, **kwargs):
"""df.index[4]"""
return self.visit(node.value)

def visit_Slice(self, node: Slice):
def visit_Slice(self, node: ast.Slice):
lower = node.lower
if lower is not None:
lower = self.visit(lower)
Expand All @@ -92,7 +91,7 @@ def visit_Slice(self, node: Slice):

return slice(lower, upper, step)

def visit_Call(self, node: Call):
def visit_Call(self, node: ast.Call):
if node.func.id in self.functions:
new_args = [self.visit(arg) for arg in node.args]
return self.functions[node.func.id](*new_args)
Expand Down Expand Up @@ -135,7 +134,7 @@ def modbus_function(registers):
# noinspection PyTypeChecker
class ModbusEvaluatorVisitor(BasicEvaluatorVisitor):
# Extension of the basic evaluator with additional modbus specific functions
def visit_Call(self, node: Call):
def visit_Call(self, node: ast.Call):
if node.func.id in self.functions:
return super().visit_Call(node)
elif node.func.id in modbus_functions:
Expand Down Expand Up @@ -164,14 +163,14 @@ class BooleanEvaluatorVisitor(BasicEvaluatorVisitor):
def visit_bool(self, node: bool):
return node

def visit_BoolOp(self, node: BoolOp):
def visit_BoolOp(self, node: ast.BoolOp):
if isinstance(node.op, (ast.And, ast.Or)):
values = map(self.visit, node.values)
return all(values) if isinstance(node.op, ast.And) else any(values)
else:
return self.generic_visit(self, node)

def visit_Compare(self, node: Compare, **kwargs):
def visit_Compare(self, node: ast.Compare, **kwargs):
# base case: we have something like a CMP b
if len(node.comparators) == 1:
bin_op = ast.BinOp(op=node.ops[0], left=node.left, right=node.comparators[0])
Expand Down
6 changes: 3 additions & 3 deletions NEMO/interlocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ def setRelayState(cls, interlock: Interlock_model, state: {0, 1}) -> Interlock_m
if interlock.card.username and interlock.card.password:
auth = (interlock.card.username, interlock.card.password)
for state_xml_name in cls.state_xml_names:
url = f"{interlock.card.server}:{interlock.card.port}/{state_xml_name}?{cls.state_parameter_template.format(interlock.channel)}={state}"
url = f"{interlock.card.server}:{interlock.card.port}/{state_xml_name}?{cls.state_parameter_template.format(interlock.channel or '')}={state}"
if not url.startswith("http") and not url.startswith("https"):
url = "http://" + url
response = requests.get(url, auth=auth)
Expand All @@ -327,7 +327,7 @@ def setRelayState(cls, interlock: Interlock_model, state: {0, 1}) -> Interlock_m
state = None
# Try with a few different lookups here since depending on the relay model, it could be relayX or relayXstate
for state_suffix in cls.state_response_suffixes:
element = responseXML.find(cls.state_parameter_template.format(interlock.channel) + state_suffix)
element = responseXML.find(cls.state_parameter_template.format(interlock.channel or '') + state_suffix)
# Explicitly check for None since 0 is a valid state to return
if element is not None:
state = int(element.text)
Expand Down Expand Up @@ -360,7 +360,7 @@ class ModbusTcpInterlock(Interlock):
def clean_interlock(self, interlock_form: InterlockAdminForm):
channel = interlock_form.cleaned_data["channel"]
error = {}
if not channel:
if channel is None:
error["channel"] = _("This field is required.")
if error:
raise ValidationError(error)
Expand Down
4 changes: 2 additions & 2 deletions NEMO/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.conf import settings
from django.contrib.auth.middleware import RemoteUserMiddleware
from django.http import HttpResponseForbidden
from django.shortcuts import redirect
from django.urls import NoReverseMatch, reverse
from django.utils.deprecation import MiddlewareMixin

Expand Down Expand Up @@ -39,8 +40,7 @@ def process_request(self, request):
from NEMO.views.authentication import all_auth_backends_are_pre_auth
# Only raise error if all we have are pre_authentication backends and they failed
if all_auth_backends_are_pre_auth():
from NEMO.views.authentication import authorization_failed
return authorization_failed(request)
return redirect('authorization_failed')


class HTTPHeaderAuthenticationMiddleware(RemoteUserAuthenticationMiddleware):
Expand Down
56 changes: 56 additions & 0 deletions NEMO/migrations/0040_version_4_2_0.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Generated by Django 3.2.13 on 2022-06-15 19:37

from django.db import migrations, models

import NEMO.utilities
from NEMO.migrations_utils import create_news_for_version


class Migration(migrations.Migration):

dependencies = [
('NEMO', '0039_version_4_1_0'),
]

def new_version_news(apps, schema_editor):
create_news_for_version(apps, "4.2.0", "")

operations = [
migrations.RunPython(new_version_news),
migrations.AddField(
model_name='staffabsence',
name='manager_note',
field=models.TextField(blank=True, help_text='A note only visible to managers.', null=True),
),
migrations.AlterField(
model_name='staffabsence',
name='description',
field=models.TextField(blank=True, help_text='The absence description. This will be visible to anyone.', null=True),
),
migrations.CreateModel(
name='ChemicalHazard',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('display_order', models.IntegerField(help_text="Chemical hazards are sorted according to display order. The lowest value category is displayed first in the 'Safety data sheet' page.")),
('logo', models.ImageField(blank=True, help_text='The logo for this hazard', upload_to=NEMO.utilities.get_hazard_logo_filename)),
],
options={
'ordering': ['display_order', 'name'],
},
),
migrations.CreateModel(
name='Chemical',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('document', models.FileField(blank=True, null=True, upload_to=NEMO.utilities.get_chemical_document_filename)),
('url', models.CharField(blank=True, max_length=200, null=True, verbose_name='URL')),
('keywords', models.TextField(blank=True, null=True)),
('hazards', models.ManyToManyField(blank=True, help_text='Select the hazards for this chemical.', to='NEMO.ChemicalHazard')),
],
options={
'ordering': ['name'],
},
),
]
Loading

0 comments on commit 0607e64

Please sign in to comment.