Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (68.3 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 hashlib
35
import uuid
36
import logging
37
import json
38
import math
39
import copy
40

    
41
from datetime import datetime, timedelta
42
import base64
43
from urllib import quote
44
from random import randint
45
import os
46

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

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

    
61
from synnefo.lib.utils import dict_merge
62

    
63
from astakos.im import settings as astakos_settings
64
from astakos.im import auth_providers as auth
65

    
66
import astakos.im.messages as astakos_messages
67
from snf_django.lib.db.managers import ForUpdateManager
68
from synnefo.lib.ordereddict import OrderedDict
69

    
70
from snf_django.lib.db.fields import intDecimalField
71
from synnefo.util.text import uenc, udec
72
from astakos.im import presentation
73

    
74
logger = logging.getLogger(__name__)
75

    
76
DEFAULT_CONTENT_TYPE = None
77
_content_type = None
78

    
79

    
80
def get_content_type():
81
    global _content_type
82
    if _content_type is not None:
83
        return _content_type
84

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

    
93
inf = float('inf')
94

    
95

    
96
def generate_token():
97
    s = os.urandom(32)
98
    return base64.urlsafe_b64encode(s).rstrip('=')
99

    
100

    
101
class Component(models.Model):
102
    name = models.CharField(_('Name'), max_length=255, unique=True,
103
                            db_index=True)
104
    url = models.CharField(_('Component url'), max_length=1024, null=True,
105
                           help_text=_("URL the component is accessible from"))
106
    auth_token = models.CharField(_('Authentication Token'), max_length=64,
107
                                  null=True, blank=True, unique=True)
108
    auth_token_created = models.DateTimeField(_('Token creation date'),
109
                                              null=True)
110
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
111
                                              null=True)
112

    
113
    def renew_token(self, expiration_date=None):
114
        for i in range(10):
115
            new_token = generate_token()
116
            count = Component.objects.filter(auth_token=new_token).count()
117
            if count == 0:
118
                break
119
            continue
120
        else:
121
            raise ValueError('Could not generate a token')
122

    
123
        self.auth_token = new_token
124
        self.auth_token_created = datetime.now()
125
        if expiration_date:
126
            self.auth_token_expires = expiration_date
127
        else:
128
            self.auth_token_expires = None
129
        msg = 'Token renewed for component %s' % self.name
130
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
131

    
132
    def __str__(self):
133
        return self.name
134

    
135
    @classmethod
136
    def catalog(cls, orderfor=None):
137
        catalog = {}
138
        components = list(cls.objects.all())
139
        default_metadata = presentation.COMPONENTS
140
        metadata = {}
141

    
142
        for component in components:
143
            d = {'url': component.url,
144
                 'name': component.name}
145
            if component.name in default_metadata:
146
                metadata[component.name] = default_metadata.get(component.name)
147
                metadata[component.name].update(d)
148
            else:
149
                metadata[component.name] = d
150

    
151
        def component_by_order(s):
152
            return s[1].get('order')
153

    
154
        def component_by_dashboard_order(s):
155
            return s[1].get('dashboard').get('order')
156

    
157
        metadata = dict_merge(metadata,
158
                              astakos_settings.COMPONENTS_META)
159

    
160
        for component, info in metadata.iteritems():
161
            default_meta = presentation.component_defaults(component)
162
            base_meta = metadata.get(component, {})
163
            settings_meta = astakos_settings.COMPONENTS_META.get(component, {})
164
            component_meta = dict_merge(default_meta, base_meta)
165
            meta = dict_merge(component_meta, settings_meta)
166
            catalog[component] = meta
167

    
168
        order_key = component_by_order
169
        if orderfor == 'dashboard':
170
            order_key = component_by_dashboard_order
171

    
172
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
173
                                             key=order_key))
174
        return ordered_catalog
175

    
176

    
177
_presentation_data = {}
178

    
179

    
180
def get_presentation(resource):
181
    global _presentation_data
182
    resource_presentation = _presentation_data.get(resource, {})
183
    if not resource_presentation:
184
        resources_presentation = presentation.RESOURCES.get('resources', {})
185
        resource_presentation = resources_presentation.get(resource, {})
186
        _presentation_data[resource] = resource_presentation
187
    return resource_presentation
188

    
189

    
190
class Service(models.Model):
191
    component = models.ForeignKey(Component)
192
    name = models.CharField(max_length=255, unique=True)
193
    type = models.CharField(max_length=255)
194

    
195

    
196
class Endpoint(models.Model):
197
    service = models.ForeignKey(Service, related_name='endpoints')
198

    
199

    
200
class EndpointData(models.Model):
201
    endpoint = models.ForeignKey(Endpoint, related_name='data')
202
    key = models.CharField(max_length=255)
203
    value = models.CharField(max_length=1024)
204

    
205
    class Meta:
206
        unique_together = (('endpoint', 'key'),)
207

    
208

    
209
class Resource(models.Model):
210
    name = models.CharField(_('Name'), max_length=255, unique=True)
211
    desc = models.TextField(_('Description'), null=True)
212
    service_type = models.CharField(_('Type'), max_length=255)
213
    service_origin = models.CharField(max_length=255, db_index=True)
214
    unit = models.CharField(_('Unit'), null=True, max_length=255)
215
    uplimit = intDecimalField(default=0)
216
    allow_in_projects = models.BooleanField(default=True)
217

    
218
    objects = ForUpdateManager()
219

    
220
    def __str__(self):
221
        return self.name
222

    
223
    def full_name(self):
224
        return str(self)
225

    
226
    def get_info(self):
227
        return {'service': self.service_origin,
228
                'description': self.desc,
229
                'unit': self.unit,
230
                'allow_in_projects': self.allow_in_projects,
231
                }
232

    
233
    @property
234
    def group(self):
235
        default = self.name
236
        return get_presentation(str(self)).get('group', default)
237

    
238
    @property
239
    def help_text(self):
240
        default = "%s resource" % self.name
241
        return get_presentation(str(self)).get('help_text', default)
242

    
243
    @property
244
    def help_text_input_each(self):
245
        default = "%s resource" % self.name
246
        return get_presentation(str(self)).get('help_text_input_each', default)
247

    
248
    @property
249
    def is_abbreviation(self):
250
        return get_presentation(str(self)).get('is_abbreviation', False)
251

    
252
    @property
253
    def report_desc(self):
254
        default = "%s resource" % self.name
255
        return get_presentation(str(self)).get('report_desc', default)
256

    
257
    @property
258
    def placeholder(self):
259
        return get_presentation(str(self)).get('placeholder', self.unit)
260

    
261
    @property
262
    def verbose_name(self):
263
        return get_presentation(str(self)).get('verbose_name', self.name)
264

    
265
    @property
266
    def display_name(self):
267
        name = self.verbose_name
268
        if self.is_abbreviation:
269
            name = name.upper()
270
        return name
271

    
272
    @property
273
    def pluralized_display_name(self):
274
        if not self.unit:
275
            return '%ss' % self.display_name
276
        return self.display_name
277

    
278

    
279
def get_resource_names():
280
    _RESOURCE_NAMES = []
281
    resources = Resource.objects.select_related('service').all()
282
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
283
    return _RESOURCE_NAMES
284

    
285

    
286
class AstakosUserManager(UserManager):
287

    
288
    def get_auth_provider_user(self, provider, **kwargs):
289
        """
290
        Retrieve AstakosUser instance associated with the specified third party
291
        id.
292
        """
293
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
294
                          kwargs.iteritems()))
295
        return self.get(auth_providers__module=provider, **kwargs)
296

    
297
    def get_by_email(self, email):
298
        return self.get(email=email)
299

    
300
    def get_by_identifier(self, email_or_username, **kwargs):
301
        try:
302
            return self.get(email__iexact=email_or_username, **kwargs)
303
        except AstakosUser.DoesNotExist:
