Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (91.1 kB)

1
# Copyright 2011-2012 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

    
40
from time import asctime
41
from datetime import datetime, timedelta
42
from base64 import b64encode
43
from urlparse import urlparse
44
from urllib import quote
45
from random import randint
46
from collections import defaultdict, namedtuple
47

    
48
from django.db import models, IntegrityError, transaction
49
from django.contrib.auth.models import User, UserManager, Group, Permission
50
from django.utils.translation import ugettext as _
51
from django.core.exceptions import ValidationError
52
from django.db.models.signals import (
53
    pre_save, post_save, post_syncdb, post_delete)
54
from django.contrib.contenttypes.models import ContentType
55

    
56
from django.dispatch import Signal
57
from django.db.models import Q
58
from django.core.urlresolvers import reverse
59
from django.utils.http import int_to_base36
60
from django.contrib.auth.tokens import default_token_generator
61
from django.conf import settings
62
from django.utils.importlib import import_module
63
from django.utils.safestring import mark_safe
64
from django.core.validators import email_re
65
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
66

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
70
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA,
71
    PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES, PROJECT_ADMINS)
72
from astakos.im import settings as astakos_settings
73
from astakos.im.endpoints.qh import (
74
    register_users, send_quotas, qh_check_users, qh_get_quota_limits,
75
    register_services, register_resources, qh_add_quota, QuotaLimits,
76
    qh_query_serials, qh_ack_serials,
77
    QuotaValues, add_quota_values)
78
from astakos.im import auth_providers
79

    
80
import astakos.im.messages as astakos_messages
81
from astakos.im.lock import with_lock
82
from .managers import ForUpdateManager
83

    
84
from synnefo.lib.quotaholder.api import QH_PRACTICALLY_INFINITE
85
from synnefo.lib.db.intdecimalfield import intDecimalField
86
from synnefo.util.text import uenc, udec
87

    
88
logger = logging.getLogger(__name__)
89

    
90
DEFAULT_CONTENT_TYPE = None
91
_content_type = None
92

    
93
def get_content_type():
94
    global _content_type
95
    if _content_type is not None:
96
        return _content_type
97

    
98
    try:
99
        content_type = ContentType.objects.get(app_label='im', model='astakosuser')
100
    except:
101
        content_type = DEFAULT_CONTENT_TYPE
102
    _content_type = content_type
103
    return content_type
104

    
105
RESOURCE_SEPARATOR = '.'
106

    
107
inf = float('inf')
108

    
109
class Service(models.Model):
110
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
111
    url = models.FilePathField()
112
    icon = models.FilePathField(blank=True)
113
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
114
                                  null=True, blank=True)
115
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
116
    auth_token_expires = models.DateTimeField(
117
        _('Token expiration date'), null=True)
118
    order = models.PositiveIntegerField(default=0)
119

    
120
    class Meta:
121
        ordering = ('order', )
122

    
123
    def renew_token(self, expiration_date=None):
124
        md5 = hashlib.md5()
125
        md5.update(self.name.encode('ascii', 'ignore'))
126
        md5.update(self.url.encode('ascii', 'ignore'))
127
        md5.update(asctime())
128

    
129
        self.auth_token = b64encode(md5.digest())
130
        self.auth_token_created = datetime.now()
131
        if expiration_date:
132
            self.auth_token_expires = expiration_date
133
        else:
134
            self.auth_token_expires = None
135

    
136
    def __str__(self):
137
        return self.name
138

    
139
    @property
140
    def resources(self):
141
        return self.resource_set.all()
142

    
143
    @resources.setter
144
    def resources(self, resources):
145
        for s in resources:
146
            self.resource_set.create(**s)
147

    
148

    
149
class ResourceMetadata(models.Model):
150
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
151
    value = models.CharField(_('Value'), max_length=255)
152

    
153
_presentation_data = {}
154
def get_presentation(resource):
155
    global _presentation_data
156
    presentation = _presentation_data.get(resource, {})
157
    if not presentation:
158
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
159
        presentation = resource_presentation.get(resource, {})
160
        _presentation_data[resource] = presentation
161
    return presentation
162

    
163
class Resource(models.Model):
164
    name = models.CharField(_('Name'), max_length=255)
165
    meta = models.ManyToManyField(ResourceMetadata)
166
    service = models.ForeignKey(Service)
167
    desc = models.TextField(_('Description'), null=True)
168
    unit = models.CharField(_('Name'), null=True, max_length=255)
169
    group = models.CharField(_('Group'), null=True, max_length=255)
170
    uplimit = intDecimalField(default=0)
171

    
172
    class Meta:
173
        unique_together = ("service", "name")
174

    
175
    def __str__(self):
176
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
177

    
178
    def full_name(self):
179
        return str(self)
180

    
181
    @property
182
    def help_text(self):
183
        return get_presentation(str(self)).get('help_text', '')
184

    
185
    @property
186
    def help_text_input_each(self):
187
        return get_presentation(str(self)).get('help_text_input_each', '')
188

    
189
    @property
190
    def is_abbreviation(self):
191
        return get_presentation(str(self)).get('is_abbreviation', False)
192

    
193
    @property
194
    def report_desc(self):
195
        return get_presentation(str(self)).get('report_desc', '')
196

    
197
    @property
198
    def placeholder(self):
199
        return get_presentation(str(self)).get('placeholder', '')
200

    
201
    @property
202
    def verbose_name(self):
203
        return get_presentation(str(self)).get('verbose_name', '')
204

    
205
    @property
206
    def display_name(self):
207
        name = self.verbose_name
208
        if self.is_abbreviation:
209
            name = name.upper()
210
        return name
211

    
212
    @property
213
    def pluralized_display_name(self):
214
        if not self.unit:
215
            return '%ss' % self.display_name
216
        return self.display_name
217

    
218
def load_service_resources():
219
    ss = []
220
    rs = []
221
    counter = 0
222
    for service_name, data in sorted(SERVICES.iteritems()):
223
        url = data.get('url')
224
        order = data.get('order', counter)
225
        counter = order + 1
226
        resources = data.get('resources') or ()
227
        service, created = Service.objects.get_or_create(
228
            name=service_name,
229
            defaults={'url': url, 'order': order}
230
        )
231
        if not created:
232
            service.url = url
233
            service.save()
234

    
235
        ss.append(service)
236

    
237
        for resource in resources:
238
            try:
239
                resource_name = resource.pop('name', '')
240
                r, created = Resource.objects.get_or_create(
241
                        service=service, name=resource_name,
242
                        defaults=resource)
243
                if not created:
244
                    r.desc = resource['desc']
245
                    r.unit = resource.get('unit', None)
246
                    r.group = resource['group']
247
                    r.uplimit = resource['uplimit']
248
                    r.save()
249

    
250
                rs.append(r)
251

    
252
            except Exception, e:
253
                print "Cannot create resource ", resource_name
254
                import traceback; traceback.print_exc()
255
                continue
256

    
257
    register_services(ss)
258
    register_resources(rs)
259

    
260
def _quota_values(capacity):
261
    return QuotaValues(
262
        quantity = 0,
263
        capacity = capacity,
264
        import_limit = QH_PRACTICALLY_INFINITE,
265
        export_limit = QH_PRACTICALLY_INFINITE)
266

    
267
def get_default_quota():
268
    _DEFAULT_QUOTA = {}
269
    resources = Resource.objects.all()
270
    for resource in resources:
271
        capacity = resource.uplimit
272
        limits = _quota_values(capacity)
273
        _DEFAULT_QUOTA[resource.full_name()] = limits
274

    
275
    return _DEFAULT_QUOTA
276

    
277
def get_resource_names():
278
    _RESOURCE_NAMES = []
279
    resources = Resource.objects.all()
280
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
281
    return _RESOURCE_NAMES
282

    
283

    
284
class AstakosUserManager(UserManager):
285

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

    
295
    def get_by_email(self, email):
296
        return self.get(email=email)
297

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

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

    
310
    def verified_user_exists(self, email_or_username):
311
        return self.user_exists(email_or_username, email_verified=True)
312

    
313
    def verified(self):
314
        return self.filter(email_verified=True)
315

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

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

    
338

    
339

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

    
347
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
348
    #                    AstakosUserProvider model.
349
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
350
                                null=True)
351
    # ex. screen_name for twitter, eppn for shibboleth
352
    third_party_identifier = models.CharField(_('Third-party identifier'),
353
                                              max_length=255, null=True,
354
                                              blank=True)
355

    
356

    
357
    #for invitations
358
    user_level = DEFAULT_USER_LEVEL
359
    level = models.IntegerField(_('Inviter level'), default=user_level)
360
    invitations = models.IntegerField(
361
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
362

    
363
    auth_token = models.CharField(_('Authentication Token'), 
364
                                  max_length=32,
365
                                  null=True, 
366
                                  blank=True, 
367
                                  help_text = _( 'test' ))
368
    auth_token_created = models.DateTimeField(_('Token creation date'), 
369
                                              null=True)
370
    auth_token_expires = models.DateTimeField(
371
        _('Token expiration date'), null=True)
372

    
373
    updated = models.DateTimeField(_('Update date'))
374
    is_verified = models.BooleanField(_('Is verified?'), default=False)
375

    
376
    email_verified = models.BooleanField(_('Email verified?'), default=False)
377

    
378
    has_credits = models.BooleanField(_('Has credits?'), default=False)
379
    has_signed_terms = models.BooleanField(
380
        _('I agree with the terms'), default=False)
381
    date_signed_terms = models.DateTimeField(
382
        _('Signed terms date'), null=True, blank=True)
383

    
384
    activation_sent = models.DateTimeField(
385
        _('Activation sent data'), null=True, blank=True)
386

    
387
    policy = models.ManyToManyField(
388
        Resource, null=True, through='AstakosUserQuota')
389

    
390
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
391

    
392
    __has_signed_terms = False
393
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
394
                                           default=False, db_index=True)
395

    
396
    objects = AstakosUserManager()
397

    
398
    def __init__(self, *args, **kwargs):
399
        super(AstakosUser, self).__init__(*args, **kwargs)
400
        self.__has_signed_terms = self.has_signed_terms
401
        if not self.id:
402
            self.is_active = False
403

    
404
    @property
405
    def realname(self):
406
        return '%s %s' % (self.first_name, self.last_name)
407

    
408
    @realname.setter
409
    def realname(self, value):
410
        parts = value.split(' ')
411
        if len(parts) == 2:
412
            self.first_name = parts[0]
413
            self.last_name = parts[1]
414
        else:
415
            self.last_name = parts[0]
416

    
417
    def add_permission(self, pname):
418
        if self.has_perm(pname):
