Issues (70)

django-data/image/submissions/views.py (5 issues)

1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
"""
4
Created on Tue Jul 24 15:49:23 2018
5
6
@author: Paolo Cozzi <[email protected]>
7
"""
8
9
import io
10
import re
11
import logging
12
13
from django.core.exceptions import ObjectDoesNotExist
14
from django.contrib import messages
15
from django.contrib.auth.mixins import LoginRequiredMixin
16
from django.http import HttpResponseRedirect, StreamingHttpResponse
17
from django.views.generic import (
18
    CreateView, DetailView, ListView, UpdateView, DeleteView)
19
from django.views.generic.detail import BaseDetailView
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
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, AnimalResource, SampleResource
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
                if 'offending_column' not in message:
180
                    txt = ("Old validation results, please re-run validation"
181
                           " step!")
182
                    logger.warning(txt)
183
                    messages.warning(
184
                        request=self.request,
185
                        message=txt,
186
                        extra_tags="alert alert-dismissible alert-warning")
187
                    editable.append(False)
188
189
                elif (uid2biosample(message['offending_column']) in
190
                        [val for sublist in VALIDATION_MESSAGES_ATTRIBUTES for
191
                         val in sublist]):
192
                    logger.debug(
193
                        "%s is editable" % message['offending_column'])
194
                    editable.append(True)
195
                else:
196
                    logger.debug(
197
                        "%s is not editable" % message['offending_column'])
198
                    editable.append(False)
199
200
            context['editable'] = editable
201
202
        except ObjectDoesNotExist:
203
            context['validation_summary'] = None
204
205
        context['submission'] = Submission.objects.get(pk=self.kwargs['pk'])
206
207
        return context
208
209
210
class EditSubmissionMixin():
211
    """A mixin to deal with Updates, expecially when searching ListViews"""
212
213 View Code Duplication
    def dispatch(self, request, *args, **kwargs):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
214
        handler = super(EditSubmissionMixin, self).dispatch(
215
                request, *args, **kwargs)
216
217
        # here I've done get_queryset. Check for submission status
218
        if hasattr(self, "submission") and not self.submission.can_edit():
219
            message = "Cannot edit submission: current status is: %s" % (
220
                    self.submission.get_status_display())
221
222
            logger.warning(message)
223
            messages.warning(
224
                request=self.request,
225
                message=message,
226
                extra_tags="alert alert-dismissible alert-warning")
227
228
            return redirect(self.submission.get_absolute_url())
229
230
        return handler
231
232
233
class SubmissionValidationSummaryFixErrorsView(
234
        EditSubmissionMixin, OwnerMixin, ListView):
235
    template_name = "submissions/submission_validation_summary_fix_errors.html"
236
237
    def get_queryset(self):
238
        """Define columns that need to change"""
239
240
        self.submission = get_object_or_404(
241
            Submission,
242
            pk=self.kwargs['pk'],
243
            owner=self.request.user)
244
245
        self.summary_type = self.kwargs['type']
246
        self.validation_summary = ValidationSummary.objects.get(
247
            submission=self.submission, type=self.summary_type)
248
        self.message = self.validation_summary.messages[
249
            int(self.kwargs['message_counter'])]
250
251
        self.offending_column = uid2biosample(
252
            self.message['offending_column'])
253
        self.show_units = True
254
        if is_target_in_message(self.message['message'],
255
                                UNITS_VALIDATION_MESSAGES):
256
            self.units = [unit.name for unit in TIME_UNITS]
257
            if self.offending_column == 'animal_age_at_collection':
258
                self.offending_column += "_units"
259
260
        elif is_target_in_message(self.message['message'],
261
                                  VALUES_VALIDATION_MESSAGES):
262
            if self.offending_column == 'storage':
263
                self.units = [unit.name for unit in SAMPLE_STORAGE]
264
            elif self.offending_column == 'storage_processing':
265
                self.units = [unit.name for unit in SAMPLE_STORAGE_PROCESSING]
266
            elif self.offending_column == 'collection_place_accuracy' or \
267
                    self.offending_column == 'birth_location_accuracy':
268
                self.units = [unit.name for unit in ACCURACIES]
269
        else:
270
            self.show_units = False
271
            self.units = None
272
        if self.summary_type == 'animal':