304
            return self.get(username__iexact=email_or_username, **kwargs)
305

    
306
    def user_exists(self, email_or_username, **kwargs):
307
        qemail = Q(email__iexact=email_or_username)
308
        qusername = Q(username__iexact=email_or_username)
309
        qextra = Q(**kwargs)
310
        return self.filter((qemail | qusername) & qextra).exists()
311

    
312
    def verified_user_exists(self, email_or_username):
313
        return self.user_exists(email_or_username, email_verified=True)
314

    
315
    def verified(self):
316
        return self.filter(email_verified=True)
317

    
318
    def uuid_catalog(self, l=None):
319
        """
320
        Returns a uuid to username mapping for the uuids appearing in l.
321
        If l is None returns the mapping for all existing users.
322
        """
323
        q = self.filter(uuid__in=l) if l is not None else self
324
        return dict(q.values_list('uuid', 'username'))
325

    
326
    def displayname_catalog(self, l=None):
327
        """
328
        Returns a username to uuid mapping for the usernames appearing in l.
329
        If l is None returns the mapping for all existing users.
330
        """
331
        if l is not None:
332
            lmap = dict((x.lower(), x) for x in l)
333
            q = self.filter(username__in=lmap.keys())
334
            values = ((lmap[n], u)
335
                      for n, u in q.values_list('username', 'uuid'))
336
        else:
337
            q = self
338
            values = self.values_list('username', 'uuid')
339
        return dict(values)
340

    
341

    
342
class AstakosUser(User):
343
    """
344
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
345
    """
346
    affiliation = models.CharField(_('Affiliation'), max_length=255,
347
                                   blank=True, null=True)
348

    
349
    #for invitations
350
    user_level = astakos_settings.DEFAULT_USER_LEVEL
351
    level = models.IntegerField(_('Inviter level'), default=user_level)
352
    invitations = models.IntegerField(
353
        _('Invitations left'),
354
        default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
355

    
356
    auth_token = models.CharField(
357
        _('Authentication Token'),
358
        max_length=64,
359
        unique=True,
360
        null=True,
361
        blank=True,
362
        help_text=_('Renew your authentication '
363
                    'token. Make sure to set the new '
364
                    'token in any client you may be '
365
                    'using, to preserve its '
366
                    'functionality.'))
367
    auth_token_created = models.DateTimeField(_('Token creation date'),
368
                                              null=True)
369
    auth_token_expires = models.DateTimeField(
370
        _('Token expiration date'), null=True)
371

    
372
    updated = models.DateTimeField(_('Update date'))
373

    
374
    # Arbitrary text to identify the reason user got deactivated.
375
    # To be used as a reference from administrators.
376
    deactivated_reason = models.TextField(
377
        _('Reason the user was disabled for'),
378
        default=None, null=True)
379
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
380
                                          blank=True)
381

    
382
    has_credits = models.BooleanField(_('Has credits?'), default=False)
383

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

    
387
    # user email is verified
388
    email_verified = models.BooleanField(_('Email verified?'), default=False)
389

    
390
    # unique string used in user email verification url
391
    verification_code = models.CharField(max_length=255, null=True,
392
                                         blank=False, unique=True)
393

    
394
    # date user email verified
395
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
396
                                       blank=True)
397

    
398
    # email verification notice was sent to the user at this time
399
    activation_sent = models.DateTimeField(_('Activation sent date'),
400
                                           null=True, blank=True)
401

    
402
    # user got rejected during moderation process
403
    is_rejected = models.BooleanField(_('Account rejected'),
404
                                      default=False)
405
    # reason user got rejected
406
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
407
                                       blank=True)
408
    # moderation status
409
    moderated = models.BooleanField(_('User moderated'), default=False)
410
    # date user moderated (either accepted or rejected)
411
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
412
                                        blank=True, null=True)
413
    # a snapshot of user instance the time got moderated
414
    moderated_data = models.TextField(null=True, default=None, blank=True)
415
    # a string which identifies how the user got moderated
416
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
417
                                       default=None, null=True, blank=True)
418
    # the email used to accept the user
419
    accepted_email = models.EmailField(null=True, default=None, blank=True)
420

    
421
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
422
                                           default=False)
423
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
424
                                             null=True, blank=True)
425
    # permanent unique user identifier
426
    uuid = models.CharField(max_length=255, null=True, blank=False,
427
                            unique=True)
428

    
429
    policy = models.ManyToManyField(
430
        Resource, null=True, through='AstakosUserQuota')
431

    
432
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
433
                                          default=False, db_index=True)
434

    
435
    objects = AstakosUserManager()
436
    forupdate = ForUpdateManager()
437

    
438
    def __init__(self, *args, **kwargs):
439
        super(AstakosUser, self).__init__(*args, **kwargs)
440
        if not self.id:
441
            self.is_active = False
442

    
443
    @property
444
    def realname(self):
445
        return '%s %s' % (self.first_name, self.last_name)
446

    
447
    @property
448
    def log_display(self):
449
        """
450
        Should be used in all logger.* calls that refer to a user so that
451
        user display is consistent across log entries.
452
        """
453
        return '%s::%s' % (self.uuid, self.email)
454

    
455
    @realname.setter
456
    def realname(self, value):
457
        parts = value.split(' ')
458
        if len(parts) == 2:
459
            self.first_name = parts[0]
460
            self.last_name = parts[1]
461
        else:
462
            self.last_name = parts[0]
463

    
464
    def add_permission(self, pname):
465
        if self.has_perm(pname):
466
            return
467
        p, created = Permission.objects.get_or_create(
468
            codename=pname,
469
            name=pname.capitalize(),
470
            content_type=get_content_type())
471
        self.user_permissions.add(p)
472

    
473
    def remove_permission(self, pname):
474
        if self.has_perm(pname):
475
            return
476
        p = Permission.objects.get(codename=pname,
477
                                   content_type=get_content_type())
478
        self.user_permissions.remove(p)
479

    
480
    def add_group(self, gname):
481
        group, _ = Group.objects.get_or_create(name=gname)
482
        self.groups.add(group)
483

    
484
    def is_project_admin(self, application_id=None):
485
        return self.uuid in astakos_settings.PROJECT_ADMINS
486

    
487
    @property
488
    def invitation(self):
489
        try:
490
            return Invitation.objects.get(username=self.email)
491
        except Invitation.DoesNotExist:
492
            return None
493

    
494
    @property
495
    def policies(self):
496
        return self.astakosuserquota_set.select_related().all()
497

    
498
    def get_resource_policy(self, resource):
499
        resource = Resource.objects.get(name=resource)
500
        default_capacity = resource.uplimit
501
        try:
502
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
503
            return policy, default_capacity
504
        except AstakosUserQuota.DoesNotExist:
505
            return None, default_capacity
506

    
507
    def update_uuid(self):
508
        while not self.uuid:
509
            uuid_val = str(uuid.uuid4())
510
            try:
511
                AstakosUser.objects.get(uuid=uuid_val)
512
            except AstakosUser.DoesNotExist, e:
513
                self.uuid = uuid_val
514
        return self.uuid
515

    
516
    def save(self, update_timestamps=True, **kwargs):
517
        if update_timestamps:
518
            if not self.id:
519
                self.date_joined = datetime.now()
520
            self.updated = datetime.now()
521

    
522
        self.update_uuid()
523

    
524
        if not self.verification_code:
525
            self.renew_verification_code()
526

    
527
        # username currently matches email
528
        if self.username != self.email.lower():
529
            self.username = self.email.lower()
530

    
531
        super(AstakosUser, self).save(**kwargs)
532

    
533
    def renew_verification_code(self):
534
        self.verification_code = str(uuid.uuid4())
535
        logger.info("Verification code renewed for %s" % self.log_display)
536

    
537
    def renew_token(self, flush_sessions=False, current_key=None):
538
        for i in range(10):
539
            new_token = generate_token()
