Passed
Pull Request — master (#55)
by Paolo
05:45
created

biosample.views.SubmitView.form_valid()   B

Complexity

Conditions 8

Size

Total Lines 65
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 65
rs 7.1253
c 0
b 0
f 0
cc 8
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
2
import os
3
import redis
4
import logging
5
6
from decouple import AutoConfig
7
8
from django.conf import settings
9
from django.urls import reverse_lazy, reverse
10
from django.utils.crypto import get_random_string
11
from django.utils.http import is_safe_url
12
from django.shortcuts import redirect, get_object_or_404
13
from django.contrib.auth import get_user_model
14
from django.contrib.auth.mixins import LoginRequiredMixin
15
from django.views.generic import TemplateView
16
from django.views.generic.edit import FormView, CreateView, ModelFormMixin
17
from django.contrib import messages
18
from django.http import HttpResponseRedirect
19
20
from pyUSIrest.client import User
21
22
from common.constants import WAITING
23
from image_app.models import Submission
24
25
from .forms import (
26
    GenerateTokenForm, RegisterUserForm, CreateUserForm, SubmitForm)
27
from .models import Account, ManagedTeam
28
from .helpers import get_auth, get_manager_auth
29
from .tasks import SplitSubmissionTask
30
31
32
# Get an instance of a logger
33
logger = logging.getLogger(__name__)
34
35
36
# define a decouple config object
37
settings_dir = os.path.join(settings.BASE_DIR, 'image')
38
config = AutoConfig(search_path=settings_dir)
39
40
41
# In programming, a mixin is a class that provides functionality to be
42
# inherited, but isn’t meant for instantiation on its own. In programming
43
# languages with multiple inheritance, mixins can be used to add enhanced
44
# functionality and behavior to classes.
45
class TokenMixin(object):
46
    """Get common stuff for Token visualization. Redirect to AAP registration
47
    if no valid AAP credentials are found for request.user"""
48
49
    def get_initial(self):
50
        """
51
        Returns the initial data to use for forms on this view.
52
        """
53
54
        initial = super(TokenMixin, self).get_initial()
55
        initial['name'] = self.request.user.biosample_account.name
56
57
        return initial
58
59 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...
60
        # get user from request and user model. This could be also Anonymous
61
        # user:however with metod decorator a login is required before dispatch
62
        # method is called
63
        User = get_user_model()
64
        user = self.request.user
65
66
        try:
67
            user.biosample_account
68
69
        except User.biosample_account.RelatedObjectDoesNotExist:
70
            logger.warning("Error for user:%s: not managed" % user)
71
            messages.warning(
72
                request=self.request,
73
                message='You need to register a valid AAP profile',
74
                extra_tags="alert alert-dismissible alert-warning")
75
76
            return redirect('accounts:registration_activation_complete')
77
78
        else:
79
            # call the default get method
80
            return super(
81
                TokenMixin, self).dispatch(request, *args, **kwargs)
82
83
84
class RegisterMixin(object):
85
    """If a biosample account is already registered, returns to dashboard"""
86
87 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...
88
        # get user from request and user model. This could be also Anonymous
89
        # user:however with metod decorator a login is required before dispatch
90
        # method is called
91
        User = get_user_model()
92
        user = self.request.user
93
94
        try:
95
            user.biosample_account
96
97
        except User.biosample_account.RelatedObjectDoesNotExist:
98
            # call the default get method
99
            return super(
100
                RegisterMixin, self).dispatch(request, *args, **kwargs)
101
102
        else:
103
            logger.warning("Error for user:%s: Already registered" % user)
104
            messages.warning(
105
                request=self.request,
106
                message='Your AAP profile is already registered',
107
                extra_tags="alert alert-dismissible alert-warning")
108
109
            return redirect('image_app:dashboard')
110
111
112
class MyFormMixin(object):
113
    """Common stuff for token generation"""
114
115
    success_url_message = "Please set this variable"
116
    success_url = reverse_lazy("image_app:dashboard")
117
118
    # add the request to the kwargs
119
    # https://chriskief.com/2012/12/18/django-modelform-formview-and-the-request-object/
120
    # this is needed to display messages (django.contronb) on pages
121
    def get_form_kwargs(self):
122
        kwargs = super(MyFormMixin, self).get_form_kwargs()
123
        kwargs['request'] = self.request
124
        return kwargs
125
126
    def get_success_url(self):
127
        """Override default function"""
128
129
        messages.success(
130
            request=self.request,
131
            message=self.success_url_message,
132
            extra_tags="alert alert-dismissible alert-success")
133
134
        return super(MyFormMixin, self).get_success_url()
135
136
    def form_invalid(self, form):
137
        messages.error(
138
            self.request,
139
            message="Please correct the errors below",
140
            extra_tags="alert alert-dismissible alert-danger")
141
142
        return super(MyFormMixin, self).form_invalid(form)
143
144
    def generate_token(self, form):
145
        """Generate token from form instance"""
146
147
        # operate on form data
148
        name = form.cleaned_data['name']
149
        password = form.cleaned_data['password']
150
151
        try:
152
            auth = get_auth(user=name, password=password)
153
154
        except ConnectionError as e:
155
            # logger exception. With repr() the exception name is rendered
156
            logger.error(repr(e))
157
158
            # maybe I typed a wrong password or there is an issue in biosample
159
            # log error in message and return form_invalid
160
            # HINT: deal with two conditions?
161
162
            # parse error message
163
            messages.error(
164
                self.request,
165
                "Unable to generate token: %s" % str(e),
166
                extra_tags="alert alert-dismissible alert-danger")
167
168
            # cant't return form_invalid here, since i need to process auth
169
            return None
170
171
        else:
172
            logger.debug("Token generated for user:%s" % name)
173
174
            self.request.session['token'] = auth.token
175
176
            # return an auth object
177
            return auth
178
179
180
class GenerateTokenView(LoginRequiredMixin, TokenMixin, MyFormMixin, FormView):
181
    """Generate AAP token. If user is not registered, redirect to accounts
182
    registration_activation_complete through TokenMixin. If yes generate
183
    token through MyFormMixin"""
184
185
    template_name = 'biosample/generate_token.html'
186
    form_class = GenerateTokenForm
187
    success_url_message = 'Token generated!'
188
189
    def dispatch(self, request, *args, **kwargs):
190
        # try to read next link
191
        next_url = request.GET.get('next', None)
192
193
        # redirect to next url. is_safe_url: is a safe redirection
194
        # (i.e. it doesn't point to a different host and uses a safe scheme).
195
        if next_url and is_safe_url(next_url):
196
            logger.debug("Got %s as next_url" % next_url)
197
            self.success_url = next_url
198
199
        return super(
200
            GenerateTokenView, self).dispatch(request, *args, **kwargs)
201
202
    def form_valid(self, form):
203
        # This method is called when valid form data has been POSTed.
204
        # It should return an HttpResponse.
205
206
        # call MyFormMixin method and generate token. Check user/password
207
        if not self.generate_token(form):
208
            return self.form_invalid(form)
209
210
        return redirect(self.get_success_url())
211
212
213
class TokenView(LoginRequiredMixin, TokenMixin, TemplateView):
214
    """Visualize token details"""
215
216
    template_name = 'biosample/token.html'
217
218
    def get_context_data(self, **kwargs):
219
        # Call the base implementation first to get a context
220
        context = super(TokenView, self).get_context_data(**kwargs)
221
222
        # get user and team object
223
        context['name'] = self.request.user.biosample_account.name
224
        context['team'] = self.request.user.biosample_account.team
225
226
        try:
227
            # add content to context
228
            auth = get_auth(token=self.request.session['token'])
229
230
            if auth.is_expired():
231
                messages.error(
232
                    self.request,
233
                    "Your token is expired",
234
                    extra_tags="alert alert-dismissible alert-danger")
235
236
            context["auth"] = auth
237
238
        except KeyError as e:
239
            logger.error(
240
                "No valid token found for %s: %s" % (
241
                    self.request.user,
242
                    e))
243
244
            messages.error(
245
                self.request,
246
                "You haven't generated any token yet",
247
                extra_tags="alert alert-dismissible alert-danger")
248
249
        return context
250
251
252
class RegisterUserView(LoginRequiredMixin, RegisterMixin, MyFormMixin,
253
                       CreateView):
254
    """Register an already existent AAP account"""
255
256
    template_name = 'biosample/register_user.html'
257
    form_class = RegisterUserForm
258
    success_url_message = 'Account registered'
259
260
    def form_valid(self, form):
261
        # This method is called when valid form data has been POSTed.
262
        # It should return an HttpResponse.
263
        team = form.cleaned_data['team']
264
265
        # call AuthMixin method and generate token. Check user/password
266
        auth = self.generate_token(form)
267
268
        if not auth:
269
            return self.form_invalid(form)
270
271
        if team.name not in auth.claims['domains']:
272
            messages.error(
273
                self.request,
274
                "You don't belong to team: %s" % team,
275
                extra_tags="alert alert-dismissible alert-danger")
276
277
            # return invalid form
278
            return self.form_invalid(form)
279
280
        # add a user to object (comes from section not from form)
281
        self.object = form.save(commit=False)
282
        self.object.user = self.request.user
283
        self.object.save()
284
285
        # call to a specific function (which does an HttpResponseRedirect
286
        # to success_url)
287
        return super(ModelFormMixin, self).form_valid(form)
288
289
290
class CreateUserView(LoginRequiredMixin, RegisterMixin, MyFormMixin, FormView):
291
    """Create a new AAP account"""
292
293
    template_name = 'biosample/create_user.html'
294
    form_class = CreateUserForm
295
    success_url_message = "Account created"
296
297
    def deal_with_errors(self, error_message, exception):
298
        """Add messages to view for encountered errors
299
300
        Args:
301
            error_message (str): An informative text
302
            exception (Exception): an exception instance
303
        """
304
305
        exception_message = "Message was: %s" % (exception)
306
307
        logger.error(error_message)
308
        logger.error(exception_message)
309
310
        messages.error(
311
            self.request,
312
            message=error_message,
313
            extra_tags="alert alert-dismissible alert-danger")
314
        messages.error(
315
            self.request,
316
            message=exception_message,
317
            extra_tags="alert alert-dismissible alert-danger")
318
319
    def get_form_kwargs(self):
320
        """Override get_form_kwargs"""
321
322
        kwargs = super(CreateUserView, self).get_form_kwargs()
323
324
        # create a new biosample user
325
        username = "image-%s" % (get_random_string(length=8))
326
327
        # add username to instance
328
        kwargs['username'] = username
329
330
        return kwargs
331
332
    # create an user or throw an exception
333
    def create_biosample_user(self, form, full_name, affiliation):
334
        """Create a biosample user
335
336
        Args:
337
            form (:py:class:`CreateUserForm`) an instantiated form object
338
            full_name (str): the user full name (Name + Surname)
339
            affiliation (str): the organization the user belongs to
340
341
        Returns:
342
            str: a biosamples user id
343
        """
344
345
        password = form.cleaned_data['password1']
346
        confirmPwd = form.cleaned_data['password2']
347
348
        # get user model associated with this session
349
        user = self.request.user
350
351
        # get email
352
        email = user.email
353
354
        biosample_user_id = None
355
356
        # creating a user
357
        logger.debug("Creating user %s" % (form.username))
358
359
        try:
360
            biosample_user_id = User.create_user(
361
                user=form.username,
362
                password=password,
363
                confirmPwd=confirmPwd,
364
                email=email,
365
                full_name=full_name,
366
                organisation=affiliation
367
            )
368
369
            logger.info("user_id %s generated" % (biosample_user_id))
370
371
        except ConnectionError as e:
372
            message = "Problem in creating user %s" % (form.username)
373
            self.deal_with_errors(message, e)
374
375
        return biosample_user_id
376
377
    def create_biosample_team(self, full_name, affiliation):
378
        """Create a biosample team
379
380
        Args:
381
            full_name (str): the user full name (Name + Surname)
382
            affiliation (str): the organization the user belongs to
383
384
        Returns:
385
            :py:class:`pyUSIrest.client.Team`: a pyUSIrest Team instance
386
            :py:class:`biosample.models.ManagedTeam`: a model object
387
        """
388
389
        # creating a new team. First create an user object
390
        # create a new auth object
391
        logger.debug("Generate a token for 'USI_MANAGER'")
392
393
        # get an Auth instance from a helpers.method
394
        auth = get_manager_auth()
395
        admin = User(auth)
396
397
        description = "Team for %s" % (full_name)
398
399
        # the values I want to return
400
        team, managed_team = None, None
401
402
        # now create a team
403
        logger.debug("Creating team for %s" % (full_name))
404
405
        try:
406
407
            team = admin.create_team(
408
                description=description,
409
                centreName=affiliation)
410
411
            logger.info("Team %s generated" % (team.name))
412
413
            # register team in ManagedTeam table
414
            managed_team, created = ManagedTeam.objects.get_or_create(
415
                name=team.name)
416
417
            if created is True:
418
                logger.info("Created: %s" % (managed_team))
419
420
        except ConnectionError as e:
421
            message = "Problem in creating team for %s" % (full_name)
422
            self.deal_with_errors(message, e)
423
424
        return team, managed_team
425
426
    def add_biosample_user_to_team(self, form, user_id, team, managed_team):
427
428
        # I need to generate a new token to see the new team
429
        logger.debug("Generate a new token for 'USI_MANAGER'")
430
431
        auth = get_manager_auth()
432
        admin = User(auth)
433
434
        # list all domain for manager
435
        logger.debug("Listing all domains for %s" % (config('USI_MANAGER')))
436
        logger.debug(", ".join(auth.claims['domains']))
437
438
        # get the domain for the new team, and then the domain_id
439
        logger.debug("Getting domain info for team %s" % (team.name))
440
        domain = admin.get_domain_by_name(team.name)
441
        domain_id = domain.domainReference
442
443
        # the value I want to return
444
        account = None
445
446
        logger.debug(
447
            "Adding user %s to team %s" % (form.username, team.name))
448
449
        try:
450
            # user to team
451
            admin.add_user_to_team(user_id=user_id, domain_id=domain_id)
452
453
            # save objects in accounts table
454
            account = Account.objects.create(
455
                user=self.request.user,
456
                name=form.username,
457
                team=managed_team
458
            )
459
460
            logger.info("%s created" % (account))
461
462
            # add message
463
            messages.debug(
464
                request=self.request,
465
                message="User %s added to team %s" % (
466
                    form.username, team.name),
467
                extra_tags="alert alert-dismissible alert-light")
468
469
        except ConnectionError as e:
470
            message = "Problem in adding user %s to team %s" % (
471
                form.username, team.name)
472
            self.deal_with_errors(message, e)
473
474
        return account
475
476
    # HINT: move to a celery task?
477
    def form_valid(self, form):
478
        """Create a new team in with biosample manager user, then create a new
479
        user and register it"""
480
481
        # get user model associated with this session
482
        user = self.request.user
483
484
        # get user info
485
        first_name = user.first_name
486
        last_name = user.last_name
487
488
        # set full name as
489
        full_name = " ".join([first_name, last_name])
490
491
        # Affiliation is the institution the user belong
492
        affiliation = user.person.affiliation.name
493
494
        # model generic errors from EBI API endpoints
495
        try:
496
            user_id = self.create_biosample_user(form, full_name, affiliation)
497
498
            if user_id is None:
499
                # return invalid form
500
                return self.form_invalid(form)
501
502
            # create a biosample team
503
            team, managed_team = self.create_biosample_team(
504
                full_name, affiliation)
505
506
            if team is None:
507
                # return invalid form
508
                return self.form_invalid(form)
509
510
            account = self.add_biosample_user_to_team(
511
                form, user_id, team, managed_team)
512
513
            if account is None:
514
                # return invalid form
515
                return self.form_invalid(form)
516
517
        except ConnectionError as e:
518
            message = (
519
                "Problem with EBI-AAP endoints. Please contact IMAGE team")
520
            self.deal_with_errors(message, e)
521
522
            # return invalid form
523
            return self.form_invalid(form)
524
525
        # call to a inherited function (which does an HttpResponseRedirect
526
        # to success_url)
527
        return super(CreateUserView, self).form_valid(form)
528
529
530
class SubmitView(LoginRequiredMixin, TokenMixin, MyFormMixin, FormView):
531
    """Call a submission task. Check that a token exists and that it's valid"""
532
533
    form_class = SubmitForm
534
    template_name = 'biosample/submit.html'
535
    submission_id = None
536
537
    def get_success_url(self):
538
        return reverse('submissions:detail', kwargs={
539
            'pk': self.submission_id})
540
541
    def form_valid(self, form):
542
        submission_id = form.cleaned_data['submission_id']
543
        name = form.cleaned_data['name']
544
        password = form.cleaned_data['password']
545
546
        # get a submission object
547
        submission = get_object_or_404(
548
            Submission,
549
            pk=submission_id,
550
            owner=self.request.user)
551
552
        # track submission id in order to render page
553
        self.submission_id = submission_id
554
555
        # check if I can submit object (statuses)
556
        if not submission.can_submit():
557
            # return super method (which calls get_success_url)
558
            logger.error(
559
                "Can't submit submission %s: current status is %s" % (
560
                    submission, submission.get_status_display()))
561
562
            return HttpResponseRedirect(self.get_success_url())
563
564
        # create an auth object if credentials are provided
565
        if name and password:
566
            # call AuthMixin method and generate token. Check user/password
567
            auth = self.generate_token(form)
568
569
            if not auth:
570
                return self.form_invalid(form)
571
572
        # check token: if expired or near to expiring, return form
573
        elif 'token' in self.request.session:
574
            auth = get_auth(token=self.request.session['token'])
575
576
        else:
577
            logger.warning(
578
                "No valid token found. Redirect to tocken generation")
579
580
            messages.error(
581
                self.request,
582
                ("You haven't generated any token yet. Generate a new one"),
583
                extra_tags="alert alert-dismissible alert-danger")
584
585
            # redirect to this form
586
            return self.form_invalid(form)
587
588
        # check tocken expiration
589
        if auth.is_expired() or auth.get_duration().seconds < 1800:
590
            logger.warning(
591
                "Token is expired or near to expire")
592
593
            messages.error(
594
                self.request,
595
                ("Your token is expired or near to expire. Generate a new "
596
                 "one"),
597
                extra_tags="alert alert-dismissible alert-danger")
598
599
            # redirect to this form
600
            return self.form_invalid(form)
601
602
        # start the submission with a valid token
603
        self.start_submission(auth, submission)
604
605
        return redirect(self.get_success_url())
606
607
    def start_submission(self, auth, submission):
608
        """Change submission status and submit data with a valid token"""
609
610
        logger.debug("Connecting to redis database")
611
612
        # here token is valid, so store it in redis database
613
        client = redis.StrictRedis(
614
            host=settings.REDIS_HOST,
615
            port=settings.REDIS_PORT,
616
            db=settings.REDIS_DB)
617
618
        key = "token:submission:{submission_id}:{user}".format(
619
            submission_id=self.submission_id,
620
            user=self.request.user)
621
622
        logger.debug("Writing token in redis")
623
624
        client.set(key, auth.token, ex=auth.get_duration().seconds)
625
626
        # Update submission status
627
        submission.status = WAITING
628
        submission.message = "Waiting for biosample submission"
629
        submission.save()
630
631
        # a valid submission start a task
632
        submit_task = SplitSubmissionTask()
633
        res = submit_task.delay(submission.id)
634
        logger.info(
635
            "Start submission process for %s with task %s" % (
636
                submission,
637
                res.task_id))
638