Completed
Push — master ( ba462b...d97c8b )
by Paolo
27s queued 13s
created

CreateUserView.add_biosample_user_to_team()   A

Complexity

Conditions 2

Size

Total Lines 49
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

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