540
            count = AstakosUser.objects.filter(auth_token=new_token).count()
541
            if count == 0:
542
                break
543
            continue
544
        else:
545
            raise ValueError('Could not generate a token')
546

    
547
        self.auth_token = new_token
548
        self.auth_token_created = datetime.now()
549
        self.auth_token_expires = self.auth_token_created + \
550
            timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
551
        if flush_sessions:
552
            self.flush_sessions(current_key)
553
        msg = 'Token renewed for %s' % self.log_display
554
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
555

    
556
    def token_expired(self):
557
        return self.auth_token_expires < datetime.now()
558

    
559
    def flush_sessions(self, current_key=None):
560
        q = self.sessions
561
        if current_key:
562
            q = q.exclude(session_key=current_key)
563

    
564
        keys = q.values_list('session_key', flat=True)
565
        if keys:
566
            msg = 'Flushing sessions: %s' % ','.join(keys)
567
            logger.log(astakos_settings.LOGGING_LEVEL, msg, [])
568
        engine = import_module(settings.SESSION_ENGINE)
569
        for k in keys:
570
            s = engine.SessionStore(k)
571
            s.flush()
572

    
573
    def __unicode__(self):
574
        return '%s (%s)' % (self.realname, self.email)
575

    
576
    def conflicting_email(self):
577
        q = AstakosUser.objects.exclude(username=self.username)
578
        q = q.filter(email__iexact=self.email)
579
        if q.count() != 0:
580
            return True
581
        return False
582

    
583
    def email_change_is_pending(self):
584
        return self.emailchanges.count() > 0
585

    
586
    @property
587
    def status_display(self):
588
        msg = ""
589
        append = None
590
        if self.is_active:
591
            msg = "Accepted/Active"
592
        if self.is_rejected:
593
            msg = "Rejected"
594
            if self.rejected_reason:
595
                msg += " (%s)" % self.rejected_reason
596
        if not self.email_verified:
597
            msg = "Pending email verification"
598
        if not self.moderated:
599
            msg = "Pending moderation"
600
        if not self.is_active and self.email_verified:
601
            msg = "Accepted/Inactive"
602
            if self.deactivated_reason:
603
                msg += " (%s)" % (self.deactivated_reason)
604

    
605
        if self.moderated and not self.is_rejected:
606
            if self.accepted_policy == 'manual':
607
                msg += " (manually accepted)"
608
            else:
609
                msg += " (accepted policy: %s)" % \
610
                    self.accepted_policy
611
        return msg
612

    
613
    @property
614
    def signed_terms(self):
615
        term = get_latest_terms()
616
        if not term:
617
            return True
618
        if not self.has_signed_terms:
619
            return False
620
        if not self.date_signed_terms:
621
            return False
622
        if self.date_signed_terms < term.date:
623
            self.has_signed_terms = False
624
            self.date_signed_terms = None
625
            self.save()
626
            return False
627
        return True
628

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
710
        modules = astakos_settings.IM_MODULES
711

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

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

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

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

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

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

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

    
745
    def get_inactive_message(self, provider_module, identifier=None):
746
        provider = self.get_auth_provider(provider_module, identifier)
747

    
748
        msg_extra = ''
749
        message = ''
750

    
751
        msg_inactive = provider.get_account_inactive_msg
752
        msg_pending = provider.get_pending_activation_msg
753
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
754
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
755
        msg_pending_mod = provider.get_pending_moderation_msg
756
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
757
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
758

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

    
774
        return mark_safe(message + u' ' + msg_extra)
775

    
776
    def owns_application(self, application):
777
        return application.owner == self
778

    
779
    def owns_project(self, project):
780
        return project.application.owner == self
781

    
782
    def is_associated(self, project):
783
        try:
784
            m = ProjectMembership.objects.get(person=self, project=project)
785
            return m.state in ProjectMembership.ASSOCIATED_STATES
786
        except ProjectMembership.DoesNotExist:
787
            return False
788

    
789
    def get_membership(self, project):
790
        try:
791
            return ProjectMembership.objects.get(
792
                project=project,
793
                person=self)
794
        except ProjectMembership.DoesNotExist:
795
            return None
796

    
797
    def membership_display(self, project):
798
        m = self.get_membership(project)
799
        if m is None:
800
            return _('Not a member')
801
        else:
802
            return m.user_friendly_state_display()
803

    
804
    def non_owner_can_view(self, maybe_project):
805
        if self.is_project_admin():
806
            return True
807
        if maybe_project is None:
808
            return False
809
        project = maybe_project
810
        if self.is_associated(project):
811
            return True
812
        if project.is_deactivated():
813
            return False
814
        return True
815

    
816

    
817
class AstakosUserAuthProviderManager(models.Manager):
818

    
819
    def active(self, **filters):
820
        return self.filter(active=True, **filters)
821

    
822
    def remove_unverified_providers(self, provider, **filters):
823
        try:
824
            existing = self.filter(module=provider, user__email_verified=False,
825
                                   **filters)
826
            for p in existing:
827
                p.user.delete()
828
        except:
829
            pass
830

    
831
    def unverified(self, provider, **filters):
832
        try:
833
            return self.get(module=provider, user__email_verified=False,
834
                            **filters).settings
835
        except AstakosUserAuthProvider.DoesNotExist:
836
            return None
837

    
838
    def verified(self, provider, **filters):
839
        try:
840
            return self.get(module=provider, user__email_verified=True,
841
                            **filters).settings
842
        except AstakosUserAuthProvider.DoesNotExist:
843
            return None
844

    
845

    
846
class AuthProviderPolicyProfileManager(models.Manager):
847

    
848
    def active(self):
849
        return self.filter(active=True)
850

    
851
    def for_user(self, user, provider):
852
        policies = {}
853
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
854
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
855
        exclusive_q = exclusive_q1 | exclusive_q2
856

    
857
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
858
            policies.update(profile.policies)
859

    
860
        user_groups = user.groups.all().values('pk')
861
        for profile in self.active().filter(groups__in=user_groups).filter(
862
                exclusive_q):
863
            policies.update(profile.policies)
864
        return policies
865

    
866
    def add_policy(self, name, provider, group_or_user, exclusive=False,
867
                   **policies):
868
        is_group = isinstance(group_or_user, Group)
869
        profile, created = self.get_or_create(name=name, provider=provider,
870
                                              is_exclusive=exclusive)
871
        profile.is_exclusive = exclusive
872
        profile.save()
873
        if is_group:
874
            profile.groups.add(group_or_user)
875
        else:
876
            profile.users.add(group_or_user)
877
        profile.set_policies(policies)
878
        profile.save()
879
        return profile
880

    
881

    
882
class AuthProviderPolicyProfile(models.Model):
883
    name = models.CharField(_('Name'), max_length=255, blank=False,
884
                            null=False, db_index=True)
885
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
886
                                null=False)
887

    
888
    # apply policies to all providers excluding the one set in provider field
889
    is_exclusive = models.BooleanField(default=False)
890

    
891
    policy_add = models.NullBooleanField(null=True, default=None)
892
    policy_remove = models.NullBooleanField(null=True, default=None)
893
    policy_create = models.NullBooleanField(null=True, default=None)
894
    policy_login = models.NullBooleanField(null=True, default=None)
895
    policy_limit = models.IntegerField(null=True, default=None)
896
    policy_required = models.NullBooleanField(null=True, default=None)
897
    policy_automoderate = models.NullBooleanField(null=True, default=None)
898
    policy_switch = models.NullBooleanField(null=True, default=None)
899

    
900
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
901
                     'automoderate')
902

    
903
    priority = models.IntegerField(null=False, default=1)
904
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
905
    users = models.ManyToManyField(AstakosUser,
906
                                   related_name='authpolicy_profiles')
907
    active = models.BooleanField(default=True)
908

    
909
    objects = AuthProviderPolicyProfileManager()
910

    
911
    class Meta:
