From 8e0f9b4e838cf177095f4cde96ead1668f7746e5 Mon Sep 17 00:00:00 2001
From: Piotr Gawron <piotr.gawron@uni.lu>
Date: Wed, 18 Nov 2020 08:59:34 +0100
Subject: [PATCH] form that allows editing visit import data

---
 smash/web/forms/visit_import_data_form.py     | 12 +++
 .../importer/csv_tns_visit_import_reader.py   |  5 ++
 smash/web/importer/log_storage.py             | 15 ++++
 smash/web/importer/warning_counter.py         | 11 +--
 smash/web/templates/study/edit.html           | 50 ++++++++++++
 .../web/templates/visit_import_data/edit.html | 81 +++++++++++++++++++
 smash/web/urls.py                             |  5 ++
 smash/web/views/study.py                      | 61 +++++++++++++-
 8 files changed, 233 insertions(+), 7 deletions(-)
 create mode 100644 smash/web/forms/visit_import_data_form.py
 create mode 100644 smash/web/importer/log_storage.py
 create mode 100644 smash/web/templates/visit_import_data/edit.html

diff --git a/smash/web/forms/visit_import_data_form.py b/smash/web/forms/visit_import_data_form.py
new file mode 100644
index 00000000..8da98260
--- /dev/null
+++ b/smash/web/forms/visit_import_data_form.py
@@ -0,0 +1,12 @@
+from django.forms import ModelForm
+
+from web.models import VisitImportData
+
+
+class VisitImportDataEditForm(ModelForm):
+    class Meta:
+        model = VisitImportData
+        fields = '__all__'
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
diff --git a/smash/web/importer/csv_tns_visit_import_reader.py b/smash/web/importer/csv_tns_visit_import_reader.py
index 2c5ed625..ea2aa27a 100644
--- a/smash/web/importer/csv_tns_visit_import_reader.py
+++ b/smash/web/importer/csv_tns_visit_import_reader.py
@@ -194,6 +194,11 @@ class TnsCsvVisitImportReader:
         try:
             visit_number = data[self.visit_import_data.visit_number_column_name]
             visit_number = int(visit_number) + (1 - self.visit_import_data.study.redcap_first_visit_number)
+            if visit_number < 1:
+                logger.warning(
+                    "Visit number is invalid. Visit number should start from: " +
+                    str(self.visit_import_data.study.redcap_first_visit_number) + ".")
+                visit_number = 1
             return visit_number
         except KeyError as e:
             raise EtlException('Visit number is not defined') from e
diff --git a/smash/web/importer/log_storage.py b/smash/web/importer/log_storage.py
new file mode 100644
index 00000000..bdc78d22
--- /dev/null
+++ b/smash/web/importer/log_storage.py
@@ -0,0 +1,15 @@
+import logging
+
+
+class LogStorageHandler(logging.Handler):
+    level_messages = {}
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.level_messages = {}
+
+    def emit(self, record):
+        level = record.levelname
+        if level not in self.level_messages:
+            self.level_messages[level] = []
+        self.level_messages[level].append(self.format(record))
diff --git a/smash/web/importer/warning_counter.py b/smash/web/importer/warning_counter.py
index 0098a08f..d3755c45 100644
--- a/smash/web/importer/warning_counter.py
+++ b/smash/web/importer/warning_counter.py
@@ -1,14 +1,15 @@
 import logging
 
+
 class MsgCounterHandler(logging.Handler):
     level2count = None
 
     def __init__(self, *args, **kwargs):
