Completed
Push — master ( 4f7ee6...646424 )
by Paolo
08:30 queued 06:53
created

CreateSubmissionView.form_valid()   A

Complexity

Conditions 3

Size

Total Lines 50
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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