Merge pull request #201 from eReuse/feature/server-side-render-actions-documents

Server side render Data Wipe Action
This commit is contained in:
Santiago L 2022-02-10 11:30:16 +01:00 committed by GitHub
commit ea26dc2e2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 255 additions and 29 deletions

View File

@ -47,9 +47,6 @@ jobs:
sudo apt-get update -qy sudo apt-get update -qy
sudo apt-get -y install postgresql-client sudo apt-get -y install postgresql-client
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install virtualenv
virtualenv env
source env/bin/activate
pip install flake8 pytest coverage pip install flake8 pytest coverage
pip install -r requirements.txt pip install -r requirements.txt
@ -65,6 +62,17 @@ jobs:
psql -h "localhost" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION citext SCHEMA public;" psql -h "localhost" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION citext SCHEMA public;"
psql -h "localhost" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION pg_trgm SCHEMA public;" psql -h "localhost" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION pg_trgm SCHEMA public;"
- name: Lint with flake8
run: |
# stop the build if:
# - E9,F63,F7,F82: Python syntax errors or undefined names
# - E501: line longer than 120 characters
# - C901: complexity greater than 10
# - F401: modules imported but unused
# See: https://flake8.pycqa.org/en/latest/user/error-codes.html
flake8 . --select=E9,F63,F7,F82,E501,C901,F401
flake8 . --exit-zero
- name: Run Tests - name: Run Tests
run: | run: |
export SECRET_KEY=`python3 -c 'import secrets; print(secrets.token_hex())'` export SECRET_KEY=`python3 -c 'import secrets; print(secrets.token_hex())'`

View File

@ -1,28 +1,29 @@
import copy
import json import json
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from flask import g, request from boltons.urlutils import URL
from flask_wtf import FlaskForm
from sqlalchemy.util import OrderedSet
from wtforms import (DateField, FloatField, HiddenField, IntegerField,
MultipleFileField, SelectField, StringField,
TextAreaField, validators)
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.action.models import (Action, RateComputer, from ereuse_devicehub.resources.action.models import RateComputer, Snapshot
Snapshot, VisualTest)
from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate
from ereuse_devicehub.resources.action.schemas import \ from ereuse_devicehub.resources.action.schemas import \
Snapshot as SnapshotSchema Snapshot as SnapshotSchema
from ereuse_devicehub.resources.action.views.snapshot import (move_json, from ereuse_devicehub.resources.action.views.snapshot import move_json, save_json
save_json)
from ereuse_devicehub.resources.device.models import (SAI, Cellphone, Computer, from ereuse_devicehub.resources.device.models import (SAI, Cellphone, Computer,
Device, Keyboard, Device, Keyboard, MemoryCardReader,
MemoryCardReader, Monitor, Mouse, Smartphone, Tablet)
Monitor, Mouse, from flask import g, request
Smartphone, Tablet) from flask_wtf import FlaskForm
from sqlalchemy.util import OrderedSet
from wtforms import (BooleanField, DateField, FileField, FloatField, Form,
HiddenField, IntegerField, MultipleFileField, SelectField,
StringField, TextAreaField, URLField, validators)
from wtforms.fields import FormField
from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.device.sync import Sync
from ereuse_devicehub.resources.documents.models import DataWipeDocument
from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware
from ereuse_devicehub.resources.hash_reports import insert_hash
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.resources.user.exceptions import InsufficientPermission from ereuse_devicehub.resources.user.exceptions import InsufficientPermission
@ -472,10 +473,18 @@ class TagDeviceForm(FlaskForm):
class NewActionForm(FlaskForm): class NewActionForm(FlaskForm):
name = StringField(u'Name', [validators.length(max=50)]) name = StringField(u'Name', [validators.length(max=50)],
description="A name or title of the event. Something to look for.")
devices = HiddenField() devices = HiddenField()
date = DateField(u'Date', validators=(validators.Optional(),)) date = DateField(u'Date', [validators.Optional()],
severity = SelectField(u'Severity', choices=[(v.name, v.name) for v in Severity]) description="""When the action ends. For some actions like booking
the time when it expires, for others like renting the
time that the end rents. For specific actions, it is the
time in which they are carried out; differs from created
in that created is where the system receives the action.""")
severity = SelectField(u'Severity', choices=[(v.name, v.name) for v in Severity],
description="""An indicator that evaluates the execution of the event.
For example, failed events are set to Error""")
description = TextAreaField(u'Description') description = TextAreaField(u'Description')
lot = HiddenField() lot = HiddenField()
type = HiddenField() type = HiddenField()
@ -537,3 +546,71 @@ class AllocateForm(NewActionForm):
is_valid = False is_valid = False
return is_valid return is_valid
class DataWipeDocumentForm(Form):
date = DateField(u'Date', [validators.Optional()],
description="Date when was data wipe")
url = URLField(u'Url', [validators.Optional()],
description="Url where the document resides")
success = BooleanField(u'Success', [validators.Optional()],
description="The erase was success or not?")
software = StringField(u'Software', [validators.Optional()],
description="Which software has you use for erase the disks")
id_document = StringField(u'Document Id', [validators.Optional()],
description="Identification number of document")
file_name = FileField(u'File', [validators.DataRequired()],
description="""This file is not stored on our servers, it is only used to
generate a digital signature and obtain the name of the file.""")
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
return is_valid
def save(self, commit=True):
file_name = ''
file_hash = ''
if self.file_name.data:
file_name = self.file_name.data.filename
file_hash = insert_hash(self.file_name.data.read(), commit=False)
self.url.data = URL(self.url.data)
self._obj = DataWipeDocument(
document_type='DataWipeDocument',
)
self.populate_obj(self._obj)
self._obj.file_name = file_name
self._obj.file_hash = file_hash
db.session.add(self._obj)
if commit:
db.session.commit()
return self._obj
class DataWipeForm(NewActionForm):
document = FormField(DataWipeDocumentForm)
def save(self):
self.document.form.save(commit=False)
Model = db.Model._decl_class_registry.data[self.type.data]()
self.instance = Model()
devices = self.devices.data
severity = self.severity.data
self.devices.data = self._devices
self.severity.data = Severity[self.severity.data]
document = copy.copy(self.document)
del self.document
self.populate_obj(self.instance)
self.instance.document = document.form._obj
db.session.add(self.instance)
db.session.commit()
self.devices.data = devices
self.severity.data = severity
self.document = document
return self.instance

