Completed
Push — master ( 8bc9c6...fbd831 )
by Paolo
22s queued 14s
created

submissions.views.ExportSubmissionView.get()   A

Complexity

Conditions 1

Size

Total Lines 31
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 31
rs 9.65
c 0
b 0
f 0
cc 1
nop 4
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
"""
4
Created on Tue Jul 24 15:49:23 2018
5
6
@author: Paolo Cozzi <[email protected]>
7
"""
8
9
import io
10
import re
11
import ast
12
import logging
13
14
from django.core.exceptions import ObjectDoesNotExist
15
from django.contrib import messages
16
from django.contrib.auth.mixins import LoginRequiredMixin
17
from django.http import HttpResponseRedirect, StreamingHttpResponse
18
from django.views.generic import (
19
    CreateView, DetailView, ListView, UpdateView, DeleteView)
20
from django.views.generic.detail import BaseDetailView
21
from django.views.generic.edit import BaseUpdateView
22
from django.shortcuts import get_object_or_404, redirect
23
from django.urls import reverse_lazy, reverse
24
25
from common.constants import (
26
    WAITING, ERROR, SUBMITTED, NEED_REVISION, CRYOWEB_TYPE, CRB_ANIM_TYPE,
27
    TIME_UNITS, VALIDATION_MESSAGES_ATTRIBUTES, SAMPLE_STORAGE,
28
    SAMPLE_STORAGE_PROCESSING, ACCURACIES, UNITS_VALIDATION_MESSAGES,
29
    VALUES_VALIDATION_MESSAGES)
30
from common.helpers import uid2biosample
31
from common.views import OwnerMixin, FormInvalidMixin
32
from crbanim.tasks import ImportCRBAnimTask
33
from cryoweb.tasks import ImportCryowebTask
34
35
from uid.models import Submission, Animal, Sample
36
from excel.tasks import ImportTemplateTask
37
38
from validation.helpers import construct_validation_message
39
from validation.models import ValidationSummary
40
from animals.tasks import BatchDeleteAnimals, BatchUpdateAnimals
41
from samples.tasks import BatchDeleteSamples, BatchUpdateSamples
42
43
from .forms import SubmissionForm, ReloadForm, UpdateSubmissionForm
44
from .helpers import is_target_in_message, AnimalResource, SampleResource
45
46
# Get an instance of a logger
47
logger = logging.getLogger(__name__)
48
49
50
class CreateSubmissionView(LoginRequiredMixin, FormInvalidMixin, CreateView):
51
    form_class = SubmissionForm
52
    model = Submission
53
54
    # template name is derived from model position and views type.
55
    # in this case, ir will be 'uid/submission_form.html' so
56
    # i need to clearly specify it
57
    template_name = "submissions/submission_form.html"
58
59
    # add user to this object
60
    def form_valid(self, form):
61
        self.object = form.save(commit=False)
62
        self.object.owner = self.request.user
63
64
        # I will have a different loading function accordingly with data type
65
        if self.object.datasource_type == CRYOWEB_TYPE:
66
            # update object and force status
67
            self.object.message = "waiting for data loading"
68
            self.object.status = WAITING
69
            self.object.save()
70
71
            # create a task
72
            my_task = ImportCryowebTask()
73
74
            # a valid submission start a task
75
            res = my_task.delay(self.object.pk)
76
            logger.info(
77
                "Start cryoweb importing process with task %s" % res.task_id)
78
79
        # I will have a different loading function accordingly with data type
80
        elif self.object.datasource_type == CRB_ANIM_TYPE:
81
            # update object and force status
82
            self.object.message = "waiting for data loading"
83
            self.object.status = WAITING
84
            self.object.save()
85
86
            # create a task
87
            my_task = ImportCRBAnimTask()
88
89
            # a valid submission start a task
90
            res = my_task.delay(self.object.pk)