912
        ordering = ['priority']
913

    
914
    @property
915
    def policies(self):
916
        policies = {}
917
        for pkey in self.POLICY_FIELDS:
918
            value = getattr(self, 'policy_%s' % pkey, None)
919
            if value is None:
920
                continue
921
            policies[pkey] = value
922
        return policies
923

    
924
    def set_policies(self, policies_dict):
925
        for key, value in policies_dict.iteritems():
926
            if key in self.POLICY_FIELDS:
927
                setattr(self, 'policy_%s' % key, value)
928
        return self.policies
929

    
930

    
931
class AstakosUserAuthProvider(models.Model):
932
    """
933
    Available user authentication methods.
934
    """
935
    affiliation = models.CharField(_('Affiliation'), max_length=255,
936
                                   blank=True, null=True, default=None)
937
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
938
    module = models.CharField(_('Provider'), max_length=255, blank=False,
939
                              default='local')
940
    identifier = models.CharField(_('Third-party identifier'),
941
                                  max_length=255, null=True,
942
                                  blank=True)
943
    active = models.BooleanField(default=True)
944
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
945
                                    default='astakos')
946
    info_data = models.TextField(default="", null=True, blank=True)
947
    created = models.DateTimeField('Creation date', auto_now_add=True)
948

    
949
    objects = AstakosUserAuthProviderManager()
950

    
951
    class Meta:
952
        unique_together = (('identifier', 'module', 'user'), )
953
        ordering = ('module', 'created')
954

    
955
    def __init__(self, *args, **kwargs):
956
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
957
        try:
958
            self.info = json.loads(self.info_data)
959
            if not self.info:
960
                self.info = {}
961
        except Exception, e:
962
            self.info = {}
963

    
964
        for key, value in self.info.iteritems():
965
            setattr(self, 'info_%s' % key, value)
966

    
967
    @property
968
    def settings(self):
969
        extra_data = {}
970

    
971
        info_data = {}
972
        if self.info_data:
973
            info_data = json.loads(self.info_data)
974

    
975
        extra_data['info'] = info_data
976

    
977
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
978
            extra_data[key] = getattr(self, key)
979

    
980
        extra_data['instance'] = self
981
        return auth.get_provider(self.module, self.user,
982
                                 self.identifier, **extra_data)
983

    
984
    def __repr__(self):
985
        return '<AstakosUserAuthProvider %s:%s>' % (
986
            self.module, self.identifier)
987

    
988
    def __unicode__(self):
989
        if self.identifier:
990
            return "%s:%s" % (self.module, self.identifier)
991
        if self.auth_backend:
992
            return "%s:%s" % (self.module, self.auth_backend)
993
        return self.module
994

    
995
    def save(self, *args, **kwargs):
996
        self.info_data = json.dumps(self.info)
997
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
998

    
999

    
1000
class ExtendedManager(models.Manager):
1001
    def _update_or_create(self, **kwargs):
1002
        assert kwargs, \
1003
            'update_or_create() must be passed at least one keyword argument'
1004
        obj, created = self.get_or_create(**kwargs)
1005
        defaults = kwargs.pop('defaults', {})
1006
        if created:
1007
            return obj, True, False
1008
        else:
1009
            try:
1010
                params = dict(
1011
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
1012
                params.update(defaults)
1013
                for attr, val in params.items():
1014
                    if hasattr(obj, attr):
1015
                        setattr(obj, attr, val)
1016
                sid = transaction.savepoint()
1017
                obj.save(force_update=True)
1018
                transaction.savepoint_commit(sid)
1019
                return obj, False, True
1020
            except IntegrityError, e:
1021
                transaction.savepoint_rollback(sid)
1022
                try:
1023
                    return self.get(**kwargs), False, False
1024
                except self.model.DoesNotExist:
1025
                    raise e
1026

    
1027
    update_or_create = _update_or_create
1028

    
1029

    
1030
class AstakosUserQuota(models.Model):
1031
    objects = ExtendedManager()
1032
    capacity = intDecimalField()
1033
    resource = models.ForeignKey(Resource)
1034
    user = models.ForeignKey(AstakosUser)
1035

    
1036
    class Meta:
1037
        unique_together = ("resource", "user")
1038

    
1039

    
1040
class ApprovalTerms(models.Model):
1041
    """
1042
    Model for approval terms
1043
    """
1044

    
1045
    date = models.DateTimeField(
1046
        _('Issue date'), db_index=True, auto_now_add=True)
1047
    location = models.CharField(_('Terms location'), max_length=255)
1048

    
1049

    
1050
class Invitation(models.Model):
1051
    """
1052
    Model for registring invitations
1053
    """
1054
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1055
                                null=True)
1056
    realname = models.CharField(_('Real name'), max_length=255)
1057
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1058
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1059
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1060
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1061
    consumed = models.DateTimeField(_('Consumption date'),
1062
                                    null=True, blank=True)
1063

    
1064
    def __init__(self, *args, **kwargs):
1065
        super(Invitation, self).__init__(*args, **kwargs)
1066
        if not self.id:
1067
            self.code = _generate_invitation_code()
1068

    
1069
    def consume(self):
1070
        self.is_consumed = True
1071
        self.consumed = datetime.now()
1072
        self.save()
1073

    
1074
    def __unicode__(self):
1075
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1076

    
1077

    
1078
class EmailChangeManager(models.Manager):
1079

    
1080
    @transaction.commit_on_success
1081
    def change_email(self, activation_key):
1082
        """
1083
        Validate an activation key and change the corresponding
1084
        ``User`` if valid.
1085

1086
        If the key is valid and has not expired, return the ``User``
1087
        after activating.
1088

1089
        If the key is not valid or has expired, return ``None``.
1090

1091
        If the key is valid but the ``User`` is already active,
1092
        return ``None``.
1093

1094
        After successful email change the activation record is deleted.
1095

1096
        Throws ValueError if there is already
1097
        """
1098
        try:
1099
            email_change = self.model.objects.get(
1100
                activation_key=activation_key)
1101
            if email_change.activation_key_expired():
1102
                email_change.delete()
1103
                raise EmailChange.DoesNotExist
1104
            # is there an active user with this address?
1105
            try:
1106
                AstakosUser.objects.get(
1107
                    email__iexact=email_change.new_email_address)
1108
            except AstakosUser.DoesNotExist:
1109
                pass
1110
            else:
1111
                raise ValueError(_('The new email address is reserved.'))
1112
            # update user
1113
            user = AstakosUser.objects.get(pk=email_change.user_id)
1114
            old_email = user.email
1115
            user.email = email_change.new_email_address
1116
            user.save()
1117
            email_change.delete()
1118
            msg = "User %s changed email from %s to %s" % (user.log_display,
1119
                                                           old_email,
1120
                                                           user.email)
1121
            logger.log(astakos_settings.LOGGING_LEVEL, msg)
1122
            return user
1123
        except EmailChange.DoesNotExist:
1124
            raise ValueError(_('Invalid activation key.'))
1125

    
1126

    
1127
class EmailChange(models.Model):
1128
    new_email_address = models.EmailField(
1129
        _(u'new e-mail address'),
1130
        help_text=_('Provide a new email address. Until you verify the new '
1131
                    'address by following the activation link that will be '
1132
                    'sent to it, your old email address will remain active.'))
1133
    user = models.ForeignKey(
1134
        AstakosUser, unique=True, related_name='emailchanges')
1135
    requested_at = models.DateTimeField(auto_now_add=True)
1136
    activation_key = models.CharField(
1137
        max_length=40, unique=True, db_index=True)
1138

    
1139
    objects = EmailChangeManager()
1140

    
1141
    def get_url(self):
1142
        return reverse('email_change_confirm',
1143
                       kwargs={'activation_key': self.activation_key})
1144

    
1145
    def activation_key_expired(self):