419
            return
420
        p, created = Permission.objects.get_or_create(
421
                                    codename=pname,
422
                                    name=pname.capitalize(),
423
                                    content_type=get_content_type())
424
        self.user_permissions.add(p)
425

    
426
    def remove_permission(self, pname):
427
        if self.has_perm(pname):
428
            return
429
        p = Permission.objects.get(codename=pname,
430
                                   content_type=get_content_type())
431
        self.user_permissions.remove(p)
432

    
433
    def is_project_admin(self, application_id=None):
434
        return self.uuid in PROJECT_ADMINS
435

    
436
    @property
437
    def invitation(self):
438
        try:
439
            return Invitation.objects.get(username=self.email)
440
        except Invitation.DoesNotExist:
441
            return None
442

    
443
    def initial_quotas(self):
444
        quotas = dict(get_default_quota())
445
        for user_quota in self.policies:
446
            resource = user_quota.resource.full_name()
447
            quotas[resource] = user_quota.quota_values()
448
        return quotas
449

    
450
    def all_quotas(self):
451
        quotas = self.initial_quotas()
452

    
453
        objects = self.projectmembership_set.select_related()
454
        memberships = objects.filter(is_active=True)
455
        for membership in memberships:
456
            application = membership.application
457
            if application is None:
458
                m = _("missing application for active membership %s"
459
                      % (membership,))
460
                raise AssertionError(m)
461

    
462
            grants = application.projectresourcegrant_set.all()
463
            for grant in grants:
464
                resource = grant.resource.full_name()
465
                prev = quotas.get(resource, 0)
466
                new = add_quota_values(prev, grant.member_quota_values())
467
                quotas[resource] = new
468
        return quotas
469

    
470
    @property
471
    def policies(self):
472
        return self.astakosuserquota_set.select_related().all()
473

    
474
    @policies.setter
475
    def policies(self, policies):
476
        for p in policies:
477
            p.setdefault('resource', '')
478
            p.setdefault('capacity', 0)
479
            p.setdefault('quantity', 0)
480
            p.setdefault('import_limit', 0)
481
            p.setdefault('export_limit', 0)
482
            p.setdefault('update', True)
483
            self.add_resource_policy(**p)
484

    
485
    def add_resource_policy(
486
            self, resource, capacity, quantity, import_limit,
487
            export_limit, update=True):
488
        """Raises ObjectDoesNotExist, IntegrityError"""
489
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
490
        resource = Resource.objects.get(service__name=s, name=r)
491
        if update:
492
            AstakosUserQuota.objects.update_or_create(
493
                user=self, resource=resource, defaults={
494
                    'capacity':capacity,
495
                    'quantity': quantity,
496
                    'import_limit':import_limit,
497
                    'export_limit':export_limit})
498
        else:
499
            q = self.astakosuserquota_set
500
            q.create(
501
                resource=resource, capacity=capacity, quanity=quantity,
502
                import_limit=import_limit, export_limit=export_limit)
503

    
504
    def remove_resource_policy(self, service, resource):
505
        """Raises ObjectDoesNotExist, IntegrityError"""
506
        resource = Resource.objects.get(service__name=service, name=resource)
507
        q = self.policies.get(resource=resource).delete()
508

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

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

    
524
        # update date_signed_terms if necessary
525
        if self.__has_signed_terms != self.has_signed_terms:
526
            self.date_signed_terms = datetime.now()
527

    
528
        self.update_uuid()
529

    
530
        if self.username != self.email.lower():
531
            # set username
532
            self.username = self.email.lower()
533

    
534
        super(AstakosUser, self).save(**kwargs)
535

    
536
    def renew_token(self, flush_sessions=False, current_key=None):
537
        md5 = hashlib.md5()
538
        md5.update(settings.SECRET_KEY)
539
        md5.update(self.username)
540
        md5.update(self.realname.encode('ascii', 'ignore'))
541
        md5.update(asctime())
542

    
543
        self.auth_token = b64encode(md5.digest())
544
        self.auth_token_created = datetime.now()
545
        self.auth_token_expires = self.auth_token_created + \
546
                                  timedelta(hours=AUTH_TOKEN_DURATION)
547
        if flush_sessions:
548
            self.flush_sessions(current_key)
549
        msg = 'Token renewed for %s' % self.email
550
        logger.log(LOGGING_LEVEL, msg)
551

    
552
    def flush_sessions(self, current_key=None):
553
        q = self.sessions
554
        if current_key:
555
            q = q.exclude(session_key=current_key)
556

    
557
        keys = q.values_list('session_key', flat=True)
558
        if keys:
559
            msg = 'Flushing sessions: %s' % ','.join(keys)
560
            logger.log(LOGGING_LEVEL, msg, [])
561
        engine = import_module(settings.SESSION_ENGINE)
562
        for k in keys:
563
            s = engine.SessionStore(k)
564
            s.flush()
565

    
566
    def __unicode__(self):
567
        return '%s (%s)' % (self.realname, self.email)
568

    
569
    def conflicting_email(self):
570
        q = AstakosUser.objects.exclude(username=self.username)
571
        q = q.filter(email__iexact=self.email)
572
        if q.count() != 0:
573
            return True
574
        return False
575

    
576
    def email_change_is_pending(self):
577
        return self.emailchanges.count() > 0
578

    
579
    @property
580
    def signed_terms(self):
581
        term = get_latest_terms()
582
        if not term:
583
            return True
584
        if not self.has_signed_terms:
585
            return False
586
        if not self.date_signed_terms:
587
            return False
588
        if self.date_signed_terms < term.date:
589
            self.has_signed_terms = False
590
            self.date_signed_terms = None
591
            self.save()
592
            return False
593
        return True
594

    
595
    def set_invitations_level(self):
596
        """
597
        Update user invitation level
598
        """
599
        level = self.invitation.inviter.level + 1
600
        self.level = level
601
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
602

    
603
    def can_login_with_auth_provider(self, provider):
604
        if not self.has_auth_provider(provider):
605
            return False
606
        else:
607
            return auth_providers.get_provider(provider).is_available_for_login()
608

    
609
    def can_add_auth_provider(self, provider, include_unverified=False, **kwargs):
610
        provider_settings = auth_providers.get_provider(provider)
611

    
612
        if not provider_settings.is_available_for_add():
613
            return False
614

    
615
        if self.has_auth_provider(provider) and \
616
           provider_settings.one_per_user:
617
            return False
618

    
619
        if 'provider_info' in kwargs:
620
            kwargs.pop('provider_info')
621

    
622
        if 'identifier' in kwargs:
623
            try:
624
                # provider with specified params already exist
625
                if not include_unverified:
626
                    kwargs['user__email_verified'] = True
627
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
628
                                                                   **kwargs)
629
            except AstakosUser.DoesNotExist:
630
                return True
631
            else:
632
                return False
633

    
634
        return True
635

    
636
    def can_remove_auth_provider(self, module):
637
        provider = auth_providers.get_provider(module)
638
        existing = self.get_active_auth_providers()
639
        existing_for_provider = self.get_active_auth_providers(module=module)
640

    
641
        if len(existing) <= 1:
642
            return False
643

    
644
        if len(existing_for_provider) == 1 and provider.is_required():
645
            return False
646

    
647
        return provider.is_available_for_remove()
648

    
649
    def can_change_password(self):
650
        return self.has_auth_provider('local', auth_backend='astakos')
651

    
652
    def can_change_email(self):
653
        non_astakos_local = self.get_auth_providers().filter(module='local')
654
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
655
        return non_astakos_local.count() == 0
656

    
657
    def has_required_auth_providers(self):
658
        required = auth_providers.REQUIRED_PROVIDERS
659
        for provider in required:
660
            if not self.has_auth_provider(provider):
661
                return False
662
        return True
663

    
664
    def has_auth_provider(self, provider, **kwargs):
665
        return bool(self.get_auth_providers().filter(module=provider,
666
                                               **kwargs).count())
667

    
668
    def add_auth_provider(self, provider, **kwargs):
669
        info_data = ''
670
        if 'provider_info' in kwargs:
671
            info_data = kwargs.pop('provider_info')
672
            if isinstance(info_data, dict):
673
                info_data = json.dumps(info_data)
674

    
675
        if self.can_add_auth_provider(provider, **kwargs):
676
            if 'identifier' in kwargs:
677
                # clean up third party pending for activation users of the same
678
                # identifier
679
                AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
680
                                                                **kwargs)
681
            self.auth_providers.create(module=provider, active=True,
682
                                       info_data=info_data,
683
                                       **kwargs)
684
        else:
685
            raise Exception('Cannot add provider')
686

    
687
    def add_pending_auth_provider(self, pending):
688
        """
689
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
690
        the current user.
691
        """
692
        if not isinstance(pending, PendingThirdPartyUser):
693
            pending = PendingThirdPartyUser.objects.get(token=pending)
694

    
695
        provider = self.add_auth_provider(pending.provider,
696
                               identifier=pending.third_party_identifier,
697
                                affiliation=pending.affiliation,
698
                                          provider_info=pending.info)
699

    
700
        if email_re.match(pending.email or '') and pending.email != self.email:
701
            self.additionalmail_set.get_or_create(email=pending.email)
702

    
703
        pending.delete()
704
        return provider
705

    
706
    def remove_auth_provider(self, provider, **kwargs):
707
        self.get_auth_providers().get(module=provider, **kwargs).delete()
708

    
709
    # user urls
710
    def get_resend_activation_url(self):
711
        return reverse('send_activation', kwargs={'user_id': self.pk})
712

    
713
    def get_provider_remove_url(self, module, **kwargs):