91
            logger.info(
92
                "Start crbanim importing process with task %s" % res.task_id)
93
94
        else:
95
            # update object and force status
96
            self.object.message = "waiting for data loading"
97
            self.object.status = WAITING
98
            self.object.save()
99
100
            # create a task
101
            my_task = ImportTemplateTask()
102
103
            # a valid submission start a task
104
            res = my_task.delay(self.object.pk)
105
            logger.info(
106
                "Start template importing process with task %s" % res.task_id)
107
108
        # a redirect to self.object.get_absolute_url()
109
        return HttpResponseRedirect(self.get_success_url())
110
111
112
class MessagesSubmissionMixin(object):
113
    """Display messages in SubmissionViews"""
114
115
    # https://stackoverflow.com/a/45696442
116
    def get_context_data(self, **kwargs):
117
        data = super().get_context_data(**kwargs)
118
119
        # get the submission message
120
        message = self.submission.message
121
122
        # check if data are loaded or not
123
        if self.submission.status in [WAITING, SUBMITTED]:
124
            messages.warning(
125
                request=self.request,
126
                message=message,
127
                extra_tags="alert alert-dismissible alert-warning")
128
129
        elif self.submission.status in [ERROR, NEED_REVISION]:
130
            messages.error(
131
                request=self.request,
132
                message=message,
133
                extra_tags="alert alert-dismissible alert-danger")
134
135
        elif message is not None and message != '':
136
            messages.info(
137
                request=self.request,
138
                message=message,
139
                extra_tags="alert alert-dismissible alert-info")
140
141
        return data
142
143
144
class DetailSubmissionView(MessagesSubmissionMixin, OwnerMixin, DetailView):
145
    model = Submission
146
    template_name = "submissions/submission_detail.html"
147
148
    def get_context_data(self, **kwargs):
149
        # pass self.object to a new submission attribute in order to call
150
        # MessagesSubmissionMixin.get_context_data()
151
        self.submission = self.object
152
153
        # Call the base implementation first to get a context
154
        context = super(DetailSubmissionView, self).get_context_data(**kwargs)
155
156
        # add submission report to context
157
        validation_summary = construct_validation_message(self.submission)
158
159
        # HINT: is this computational intensive?
160
        context["validation_summary"] = validation_summary
161
162
        return context
163
164
165
class SubmissionValidationSummaryView(OwnerMixin, DetailView):
166
    model = Submission
167
    template_name = "submissions/submission_validation_summary.html"
168
169
    def get_context_data(self, **kwargs):
170
        context = super().get_context_data(**kwargs)
171
        summary_type = self.kwargs['type']
172
        try:
173
            validation_summary = self.object.validationsummary_set\
174
                .get(type=summary_type)
175
            context['validation_summary'] = validation_summary
176
177
            editable = list()
178
179
            for message in validation_summary.messages:
180
                message = ast.literal_eval(message)
181
182
                if 'offending_column' not in message:
183
                    txt = ("Old validation results, please re-run validation"
184
                           " step!")
185
                    logger.warning(txt)
186
                    messages.warning(
187
                        request=self.request,
188
                        message=txt,
189
                        extra_tags="alert alert-dismissible alert-warning")
190
                    editable.append(False)
191
192
                elif (uid2biosample(message['offending_column']) in
193
                        [val for sublist in VALIDATION_MESSAGES_ATTRIBUTES for
194
                         val in sublist]):
195
                    logger.debug(
196
                        "%s is editable" % message['offending_column'])
197
                    editable.append(True)
198
                else:
199
                    logger.debug(
200
                        "%s is not editable" % message['offending_column'])
201
                    editable.append(False)
202
203
            context['editable'] = editable
204
205
        except ObjectDoesNotExist:
206
            context['validation_summary'] = None
207
208
        context['submission'] = Submission.objects.get(pk=self.kwargs['pk'])
209
210
        return context