-        super(MsgCounterHandler, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         self.level2count = {}
 
     def emit(self, record):
-        l = record.levelname
-        if l not in self.level2count:
-            self.level2count[l] = 0
-        self.level2count[l] += 1
+        level = record.levelname
+        if level not in self.level2count:
+            self.level2count[level] = 0
+        self.level2count[level] += 1
diff --git a/smash/web/templates/study/edit.html b/smash/web/templates/study/edit.html
index 5808f338..1512351c 100644
--- a/smash/web/templates/study/edit.html
+++ b/smash/web/templates/study/edit.html
@@ -94,6 +94,56 @@
                             </div>
                         </div><!-- /.box-body -->
 
+                        <div class="box-header with-border">
+                            <h3>ETL</h3>
+                        </div>
+                        <div class="box-body">
+                            <table class="table table-bordered table-striped">
+                                <thead>
+                                <tr>
+                                    <th class="text-center">Action Type</th>
+                                    <th class="text-center">File</th>
+                                    <th class="text-center">File Type</th>
+                                    <th class="text-center">Run at</th>
+                                    <th class="text-center">Worker</th>
+                                    <th class="text-center">Edit</th>
+                                    <th class="text-center">Run manually</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                {% for etl_entry in etl_entries %}
+                                    <tr>
+                                        <td class="text-center">{{ etl_entry.type }}</td>
+                                        <td class="text-center">{{ etl_entry.file }}</td>
+                                        <td class="text-center">{{ etl_entry.filetype }}</td>
+                                        <td class="text-center">{{ etl_entry.run_at }}</td>
+                                        <td class="text-center">{{ etl_entry.worker }}</td>
+                                        <td class="text-center"><a title="Edit ETL"
+                                                {% if etl_entry.type == 'Import visit' %}
+                                                                   href="{% url 'web.views.import_visit_edit' study_id etl_entry.id %}"
+                                                {% else %}
+                                                                   href="#"
+                                                {% endif %}
+                                                                   type="button"
+                                                                   class="btn btn-block btn-default">EDIT</a>
+                                        </td>
+                                        <td class="text-center">
+                                            {% if etl_entry.available %}
+                                                <a href="{% url 'web.views.import_visit_execute' study_id etl_entry.id %}"
+                                                   type="button"
+                                                   class="btn btn-block btn-default">Run</a>
+                                            {% else %}
+                                                <a type="button"
+                                                   class="btn btn-block btn-warning" disabled>Unavailable</a>
+
+                                            {% endif %}
+                                        </td>
+                                    </tr>
+                                {% endfor %}
+                                </tbody>
+                            </table>
+                        </div>
+
                         {% include 'includes/custom_study_subject_field_box.html' with study=study fields=study.customstudysubjectfield_set.all %}
 
                         <div class="box-header with-border">
diff --git a/smash/web/templates/visit_import_data/edit.html b/smash/web/templates/visit_import_data/edit.html
new file mode 100644
index 00000000..ad50e6e8
--- /dev/null
+++ b/smash/web/templates/visit_import_data/edit.html
@@ -0,0 +1,81 @@
+{% extends "_base.html" %}
+{% load static %}
+{% load filters %}
+
+{% block styles %}
+    {{ block.super }}
+    <link rel="stylesheet" href="{% static 'AdminLTE/plugins/awesomplete/awesomplete.css' %}"/>
+
+    {% include "includes/datepicker.css.html" %}
+{% endblock styles %}
+
+{% block ui_active_tab %}'subjects'{% endblock ui_active_tab %}
+{% block page_header %}Edit visit import data{% endblock page_header %}
+{% block page_description %}{% endblock page_description %}
+
+{% block title %}{{ block.super }} - Edit visit import data{% endblock %}
+
+{% block breadcrumb %}
+    {% include "subjects/breadcrumb.html" %}
+{% endblock breadcrumb %}
+
+{% block maincontent %}
+
+    {% block content %}
+        <div class="row">
+            <div class="col-md-12">
+                <div class="box box-success">
+                    <div class="box-header with-border">
+                        <h3 class="box-title">Enter visit import details</h3>
+                    </div>
+
+
+                    <form method="post" action="" class="form-horizontal">
+                        {% csrf_token %}
+
+                        <div class="box-body">
+                            {% for field in form %}
+                                <div class="form-group {% if field.errors %}has-error{% endif %}">
+                                    <label class="col-sm-4  col-lg-offset-1 col-lg-2 control-label">
+                                        {{ field.label }}
+                                    </label>
+
+                                    <div class="col-sm-8 col-lg-4">
+                                        {{ field|add_class:'form-control' }}
+                                    </div>
+
+                                    {% if field.errors %}
+                                        <span class="help-block">
+  							{{ field.errors }}
+  						</span>
+                                    {% endif %}
+                                </div>
+                            {% endfor %}
+                        </div><!-- /.box-body -->
+                        <div class="box-footer">
+                            <div class="col-sm-6">
+                                <button type="submit" class="btn btn-block btn-success">Save</button>
+                            </div>
+                            <div class="col-sm-6">
+                                <a href="{% url 'web.views.edit_study' study_id %}"
+                                   class="btn btn-block btn-default">Cancel</a>
+                            </div>
+                        </div><!-- /.box-footer -->
+                    </form>
+                </div>
+
+            </div>
+        </div>
+
+    {% endblock %}
+
+
+{% endblock maincontent %}
+
+{% block scripts %}
+    {{ block.super }}
+
+    <script src="{% static 'AdminLTE/plugins/awesomplete/awesomplete.min.js' %}"></script>
+
+    {% include "includes/datetimepicker.js.html" %}
+{% endblock scripts %}
diff --git a/smash/web/urls.py b/smash/web/urls.py
index 7e5e1239..dcf6a803 100644
--- a/smash/web/urls.py
+++ b/smash/web/urls.py
@@ -253,6 +253,11 @@ urlpatterns = [
     url(r'^study/(?P<study_id>\d+)/custom_study_subject_field/(?P<field_id>\d+)/delete',
         views.study.custom_study_subject_field_delete, name='web.views.custom_study_subject_field_delete'),
 
+    url(r'^study/(?P<study_id>\d+)/import_visit_edit/(?P<import_id>\d+)/edit',
+        views.study.import_visit_edit, name='web.views.import_visit_edit'),
+    url(r'^study/(?P<study_id>\d+)/import_visit_edit/(?P<import_id>\d+)/execute',
+        views.study.import_visit_execute, name='web.views.import_visit_execute'),
+
     ####################
     #       EXPORT     #
     ####################
diff --git a/smash/web/views/study.py b/smash/web/views/study.py
index 8d8bc07d..2c1cdd67 100644
--- a/smash/web/views/study.py
+++ b/smash/web/views/study.py
@@ -8,7 +8,10 @@ from web.decorators import PermissionDecorator
 from web.forms import StudyColumnsEditForm, StudyEditForm, StudyNotificationParametersEditForm, \
     StudyRedCapColumnsEditForm
 from web.forms.custom_study_subject_field_forms import CustomStudySubjectFieldAddForm, CustomStudySubjectFieldEditForm
-from web.models import Study
+from web.forms.visit_import_data_form import VisitImportDataEditForm
+from web.importer import TnsCsvVisitImportReader
+from web.importer.log_storage import LogStorageHandler
+from web.models import Study, VisitImportData
 from web.models.custom_data import CustomStudySubjectField
 from web.views import wrap_response
 
@@ -51,11 +54,22 @@ def study_edit(request, study_id):
         redcap_columns_form = StudyRedCapColumnsEditForm(instance=study.redcap_columns,
                                                          prefix="redcap")
 
+    etl_entries = []
+    for import_data in VisitImportData.objects.filter(study=study).all():
+        etl_entries.append({'type': 'Import visit',
+                            'file': import_data.filename,
+                            'filetype': 'CSV',
+                            'run_at': import_data.run_at_times,
+                            'id': import_data.id,
+                            'available': import_data.filename != '' and import_data.filename is not None,
+                            'worker': str(import_data.import_worker)
+                            })
     return wrap_response(request, 'study/edit.html', {
         'study_form': study_form,
         'notifications_form': notifications_form,
         'study_columns_form': study_columns_form,
-        'redcap_columns_form': redcap_columns_form
+        'redcap_columns_form': redcap_columns_form,
+        'etl_entries': etl_entries
     })
 
 
@@ -96,9 +110,52 @@ def custom_study_subject_field_edit(request, study_id, field_id):
     })
 
 
