Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ c3763b39

History | View | Annotate | Download (67.5 kB)

1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import uuid
35
import logging
36
import json
37
import copy
38

    
39
from datetime import datetime, timedelta
40
import base64
41
from urllib import quote
42
from random import randint
43
import os
44

    
45
from django.db import models, transaction
46
from django.contrib.auth.models import User, UserManager, Group, Permission
47
from django.utils.translation import ugettext as _
48
from django.db.models.signals import pre_save, post_save
49
from django.contrib.contenttypes.models import ContentType
50

    
51
from django.db.models import Q
52
from django.core.urlresolvers import reverse
53
from django.utils.http import int_to_base36
54
from django.contrib.auth.tokens import default_token_generator
55
from django.conf import settings
56
from django.utils.importlib import import_module
57
from django.utils.safestring import mark_safe
58

    
59
from synnefo.lib.utils import dict_merge
60

    
61
from astakos.im import settings as astakos_settings
62
from astakos.im import auth_providers as auth
63

    
64
import astakos.im.messages as astakos_messages
65
from synnefo.lib.ordereddict import OrderedDict
66

    
67
from synnefo.util.text import uenc, udec
68
from synnefo.util import units
69
from astakos.im import presentation
70

    
71
logger = logging.getLogger(__name__)
72

    
73
DEFAULT_CONTENT_TYPE = None
74
_content_type = None
75

    
76

    
77
def get_content_type():
78
    global _content_type
79
    if _content_type is not None:
80
        return _content_type
81

    
82
    try:
83
        content_type = ContentType.objects.get(app_label='im',
84
                                               model='astakosuser')
85
    except:
86
        content_type = DEFAULT_CONTENT_TYPE
87
    _content_type = content_type
88
    return content_type
89

    
90
inf = float('inf')
91

    
92

    
93
def generate_token():
94
    s = os.urandom(32)
95
    return base64.urlsafe_b64encode(s).rstrip('=')
96

    
97

    
98
def _partition_by(f, l):
99
    d = {}
100
    for x in l:
101
        group = f(x)
102
        group_l = d.get(group, [])
103
        group_l.append(x)
104
        d[group] = group_l
105
    return d
106

    
107

    
108
def first_of_group(f, l):
109
    Nothing = type("Nothing", (), {})
110
    last_group = Nothing
111
    d = {}
112
    for x in l:
113
        group = f(x)
114
        if group != last_group:
115
            last_group = group
116
            d[group] = x
117
    return d
118

    
119

    
120
class Component(models.Model):
121
    name = models.CharField(_('Name'), max_length=255, unique=True,
122
                            db_index=True)
123
    url = models.CharField(_('Component url'), max_length=1024, null=True,
124
                           help_text=_("URL the component is accessible from"))
125
    base_url = models.CharField(max_length=1024, null=True)
126
    auth_token = models.CharField(_('Authentication Token'), max_length=64,
127
                                  null=True, blank=True, unique=True)
128
    auth_token_created = models.DateTimeField(_('Token creation date'),
129
                                              null=True)
130
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
131
                                              null=True)
132

    
133
    def renew_token(self, expiration_date=None):
134
        for i in range(10):
135
            new_token = generate_token()
136
            count = Component.objects.filter(auth_token=new_token).count()
137
            if count == 0:
138
                break
139
            continue
140
        else:
141
            raise ValueError('Could not generate a token')
142

    
143
        self.auth_token = new_token
144
        self.auth_token_created = datetime.now()
145
        if expiration_date:
146
            self.auth_token_expires = expiration_date
147
        else:
148
            self.auth_token_expires = None
149
        msg = 'Token renewed for component %s'
150
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.name)
151

    
152
    def __str__(self):
153
        return self.name
154

    
155
    @classmethod
156
    def catalog(cls, orderfor=None):
157
        catalog = {}
158
        components = list(cls.objects.all())
159
        default_metadata = presentation.COMPONENTS
160
        metadata = {}
161

    
162
        for component in components:
163
            d = {'url': component.url,
164
                 'name': component.name}
165
            if component.name in default_metadata:
166
                metadata[component.name] = default_metadata.get(component.name)
167
                metadata[component.name].update(d)
168
            else:
169
                metadata[component.name] = d
170

    
171
        def component_by_order(s):
172
            return s[1].get('order')
173

    
174
        def component_by_dashboard_order(s):
175
            return s[1].get('dashboard').get('order')
176

    
177
        metadata = dict_merge(metadata,
178
                              astakos_settings.COMPONENTS_META)
179

    
180
        for component, info in metadata.iteritems():
181
            default_meta = presentation.component_defaults(component)
182
            base_meta = metadata.get(component, {})
183
            settings_meta = astakos_settings.COMPONENTS_META.get(component, {})
184
            component_meta = dict_merge(default_meta, base_meta)
185
            meta = dict_merge(component_meta, settings_meta)
186
            catalog[component] = meta
187

    
188
        order_key = component_by_order
189
        if orderfor == 'dashboard':
190
            order_key = component_by_dashboard_order
191

    
192
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
193
                                             key=order_key))
194
        return ordered_catalog
195

    
196

    
197
_presentation_data = {}
198

    
199

    
200
def get_presentation(resource):
201
    global _presentation_data
202
    resource_presentation = _presentation_data.get(resource, {})
203
    if not resource_presentation:
204
        resources_presentation = presentation.RESOURCES.get('resources', {})
205
        resource_presentation = resources_presentation.get(resource, {})
206
        _presentation_data[resource] = resource_presentation
207
    return resource_presentation
208

    
209

    
210
class Service(models.Model):
211
    component = models.ForeignKey(Component)
212
    name = models.CharField(max_length=255, unique=True)
213
    type = models.CharField(max_length=255)
214

    
215

    
216
class Endpoint(models.Model):
217
    service = models.ForeignKey(Service, related_name='endpoints')
218

    
219

    
220
class EndpointData(models.Model):
221
    endpoint = models.ForeignKey(Endpoint, related_name='data')
222
    key = models.CharField(max_length=255)
223
    value = models.CharField(max_length=1024)
224

    
225
    class Meta:
226
        unique_together = (('endpoint', 'key'),)
227

    
228

    
229
class Resource(models.Model):
230
    name = models.CharField(_('Name'), max_length=255, unique=True)
231
    desc = models.TextField(_('Description'), null=True)
232
    service_type = models.CharField(_('Type'), max_length=255)
233
    service_origin = models.CharField(max_length=255, db_index=True)
234
    unit = models.CharField(_('Unit'), null=True, max_length=255)
235
    uplimit = models.BigIntegerField(default=0)
236
    project_default = models.BigIntegerField()
237
    ui_visible = models.BooleanField(default=True)
238
    api_visible = models.BooleanField(default=True)
239

    
240
    def __str__(self):
241
        return self.name
242

    
243
    def full_name(self):
244
        return str(self)
245

    
246
    def get_info(self):
247
        return {'service': self.service_origin,
248
                'description': self.desc,
249
                'unit': self.unit,
250
                'ui_visible': self.ui_visible,
251
                'api_visible': self.api_visible,
252
                }
253

    
254
    @property
255
    def group(self):
256
        default = self.name
257
        return get_presentation(str(self)).get('group', default)
258

    
259
    @property
260
    def help_text(self):
261
        default = "%s resource" % self.name
262
        return get_presentation(str(self)).get('help_text', default)
263

    
264
    @property
265
    def help_text_input_each(self):
266
        default = "%s resource" % self.name
267
        return get_presentation(str(self)).get('help_text_input_each', default)
268

    
269
    @property
270
    def is_abbreviation(self):
271
        return get_presentation(str(self)).get('is_abbreviation', False)
272

    
273
    @property
274
    def report_desc(self):
275
        default = "%s resource" % self.name
276
        return get_presentation(str(self)).get('report_desc', default)
277

    
278
    @property
279
    def placeholder(self):
280
        return get_presentation(str(self)).get('placeholder', self.unit)
281

    
282
    @property
283
    def verbose_name(self):
284
        return get_presentation(str(self)).get('verbose_name', self.name)
285

    
286
    @property
287
    def display_name(self):
288
        name = self.verbose_name
289
        if self.is_abbreviation:
290
            name = name.upper()
291
        return name
292

    
293
    @property
294
    def pluralized_display_name(self):
295
        if not self.unit:
296
            return '%ss' % self.display_name
297
        return self.display_name
298

    
299

    
300
def get_resource_names():
301
    _RESOURCE_NAMES = []