211
212
213
class EditSubmissionMixin():
214
    """A mixin to deal with Updates, expecially when searching ListViews"""
215
216 View Code Duplication
    def dispatch(self, request, *args, **kwargs):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
217
        handler = super(EditSubmissionMixin, self).dispatch(
218
                request, *args, **kwargs)
219
220
        # here I've done get_queryset. Check for submission status
221
        if hasattr(self, "submission") and not self.submission.can_edit():
222
            message = "Cannot edit submission: current status is: %s" % (
223
                    self.submission.get_status_display())
224
225
            logger.warning(message)
226
            messages.warning(
227
                request=self.request,
228
                message=message,
229
                extra_tags="alert alert-dismissible alert-warning")
230
231
            return redirect(self.submission.get_absolute_url())
232
233
        return handler
234
235
236
class SubmissionValidationSummaryFixErrorsView(
237
        EditSubmissionMixin, OwnerMixin, ListView):
238
    template_name = "submissions/submission_validation_summary_fix_errors.html"
239
240
    def get_queryset(self):
241
        """Define columns that need to change"""
242
243
        self.submission = get_object_or_404(
244
            Submission,
245
            pk=self.kwargs['pk'],
246
            owner=self.request.user)
247
248
        self.summary_type = self.kwargs['type']
249
        self.validation_summary = ValidationSummary.objects.get(
250
            submission=self.submission, type=self.summary_type)
251
        self.message = ast.literal_eval(self.validation_summary.messages[
252
                                            int(self.kwargs['message_counter'])
253
                                        ])
254
        self.offending_column = uid2biosample(
255
            self.message['offending_column'])
256
        self.show_units = True
257
        if is_target_in_message(self.message['message'],
258
                                UNITS_VALIDATION_MESSAGES):
259
            self.units = [unit.name for unit in TIME_UNITS]
260
            if self.offending_column == 'animal_age_at_collection':
261
                self.offending_column += "_units"
262
263
        elif is_target_in_message(self.message['message'],
264
                                  VALUES_VALIDATION_MESSAGES):
265
            if self.offending_column == 'storage':
266
                self.units = [unit.name for unit in SAMPLE_STORAGE]
267
            elif self.offending_column == 'storage_processing':
268
                self.units = [unit.name for unit in SAMPLE_STORAGE_PROCESSING]
269
            elif self.offending_column == 'collection_place_accuracy' or \
270
                    self.offending_column == 'birth_location_accuracy':
271
                self.units = [unit.name for unit in ACCURACIES]
272
        else:
273
            self.show_units = False
274
            self.units = None
275
        if self.summary_type == 'animal':
276
            return Animal.objects.filter(id__in=self.message['ids'])
277
        elif self.summary_type == 'sample':
278
            return Sample.objects.filter(id__in=self.message['ids'])
279
280
    def get_context_data(self, **kwargs):
281
        # Call the base implementation first to get a context
282
        context = super(
283
            SubmissionValidationSummaryFixErrorsView, self
284
        ).get_context_data(**kwargs)
285
286
        # add submission to context
287
        context["message"] = self.message
288
        context["type"] = self.summary_type
289
        context['attribute_to_edit'] = self.offending_column
290
        for attributes in VALIDATION_MESSAGES_ATTRIBUTES:
291
            if self.offending_column in attributes:
292
                context['attributes_to_show'] = [
293
                    attr for attr in attributes if
294
                    attr != self.offending_column
295
                ]
296
        context['submission'] = self.submission
297
        context['show_units'] = self.show_units
298
        if self.units:
299
            context['units'] = self.units
300
        return context
301
302
303
# a detail view since I need to operate on a submission object
304
# HINT: rename to a more informative name?
305
class EditSubmissionView(
306
        EditSubmissionMixin, MessagesSubmissionMixin, OwnerMixin, ListView):
307
    template_name = "submissions/submission_edit.html"
308
    paginate_by = 10