714
        return reverse('remove_auth_provider', kwargs={
715
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
716

    
717
    def get_activation_url(self, nxt=False):
718
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
719
                                 quote(self.auth_token))
720
        if nxt:
721
            url += "&next=%s" % quote(nxt)
722
        return url
723

    
724
    def get_password_reset_url(self, token_generator=default_token_generator):
725
        return reverse('django.contrib.auth.views.password_reset_confirm',
726
                          kwargs={'uidb36':int_to_base36(self.id),
727
                                  'token':token_generator.make_token(self)})
728

    
729
    def get_primary_auth_provider(self):
730
        return self.get_auth_providers().filter()[0]
731

    
732
    def get_auth_providers(self):
733
        return self.auth_providers
734

    
735
    def get_available_auth_providers(self):
736
        """
737
        Returns a list of providers available for user to connect to.
738
        """
739
        providers = []
740
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
741
            if self.can_add_auth_provider(module):
742
                providers.append(provider_settings(self))
743

    
744
        modules = astakos_settings.IM_MODULES
745
        def key(p):
746
            if not p.module in modules:
747
                return 100
748
            return modules.index(p.module)
749
        providers = sorted(providers, key=key)
750
        return providers
751

    
752
    def get_active_auth_providers(self, **filters):
753
        providers = []
754
        for provider in self.get_auth_providers().active(**filters):
755
            if auth_providers.get_provider(provider.module).is_available_for_login():
756
                providers.append(provider)
757

    
758
        modules = astakos_settings.IM_MODULES
759
        def key(p):
760
            if not p.module in modules:
761
                return 100
762
            return modules.index(p.module)
763
        providers = sorted(providers, key=key)
764
        return providers
765

    
766
    @property
767
    def auth_providers_display(self):
768
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
769

    
770
    def get_inactive_message(self):
771
        msg_extra = ''
772
        message = ''
773
        if self.activation_sent:
774
            if self.email_verified:
775
                message = _(astakos_messages.ACCOUNT_INACTIVE)
776
            else:
777
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
778
                if astakos_settings.MODERATION_ENABLED:
779
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
780
                else:
781
                    url = self.get_resend_activation_url()
782
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
783
                                u' ' + \
784
                                _('<a href="%s">%s?</a>') % (url,
785
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
786
        else:
787
            if astakos_settings.MODERATION_ENABLED:
788
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
789
            else:
790
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
791
                url = self.get_resend_activation_url()
792
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
793
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
794

    
795
        return mark_safe(message + u' '+ msg_extra)
796

    
797
    def owns_application(self, application):
798
        return application.owner == self
799

    
800
    def owns_project(self, project):
801
        return project.application.owner == self
802

    
803
    def is_associated(self, project):
804
        try:
805
            m = ProjectMembership.objects.get(person=self, project=project)
806
            return m.state in ProjectMembership.ASSOCIATED_STATES
807
        except ProjectMembership.DoesNotExist:
808
            return False
809

    
810
    def get_membership(self, project):
811
        try:
812
            return ProjectMembership.objects.get(
813
                project=project,
814
                person=self)
815
        except ProjectMembership.DoesNotExist:
816
            return None
817

    
818
    def membership_display(self, project):
819
        m = self.get_membership(project)
820
        if m is None:
821
            return _('Not a member')
822
        else:
823
            return m.user_friendly_state_display()
824

    
825
    def non_owner_can_view(self, maybe_project):
826
        if self.is_project_admin():
827
            return True
828
        if maybe_project is None:
829
            return False
830
        project = maybe_project
831
        if self.is_associated(project):
832
            return True
833
        if project.is_deactivated():
834
            return False
835
        return True
836

    
837

    
838
class AstakosUserAuthProviderManager(models.Manager):
839

    
840
    def active(self, **filters):
841
        return self.filter(active=True, **filters)
842

    
843
    def remove_unverified_providers(self, provider, **filters):
844
        try:
845
            existing = self.filter(module=provider, user__email_verified=False, **filters)
846
            for p in existing:
847
                p.user.delete()
848
        except:
849
            pass
850

    
851

    
852

    
853
class AstakosUserAuthProvider(models.Model):
854
    """
855
    Available user authentication methods.
856
    """
857
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
858
                                   null=True, default=None)
859
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
860
    module = models.CharField(_('Provider'), max_length=255, blank=False,
861
                                default='local')
862
    identifier = models.CharField(_('Third-party identifier'),
863
                                              max_length=255, null=True,
864
                                              blank=True)
865
    active = models.BooleanField(default=True)
866
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
867
                                   default='astakos')
868
    info_data = models.TextField(default="", null=True, blank=True)
869
    created = models.DateTimeField('Creation date', auto_now_add=True)
870

    
871
    objects = AstakosUserAuthProviderManager()
872

    
873
    class Meta:
874
        unique_together = (('identifier', 'module', 'user'), )
875
        ordering = ('module', 'created')
876

    
877
    def __init__(self, *args, **kwargs):
878
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
879
        try:
880
            self.info = json.loads(self.info_data)
881
            if not self.info:
882
                self.info = {}
883
        except Exception, e:
884
            self.info = {}
885

    
886
        for key,value in self.info.iteritems():
887
            setattr(self, 'info_%s' % key, value)
888

    
889

    
890
    @property
891
    def settings(self):
892
        return auth_providers.get_provider(self.module)
893

    
894
    @property
895
    def details_display(self):
896
        try:
897
            params = self.user.__dict__
898
            params.update(self.__dict__)
899
            return self.settings.get_details_tpl_display % params
900
        except:
901
            return ''
902

    
903
    @property
904
    def title_display(self):
905
        title_tpl = self.settings.get_title_display
906
        try:
907
            if self.settings.get_user_title_display:
908
                title_tpl = self.settings.get_user_title_display
909
        except Exception, e:
910
            pass
911
        try:
912
          return title_tpl % self.__dict__
913
        except:
914
          return self.settings.get_title_display % self.__dict__
915

    
916
    def can_remove(self):
917
        return self.user.can_remove_auth_provider(self.module)
918

    
919
    def delete(self, *args, **kwargs):
920
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
921
        if self.module == 'local':
922
            self.user.set_unusable_password()
923
            self.user.save()
924
        return ret
925

    
926
    def __repr__(self):
927
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
928

    
929
    def __unicode__(self):
930
        if self.identifier:
931
            return "%s:%s" % (self.module, self.identifier)
932
        if self.auth_backend:
933
            return "%s:%s" % (self.module, self.auth_backend)
934
        return self.module
935

    
936
    def save(self, *args, **kwargs):
937
        self.info_data = json.dumps(self.info)
938
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
939

    
940

    
941
class ExtendedManager(models.Manager):
942
    def _update_or_create(self, **kwargs):
943
        assert kwargs, \
944
            'update_or_create() must be passed at least one keyword argument'
945
        obj, created = self.get_or_create(**kwargs)
946
        defaults = kwargs.pop('defaults', {})
947
        if created:
948
            return obj, True, False
949
        else:
950
            try:
951
                params = dict(
952
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
953
                params.update(defaults)
954
                for attr, val in params.items():
955
                    if hasattr(obj, attr):
956
                        setattr(obj, attr, val)
957
                sid = transaction.savepoint()
958
                obj.save(force_update=True)
959
                transaction.savepoint_commit(sid)
960
                return obj, False, True
961
            except IntegrityError, e:
962
                transaction.savepoint_rollback(sid)
963
                try:
964
                    return self.get(**kwargs), False, False
965
                except self.model.DoesNotExist:
966
                    raise e
967

    
968
    update_or_create = _update_or_create
969

    
970

    
971
class AstakosUserQuota(models.Model):
972
    objects = ExtendedManager()
973
    capacity = intDecimalField()
974
    quantity = intDecimalField(default=0)
975
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
976
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
977
    resource = models.ForeignKey(Resource)
978
    user = models.ForeignKey(AstakosUser)
979

    
980
    class Meta:
981
        unique_together = ("resource", "user")
982

    
983
    def quota_values(self):
984
        return QuotaValues(
985
            quantity = self.quantity,
986
            capacity = self.capacity,
987
            import_limit = self.import_limit,
988
            export_limit = self.export_limit)
989

    
990

    
991
class ApprovalTerms(models.Model):
992
    """
993
    Model for approval terms
994
    """
995

    
996
    date = models.DateTimeField(
997
        _('Issue date'), db_index=True, auto_now_add=True)
998
    location = models.CharField(_('Terms location'), max_length=255)
999

    
1000

    
1001
class Invitation(models.Model):
1002
    """
1003
    Model for registring invitations
1004
    """
1005
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1006
                                null=True)
1007
    realname = models.CharField(_('Real name'), max_length=255)
1008
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1009
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1010
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1011
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1012
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1013

    
1014
    def __init__(self, *args, **kwargs):
1015
        super(Invitation, self).__init__(*args, **kwargs)
1016
        if not self.id:
1017
            self.code = _generate_invitation_code()
1018

    
1019
    def consume(self):
1020
        self.is_consumed = True
1021
        self.consumed = datetime.now()
1022
        self.save()
1023

    
1024
    def __unicode__(self):
1025
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1026

    
1027

    
1028
class EmailChangeManager(models.Manager):
1029

    
1030
    @transaction.commit_on_success
1031
    def change_email(self, activation_key):
1032
        """
1033
        Validate an activation key and change the corresponding
1034
        ``User`` if valid.
1035

1036
        If the key is valid and has not expired, return the ``User``
1037
        after activating.
1038

1039
        If the key is not valid or has expired, return ``None``.
1040

1041
        If the key is valid but the ``User`` is already active,
1042
        return ``None``.
1043

1044
        After successful email change the activation record is deleted.
1045

1046
        Throws ValueError if there is already
1047
        """
1048
        try:
1049
            email_change = self.model.objects.get(
1050
                activation_key=activation_key)
1051
            if email_change.activation_key_expired():
1052
                email_change.delete()
1053
                raise EmailChange.DoesNotExist
1054
            # is there an active user with this address?
1055
            try:
1056
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1057
            except AstakosUser.DoesNotExist:
1058
                pass
1059
            else:
1060
                raise ValueError(_('The new email address is reserved.'))
1061
            # update user
1062
            user = AstakosUser.objects.get(pk=email_change.user_id)
1063
            old_email = user.email
1064
            user.email = email_change.new_email_address
1065
            user.save()
1066
            email_change.delete()
1067
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1068
                                                          user.email)
1069
            logger.log(LOGGING_LEVEL, msg)
1070
            return user
1071
        except EmailChange.DoesNotExist:
1072
            raise ValueError(_('Invalid activation key.'))
1073

    
1074

    
1075
class EmailChange(models.Model):
1076
    new_email_address = models.EmailField(
1077
        _(u'new e-mail address'),
1078
        help_text=_('Provide a new email address. Until you verify the new '
1079
                    'address by following the activation link that will be '
1080
                    'sent to it, your old email address will remain active.'))
1081
    user = models.ForeignKey(
1082
        AstakosUser, unique=True, related_name='emailchanges')
1083
    requested_at = models.DateTimeField(auto_now_add=True)
1084
    activation_key = models.CharField(
1085
        max_length=40, unique=True, db_index=True)
1086

    
1087
    objects = EmailChangeManager()
1088

    
1089
    def get_url(self):
1090
        return reverse('email_change_confirm',
1091
                      kwargs={'activation_key': self.activation_key})
1092

    
1093
    def activation_key_expired(self):
1094
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1095
        return self.requested_at + expiration_date < datetime.now()
1096

    
1097

    
1098
class AdditionalMail(models.Model):
1099
    """
1100
    Model for registring invitations
1101
    """
1102
    owner = models.ForeignKey(AstakosUser)