302
    resources = Resource.objects.select_related('service').all()
303
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
304
    return _RESOURCE_NAMES
305

    
306

    
307
def split_realname(value):
308
    parts = value.split(' ')
309
    if len(parts) == 2:
310
        return parts
311
    else:
312
        return ('', value)
313

    
314

    
315
class AstakosUserManager(UserManager):
316

    
317
    def get_auth_provider_user(self, provider, **kwargs):
318
        """
319
        Retrieve AstakosUser instance associated with the specified third party
320
        id.
321
        """
322
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
323
                          kwargs.iteritems()))
324
        return self.get(auth_providers__module=provider, **kwargs)
325

    
326
    def get_by_email(self, email):
327
        return self.get(email=email)
328

    
329
    def get_by_identifier(self, email_or_username, **kwargs):
330
        try:
331
            return self.get(email__iexact=email_or_username, **kwargs)
332
        except AstakosUser.DoesNotExist:
333
            return self.get(username__iexact=email_or_username, **kwargs)
334

    
335
    def user_exists(self, email_or_username, **kwargs):
336
        qemail = Q(email__iexact=email_or_username)
337
        qusername = Q(username__iexact=email_or_username)
338
        qextra = Q(**kwargs)
339
        return self.filter((qemail | qusername) & qextra).exists()
340

    
341
    def unverified_namesakes(self, email_or_username):
342
        q = Q(email__iexact=email_or_username)
343
        q |= Q(username__iexact=email_or_username)
344
        return self.filter(q & Q(email_verified=False))
345

    
346
    def verified_user_exists(self, email_or_username):
347
        return self.user_exists(email_or_username, email_verified=True)
348

    
349
    def verified(self):
350
        return self.filter(email_verified=True)
351

    
352
    def accepted(self):
353
        return self.filter(moderated=True, is_rejected=False)
354

    
355
    def uuid_catalog(self, l=None):
356
        """
357
        Returns a uuid to username mapping for the uuids appearing in l.
358
        If l is None returns the mapping for all existing users.
359
        """
360
        q = self.filter(uuid__in=l) if l is not None else self
361
        return dict(q.values_list('uuid', 'username'))
362

    
363
    def displayname_catalog(self, l=None):
364
        """
365
        Returns a username to uuid mapping for the usernames appearing in l.
366
        If l is None returns the mapping for all existing users.
367
        """
368
        if l is not None:
369
            lmap = dict((x.lower(), x) for x in l)
370
            q = self.filter(username__in=lmap.keys())
371
            values = ((lmap[n], u)
372
                      for n, u in q.values_list('username', 'uuid'))
373
        else:
374
            q = self
375
            values = self.values_list('username', 'uuid')
376
        return dict(values)
377

    
378

    
379
class AstakosUser(User):
380
    """
381
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
382
    """
383
    affiliation = models.CharField(_('Affiliation'), max_length=255,
384
                                   blank=True, null=True)
385

    
386
    #for invitations
387
    user_level = astakos_settings.DEFAULT_USER_LEVEL
388
    level = models.IntegerField(_('Inviter level'), default=user_level)
389
    invitations = models.IntegerField(
390
        _('Invitations left'),
391
        default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
392

    
393
    auth_token = models.CharField(
394
        _('Authentication Token'),
395
        max_length=64,
396
        unique=True,
397
        null=True,
398
        blank=True,
399
        help_text=_('Renew your authentication '
400
                    'token. Make sure to set the new '
401
                    'token in any client you may be '
402
                    'using, to preserve its '
403
                    'functionality.'))
404
    auth_token_created = models.DateTimeField(_('Token creation date'),
405
                                              null=True)
406
    auth_token_expires = models.DateTimeField(
407
        _('Token expiration date'), null=True)
408

    
409
    updated = models.DateTimeField(_('Update date'))
410

    
411
    # Arbitrary text to identify the reason user got deactivated.
412
    # To be used as a reference from administrators.
413
    deactivated_reason = models.TextField(
414
        _('Reason the user was disabled for'),
415
        default=None, null=True)
416
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
417
                                          blank=True)
418

    
419
    has_credits = models.BooleanField(_('Has credits?'), default=False)
420

    
421
    # this is set to True when user profile gets updated for the first time
422
    is_verified = models.BooleanField(_('Is verified?'), default=False)
423

    
424
    # user email is verified
425
    email_verified = models.BooleanField(_('Email verified?'), default=False)
426

    
427
    # unique string used in user email verification url
428
    verification_code = models.CharField(max_length=255, null=True,
429
                                         blank=False, unique=True)
430

    
431
    # date user email verified
432
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
433
                                       blank=True)
434

    
435
    # email verification notice was sent to the user at this time
436
    activation_sent = models.DateTimeField(_('Activation sent date'),
437
                                           null=True, blank=True)
438

    
439
    # user got rejected during moderation process
440
    is_rejected = models.BooleanField(_('Account rejected'),
441
                                      default=False)
442
    # reason user got rejected
443
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
444
                                       blank=True)
445
    # moderation status
446
    moderated = models.BooleanField(_('User moderated'), default=False)
447
    # date user moderated (either accepted or rejected)
448
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
449
                                        blank=True, null=True)
450
    # a snapshot of user instance the time got moderated
451
    moderated_data = models.TextField(null=True, default=None, blank=True)
452
    # a string which identifies how the user got moderated
453
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
454
                                       default=None, null=True, blank=True)
455
    # the email used to accept the user
456
    accepted_email = models.EmailField(null=True, default=None, blank=True)
457

    
458
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
459
                                           default=False)
460
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
461
                                             null=True, blank=True)
462
    # permanent unique user identifier
463
    uuid = models.CharField(max_length=255, null=False, blank=False,
464
                            unique=True)
465

    
466
    policy = models.ManyToManyField(
467
        Resource, null=True, through='AstakosUserQuota')
468

    
469
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
470
                                          default=False, db_index=True)
471

    
472
    objects = AstakosUserManager()
473

    
474
    @property
475
    def realname(self):
476
        return '%s %s' % (self.first_name, self.last_name)
477

    
478
    @property
479
    def log_display(self):
480
        """
481
        Should be used in all logger.* calls that refer to a user so that
482
        user display is consistent across log entries.
483
        """
484
        return '%s::%s' % (self.uuid, self.email)
485

    
486
    @realname.setter
487
    def realname(self, value):
488
        first, last = split_realname(value)
489
        self.first_name = first
490
        self.last_name = last
491

    
492
    def add_permission(self, pname):
493
        if self.has_perm(pname):
494
            return
495
        p, created = Permission.objects.get_or_create(
496
            codename=pname,
497
            name=pname.capitalize(),
498
            content_type=get_content_type())
499
        self.user_permissions.add(p)
500

    
501
    def remove_permission(self, pname):
502
        if self.has_perm(pname):
503
            return
504
        p = Permission.objects.get(codename=pname,
505
                                   content_type=get_content_type())
506
        self.user_permissions.remove(p)
507

    
508
    def add_group(self, gname):
509
        group, _ = Group.objects.get_or_create(name=gname)
510
        self.groups.add(group)
511

    
512
    def is_accepted(self):
513
        return self.moderated and not self.is_rejected
514

    
515
    def is_project_admin(self):
516
        return self.uuid in astakos_settings.PROJECT_ADMINS
517

    
518
    @property
519
    def invitation(self):
520
        try:
521
            return Invitation.objects.get(username=self.email)
522
        except Invitation.DoesNotExist:
523
            return None
524

    
525
    @property
526
    def policies(self):
527
        return self.astakosuserquota_set.select_related().all()
528

    
529
    def get_resource_policy(self, resource):
530
        return AstakosUserQuota.objects.select_related("resource").\
531
            get(user=self, resource__name=resource)
532

    
533
    def fix_username(self):
534
        self.username = self.email.lower()
535

    
536
    def set_email(self, email):
537
        self.email = email
538
        self.fix_username()
539

    
540
    def save(self, update_timestamps=True, **kwargs):
541
        if update_timestamps:
542
            self.updated = datetime.now()
543

    
544
        super(AstakosUser, self).save(**kwargs)
545

    
546
    def renew_verification_code(self):
547
        self.verification_code = str(uuid.uuid4())
548
        logger.info("Verification code renewed for %s" % self.log_display)
549

    
550
    def renew_token(self, flush_sessions=False, current_key=None):
551
        for i in range(10):
552
            new_token = generate_token()