309
310
    # set the columns for this union query
311
    headers = [
312
        'id',
313
        'name',
314
        'material',
315
        'biosample_id',
316
        'status',
317
        'last_changed',
318
        'last_submitted'
319
    ]
320
321
    def get_queryset(self):
322
        """Subsetting names relying submission id"""
323
324
        self.submission = get_object_or_404(
325
            Submission,
326
            pk=self.kwargs['pk'],
327
            owner=self.request.user)
328
329
        # need to perform 2 distinct queryset
330
        animal_qs = Animal.objects.filter(
331
            submission=self.submission).values_list(*self.headers)
332
333
        sample_qs = Sample.objects.filter(
334
            submission=self.submission).values_list(*self.headers)
335
336
        return animal_qs.union(sample_qs).order_by('material', 'id')
337
338
    def get_context_data(self, **kwargs):
339
        # Call the base implementation first to get a context
340
        context = super(EditSubmissionView, self).get_context_data(**kwargs)
341
342
        # add submission to context
343
        context["submission"] = self.submission
344
345
        # modify queryset to a more useful object
346
        object_list = context["object_list"]
347
348
        # the new result object
349
        new_object_list = []
350
351
        for element in object_list:
352
            # modify element in a dictionary
353
            element = dict(zip(self.headers, element))
354
355
            if element['material'] == 'Organism':
356
                # change material to be more readable
357
                element['material'] = 'animal'
358
                element['model'] = Animal.objects.get(pk=element['id'])
359
360
            else:
361
                # this is a specimen
362
                element['material'] = 'sample'
363
                element['model'] = Sample.objects.get(pk=element['id'])
364
365
            new_object_list.append(element)
366
367
        # ovverride the default object list
368
        context["object_list"] = new_object_list
369
370
        return context
371
372
373
# streaming CSV large files, as described in
374
# https://docs.djangoproject.com/en/2.2/howto/outputting-csv/#streaming-large-csv-files
375
class ExportSubmissionView(OwnerMixin, BaseDetailView):
376
    model = Submission
377
378
    def get(self, request, *args, **kwargs):
379
        """A view that streams a large CSV file."""
380
381
        # required to call queryset and to initilize the proper BaseDetailView
382
        # attributes
383
        self.object = self.get_object()
384
385
        # ok define two distinct queryset to filter animals and samples
386
        # relying on a submission object (self.object)
387
        animal_qs = Animal.objects.filter(submission=self.object)
388
        sample_qs = Sample.objects.filter(submission=self.object)
389
390
        # get the two import_export.resources.ModelResource objects
391
        animal_resource = AnimalResource()
392
        sample_resource = SampleResource()
393
394
        # get the two data objects relying on custom queryset
395
        animal_dataset = animal_resource.export(animal_qs)
396
        sample_dataset = sample_resource.export(sample_qs)
397
398
        # merge the two tablib.Datasets into one
399
        merged_dataset = animal_dataset.stack(sample_dataset)
400
401
        # streaming a response
402
        response = StreamingHttpResponse(
403
            io.StringIO(merged_dataset.csv),
404
            content_type="text/csv")
405
        response['Content-Disposition'] = (
406
            'attachment; filename="submission_%s_names.csv"' % self.object.id)
407
408
        return response
409
410
411
class ListSubmissionsView(OwnerMixin, ListView):
412
    model = Submission
413
    template_name = "submissions/submission_list.html"
414
    ordering = ['-created_at']
415
    paginate_by = 10
416
417
418
class ReloadSubmissionView(OwnerMixin, FormInvalidMixin, UpdateView):
419
    form_class = ReloadForm
420
    model = Submission
421
    template_name = 'submissions/submission_reload.html'
422
423
    def form_valid(self, form):
424
        self.object = form.save(commit=False)
425
426
        # update object and force status
427
        self.object.message = "waiting for data loading"
428
        self.object.status = WAITING