1103
    email = models.EmailField()
1104

    
1105

    
1106
def _generate_invitation_code():
1107
    while True:
1108
        code = randint(1, 2L ** 63 - 1)
1109
        try:
1110
            Invitation.objects.get(code=code)
1111
            # An invitation with this code already exists, try again
1112
        except Invitation.DoesNotExist:
1113
            return code
1114

    
1115

    
1116
def get_latest_terms():
1117
    try:
1118
        term = ApprovalTerms.objects.order_by('-id')[0]
1119
        return term
1120
    except IndexError:
1121
        pass
1122
    return None
1123

    
1124
class PendingThirdPartyUser(models.Model):
1125
    """
1126
    Model for registring successful third party user authentications
1127
    """
1128
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1129
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1130
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1131
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1132
                                  null=True)
1133
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1134
                                 null=True)
1135
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1136
                                   null=True)
1137
    username = models.CharField(_('username'), max_length=30, unique=True,  
1138
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1139
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1140
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1141
    info = models.TextField(default="", null=True, blank=True)
1142

    
1143
    class Meta:
1144
        unique_together = ("provider", "third_party_identifier")
1145

    
1146
    def get_user_instance(self):
1147
        d = self.__dict__
1148
        d.pop('_state', None)
1149
        d.pop('id', None)
1150
        d.pop('token', None)
1151
        d.pop('created', None)
1152
        d.pop('info', None)
1153
        user = AstakosUser(**d)
1154

    
1155
        return user
1156

    
1157
    @property
1158
    def realname(self):
1159
        return '%s %s' %(self.first_name, self.last_name)
1160

    
1161
    @realname.setter
1162
    def realname(self, value):
1163
        parts = value.split(' ')
1164
        if len(parts) == 2:
1165
            self.first_name = parts[0]
1166
            self.last_name = parts[1]
1167
        else:
1168
            self.last_name = parts[0]
1169

    
1170
    def save(self, **kwargs):
1171
        if not self.id:
1172
            # set username
1173
            while not self.username:
1174
                username =  uuid.uuid4().hex[:30]
1175
                try:
1176
                    AstakosUser.objects.get(username = username)
1177
                except AstakosUser.DoesNotExist, e:
1178
                    self.username = username
1179
        super(PendingThirdPartyUser, self).save(**kwargs)
1180

    
1181
    def generate_token(self):
1182
        self.password = self.third_party_identifier
1183
        self.last_login = datetime.now()
1184
        self.token = default_token_generator.make_token(self)
1185

    
1186
    def existing_user(self):
1187
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1188
                                         auth_providers__identifier=self.third_party_identifier)
1189

    
1190
class SessionCatalog(models.Model):
1191
    session_key = models.CharField(_('session key'), max_length=40)
1192
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1193

    
1194

    
1195
### PROJECTS ###
1196
################
1197

    
1198
def synced_model_metaclass(class_name, class_parents, class_attributes):
1199

    
1200
    new_attributes = {}
1201
    sync_attributes = {}
1202

    
1203
    for name, value in class_attributes.iteritems():
1204
        sync, underscore, rest = name.partition('_')
1205
        if sync == 'sync' and underscore == '_':
1206
            sync_attributes[rest] = value
1207
        else:
1208
            new_attributes[name] = value
1209

    
1210
    if 'prefix' not in sync_attributes:
1211
        m = ("you did not specify a 'sync_prefix' attribute "
1212
             "in class '%s'" % (class_name,))
1213
        raise ValueError(m)
1214

    
1215
    prefix = sync_attributes.pop('prefix')
1216
    class_name = sync_attributes.pop('classname', prefix + '_model')
1217

    
1218
    for name, value in sync_attributes.iteritems():
1219
        newname = prefix + '_' + name
1220
        if newname in new_attributes:
1221
            m = ("class '%s' was specified with prefix '%s' "
1222
                 "but it already has an attribute named '%s'"
1223
                 % (class_name, prefix, newname))
1224
            raise ValueError(m)
1225

    
1226
        new_attributes[newname] = value
1227

    
1228
    newclass = type(class_name, class_parents, new_attributes)
1229
    return newclass
1230

    
1231

    
1232
def make_synced(prefix='sync', name='SyncedState'):
1233

    
1234
    the_name = name
1235
    the_prefix = prefix
1236

    
1237
    class SyncedState(models.Model):
1238

    
1239
        sync_classname      = the_name
1240
        sync_prefix         = the_prefix
1241
        __metaclass__       = synced_model_metaclass
1242

    
1243
        sync_new_state      = models.BigIntegerField(null=True)
1244
        sync_synced_state   = models.BigIntegerField(null=True)
1245
        STATUS_SYNCED       = 0
1246
        STATUS_PENDING      = 1
1247
        sync_status         = models.IntegerField(db_index=True)
1248

    
1249
        class Meta:
1250
            abstract = True
1251

    
1252
        class NotSynced(Exception):
1253
            pass
1254

    
1255
        def sync_init_state(self, state):
1256
            self.sync_synced_state = state
1257
            self.sync_new_state = state
1258
            self.sync_status = self.STATUS_SYNCED
1259

    
1260
        def sync_get_status(self):
1261
            return self.sync_status
1262

    
1263
        def sync_set_status(self):
1264
            if self.sync_new_state != self.sync_synced_state:
1265
                self.sync_status = self.STATUS_PENDING
1266
            else:
1267
                self.sync_status = self.STATUS_SYNCED
1268

    
1269
        def sync_set_synced(self):
1270
            self.sync_synced_state = self.sync_new_state
1271
            self.sync_status = self.STATUS_SYNCED
1272

    
1273
        def sync_get_synced_state(self):
1274
            return self.sync_synced_state
1275

    
1276
        def sync_set_new_state(self, new_state):
1277
            self.sync_new_state = new_state
1278
            self.sync_set_status()
1279

    
1280
        def sync_get_new_state(self):
1281
            return self.sync_new_state
1282

    
1283
        def sync_set_synced_state(self, synced_state):
1284
            self.sync_synced_state = synced_state
1285
            self.sync_set_status()
1286

    
1287
        def sync_get_pending_objects(self):
1288
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1289
            return self.objects.filter(**kw)
1290

    
1291
        def sync_get_synced_objects(self):
1292
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1293
            return self.objects.filter(**kw)
1294

    
1295
        def sync_verify_get_synced_state(self):
1296
            status = self.sync_get_status()
1297
            state = self.sync_get_synced_state()
1298
            verified = (status == self.STATUS_SYNCED)
1299
            return state, verified
1300

    
1301
        def sync_is_synced(self):
1302
            state, verified = self.sync_verify_get_synced_state()
1303
            return verified
1304

    
1305
    return SyncedState
1306

    
1307
SyncedState = make_synced(prefix='sync', name='SyncedState')
1308

    
1309

    
1310
class ChainManager(ForUpdateManager):
1311

    
1312
    def search_by_name(self, *search_strings):
1313
        projects = Project.objects.search_by_name(*search_strings)
1314
        chains = [p.id for p in projects]
1315
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1316
        apps = (app for app in apps if app.is_latest())
1317
        app_chains = [app.chain for app in apps if app.chain not in chains]
1318
        return chains + app_chains
1319

    
1320
    def all_full_state(self):
1321
        d = {}
1322
        chains = self.all()
1323
        for chain in chains:
1324
            d[chain.pk] = chain.full_state()
1325
        return d
1326

    
1327
    def of_project(self, project):
1328
        if project is None:
1329
            return None
1330
        try:
1331
            return self.get(chain=project.id)
1332
        except Chain.DoesNotExist:
1333
            raise AssertionError('project with no chain')
1334

    
1335

    
1336
class Chain(models.Model):
1337
    chain  =   models.AutoField(primary_key=True)
1338

    
1339
    def __str__(self):
1340
        return "%s" % (self.chain,)
1341

    
1342
    objects = ChainManager()
1343

    
1344
    PENDING            = 0
1345
    DENIED             = 3
1346
    DISMISSED          = 4
1347
    CANCELLED          = 5
1348

    
1349
    APPROVED           = 10
1350
    APPROVED_PENDING   = 11
1351
    SUSPENDED          = 12
1352
    SUSPENDED_PENDING  = 13
1353
    TERMINATED         = 14
1354
    TERMINATED_PENDING = 15
1355

    
1356
    PENDING_STATES = [PENDING,
1357
                      APPROVED_PENDING,
1358
                      SUSPENDED_PENDING,
1359
                      TERMINATED_PENDING,
1360
                      ]
1361

    
1362
    SKIP_STATES = [DISMISSED,
1363
                   CANCELLED,
1364
                   TERMINATED]
1365

    
1366
    STATE_DISPLAY = {
1367
        PENDING            : _("Pending"),
1368
        DENIED             : _("Denied"),
1369
        DISMISSED          : _("Dismissed"),
1370
        CANCELLED          : _("Cancelled"),
1371
        APPROVED           : _("Active"),
1372
        APPROVED_PENDING   : _("Active - Pending"),
1373
        SUSPENDED          : _("Suspended"),
1374
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1375
        TERMINATED         : _("Terminated"),
1376
        TERMINATED_PENDING : _("Terminated - Pending"),
1377
        }
1378

    
1379

    
1380
    @classmethod
1381
    def _chain_state(cls, project_state, app_state):
1382
        s = CHAIN_STATE.get((project_state, app_state), None)
1383
        if s is None:
1384
            raise AssertionError('inconsistent chain state')
1385
        return s
1386

    
1387
    @classmethod
1388
    def chain_state(cls, project, app):
1389
        p_state = project.state if project else None
1390
        return cls._chain_state(p_state, app.state)
1391

    
1392
    @classmethod
1393
    def state_display(cls, s):
1394
        if s is None:
1395
            return _("Unknown")
1396
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1397

    
1398
    def last_application(self):
1399
        return self.chained_apps.order_by('-id')[0]
1400

    
1401
    def get_project(self):
1402
        try:
1403
            return self.chained_project
1404
        except Project.DoesNotExist:
1405
            return None
1406

    
1407
    def get_elements(self):
1408
        project = self.get_project()
1409
        app = self.last_application()
1410
        return project, app
1411

    
1412
    def full_state(self):
1413
        project, app = self.get_elements()
1414
        s = self.chain_state(project, app)
1415
        return s, project, app
1416

    
1417
def new_chain():
1418
    c = Chain.objects.create()
1419
    return c
1420

    
1421

    
1422
class ProjectApplicationManager(ForUpdateManager):
1423

    
1424
    def user_visible_projects(self, *filters, **kw_filters):
1425
        model = self.model
1426
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1427

    
1428
    def user_visible_by_chain(self, flt):