553
            count = AstakosUser.objects.filter(auth_token=new_token).count()
554
            if count == 0:
555
                break
556
            continue
557
        else:
558
            raise ValueError('Could not generate a token')
559

    
560
        self.auth_token = new_token
561
        self.auth_token_created = datetime.now()
562
        self.auth_token_expires = self.auth_token_created + \
563
            timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
564
        if flush_sessions:
565
            self.flush_sessions(current_key)
566
        self.delete_online_access_tokens()
567
        msg = 'Token renewed for %s'
568
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
569

    
570
    def token_expired(self):
571
        return self.auth_token_expires < datetime.now()
572

    
573
    def flush_sessions(self, current_key=None):
574
        q = self.sessions
575
        if current_key:
576
            q = q.exclude(session_key=current_key)
577

    
578
        keys = q.values_list('session_key', flat=True)
579
        if keys:
580
            msg = 'Flushing sessions: %s'
581
            logger.log(astakos_settings.LOGGING_LEVEL, msg, ','.join(keys))
582
        engine = import_module(settings.SESSION_ENGINE)
583
        for k in keys:
584
            s = engine.SessionStore(k)
585
            s.flush()
586

    
587
    def __unicode__(self):
588
        return '%s (%s)' % (self.realname, self.email)
589

    
590
    def conflicting_email(self):
591
        q = AstakosUser.objects.exclude(username=self.username)
592
        q = q.filter(email__iexact=self.email)
593
        if q.count() != 0:
594
            return True
595
        return False
596

    
597
    def email_change_is_pending(self):
598
        return self.emailchanges.count() > 0
599

    
600
    @property
601
    def status_display(self):
602
        msg = ""
603
        if self.is_active:
604
            msg = "Accepted/Active"
605
        if self.is_rejected:
606
            msg = "Rejected"
607
            if self.rejected_reason:
608
                msg += " (%s)" % self.rejected_reason
609
        if not self.email_verified:
610
            msg = "Pending email verification"
611
        if not self.moderated:
612
            msg = "Pending moderation"
613
        if not self.is_active and self.email_verified:
614
            msg = "Accepted/Inactive"
615
            if self.deactivated_reason:
616
                msg += " (%s)" % (self.deactivated_reason)
617

    
618
        if self.moderated and not self.is_rejected:
619
            if self.accepted_policy == 'manual':
620
                msg += " (manually accepted)"
621
            else:
622
                msg += " (accepted policy: %s)" % \
623
                    self.accepted_policy
624
        return msg
625

    
626
    @property
627
    def signed_terms(self):
628
        return self.has_signed_terms
629

    
630
    def set_invitations_level(self):
631
        """
632
        Update user invitation level
633
        """
634
        level = self.invitation.inviter.level + 1
635
        self.level = level
636
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
637

    
638
    def can_change_password(self):
639
        return self.has_auth_provider('local', auth_backend='astakos')
640

    
641
    def can_change_email(self):
642
        if not self.has_auth_provider('local'):
643
            return True
644

    
645
        local = self.get_auth_provider('local')._instance
646
        return local.auth_backend == 'astakos'
647

    
648
    # Auth providers related methods
649
    def get_auth_provider(self, module=None, identifier=None, **filters):
650
        if not module:
651
            return self.auth_providers.active()[0].settings
652

    
653
        params = {'module': module}
654
        if identifier:
655
            params['identifier'] = identifier
656
        params.update(filters)
657
        return self.auth_providers.active().get(**params).settings
658

    
659
    def has_auth_provider(self, provider, **kwargs):
660
        return bool(self.auth_providers.active().filter(module=provider,
661
                                                        **kwargs).count())
662

    
663
    def get_required_providers(self, **kwargs):
664
        return auth.REQUIRED_PROVIDERS.keys()
665

    
666
    def missing_required_providers(self):
667
        required = self.get_required_providers()
668
        missing = []
669
        for provider in required:
670
            if not self.has_auth_provider(provider):
671
                missing.append(auth.get_provider(provider, self))
672
        return missing
673

    
674
    def get_available_auth_providers(self, **filters):
675
        """
676
        Returns a list of providers available for add by the user.
677
        """
678
        modules = astakos_settings.IM_MODULES
679
        providers = []
680
        for p in modules:
681
            providers.append(auth.get_provider(p, self))
682
        available = []
683

    
684
        for p in providers:
685
            if p.get_add_policy:
686
                available.append(p)
687
        return available
688

    
689
    def get_disabled_auth_providers(self, **filters):
690
        providers = self.get_auth_providers(**filters)
691
        disabled = []
692
        for p in providers:
693
            if not p.get_login_policy:
694
                disabled.append(p)
695
        return disabled
696

    
697
    def get_enabled_auth_providers(self, **filters):
698
        providers = self.get_auth_providers(**filters)
699
        enabled = []
700
        for p in providers:
701
            if p.get_login_policy:
702
                enabled.append(p)
703
        return enabled
704

    
705
    def get_auth_providers(self, **filters):
706
        providers = []
707
        for provider in self.auth_providers.active(**filters):
708
            if provider.settings.module_enabled:
709
                providers.append(provider.settings)
710

    
711
        modules = astakos_settings.IM_MODULES
712

    
713
        def key(p):
714
            if not p.module in modules:
715
                return 100
716
            return modules.index(p.module)
717

    
718
        providers = sorted(providers, key=key)
719
        return providers
720

    
721
    # URL methods
722
    @property
723
    def auth_providers_display(self):
724
        return ",".join(["%s:%s" % (p.module, p.identifier) for p in
725
                         self.get_enabled_auth_providers()])
726

    
727
    def add_auth_provider(self, module='local', identifier=None, **params):
728
        provider = auth.get_provider(module, self, identifier, **params)
729
        provider.add_to_user()
730

    
731
    def get_resend_activation_url(self):
732
        return reverse('send_activation', kwargs={'user_id': self.pk})
733

    
734
    def get_activation_url(self, nxt=False):
735
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
736
                              quote(self.verification_code))
737
        if nxt:
738
            url += "&next=%s" % quote(nxt)
739
        return url
740

    
741
    def get_password_reset_url(self, token_generator=default_token_generator):
742
        return reverse('astakos.im.views.target.local.password_reset_confirm',
743
                       kwargs={'uidb36': int_to_base36(self.id),
744
                               'token': token_generator.make_token(self)})
745

    
746
    def get_inactive_message(self, provider_module, identifier=None):
747
        try:
748
            provider = self.get_auth_provider(provider_module, identifier)
749
        except AstakosUserAuthProvider.DoesNotExist:
750
            provider = auth.get_provider(provider_module, self)
751

    
752
        msg_extra = ''
753
        message = ''
754

    
755
        msg_inactive = provider.get_account_inactive_msg
756
        msg_pending = provider.get_pending_activation_msg
757
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
758
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
759
        msg_pending_mod = provider.get_pending_moderation_msg
760
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
761
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
762

    
763
        if not self.email_verified:
764
            message = msg_pending
765
            url = self.get_resend_activation_url()
766
            msg_extra = msg_pending_help + \
767
                u' ' + \
768
                '<a href="%s">%s?</a>' % (url, msg_resend)
769
        else:
770
            if not self.moderated:
771
                message = msg_pending_mod
772
            else:
773
                if self.is_rejected:
774
                    message = msg_rejected
775
                else:
776
                    message = msg_inactive
777

    
778
        return mark_safe(message + u' ' + msg_extra)
779

    
780
    def owns_application(self, application):
781
        return application.owner == self
782

    
783
    def owns_project(self, project):
784
        return project.owner == self
785

    
786
    def is_associated(self, project):
787
        try:
788
            m = ProjectMembership.objects.get(person=self, project=project)
789
            return m.state in ProjectMembership.ASSOCIATED_STATES
790
        except ProjectMembership.DoesNotExist:
791
            return False
792

    
793
    def get_membership(self, project):
794
        try:
795
            return ProjectMembership.objects.get(
796
                project=project,
797
                person=self)
798
        except ProjectMembership.DoesNotExist:
799
            return None
800

    
801
    def membership_display(self, project):
802
        m = self.get_membership(project)
803
        if m is None:
804
            return _('Not a member')
805
        else:
806
            return m.user_friendly_state_display()
807

    
808
    def non_owner_can_view(self, maybe_project):
809
        if self.is_project_admin():
810
            return True
811
        if maybe_project is None:
812
            return False
813
        project = maybe_project