429
        self.object.save()
430
431
        # call the proper method
432
        if self.object.datasource_type == CRYOWEB_TYPE:
433
            # a valid submission start a task
434
            my_task = ImportCryowebTask()
435
436
            res = my_task.delay(self.object.pk)
437
            logger.info(
438
                "Start cryoweb reload process with task %s" % res.task_id)
439
440
        elif self.object.datasource_type == CRB_ANIM_TYPE:
441
            # a valid submission start a task
442
            my_task = ImportCRBAnimTask()
443
444
            # a valid submission start a task
445
            res = my_task.delay(self.object.pk)
446
            logger.info(
447
                "Start crbanim reload process with task %s" % res.task_id)
448
449
        else:
450
            # a valid submission start a task
451
            my_task = ImportTemplateTask()
452
453
            # a valid submission start a task
454
            res = my_task.delay(self.object.pk)
455
            logger.info(
456
                "Start template reload process with task %s" % res.task_id)
457
458
        # a redirect to self.object.get_absolute_url()
459
        return HttpResponseRedirect(self.get_success_url())
460
461
462
class DeleteSubmissionMixin():
463
    """Prevent a delete relying on statuses"""
464
465 View Code Duplication
    def dispatch(self, request, *args, **kwargs):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
466
        handler = super(DeleteSubmissionMixin, self).dispatch(
467
                request, *args, **kwargs)
468
469
        # here I've done get_queryset. Check for submission status
470
        if hasattr(self, "object") and not self.object.can_edit():
471
            message = "Cannot delete %s: submission status is: %s" % (
472
                    self.object, self.object.get_status_display())
473
474
            logger.warning(message)
475
            messages.warning(
476
                request=self.request,
477
                message=message,
478
                extra_tags="alert alert-dismissible alert-warning")
479
480
            return redirect(self.object.get_absolute_url())
481
482
        return handler
483
484
485
class BatchDeleteMixin(
486
        DeleteSubmissionMixin, OwnerMixin):
487
488
    model = Submission
489
    delete_type = None
490
491
    def get_context_data(self, **kwargs):
492
        """Add custom values to template context"""
493
494
        context = super().get_context_data(**kwargs)
495
496
        context['delete_type'] = self.delete_type
497
        context['pk'] = self.object.id
498
499
        return context
500
501
    def post(self, request, *args, **kwargs):
502
        # get object (Submission) like BaseUpdateView does
503
        submission = self.get_object()
504
505
        # get arguments from post object
506
        pk = self.kwargs['pk']
507
        keys_to_delete = set()
508
509
        # process all keys in form
510
        for key in request.POST['to_delete'].split('\n'):
511
            keys_to_delete.add(key.rstrip())
512
513
        submission.message = 'waiting for batch delete to complete'
514
        submission.status = WAITING
515
        submission.save()
516
517
        if self.delete_type == 'Animals':
518
            # Batch delete task for animals
519
            my_task = BatchDeleteAnimals()
520
            summary_obj, created = ValidationSummary.objects.get_or_create(
521
                submission=submission, type='animal')
522
523
        elif self.delete_type == 'Samples':
524
            # Batch delete task for samples
525
            my_task = BatchDeleteSamples()
526
            summary_obj, created = ValidationSummary.objects.get_or_create(
527
                submission=submission, type='sample')
528
529
        # reset validation counters
530
        summary_obj.reset()
0 ignored issues
show
introduced by
The variable summary_obj does not seem to be defined for all execution paths.
Loading history...
531
        res = my_task.delay(pk, [item for item in keys_to_delete])
0 ignored issues
show
introduced by
The variable my_task does not seem to be defined for all execution paths.
Loading history...
532
533
        logger.info(
534
            "Start %s batch delete with task %s" % (
535
                self.delete_type, res.task_id))
536
537
        return HttpResponseRedirect(reverse('submissions:detail', args=(pk,)))