1146
        expiration_date = timedelta(
1147
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1148
        return self.requested_at + expiration_date < datetime.now()
1149

    
1150

    
1151
class AdditionalMail(models.Model):
1152
    """
1153
    Model for registring invitations
1154
    """
1155
    owner = models.ForeignKey(AstakosUser)
1156
    email = models.EmailField()
1157

    
1158

    
1159
def _generate_invitation_code():
1160
    while True:
1161
        code = randint(1, 2L ** 63 - 1)
1162
        try:
1163
            Invitation.objects.get(code=code)
1164
            # An invitation with this code already exists, try again
1165
        except Invitation.DoesNotExist:
1166
            return code
1167

    
1168

    
1169
def get_latest_terms():
1170
    try:
1171
        term = ApprovalTerms.objects.order_by('-id')[0]
1172
        return term
1173
    except IndexError:
1174
        pass
1175
    return None
1176

    
1177

    
1178
class PendingThirdPartyUser(models.Model):
1179
    """
1180
    Model for registring successful third party user authentications
1181
    """
1182
    third_party_identifier = models.CharField(
1183
        _('Third-party identifier'), max_length=255, null=True, blank=True)
1184
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1185
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1186
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1187
                                  null=True)
1188
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1189
                                 null=True)
1190
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1191
                                   null=True)
1192
    username = models.CharField(
1193
        _('username'), max_length=30, unique=True,
1194
        help_text=_("Required. 30 characters or fewer. "
1195
                    "Letters, numbers and @/./+/-/_ characters"))
1196
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1197
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1198
    info = models.TextField(default="", null=True, blank=True)
1199

    
1200
    class Meta:
1201
        unique_together = ("provider", "third_party_identifier")
1202

    
1203
    def get_user_instance(self):
1204
        """
1205
        Create a new AstakosUser instance based on details provided when user
1206
        initially signed up.
1207
        """
1208
        d = copy.copy(self.__dict__)
1209
        d.pop('_state', None)
1210
        d.pop('id', None)
1211
        d.pop('token', None)
1212
        d.pop('created', None)
1213
        d.pop('info', None)
1214
        d.pop('affiliation', None)
1215
        d.pop('provider', None)
1216
        d.pop('third_party_identifier', None)
1217
        user = AstakosUser(**d)
1218

    
1219
        return user
1220

    
1221
    @property
1222
    def realname(self):
1223
        return '%s %s' % (self.first_name, self.last_name)
1224

    
1225
    @realname.setter
1226
    def realname(self, value):
1227
        parts = value.split(' ')
1228
        if len(parts) == 2:
1229
            self.first_name = parts[0]
1230
            self.last_name = parts[1]
1231
        else:
1232
            self.last_name = parts[0]
1233

    
1234
    def save(self, **kwargs):
1235
        if not self.id:
1236
            # set username
1237
            while not self.username:
1238
                username = uuid.uuid4().hex[:30]
1239
                try:
1240
                    AstakosUser.objects.get(username=username)
1241
                except AstakosUser.DoesNotExist, e:
1242
                    self.username = username
1243
        super(PendingThirdPartyUser, self).save(**kwargs)
1244

    
1245
    def generate_token(self):
1246
        self.password = self.third_party_identifier
1247
        self.last_login = datetime.now()
1248
        self.token = default_token_generator.make_token(self)
1249

    
1250
    def existing_user(self):
1251
        return AstakosUser.objects.filter(
1252
            auth_providers__module=self.provider,
1253
            auth_providers__identifier=self.third_party_identifier)
1254

    
1255
    def get_provider(self, user):
1256
        params = {
1257
            'info_data': self.info,
1258
            'affiliation': self.affiliation
1259
        }
1260
        return auth.get_provider(self.provider, user,
1261
                                 self.third_party_identifier, **params)
1262

    
1263

    
1264
class SessionCatalog(models.Model):
1265
    session_key = models.CharField(_('session key'), max_length=40)
1266
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1267

    
1268

    
1269
class UserSetting(models.Model):
1270
    user = models.ForeignKey(AstakosUser)
1271
    setting = models.CharField(max_length=255)
1272
    value = models.IntegerField()
1273

    
1274
    objects = ForUpdateManager()
1275

    
1276
    class Meta:
1277
        unique_together = ("user", "setting")
1278

    
1279

    
1280
### PROJECTS ###
1281
################
1282

    
1283
class Chain(models.Model):
1284
    chain = models.AutoField(primary_key=True)
1285
    objects = ForUpdateManager()
1286

    
1287
    def __str__(self):
1288
        return "%s" % (self.chain,)
1289

    
1290

    
1291
def new_chain():
1292
    c = Chain.objects.create()
1293
    return c
1294

    
1295

    
1296
class ProjectApplicationManager(ForUpdateManager):
1297

    
1298
    def user_visible_projects(self, *filters, **kw_filters):
1299
        model = self.model
1300
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1301

    
1302
    def user_visible_by_chain(self, flt):
1303
        model = self.model
1304
        pending = self.filter(
1305
            model.Q_PENDING | model.Q_DENIED).values_list('chain')
1306
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1307
        by_chain = dict(pending.annotate(models.Max('id')))
1308
        by_chain.update(approved.annotate(models.Max('id')))
1309
        return self.filter(flt, id__in=by_chain.values())
1310

    
1311
    def user_accessible_projects(self, user):
1312
        """
1313
        Return projects accessed by specified user.
1314
        """
1315
        if user.is_project_admin():
1316
            participates_filters = Q()
1317
        else:
1318
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1319
                Q(project__projectmembership__person=user)
1320

    
1321
        return self.user_visible_by_chain(
1322
            participates_filters).order_by('issue_date').distinct()
1323

    
1324
    def search_by_name(self, *search_strings):
1325
        q = Q()
1326
        for s in search_strings:
1327
            q = q | Q(name__icontains=s)
1328
        return self.filter(q)
1329

    
1330
    def latest_of_chain(self, chain_id):
1331
        try:
1332
            return self.filter(chain=chain_id).order_by('-id')[0]
1333
        except IndexError:
1334
            return None
1335

    
1336

    
1337
class ProjectApplication(models.Model):
1338
    applicant = models.ForeignKey(
1339
        AstakosUser,
1340
        related_name='projects_applied',
1341
        db_index=True)
1342

    
1343
    PENDING = 0
1344
    APPROVED = 1
1345
    REPLACED = 2
1346
    DENIED = 3
1347
    DISMISSED = 4
1348
    CANCELLED = 5
1349

    
1350
    state = models.IntegerField(default=PENDING,
1351
                                db_index=True)
1352
    owner = models.ForeignKey(
1353
        AstakosUser,
1354
        related_name='projects_owned',
1355
        db_index=True)
1356
    chain = models.ForeignKey('Project',
1357
                              related_name='chained_apps',
1358
                              db_column='chain')
1359
    name = models.CharField(max_length=80)
1360
    homepage = models.URLField(max_length=255, null=True,
1361
                               verify_exists=False)
1362
    description = models.TextField(null=True, blank=True)
1363
    start_date = models.DateTimeField(null=True, blank=True)
1364
    end_date = models.DateTimeField()
1365
    member_join_policy = models.IntegerField()
1366
    member_leave_policy = models.IntegerField()
1367
    limit_on_members_number = models.PositiveIntegerField(null=True)
1368
    resource_grants = models.ManyToManyField(
1369
        Resource,
1370
        null=True,
1371
        blank=True,
1372
        through='ProjectResourceGrant')
1373
    comments = models.TextField(null=True, blank=True)
1374
    issue_date = models.DateTimeField(auto_now_add=True)
1375
    response_date = models.DateTimeField(null=True, blank=True)
1376
    response = models.TextField(null=True, blank=True)
1377

    
1378
    objects = ProjectApplicationManager()
1379

    
1380
    # Compiled queries
1381
    Q_PENDING = Q(state=PENDING)
1382
    Q_APPROVED = Q(state=APPROVED)