814
        if self.is_associated(project):
815
            return True
816
        if project.is_deactivated():
817
            return False
818
        return True
819

    
820
    def delete_online_access_tokens(self):
821
        offline_tokens = self.token_set.filter(access_token='online')
822
        logger.info('The following access tokens will be deleted: %s',
823
                    offline_tokens)
824
        offline_tokens.delete()
825

    
826

    
827
class AstakosUserAuthProviderManager(models.Manager):
828

    
829
    def active(self, **filters):
830
        return self.filter(active=True, **filters)
831

    
832
    def remove_unverified_providers(self, provider, **filters):
833
        try:
834
            existing = self.filter(module=provider, user__email_verified=False,
835
                                   **filters)
836
            for p in existing:
837
                p.user.delete()
838
        except:
839
            pass
840

    
841
    def unverified(self, provider, **filters):
842
        try:
843

    
844
            return self.select_for_update().get(module=provider,
845
                                                user__email_verified=False,
846
                                                **filters).settings
847
        except AstakosUserAuthProvider.DoesNotExist:
848
            return None
849

    
850
    def verified(self, provider, **filters):
851
        try:
852
            return self.get(module=provider, user__email_verified=True,
853
                            **filters).settings
854
        except AstakosUserAuthProvider.DoesNotExist:
855
            return None
856

    
857

    
858
class AuthProviderPolicyProfileManager(models.Manager):
859

    
860
    def active(self):
861
        return self.filter(active=True)
862

    
863
    def for_user(self, user, provider):
864
        policies = {}
865
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
866
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
867
        exclusive_q = exclusive_q1 | exclusive_q2
868

    
869
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
870
            policies.update(profile.policies)
871

    
872
        user_groups = user.groups.all().values('pk')
873
        for profile in self.active().filter(groups__in=user_groups).filter(
874
                exclusive_q):
875
            policies.update(profile.policies)
876
        return policies
877

    
878
    def add_policy(self, name, provider, group_or_user, exclusive=False,
879
                   **policies):
880
        is_group = isinstance(group_or_user, Group)
881
        profile, created = self.get_or_create(name=name, provider=provider,
882
                                              is_exclusive=exclusive)
883
        profile.is_exclusive = exclusive
884
        profile.save()
885
        if is_group:
886
            profile.groups.add(group_or_user)
887
        else:
888
            profile.users.add(group_or_user)
889
        profile.set_policies(policies)
890
        profile.save()
891
        return profile
892

    
893

    
894
class AuthProviderPolicyProfile(models.Model):
895
    name = models.CharField(_('Name'), max_length=255, blank=False,
896
                            null=False, db_index=True)
897
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
898
                                null=False)
899

    
900
    # apply policies to all providers excluding the one set in provider field
901
    is_exclusive = models.BooleanField(default=False)
902

    
903
    policy_add = models.NullBooleanField(null=True, default=None)
904
    policy_remove = models.NullBooleanField(null=True, default=None)
905
    policy_create = models.NullBooleanField(null=True, default=None)
906
    policy_login = models.NullBooleanField(null=True, default=None)
907
    policy_limit = models.IntegerField(null=True, default=None)
908
    policy_required = models.NullBooleanField(null=True, default=None)
909
    policy_automoderate = models.NullBooleanField(null=True, default=None)
910
    policy_switch = models.NullBooleanField(null=True, default=None)
911

    
912
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
913
                     'automoderate')
914

    
915
    priority = models.IntegerField(null=False, default=1)
916
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
917
    users = models.ManyToManyField(AstakosUser,
918
                                   related_name='authpolicy_profiles')
919
    active = models.BooleanField(default=True)
920

    
921
    objects = AuthProviderPolicyProfileManager()
922

    
923
    class Meta:
924
        ordering = ['priority']
925

    
926
    @property
927
    def policies(self):
928
        policies = {}
929
        for pkey in self.POLICY_FIELDS:
930
            value = getattr(self, 'policy_%s' % pkey, None)
931
            if value is None:
932
                continue
933
            policies[pkey] = value
934
        return policies
935

    
936
    def set_policies(self, policies_dict):
937
        for key, value in policies_dict.iteritems():
938
            if key in self.POLICY_FIELDS:
939
                setattr(self, 'policy_%s' % key, value)
940
        return self.policies
941

    
942

    
943
class AstakosUserAuthProvider(models.Model):
944
    """
945
    Available user authentication methods.
946
    """
947
    affiliation = models.CharField(_('Affiliation'), max_length=255,
948
                                   blank=True, null=True, default=None)
949
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
950
    module = models.CharField(_('Provider'), max_length=255, blank=False,
951
                              default='local')
952
    identifier = models.CharField(_('Third-party identifier'),
953
                                  max_length=255, null=True,
954
                                  blank=True)
955
    active = models.BooleanField(default=True)
956
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
957
                                    default='astakos')
958
    info_data = models.TextField(default="", null=True, blank=True)
959
    created = models.DateTimeField('Creation date', auto_now_add=True)
960

    
961
    objects = AstakosUserAuthProviderManager()
962

    
963
    class Meta:
964
        unique_together = (('identifier', 'module', 'user'), )
965
        ordering = ('module', 'created')
966

    
967
    def __init__(self, *args, **kwargs):
968
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
969
        try:
970
            self.info = json.loads(self.info_data)
971
            if not self.info:
972
                self.info = {}
973
        except Exception:
974
            self.info = {}
975

    
976
        for key, value in self.info.iteritems():
977
            setattr(self, 'info_%s' % key, value)
978

    
979
    @property
980
    def settings(self):
981
        extra_data = {}
982

    
983
        info_data = {}
984
        if self.info_data:
985
            info_data = json.loads(self.info_data)
986

    
987
        extra_data['info'] = info_data
988

    
989
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
990
            extra_data[key] = getattr(self, key)
991

    
992
        extra_data['instance'] = self
993
        return auth.get_provider(self.module, self.user,
994
                                 self.identifier, **extra_data)
995

    
996
    def __repr__(self):
997
        return '<AstakosUserAuthProvider %s:%s>' % (
998
            self.module, self.identifier)
999

    
1000
    def __unicode__(self):
1001
        if self.identifier:
1002
            return "%s:%s" % (self.module, self.identifier)
1003
        if self.auth_backend:
1004
            return "%s:%s" % (self.module, self.auth_backend)
1005
        return self.module
1006

    
1007
    def save(self, *args, **kwargs):
1008
        self.info_data = json.dumps(self.info)
1009
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1010

    
1011

    
1012
class AstakosUserQuota(models.Model):
1013
    capacity = models.BigIntegerField()
1014
    resource = models.ForeignKey(Resource)
1015
    user = models.ForeignKey(AstakosUser)
1016

    
1017
    class Meta:
1018
        unique_together = ("resource", "user")
1019

    
1020

    
1021
class ApprovalTerms(models.Model):
1022
    """
1023
    Model for approval terms
1024
    """
1025

    
1026
    date = models.DateTimeField(
1027
        _('Issue date'), db_index=True, auto_now_add=True)
1028
    location = models.CharField(_('Terms location'), max_length=255)
1029

    
1030

    
1031
class Invitation(models.Model):
1032
    """
1033
    Model for registring invitations
1034
    """
1035
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1036
                                null=True)
1037
    realname = models.CharField(_('Real name'), max_length=255)
1038
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1039
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1040
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1041
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1042
    consumed = models.DateTimeField(_('Consumption date'),
1043
                                    null=True, blank=True)
1044

    
1045
    def __init__(self, *args, **kwargs):
1046
        super(Invitation, self).__init__(*args, **kwargs)
1047
        if not self.id:
1048
            self.code = _generate_invitation_code()
1049

    
1050
    def consume(self):
1051
        self.is_consumed = True
1052
        self.consumed = datetime.now()
1053
        self.save()
1054

    
1055
    def __unicode__(self):
1056
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1057

    
1058

    
1059
class EmailChangeManager(models.Manager):
1060

    
1061
    @transaction.commit_on_success
1062
    def change_email(self, activation_key):
1063
        """
1064
        Validate an activation key and change the corresponding
1065
        ``User`` if valid.
1066

1067
        If the key is valid and has not expired, return the ``User``
1068
        after activating.
1069

1070
        If the key is not valid or has expired, return ``None``.
1071

1072
        If the key is valid but the ``User`` is already active,
1073
        return ``None``.
1074

1075
        After successful email change the activation record is deleted.
1076

1077
        Throws ValueError if there is already
1078
        """