1429
        model = self.model
1430
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1431
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1432
        by_chain = dict(pending.annotate(models.Max('id')))
1433
        by_chain.update(approved.annotate(models.Max('id')))
1434
        return self.filter(flt, id__in=by_chain.values())
1435

    
1436
    def user_accessible_projects(self, user):
1437
        """
1438
        Return projects accessed by specified user.
1439
        """
1440
        if user.is_project_admin():
1441
            participates_filters = Q()
1442
        else:
1443
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1444
                                   Q(project__projectmembership__person=user)
1445

    
1446
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1447

    
1448
    def search_by_name(self, *search_strings):
1449
        q = Q()
1450
        for s in search_strings:
1451
            q = q | Q(name__icontains=s)
1452
        return self.filter(q)
1453

    
1454
    def latest_of_chain(self, chain_id):
1455
        try:
1456
            return self.filter(chain=chain_id).order_by('-id')[0]
1457
        except IndexError:
1458
            return None
1459

    
1460

    
1461
class ProjectApplication(models.Model):
1462
    applicant               =   models.ForeignKey(
1463
                                    AstakosUser,
1464
                                    related_name='projects_applied',
1465
                                    db_index=True)
1466

    
1467
    PENDING     =    0
1468
    APPROVED    =    1
1469
    REPLACED    =    2
1470
    DENIED      =    3
1471
    DISMISSED   =    4
1472
    CANCELLED   =    5
1473

    
1474
    state                   =   models.IntegerField(default=PENDING,
1475
                                                    db_index=True)
1476

    
1477
    owner                   =   models.ForeignKey(
1478
                                    AstakosUser,
1479
                                    related_name='projects_owned',
1480
                                    db_index=True)
1481

    
1482
    chain                   =   models.ForeignKey(Chain,
1483
                                                  related_name='chained_apps',
1484
                                                  db_column='chain')
1485
    precursor_application   =   models.ForeignKey('ProjectApplication',
1486
                                                  null=True,
1487
                                                  blank=True)
1488

    
1489
    name                    =   models.CharField(max_length=80)
1490
    homepage                =   models.URLField(max_length=255, null=True,
1491
                                                verify_exists=False)
1492
    description             =   models.TextField(null=True, blank=True)
1493
    start_date              =   models.DateTimeField(null=True, blank=True)
1494
    end_date                =   models.DateTimeField()
1495
    member_join_policy      =   models.IntegerField()
1496
    member_leave_policy     =   models.IntegerField()
1497
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1498
    resource_grants         =   models.ManyToManyField(
1499
                                    Resource,
1500
                                    null=True,
1501
                                    blank=True,
1502
                                    through='ProjectResourceGrant')
1503
    comments                =   models.TextField(null=True, blank=True)
1504
    issue_date              =   models.DateTimeField(auto_now_add=True)
1505
    response_date           =   models.DateTimeField(null=True, blank=True)
1506

    
1507
    objects                 =   ProjectApplicationManager()
1508

    
1509
    # Compiled queries
1510
    Q_PENDING  = Q(state=PENDING)
1511
    Q_APPROVED = Q(state=APPROVED)
1512
    Q_DENIED   = Q(state=DENIED)
1513

    
1514
    class Meta:
1515
        unique_together = ("chain", "id")
1516

    
1517
    def __unicode__(self):
1518
        return "%s applied by %s" % (self.name, self.applicant)
1519

    
1520
    # TODO: Move to a more suitable place
1521
    APPLICATION_STATE_DISPLAY = {
1522
        PENDING  : _('Pending review'),
1523
        APPROVED : _('Approved'),
1524
        REPLACED : _('Replaced'),
1525
        DENIED   : _('Denied'),
1526
        DISMISSED: _('Dismissed'),
1527
        CANCELLED: _('Cancelled')
1528
    }
1529

    
1530
    def get_project(self):
1531
        try:
1532
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1533
            return Project
1534
        except Project.DoesNotExist, e:
1535
            return None
1536

    
1537
    def state_display(self):
1538
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1539

    
1540
    def project_state_display(self):
1541
        try:
1542
            project = self.project
1543
            return project.state_display()
1544
        except Project.DoesNotExist:
1545
            return self.state_display()
1546

    
1547
    def add_resource_policy(self, service, resource, uplimit):
1548
        """Raises ObjectDoesNotExist, IntegrityError"""
1549
        q = self.projectresourcegrant_set
1550
        resource = Resource.objects.get(service__name=service, name=resource)
1551
        q.create(resource=resource, member_capacity=uplimit)
1552

    
1553
    def members_count(self):
1554
        return self.project.approved_memberships.count()
1555

    
1556
    @property
1557
    def grants(self):
1558
        return self.projectresourcegrant_set.values(
1559
            'member_capacity', 'resource__name', 'resource__service__name')
1560

    
1561
    @property
1562
    def resource_policies(self):
1563
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1564

    
1565
    @resource_policies.setter
1566
    def resource_policies(self, policies):
1567
        for p in policies:
1568
            service = p.get('service', None)
1569
            resource = p.get('resource', None)
1570
            uplimit = p.get('uplimit', 0)
1571
            self.add_resource_policy(service, resource, uplimit)
1572

    
1573
    def pending_modifications_incl_me(self):
1574
        q = self.chained_applications()
1575
        q = q.filter(Q(state=self.PENDING))
1576
        return q
1577

    
1578
    def last_pending_incl_me(self):
1579
        try:
1580
            return self.pending_modifications_incl_me().order_by('-id')[0]
1581
        except IndexError:
1582
            return None
1583

    
1584
    def pending_modifications(self):
1585
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1586

    
1587
    def last_pending(self):
1588
        try:
1589
            return self.pending_modifications().order_by('-id')[0]
1590
        except IndexError:
1591
            return None
1592

    
1593
    def is_modification(self):
1594
        # if self.state != self.PENDING:
1595
        #     return False
1596
        parents = self.chained_applications().filter(id__lt=self.id)
1597
        parents = parents.filter(state__in=[self.APPROVED])
1598
        return parents.count() > 0
1599

    
1600
    def chained_applications(self):
1601
        return ProjectApplication.objects.filter(chain=self.chain)
1602

    
1603
    def is_latest(self):
1604
        return self.chained_applications().order_by('-id')[0] == self
1605

    
1606
    def has_pending_modifications(self):
1607
        return bool(self.last_pending())
1608

    
1609
    def denied_modifications(self):
1610
        q = self.chained_applications()
1611
        q = q.filter(Q(state=self.DENIED))
1612
        q = q.filter(~Q(id=self.id))
1613
        return q
1614

    
1615
    def last_denied(self):
1616
        try:
1617
            return self.denied_modifications().order_by('-id')[0]
1618
        except IndexError:
1619
            return None
1620

    
1621
    def has_denied_modifications(self):
1622
        return bool(self.last_denied())
1623

    
1624
    def is_applied(self):
1625
        try:
1626
            self.project
1627
            return True
1628
        except Project.DoesNotExist:
1629
            return False
1630

    
1631
    def get_project(self):
1632
        try:
1633
            return Project.objects.get(id=self.chain)
1634
        except Project.DoesNotExist:
1635
            return None
1636

    
1637
    def project_exists(self):
1638
        return self.get_project() is not None
1639

    
1640
    def _get_project_for_update(self):
1641
        try:
1642
            objects = Project.objects.select_for_update()
1643
            project = objects.get(id=self.chain)
1644
            return project
1645
        except Project.DoesNotExist:
1646
            return None
1647

    
1648
    def can_cancel(self):
1649
        return self.state == self.PENDING
1650

    
1651
    def cancel(self):
1652
        if not self.can_cancel():
1653
            m = _("cannot cancel: application '%s' in state '%s'") % (
1654
                    self.id, self.state)
1655
            raise AssertionError(m)
1656

    
1657
        self.state = self.CANCELLED
1658
        self.save()
1659

    
1660
    def can_dismiss(self):
1661
        return self.state == self.DENIED
1662

    
1663
    def dismiss(self):
1664
        if not self.can_dismiss():
1665
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1666
                    self.id, self.state)
1667
            raise AssertionError(m)
1668

    
1669
        self.state = self.DISMISSED
1670
        self.save()
1671

    
1672
    def can_deny(self):
1673
        return self.state == self.PENDING
1674

    
1675
    def deny(self):
1676
        if not self.can_deny():
1677
            m = _("cannot deny: application '%s' in state '%s'") % (
1678
                    self.id, self.state)
1679
            raise AssertionError(m)
1680

    
1681
        self.state = self.DENIED
1682
        self.response_date = datetime.now()
1683
        self.save()
1684

    
1685
    def can_approve(self):
1686
        return self.state == self.PENDING
1687

    
1688
    def approve(self, approval_user=None):
1689
        """
1690
        If approval_user then during owner membership acceptance
1691
        it is checked whether the request_user is eligible.
1692

1693
        Raises:
1694
            PermissionDenied
1695
        """
1696

    
1697
        if not transaction.is_managed():
1698
            raise AssertionError("NOPE")
1699

    
1700
        new_project_name = self.name
1701
        if not self.can_approve():
1702
            m = _("cannot approve: project '%s' in state '%s'") % (
1703
                    new_project_name, self.state)
1704
            raise AssertionError(m) # invalid argument
1705

    
1706
        now = datetime.now()
1707
        project = self._get_project_for_update()
1708

    
1709
        try:
1710
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1711
            conflicting_project = Project.objects.get(q)
1712
            if (conflicting_project != project):
1713
                m = (_("cannot approve: project with name '%s' "
1714
                       "already exists (id: %s)") % (
1715
                        new_project_name, conflicting_project.id))
1716
                raise PermissionDenied(m) # invalid argument
1717
        except Project.DoesNotExist:
1718
            pass
1719

    
1720
        new_project = False
1721
        if project is None:
1722
            new_project = True
1723
            project = Project(id=self.chain)
1724

    
1725
        project.name = new_project_name
1726
        project.application = self
1727
        project.last_approval_date = now
1728
        if not new_project:
1729
            project.is_modified = True
1730

    
1731
        project.save()
1732

    
1733
        self.state = self.APPROVED
1734
        self.response_date = now
1735
        self.save()
1736

    
1737
    @property
1738
    def member_join_policy_display(self):
1739
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1740

    
1741
    @property
1742
    def member_leave_policy_display(self):
1743
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1744

    
1745
class ProjectResourceGrant(models.Model):
1746

    
1747
    resource                =   models.ForeignKey(Resource)
1748
    project_application     =   models.ForeignKey(ProjectApplication,
1749
                                                  null=True)
