Completed
Pull Request — devel (#72)
by Paolo
06:48
created

submissions.views   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 623
Duplicated Lines 5.78 %

Importance

Changes 0
Metric Value
wmc 53
eloc 381
dl 36
loc 623
rs 6.96
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
B MessagesSubmissionMixin.get_context_data() 0 26 5
A EditSubmissionMixin.dispatch() 18 18 3
B SubmissionValidationSummaryView.get_context_data() 0 42 5
C SubmissionValidationSummaryFixErrorsView.get_queryset() 0 39 10
A DetailSubmissionView.get_context_data() 0 15 1
A SubmissionValidationSummaryFixErrorsView.get_context_data() 0 21 4
A CreateSubmissionView.form_valid() 0 50 3
A DeleteSubmissionView.delete() 0 16 1
A EditSubmissionView.get_context_data() 0 48 3
A DeleteSubmissionView.get_context_data() 0 20 1
B FixValidation.post() 0 40 5
A BatchDeleteMixin.post() 0 37 4
A ReloadSubmissionView.form_valid() 0 37 3
A DeleteSubmissionMixin.dispatch() 18 18 3
A BatchDeleteMixin.get_context_data() 0 9 1
A EditSubmissionView.get_queryset() 0 16 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like submissions.views often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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