Completed
Push — master ( 726486...b38261 )
by Paolo
19s queued 14s
created

submissions.views   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 576
Duplicated Lines 6.25 %

Importance

Changes 0
Metric Value
wmc 56
eloc 358
dl 36
loc 576
rs 5.5199
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
B MessagesSubmissionMixin.get_context_data() 0 26 5
A CreateSubmissionView.form_invalid() 0 7 1
A DetailSubmissionView.get_context_data() 0 15 1
A CreateSubmissionView.form_valid() 0 47 3
A DeleteSubmissionView.delete() 0 16 1
A DeleteAnimalsView.get_context_data() 0 9 1
A EditSubmissionView.dispatch() 18 18 3
A EditSubmissionView.get_context_data() 0 8 1
A DeleteSubmissionView.get_context_data() 0 21 3
B FixValidation.post() 0 40 5
B SubmissionValidationSummaryView.get_context_data() 0 42 5
C SubmissionValidationSummaryFixErrorsView.get_queryset() 0 39 10
A SubmissionValidationSummaryFixErrorsView.get_context_data() 0 21 4
A DeleteSamplesView.get_context_data() 0 9 1
A ReloadSubmissionView.form_valid() 0 37 3
A ReloadSubmissionView.form_invalid() 0 7 1
A DeleteSubmissionView.dispatch() 18 18 3
A EditSubmissionView.get_queryset() 0 17 1
A BatchDelete.post() 0 37 4

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