1750
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1751
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1752
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1753
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1754
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1755
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1756

    
1757
    objects = ExtendedManager()
1758

    
1759
    class Meta:
1760
        unique_together = ("resource", "project_application")
1761

    
1762
    def member_quota_values(self):
1763
        return QuotaValues(
1764
            quantity = 0,
1765
            capacity = self.member_capacity,
1766
            import_limit = self.member_import_limit,
1767
            export_limit = self.member_export_limit)
1768

    
1769
    def display_member_capacity(self):
1770
        if self.member_capacity:
1771
            if self.resource.unit:
1772
                return ProjectResourceGrant.display_filesize(
1773
                    self.member_capacity)
1774
            else:
1775
                if math.isinf(self.member_capacity):
1776
                    return 'Unlimited'
1777
                else:
1778
                    return self.member_capacity
1779
        else:
1780
            return 'Unlimited'
1781

    
1782
    def __str__(self):
1783
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1784
                                        self.display_member_capacity())
1785

    
1786
    @classmethod
1787
    def display_filesize(cls, value):
1788
        try:
1789
            value = float(value)
1790
        except:
1791
            return
1792
        else:
1793
            if math.isinf(value):
1794
                return 'Unlimited'
1795
            if value > 1:
1796
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1797
                                [0, 0, 0, 0, 0, 0])
1798
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1799
                quotient = float(value) / 1024**exponent
1800
                unit, value_decimals = unit_list[exponent]
1801
                format_string = '{0:.%sf} {1}' % (value_decimals)
1802
                return format_string.format(quotient, unit)
1803
            if value == 0:
1804
                return '0 bytes'
1805
            if value == 1:
1806
                return '1 byte'
1807
            else:
1808
               return '0'
1809

    
1810

    
1811
class ProjectManager(ForUpdateManager):
1812

    
1813
    def terminated_projects(self):
1814
        q = self.model.Q_TERMINATED
1815
        return self.filter(q)
1816

    
1817
    def not_terminated_projects(self):
1818
        q = ~self.model.Q_TERMINATED
1819
        return self.filter(q)
1820

    
1821
    def terminating_projects(self):
1822
        q = self.model.Q_TERMINATED & Q(is_active=True)
1823
        return self.filter(q)
1824

    
1825
    def deactivated_projects(self):
1826
        q = self.model.Q_DEACTIVATED
1827
        return self.filter(q)
1828

    
1829
    def deactivating_projects(self):
1830
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1831
        return self.filter(q)
1832

    
1833
    def modified_projects(self):
1834
        return self.filter(is_modified=True)
1835

    
1836
    def reactivating_projects(self):
1837
        return self.filter(state=Project.APPROVED, is_active=False)
1838

    
1839
    def expired_projects(self):
1840
        q = (~Q(state=Project.TERMINATED) &
1841
              Q(application__end_date__lt=datetime.now()))
1842
        return self.filter(q)
1843

    
1844
    def search_by_name(self, *search_strings):
1845
        q = Q()
1846
        for s in search_strings:
1847
            q = q | Q(name__icontains=s)
1848
        return self.filter(q)
1849

    
1850

    
1851
class Project(models.Model):
1852

    
1853
    id                          =   models.OneToOneField(Chain,
1854
                                                      related_name='chained_project',
1855
                                                      db_column='id',
1856
                                                      primary_key=True)
1857

    
1858
    application                 =   models.OneToOneField(
1859
                                            ProjectApplication,
1860
                                            related_name='project')
1861
    last_approval_date          =   models.DateTimeField(null=True)
1862

    
1863
    members                     =   models.ManyToManyField(
1864
                                            AstakosUser,
1865
                                            through='ProjectMembership')
1866

    
1867
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1868
    deactivation_date           =   models.DateTimeField(null=True)
1869

    
1870
    creation_date               =   models.DateTimeField(auto_now_add=True)
1871
    name                        =   models.CharField(
1872
                                            max_length=80,
1873
                                            null=True,
1874
                                            db_index=True,
1875
                                            unique=True)
1876

    
1877
    APPROVED    = 1
1878
    SUSPENDED   = 10
1879
    TERMINATED  = 100
1880

    
1881
    is_modified                 =   models.BooleanField(default=False,
1882
                                                        db_index=True)
1883
    is_active                   =   models.BooleanField(default=True,
1884
                                                        db_index=True)
1885
    state                       =   models.IntegerField(default=APPROVED,
1886
                                                        db_index=True)
1887

    
1888
    objects     =   ProjectManager()
1889

    
1890
    # Compiled queries
1891
    Q_TERMINATED  = Q(state=TERMINATED)
1892
    Q_SUSPENDED   = Q(state=SUSPENDED)
1893
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1894

    
1895
    def __str__(self):
1896
        return uenc(_("<project %s '%s'>") %
1897
                    (self.id, udec(self.application.name)))
1898

    
1899
    __repr__ = __str__
1900

    
1901
    def __unicode__(self):
1902
        return _("<project %s '%s'>") % (self.id, self.application.name)
1903

    
1904
    STATE_DISPLAY = {
1905
        APPROVED   : 'Active',
1906
        SUSPENDED  : 'Suspended',
1907
        TERMINATED : 'Terminated'
1908
        }
1909

    
1910
    def state_display(self):
1911
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1912

    
1913
    def admin_state_display(self):
1914
        s = self.state_display()
1915
        if self.sync_pending():
1916
            s += ' (sync pending)'
1917
        return s
1918

    
1919
    def sync_pending(self):
1920
        if self.state != self.APPROVED:
1921
            return self.is_active
1922
        return not self.is_active or self.is_modified
1923

    
1924
    def expiration_info(self):
1925
        return (str(self.id), self.name, self.state_display(),
1926
                str(self.application.end_date))
1927

    
1928
    def is_deactivated(self, reason=None):
1929
        if reason is not None:
1930
            return self.state == reason
1931

    
1932
        return self.state != self.APPROVED
1933

    
1934
    def is_deactivating(self, reason=None):
1935
        if not self.is_active:
1936
            return False
1937

    
1938
        return self.is_deactivated(reason)
1939

    
1940
    def is_deactivated_strict(self, reason=None):
1941
        if self.is_active:
1942
            return False
1943

    
1944
        return self.is_deactivated(reason)
1945

    
1946
    ### Deactivation calls
1947

    
1948
    def deactivate(self):
1949
        self.deactivation_date = datetime.now()
1950
        self.is_active = False
1951

    
1952
    def reactivate(self):
1953
        self.deactivation_date = None
1954
        self.is_active = True
1955

    
1956
    def terminate(self):
1957
        self.deactivation_reason = 'TERMINATED'
1958
        self.state = self.TERMINATED
1959
        self.name = None
1960
        self.save()
1961

    
1962
    def suspend(self):
1963
        self.deactivation_reason = 'SUSPENDED'
1964
        self.state = self.SUSPENDED
1965
        self.save()
1966

    
1967
    def resume(self):
1968
        self.deactivation_reason = None
1969
        self.state = self.APPROVED
1970
        self.save()
1971

    
1972
    ### Logical checks
1973

    
1974
    def is_inconsistent(self):
1975
        now = datetime.now()
1976
        dates = [self.creation_date,
1977
                 self.last_approval_date,
1978
                 self.deactivation_date]
1979
        return any([date > now for date in dates])
1980

    
1981
    def is_active_strict(self):
1982
        return self.is_active and self.state == self.APPROVED
1983

    
1984
    def is_approved(self):
1985
        return self.state == self.APPROVED
1986

    
1987
    @property
1988
    def is_alive(self):
1989
        return not self.is_terminated
1990

    
1991
    @property
1992
    def is_terminated(self):
1993
        return self.is_deactivated(self.TERMINATED)
1994

    
1995
    @property
1996
    def is_suspended(self):
1997
        return self.is_deactivated(self.SUSPENDED)
1998

    
1999
    def violates_resource_grants(self):
2000
        return False
2001

    
2002
    def violates_members_limit(self, adding=0):
2003
        application = self.application
2004
        limit = application.limit_on_members_number
2005
        if limit is None:
2006
            return False
2007
        return (len(self.approved_members) + adding > limit)
2008

    
2009

    
2010
    ### Other
2011

    
2012
    def count_pending_memberships(self):
2013
        memb_set = self.projectmembership_set
2014
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2015
        return memb_count
2016

    
2017
    def members_count(self):
2018
        return self.approved_memberships.count()
2019

    
2020
    @property
2021
    def approved_memberships(self):
2022
        query = ProjectMembership.Q_ACCEPTED_STATES
2023
        return self.projectmembership_set.filter(query)
2024

    
2025
    @property
2026
    def approved_members(self):
2027
        return [m.person for m in self.approved_memberships]
2028

    
2029
    def add_member(self, user):
2030
        """
2031
        Raises:
2032
            django.exceptions.PermissionDenied
2033
            astakos.im.models.AstakosUser.DoesNotExist
2034
        """
2035
        if isinstance(user, (int, long)):
2036
            user = AstakosUser.objects.get(user=user)
2037

    
2038
        m, created = ProjectMembership.objects.get_or_create(
2039
            person=user, project=self
2040
        )
2041
        m.accept()
2042

    
2043
    def remove_member(self, user):
2044
        """
2045
        Raises:
2046
            django.exceptions.PermissionDenied
2047
            astakos.im.models.AstakosUser.DoesNotExist
2048
            astakos.im.models.ProjectMembership.DoesNotExist
2049
        """
2050
        if isinstance(user, (int, long)):
2051
            user = AstakosUser.objects.get(user=user)
2052

    
2053
        m = ProjectMembership.objects.get(person=user, project=self)
2054
        m.remove()
2055

    
2056

    
2057
CHAIN_STATE = {
2058
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2059
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2060
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2061
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2062
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2063

    
2064
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2065
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2066
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2067
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2068
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2069

    
2070
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2071
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2072
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2073
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2074
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2075

    
2076
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2077
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2078
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2079
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2080
    }
2081

    
2082

    
2083
class PendingMembershipError(Exception):
2084
    pass
2085

    
2086

    
2087
class ProjectMembershipManager(ForUpdateManager):
2088

    
2089
    def any_accepted(self):
