diff --git a/README.md b/README.md index 1cc046d..1f1214f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ Basic features are: - location management - monitoring of storage conditions -- audit trails - barcode labels - logs - user and permission management diff --git a/lab/forms.py b/lab/forms.py index b007eb5..37f1181 100644 --- a/lab/forms.py +++ b/lab/forms.py @@ -336,10 +336,11 @@ class TypeAttributesFormNew(forms.Form): widget=forms.Select(attrs={'class': 'form-control'}), help_text='Select a type.') list_values = forms.CharField(label='list values', max_length=DEFAULT, required=False, - help_text='Enter values via comma separated list.', + help_text='Enter values separated by commas to generate a selectable drop down ' + 'list. This field is optional.', widget=forms.TextInput(attrs={'class': 'form-control'})) default_value = forms.CharField(label='default value', max_length=DEFAULT, required=False, - help_text='Enter a default value.', + help_text='Enter a default value. This field is optional.', widget=forms.TextInput(attrs={'class': 'form-control'})) mandatory = forms.BooleanField(label='mandatory', required=False, help_text='Define if field is mandatory.', widget=forms.CheckboxInput(attrs={'class': 'form-control', 'style': 'align: left'})) diff --git a/lab/framework.py b/lab/framework.py index 6ea8cb5..81cd04a 100644 --- a/lab/framework.py +++ b/lab/framework.py @@ -454,10 +454,16 @@ def verify_checksum(self, row): to_verify += str(SECRET) checksum = row['checksum'] try: - return argon2.verify(to_verify, checksum) + result = argon2.verify(to_verify, checksum) + if not result: + # return false + log entry + message = 'Checksum for "{}" of table "{}" was not correct. Data integrity is at risk!'. \ + format(row[self.unique], self.table_name) + log.warning(message) + return result except ValueError or TypeError: # return false + log entry - message = 'Checksum for "{}" of table "{}" was not correct.\nData integrity is at risk!'.\ + message = 'Checksum for "{}" of table "{}" was not correct. Data integrity is at risk!'.\ format(row[self.unique], self.table_name) log.warning(message) return False @@ -641,14 +647,6 @@ def get(self, **dic): _list.append(tmp) return _list - @property - def export(self): - value_list = self.table.objects.values_list(*self.header) - _return = [tuple(custom.capitalize(self.header))] - for row in value_list: - _return.append(row) - return _return - class GetLog(GetStandard): pass @@ -678,8 +676,8 @@ def _table_header_dynamic(self): def query_dynamic(self, id_main): """Query dynamic table for main record id. - :param id_ref: main record identifier id - :type id_ref: int + :param id_main: main record identifier id + :type id_main: int :return: records for id_ref :rtype: django.db.models.query.QuerySet @@ -708,12 +706,17 @@ def html_header(self): return custom.capitalize(_header) def table_row_head_total(self, row, query_dynamic): - # TODO #59 if self.verify_checksum(row=row): + _success_list = list() for row in query_dynamic: if not self.verify_checksum_dynamic(row=row): - return '' - return '' + _success_list.append(False) + else: + _success_list.append(True) + if custom.check_equal(_success_list): + return '' + else: + return '' else: return '' @@ -733,11 +736,21 @@ def verify_checksum_dynamic(self, row): to_verify += str(SECRET) checksum = row['checksum'] try: - return argon2.verify(to_verify, checksum) + result = argon2.verify(to_verify, checksum) + if not result: + # return false + log entry + unique_main = models.Reagents.objects.filter(id=row['id_main'])[0].reagent + message = 'Checksum for "{}" attribute "{}" with id "{}" of table "{}" was not correct. ' \ + 'Data integrity is at risk!'. \ + format(unique_main, row['type_attribute'], row[self.dynamic_table_unique], self.dynamic_table_name) + log.warning(message) + return result except ValueError or TypeError: # return false + log entry - message = 'Checksum for "{}" of table "{}" was not correct.\nData integrity is at risk!'. \ - format(row[self.dynamic_table_unique], self.dynamic_table_name) + unique_main = models.Reagents.objects.filter(id=row['id_main'])[0].reagent + message = 'Checksum for "{}" attribute "{}" with id "{}" of table "{}" was not correct. ' \ + 'Data integrity is at risk!'. \ + format(unique_main, row['type_attribute'], row[self.dynamic_table_unique], self.dynamic_table_name) log.warning(message) return False @@ -770,50 +783,137 @@ def get(self, **dic): _list.append(tmp) return _list + @property + def export(self): + _query = self.query(order_by=self.order_by, type=self.type) + _list = list() + for row in _query: + _query_dynamic = self.query_dynamic(row['id']) + _tuple = tuple() + for field in self.header_start: + _tuple += (row[field], ) -class GetDynamicAuditTrail(GetStandard): - def __init__(self, table, dynamic_table, type): - super().__init__(table) - self.type = type + for field in self.type_attributes: + if field not in self.dynamic_table.objects.list_of_type_attributes(id_main=row['id']): + _tuple += ('', ) + else: + for row_dynamic in _query_dynamic: + if field == row_dynamic['type_attribute']: + _tuple += (row_dynamic['value'], ) + _tuple += (row['version'], ) + _list.append(_tuple) + _return = [tuple(custom.capitalize(self.header_start)) + + tuple(custom.capitalize(self.type_attributes)) + + ('Version', )] + for row in _list: + _return.append(row) + return _return + + +class GetDynamicAuditTrail(GetDynamic): + def __init__(self, table, dynamic_table, type, dt=None): + super().__init__(table, dynamic_table, type) + if dt: + self.dt = datetime.timedelta(seconds=int(dt) * 60) + self.utc_offset = custom.fill_up_time_delta(dt) + + def query_dynamic_at(self, id_main, version): + """Query dynamic table for main record id. + + :param id_main: main record identifier id + :type id_main: int + + :param version: version of the audit trail record stored in id_ref + :type version: int + + :return: records for id_ref + :rtype: django.db.models.query.QuerySet + """ + return self.dynamic_table.objects.filter(id_main=id_main, id_ref=version).values() + + @property + def header(self): + _header = self._table_header + _header.remove('id') + _header.remove('checksum') + _header.remove('id_ref') + return _header + + @property + def header_start(self): + _header_start = self.header + if self.affiliation == 'Reagents': + _header_start.remove('type') + _header_start.remove('version') + _header_start.remove('action') + _header_start.remove('user') + _header_start.remove('timestamp') + return _header_start + + @property + def header_dynamic(self): + _header_dynamic = self._table_header_dynamic + _header_dynamic.remove('id') + _header_dynamic.remove('checksum') + _header_dynamic.remove('id_ref') + # _header_dynamic.remove('id_main') + return _header_dynamic + + @property + def html_header(self): + _header = self.header_start + self.type_attributes + _header.append('Version') + _header.append('action') + _header.append('user') + _header.append('timestamp') + return custom.capitalize(_header) def table_row_head_total(self, row, query_dynamic): - # TODO #59 if self.verify_checksum(row=row): + _success_list = list() for row in query_dynamic: if not self.verify_checksum_dynamic(row=row): - return '' - return '' + _success_list.append(False) + else: + _success_list.append(True) + if custom.check_equal(_success_list): + return '' + else: + return '' else: return '' def get(self, **dic): - _query = self.query(order_by=self.order_by, type=self.type) + _query = self.query(order_by=self.order_by, type=self.type, **dic) _list = list() for row in _query: - _query_dynamic = self.query_dynamic(row['id']) + _query_dynamic = self.query_dynamic_at(id_main=row['id_ref'], version=row['version']) tmp = self.table_row_head_total(row=row, query_dynamic=_query_dynamic) # adding all tds for builder_header_start for field in self.header_start: - # tagging the unique field - if field == self.unique: - tmp += '{}'.format(row[field]) - else: - tmp += '{}'.format(row[field]) + # only payload + tmp += '{}'.format(row[field]) # adding all tds for builder_header_dynamic for field in self.type_attributes: - if field not in self.dynamic_table.objects.list_of_type_attributes(id_main=row['id']): - tmp += '' + if field not in self.dynamic_table.objects.list_of_type_attributes(id_main=row['id_ref'], + version=row['version']): + tmp += '' else: for row_dynamic in _query_dynamic: if field == row_dynamic['type_attribute']: - tmp += '{}'.format(row_dynamic['value']) + tmp += '{}'.format(row_dynamic['value']) # adding all tds for builder_header_end tmp += '{}'.format(row['version']) + tmp += '{}'.format(row['action']) + tmp += '{}'.format(row['user']) + tmp += '{}'.format(custom.format_timestamp(timestamp=row['timestamp'], + dt=self.dt, + utc_offset=self.utc_offset)) # close table tmp += '' # append table row _list.append(tmp) - return _list + return True, _list class TableManipulation(Master): @@ -1021,6 +1121,27 @@ def new_dynamic(self, user, identifier, main_version, timestamp, **kwargs): def new_boxing(self, **kwargs): return self.new_log(text='boxing', unique='box', **kwargs) + def clear_boxing(self, **kwargs): + """Dedicated function to clear boxing records. WARNING, does not suit framework! + + :return: flag + :rtype: bool + """ + try: + # parse record data + checksum = self.parsing(**kwargs) + self.table.objects.filter(box=kwargs['box'], + position=kwargs['position']).update(**self.dict, checksum=checksum) + # success message + log entry + message = 'Boxing record for "{}" has been cleared.'.format(kwargs['object']) + log.info(message) + except: + # raise error + message = 'Could not clear boxing record for "{}".'.format(kwargs['object']) + raise NameError(message) + else: + return True + def edit_boxing(self, **kwargs): """Dedicated function to update boxing records. WARNING, does not suit framework! @@ -1235,6 +1356,14 @@ def delete_multiple(self, user, records): pass return True + def delete_at(self, user, record): + # setting the user for identification + self.user = user + if self.delete(record): + return self.audit_trail(action='Delete') + else: + return False + def delete(self, record): # check if record is existing in the db if self.table.objects.exist(record): diff --git a/lab/models.py b/lab/models.py index 605ab15..f07b0b7 100644 --- a/lab/models.py +++ b/lab/models.py @@ -387,7 +387,12 @@ class DynamicReagents(models.Model): # audit trail manager class DynamicReagentsAuditTrailManager(GlobalAuditTrailManager): - pass + def list_of_type_attributes(self, id_main, version): + try: + return list(self.filter(id_main=id_main, id_ref=version).order_by('id'). + values_list('type_attribute', flat=True)) + except IndexError: + return list() # audit trail table @@ -857,7 +862,16 @@ def method(self, unique): return self.filter(object=unique)[0].type def box(self, unique): - return self.filter(object=unique)[0].box + try: + return self.filter(object=unique)[0].box + except IndexError: + return None + + def position(self, unique): + try: + return self.filter(object=unique)[0].position + except IndexError: + return None def type(self, unique): try: diff --git a/lab/templates/lab/audittrail.html b/lab/templates/lab/audittrail.html index 4a6b3b7..cb20414 100644 --- a/lab/templates/lab/audittrail.html +++ b/lab/templates/lab/audittrail.html @@ -23,7 +23,7 @@ var item = $('#id_table').find('#id_unique').find("td:first").text(); $.ajax({ method: "GET", - url: "/" + "{{ content }}" + "/audit_trail/", + url: "/" + "{{ content }}" + "/audit_trail/{% if content_dynamic %}{{ content_dynamic }}/{% endif %}", data: { 'unique': item }, diff --git a/lab/templates/lab/core.html b/lab/templates/lab/core.html deleted file mode 100644 index af1c520..0000000 --- a/lab/templates/lab/core.html +++ /dev/null @@ -1,128 +0,0 @@ - - - \ No newline at end of file diff --git a/lab/templates/lab/export.html b/lab/templates/lab/export.html index e115bbc..ac2116c 100644 --- a/lab/templates/lab/export.html +++ b/lab/templates/lab/export.html @@ -18,6 +18,10 @@ \ No newline at end of file diff --git a/lab/templates/lab/index.html b/lab/templates/lab/index.html index a68b346..6ed7372 100644 --- a/lab/templates/lab/index.html +++ b/lab/templates/lab/index.html @@ -122,7 +122,7 @@ {% endif %} @@ -313,7 +313,7 @@ -
+
+ {% if content != 'movement_log' and content != 'home' and content != 'login_log' %}