View File

@ -8,7 +8,7 @@ from ereuse_devicehub.inventory.forms import (AllocateForm, LotDeviceForm,
LotForm, NewActionForm, LotForm, NewActionForm,
NewDeviceForm, TagDeviceForm, NewDeviceForm, TagDeviceForm,
TagForm, TagUnnamedForm, TagForm, TagUnnamedForm,
UploadSnapshotForm) UploadSnapshotForm, DataWipeForm)
from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
@ -36,6 +36,7 @@ class DeviceListMix(View):
devices = sorted(devices, key=lambda x: x.updated, reverse=True) devices = sorted(devices, key=lambda x: x.updated, reverse=True)
form_new_action = NewActionForm(lot=lot.id) form_new_action = NewActionForm(lot=lot.id)
form_new_allocate = AllocateForm(lot=lot.id) form_new_allocate = AllocateForm(lot=lot.id)
form_new_datawipe = DataWipeForm(lot=lot.id)
else: else:
devices = Device.query.filter( devices = Device.query.filter(
Device.owner_id == current_user.id).filter( Device.owner_id == current_user.id).filter(
@ -43,6 +44,7 @@ class DeviceListMix(View):
Device.updated.desc()) Device.updated.desc())
form_new_action = NewActionForm() form_new_action = NewActionForm()
form_new_allocate = AllocateForm() form_new_allocate = AllocateForm()
form_new_datawipe = DataWipeForm()
action_devices = form_new_action.devices.data action_devices = form_new_action.devices.data
list_devices = [] list_devices = []
@ -56,6 +58,7 @@ class DeviceListMix(View):
'form_tag_device': TagDeviceForm(), 'form_tag_device': TagDeviceForm(),
'form_new_action': form_new_action, 'form_new_action': form_new_action,
'form_new_allocate': form_new_allocate, 'form_new_allocate': form_new_allocate,
'form_new_datawipe': form_new_datawipe,
'lot': lot, 'lot': lot,
'tags': tags, 'tags': tags,
'list_devices': list_devices 'list_devices': list_devices
@ -331,8 +334,29 @@ class NewAllocateView(NewActionView, DeviceListMix):
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)
class NewDataWipeView(NewActionView, DeviceListMix):
methods = ['POST']
form_class = DataWipeForm
def dispatch_request(self):
self.form = self.form_class()
if self.form.validate_on_submit():
instance = self.form.save()
messages.success('Action "{}" created successfully!'.format(instance.type))
next_url = self.get_next_url()
return flask.redirect(next_url)
lot_id = self.form.lot.data
self.get_context(lot_id)
self.context['form_new_datawipe'] = self.form
return flask.render_template(self.template_name, **self.context)
devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add')) devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add'))
devices.add_url_rule('/action/allocate/add/', view_func=NewAllocateView.as_view('allocate_add')) devices.add_url_rule('/action/allocate/add/', view_func=NewAllocateView.as_view('allocate_add'))
devices.add_url_rule('/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add'))
devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist')) devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist'))
devices.add_url_rule('/device/<string:id>/', view_func=DeviceDetailView.as_view('device_details')) devices.add_url_rule('/device/<string:id>/', view_func=DeviceDetailView.as_view('device_details'))
devices.add_url_rule('/lot/<string:lot_id>/device/', view_func=DeviceListView.as_view('lotdevicelist')) devices.add_url_rule('/lot/<string:lot_id>/device/', view_func=DeviceListView.as_view('lotdevicelist'))