1383
    Q_DENIED = Q(state=DENIED)
1384

    
1385
    class Meta:
1386
        unique_together = ("chain", "id")
1387

    
1388
    def __unicode__(self):
1389
        return "%s applied by %s" % (self.name, self.applicant)
1390

    
1391
    # TODO: Move to a more suitable place
1392
    APPLICATION_STATE_DISPLAY = {
1393
        PENDING:   _('Pending review'),
1394
        APPROVED:  _('Approved'),
1395
        REPLACED:  _('Replaced'),
1396
        DENIED:    _('Denied'),
1397
        DISMISSED: _('Dismissed'),
1398
        CANCELLED: _('Cancelled')
1399
    }
1400

    
1401
    @property
1402
    def log_display(self):
1403
        return "application %s (%s) for project %s" % (
1404
            self.id, self.name, self.chain)
1405

    
1406
    def state_display(self):
1407
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1408

    
1409
    def project_state_display(self):
1410
        try:
1411
            project = self.project
1412
            return project.state_display()
1413
        except Project.DoesNotExist:
1414
            return self.state_display()
1415

    
1416
    def add_resource_policy(self, resource, uplimit):
1417
        """Raises ObjectDoesNotExist, IntegrityError"""
1418
        q = self.projectresourcegrant_set
1419
        resource = Resource.objects.get(name=resource)
1420
        q.create(resource=resource, member_capacity=uplimit)
1421

    
1422
    def members_count(self):
1423
        return self.project.approved_memberships.count()
1424

    
1425
    @property
1426
    def grants(self):
1427
        return self.projectresourcegrant_set.values('member_capacity',
1428
                                                    'resource__name')
1429

    
1430
    @property
1431
    def resource_policies(self):
1432
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1433

    
1434
    def set_resource_policies(self, policies):
1435
        for resource, uplimit in policies:
1436
            self.add_resource_policy(resource, uplimit)
1437

    
1438
    def pending_modifications_incl_me(self):
1439
        q = self.chained_applications()
1440
        q = q.filter(Q(state=self.PENDING))
1441
        return q
1442

    
1443
    def last_pending_incl_me(self):
1444
        try:
1445
            return self.pending_modifications_incl_me().order_by('-id')[0]
1446
        except IndexError:
1447
            return None
1448

    
1449
    def pending_modifications(self):
1450
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1451

    
1452
    def last_pending(self):
1453
        try:
1454
            return self.pending_modifications().order_by('-id')[0]
1455
        except IndexError:
1456
            return None
1457

    
1458
    def is_modification(self):
1459
        # if self.state != self.PENDING:
1460
        #     return False
1461
        parents = self.chained_applications().filter(id__lt=self.id)
1462
        parents = parents.filter(state__in=[self.APPROVED])
1463
        return parents.count() > 0
1464

    
1465
    def chained_applications(self):
1466
        return ProjectApplication.objects.filter(chain=self.chain)
1467

    
1468
    def is_latest(self):
1469
        return self.chained_applications().order_by('-id')[0] == self
1470

    
1471
    def has_pending_modifications(self):
1472
        return bool(self.last_pending())
1473

    
1474
    def denied_modifications(self):
1475
        q = self.chained_applications()
1476
        q = q.filter(Q(state=self.DENIED))
1477
        q = q.filter(~Q(id=self.id))
1478
        return q
1479

    
1480
    def last_denied(self):
1481
        try:
1482
            return self.denied_modifications().order_by('-id')[0]
1483
        except IndexError:
1484
            return None
1485

    
1486
    def has_denied_modifications(self):
1487
        return bool(self.last_denied())
1488

    
1489
    def is_applied(self):
1490
        try:
1491
            self.project
1492
            return True
1493
        except Project.DoesNotExist:
1494
            return False
1495

    
1496
    def can_cancel(self):
1497
        return self.state == self.PENDING
1498

    
1499
    def cancel(self):
1500
        if not self.can_cancel():
1501
            m = _("cannot cancel: application '%s' in state '%s'") % (
1502
                self.id, self.state)
1503
            raise AssertionError(m)
1504

    
1505
        self.state = self.CANCELLED
1506
        self.save()
1507

    
1508
    def can_dismiss(self):
1509
        return self.state == self.DENIED
1510

    
1511
    def dismiss(self):
1512
        if not self.can_dismiss():
1513
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1514
                self.id, self.state)
1515
            raise AssertionError(m)
1516

    
1517
        self.state = self.DISMISSED
1518
        self.save()
1519

    
1520
    def can_deny(self):
1521
        return self.state == self.PENDING
1522

    
1523
    def deny(self, reason):
1524
        if not self.can_deny():
1525
            m = _("cannot deny: application '%s' in state '%s'") % (
1526
                self.id, self.state)
1527
            raise AssertionError(m)
1528

    
1529
        self.state = self.DENIED
1530
        self.response_date = datetime.now()
1531
        self.response = reason
1532
        self.save()
1533

    
1534
    def can_approve(self):
1535
        return self.state == self.PENDING
1536

    
1537
    def approve(self, reason):
1538
        if not self.can_approve():
1539
            m = _("cannot approve: project '%s' in state '%s'") % (
1540
                self.name, self.state)
1541
            raise AssertionError(m)  # invalid argument
1542

    
1543
        now = datetime.now()
1544
        self.state = self.APPROVED
1545
        self.response_date = now
1546
        self.response = reason
1547
        self.save()
1548

    
1549
    @property
1550
    def member_join_policy_display(self):
1551
        policy = self.member_join_policy
1552
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1553

    
1554
    @property
1555
    def member_leave_policy_display(self):
1556
        policy = self.member_leave_policy
1557
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1558

    
1559

    
1560
class ProjectResourceGrant(models.Model):
1561

    
1562
    resource = models.ForeignKey(Resource)
1563
    project_application = models.ForeignKey(ProjectApplication,
1564
                                            null=True)
1565
    project_capacity = intDecimalField(null=True)
1566
    member_capacity = intDecimalField(default=0)
1567

    
1568
    objects = ExtendedManager()
1569

    
1570
    class Meta:
1571
        unique_together = ("resource", "project_application")
1572

    
1573
    def display_member_capacity(self):
1574
        if self.member_capacity:
1575
            if self.resource.unit:
1576
                return ProjectResourceGrant.display_filesize(
1577
                    self.member_capacity)
1578
            else:
1579
                if math.isinf(self.member_capacity):
1580
                    return 'Unlimited'
1581
                else:
1582
                    return self.member_capacity
1583
        else:
1584
            return 'Unlimited'
1585

    
1586
    def __str__(self):
1587
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1588
                                        self.display_member_capacity())
1589

    
1590
    @classmethod
1591
    def display_filesize(cls, value):
1592
        try:
1593
            value = float(value)
1594
        except:
1595
            return
1596
        else:
1597
            if math.isinf(value):
1598
                return 'Unlimited'
1599
            if value > 1:
1600
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1601
                                [0, 0, 0, 0, 0, 0])
1602
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1603
                quotient = float(value) / 1024**exponent
1604
                unit, value_decimals = unit_list[exponent]
1605
                format_string = '{0:.%sf} {1}' % (value_decimals)
1606
                return format_string.format(quotient, unit)
1607
            if value == 0:
1608
                return '0 bytes'
1609
            if value == 1:
1610
                return '1 byte'
1611
            else:
1612
                return '0'
1613

    
1614

    
1615
def _distinct(f, l):
1616
    d = {}
1617
    last = None
1618
    for x in l:
1619
        group = f(x)
1620
        if group == last:
1621
            continue
1622
        last = group
1623
        d[group] = x
1624
    return d
1625

    
1626

    
1627
def invert_dict(d):
1628
    return dict((v, k) for k, v in d.iteritems())
1629

    
1630

    
1631
class ProjectManager(ForUpdateManager):
1632

    
1633
    def all_with_pending(self, flt=None):
