...
 
Commits (47)
# - Create for each LOFAR package a variable containing the absolute path to
# its source directory.
#
# Generated by gen_LofarPackageList_cmake.sh at Wed May 29 15:45:16 CEST 2019
# Generated by gen_LofarPackageList_cmake.sh at Fri 13 Sep 2019 09:27:12 AM CEST
#
# ---- DO NOT EDIT ----
#
......
......@@ -5,4 +5,3 @@ lofar_package(Maintenance DEPENDS DBInterface MDB_tools MDB_WebView)
lofar_add_package(DBInterface)
lofar_add_package(MDB_tools)
lofar_add_package(MDB_WebView)
......@@ -19,12 +19,15 @@ find_python_module(django_filters REQUIRED)
# includes every python file excepts for the manage.py
FILE(GLOB_RECURSE PY_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ./*.py)
foreach(LIST_ITEM ${PY_FILES})
MESSAGE(STATUS ${LIST_ITEM})
endforeach(LIST_ITEM ${PY_FILES})
add_subdirectory(bin)
add_subdirectory(test)
add_subdirectory(monitoringdb/templates)
python_install(${PY_FILES} DESTINATION lofar/maintenance)
......
......@@ -36,6 +36,7 @@ ALLOWED_HOSTS = ['lofarmonitortest.control.lofar',
'lofarmonitor.control.lofar',
'stationmonitor.control.lofar',
'localhost',
'*',
'127.0.0.1']
......@@ -50,9 +51,11 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'rest_framework',
'django_filters',
'crispy_forms',
'lofar.maintenance.monitoringdb.apps.MonitoringDbConfig',
]
CRISPY_TEMPLATE_PACK = 'bootstrap4'
MIDDLEWARE = [
# 'silk.middleware.SilkyMiddleware',
'django.middleware.security.SecurityMiddleware',
......@@ -67,10 +70,13 @@ MIDDLEWARE = [
ROOT_URLCONF = 'lofar.maintenance.django_postgresql.urls'
# SILKY_PYTHON_PROFILER = True
# SILKY_PYTHON_PROFILER_BINARY = True
BASE_TEMPLATE_DIR = 'var/www/templates'
BASE_TEMPLATE_DIR = os.environ.get('MDB_TEMPLATE_DIR', BASE_TEMPLATE_DIR)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_TEMPLATE_DIR],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
......@@ -208,6 +214,9 @@ REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
}
LOGIN_REDIRECT_URL = '/maintenance'
LOGOUT_REDIRECT_URL = '/maintenance'
CELERY_BROKER_URL= 'amqp://guest@rabbitmq-broker//'
CELERY_RESULT_BACKEND = 'amqp://guest@rabbitmq-broker//'
......
......@@ -14,12 +14,18 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf.urls import include, url
from django.conf.urls import include
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from lofar.maintenance.monitoringdb.urls import urlpatterns as urls
urlpatterns = [] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')),] + static(settings.STATIC_URL,
document_root=settings.STATIC_ROOT)
urlpatterns += urls
# Generated by Django 2.2.1 on 2019-07-22 09:52
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('monitoringdb', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Failures',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='RepairActionType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='ReplacedPart',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.CharField(default=None, max_length=1000, null=True)),
('old_part_id', models.CharField(default=None, max_length=100, null=True)),
('new_part_id', models.CharField(default=None, max_length=100, null=True)),
],
),
migrations.CreateModel(
name='RepairAction',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('when', models.DateTimeField(auto_now=True)),
('component_type', models.CharField(max_length=100)),
('comment', models.TextField(default='')),
('affected_components', models.ManyToManyField(to='monitoringdb.Component')),
('affected_elements', models.ManyToManyField(to='monitoringdb.Element')),
('failure', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='monitoringdb.Failures')),
('replaced_parts', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='monitoringdb.ReplacedPart')),
('station', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='monitoringdb.Station')),
('type_of_repair', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='monitoringdb.RepairActionType')),
],
options={
'ordering': ['when'],
},
),
]
from django.db.models import Model, ForeignKey, DateTimeField, TextField, DO_NOTHING, ManyToManyField, CharField
from .station import Station
from .component import Component
from .element import Element
from datetime import datetime
class Failures(Model):
class Meta:
ordering = ['name']
name = CharField(max_length=100, unique=True)
class RepairActionType(Model):
class Meta:
ordering = ['name']
name = CharField(max_length=100, unique=True)
class ReplacedPart(Model):
description = CharField(max_length=1000, null=True, default=None)
old_part_id = CharField(max_length=100, null=True, default=None)
new_part_id = CharField(max_length=100, null=True, default=None)
class RepairAction(Model):
class Meta:
ordering = ['when']
station = ForeignKey(Station, on_delete=DO_NOTHING)
when = DateTimeField(default=datetime.now)
component_type = CharField(max_length=100)
affected_components = ManyToManyField(Component)
affected_elements = ManyToManyField(Element)
failure = ForeignKey(Failures, on_delete=DO_NOTHING)
type_of_repair = ForeignKey(RepairActionType, on_delete=DO_NOTHING)
replaced_parts = ForeignKey(ReplacedPart, on_delete=DO_NOTHING, null=True)
comment = TextField(default="")
\ No newline at end of file
from rest_framework.serializers import ModelSerializer, Serializer
import rest_framework.serializers as serializers
from lofar.maintenance.monitoringdb.models.repairs import RepairAction, ReplacedPart, Failures,\
RepairActionType
from lofar.maintenance.monitoringdb.serializers.station import StationSerializer
from lofar.maintenance.monitoringdb.serializers.component import ComponentSerializer
from lofar.maintenance.monitoringdb.serializers.element import ElementSerializer
class ReplacedPartSerializer(ModelSerializer):
class Meta:
model = ReplacedPart
fields = '__all__'
class FailuresSerializer(ModelSerializer):
class Meta:
model = Failures
fields = '__all__'
class RepairActionTypeSerializer(ModelSerializer):
class Meta:
model = RepairActionType
fields = '__all__'
class RepairActionSerializer(ModelSerializer):
station = StationSerializer()
affected_components = ComponentSerializer(many=True)
affected_elements = ElementSerializer(many=True)
failure = FailuresSerializer()
type_of_repair = RepairActionTypeSerializer()
replaced_parts = ReplacedPartSerializer(many=True)
class Meta:
model = RepairAction
fields = '__all__'
class InsertRepairSerializer(Serializer):
station_name = serializers.CharField()
when_date = serializers.DateField()
when_time = serializers.TimeField()
component_type = serializers.CharField()
component_id = serializers.ListField(serializers.IntegerField(required=False), allow_empty=True)
element_id = serializers.ListField(serializers.IntegerField(required=False), allow_empty=True)
failure = serializers.CharField()
action_type = serializers.CharField(default="Unknown")
comment = serializers.CharField(required=False, allow_blank=True)
old_component_id = serializers.CharField(required=False)
new_component_id = serializers.CharField(required=False)
MACRO(add_django_template path_in installation_path)
file(GLOB_RECURSE template_files RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" ${path_in}/*.html)
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory ${installation_path})
foreach(file ${template_files})
message(STATUS "linking template file for local testing: " ${installation_path}/${file})
get_filename_component(filename ${file} NAME)
get_filename_component(directory ${file} DIRECTORY)
message(STATUS "subdir, file : " ${directory} ", " ${file})
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory
${PROJECT_BINARY_DIR}/${installation_path}/${directory})
message(STATUS "paths - " ${file})
execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink
${CMAKE_CURRENT_SOURCE_DIR}/${file} ${PROJECT_BINARY_DIR}/${installation_path}/${file})
install(FILES ${file} DESTINATION ${installation_path}/${directory})
endforeach(file)
ENDMACRO()
add_django_template(. var/www/templates)
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>LOFAR Maintenance</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<style>
.main-navbar{
background-color: #1c3e5c;
padding: .3rem 1rem;
color: #fff;
}
.login-text {
text-align: center;
margin-top: auto;
margin-bottom: auto;
}
</style>
</head>
<body>
<nav class="main-navbar navbar navbar-expand-lg navbar-dark" >
<nav class="navbar-brand">
<h1><em style="font-weight: 700; font-style: normal;">LOFAR</em> maintenance</h1>
</nav>
</nav>
{% load model_utils %}
{% if user.is_authenticated %}
<div class="row" style="margin-bottom=.5em;">
<label class="col-2 login-text"> Hi {{ user.username }}!
<a href="{% url 'logout' %}" class="col-2">logout</a>
</label>
<a href="{% url 'maintenance-home' %}" class="btn btn-primary col-2" style="margin: .3em;">Dashboard</a>
{% if user|has_group:'maintenance' %}
<a href="{% url 'insert_repair_action' %}" class="btn btn-primary col-2" style="margin: .3em;">Insert new repair action</a>
<a href="{% url 'insert_excel' %}" class="btn btn-primary col-2" style="margin: .3em;" >Import maintenance sheet</a>
{% endif %}
</div>
{% else %}
<div class="row" style="margin:.3em;">
<div class="col-2 login-text" >You are not logged in</div>
<a class="btn btn-primary" href="{% url 'login' %}" style="margin:.2em">login</a>
<a class="btn btn-secondary" href="{% url 'signup' %}" style="margin:.2em">sign up</a>
</div>
{% endif %}
<main>
{% block content %}
{% endblock %}
</main>
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
</body>
</html>
\ No newline at end of file
<!-- templates/logout/logout.html -->
{% extends 'base.html' %}
{% block content %}
<center>
<h2>Logout</h2>
{{ context }}
</center>
{% endblock content %}
\ No newline at end of file
{% extends 'base.html' %}
{% load model_utils %}
{% block content %}
<div class='col-sm-6 col-sm-offset-3'>
{% if request.user|has_group:"maintenance"%}
<p>Are you sure you want to delete this object?</p>
<div class="col-12" style="margin:auto;">
<table class="table table-striped table-bordered">
<thead class="thead-dark">
<tr>
<th>id</th>
<th>Station</th>
<th>Date</th>
<th>Failure</th>
<th>Action</th>
<th>Component</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ object.pk }}</td>
<td>{{ object.station.name }}</td>
<td>{{ object.when| date:"d M Y H:i" }}</td>
<td>{{ object.failure.name }}</td>
<td>{{ object.type_of_repair.name }}</td>
<td>{{ object.component_type }}</td>
</tr>
</tbody>
</table>
</div>
<form action="" method="POST">
{% csrf_token %}
<input type="submit" class='btn btn-danger' value="Delete"/>
<input type="button" class='btn btn-secondary' value="Cancel" onclick="window.location='{% url 'maintenance-home' %}'"/>
</form>
{% else %}
<h4>You're not allowed to be here.</h4>
<a href="{% url 'maintenance-home' %}">Dashboard</a>
{% endif %}
</div>
{% endblock %}
\ No newline at end of file
<!-- templates/maintenance/home.html -->
{% extends 'base.html' %}
{% block content %}
{% load model_utils %}
<div class="col-12" style="margin:auto;">
<table class="table table-striped table-bordered">
<thead class="thead-dark">
<tr>
<th>id</th>
<th>Station</th>
<th>Date</th>
<th>Failure</th>
<th>Action</th>
<th>Component</th>
<th></th>
{% if user|has_group:'maintenance' %}
<th></th>
{% endif %}
</tr>
</thead>
{% for repair_action in object_list %}
<tr>
<td>{{ repair_action.pk }}</td>
<td>{{ repair_action.station.name }}</td>
<td>{{ repair_action.when| date:"d M Y H:i" }}</td>
<td>{{ repair_action.failure.name }}</td>
<td>{{ repair_action.type_of_repair.name }}</td>
<td>{{ repair_action.component_type }}</td>
<td><a class="btn btn-primary" href="{% url 'repair-edit' repair_action.pk %}">?</a></td>
{% if user|has_group:'maintenance' %}
<td><a class="btn btn-danger" href="{% url 'repair-remove' repair_action.pk %}">X</a></td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
<div style="margin:auto;" class="row">
{% if is_paginated %}
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="btn">
<span><a href="?page={{ page_obj.previous_page_number }}">Previous</a></span>
</li>
{% endif %}
<li class="btn">
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.</span>
</li>
{% if page_obj.has_next %}
<li>
<span class="btn"><a href="?page={{ page_obj.next_page_number }}">Next</a></span>
</li>
{% endif %}
</ul>
{% endif %}
</div>
{% endblock content %}
\ No newline at end of file
<!-- templates/maintenance/insert_excel.html -->
{% extends 'base.html' %}
{% load model_utils %}
{% block content %}
<link href="https://unpkg.com/tabulator-tables@4.4.1/dist/css/bootstrap/tabulator_bootstrap4.min.css" rel="stylesheet">
<script type="text/javascript" src="https://unpkg.com/tabulator-tables@4.4.1/dist/js/tabulator.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
{{ form }}
{% if request.user|has_group:"maintenance" %}
<input type="submit" class="btn btn-primary" value="Insert"/>
{% endif %}
</form>
<div class="col-12" style="margin:auto;margin-top: 2em;"><table id="invalidlines"></table></div>
<script type="text/javascript">
var invalid_lines= {{ invalid_lines | safe }}
</script>
<script>
console.log("data", invalid_lines)
var table = new Tabulator("#invalidlines", {
data: invalid_lines,
height: 400,
columns : [
{title:"Station name", field:"station_name", editor:"input"},
{title:"Component Type", field:"component_type", editor:"input"},
{title:"Component Id", field:"component_id", editor:"input"},
{title:"Element Id", field:"element_id", editor:"input"},
{title:"Date", field:"when_date", editor:"input"},
{title:"Failure", field:"failure", editor:"input"},
{title:"Action", field:"action_type", editor:"input"},
{title:"Comment", field: "comment", editor:"input"},
{title:"Errors", field:"error_message", editor:"input"}
]
});
table.setData(invalid_lines);
</script>
<script>
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
function update(){
data = table.getData()
axios.post('/api/repairs/raw/insert_bulk', data).then((response) => {
if(response.data.length > 0){
table.setData(response.data)
}else{
window.location.href = '/maintenance'
}
})
}
</script>
<div class="d-flex justify-content-center" style="margin-top:3em">
{% if request.user|has_group:"maintenance" and invalid_lines %}
<button class="btn btn-primary col-2" onclick="update()">Update</button>
{% endif %}
</div>
{% endblock content %}
\ No newline at end of file
<!-- templates/maintenance/insert_repair_action.html -->
{% extends 'base.html' %}
{% block component_ids %}
{% for component in object.affected_components.all %} {{ component.component_id }} {% endfor %}
{% endblock %}
{% load model_utils %}
{% block content %}
<form style="width:80%;margin:auto;" method="post">
{% csrf_token %}
<div class="form-group row">
<label for="station_select" class="col-sm-2 col-form-label">Station</label>
<div class="col-sm-10">
<select id="station_select" class="form-control" name="station_name" {% can_edit_if_has_group "maintenance" %}>
<option selected value=""> select station </option>
{% for station in station_names %}
{% if station in object.station.name %}
<option value="{{station}}" selected>{{station}}</option>
{% else %}
<option value="{{station}}">{{station}}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group row">
<label for="datetime_select" class="col-sm-2 col-form-label">When</label>
<div class="col" id="datetime_select">
<input class="form-control" type="date" name="when_date" max="3000-12-31" value="{{ object.when|date:'Y-m-d' }}"
{% can_edit_if_has_group "maintenance" %}/>
</div>
<div class="col">
<input class="form-control" type="time" name="when_time" value="{{ object.when|date:'H:i' }}" {% can_edit_if_has_group "maintenance" %}/>
</div>
</div>
<div class="form-group row">
<label for="component_select" class="col-sm-2 col-form-label">Affected component</label>
<div class="col-sm-2" id="component_select">
<datalist id="component_types">
{% for component_type in component_types %}
<option>{{component_type}}</option>
{% endfor %}
</datalist>
<input class="form-control" type="text" list="component_types" id="component_type" name="component_type"
placeholder="component type" required value="{{ object.component_type }}" {% can_edit_if_has_group "maintenance" %} >
</div>
<input class="form-control col-sm-1" type="text" id="{{component.id}}" placeholder="id"
name="component_id" value="{{ object.affected_components.all|value_list:'component_id'|join:', ' }}" {% can_edit_if_has_group "maintenance" %}/>
<input class="form-control col-sm-1" type="text" id="element_id" style="display:none;"
value="{{ object.affected_components.all|value_list:'component_id'|join:', ' }}"
name="element_id" placeholder="element" {% can_edit_if_has_group "maintenance" %}/>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="failure">Error type</label>
<div class="col">
<datalist id="failure_type_list">
{% for failure_type in failure_types %}
<option>{{failure_type}}</option>
{% endfor %}
</datalist>
<input class="form-control" type="text" list="failure_type_list" placeholder="enter failure type here" id="failure" name="failure"
value="{{ object.failure.name }}" {% can_edit_if_has_group "maintenance" %}>
</div>
</div>
<div class="form-group row">
<label for="type_of_repair_row" class="col-sm-2 col-form-label">Repair action</label>
<div class="col-sm-2" id="type_of_repair_row">
<datalist id="type_of_repair_list">
{% for repair_type in repair_types %}
<option>{{repair_type}}</option>
{% endfor %}
</datalist>
<input class="form-control" type="text" list="type_of_repair_list" id="action_type"
name="action_type" value="{{ object.type_of_repair.name }}"
placeholder="action type" required {% can_edit_if_has_group "maintenance" %}>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="comment">Comment</label>
<div class="col">
<input class="form-control" type="text" placeholder="insert your comment here" id="comment"
value="{{ object.comment }}" name="comment" {% can_edit_if_has_group "maintenance" %}>
</div>
</div>
{% if user|has_group:'maintenance' %}
<input type="submit" class="btn btn-primary" value="Insert"/>
{% endif %}
</form>
{{ form.errors }}
{{ form.non_field_errors }}
<script>
change_element_id_selection_visibility = function () {
if( $("#component_type").val() == "HBA" ){
$("#element_id").show();
}else{
$("#element_id").hide();
$("#element_id").value = ''
}
}
$(document).ready(change_element_id_selection_visibility)
$("#component_type").change(change_element_id_selection_visibility);
</script>
{% endblock content %}
\ No newline at end of file
<!-- templates/registration/login.html -->
{% extends 'base.html' %}
{% block content %}
<center>
<h2>Login</h2>
<form method="post">
{% csrf_token %}
<form>
{{ form.as_p }}
<button class="btn btn-primary" type="submit">Login</button>
</form>
</center>
{% endblock content %}
\ No newline at end of file
<!-- templates/signup.html -->
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Sign Up{% endblock %}
{% block content %}
<div class="col-10" style="margin:auto">
<h2 class="col-2" style="margin:auto">Sign up</h2>
<form method="post">
{% csrf_token %}
{{ form | crispy }}
<button type="submit">Sign up</button>
</form>
</div>
{% endblock %}
from django import template
register = template.Library()
@register.filter(name='value_list')
def value_list(value=None, arg=None):
if value and hasattr(value, '__iter__'):
return [getattr(item, arg) for item in value]
else:
return ''
@register.filter(name='has_group')
def has_group(user, group_name):
return user.groups.filter(name=group_name).exists()
@register.simple_tag(takes_context=True)
def can_edit_if_has_group(context, group_name):
if not has_group(context.request.user, group_name):
return 'readonly'
else:
return ''
from django.conf.urls import url, include
from django.urls import path
from rest_framework import routers
from rest_framework.documentation import include_docs_urls
......@@ -7,10 +8,30 @@ from .views.controllers import *
from .views.logs_view import *
from .views.rtsm_views import *
from .views.station_test_views import *
from .views.repairs_view import *
from .views.users import *
log_router = routers.DefaultRouter()
log_router.register(r'action_log', ActionLogViewSet)
# Repairs Frontend
repairs_url = [
path(r'maintenance', MaintenanceHome.as_view(), name='maintenance-home'),
path(r'maintenance/insert', InsertRepair.as_view(), name='insert_repair_action'),
path(r'maintenance/insert_excel', InsertRepairExcel.as_view(), name='insert_excel'),
path(r'maintenance/edit/<pk>', RepairDetail.as_view(), name='repair-edit'),
path(r'maintenance/remove/<pk>', RepairActionRemove.as_view(), name='repair-remove')
]
# Repairs
repairs_router = routers.DefaultRouter()
repairs_router.register(r'repairs_actions', RepairActionView)
repairs_router.register(r'failures', FailuresView)
repairs_router.register(r'replaced_parts', ReplacedPartView)
repairs_router.register(r'repair_actions_type', RepairActionTypeView)
# WinCC
wincc_router = routers.DefaultRouter()
wincc_router.register(r'antenna_status', WinCCAntennaStatusViewSet)
......@@ -38,14 +59,14 @@ rtsm_router.register(r'error_summary_plot', RTSMSummaryPlot, base_name='rtsm-sum
rtsm_router.register(r'', RTSMObservationViewSet)
urlpatterns = [
url(r'^api/stationtests/', include(station_test_router.urls)),
url(r'^api/rtsm/', include(rtsm_router.urls)),
url(r'^api/wincc/', include(wincc_router.urls)),
url(r'^api/repairs/', include(repairs_router.urls)),
url(r'^api/repairs/raw/insert$', insert_raw_repair),
url(r'^api/repairs/raw/insert_bulk$', insert_raw_repair_bulk),
url(r'^api/log/', include(log_router.urls)),
url(r'^api/api-auth', include('rest_framework.urls', namespace='rest_framework')),
url(r'^api/api-auth/', include('rest_framework.urls')),
url(r'^api/stationtests/raw/insert', insert_raw_station_test),
url(r'^api/rtsm/raw/insert', insert_raw_rtsm_test),
url(r'^api/view/ctrl_stationoverview', ControllerStationOverview.as_view()),
......@@ -56,5 +77,7 @@ urlpatterns = [
url(r'^api/view/ctrl_station_component_errors', ControllerStationComponentErrors.as_view()),
url(r'^api/view/ctrl_station_component_element_errors',
ControllerStationComponentElementErrors.as_view()),
url(r'^api/docs', include_docs_urls(title='Monitoring DB API'))
]
url(r'^api/view/ctrl_list_observations', ControllerListObservations.as_view()),
url(r'^api/docs', include_docs_urls(title='Monitoring DB API')),
path('signup/', SignUp.as_view(), name='signup')
] + repairs_url
......@@ -469,6 +469,123 @@ class ControllerStationTestsSummary(ValidableReadOnlyView):
return Response(status=status.HTTP_200_OK, data=response_payload)
class ControllerListObservations(ValidableReadOnlyView):
description = "Overview of the observations performed on the stations for a given range from_date, to_date"
station_group = 'A'
errors_only = 'true'
error_types = []
fields = [
coreapi.Field(
"station_group",
required=False,
location='query',
schema=coreschema.Enum(['C', 'R', 'I', 'A'], description=
'Station group to select for choices are [C|R|I|A]',
)
),
coreapi.Field(
"errors_only",
required=False,
location='query',
type=parse_bool,
schema=coreschema.Boolean(
description='displays or not only the station with more than one error')
),
coreapi.Field(
"from_date",
required=True,
location='query',
schema=coreschema.String(
description='select rtsm from date (ex. YYYY-MM-DD)')
),
coreapi.Field(
"to_date",
required=True,
location='query',
schema=coreschema.String(
description='select rtsm from date (ex. YYYY-MM-DD)')
),
coreapi.Field(
"error_types",
required=False,
location='query',
type=parse_array,
schema=coreschema.Array(description='select the error types to filter for',
items=coreschema.Enum(_get_unique_error_types()),
unique_items=True)
)
]
def compute_response(self):
self.from_date = parse_date(self.from_date)
self.to_date = parse_date(self.to_date)
filtered_entities = RTSMObservation.objects \
.filter(start_datetime__gte=self.from_date).filter(end_datetime__lte=self.to_date)
if self.station_group != 'A':
filtered_entities = filtered_entities \
.filter(station__type=self.station_group)
if self.errors_only:
filtered_entities = filtered_entities.exclude(errors__isnull=True)
errors_summary = filtered_entities \
.values('sas_id',
'station__name',
'start_datetime',
'end_datetime',
'errors__error_type',
'errors__mode') \
.annotate(total=Count('errors__error_type')) \
.order_by('sas_id', 'station__name')
if self.error_types:
errors_summary = errors_summary.filter(errors__error_type__in=self.error_types)
response = dict()
for error_summary in errors_summary:
observation_id = error_summary['sas_id']
station_name = error_summary['station__name']
start_datetime = error_summary['start_datetime']
end_datetime = error_summary['end_datetime']
mode = error_summary['errors__mode']
error_type = error_summary['errors__error_type']
total = error_summary['total']
if observation_id not in response:
response[observation_id] = OrderedDict()
response[observation_id]['observation_id'] = observation_id
response[observation_id]['start_datetime'] = start_datetime
response[observation_id]['end_datetime'] = end_datetime
response[observation_id]['total_component_errors'] = 0
response[observation_id]['mode'] = list()
response[observation_id]['station_involved'] = dict()
if total == 0:
continue
response[observation_id]['total_component_errors'] += total
station_involved_summary = response[observation_id]['station_involved']
response[observation_id]['mode'] += [mode] \
if mode not in response[observation_id]['mode'] else []
if station_name not in station_involved_summary:
station_involved_summary[station_name] = OrderedDict()
station_involved_summary[station_name]['station_name'] = station_name
station_involved_summary[station_name]['n_errors'] = 0
station_involved_summary[station_name]['component_error_summary'] = OrderedDict()
station_involved_summary[station_name]['n_errors'] += total
station_involved_summary[station_name]['component_error_summary'][error_type] = total
response_payload = sorted(response.values(),
key=lambda item: item['start_datetime'],
reverse=True)
return Response(status=status.HTTP_200_OK, data=response_payload)
class ControllerLatestObservations(ValidableReadOnlyView):
description = "Overview of the latest observations performed on the stations"
......@@ -945,7 +1062,7 @@ class ControllerStationComponentErrors(ValidableReadOnlyView):
rcu_id=component_error.rcu,
polarization=polarization,
details=dict(
url=url_to_plot,
plot_id=component_error.pk,
component_id=component_id,
percentage=component_error.percentage * 100.,
n_samples=component_error.observation.samples,
......@@ -1232,9 +1349,9 @@ class ControllerStationComponentElementErrors(ValidableReadOnlyView):
samples=samples,
percentage=percentage * 100.,
count=count,
plot_id=item['pk'],
mode=mode,
rcu=rcu,
url=url_to_plot)
rcu=rcu)
return list(errors.values())
def compute_station_tests_error_list(self):
......
This diff is collapsed.
......@@ -100,14 +100,17 @@ def insert_raw_rtsm_test(request: HttpRequest):
logs_pk += [handle_rtsm_insert(content, remote_addr)]
elif request.FILES:
files = request.FILES
for station_name, file in zip(request.data['station_name'], files.values()):
for file in files.values():
station_name = file.name.split('_')[0]
content_data = file.read().decode('UTF-8')
if not is_rtsm_test(content_data):
return Response(exception=True,
data="the attached file is not a RTSM %s" % file.name,
data="the attached file is not a RTSM %s" % file.name,
status=status.HTTP_400_BAD_REQUEST)
content = dict(station_name=request.data['station_name'],
content = dict(station_name=station_name,
content=content_data)
logs_pk += [handle_rtsm_insert(content, remote_addr)]
else:
......
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.views import generic
class SignUp(generic.CreateView):
form_class = UserCreationForm
success_url = reverse_lazy('login')
template_name = 'registration/signup.html'
\ No newline at end of file
......@@ -12,5 +12,6 @@ RUN pip3 install beautifultable==0.7.0 \
django-filter==2.1 \
inotify==0.2.10 \
matplotlib==3.1.0 \
requests==2.22
requests==2.22 \
django-crispy-forms==1.7.2 \
pandas==0.23.4
\ No newline at end of file
......@@ -6,6 +6,22 @@ server{
root /opt/lofar/share/www;
proxy_cache_revalidate on;
}
location /admin {
proxy_pass http://lofar-maintenance-restservice:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on;
}
location /accounts {
proxy_pass http://lofar-maintenance-restservice:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on;
}
location /api {
proxy_read_timeout 6000s;
proxy_pass http://lofar-maintenance-restservice:8000;
......
......@@ -11,6 +11,7 @@ RUN mkdir -p ~/.lofar/dbcredentials && echo "[database:mdb]" >> ~/.lofar/dbcrede
COPY ./entrypoint.sh /root/
SHELL ["/bin/sh", "-c"]
ENV MDB_TEMPLATE_DIR=/opt/lofar/var/www/templates/
ENTRYPOINT /root/entrypoint.sh
......
......@@ -3,6 +3,7 @@
"version": "0.1.0",
"description": "WebPage meant to display the content of the maintenance db in the web browser,",
"proxy": "http://localhost:8000",
"homepage": "/lofmonitor",
"scripts": {
"flow": "flow",
"build-css": "node-sass-chokidar src/ -o src/",
......
var RESTAPI = {
api_url:'/api',
entity_map: {
station : 'stationtests/station',
station_test: 'stationtests/station_test'
},
format: '?format=json',
composeBaseUrl(entity_name){
return this.api_url + '/' + this.entity_map[entity_name] + '/' + this.format
},
composeQuery(parameters){
let query_string = ''
for (let key in parameters){
if (parameters[key] !== ''){
query_string += '&' + key + '=' + parameters[key]
}
}
return query_string
},
get_url(entity_name, page, parameters){
parameters['page'] = page;
return this.composeBaseUrl(entity_name) +
this.composeQuery(parameters);
}
}
export default RESTAPI;
......@@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import * as serviceWorker from './registerServiceWorker';
// Redux
import { Provider } from "react-redux";
......@@ -16,4 +16,4 @@ ReactDOM.render(
document.getElementById('root')
);
registerServiceWorker();
serviceWorker.unregister();
......@@ -11,9 +11,9 @@ import {Responsive, WidthProvider} from 'react-grid-layout';
import * as moment from 'moment';
import { connect } from "react-redux";
import { setNewLayout } from "redux/actions/landingPageActions";
import { composeQueryString } from 'utils/utils.js';
import { createGridPanel } from 'utils/grid.js';
import { Toolbar, StationGroupSelector, ErrorTypesSelector, ErrorsOnlySelector, PeriodSelector } from 'components/Toolbar'
import RESTAPI from 'utils/api_configuration'
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
......@@ -28,24 +28,20 @@ const ResponsiveGridLayout = WidthProvider(Responsive);
class LandingPageC extends Component {
getStationOverviewURL() {
const url = '/api/view/ctrl_stationoverview';
const parametersString = composeQueryString({
let parameters = {
format: 'json',
station_group: this.props.selectedStationGroup,
errors_only: this.props.errorsOnly,
n_station_tests: 4,
n_rtsm: 6,
error_types: this.props.selectedErrorTypes
});
};
return `${url}?${parametersString}`;
return RESTAPI.composeApiUrl('ctrl_stationoverview', parameters);
}
getStationTestSummaryURL() {
const url = '/api/view/ctrl_stationtestsummary';
const parametersString = composeQueryString({
const parameters = {
// ---- Mandatory parameters
format: 'json',
lookback_time: this.props.period,
......@@ -53,25 +49,26 @@ class LandingPageC extends Component {
station_group: this.props.selectedStationGroup,
errors_only: this.props.errorsOnly,
error_types: this.props.selectedErrorTypes
});
};
return `${url}?${parametersString}`;
return RESTAPI.composeApiUrl('ctrl_stationtestsummary', parameters);
}
getLatestObservationURL() {
let nDaysAgo = moment().add(-this.props.period, 'days');
let nDaysAgo_String = nDaysAgo.format('YYYY-MM-DD');
const url = '/api/view/ctrl_latest_observation?format=json'
const parameters = {}
// ---- Mandatory parameters
parameters.from_date = nDaysAgo_String
// ---- Optional parameters
parameters.station_group = this.props.selectedStationGroup
parameters.errors_only = this.props.errorsOnly
parameters.error_types = this.props.selectedErrorTypes
const parametersString = composeQueryString(parameters)
return `${url}&${parametersString}`;
const parameters = {
// ---- Mandatory parameters
format: 'json',
from_date: nDaysAgo_String,
// ---- Optional parameters
station_group: this.props.selectedStationGroup,
errors_only: this.props.errorsOnly,
error_types: this.props.selectedErrorTypes
}
return RESTAPI.composeApiUrl('ctrl_latest_observation', parameters);
}
getStationStatisticsURL() {
......@@ -79,24 +76,24 @@ class LandingPageC extends Component {
let now = moment().format('YYYY-MM-DD');
let averaging_interval = this.props.station_statistics.averaging_window
let test_type = this.props.station_statistics.test_type
let url = '/api/view/ctrl_stationtest_statistics?format=json'
const parameters = {}
// ---- Mandatory parameters
// select from
parameters.from_date = nDaysAgo
// select to
parameters.to_date = now
// select averaging interval
parameters.averaging_interval = averaging_interval
// ---- Optional parameters
parameters.errors_only = this.props.errorsOnly
parameters.station_group = this.props.selectedStationGroup
parameters.test_type = test_type
parameters.error_types = this.props.selectedErrorTypes
const parametersString = composeQueryString(parameters)
return `${url}&${parametersString}`;
const parameters = {
// ---- Mandatory parameters
format: 'json',
// select from
from_date: nDaysAgo,
// select to
to_date: now,
// select averaging interval
averaging_interval: averaging_interval,
// ---- Optional parameters
errors_only: this.props.errorsOnly,
station_group: this.props.selectedStationGroup,
test_type: test_type,
error_types: this.props.selectedErrorTypes
}
return RESTAPI.composeApiUrl('ctrl_stationtest_statistics', parameters);
}
render() {
......
......@@ -7,6 +7,7 @@ import EnlargeableImage from 'components/EnlargeableImage'
import {componentStatusOK, LOFARTESTS} from 'utils/LOFARDefinitions';
import {datetime_format} from 'utils/constants'
import {formatDateUTC} from 'utils/utils'
import RESTAPI from 'utils/api_configuration'
import moment from 'moment';
......@@ -84,7 +85,7 @@ function ErrorDetailRow({data, rowkey}) {
polarization: 1,
rcu_id: 1,
antenna_id: 1,
url: 1
plot_id: 1
},
img = null;
......@@ -112,8 +113,10 @@ function ErrorDetailRow({data, rowkey}) {
err_items.push(<li key="default"><em>See element error.</em></li>);
}
if (data.details.url) {
img = <EnlargeableImage url={data.details.url} />
if (data.details.plot_id) {
let url = RESTAPI.composeApiUrl('error_summary_plot');
url = `${url}/${data.details.plot_id}/`;
img = <EnlargeableImage url={url} />
}
return (
......
......@@ -6,8 +6,8 @@ import moment from 'moment';
import NavigationBar from 'components/NavigationBar'
import StationTestView from './components/StationTestView';
import StationTestChildView from './components/StationTestChildView';
import { composeQueryString } from 'utils/utils.js';
import { Toolbar, StationAutoComplete, DateRangeSelector, TestTypeSelector, ErrorTypesSelector } from 'components/Toolbar'
import RESTAPI from 'utils/api_configuration'
/*
* Display an Alert
......@@ -67,10 +67,7 @@ class StationOverviewPageC extends Component {
error_types: this.props.selectedErrorTypes
};
const baseURL = '/api/view/ctrl_station_component_errors';
const queryString = composeQueryString(parameters);
return `${baseURL}?${queryString}`
return RESTAPI.composeApiUrl('ctrl_station_component_errors', parameters);
}
isParameterMissing(){
......
......@@ -4,9 +4,9 @@ import moment from 'moment'
import {connect} from "react-redux";
import { Toolbar, StationAutoComplete, DateRangeSelector, TestTypeSelector, AntennaTypeSelector, AntennaIdSelector } from 'components/Toolbar'
import {Alert, Container, Col, Row} from "reactstrap";
import { composeQueryString } from 'utils/utils.js';
import AntennaErrorDetails from './components/AntennaErrorDetails'
import AntennaView from './components/AntennaView';
import RESTAPI from 'utils/api_configuration'
// Component to display an alert
......@@ -27,19 +27,21 @@ class TilesPageC extends Component {
}
getTilesDataURL () {
const url = '/api/view/ctrl_station_component_element_errors?format=json';
const parameters = {};
// ---- Mandatory parameters
parameters.station_name = this.props.selectedStation;
// ---- Optional parameters
parameters.from_date = moment(this.props.startDate).format('YYYY-MM-DD');
parameters.to_date = moment(this.props.endDate).format('YYYY-MM-DD');
parameters.component_type = this.props.antennaType;
parameters.antenna_id = this.props.antennaId;
parameters.test_type = this.props.testType === 'B' ? 'A' : this.props.testType;
const parametersString = composeQueryString(parameters);
return `${url}&${parametersString}`;
let parameters = {
// ---- Mandatory parameters
format: 'json',
station_name: this.props.selectedStation,
// ---- Optional parameters
from_date: moment(this.props.startDate).format('YYYY-MM-DD'),
to_date: moment(this.props.endDate).format('YYYY-MM-DD'),
component_type: this.props.antennaType,
antenna_id: this.props.antennaId,
test_type: this.props.testType === 'B' ? 'A' : this.props.testType
};
return RESTAPI.composeApiUrl('ctrl_station_component_element_errors', parameters);
}
render() {
......
import axios from 'axios';
import { stringSort } from '../../utils/utils.js';
import RESTAPI from 'utils/api_configuration'
export const FETCH_ERRORTYPES_BEGIN = 'FETCH_ERRORTYPES_BEGIN';
export const FETCH_ERRORTYPES_SUCCESS = 'FETCH_ERRORTYPES_SUCCESS';
......@@ -10,9 +11,6 @@ export const FETCH_STATIONS_SUCCESS = 'FETCH_STATIONS_SUCCESS';
export const FETCH_STATIONS_FAILURE = 'FETCH_STATIONS_FAILURE';
/* ERROR TYPES */
const errorTypesURL = '/api/view/ctrl_list_component_error_types';
export const fetchErrorTypesBegin = () => ({
type: FETCH_ERRORTYPES_BEGIN
});
......@@ -31,8 +29,9 @@ export const fetchErrorTypesFailure = error => ({
export function fetchErrorTypes() {
return dispatch => {
// Not used: dispatch(fetchErrorTypesBegin());
let url = RESTAPI.composeApiUrl('ctrl_list_component_error_types');
return