View File

@ -26,13 +26,16 @@ class ReportHash(db.Model):
hash3.comment = """The normalized name of the hash.""" hash3.comment = """The normalized name of the hash."""
def insert_hash(bfile): def insert_hash(bfile, commit=True):
hash3 = hashlib.sha3_256(bfile).hexdigest() hash3 = hashlib.sha3_256(bfile).hexdigest()
db_hash = ReportHash(hash3=hash3) db_hash = ReportHash(hash3=hash3)
db.session.add(db_hash) db.session.add(db_hash)
if commit:
db.session.commit() db.session.commit()
db.session.flush() db.session.flush()
return hash3
def verify_hash(bfile): def verify_hash(bfile):
hash3 = hashlib.sha3_256(bfile.read()).hexdigest() hash3 = hashlib.sha3_256(bfile.read()).hexdigest()

View File

@ -1,8 +1,12 @@
$(document).ready(function() { $(document).ready(function() {
var show_action_form = $("#allocateModal").data('show-action-form'); var show_allocate_form = $("#allocateModal").data('show-action-form');
if (show_action_form != "None") { var show_datawipe_form = $("#datawipeModal").data('show-action-form');
if (show_allocate_form != "None") {
$("#allocateModal .btn-primary").show(); $("#allocateModal .btn-primary").show();
newAllocate(show_action_form); newAllocate(show_allocate_form);
} else if (show_datawipe_form != "None") {
$("#datawipeModal .btn-primary").show();
newDataWipe(show_datawipe_form);
} else { } else {
$(".deviceSelect").on("change", deviceSelect); $(".deviceSelect").on("change", deviceSelect);
} }
@ -26,6 +30,9 @@ function deviceSelect() {
$("#allocateModal .pol").show(); $("#allocateModal .pol").show();
$("#allocateModal .btn-primary").hide(); $("#allocateModal .btn-primary").hide();
$("#datawipeModal .pol").show();
$("#datawipeModal .btn-primary").hide();
} else { } else {
$("#addingLotModal .pol").hide(); $("#addingLotModal .pol").hide();
$("#addingLotModal .btn-primary").show(); $("#addingLotModal .btn-primary").show();
@ -39,6 +46,9 @@ function deviceSelect() {
$("#allocateModal .pol").hide(); $("#allocateModal .pol").hide();
$("#allocateModal .btn-primary").show(); $("#allocateModal .btn-primary").show();
$("#datawipeModal .pol").hide();
$("#datawipeModal .btn-primary").show();
$("#addingTagModal .pol").hide(); $("#addingTagModal .pol").hide();
} }
} }
@ -69,11 +79,20 @@ function newAllocate(action) {
$("#activeAllocateModal").click(); $("#activeAllocateModal").click();
} }
function newDataWipe(action) {
$("#datawipeModal #type").val(action);
$("#datawipeModal #title-action").html(action);
get_device_list();
deviceSelect();
$("#activeDatawipeModal").click();
}
function get_device_list() { function get_device_list() {
var devices = $(".deviceSelect").filter(':checked'); var devices = $(".deviceSelect").filter(':checked');
/* Insert the correct count of devices in actions form */ /* Insert the correct count of devices in actions form */
var devices_count = devices.length; var devices_count = devices.length;
$("#datawipeModal .devices-count").html(devices_count);
$("#allocateModal .devices-count").html(devices_count); $("#allocateModal .devices-count").html(devices_count);
$("#actionModal .devices-count").html(devices_count); $("#actionModal .devices-count").html(devices_count);

View File

@ -0,0 +1,85 @@
<div class="modal fade" id="datawipeModal" tabindex="-1" style="display: none;" aria-hidden="true"
data-show-action-form="{{ form_new_datawipe.type.data }}">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Action <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ url_for('inventory.devices.datawipe_add') }}" method="post" enctype="multipart/form-data">
{{ form_new_datawipe.csrf_token }}
<div class="modal-body">
{% for field in form_new_datawipe %}
{% if field != form_new_datawipe.csrf_token %}
{% if field == form_new_datawipe.devices %}
<div class="col-12">
{{ field.label(class_="form-label") }}: <span class="devices-count"></span>
{{ field(class_="devicesList") }}
<p class="text-danger pol" style="display: none;">
You need select first some device before to do one action
</p>
<p class="enumeration-devices"></p>
</div>
{% elif field == form_new_datawipe.lot %}
{{ field }}
{% elif field == form_new_datawipe.type %}
{{ field }}
{% elif field == form_new_datawipe.document %}
{% for _field in field %}
<div class="col-12">
{{ _field.label(class_="form-label") }}
{% if _field == field.success %}
<div class="form-check form-switch">
{{ _field(class_="form-check-input") }}
<small class="text-muted">{{ _field.description }}</small>
</div>
{% else %}
{{ _field(class_="form-control") }}
<small class="text-muted">{{ _field.description }}</small>
{% endif %}
{% if _field.errors %}
<p class="text-danger">
{% for error in _field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="col-12">
{{ field.label(class_="form-label") }}
{% if field == form_new_datawipe.success %}
<div class="form-check form-switch">
{{ field(class_="form-check-input") }}
<small class="text-muted">{{ field.description }}</small>
</div>
{% else %}
{{ field(class_="form-control") }}
<small class="text-muted">{{ field.description }}</small>
{% endif %}
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endif %}
{% endif %}
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" style="display: none;" value="Create" />
</div>
</form>
</div>
</div>
</div>

View File

@ -80,6 +80,7 @@
</button> </button>
<span class="d-none" id="activeActionModal" data-bs-toggle="modal" data-bs-target="#actionModal"></span> <span class="d-none" id="activeActionModal" data-bs-toggle="modal" data-bs-target="#actionModal"></span>
<span class="d-none" id="activeAllocateModal" data-bs-toggle="modal" data-bs-target="#allocateModal"></span> <span class="d-none" id="activeAllocateModal" data-bs-toggle="modal" data-bs-target="#allocateModal"></span>
<span class="d-none" id="activeDatawipeModal" data-bs-toggle="modal" data-bs-target="#datawipeModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnActions"> <ul class="dropdown-menu" aria-labelledby="btnActions">
<li> <li>
Status actions Status actions
@ -139,7 +140,7 @@
</a> </a>
</li> </li>
<li> <li>
<a href="javascript:void()" class="dropdown-item"> <a href="javascript:newDataWipe('DataWipe')" class="dropdown-item">
<i class="bi bi-eraser-fill"></i> <i class="bi bi-eraser-fill"></i>
DataWipe DataWipe
</a> </a>
@ -280,6 +281,7 @@
{% include "inventory/removelot.html" %} {% include "inventory/removelot.html" %}
{% include "inventory/actions.html" %} {% include "inventory/actions.html" %}
{% include "inventory/allocate.html" %} {% include "inventory/allocate.html" %}
{% include "inventory/data_wipe.html" %}
<!-- CDN --> <!-- CDN -->
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest"></script> <script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest"></script>

8
tox.ini Normal file
View File

@ -0,0 +1,8 @@
[flake8]
count = True
exclude =
migrations
max-complexity = 10
max-line-length = 120
show-source = True
statistics = True