1634
        flt = Q() if flt is None else flt
1635
        projects = list(self.select_related(
1636
            'application', 'application__owner').filter(flt))
1637

    
1638
        objs = ProjectApplication.objects.select_related('owner')
1639
        apps = objs.filter(state=ProjectApplication.PENDING,
1640
                           chain__in=projects).order_by('chain', '-id')
1641
        app_d = _distinct(lambda app: app.chain_id, apps)
1642
        return [(project, app_d.get(project.pk)) for project in projects]
1643

    
1644
    def expired_projects(self):
1645
        model = self.model
1646
        q = ((model.o_state_q(model.O_ACTIVE) |
1647
              model.o_state_q(model.O_SUSPENDED)) &
1648
             Q(application__end_date__lt=datetime.now()))
1649
        return self.filter(q)
1650

    
1651

    
1652
class Project(models.Model):
1653

    
1654
    id = models.BigIntegerField(db_column='id', primary_key=True)
1655

    
1656
    application = models.OneToOneField(
1657
        ProjectApplication,
1658
        related_name='project')
1659
    last_approval_date = models.DateTimeField(null=True)
1660

    
1661
    members = models.ManyToManyField(
1662
        AstakosUser,
1663
        through='ProjectMembership')
1664

    
1665
    deactivation_reason = models.CharField(max_length=255, null=True)
1666
    deactivation_date = models.DateTimeField(null=True)
1667

    
1668
    creation_date = models.DateTimeField(auto_now_add=True)
1669
    name = models.CharField(
1670
        max_length=80,
1671
        null=True,
1672
        db_index=True,
1673
        unique=True)
1674

    
1675
    NORMAL = 1
1676
    SUSPENDED = 10
1677
    TERMINATED = 100
1678

    
1679
    state = models.IntegerField(default=NORMAL,
1680
                                db_index=True)
1681

    
1682
    objects = ProjectManager()
1683

    
1684
    def __str__(self):
1685
        return uenc(_("<project %s '%s'>") %
1686
                    (self.id, udec(self.application.name)))
1687

    
1688
    __repr__ = __str__
1689

    
1690
    def __unicode__(self):
1691
        return _("<project %s '%s'>") % (self.id, self.application.name)
1692

    
1693
    O_PENDING = 0
1694
    O_ACTIVE = 1
1695
    O_DENIED = 3
1696
    O_DISMISSED = 4
1697
    O_CANCELLED = 5
1698
    O_SUSPENDED = 10
1699
    O_TERMINATED = 100
1700

    
1701
    O_STATE_DISPLAY = {
1702
        O_PENDING:    _("Pending"),
1703
        O_ACTIVE:     _("Active"),
1704
        O_DENIED:     _("Denied"),
1705
        O_DISMISSED:  _("Dismissed"),
1706
        O_CANCELLED:  _("Cancelled"),
1707
        O_SUSPENDED:  _("Suspended"),
1708
        O_TERMINATED: _("Terminated"),
1709
    }
1710

    
1711
    OVERALL_STATE = {
1712
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1713
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1714
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1715
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1716
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1717
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1718
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1719
    }
1720

    
1721
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1722

    
1723
    @classmethod
1724
    def o_state_q(cls, o_state):
1725
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1726
        return Q(state=p_state, application__state=a_state)
1727

    
1728
    INITIALIZED_STATES = [O_ACTIVE,
1729
                          O_SUSPENDED,
1730
                          O_TERMINATED,
1731
                          ]
1732

    
1733
    RELEVANT_STATES = [O_PENDING,
1734
                       O_DENIED,
1735
                       O_ACTIVE,
1736
                       O_SUSPENDED,
1737
                       O_TERMINATED,
1738
                       ]
1739

    
1740
    SKIP_STATES = [O_DISMISSED,
1741
                   O_CANCELLED,
1742
                   O_TERMINATED,
1743
                   ]
1744

    
1745
    @classmethod
1746
    def _overall_state(cls, project_state, app_state):
1747
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1748

    
1749
    def overall_state(self):
1750
        return self._overall_state(self.state, self.application.state)
1751

    
1752
    def last_pending_application(self):
1753
        apps = self.chained_apps.filter(
1754
            state=ProjectApplication.PENDING).order_by('-id')
1755
        if apps:
1756
            return apps[0]
1757
        return None
1758

    
1759
    def state_display(self):
1760
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1761

    
1762
    def expiration_info(self):
1763
        return (str(self.id), self.name, self.state_display(),
1764
                str(self.application.end_date))
1765

    
1766
    def is_deactivated(self, reason=None):
1767
        if reason is not None:
1768
            return self.state == reason
1769

    
1770
        return self.state != self.NORMAL
1771

    
1772
    def is_active(self):
1773
        return self.overall_state() == self.O_ACTIVE
1774

    
1775
    def is_initialized(self):
1776
        return self.overall_state() in self.INITIALIZED_STATES
1777

    
1778
    ### Deactivation calls
1779

    
1780
    def terminate(self):
1781
        self.deactivation_reason = 'TERMINATED'
1782
        self.deactivation_date = datetime.now()
1783
        self.state = self.TERMINATED
1784
        self.name = None
1785
        self.save()
1786

    
1787
    def suspend(self):
1788
        self.deactivation_reason = 'SUSPENDED'
1789
        self.deactivation_date = datetime.now()
1790
        self.state = self.SUSPENDED
1791
        self.save()
1792

    
1793
    def resume(self):
1794
        self.deactivation_reason = None
1795
        self.deactivation_date = None
1796
        self.state = self.NORMAL
1797
        self.save()
1798

    
1799
    ### Logical checks
1800

    
1801
    def is_inconsistent(self):
1802
        now = datetime.now()
1803
        dates = [self.creation_date,
1804
                 self.last_approval_date,
1805
                 self.deactivation_date]
1806
        return any([date > now for date in dates])
1807

    
1808
    @property
1809
    def is_alive(self):
1810
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1811

    
1812
    @property
1813
    def is_terminated(self):
1814
        return self.is_deactivated(self.TERMINATED)
1815

    
1816
    @property
1817
    def is_suspended(self):
1818
        return self.is_deactivated(self.SUSPENDED)
1819

    
1820
    def violates_members_limit(self, adding=0):
1821
        application = self.application
1822
        limit = application.limit_on_members_number
1823
        if limit is None:
1824
            return False
1825
        return (len(self.approved_members) + adding > limit)
1826

    
1827
    ### Other
1828

    
1829
    def count_pending_memberships(self):
1830
        return self.projectmembership_set.requested().count()
1831

    
1832
    def members_count(self):
1833
        return self.approved_memberships.count()
1834

    
1835
    @property
1836
    def approved_memberships(self):
1837
        query = ProjectMembership.Q_ACCEPTED_STATES
1838
        return self.projectmembership_set.filter(query)
1839

    
1840
    @property
1841
    def approved_members(self):
1842
        return [m.person for m in self.approved_memberships]
1843

    
1844

    
1845
class ProjectMembershipManager(ForUpdateManager):
1846

    
1847
    def any_accepted(self):
1848
        q = self.model.Q_ACCEPTED_STATES
1849
        return self.filter(q)
1850

    
1851
    def actually_accepted(self):
1852
        q = self.model.Q_ACTUALLY_ACCEPTED
1853
        return self.filter(q)
1854

    
1855
    def requested(self):
1856
        return self.filter(state=ProjectMembership.REQUESTED)
1857

    
1858
    def suspended(self):
1859
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1860

    
1861

    
1862
class ProjectMembership(models.Model):
1863

    
1864
    person = models.ForeignKey(AstakosUser)
1865
    request_date = models.DateTimeField(auto_now_add=True)
1866
    project = models.ForeignKey(Project)
1867

    
1868
    REQUESTED = 0
1869
    ACCEPTED = 1
1870
    LEAVE_REQUESTED = 5