+# noinspection PyUnusedLocal
 @PermissionDecorator('change_study', 'configuration')
 def custom_study_subject_field_delete(request, study_id, field_id):
     study = get_object_or_404(Study, id=study_id)
     field = get_object_or_404(CustomStudySubjectField, id=field_id)
     field.delete()
     return redirect('web.views.edit_study', study_id=study.id)
+
+
+@PermissionDecorator('change_study', 'configuration')
+def import_visit_edit(request, study_id, import_id):
+    study = get_object_or_404(Study, id=study_id)
+    import_data = get_object_or_404(VisitImportData, id=import_id)
+    if request.method == 'POST':
+        visit_import_data_form = VisitImportDataEditForm(request.POST, instance=import_data)
+        if visit_import_data_form.is_valid():
+            visit_import_data_form.save()
+            return redirect('web.views.edit_study', study_id=study.id)
+    else:
+        visit_import_data_form = VisitImportDataEditForm(instance=import_data)
+
+    return wrap_response(request, 'visit_import_data/edit.html', {
+        'form': visit_import_data_form,
+        'study_id': study.id
+    })
+
+
+@PermissionDecorator('change_study', 'configuration')
+def import_visit_execute(request, study_id, import_id):
+    study = get_object_or_404(Study, id=study_id)
+    import_data = get_object_or_404(VisitImportData, id=import_id)
+    if import_data.file_available():
+        reader = TnsCsvVisitImportReader(import_data)
+        log_storage = LogStorageHandler()
+        logging.getLogger('').addHandler(log_storage)
+        reader.load_data()
+        logging.getLogger('').removeHandler(log_storage)
+        messages.add_message(request, messages.SUCCESS,
+                             str(reader.processed_count) + ' appointment(s) were added/updated successfully.')
+        if reader.problematic_count > 0:
+            messages.add_message(request, messages.ERROR,
+                                 str(reader.problematic_count) + ' problematic entries encountered.')
+        if "WARNING" in log_storage.level_messages:
+            for entry in log_storage.level_messages["WARNING"]:
+                messages.add_message(request, messages.WARNING, entry)
+    else:
+        messages.add_message(request, messages.ERROR, import_data.get_absolute_file_path() + ' is not available.')
+
+    return redirect('web.views.edit_study', study_id=study.id)
-- 
GitLab