273
            return Animal.objects.filter(id__in=self.message['ids'])
274
        elif self.summary_type == 'sample':
275
            return Sample.objects.filter(id__in=self.message['ids'])
276
277
    def get_context_data(self, **kwargs):
278
        # Call the base implementation first to get a context
279
        context = super(
280
            SubmissionValidationSummaryFixErrorsView, self
281
        ).get_context_data(**kwargs)
282
283
        # add submission to context
284
        context["message"] = self.message
285
        context["type"] = self.summary_type
286
        context['attribute_to_edit'] = self.offending_column
287
        for attributes in VALIDATION_MESSAGES_ATTRIBUTES:
288
            if self.offending_column in attributes:
289
                context['attributes_to_show'] = [
290
                    attr for attr in attributes if
291
                    attr != self.offending_column
292
                ]
293
        context['submission'] = self.submission
294
        context['show_units'] = self.show_units
295
        if self.units:
296
            context['units'] = self.units
297
        return context
298
299
300
# a detail view since I need to operate on a submission object
301
# HINT: rename to a more informative name?
302
class EditSubmissionView(
303
        EditSubmissionMixin, MessagesSubmissionMixin, OwnerMixin, ListView):
304
    template_name = "submissions/submission_edit.html"
305
    paginate_by = 10
306
307
    # set the columns for this union query
308
    headers = [
309
        'id',
310
        'name',
311
        'material',
312
        'biosample_id',
313
        'status',
314
        'last_changed',
315
        'last_submitted'
316
    ]
317
318
    def get_queryset(self):
319
        """Subsetting names relying submission id"""
320
321
        self.submission = get_object_or_404(
322
            Submission,
323
            pk=self.kwargs['pk'],
324
            owner=self.request.user)
325
326
        # need to perform 2 distinct queryset
327
        animal_qs = Animal.objects.filter(
328
            submission=self.submission).values_list(*self.headers)
329
330
        sample_qs = Sample.objects.filter(
331
            submission=self.submission).values_list(*self.headers)
332
333
        return animal_qs.union(sample_qs).order_by('material', 'id')
334
335
    def get_context_data(self, **kwargs):
336
        # Call the base implementation first to get a context
337
        context = super(EditSubmissionView, self).get_context_data(**kwargs)
338
339
        # add submission to context
340
        context["submission"] = self.submission
341
342
        # modify queryset to a more useful object
343
        object_list = context["object_list"]
344
345
        # the new result object
346
        new_object_list = []
347
348
        for element in object_list:
349
            # modify element in a dictionary
350
            element = dict(zip(self.headers, element))
351
352
            if element['material'] == 'Organism':
353
                # change material to be more readable
354
                element['material'] = 'animal'
355
                element['model'] = Animal.objects.get(pk=element['id'])
356
357
            else:
358
                # this is a specimen
359
                element['material'] = 'sample'
360
                element['model'] = Sample.objects.get(pk=element['id'])
361
362
            new_object_list.append(element)
363
364
        # ovverride the default object list
365
        context["object_list"] = new_object_list
366
367
        return context
368
369
370
# streaming CSV large files, as described in
371
# https://docs.djangoproject.com/en/2.2/howto/outputting-csv/#streaming-large-csv-files
372
class ExportSubmissionView(OwnerMixin, BaseDetailView):
373
    model = Submission
374
375
    def get(self, request, *args, **kwargs):
376
        """A view that streams a large CSV file."""
377
378
        # required to call queryset and to initilize the proper BaseDetailView
379
        # attributes
380
        self.object = self.get_object()
381
382
        # ok define two distinct queryset to filter animals and samples
383
        # relying on a submission object (self.object)
384
        animal_qs = Animal.objects.filter(submission=self.object)
385
        sample_qs = Sample.objects.filter(submission=self.object)
386
387
        # get the two import_export.resources.ModelResource objects
388
        animal_resource = AnimalResource()
389
        sample_resource = SampleResource()
390
391
        # get the two data objects relying on custom queryset
392
        animal_dataset = animal_resource.export(animal_qs)
393
        sample_dataset = sample_resource.export(sample_qs)
394
395
        # merge the two tablib.Datasets into one
396
        merged_dataset = animal_dataset.stack(sample_dataset)
397
398
        # streaming a response