2090
        q = (Q(state=ProjectMembership.ACCEPTED) |
2091
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
2092
        return self.filter(q)
2093

    
2094
    def actually_accepted(self):
2095
        q = self.model.Q_ACTUALLY_ACCEPTED
2096
        return self.filter(q)
2097

    
2098
    def requested(self):
2099
        return self.filter(state=ProjectMembership.REQUESTED)
2100

    
2101
    def suspended(self):
2102
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2103

    
2104
class ProjectMembership(models.Model):
2105

    
2106
    person              =   models.ForeignKey(AstakosUser)
2107
    request_date        =   models.DateField(auto_now_add=True)
2108
    project             =   models.ForeignKey(Project)
2109

    
2110
    REQUESTED           =   0
2111
    ACCEPTED            =   1
2112
    LEAVE_REQUESTED     =   5
2113
    # User deactivation
2114
    USER_SUSPENDED      =   10
2115
    # Project deactivation
2116
    PROJECT_DEACTIVATED =   100
2117

    
2118
    REMOVED             =   200
2119

    
2120
    ASSOCIATED_STATES   =   set([REQUESTED,
2121
                                 ACCEPTED,
2122
                                 LEAVE_REQUESTED,
2123
                                 USER_SUSPENDED,
2124
                                 PROJECT_DEACTIVATED])
2125

    
2126
    ACCEPTED_STATES     =   set([ACCEPTED,
2127
                                 LEAVE_REQUESTED,
2128
                                 USER_SUSPENDED,
2129
                                 PROJECT_DEACTIVATED])
2130

    
2131
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2132

    
2133
    state               =   models.IntegerField(default=REQUESTED,
2134
                                                db_index=True)
2135
    is_pending          =   models.BooleanField(default=False, db_index=True)
2136
    is_active           =   models.BooleanField(default=False, db_index=True)
2137
    application         =   models.ForeignKey(
2138
                                ProjectApplication,
2139
                                null=True,
2140
                                related_name='memberships')
2141
    pending_application =   models.ForeignKey(
2142
                                ProjectApplication,
2143
                                null=True,
2144
                                related_name='pending_memberships')
2145
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2146

    
2147
    acceptance_date     =   models.DateField(null=True, db_index=True)
2148
    leave_request_date  =   models.DateField(null=True)
2149

    
2150
    objects     =   ProjectMembershipManager()
2151

    
2152
    # Compiled queries
2153
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2154
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2155

    
2156
    MEMBERSHIP_STATE_DISPLAY = {
2157
        REQUESTED           : _('Requested'),
2158
        ACCEPTED            : _('Accepted'),
2159
        LEAVE_REQUESTED     : _('Leave Requested'),
2160
        USER_SUSPENDED      : _('Suspended'),
2161
        PROJECT_DEACTIVATED : _('Accepted'), # sic
2162
        REMOVED             : _('Pending removal'),
2163
        }
2164

    
2165
    USER_FRIENDLY_STATE_DISPLAY = {
2166
        REQUESTED           : _('Join requested'),
2167
        ACCEPTED            : _('Accepted member'),
2168
        LEAVE_REQUESTED     : _('Requested to leave'),
2169
        USER_SUSPENDED      : _('Suspended member'),
2170
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
2171
        REMOVED             : _('Pending removal'),
2172
        }
2173

    
2174
    def state_display(self):
2175
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2176

    
2177
    def user_friendly_state_display(self):
2178
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2179

    
2180
    def get_combined_state(self):
2181
        return self.state, self.is_active, self.is_pending
2182

    
2183
    class Meta:
2184
        unique_together = ("person", "project")
2185
        #index_together = [["project", "state"]]
2186

    
2187
    def __str__(self):
2188
        return uenc(_("<'%s' membership in '%s'>") % (
2189
                self.person.username, self.project))
2190

    
2191
    __repr__ = __str__
2192

    
2193
    def __init__(self, *args, **kwargs):
2194
        self.state = self.REQUESTED
2195
        super(ProjectMembership, self).__init__(*args, **kwargs)
2196

    
2197
    def _set_history_item(self, reason, date=None):
2198
        if isinstance(reason, basestring):
2199
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2200

    
2201
        history_item = ProjectMembershipHistory(
2202
                            serial=self.id,
2203
                            person=self.person_id,
2204
                            project=self.project_id,
2205
                            date=date or datetime.now(),
2206
                            reason=reason)
2207
        history_item.save()
2208
        serial = history_item.id
2209

    
2210
    def can_accept(self):
2211
        return self.state == self.REQUESTED
2212

    
2213
    def accept(self):
2214
        if self.is_pending:
2215
            m = _("%s: attempt to accept while is pending") % (self,)
2216
            raise AssertionError(m)
2217

    
2218
        if not self.can_accept():
2219
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2220
            raise AssertionError(m)
2221

    
2222
        now = datetime.now()
2223
        self.acceptance_date = now
2224
        self._set_history_item(reason='ACCEPT', date=now)
2225
        if self.project.is_approved():
2226
            self.state = self.ACCEPTED
2227
            self.is_pending = True
2228
        else:
2229
            self.state = self.PROJECT_DEACTIVATED
2230

    
2231
        self.save()
2232

    
2233
    def can_leave(self):
2234
        return self.state in self.ACCEPTED_STATES
2235

    
2236
    def leave_request(self):
2237
        if self.is_pending:
2238
            m = _("%s: attempt to request to leave while is pending") % (self,)
2239
            raise AssertionError(m)
2240

    
2241
        if not self.can_leave():
2242
            m = _("%s: attempt to request to leave in state '%s'") % (
2243
                self, self.state)
2244
            raise AssertionError(m)
2245

    
2246
        self.leave_request_date = datetime.now()
2247
        self.state = self.LEAVE_REQUESTED
2248
        self.save()
2249

    
2250
    def can_deny_leave(self):
2251
        return self.state == self.LEAVE_REQUESTED
2252

    
2253
    def leave_request_deny(self):
2254
        if self.is_pending:
2255
            m = _("%s: attempt to deny leave request while is pending") % (
2256
                self,)
2257
            raise AssertionError(m)
2258

    
2259
        if not self.can_deny_leave():
2260
            m = _("%s: attempt to deny leave request in state '%s'") % (
2261
                self, self.state)
2262
            raise AssertionError(m)
2263

    
2264
        self.leave_request_date = None
2265
        self.state = self.ACCEPTED
2266
        self.save()
2267

    
2268
    def can_cancel_leave(self):
2269
        return self.state == self.LEAVE_REQUESTED
2270

    
2271
    def leave_request_cancel(self):
2272
        if self.is_pending:
2273
            m = _("%s: attempt to cancel leave request while is pending") % (
2274
                self,)
2275
            raise AssertionError(m)
2276

    
2277
        if not self.can_cancel_leave():
2278
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2279
                self, self.state)
2280
            raise AssertionError(m)
2281

    
2282
        self.leave_request_date = None
2283
        self.state = self.ACCEPTED
2284
        self.save()
2285

    
2286
    def can_remove(self):
2287
        return self.state in self.ACCEPTED_STATES
2288

    
2289
    def remove(self):
2290
        if self.is_pending:
2291
            m = _("%s: attempt to remove while is pending") % (self,)
2292
            raise AssertionError(m)
2293

    
2294
        if not self.can_remove():
2295
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2296
            raise AssertionError(m)
2297

    
2298
        self._set_history_item(reason='REMOVE')
2299
        self.state = self.REMOVED
2300
        self.is_pending = True
2301
        self.save()
2302

    
2303
    def can_reject(self):
2304
        return self.state == self.REQUESTED
2305

    
2306
    def reject(self):
2307
        if self.is_pending:
2308
            m = _("%s: attempt to reject while is pending") % (self,)
2309
            raise AssertionError(m)
2310

    
2311
        if not self.can_reject():
2312
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2313
            raise AssertionError(m)
2314

    
2315
        # rejected requests don't need sync,
2316
        # because they were never effected
2317
        self._set_history_item(reason='REJECT')
2318
        self.delete()
2319

    
2320
    def can_cancel(self):
2321
        return self.state == self.REQUESTED
2322

    
2323
    def cancel(self):
2324
        if self.is_pending:
2325
            m = _("%s: attempt to cancel while is pending") % (self,)
2326
            raise AssertionError(m)
2327

    
2328
        if not self.can_cancel():
2329
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2330
            raise AssertionError(m)
2331

    
2332
        # rejected requests don't need sync,
2333
        # because they were never effected
2334
        self._set_history_item(reason='CANCEL')
2335
        self.delete()
2336

    
2337
    def get_diff_quotas(self, sub_list=None, add_list=None):
2338
        if sub_list is None:
2339
            sub_list = []
2340

    
2341
        if add_list is None:
2342
            add_list = []
2343

    
2344
        sub_append = sub_list.append
2345
        add_append = add_list.append
2346
        holder = self.person.uuid
2347

    
2348
        synced_application = self.application
2349
        if synced_application is not None:
2350
            cur_grants = synced_application.projectresourcegrant_set.all()
2351
            for grant in cur_grants:
2352
                sub_append(QuotaLimits(
2353
                               holder       = holder,
2354
                               resource     = str(grant.resource),
2355
                               capacity     = grant.member_capacity,
2356
                               import_limit = grant.member_import_limit,
2357
                               export_limit = grant.member_export_limit))
2358

    
2359
        pending_application = self.pending_application
2360
        if pending_application is not None:
2361
            new_grants = pending_application.projectresourcegrant_set.all()
2362
            for new_grant in new_grants:
2363
                add_append(QuotaLimits(
2364
                               holder       = holder,
2365
                               resource     = str(new_grant.resource),
2366
                               capacity     = new_grant.member_capacity,
2367
                               import_limit = new_grant.member_import_limit,
2368
                               export_limit = new_grant.member_export_limit))
2369

    
2370
        return (sub_list, add_list)
2371

    
2372
    def set_sync(self):
2373
        if not self.is_pending:
2374
            m = _("%s: attempt to sync a non pending membership") % (self,)
2375
            raise AssertionError(m)
2376

    
2377
        state = self.state
2378
        if state in self.ACTUALLY_ACCEPTED:
2379
            pending_application = self.pending_application
2380
            if pending_application is None:
2381
                m = _("%s: attempt to sync an empty pending application") % (
2382
                    self,)
2383
                raise AssertionError(m)
2384

    
2385
            self.application = pending_application
2386
            self.is_active = True
2387

    
2388
            self.pending_application = None
2389
            self.pending_serial = None
2390

    
2391
            # project.application may have changed in the meantime,
2392
            # in which case we stay PENDING;
2393
            # we are safe to check due to select_for_update
2394
            if self.application == self.project.application:
2395
                self.is_pending = False
2396
            self.save()
2397

    
2398
        elif state == self.PROJECT_DEACTIVATED:
2399
            if self.pending_application:
2400
                m = _("%s: attempt to sync in state '%s' "
2401
                      "with a pending application") % (self, state)
2402
                raise AssertionError(m)
2403

    
2404
            self.application = None
2405
            self.is_active = False
2406
            self.pending_serial = None
2407
            self.is_pending = False