538
539
540
class DeleteAnimalsView(BatchDeleteMixin, DetailView):
541
    model = Submission
542
    template_name = 'submissions/submission_batch_delete.html'
543
    delete_type = 'Animals'
544
545
546
class DeleteSamplesView(BatchDeleteMixin, DetailView):
547
    model = Submission
548
    template_name = 'submissions/submission_batch_delete.html'
549
    delete_type = 'Samples'
550
551
552
class DeleteSubmissionView(DeleteSubmissionMixin, OwnerMixin, DeleteView):
553
    model = Submission
554
    template_name = "submissions/submission_confirm_delete.html"
555
    success_url = reverse_lazy('uid:dashboard')
556
557
    # https://stackoverflow.com/a/39533619/4385116
558
    def get_context_data(self, **kwargs):
559
        # determining related objects
560
        context = super().get_context_data(**kwargs)
561
562
        # counting object relying submission
563
        animal_count = Animal.objects.filter(
564
            submission=self.object).count()
565
        sample_count = Sample.objects.filter(
566
            submission=self.object).count()
567
568
        # get only sample and animals from model_count
569
        info_deleted = {
570
            'animals': animal_count,
571
            'samples': sample_count
572
        }
573
574
        # add info to context
575
        context['info_deleted'] = dict(info_deleted).items()
576
577
        return context
578
579
    # https://ccbv.co.uk/projects/Django/1.11/django.views.generic.edit/DeleteView/#delete
580
    def delete(self, request, *args, **kwargs):
581
        """
582
        Add a message after calling base delete method
583
        """
584
585
        httpresponseredirect = super().delete(request, *args, **kwargs)
586
587
        message = "Submission %s was successfully deleted" % self.object.title
588
        logger.info(message)
589
590
        messages.info(
591
            request=self.request,
592
            message=message,
593
            extra_tags="alert alert-dismissible alert-info")
594
595
        return httpresponseredirect
596
597
598
class UpdateSubmissionView(OwnerMixin, FormInvalidMixin, UpdateView):
599
    form_class = UpdateSubmissionForm
600
    model = Submission
601
    template_name = 'submissions/submission_update.html'
602
603
604
class FixValidation(OwnerMixin, BaseUpdateView):
605
    model = Submission
606
607
    def post(self, request, **kwargs):
608
        # get object (Submission) like BaseUpdateView does
609
        submission = self.get_object()
610
611
        # Fetch all required ids from input names and use it as keys
612
        keys_to_fix = dict()
613
        for key_to_fix in request.POST:
614
            if 'to_edit' in key_to_fix:
615
                keys_to_fix[
616
                    int(re.search('to_edit(.*)', key_to_fix).groups()[0])] \
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable int does not seem to be defined.
Loading history...
617
                    = request.POST[key_to_fix]
618
619
        pk = self.kwargs['pk']
620
        record_type = self.kwargs['record_type']
621
        attribute_to_edit = self.kwargs['attribute_to_edit']
622
623
        submission.message = "waiting for data updating"
624
        submission.status = WAITING
625
        submission.save()
626
627
        # Update validation summary
628
        summary_obj, created = ValidationSummary.objects.get_or_create(
629
            submission=submission, type=record_type)
630
        summary_obj.submission = submission
631
        summary_obj.reset()
632
633
        # create a task
634
        if record_type == 'animal':
635
            my_task = BatchUpdateAnimals()
636
        elif record_type == 'sample':
637
            my_task = BatchUpdateSamples()
638
        else:
639
            return HttpResponseRedirect(
640
                reverse('submissions:detail', args=(pk,)))
641
642
        # a valid submission start a task
643
        res = my_task.delay(pk, keys_to_fix, attribute_to_edit)
644
        logger.info(
645
            "Start fix validation process with task %s" % res.task_id)
646
        return HttpResponseRedirect(reverse('submissions:detail', args=(pk,)))
647