1871
    # User deactivation
1872
    USER_SUSPENDED = 10
1873

    
1874
    REMOVED = 200
1875

    
1876
    ASSOCIATED_STATES = set([REQUESTED,
1877
                             ACCEPTED,
1878
                             LEAVE_REQUESTED,
1879
                             USER_SUSPENDED,
1880
                             ])
1881

    
1882
    ACCEPTED_STATES = set([ACCEPTED,
1883
                           LEAVE_REQUESTED,
1884
                           USER_SUSPENDED,
1885
                           ])
1886

    
1887
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1888

    
1889
    state = models.IntegerField(default=REQUESTED,
1890
                                db_index=True)
1891
    acceptance_date = models.DateTimeField(null=True, db_index=True)
1892
    leave_request_date = models.DateTimeField(null=True)
1893

    
1894
    objects = ProjectMembershipManager()
1895

    
1896
    # Compiled queries
1897
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1898
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1899

    
1900
    MEMBERSHIP_STATE_DISPLAY = {
1901
        REQUESTED:       _('Requested'),
1902
        ACCEPTED:        _('Accepted'),
1903
        LEAVE_REQUESTED: _('Leave Requested'),
1904
        USER_SUSPENDED:  _('Suspended'),
1905
        REMOVED:         _('Pending removal'),
1906
    }
1907

    
1908
    USER_FRIENDLY_STATE_DISPLAY = {
1909
        REQUESTED:       _('Join requested'),
1910
        ACCEPTED:        _('Accepted member'),
1911
        LEAVE_REQUESTED: _('Requested to leave'),
1912
        USER_SUSPENDED:  _('Suspended member'),
1913
        REMOVED:         _('Pending removal'),
1914
    }
1915

    
1916
    def state_display(self):
1917
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1918

    
1919
    def user_friendly_state_display(self):
1920
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1921

    
1922
    class Meta:
1923
        unique_together = ("person", "project")
1924
        #index_together = [["project", "state"]]
1925

    
1926
    def __str__(self):
1927
        return uenc(_("<'%s' membership in '%s'>") %
1928
                    (self.person.username, self.project))
1929

    
1930
    __repr__ = __str__
1931

    
1932
    def __init__(self, *args, **kwargs):
1933
        self.state = self.REQUESTED
1934
        super(ProjectMembership, self).__init__(*args, **kwargs)
1935

    
1936
    def _set_history_item(self, reason, date=None):
1937
        if isinstance(reason, basestring):
1938
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1939

    
1940
        history_item = ProjectMembershipHistory(
1941
            serial=self.id,
1942
            person=self.person_id,
1943
            project=self.project_id,
1944
            date=date or datetime.now(),
1945
            reason=reason)
1946
        history_item.save()
1947
        serial = history_item.id
1948

    
1949
    def can_accept(self):
1950
        return self.state == self.REQUESTED
1951

    
1952
    def accept(self):
1953
        if not self.can_accept():
1954
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
1955
            raise AssertionError(m)
1956

    
1957
        now = datetime.now()
1958
        self.acceptance_date = now
1959
        self._set_history_item(reason='ACCEPT', date=now)
1960
        self.state = self.ACCEPTED
1961
        self.save()
1962

    
1963
    def can_leave(self):
1964
        return self.state in self.ACCEPTED_STATES
1965

    
1966
    def leave_request(self):
1967
        if not self.can_leave():
1968
            m = _("%s: attempt to request to leave in state '%s'") % (
1969
                self, self.state)
1970
            raise AssertionError(m)
1971

    
1972
        self.leave_request_date = datetime.now()
1973
        self.state = self.LEAVE_REQUESTED
1974
        self.save()
1975

    
1976
    def can_deny_leave(self):
1977
        return self.state == self.LEAVE_REQUESTED
1978

    
1979
    def leave_request_deny(self):
1980
        if not self.can_deny_leave():
1981
            m = _("%s: attempt to deny leave request in state '%s'") % (
1982
                self, self.state)
1983
            raise AssertionError(m)
1984

    
1985
        self.leave_request_date = None
1986
        self.state = self.ACCEPTED
1987
        self.save()
1988

    
1989
    def can_cancel_leave(self):
1990
        return self.state == self.LEAVE_REQUESTED
1991

    
1992
    def leave_request_cancel(self):
1993
        if not self.can_cancel_leave():
1994
            m = _("%s: attempt to cancel leave request in state '%s'") % (
1995
                self, self.state)
1996
            raise AssertionError(m)
1997

    
1998
        self.leave_request_date = None
1999
        self.state = self.ACCEPTED
2000
        self.save()
2001

    
2002
    def can_remove(self):
2003
        return self.state in self.ACCEPTED_STATES
2004

    
2005
    def remove(self):
2006
        if not self.can_remove():
2007
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2008
            raise AssertionError(m)
2009

    
2010
        self._set_history_item(reason='REMOVE')
2011
        self.delete()
2012

    
2013
    def can_reject(self):
2014
        return self.state == self.REQUESTED
2015

    
2016
    def reject(self):
2017
        if not self.can_reject():
2018
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2019
            raise AssertionError(m)
2020

    
2021
        # rejected requests don't need sync,
2022
        # because they were never effected
2023
        self._set_history_item(reason='REJECT')
2024
        self.delete()
2025

    
2026
    def can_cancel(self):
2027
        return self.state == self.REQUESTED
2028

    
2029
    def cancel(self):
2030
        if not self.can_cancel():
2031
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2032
            raise AssertionError(m)
2033

    
2034
        # rejected requests don't need sync,
2035
        # because they were never effected
2036
        self._set_history_item(reason='CANCEL')
2037
        self.delete()
2038

    
2039

    
2040
class Serial(models.Model):
2041
    serial = models.AutoField(primary_key=True)
2042

    
2043

    
2044
class ProjectMembershipHistory(models.Model):
2045
    reasons_list = ['ACCEPT', 'REJECT', 'REMOVE']
2046
    reasons = dict((k, v) for v, k in enumerate(reasons_list))
2047

    
2048
    person = models.BigIntegerField()
2049
    project = models.BigIntegerField()
2050
    date = models.DateTimeField(auto_now_add=True)
2051
    reason = models.IntegerField()
2052
    serial = models.BigIntegerField()
2053

    
2054

    
2055
### SIGNALS ###
2056
################
2057

    
2058
def create_astakos_user(u):
2059
    try:
2060
        AstakosUser.objects.get(user_ptr=u.pk)
2061
    except AstakosUser.DoesNotExist:
2062
        extended_user = AstakosUser(user_ptr_id=u.pk)
2063
        extended_user.__dict__.update(u.__dict__)
2064
        extended_user.save()
2065
        if not extended_user.has_auth_provider('local'):
2066
            extended_user.add_auth_provider('local')
2067
    except BaseException, e:
2068
        logger.exception(e)
2069

    
2070

    
2071
def fix_superusers():
2072
    # Associate superusers with AstakosUser
2073
    admins = User.objects.filter(is_superuser=True)
2074
    for u in admins:
2075
        create_astakos_user(u)
2076

    
2077

    
2078
def user_post_save(sender, instance, created, **kwargs):
2079
    if not created:
2080
        return
2081
    create_astakos_user(instance)
2082
post_save.connect(user_post_save, sender=User)
2083

    
2084

    
2085
def astakosuser_post_save(sender, instance, created, **kwargs):
2086
    pass
2087

    
2088
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2089

    
2090

    
2091
def resource_post_save(sender, instance, created, **kwargs):
2092
    pass
2093

    
2094
post_save.connect(resource_post_save, sender=Resource)
2095

    
2096

    
2097
def renew_token(sender, instance, **kwargs):
2098
    if not instance.auth_token:
2099
        instance.renew_token()
2100
pre_save.connect(renew_token, sender=AstakosUser)
2101
pre_save.connect(renew_token, sender=Component)