1079
        try:
1080
            email_change = self.model.objects.get(
1081
                activation_key=activation_key)
1082
            if email_change.activation_key_expired():
1083
                email_change.delete()
1084
                raise EmailChange.DoesNotExist
1085
            # is there an active user with this address?
1086
            try:
1087
                AstakosUser.objects.get(
1088
                    email__iexact=email_change.new_email_address)
1089
            except AstakosUser.DoesNotExist:
1090
                pass
1091
            else:
1092
                raise ValueError(_('The new email address is reserved.'))
1093
            # update user
1094
            user = AstakosUser.objects.select_for_update().\
1095
                get(pk=email_change.user_id)
1096
            old_email = user.email
1097
            user.set_email(email_change.new_email_address)
1098
            user.save()
1099
            email_change.delete()
1100
            msg = "User %s changed email from %s to %s"
1101
            logger.log(astakos_settings.LOGGING_LEVEL, msg, user.log_display,
1102
                       old_email, user.email)
1103
            return user
1104
        except EmailChange.DoesNotExist:
1105
            raise ValueError(_('Invalid activation key.'))
1106

    
1107

    
1108
class EmailChange(models.Model):
1109
    new_email_address = models.EmailField(
1110
        _(u'new e-mail address'),
1111
        help_text=_('Provide a new email address. Until you verify the new '
1112
                    'address by following the activation link that will be '
1113
                    'sent to it, your old email address will remain active.'))
1114
    user = models.ForeignKey(
1115
        AstakosUser, unique=True, related_name='emailchanges')
1116
    requested_at = models.DateTimeField(auto_now_add=True)
1117
    activation_key = models.CharField(
1118
        max_length=40, unique=True, db_index=True)
1119

    
1120
    objects = EmailChangeManager()
1121

    
1122
    def get_url(self):
1123
        return reverse('email_change_confirm',
1124
                       kwargs={'activation_key': self.activation_key})
1125

    
1126
    def activation_key_expired(self):
1127
        expiration_date = timedelta(
1128
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1129
        return self.requested_at + expiration_date < datetime.now()
1130

    
1131

    
1132
class AdditionalMail(models.Model):
1133
    """
1134
    Model for registring invitations
1135
    """
1136
    owner = models.ForeignKey(AstakosUser)
1137
    email = models.EmailField()
1138

    
1139

    
1140
def _generate_invitation_code():
1141
    while True:
1142
        code = randint(1, 2L ** 63 - 1)
1143
        try:
1144
            Invitation.objects.get(code=code)
1145
            # An invitation with this code already exists, try again
1146
        except Invitation.DoesNotExist:
1147
            return code
1148

    
1149

    
1150
def get_latest_terms():
1151
    try:
1152
        term = ApprovalTerms.objects.order_by('-id')[0]
1153
        return term
1154
    except IndexError:
1155
        pass
1156
    return None
1157

    
1158

    
1159
class PendingThirdPartyUser(models.Model):
1160
    """
1161
    Model for registring successful third party user authentications
1162
    """
1163
    third_party_identifier = models.CharField(
1164
        _('Third-party identifier'), max_length=255, null=True, blank=True)
1165
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1166
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1167
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1168
                                  null=True)
1169
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1170
                                 null=True)
1171
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1172
                                   null=True)
1173
    username = models.CharField(
1174
        _('username'), max_length=30, unique=True,
1175
        help_text=_("Required. 30 characters or fewer. "
1176
                    "Letters, numbers and @/./+/-/_ characters"))
1177
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1178
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1179
    info = models.TextField(default="", null=True, blank=True)
1180

    
1181
    class Meta:
1182
        unique_together = ("provider", "third_party_identifier")
1183

    
1184
    def get_user_instance(self):
1185
        """
1186
        Create a new AstakosUser instance based on details provided when user
1187
        initially signed up.
1188
        """
1189
        d = copy.copy(self.__dict__)
1190
        d.pop('_state', None)
1191
        d.pop('id', None)
1192
        d.pop('token', None)
1193
        d.pop('created', None)
1194
        d.pop('info', None)
1195
        d.pop('affiliation', None)
1196
        d.pop('provider', None)
1197
        d.pop('third_party_identifier', None)
1198
        user = AstakosUser(**d)
1199

    
1200
        return user
1201

    
1202
    @property
1203
    def realname(self):
1204
        return '%s %s' % (self.first_name, self.last_name)
1205

    
1206
    @realname.setter
1207
    def realname(self, value):
1208
        first, last = split_realname(value)
1209
        self.first_name = first
1210
        self.last_name = last
1211

    
1212
    def save(self, *args, **kwargs):
1213
        if not self.id:
1214
            # set username
1215
            while not self.username:
1216
                username = uuid.uuid4().hex[:30]
1217
                try:
1218
                    AstakosUser.objects.get(username=username)
1219
                except AstakosUser.DoesNotExist:
1220
                    self.username = username
1221
        super(PendingThirdPartyUser, self).save(*args, **kwargs)
1222

    
1223
    def generate_token(self):
1224
        self.password = self.third_party_identifier
1225
        self.last_login = datetime.now()
1226
        self.token = default_token_generator.make_token(self)
1227

    
1228
    def existing_user(self):
1229
        return AstakosUser.objects.filter(
1230
            auth_providers__module=self.provider,
1231
            auth_providers__identifier=self.third_party_identifier)
1232

    
1233
    def get_provider(self, user):
1234
        params = {
1235
            'info_data': self.info,
1236
            'affiliation': self.affiliation
1237
        }
1238
        return auth.get_provider(self.provider, user,
1239
                                 self.third_party_identifier, **params)
1240

    
1241

    
1242
class SessionCatalog(models.Model):
1243
    session_key = models.CharField(_('session key'), max_length=40)
1244
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1245

    
1246

    
1247
class UserSetting(models.Model):
1248
    user = models.ForeignKey(AstakosUser)
1249
    setting = models.CharField(max_length=255)
1250
    value = models.IntegerField()
1251

    
1252
    class Meta:
1253
        unique_together = ("user", "setting")
1254

    
1255

    
1256
### PROJECTS ###
1257
################
1258

    
1259
class Chain(models.Model):
1260
    chain = models.AutoField(primary_key=True)
1261

    
1262
    def __str__(self):
1263
        return "%s" % (self.chain,)
1264

    
1265

    
1266
def new_chain():
1267
    c = Chain.objects.create()
1268
    return c
1269

    
1270

    
1271
class ProjectApplicationManager(models.Manager):
1272

    
1273
    def pending_per_project(self, projects):
1274
        apps = self.filter(state=self.model.PENDING,
1275
                           chain__in=projects).order_by('chain', '-id')
1276
        checked_chain = None
1277
        projs = {}
1278
        for app in apps:
1279
            chain = app.chain_id
1280
            if chain != checked_chain:
1281
                checked_chain = chain
1282
                projs[chain] = app
1283
        return projs
1284

    
1285

    
1286
class ProjectApplication(models.Model):
1287
    applicant = models.ForeignKey(
1288
        AstakosUser,
1289
        related_name='projects_applied',
1290
        db_index=True)
1291

    
1292
    PENDING = 0
1293
    APPROVED = 1
1294
    REPLACED = 2
1295
    DENIED = 3
1296
    DISMISSED = 4
1297
    CANCELLED = 5
1298

    
1299
    state = models.IntegerField(default=PENDING,
1300
                                db_index=True)
1301
    owner = models.ForeignKey(
1302
        AstakosUser,
1303
        related_name='projects_owned',
1304
        null=True,
1305
        db_index=True)
1306
    chain = models.ForeignKey('Project',
1307
                              related_name='chained_apps',
1308
                              db_column='chain')
1309
    name = models.CharField(max_length=80, null=True)
1310
    homepage = models.URLField(max_length=255, null=True,
1311
                               verify_exists=False)
1312
    description = models.TextField(null=True, blank=True)
1313
    start_date = models.DateTimeField(null=True, blank=True)
1314
    end_date = models.DateTimeField(null=True)
1315
    member_join_policy = models.IntegerField(null=True)
1316
    member_leave_policy = models.IntegerField(null=True)
1317
    limit_on_members_number = models.BigIntegerField(null=True)
1318
    resource_grants = models.ManyToManyField(
1319
        Resource,
1320
        null=True,
1321
        blank=True,
1322
        through='ProjectResourceGrant')
1323
    comments = models.TextField(null=True, blank=True)
