Compare commits

...

4 Commits

6 changed files with 278 additions and 0 deletions

41
admin/forms.py Normal file
View File

@ -0,0 +1,41 @@
from django import forms
from utils.device import create_annotation, create_doc, create_index
from utils.save_snapshots import move_json, save_in_disk
from django.forms.formsets import BaseFormSet
from django.forms import formset_factory
from django.core.exceptions import ValidationError
class CustomStatusLabelForm(forms.Form):
label_name = forms.CharField(
label="Annotation Name",
max_length=50,
widget=forms.TextInput(attrs={'class': 'form-control'})
)
class CustomStatusValueForm(forms.Form):
label_state = forms.CharField(
label="Possible State",
max_length=50,
widget=forms.TextInput(attrs={'class': 'form-control'})
)
class CustomStatusValueFormSet(BaseFormSet):
"""Validation for inputs (no two values should be the same)"""
def clean(self):
if any(self.errors):
return
label = []
labels = []
for form in self.forms:
label = form.cleaned_data.get('label_state')
if label:
if label in labels:
raise ValidationError("Duplicate labels are not allowed.")
labels.append(label)
CustomStatusFormSet = formset_factory(CustomStatusValueForm ,formset = CustomStatusValueFormSet)

View File

@ -10,9 +10,15 @@
<div class="row">
<div class="col">
<a href="{% url 'admin:institution' user.institution.pk %}" class="btn btn-green-admin">
{% translate "Institution" %}
</a>
<a href="{% url 'admin:reserved'%}" class="btn btn-green-admin">
{% translate "Reserved Annotations" %}
</a>
</div>
</div>

View File

@ -0,0 +1,130 @@
{% extends "base.html" %}
{% load i18n %}
{% load django_bootstrap5 %}
{% block content %}
<div class="container mt-5">
<div class="row mb-4">
<div class="col">
<h3 class="text-center">{{ subtitle }}</h3>
</div>
</div>
<form role="form" method="post" novalidate>
{% csrf_token %}
<div class="mb-3">
<h5 class="mt-4">{% translate "Status name" %}</h5>
{% bootstrap_field form.label_name show_label=False %}
</div>
<h5 class="mt-4">{% translate "Possible States" %}</h5>
<div id="formset">
{{ formset.management_form }}
{% for form in formset %}
<div class="row mb-3 formset-form">
<div class="col-md-10">
{% bootstrap_field form.label_state show_label=False %}
</div>
<div class="col-md-2 d-flex align-items-center">
{% if forloop.first %}
<button type="button" class="btn btn-sm btn-success add-form">
&#43;
</button>
{% endif %}
<button type="button" class="btn btn-sm btn-danger remove-form ms-2">
&minus;
</button>
</div>
</div>
{% endfor %}
</div>
<div class="container">
<a class="btn btn-grey" href="{% url 'admin:panel' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>
</form>
</div>
<script>
//TODO: change this to jquery formset plugin
document.addEventListener('DOMContentLoaded', function() {
var formsetDiv = document.getElementById('formset');
var totalForms = document.getElementById('id_form-TOTAL_FORMS');
function updateElementIndex(el, prefix, index) {
var idRegex = new RegExp('(' + prefix + '-\\d+-)');
var replacement = prefix + '-' + index + '-';
if (el.id) el.id = el.id.replace(idRegex, replacement);
if (el.name) el.name = el.name.replace(idRegex, replacement);
}
function addForm(e) {
e.preventDefault();
var formCount = parseInt(totalForms.value);
var newForm = document.querySelector('.formset-form').cloneNode(true);
newForm.querySelectorAll('input').forEach(function(input) {
updateElementIndex(input, 'form', formCount);
input.value = '';
});
// Remove any hidden inputs (management form fields)
newForm.querySelectorAll('input[type="hidden"]').forEach(function(hiddenInput) {
hiddenInput.remove();
});
// Attach event listeners to the new buttons
var addButton = newForm.querySelector('.add-form');
var removeButton = newForm.querySelector('.remove-form');
addButton.addEventListener('click', addForm);
removeButton.addEventListener('click', removeForm);
// Ensure only the first form has the add button
formsetDiv.querySelectorAll('.add-form').forEach(function(button, index) {
if (index > 0) {
button.remove();
}
});
formsetDiv.appendChild(newForm);
totalForms.value = formCount + 1;
}
function removeForm(e) {
e.preventDefault();
var formToRemove = e.target.closest('.formset-form');
formToRemove.remove();
var forms = formsetDiv.querySelectorAll('.formset-form');
totalForms.value = forms.length;
// Re-index form elements
forms.forEach(function(form, index) {
form.querySelectorAll('input').forEach(function(input) {
updateElementIndex(input, 'form', index);
});
});
// Ensure only the first form has the add button
formsetDiv.querySelectorAll('.add-form').forEach(function(button, index) {
if (index > 0) {
button.remove();
}
});
}
// Initial event listeners
document.querySelectorAll('.add-form').forEach(function(button) {
button.addEventListener('click', addForm);
});
document.querySelectorAll('.remove-form').forEach(function(button) {
button.addEventListener('click', removeForm);
});
});
</script>
{% endblock %}