399
        response = StreamingHttpResponse(
400
            io.StringIO(merged_dataset.csv),
401
            content_type="text/csv")
402
        response['Content-Disposition'] = (
403
            'attachment; filename="submission_%s_names.csv"' % self.object.id)
404
405
        return response
406
407
408
class ListSubmissionsView(OwnerMixin, ListView):
409
    model = Submission
410
    template_name = "submissions/submission_list.html"
411
    ordering = ['-created_at']
412
    paginate_by = 10
413
414
415
class ReloadSubmissionView(OwnerMixin, FormInvalidMixin, UpdateView):
416
    form_class = ReloadForm
417
    model = Submission
418
    template_name = 'submissions/submission_reload.html'
419
420
    def form_valid(self, form):
421
        self.object = form.save(commit=False)
422
423
        # update object and force status
424
        self.object.message = "waiting for data loading"
425
        self.object.status = WAITING
426
        self.object.save()
427
428
        # call the proper method
429
        if self.object.datasource_type == CRYOWEB_TYPE:
430
            # a valid submission start a task
431
            my_task = ImportCryowebTask()
432
433
            res = my_task.delay(self.object.pk)
434
            logger.info(
435
                "Start cryoweb reload process with task %s" % res.task_id)
436
437
        elif self.object.datasource_type == CRB_ANIM_TYPE:
438
            # a valid submission start a task
439
            my_task = ImportCRBAnimTask()
440
441
            # a valid submission start a task
442
            res = my_task.delay(self.object.pk)
443
            logger.info(
444
                "Start crbanim reload process with task %s" % res.task_id)
445
446
        else:
447
            # a valid submission start a task
448
            my_task = ImportTemplateTask()
449
450
            # a valid submission start a task
451
            res = my_task.delay(self.object.pk)
452
            logger.info(
453
                "Start template reload process with task %s" % res.task_id)
454
455
        # a redirect to self.object.get_absolute_url()
456
        return HttpResponseRedirect(self.get_success_url())
457
458
459
class DeleteSubmissionMixin():
460
    """Prevent a delete relying on statuses"""
461
462 View Code Duplication
    def dispatch(self, request, *args, **kwargs):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
463
        handler = super(DeleteSubmissionMixin, self).dispatch(
464
                request, *args, **kwargs)
465
466
        # here I've done get_queryset. Check for submission status
467
        if hasattr(self, "object") and not self.object.can_delete():
468
            message = "Cannot delete %s: submission status is: %s" % (
469
                    self.object, self.object.get_status_display())
470
471
            logger.warning(message)
472
            messages.warning(
473
                request=self.request,
474
                message=message,
475
                extra_tags="alert alert-dismissible alert-warning")
476
477
            return redirect(self.object.get_absolute_url())
478
479
        return handler
480
481
482
class BatchDeleteMixin(
483
        DeleteSubmissionMixin, OwnerMixin):
484
485
    model = Submission
486
    delete_type = None
487
488
    def get_context_data(self, **kwargs):
489
        """Add custom values to template context"""
490
491
        context = super().get_context_data(**kwargs)
492
493
        context['delete_type'] = self.delete_type
494
        context['pk'] = self.object.id
495
496
        return context
497
498
    def post(self, request, *args, **kwargs):
499
        # get object (Submission) like BaseUpdateView does
500
        submission = self.get_object()
501
502
        # get arguments from post object
503
        pk = self.kwargs['pk']
504
        keys_to_delete = set()
505
506
        # process all keys in form
507
        for key in request.POST['to_delete'].split('\n'):
508
            keys_to_delete.add(key.rstrip())
509
510
        submission.message = 'waiting for batch delete to complete'
511
        submission.status = WAITING
512
        submission.save()
513
514
        if self.delete_type == 'Animals':
515
            # Batch delete task for animals
516
            my_task = BatchDeleteAnimals()
517
            summary_obj, created = ValidationSummary.objects.get_or_create(
518
                submission=submission, type='animal')
519
520
        elif self.delete_type == 'Samples':
521
            # Batch delete task for samples
522
            my_task = BatchDeleteSamples()
523
            summary_obj, created = ValidationSummary.objects.get_or_create(
524
                submission=submission, type='sample')
525
526
        # reset validation counters
527
        summary_obj.reset()