1324
    issue_date = models.DateTimeField(auto_now_add=True)
1325
    response_date = models.DateTimeField(null=True, blank=True)
1326
    response = models.TextField(null=True, blank=True)
1327
    response_actor = models.ForeignKey(AstakosUser, null=True,
1328
                                       related_name='responded_apps')
1329
    waive_date = models.DateTimeField(null=True, blank=True)
1330
    waive_reason = models.TextField(null=True, blank=True)
1331
    waive_actor = models.ForeignKey(AstakosUser, null=True,
1332
                                    related_name='waived_apps')
1333
    private = models.NullBooleanField(default=False)
1334

    
1335
    objects = ProjectApplicationManager()
1336

    
1337
    # Compiled queries
1338
    Q_PENDING = Q(state=PENDING)
1339
    Q_APPROVED = Q(state=APPROVED)
1340
    Q_DENIED = Q(state=DENIED)
1341

    
1342
    class Meta:
1343
        unique_together = ("chain", "id")
1344

    
1345
    def __unicode__(self):
1346
        return "%s applied by %s" % (self.name, self.applicant)
1347

    
1348
    # TODO: Move to a more suitable place
1349
    APPLICATION_STATE_DISPLAY = {
1350
        PENDING:   _('Pending review'),
1351
        APPROVED:  _('Approved'),
1352
        REPLACED:  _('Replaced'),
1353
        DENIED:    _('Denied'),
1354
        DISMISSED: _('Dismissed'),
1355
        CANCELLED: _('Cancelled')
1356
    }
1357

    
1358
    @property
1359
    def log_display(self):
1360
        return "application %s (%s) for project %s" % (
1361
            self.id, self.name, self.chain)
1362

    
1363
    def state_display(self):
1364
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1365

    
1366
    @property
1367
    def grants(self):
1368
        return self.projectresourcegrant_set.values('member_capacity',
1369
                                                    'resource__name')
1370

    
1371
    @property
1372
    def resource_policies(self):
1373
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1374

    
1375
    def is_modification(self):
1376
        # if self.state != self.PENDING:
1377
        #     return False
1378
        parents = self.chained_applications().filter(id__lt=self.id)
1379
        parents = parents.filter(state__in=[self.APPROVED])
1380
        return parents.count() > 0
1381

    
1382
    def chained_applications(self):
1383
        return ProjectApplication.objects.filter(chain=self.chain)
1384

    
1385
    def denied_modifications(self):
1386
        q = self.chained_applications()
1387
        q = q.filter(Q(state=self.DENIED))
1388
        q = q.filter(~Q(id=self.id))
1389
        return q
1390

    
1391
    def last_denied(self):
1392
        try:
1393
            return self.denied_modifications().order_by('-id')[0]
1394
        except IndexError:
1395
            return None
1396

    
1397
    def has_denied_modifications(self):
1398
        return bool(self.last_denied())
1399

    
1400
    def can_cancel(self):
1401
        return self.state == self.PENDING
1402

    
1403
    def cancel(self, actor=None, reason=None):
1404
        if not self.can_cancel():
1405
            m = _("cannot cancel: application '%s' in state '%s'") % (
1406
                self.id, self.state)
1407
            raise AssertionError(m)
1408

    
1409
        self.state = self.CANCELLED
1410
        self.waive_date = datetime.now()
1411
        self.waive_reason = reason
1412
        self.waive_actor = actor
1413
        self.save()
1414

    
1415
    def can_dismiss(self):
1416
        return self.state == self.DENIED
1417

    
1418
    def dismiss(self, actor=None, reason=None):
1419
        if not self.can_dismiss():
1420
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1421
                self.id, self.state)
1422
            raise AssertionError(m)
1423

    
1424
        self.state = self.DISMISSED
1425
        self.waive_date = datetime.now()
1426
        self.waive_reason = reason
1427
        self.waive_actor = actor
1428
        self.save()
1429

    
1430
    def can_deny(self):
1431
        return self.state == self.PENDING
1432

    
1433
    def deny(self, actor=None, reason=None):
1434
        if not self.can_deny():
1435
            m = _("cannot deny: application '%s' in state '%s'") % (
1436
                self.id, self.state)
1437
            raise AssertionError(m)
1438

    
1439
        self.state = self.DENIED
1440
        self.response_date = datetime.now()
1441
        self.response = reason
1442
        self.response_actor = actor
1443
        self.save()
1444

    
1445
    def can_approve(self):
1446
        return self.state == self.PENDING
1447

    
1448
    def approve(self, actor=None, reason=None):
1449
        if not self.can_approve():
1450
            m = _("cannot approve: project '%s' in state '%s'") % (
1451
                self.name, self.state)
1452
            raise AssertionError(m)  # invalid argument
1453

    
1454
        now = datetime.now()
1455
        self.state = self.APPROVED
1456
        self.response_date = now
1457
        self.response = reason
1458
        self.response_actor = actor
1459
        self.save()
1460

    
1461
    @property
1462
    def member_join_policy_display(self):
1463
        policy = self.member_join_policy
1464
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1465

    
1466
    @property
1467
    def member_leave_policy_display(self):
1468
        policy = self.member_leave_policy
1469
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1470

    
1471

    
1472
class ProjectResourceGrantManager(models.Manager):
1473
    def grants_per_app(self, applications):
1474
        app_ids = [app.id for app in applications]
1475
        grants = self.filter(
1476
            project_application__in=app_ids).select_related("resource")
1477
        return _partition_by(lambda g: g.project_application_id, grants)
1478

    
1479

    
1480
class ProjectResourceGrant(models.Model):
1481

    
1482
    resource = models.ForeignKey(Resource)
1483
    project_application = models.ForeignKey(ProjectApplication)
1484
    project_capacity = models.BigIntegerField()
1485
    member_capacity = models.BigIntegerField()
1486

    
1487
    objects = ProjectResourceGrantManager()
1488

    
1489
    class Meta:
1490
        unique_together = ("resource", "project_application")
1491

    
1492
    def display_member_capacity(self):
1493
        return units.show(self.member_capacity, self.resource.unit)
1494

    
1495
    def __str__(self):
1496
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1497
                                        self.display_member_capacity())
1498

    
1499

    
1500
class ProjectManager(models.Manager):
1501
    def expired_projects(self):
1502
        model = self.model
1503
        q = (Q(state__in=[model.NORMAL, model.SUSPENDED]) &
1504
             Q(end_date__lt=datetime.now()))
1505
        return self.filter(q)
1506

    
1507
    def user_accessible_projects(self, user):
1508
        """
1509
        Return projects accessible by specified user.
1510
        """
1511
        model = self.model
1512
        if user.is_project_admin():
1513
            flt = Q()
1514
        else:
1515
            membs = user.projectmembership_set.associated()
1516
            memb_projects = membs.values_list("project", flat=True)
1517
            flt = (Q(owner=user) |
1518
                   Q(last_application__applicant=user) |
1519
                   Q(id__in=memb_projects))
1520

    
1521
        relevant = ~Q(state=model.DELETED)
1522
        return self.filter(flt, relevant).order_by(
1523
            'creation_date').select_related('last_application', 'owner')
1524

    
1525
    def search_by_name(self, *search_strings):
1526
        q = Q()
1527
        for s in search_strings:
1528
            q = q | Q(name__icontains=s)
1529
        return self.filter(q)
1530

    
1531
    def initialized(self, flt=None):
1532
        q = Q(state__in=self.model.INITIALIZED_STATES)
1533
        if flt is not None:
1534
            q &= flt
1535
        return self.filter(q)
1536

    
1537

    
1538
class Project(models.Model):
1539

    
1540
    id = models.BigIntegerField(db_column='id', primary_key=True)
1541

    
1542
    last_application = models.ForeignKey(ProjectApplication, null=True,
1543
                                         related_name='last_of_project')
1544

    
1545
    members = models.ManyToManyField(
1546
        AstakosUser,
1547
        through='ProjectMembership')
1548

    
1549
    creation_date = models.DateTimeField(auto_now_add=True)
1550
    name = models.CharField(
1551
        max_length=80,
1552
        null=True,
1553
        db_index=True,
1554
        unique=True)
1555

    
1556
    UNINITIALIZED = 0
1557
    NORMAL = 1
1558
    SUSPENDED = 10
1559
    TERMINATED = 100
1560
    DELETED = 1000
