Completed
Pull Request — master (#41)
by Paolo
08:04
created

submissions.views   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 563
Duplicated Lines 6.39 %

Importance

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