2408
            self.save()
2409

    
2410
        elif state == self.REMOVED:
2411
            self.delete()
2412

    
2413
        else:
2414
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2415
            raise AssertionError(m)
2416

    
2417
    def reset_sync(self):
2418
        if not self.is_pending:
2419
            m = _("%s: attempt to reset a non pending membership") % (self,)
2420
            raise AssertionError(m)
2421

    
2422
        state = self.state
2423
        if state in [self.ACCEPTED, self.LEAVE_REQUESTED,
2424
                     self.PROJECT_DEACTIVATED, self.REMOVED]:
2425
            self.pending_application = None
2426
            self.pending_serial = None
2427
            self.save()
2428
        else:
2429
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2430
            raise AssertionError(m)
2431

    
2432
class Serial(models.Model):
2433
    serial  =   models.AutoField(primary_key=True)
2434

    
2435
def new_serial():
2436
    s = Serial.objects.create()
2437
    serial = s.serial
2438
    s.delete()
2439
    return serial
2440

    
2441
class SyncError(Exception):
2442
    pass
2443

    
2444
def reset_serials(serials):
2445
    sfu = ProjectMembership.objects.select_for_update()
2446
    memberships = list(sfu.filter(pending_serial__in=serials))
2447

    
2448
    if memberships:
2449
        for membership in memberships:
2450
            membership.reset_sync()
2451

    
2452
        transaction.commit()
2453

    
2454
def sync_finish_serials(serials_to_ack=None):
2455
    if serials_to_ack is None:
2456
        serials_to_ack = qh_query_serials([])
2457

    
2458
    serials_to_ack = set(serials_to_ack)
2459
    sfu = ProjectMembership.objects.select_for_update()
2460
    memberships = list(sfu.filter(pending_serial__isnull=False))
2461

    
2462
    if memberships:
2463
        for membership in memberships:
2464
            serial = membership.pending_serial
2465
            if serial in serials_to_ack:
2466
                membership.set_sync()
2467
            else:
2468
                membership.reset_sync()
2469

    
2470
        transaction.commit()
2471

    
2472
    qh_ack_serials(list(serials_to_ack))
2473
    return len(memberships)
2474

    
2475
def pre_sync_projects(sync=True):
2476
    ACCEPTED = ProjectMembership.ACCEPTED
2477
    LEAVE_REQUESTED = ProjectMembership.LEAVE_REQUESTED
2478
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2479
    psfu = Project.objects.select_for_update()
2480

    
2481
    modified = list(psfu.modified_projects())
2482
    if sync:
2483
        for project in modified:
2484
            objects = project.projectmembership_set.select_for_update()
2485

    
2486
            memberships = objects.actually_accepted()
2487
            for membership in memberships:
2488
                membership.is_pending = True
2489
                membership.save()
2490

    
2491
    reactivating = list(psfu.reactivating_projects())
2492
    if sync:
2493
        for project in reactivating:
2494
            objects = project.projectmembership_set.select_for_update()
2495

    
2496
            memberships = objects.filter(state=PROJECT_DEACTIVATED)
2497
            for membership in memberships:
2498
                membership.is_pending = True
2499
                if membership.leave_request_date is None:
2500
                    membership.state = ACCEPTED
2501
                else:
2502
                    membership.state = LEAVE_REQUESTED
2503
                membership.save()
2504

    
2505
    deactivating = list(psfu.deactivating_projects())
2506
    if sync:
2507
        for project in deactivating:
2508
            objects = project.projectmembership_set.select_for_update()
2509

    
2510
            # Note: we keep a user-level deactivation
2511
            # (e.g. USER_SUSPENDED) intact
2512
            memberships = objects.actually_accepted()
2513
            for membership in memberships:
2514
                membership.is_pending = True
2515
                membership.state = PROJECT_DEACTIVATED
2516
                membership.save()
2517

    
2518
    return (modified, reactivating, deactivating)
2519

    
2520
def set_sync_projects(exclude=None):
2521

    
2522
    ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED
2523
    objects = ProjectMembership.objects.select_for_update()
2524

    
2525
    sub_quota, add_quota = [], []
2526

    
2527
    serial = new_serial()
2528

    
2529
    pending = objects.filter(is_pending=True)
2530
    for membership in pending:
2531

    
2532
        if membership.pending_application:
2533
            m = "%s: impossible: pending_application is not None (%s)" % (
2534
                membership, membership.pending_application)
2535
            raise AssertionError(m)
2536
        if membership.pending_serial:
2537
            m = "%s: impossible: pending_serial is not None (%s)" % (
2538
                membership, membership.pending_serial)
2539
            raise AssertionError(m)
2540

    
2541
        if exclude is not None:
2542
            uuid = membership.person.uuid
2543
            if uuid in exclude:
2544
                logger.warning("Excluded from sync: %s" % uuid)
2545
                continue
2546

    
2547
        if membership.state in ACTUALLY_ACCEPTED:
2548
            membership.pending_application = membership.project.application
2549

    
2550
        membership.pending_serial = serial
2551
        membership.get_diff_quotas(sub_quota, add_quota)
2552
        membership.save()
2553

    
2554
    transaction.commit()
2555
    return serial, sub_quota, add_quota
2556

    
2557
def do_sync_projects():
2558
    serial, sub_quota, add_quota = set_sync_projects()
2559
    r = qh_add_quota(serial, sub_quota, add_quota)
2560
    if not r:
2561
        return serial
2562

    
2563
    m = "cannot sync serial: %d" % serial
2564
    logger.error(m)
2565
    logger.error("Failed: %s" % r)
2566

    
2567
    reset_serials([serial])
2568
    uuids = set(uuid for (uuid, resource) in r)
2569
    serial, sub_quota, add_quota = set_sync_projects(exclude=uuids)
2570
    r = qh_add_quota(serial, sub_quota, add_quota)
2571
    if not r:
2572
        return serial
2573

    
2574
    m = "cannot sync serial: %d" % serial
2575
    logger.error(m)
2576
    logger.error("Failed: %s" % r)
2577
    raise SyncError(m)
2578

    
2579
def post_sync_projects():
2580
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2581
    Q_ACTUALLY_ACCEPTED = ProjectMembership.Q_ACTUALLY_ACCEPTED
2582
    psfu = Project.objects.select_for_update()
2583

    
2584
    modified = psfu.modified_projects()
2585
    for project in modified:
2586
        objects = project.projectmembership_set.select_for_update()
2587

    
2588
        memberships = list(objects.filter(Q_ACTUALLY_ACCEPTED &
2589
                                          Q(is_pending=True)))
2590
        if not memberships:
2591
            project.is_modified = False
2592
            project.save()
2593

    
2594
    reactivating = psfu.reactivating_projects()
2595
    for project in reactivating:
2596
        objects = project.projectmembership_set.select_for_update()
2597
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2598
                                          Q(is_pending=True)))
2599
        if not memberships:
2600
            project.reactivate()
2601
            project.save()
2602

    
2603
    deactivating = psfu.deactivating_projects()
2604
    for project in deactivating:
2605
        objects = project.projectmembership_set.select_for_update()
2606

    
2607
        memberships = list(objects.filter(Q_ACTUALLY_ACCEPTED |
2608
                                          Q(is_pending=True)))
2609
        if not memberships:
2610
            project.deactivate()
2611
            project.save()
2612

    
2613
    transaction.commit()
2614

    
2615
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2616
    @with_lock(retries, retry_wait)
2617
    def _sync_projects(sync):
2618
        sync_finish_serials()
2619
        # Informative only -- no select_for_update()
2620
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2621

    
2622
        projects_log = pre_sync_projects(sync)
2623
        if sync:
2624
            serial = do_sync_projects()
2625
            sync_finish_serials([serial])
2626
            post_sync_projects()
2627

    
2628
        return (pending, projects_log)
2629
    return _sync_projects(sync)
2630

    
2631
def all_users_quotas(users):
2632
    quotas = {}
2633
    for user in users:
2634
        quotas[user.uuid] = user.all_quotas()
2635
    return quotas
2636

    
2637
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2638
    @with_lock(retries, retry_wait)
2639
    def _sync_users(users, sync):
2640
        sync_finish_serials()
2641

    
2642
        existing, nonexisting = qh_check_users(users)
2643
        resources = get_resource_names()
2644
        registered_quotas = qh_get_quota_limits(existing, resources)
2645
        astakos_quotas = all_users_quotas(users)
2646

    
2647
        if sync:
2648
            r = register_users(nonexisting)
2649
            r = send_quotas(astakos_quotas)
2650

    
2651
        return (existing, nonexisting, registered_quotas, astakos_quotas)
2652
    return _sync_users(users, sync)
2653

    
2654
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2655
    users = AstakosUser.objects.verified()
2656
    return sync_users(users, sync, retries, retry_wait)
2657

    
2658
class ProjectMembershipHistory(models.Model):
2659
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2660
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2661

    
2662
    person  =   models.BigIntegerField()
2663
    project =   models.BigIntegerField()
2664
    date    =   models.DateField(auto_now_add=True)
2665
    reason  =   models.IntegerField()
2666
    serial  =   models.BigIntegerField()
2667

    
2668
### SIGNALS ###
2669
################
2670

    
2671
def create_astakos_user(u):
2672
    try:
2673
        AstakosUser.objects.get(user_ptr=u.pk)
2674
    except AstakosUser.DoesNotExist:
2675
        extended_user = AstakosUser(user_ptr_id=u.pk)
2676
        extended_user.__dict__.update(u.__dict__)
2677
        extended_user.save()
2678
        if not extended_user.has_auth_provider('local'):
2679
            extended_user.add_auth_provider('local')
2680
    except BaseException, e:
2681
        logger.exception(e)
2682

    
2683
def fix_superusers():
2684
    # Associate superusers with AstakosUser
2685
    admins = User.objects.filter(is_superuser=True)
2686
    for u in admins:
2687
        create_astakos_user(u)
2688

    
2689
def user_post_save(sender, instance, created, **kwargs):
2690
    if not created:
2691
        return
2692
    create_astakos_user(instance)
2693
post_save.connect(user_post_save, sender=User)
2694

    
2695
def astakosuser_post_save(sender, instance, created, **kwargs):
2696
    pass
2697

    
2698
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2699

    
2700
def resource_post_save(sender, instance, created, **kwargs):
2701
    pass
2702

    
2703
post_save.connect(resource_post_save, sender=Resource)
2704

    
2705
def renew_token(sender, instance, **kwargs):
2706
    if not instance.auth_token:
2707
        instance.renew_token()
2708
pre_save.connect(renew_token, sender=AstakosUser)
2709
pre_save.connect(renew_token, sender=Service)