View File

@ -10,4 +10,5 @@ urlpatterns = [
path("users/edit/<int:pk>", views.EditUserView.as_view(), name="edit_user"),
path("users/delete/<int:pk>", views.DeleteUserView.as_view(), name="delete_user"),
path("institution/<int:pk>", views.InstitutionView.as_view(), name="institution"),
path("reserved", views.AddReservedAnnotationView.as_view(), name="reserved"),
]

View File

@ -8,9 +8,16 @@ from django.views.generic.edit import (
UpdateView,
DeleteView,
)
from django.views.generic import FormView
import logging
from dashboard.mixins import DashboardView, Http403
from user.models import User, Institution
from admin.email import NotifyActivateUserByEmail
from admin.forms import CustomStatusLabelForm, CustomStatusValueForm, CustomStatusFormSet
from evidence.models import Annotation, AllowedValue
import uuid
logger = logging.getLogger('dhub')
class AdminView(DashboardView):
@ -124,3 +131,70 @@ class InstitutionView(AdminView, UpdateView):
self.object = self.request.user.institution
kwargs = super().get_form_kwargs()
return kwargs
class AddReservedAnnotationView(AdminView, FormView):
template_name = "reserved.html"
title = _("New Custom State Labels")
breadcrumb = "Admin / Custom State Labels (new name?)"
success_url = reverse_lazy('admin:panel')
form_class = CustomStatusLabelForm
formset_class = CustomStatusFormSet
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.POST:
context['form'] = self.form_class(self.request.POST)
context['formset'] = self.formset_class(self.request.POST)
else:
context['form'] = self.form_class()
context['formset'] = self.formset_class()
context['subtitle'] = _("Add Custom Status Label")
return context
def form_valid(self, form):
context = self.get_context_data()
formset = context['formset']
form = context['form']
if form.is_valid():
label_name = form.cleaned_data['label_name']
else:
return self.form_invalid(form)
if formset.is_valid():
annotation = Annotation.objects.create(
uuid=uuid.uuid4(),
owner=self.request.user.institution,
user=self.request.user,
type=Annotation.Type.ADMIN,
key=label_name,
value=label_name
)
first_state = None
for form in formset:
state = form.cleaned_data.get('label_state')
if state and not first_state:
first_state = state
annotation.value = state
annotation.save()
if state:
AllowedValue.objects.create(
annotation=annotation,
value=state
)
logger.info("Saving custom label to db: " + label_name)
self.success_message = _("Custom status label has been added.")
self.success_url = reverse_lazy('admin:panel')
return super().form_valid(form)
else:
logger.error("Formset is not valid")
logger.error(formset.errors)
return self.form_invalid(form)

View File

@ -15,6 +15,7 @@ class Annotation(models.Model):
USER = 1, "User"
DOCUMENT = 2, "Document"
ERASE_SERVER = 3, "EraseServer"
ADMIN = 4, "Admin"
created = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField()
@ -28,7 +29,32 @@ class Annotation(models.Model):
constraints = [
models.UniqueConstraint(fields=["type", "key", "uuid"], name="unique_type_key_uuid")
]
#TODO: check if this works properly
def clean(self):
super().clean()
if self.type == self.Type.ADMIN:
if Annotation.objects.filter(type=self.Type.ADMIN, key=self.key).exists():
raise ValidationError(f"The key '{self.key}' is already reserved by admin.")
else:
if Annotation.objects.filter(type=self.Type.ADMIN, key=self.key).exists():
raise ValidationError(f"The key '{self.key}' is reserved by admin and cannot be used.")
class AllowedValue(models.Model):
annotation = models.ForeignKey(Annotation, on_delete=models.CASCADE, limit_choices_to={'type': Annotation.Type.ADMIN})
value = models.CharField(max_length=50)
def __str__(self):
return self.value
#This represents a change on a System-type Annotation
class Action(models.Model):
annotation = models.ForeignKey(Annotation, on_delete=models.CASCADE, limit_choices_to={'type': Annotation.Type.ADMIN})
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
comment = models.CharField(max_length=250)
class Evidence:
def __init__(self, uuid):