0 ignored issues
show
The variable summary_obj does not seem to be defined for all execution paths.
Loading history...
528
        res = my_task.delay(pk, [item for item in keys_to_delete])
0 ignored issues
show
The variable my_task does not seem to be defined for all execution paths.
Loading history...
529
530
        logger.info(
531
            "Start %s batch delete with task %s" % (
532
                self.delete_type, res.task_id))
533
534
        return HttpResponseRedirect(reverse('submissions:detail', args=(pk,)))
535
536
537
class DeleteAnimalsView(BatchDeleteMixin, DetailView):
538
    model = Submission
539
    template_name = 'submissions/submission_batch_delete.html'
540
    delete_type = 'Animals'
541
542
543
class DeleteSamplesView(BatchDeleteMixin, DetailView):
544
    model = Submission
545
    template_name = 'submissions/submission_batch_delete.html'
546
    delete_type = 'Samples'
547
548
549
class DeleteSubmissionView(DeleteSubmissionMixin, OwnerMixin, DeleteView):
550
    model = Submission
551
    template_name = "submissions/submission_confirm_delete.html"
552
    success_url = reverse_lazy('uid:dashboard')
553
554
    # https://stackoverflow.com/a/39533619/4385116
555
    def get_context_data(self, **kwargs):
556
        # determining related objects
557
        context = super().get_context_data(**kwargs)
558
559
        # counting object relying submission
560
        animal_count = Animal.objects.filter(
561
            submission=self.object).count()
562
        sample_count = Sample.objects.filter(
563
            submission=self.object).count()
564
565
        # get only sample and animals from model_count
566
        info_deleted = {
567
            'animals': animal_count,
568
            'samples': sample_count
569
        }
570
571
        # add info to context
572
        context['info_deleted'] = dict(info_deleted).items()
573
574
        return context
575
576
    # https://ccbv.co.uk/projects/Django/1.11/django.views.generic.edit/DeleteView/#delete
577
    def delete(self, request, *args, **kwargs):
578
        """
579
        Add a message after calling base delete method
580
        """
581
582
        httpresponseredirect = super().delete(request, *args, **kwargs)
583
584
        message = "Submission %s was successfully deleted" % self.object.title
585
        logger.info(message)
586
587
        messages.info(
588
            request=self.request,
589
            message=message,
590
            extra_tags="alert alert-dismissible alert-info")
591
592
        return httpresponseredirect
593
594
595
class UpdateSubmissionView(OwnerMixin, FormInvalidMixin, UpdateView):
596
    form_class = UpdateSubmissionForm
597
    model = Submission
598
    template_name = 'submissions/submission_update.html'
599
600
601
class FixValidation(OwnerMixin, BaseUpdateView):
602
    model = Submission
603
604
    def post(self, request, **kwargs):
605
        # get object (Submission) like BaseUpdateView does
606
        submission = self.get_object()
607
608
        # Fetch all required ids from input names and use it as keys
609
        keys_to_fix = dict()
610
        for key_to_fix in request.POST:
611
            if 'to_edit' in key_to_fix:
612
                keys_to_fix[
613
                    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...
614
                    = request.POST[key_to_fix]
615
616
        pk = self.kwargs['pk']
617
        record_type = self.kwargs['record_type']
618
        attribute_to_edit = self.kwargs['attribute_to_edit']
619
620
        submission.message = "waiting for data updating"
621
        submission.status = WAITING
622
        submission.save()
623
624
        # Update validation summary
625
        summary_obj, created = ValidationSummary.objects.get_or_create(
626
            submission=submission, type=record_type)
627
        summary_obj.submission = submission
628
        summary_obj.reset()
629
630
        # create a task
631
        if record_type == 'animal':
632
            my_task = BatchUpdateAnimals()
633
        elif record_type == 'sample':
634
            my_task = BatchUpdateSamples()
635
        else:
636
            return HttpResponseRedirect(
637
                reverse('submissions:detail', args=(pk,)))
638
639
        # a valid submission start a task
640
        res = my_task.delay(pk, keys_to_fix, attribute_to_edit)
641
        logger.info(
642
            "Start fix validation process with task %s" % res.task_id)
643
        return HttpResponseRedirect(reverse('submissions:detail', args=(pk,)))
644