1561

    
1562
    INITIALIZED_STATES = [NORMAL,
1563
                          SUSPENDED,
1564
                          TERMINATED,
1565
                          ]
1566

    
1567
    ALIVE_STATES = [NORMAL,
1568
                    SUSPENDED,
1569
                    ]
1570

    
1571
    SKIP_STATES = [DELETED,
1572
                   TERMINATED,
1573
                   ]
1574

    
1575
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1576

    
1577
    state = models.IntegerField(default=UNINITIALIZED,
1578
                                db_index=True)
1579
    uuid = models.CharField(max_length=255, unique=True)
1580

    
1581
    owner = models.ForeignKey(
1582
        AstakosUser,
1583
        related_name='projs_owned',
1584
        null=True,
1585
        db_index=True)
1586
    realname = models.CharField(max_length=80)
1587
    homepage = models.URLField(max_length=255, verify_exists=False)
1588
    description = models.TextField(blank=True)
1589
    end_date = models.DateTimeField()
1590
    member_join_policy = models.IntegerField()
1591
    member_leave_policy = models.IntegerField()
1592
    limit_on_members_number = models.BigIntegerField()
1593
    resource_grants = models.ManyToManyField(
1594
        Resource,
1595
        null=True,
1596
        blank=True,
1597
        through='ProjectResourceQuota')
1598
    private = models.BooleanField(default=False)
1599

    
1600
    objects = ProjectManager()
1601

    
1602
    def __str__(self):
1603
        return uenc(_("<project %s '%s'>") %
1604
                    (self.id, udec(self.realname)))
1605

    
1606
    __repr__ = __str__
1607

    
1608
    def __unicode__(self):
1609
        return _("<project %s '%s'>") % (self.id, self.realname)
1610

    
1611
    O_UNINITIALIZED = -1
1612
    O_PENDING = 0
1613
    O_ACTIVE = 1
1614
    O_ACTIVE_PENDING = 2
1615
    O_DENIED = 3
1616
    O_DISMISSED = 4
1617
    O_CANCELLED = 5
1618
    O_SUSPENDED = 10
1619
    O_TERMINATED = 100
1620
    O_DELETED = 1000
1621

    
1622
    O_STATE_DISPLAY = {
1623
        O_UNINITIALIZED: _("Uninitialized"),
1624
        O_PENDING:    _("Pending"),
1625
        O_ACTIVE:     _("Active"),
1626
        O_DENIED:     _("Denied"),
1627
        O_DISMISSED:  _("Dismissed"),
1628
        O_CANCELLED:  _("Cancelled"),
1629
        O_SUSPENDED:  _("Suspended"),
1630
        O_TERMINATED: _("Terminated"),
1631
        O_DELETED:    _("Deleted"),
1632
    }
1633

    
1634
    O_STATE_UNINITIALIZED = {
1635
        None: O_UNINITIALIZED,
1636
        ProjectApplication.PENDING: O_PENDING,
1637
        ProjectApplication.DENIED:  O_DENIED,
1638
        }
1639
    O_STATE_DELETED = {
1640
        None: O_DELETED,
1641
        ProjectApplication.DISMISSED: O_DISMISSED,
1642
        ProjectApplication.CANCELLED: O_CANCELLED,
1643
        }
1644

    
1645
    OVERALL_STATE = {
1646
        NORMAL: lambda app_state: Project.O_ACTIVE,
1647
        UNINITIALIZED: lambda app_state: Project.O_STATE_UNINITIALIZED.get(
1648
            app_state, None),
1649
        DELETED: lambda app_state: Project.O_STATE_DELETED.get(
1650
            app_state, None),
1651
        SUSPENDED: lambda app_state: Project.O_SUSPENDED,
1652
        TERMINATED: lambda app_state: Project.O_TERMINATED,
1653
        }
1654

    
1655
    @classmethod
1656
    def _overall_state(cls, project_state, app_state):
1657
        os = cls.OVERALL_STATE.get(project_state, None)
1658
        if os is None:
1659
            return None
1660
        return os(app_state)
1661

    
1662
    def overall_state(self):
1663
        app_state = (self.last_application.state
1664
                     if self.last_application else None)
1665
        return self._overall_state(self.state, app_state)
1666

    
1667
    def last_pending_application(self):
1668
        app = self.last_application
1669
        if app and app.state == ProjectApplication.PENDING:
1670
            return app
1671
        return None
1672

    
1673
    def last_pending_modification(self):
1674
        last_pending = self.last_pending_application()
1675
        if self.state != Project.UNINITIALIZED:
1676
            return last_pending
1677
        return None
1678

    
1679
    def state_display(self):
1680
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1681

    
1682
    def expiration_info(self):
1683
        return (str(self.id), self.name, self.state_display(),
1684
                str(self.end_date))
1685

    
1686
    def last_deactivation(self):
1687
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1688
        ls = objs.order_by("-date")
1689
        if not ls:
1690
            return None
1691
        return ls[0]
1692

    
1693
    def is_deactivated(self, reason=None):
1694
        if reason is not None:
1695
            return self.state == reason
1696

    
1697
        return self.state != self.NORMAL
1698

    
1699
    def is_active(self):
1700
        return self.state == self.NORMAL
1701

    
1702
    def is_initialized(self):
1703
        return self.state in self.INITIALIZED_STATES
1704

    
1705
    ### Deactivation calls
1706

    
1707
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1708
                    comments=None):
1709
        now = datetime.now()
1710
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1711
                        actor=actor, reason=reason, comments=comments)
1712

    
1713
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1714
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1715
                         comments=comments)
1716
        self.state = to_state
1717
        self.save()
1718

    
1719
    def terminate(self, actor=None, reason=None):
1720
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1721
        self.name = None
1722
        self.save()
1723

    
1724
    def suspend(self, actor=None, reason=None):
1725
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1726

    
1727
    def resume(self, actor=None, reason=None):
1728
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1729
        if self.name is None:
1730
            self.name = self.realname
1731
            self.save()
1732

    
1733
    def activate(self, actor=None, reason=None):
1734
        assert self.state != self.DELETED, \
1735
            "cannot activate: %s is deleted" % self
1736
        if self.state != self.NORMAL:
1737
            self.set_state(self.NORMAL, actor=actor, reason=reason)
1738
        if self.name != self.realname:
1739
            self.name = self.realname
1740
            self.save()
1741

    
1742
    def set_deleted(self, actor=None, reason=None):
1743
        self.set_state(self.DELETED, actor=actor, reason=reason)
1744

    
1745
    ### Logical checks
1746

    
1747
    @property
1748
    def is_alive(self):
1749
        return self.state in [self.NORMAL, self.SUSPENDED]
1750

    
1751
    @property
1752
    def is_terminated(self):
1753
        return self.is_deactivated(self.TERMINATED)
1754

    
1755
    @property
1756
    def is_suspended(self):
1757
        return self.is_deactivated(self.SUSPENDED)
1758

    
1759
    def violates_members_limit(self, adding=0):
1760
        limit = self.limit_on_members_number
1761
        return (len(self.approved_members) + adding > limit)
1762

    
1763
    ### Other
1764

    
1765
    def count_pending_memberships(self):
1766
        return self.projectmembership_set.requested().count()
1767

    
1768
    def members_count(self):
1769
        return self.approved_memberships.count()
1770

    
1771
    @property
1772
    def approved_memberships(self):
1773
        query = ProjectMembership.Q_ACCEPTED_STATES
1774
        return self.projectmembership_set.filter(query)
1775

    
1776
    @property
1777
    def approved_members(self):
1778
        return [m.person for m in self.approved_memberships]
1779

    
1780
    @property
1781
    def member_join_policy_display(self):
1782
        policy = self.member_join_policy
1783
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1784

    
1785
    @property
1786
    def member_leave_policy_display(self):
1787
        policy = self.member_leave_policy
1788
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1789

    
1790

    
1791
def create_project(**kwargs):
1792
    if "uuid" not in kwargs:
1793
        kwargs["uuid"] = str(uuid.uuid4())
1794
    return Project.objects.create(**kwargs)
1795

    
1796

    
1797
class ProjectResourceQuotaManager(models.Manager):
1798
    def quotas_per_project(self, projects):
1799
        proj_ids = [proj.id for proj in projects]
1800
        quotas = self.filter(
1801
            project__in=proj_ids).select_related("resource")
1802
        return _partition_by(lambda g: g.project_id, quotas)
1803

    
1804

    
1805
class ProjectResourceQuota(models.Model):
1806

    
1807
    resource = models.ForeignKey(Resource)
1808
    project = models.ForeignKey(Project)
1809
    project_capacity = models.BigIntegerField(default=0)
1810
    member_capacity = models.BigIntegerField(default=0)
1811

    
1812
    objects = ProjectResourceQuotaManager()
1813

    
1814
    class Meta:
1815
        unique_together = ("resource", "project")
1816

    
1817

    
1818
class ProjectLogManager(models.Manager):
1819
    def last_deactivations(self, projects):
1820
        logs = self.filter(
1821
            project__in=projects,
1822
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1823
        return first_of_group(lambda l: l.project_id, logs)
1824

    
1825

    
1826
class ProjectLog(models.Model):
1827
    project = models.ForeignKey(Project, related_name="log")
1828
    from_state = models.IntegerField(null=True)
1829
    to_state = models.IntegerField()
1830
    date = models.DateTimeField()
1831
    actor = models.ForeignKey(AstakosUser, null=True)
1832
    reason = models.TextField(null=True)
1833
    comments = models.TextField(null=True)
1834

    
1835
    objects = ProjectLogManager()
1836

    
1837

    
1838
class ProjectLock(models.Model):
1839
    pass
1840

    
1841

    
1842
class ProjectMembershipManager(models.Manager):
1843

    
1844
    def any_accepted(self):
1845
        q = self.model.Q_ACCEPTED_STATES
1846
        return self.filter(q)
1847

    
1848
    def actually_accepted(self):
1849
        q = self.model.Q_ACTUALLY_ACCEPTED
1850
        return self.filter(q)
1851

    
1852
    def requested(self):
1853
        return self.filter(state=ProjectMembership.REQUESTED)
1854

    
1855
    def suspended(self):
1856
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1857

    
1858
    def associated(self):
1859
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1860

    
1861
    def any_accepted_per_project(self, projects):
1862
        ms = self.any_accepted().filter(project__in=projects)
1863
        return _partition_by(lambda m: m.project_id, ms)
1864

    
1865
    def requested_per_project(self, projects):
1866
        ms = self.requested().filter(project__in=projects)
1867
        return _partition_by(lambda m: m.project_id, ms)
1868

    
1869
    def one_per_project(self):
1870
        ms = self.all().select_related(
1871
            'project', 'project__application',
1872
            'project__application__owner', 'project_application__applicant',
1873
            'person')
1874
        m_per_p = {}
1875
        for m in ms:
1876
            m_per_p[m.project_id] = m
1877
        return m_per_p
1878

    
1879

    
1880
class ProjectMembership(models.Model):
1881

    
1882
    person = models.ForeignKey(AstakosUser)
1883
    project = models.ForeignKey(Project)
1884

    
1885
    REQUESTED = 0
1886
    ACCEPTED = 1
1887
    LEAVE_REQUESTED = 5
1888
    # User deactivation
1889
    USER_SUSPENDED = 10
1890
    REJECTED = 100
1891
    CANCELLED = 101
1892
    REMOVED = 200
1893

    
1894
    ASSOCIATED_STATES = set([REQUESTED,
1895
                             ACCEPTED,
1896
                             LEAVE_REQUESTED,
1897
                             USER_SUSPENDED,
1898
                             ])
1899

    
1900
    ACCEPTED_STATES = set([ACCEPTED,
1901
                           LEAVE_REQUESTED,
1902
                           USER_SUSPENDED,
1903
                           ])
1904

    
1905
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1906

    
1907
    state = models.IntegerField(default=REQUESTED,
1908
                                db_index=True)
1909

    
1910
    objects = ProjectMembershipManager()
1911

    
1912
    # Compiled queries
1913
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1914
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1915

    
1916
    MEMBERSHIP_STATE_DISPLAY = {
1917
        REQUESTED:       _('Requested'),
1918
        ACCEPTED:        _('Accepted'),
1919
        LEAVE_REQUESTED: _('Leave Requested'),
1920
        USER_SUSPENDED:  _('Suspended'),
1921
        REJECTED:        _('Rejected'),
1922
        CANCELLED:       _('Cancelled'),
1923
        REMOVED:         _('Removed'),
1924
    }
1925

    
1926
    USER_FRIENDLY_STATE_DISPLAY = {
1927
        REQUESTED:       _('Join requested'),
1928
        ACCEPTED:        _('Accepted member'),
1929
        LEAVE_REQUESTED: _('Requested to leave'),
1930
        USER_SUSPENDED:  _('Suspended member'),
1931
        REJECTED:        _('Request rejected'),
1932
        CANCELLED:       _('Request cancelled'),
1933
        REMOVED:         _('Removed member'),
1934
    }
1935

    
1936
    def state_display(self):
1937
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1938

    
1939
    def user_friendly_state_display(self):
1940
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1941

    
1942
    class Meta:
1943
        unique_together = ("person", "project")
1944
        #index_together = [["project", "state"]]
1945

    
1946
    def __str__(self):
1947
        return uenc(_("<'%s' membership in '%s'>") %
1948
                    (self.person.username, self.project))
1949

    
1950
    __repr__ = __str__
1951

    
1952
    def latest_log(self):
1953
        logs = self.log.all()
1954
        logs_d = _partition_by(lambda l: l.to_state, logs)
1955
        for s, s_logs in logs_d.iteritems():
1956
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1957
        return logs_d
1958

    
1959
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1960
                    comments=None):
1961
        now = datetime.now()
1962
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1963
                        actor=actor, reason=reason, comments=comments)
1964

    
1965
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1966
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1967
                         comments=comments)
1968
        self.state = to_state
1969
        self.save()
1970

    
1971
    ACTION_CHECKS = {
1972
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
1973
        "accept": lambda m: m.state == m.REQUESTED,
1974
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
1975
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
1976
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
1977
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1978
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1979
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
1980
        "reject": lambda m: m.state == m.REQUESTED,
1981
        "cancel": lambda m: m.state == m.REQUESTED,
1982
    }
1983

    
1984
    ACTION_STATES = {
1985
        "join":          REQUESTED,
1986
        "accept":        ACCEPTED,
1987
        "enroll":        ACCEPTED,
1988
        "leave_request": LEAVE_REQUESTED,
1989
        "deny_leave":    ACCEPTED,
1990
        "cancel_leave":  ACCEPTED,
1991
        "remove":        REMOVED,
1992
        "reject":        REJECTED,
1993
        "cancel":        CANCELLED,
1994
    }
1995

    
1996
    def check_action(self, action):
1997
        try:
1998
            check = self.ACTION_CHECKS[action]
1999
        except KeyError:
2000
            raise ValueError("No check found for action '%s'" % action)
2001
        return check(self)
2002

    
2003
    def perform_action(self, action, actor=None, reason=None):
2004
        if not self.check_action(action):
2005
            m = _("%s: attempted action '%s' in state '%s'") % (
2006
                self, action, self.state)
2007
            raise AssertionError(m)
2008
        try:
2009
            s = self.ACTION_STATES[action]
2010
        except KeyError:
2011
            raise ValueError("No such action '%s'" % action)
2012
        return self.set_state(s, actor=actor, reason=reason)
2013

    
2014

    
2015
class ProjectMembershipLogManager(models.Manager):
2016
    def last_logs(self, memberships):
2017
        logs = self.filter(membership__in=memberships).order_by("-date")
2018
        logs = _partition_by(lambda l: l.membership_id, logs)
2019

    
2020
        for memb_id, m_logs in logs.iteritems():
2021
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
2022
        return logs
2023

    
2024

    
2025
class ProjectMembershipLog(models.Model):
2026
    membership = models.ForeignKey(ProjectMembership, related_name="log")
2027
    from_state = models.IntegerField(null=True)
2028
    to_state = models.IntegerField()
2029
    date = models.DateTimeField()
2030
    actor = models.ForeignKey(AstakosUser, null=True)
2031
    reason = models.TextField(null=True)
2032
    comments = models.TextField(null=True)
2033

    
2034
    objects = ProjectMembershipLogManager()
2035

    
2036

    
2037
### SIGNALS ###
2038
################
2039

    
2040
def resource_post_save(sender, instance, created, **kwargs):
2041
    pass
2042

    
2043
post_save.connect(resource_post_save, sender=Resource)
2044

    
2045

    
2046
def renew_token(sender, instance, **kwargs):
2047
    if not instance.auth_token:
2048
        instance.renew_token()
2049
pre_save.connect(renew_token, sender=Component)