Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (70.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

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

    
47
from django.db import models, IntegrityError, transaction, connection
48
from django.contrib.auth.models import User, UserManager, Group, Permission
49
from django.utils.translation import ugettext as _
50
from django.db import transaction
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, BILLING_FIELDS,
70
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
71
    SITENAME, SERVICES, MODERATION_ENABLED)
72
from astakos.im import settings as astakos_settings
73
from astakos.im.endpoints.qh import (
74
    register_users, send_quota, register_resources, qh_add_quota, QuotaLimits,
75
    qh_query_serials, qh_ack_serials)
76
from astakos.im import auth_providers
77
#from astakos.im.endpoints.aquarium.producer import report_user_event
78
#from astakos.im.tasks import propagate_groupmembers_quota
79

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

    
83
logger = logging.getLogger(__name__)
84

    
85
DEFAULT_CONTENT_TYPE = None
86
_content_type = None
87

    
88
PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
89

    
90
def get_content_type():
91
    global _content_type
92
    if _content_type is not None:
93
        return _content_type
94

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

    
102
RESOURCE_SEPARATOR = '.'
103

    
104
inf = float('inf')
105

    
106
class Service(models.Model):
107
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
108
    url = models.FilePathField()
109
    icon = models.FilePathField(blank=True)
110
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
111
                                  null=True, blank=True)
112
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
113
    auth_token_expires = models.DateTimeField(
114
        _('Token expiration date'), null=True)
115

    
116
    def renew_token(self):
117
        md5 = hashlib.md5()
118
        md5.update(self.name.encode('ascii', 'ignore'))
119
        md5.update(self.url.encode('ascii', 'ignore'))
120
        md5.update(asctime())
121

    
122
        self.auth_token = b64encode(md5.digest())
123
        self.auth_token_created = datetime.now()
124
        self.auth_token_expires = self.auth_token_created + \
125
            timedelta(hours=AUTH_TOKEN_DURATION)
126

    
127
    def __str__(self):
128
        return self.name
129

    
130
    @property
131
    def resources(self):
132
        return self.resource_set.all()
133

    
134
    @resources.setter
135
    def resources(self, resources):
136
        for s in resources:
137
            self.resource_set.create(**s)
138

    
139
    def add_resource(self, service, resource, uplimit, update=True):
140
        """Raises ObjectDoesNotExist, IntegrityError"""
141
        resource = Resource.objects.get(service__name=service, name=resource)
142
        if update:
143
            AstakosUserQuota.objects.update_or_create(user=self,
144
                                                      resource=resource,
145
                                                      defaults={'uplimit': uplimit})
146
        else:
147
            q = self.astakosuserquota_set
148
            q.create(resource=resource, uplimit=uplimit)
149

    
150

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

    
155

    
156
class Resource(models.Model):
157
    name = models.CharField(_('Name'), max_length=255)
158
    meta = models.ManyToManyField(ResourceMetadata)
159
    service = models.ForeignKey(Service)
160
    desc = models.TextField(_('Description'), null=True)
161
    unit = models.CharField(_('Name'), null=True, max_length=255)
162
    group = models.CharField(_('Group'), null=True, max_length=255)
163

    
164
    class Meta:
165
        unique_together = ("name", "service")
166

    
167
    def __str__(self):
168
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
169

    
170

    
171
class GroupKind(models.Model):
172
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
173

    
174
    def __str__(self):
175
        return self.name
176

    
177

    
178
class AstakosGroup(Group):
179
    kind = models.ForeignKey(GroupKind)
180
    homepage = models.URLField(
181
        _('Homepage Url'), max_length=255, null=True, blank=True)
182
    desc = models.TextField(_('Description'), null=True)
183
    policy = models.ManyToManyField(
184
        Resource,
185
        null=True,
186
        blank=True,
187
        through='AstakosGroupQuota'
188
    )
189
    creation_date = models.DateTimeField(
190
        _('Creation date'),
191
        default=datetime.now()
192
    )
193
    issue_date = models.DateTimeField(
194
        _('Start date'),
195
        null=True
196
    )
197
    expiration_date = models.DateTimeField(
198
        _('Expiration date'),
199
        null=True
200
    )
201
    moderation_enabled = models.BooleanField(
202
        _('Moderated membership?'),
203
        default=True
204
    )
205
    approval_date = models.DateTimeField(
206
        _('Activation date'),
207
        null=True,
208
        blank=True
209
    )
210
    estimated_participants = models.PositiveIntegerField(
211
        _('Estimated #members'),
212
        null=True,
213
        blank=True,
214
    )
215
    max_participants = models.PositiveIntegerField(
216
        _('Maximum numder of participants'),
217
        null=True,
218
        blank=True
219
    )
220

    
221
    @property
222
    def is_disabled(self):
223
        if not self.approval_date:
224
            return True
225
        return False
226

    
227
    @property
228
    def is_enabled(self):
229
        if self.is_disabled:
230
            return False
231
        if not self.issue_date:
232
            return False
233
        if not self.expiration_date:
234
            return True
235
        now = datetime.now()
236
        if self.issue_date > now:
237
            return False
238
        if now >= self.expiration_date:
239
            return False
240
        return True
241

    
242
    def enable(self):
243
        if self.is_enabled:
244
            return
245
        self.approval_date = datetime.now()
246
        self.save()
247
        quota_disturbed.send(sender=self, users=self.approved_members)
248
        #propagate_groupmembers_quota.apply_async(
249
        #    args=[self], eta=self.issue_date)
250
        #propagate_groupmembers_quota.apply_async(
251
        #    args=[self], eta=self.expiration_date)
252

    
253
    def disable(self):
254
        if self.is_disabled:
255
            return
256
        self.approval_date = None
257
        self.save()
258
        quota_disturbed.send(sender=self, users=self.approved_members)
259

    
260
    def approve_member(self, person):
261
        m, created = self.membership_set.get_or_create(person=person)
262
        m.approve()
263

    
264
    @property
265
    def members(self):
266
        q = self.membership_set.select_related().all()
267
        return [m.person for m in q]
268

    
269
    @property
270
    def approved_members(self):
271
        q = self.membership_set.select_related().all()
272
        return [m.person for m in q if m.is_approved]
273

    
274
    @property
275
    def quota(self):
276
        d = defaultdict(int)
277
        for q in self.astakosgroupquota_set.select_related().all():
278
            d[q.resource] += q.uplimit or inf
279
        return d
280

    
281
    def add_policy(self, service, resource, uplimit, update=True):
282
        """Raises ObjectDoesNotExist, IntegrityError"""
283
        resource = Resource.objects.get(service__name=service, name=resource)
284
        if update:
285
            AstakosGroupQuota.objects.update_or_create(
286
                group=self,
287
                resource=resource,
288
                defaults={'uplimit': uplimit}
289
            )
290
        else:
291
            q = self.astakosgroupquota_set
292
            q.create(resource=resource, uplimit=uplimit)
293

    
294
    @property
295
    def policies(self):
296
        return self.astakosgroupquota_set.select_related().all()
297

    
298
    @policies.setter
299
    def policies(self, policies):
300
        for p in policies:
301
            service = p.get('service', None)
302
            resource = p.get('resource', None)
303
            uplimit = p.get('uplimit', 0)
304
            update = p.get('update', True)
305
            self.add_policy(service, resource, uplimit, update)
306

    
307
    @property
308
    def owners(self):
309
        return self.owner.all()
310

    
311
    @property
312
    def owner_details(self):
313
        return self.owner.select_related().all()
314

    
315
    @owners.setter
316
    def owners(self, l):
317
        self.owner = l
318
        map(self.approve_member, l)
319

    
320
_default_quota = {}
321
def get_default_quota():
322
    global _default_quota
323
    if _default_quota:
324
        return _default_quota
325
    for s, data in SERVICES.iteritems():
326
        map(
327
            lambda d:_default_quota.update(
328
                {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
329
            ),
330
            data.get('resources', {})
331
        )
332
    return _default_quota
333

    
334
class AstakosUserManager(UserManager):
335

    
336
    def get_auth_provider_user(self, provider, **kwargs):
337
        """
338
        Retrieve AstakosUser instance associated with the specified third party
339
        id.
340
        """
341
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
342
                          kwargs.iteritems()))
343
        return self.get(auth_providers__module=provider, **kwargs)
344

    
345
    def get_by_email(self, email):
346
        return self.get(email=email)
347

    
348
class AstakosUser(User):
349
    """
350
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
351
    """
352
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
353
                                   null=True)
354

    
355
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
356
    #                    AstakosUserProvider model.
357
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
358
                                null=True)
359
    # ex. screen_name for twitter, eppn for shibboleth
360
    third_party_identifier = models.CharField(_('Third-party identifier'),
361
                                              max_length=255, null=True,
362
                                              blank=True)
363

    
364

    
365
    #for invitations
366
    user_level = DEFAULT_USER_LEVEL
367
    level = models.IntegerField(_('Inviter level'), default=user_level)
368
    invitations = models.IntegerField(
369
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
370

    
371
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
372
                                  null=True, blank=True)
373
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
374
    auth_token_expires = models.DateTimeField(
375
        _('Token expiration date'), null=True)
376

    
377
    updated = models.DateTimeField(_('Update date'))
378
    is_verified = models.BooleanField(_('Is verified?'), default=False)
379

    
380
    email_verified = models.BooleanField(_('Email verified?'), default=False)
381

    
382
    has_credits = models.BooleanField(_('Has credits?'), default=False)
383
    has_signed_terms = models.BooleanField(
384
        _('I agree with the terms'), default=False)
385
    date_signed_terms = models.DateTimeField(
386
        _('Signed terms date'), null=True, blank=True)
387

    
388
    activation_sent = models.DateTimeField(
389
        _('Activation sent data'), null=True, blank=True)
390

    
391
    policy = models.ManyToManyField(
392
        Resource, null=True, through='AstakosUserQuota')
393

    
394
    astakos_groups = models.ManyToManyField(
395
        AstakosGroup, verbose_name=_('agroups'), blank=True,
396
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
397
        through='Membership')
398

    
399
    __has_signed_terms = False
400
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
401
                                           default=False, db_index=True)
402

    
403
    objects = AstakosUserManager()
404

    
405
    owner = models.ManyToManyField(
406
        AstakosGroup, related_name='owner', null=True)
407

    
408
    def __init__(self, *args, **kwargs):
409
        super(AstakosUser, self).__init__(*args, **kwargs)
410
        self.__has_signed_terms = self.has_signed_terms
411
        if not self.id:
412
            self.is_active = False
413

    
414
    @property
415
    def realname(self):
416
        return '%s %s' % (self.first_name, self.last_name)
417

    
418
    @realname.setter
419
    def realname(self, value):
420
        parts = value.split(' ')
421
        if len(parts) == 2:
422
            self.first_name = parts[0]
423
            self.last_name = parts[1]
424
        else:
425
            self.last_name = parts[0]
426

    
427
    def add_permission(self, pname):
428
        if self.has_perm(pname):
429
            return
430
        p, created = Permission.objects.get_or_create(
431
                                    codename=pname,
432
                                    name=pname.capitalize(),
433
                                    content_type=get_content_type())
434
        self.user_permissions.add(p)
435

    
436
    def remove_permission(self, pname):
437
        if self.has_perm(pname):
438
            return
439
        p = Permission.objects.get(codename=pname,
440
                                   content_type=get_content_type())
441
        self.user_permissions.remove(p)
442

    
443
    @property
444
    def invitation(self):
445
        try:
446
            return Invitation.objects.get(username=self.email)
447
        except Invitation.DoesNotExist:
448
            return None
449

    
450
    @property
451
    def quota(self):
452
        """Returns a dict with the sum of quota limits per resource"""
453
        d = defaultdict(int)
454
        default_quota = get_default_quota()
455
        d.update(default_quota)
456
        for q in self.policies:
457
            d[q.resource] += q.uplimit or inf
458
        for m in self.projectmembership_set.select_related().all():
459
            if not m.acceptance_date:
460
                continue
461
            p = m.project
462
            if not p.is_active:
463
                continue
464
            grants = p.application.projectresourcegrant_set.all()
465
            for g in grants:
466
                d[str(g.resource)] += g.member_capacity or inf
467
        # TODO set default for remaining
468
        return d
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
            service = policies.get('service', None)
478
            resource = policies.get('resource', None)
479
            uplimit = policies.get('uplimit', 0)
480
            update = policies.get('update', True)
481
            self.add_policy(service, resource, uplimit, update)
482

    
483
    def add_policy(self, service, resource, uplimit, update=True):
484
        """Raises ObjectDoesNotExist, IntegrityError"""
485
        resource = Resource.objects.get(service__name=service, name=resource)
486
        if update:
487
            AstakosUserQuota.objects.update_or_create(user=self,
488
                                                      resource=resource,
489
                                                      defaults={'uplimit': uplimit})
490
        else:
491
            q = self.astakosuserquota_set
492
            q.create(resource=resource, uplimit=uplimit)
493

    
494
    def remove_policy(self, service, resource):
495
        """Raises ObjectDoesNotExist, IntegrityError"""
496
        resource = Resource.objects.get(service__name=service, name=resource)
497
        q = self.policies.get(resource=resource).delete()
498

    
499
    @property
500
    def extended_groups(self):
501
        return self.membership_set.select_related().all()
502

    
503
    @extended_groups.setter
504
    def extended_groups(self, groups):
505
        #TODO exceptions
506
        for name in (groups or ()):
507
            group = AstakosGroup.objects.get(name=name)
508
            self.membership_set.create(group=group)
509

    
510
    def save(self, update_timestamps=True, **kwargs):
511
        if update_timestamps:
512
            if not self.id:
513
                self.date_joined = datetime.now()
514
            self.updated = datetime.now()
515

    
516
        # update date_signed_terms if necessary
517
        if self.__has_signed_terms != self.has_signed_terms:
518
            self.date_signed_terms = datetime.now()
519

    
520
        if not self.id:
521
            # set username
522
            self.username = self.email
523

    
524
        self.validate_unique_email_isactive()
525

    
526
        super(AstakosUser, self).save(**kwargs)
527

    
528
    def renew_token(self, flush_sessions=False, current_key=None):
529
        md5 = hashlib.md5()
530
        md5.update(settings.SECRET_KEY)
531
        md5.update(self.username)
532
        md5.update(self.realname.encode('ascii', 'ignore'))
533
        md5.update(asctime())
534

    
535
        self.auth_token = b64encode(md5.digest())
536
        self.auth_token_created = datetime.now()
537
        self.auth_token_expires = self.auth_token_created + \
538
                                  timedelta(hours=AUTH_TOKEN_DURATION)
539
        if flush_sessions:
540
            self.flush_sessions(current_key)
541
        msg = 'Token renewed for %s' % self.email
542
        logger.log(LOGGING_LEVEL, msg)
543

    
544
    def flush_sessions(self, current_key=None):
545
        q = self.sessions
546
        if current_key:
547
            q = q.exclude(session_key=current_key)
548

    
549
        keys = q.values_list('session_key', flat=True)
550
        if keys:
551
            msg = 'Flushing sessions: %s' % ','.join(keys)
552
            logger.log(LOGGING_LEVEL, msg, [])
553
        engine = import_module(settings.SESSION_ENGINE)
554
        for k in keys:
555
            s = engine.SessionStore(k)
556
            s.flush()
557

    
558
    def __unicode__(self):
559
        return '%s (%s)' % (self.realname, self.email)
560

    
561
    def conflicting_email(self):
562
        q = AstakosUser.objects.exclude(username=self.username)
563
        q = q.filter(email__iexact=self.email)
564
        if q.count() != 0:
565
            return True
566
        return False
567

    
568
    def validate_unique_email_isactive(self):
569
        """
570
        Implements a unique_together constraint for email and is_active fields.
571
        """
572
        q = AstakosUser.objects.all()
573
        q = q.filter(email = self.email)
574
        if self.id:
575
            q = q.filter(~Q(id = self.id))
576
        if q.count() != 0:
577
            m = 'Another account with the same email = %(email)s & \
578
                is_active = %(is_active)s found.' % self.__dict__
579
            raise ValidationError(m)
580

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

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

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

    
611
    def can_add_auth_provider(self, provider, **kwargs):
612
        provider_settings = auth_providers.get_provider(provider)
613
        if not provider_settings.is_available_for_login():
614
            return False
615

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

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

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

    
633
        return True
634

    
635
    def can_remove_auth_provider(self, provider):
636
        if len(self.get_active_auth_providers()) <= 1:
637
            return False
638
        return True
639

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

    
643
    def has_auth_provider(self, provider, **kwargs):
644
        return bool(self.auth_providers.filter(module=provider,
645
                                               **kwargs).count())
646

    
647
    def add_auth_provider(self, provider, **kwargs):
648
        info_data = ''
649
        if 'provider_info' in kwargs:
650
            info_data = kwargs.pop('provider_info')
651
            if isinstance(info_data, dict):
652
                info_data = json.dumps(info_data)
653

    
654
        if self.can_add_auth_provider(provider, **kwargs):
655
            self.auth_providers.create(module=provider, active=True,
656
                                       info_data=info_data,
657
                                       **kwargs)
658
        else:
659
            raise Exception('Cannot add provider')
660

    
661
    def add_pending_auth_provider(self, pending):
662
        """
663
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
664
        the current user.
665
        """
666
        if not isinstance(pending, PendingThirdPartyUser):
667
            pending = PendingThirdPartyUser.objects.get(token=pending)
668

    
669
        provider = self.add_auth_provider(pending.provider,
670
                               identifier=pending.third_party_identifier,
671
                                affiliation=pending.affiliation,
672
                                          provider_info=pending.info)
673

    
674
        if email_re.match(pending.email or '') and pending.email != self.email:
675
            self.additionalmail_set.get_or_create(email=pending.email)
676

    
677
        pending.delete()
678
        return provider
679

    
680
    def remove_auth_provider(self, provider, **kwargs):
681
        self.auth_providers.get(module=provider, **kwargs).delete()
682

    
683
    # user urls
684
    def get_resend_activation_url(self):
685
        return reverse('send_activation', kwargs={'user_id': self.pk})
686

    
687
    def get_provider_remove_url(self, module, **kwargs):
688
        return reverse('remove_auth_provider', kwargs={
689
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
690

    
691
    def get_activation_url(self, nxt=False):
692
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
693
                                 quote(self.auth_token))
694
        if nxt:
695
            url += "&next=%s" % quote(nxt)
696
        return url
697

    
698
    def get_password_reset_url(self, token_generator=default_token_generator):
699
        return reverse('django.contrib.auth.views.password_reset_confirm',
700
                          kwargs={'uidb36':int_to_base36(self.id),
701
                                  'token':token_generator.make_token(self)})
702

    
703
    def get_auth_providers(self):
704
        return self.auth_providers.all()
705

    
706
    def get_available_auth_providers(self):
707
        """
708
        Returns a list of providers available for user to connect to.
709
        """
710
        providers = []
711
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
712
            if self.can_add_auth_provider(module):
713
                providers.append(provider_settings(self))
714

    
715
        return providers
716

    
717
    def get_active_auth_providers(self):
718
        providers = []
719
        for provider in self.auth_providers.active():
720
            if auth_providers.get_provider(provider.module).is_available_for_login():
721
                providers.append(provider)
722
        return providers
723

    
724
    @property
725
    def auth_providers_display(self):
726
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
727

    
728
    def get_inactive_message(self):
729
        msg_extra = ''
730
        message = ''
731
        if self.activation_sent:
732
            if self.email_verified:
733
                message = _(astakos_messages.ACCOUNT_INACTIVE)
734
            else:
735
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
736
                if MODERATION_ENABLED:
737
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
738
                else:
739
                    url = self.get_resend_activation_url()
740
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
741
                                u' ' + \
742
                                _('<a href="%s">%s?</a>') % (url,
743
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
744
        else:
745
            if MODERATION_ENABLED:
746
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
747
            else:
748
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
749
                url = self.get_resend_activation_url()
750
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
751
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
752

    
753
        return mark_safe(message + u' '+ msg_extra)
754

    
755

    
756
class AstakosUserAuthProviderManager(models.Manager):
757

    
758
    def active(self):
759
        return self.filter(active=True)
760

    
761

    
762
class AstakosUserAuthProvider(models.Model):
763
    """
764
    Available user authentication methods.
765
    """
766
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
767
                                   null=True, default=None)
768
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
769
    module = models.CharField(_('Provider'), max_length=255, blank=False,
770
                                default='local')
771
    identifier = models.CharField(_('Third-party identifier'),
772
                                              max_length=255, null=True,
773
                                              blank=True)
774
    active = models.BooleanField(default=True)
775
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
776
                                   default='astakos')
777
    info_data = models.TextField(default="", null=True, blank=True)
778
    created = models.DateTimeField('Creation date', auto_now_add=True)
779

    
780
    objects = AstakosUserAuthProviderManager()
781

    
782
    class Meta:
783
        unique_together = (('identifier', 'module', 'user'), )
784
        ordering = ('module', 'created')
785

    
786
    def __init__(self, *args, **kwargs):
787
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
788
        try:
789
            self.info = json.loads(self.info_data)
790
            if not self.info:
791
                self.info = {}
792
        except Exception, e:
793
            self.info = {}
794

    
795
        for key,value in self.info.iteritems():
796
            setattr(self, 'info_%s' % key, value)
797

    
798

    
799
    @property
800
    def settings(self):
801
        return auth_providers.get_provider(self.module)
802

    
803
    @property
804
    def details_display(self):
805
        try:
806
          return self.settings.get_details_tpl_display % self.__dict__
807
        except:
808
          return ''
809

    
810
    @property
811
    def title_display(self):
812
        title_tpl = self.settings.get_title_display
813
        try:
814
            if self.settings.get_user_title_display:
815
                title_tpl = self.settings.get_user_title_display
816
        except Exception, e:
817
            pass
818
        try:
819
          return title_tpl % self.__dict__
820
        except:
821
          return self.settings.get_title_display % self.__dict__
822

    
823
    def can_remove(self):
824
        return self.user.can_remove_auth_provider(self.module)
825

    
826
    def delete(self, *args, **kwargs):
827
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
828
        if self.module == 'local':
829
            self.user.set_unusable_password()
830
            self.user.save()
831
        return ret
832

    
833
    def __repr__(self):
834
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
835

    
836
    def __unicode__(self):
837
        if self.identifier:
838
            return "%s:%s" % (self.module, self.identifier)
839
        if self.auth_backend:
840
            return "%s:%s" % (self.module, self.auth_backend)
841
        return self.module
842

    
843
    def save(self, *args, **kwargs):
844
        self.info_data = json.dumps(self.info)
845
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
846

    
847

    
848
class Membership(models.Model):
849
    person = models.ForeignKey(AstakosUser)
850
    group = models.ForeignKey(AstakosGroup)
851
    date_requested = models.DateField(default=datetime.now(), blank=True)
852
    date_joined = models.DateField(null=True, db_index=True, blank=True)
853

    
854
    class Meta:
855
        unique_together = ("person", "group")
856

    
857
    def save(self, *args, **kwargs):
858
        if not self.id:
859
            if not self.group.moderation_enabled:
860
                self.date_joined = datetime.now()
861
        super(Membership, self).save(*args, **kwargs)
862

    
863
    @property
864
    def is_approved(self):
865
        if self.date_joined:
866
            return True
867
        return False
868

    
869
    def approve(self):
870
        if self.is_approved:
871
            return
872
        if self.group.max_participants:
873
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
874
            'Maximum participant number has been reached.'
875
        self.date_joined = datetime.now()
876
        self.save()
877
        quota_disturbed.send(sender=self, users=(self.person,))
878

    
879
    def disapprove(self):
880
        approved = self.is_approved()
881
        self.delete()
882
        if approved:
883
            quota_disturbed.send(sender=self, users=(self.person,))
884

    
885
class ExtendedManager(models.Manager):
886
    def _update_or_create(self, **kwargs):
887
        assert kwargs, \
888
            'update_or_create() must be passed at least one keyword argument'
889
        obj, created = self.get_or_create(**kwargs)
890
        defaults = kwargs.pop('defaults', {})
891
        if created:
892
            return obj, True, False
893
        else:
894
            try:
895
                params = dict(
896
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
897
                params.update(defaults)
898
                for attr, val in params.items():
899
                    if hasattr(obj, attr):
900
                        setattr(obj, attr, val)
901
                sid = transaction.savepoint()
902
                obj.save(force_update=True)
903
                transaction.savepoint_commit(sid)
904
                return obj, False, True
905
            except IntegrityError, e:
906
                transaction.savepoint_rollback(sid)
907
                try:
908
                    return self.get(**kwargs), False, False
909
                except self.model.DoesNotExist:
910
                    raise e
911

    
912
    update_or_create = _update_or_create
913

    
914
class AstakosGroupQuota(models.Model):
915
    objects = ExtendedManager()
916
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
917
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
918
    resource = models.ForeignKey(Resource)
919
    group = models.ForeignKey(AstakosGroup, blank=True)
920

    
921
    class Meta:
922
        unique_together = ("resource", "group")
923

    
924
class AstakosUserQuota(models.Model):
925
    objects = ExtendedManager()
926
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
927
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
928
    resource = models.ForeignKey(Resource)
929
    user = models.ForeignKey(AstakosUser)
930

    
931
    class Meta:
932
        unique_together = ("resource", "user")
933

    
934

    
935
class ApprovalTerms(models.Model):
936
    """
937
    Model for approval terms
938
    """
939

    
940
    date = models.DateTimeField(
941
        _('Issue date'), db_index=True, default=datetime.now())
942
    location = models.CharField(_('Terms location'), max_length=255)
943

    
944

    
945
class Invitation(models.Model):
946
    """
947
    Model for registring invitations
948
    """
949
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
950
                                null=True)
951
    realname = models.CharField(_('Real name'), max_length=255)
952
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
953
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
954
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
955
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
956
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
957

    
958
    def __init__(self, *args, **kwargs):
959
        super(Invitation, self).__init__(*args, **kwargs)
960
        if not self.id:
961
            self.code = _generate_invitation_code()
962

    
963
    def consume(self):
964
        self.is_consumed = True
965
        self.consumed = datetime.now()
966
        self.save()
967

    
968
    def __unicode__(self):
969
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
970

    
971

    
972
class EmailChangeManager(models.Manager):
973
    @transaction.commit_on_success
974
    def change_email(self, activation_key):
975
        """
976
        Validate an activation key and change the corresponding
977
        ``User`` if valid.
978

979
        If the key is valid and has not expired, return the ``User``
980
        after activating.
981

982
        If the key is not valid or has expired, return ``None``.
983

984
        If the key is valid but the ``User`` is already active,
985
        return ``None``.
986

987
        After successful email change the activation record is deleted.
988

989
        Throws ValueError if there is already
990
        """
991
        try:
992
            email_change = self.model.objects.get(
993
                activation_key=activation_key)
994
            if email_change.activation_key_expired():
995
                email_change.delete()
996
                raise EmailChange.DoesNotExist
997
            # is there an active user with this address?
998
            try:
999
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1000
            except AstakosUser.DoesNotExist:
1001
                pass
1002
            else:
1003
                raise ValueError(_('The new email address is reserved.'))
1004
            # update user
1005
            user = AstakosUser.objects.get(pk=email_change.user_id)
1006
            user.email = email_change.new_email_address
1007
            user.save()
1008
            email_change.delete()
1009
            return user
1010
        except EmailChange.DoesNotExist:
1011
            raise ValueError(_('Invalid activation key.'))
1012

    
1013

    
1014
class EmailChange(models.Model):
1015
    new_email_address = models.EmailField(
1016
        _(u'new e-mail address'),
1017
        help_text=_('Your old email address will be used until you verify your new one.'))
1018
    user = models.ForeignKey(
1019
        AstakosUser, unique=True, related_name='emailchange_user')
1020
    requested_at = models.DateTimeField(default=datetime.now())
1021
    activation_key = models.CharField(
1022
        max_length=40, unique=True, db_index=True)
1023

    
1024
    objects = EmailChangeManager()
1025

    
1026
    def activation_key_expired(self):
1027
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1028
        return self.requested_at + expiration_date < datetime.now()
1029

    
1030

    
1031
class AdditionalMail(models.Model):
1032
    """
1033
    Model for registring invitations
1034
    """
1035
    owner = models.ForeignKey(AstakosUser)
1036
    email = models.EmailField()
1037

    
1038

    
1039
def _generate_invitation_code():
1040
    while True:
1041
        code = randint(1, 2L ** 63 - 1)
1042
        try:
1043
            Invitation.objects.get(code=code)
1044
            # An invitation with this code already exists, try again
1045
        except Invitation.DoesNotExist:
1046
            return code
1047

    
1048

    
1049
def get_latest_terms():
1050
    try:
1051
        term = ApprovalTerms.objects.order_by('-id')[0]
1052
        return term
1053
    except IndexError:
1054
        pass
1055
    return None
1056

    
1057
class PendingThirdPartyUser(models.Model):
1058
    """
1059
    Model for registring successful third party user authentications
1060
    """
1061
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1062
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1063
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1064
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
1065
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
1066
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
1067
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1068
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1069
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1070
    info = models.TextField(default="", null=True, blank=True)
1071

    
1072
    class Meta:
1073
        unique_together = ("provider", "third_party_identifier")
1074

    
1075
    def get_user_instance(self):
1076
        d = self.__dict__
1077
        d.pop('_state', None)
1078
        d.pop('id', None)
1079
        d.pop('token', None)
1080
        d.pop('created', None)
1081
        d.pop('info', None)
1082
        user = AstakosUser(**d)
1083

    
1084
        return user
1085

    
1086
    @property
1087
    def realname(self):
1088
        return '%s %s' %(self.first_name, self.last_name)
1089

    
1090
    @realname.setter
1091
    def realname(self, value):
1092
        parts = value.split(' ')
1093
        if len(parts) == 2:
1094
            self.first_name = parts[0]
1095
            self.last_name = parts[1]
1096
        else:
1097
            self.last_name = parts[0]
1098

    
1099
    def save(self, **kwargs):
1100
        if not self.id:
1101
            # set username
1102
            while not self.username:
1103
                username =  uuid.uuid4().hex[:30]
1104
                try:
1105
                    AstakosUser.objects.get(username = username)
1106
                except AstakosUser.DoesNotExist, e:
1107
                    self.username = username
1108
        super(PendingThirdPartyUser, self).save(**kwargs)
1109

    
1110
    def generate_token(self):
1111
        self.password = self.third_party_identifier
1112
        self.last_login = datetime.now()
1113
        self.token = default_token_generator.make_token(self)
1114

    
1115
class SessionCatalog(models.Model):
1116
    session_key = models.CharField(_('session key'), max_length=40)
1117
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1118

    
1119
class MemberJoinPolicy(models.Model):
1120
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1121
    description = models.CharField(_('Description'), max_length=80)
1122

    
1123
    def __str__(self):
1124
        return self.policy
1125

    
1126
class MemberLeavePolicy(models.Model):
1127
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1128
    description = models.CharField(_('Description'), max_length=80)
1129

    
1130
    def __str__(self):
1131
        return self.policy
1132

    
1133
_auto_accept_join = False
1134
def get_auto_accept_join():
1135
    global _auto_accept_join
1136
    if _auto_accept_join is not False:
1137
        return _auto_accept_join
1138
    try:
1139
        auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
1140
    except:
1141
        auto_accept = None
1142
    _auto_accept_join = auto_accept
1143
    return auto_accept
1144

    
1145
_closed_join = False
1146
def get_closed_join():
1147
    global _closed_join
1148
    if _closed_join is not False:
1149
        return _closed_join
1150
    try:
1151
        closed = MemberJoinPolicy.objects.get(policy='closed')
1152
    except:
1153
        closed = None
1154
    _closed_join = closed
1155
    return closed
1156

    
1157
_auto_accept_leave = False
1158
def get_auto_accept_leave():
1159
    global _auto_accept_leave
1160
    if _auto_accept_leave is not False:
1161
        return _auto_accept_leave
1162
    try:
1163
        auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
1164
    except:
1165
        auto_accept = None
1166
    _auto_accept_leave = auto_accept
1167
    return auto_accept
1168

    
1169
_closed_leave = False
1170
def get_closed_leave():
1171
    global _closed_leave
1172
    if _closed_leave is not False:
1173
        return _closed_leave
1174
    try:
1175
        closed = MemberLeavePolicy.objects.get(policy='closed')
1176
    except:
1177
        closed = None
1178
    _closed_leave = closed
1179
    return closeds
1180

    
1181

    
1182
### PROJECTS ###
1183
################
1184

    
1185

    
1186
def synced_model_metaclass(class_name, class_parents, class_attributes):
1187

    
1188
    new_attributes = {}
1189
    sync_attributes = {}
1190

    
1191
    for name, value in class_attributes.iteritems():
1192
        sync, underscore, rest = name.partition('_')
1193
        if sync == 'sync' and underscore == '_':
1194
            sync_attributes[rest] = value
1195
        else:
1196
            new_attributes[name] = value
1197

    
1198
    if 'prefix' not in sync_attributes:
1199
        m = ("you did not specify a 'sync_prefix' attribute "
1200
             "in class '%s'" % (class_name,))
1201
        raise ValueError(m)
1202

    
1203
    prefix = sync_attributes.pop('prefix')
1204
    class_name = sync_attributes.pop('classname', prefix + '_model')
1205

    
1206
    for name, value in sync_attributes.iteritems():
1207
        newname = prefix + '_' + name
1208
        if newname in new_attributes:
1209
            m = ("class '%s' was specified with prefix '%s' "
1210
                 "but it already has an attribute named '%s'"
1211
                 % (class_name, prefix, newname))
1212
            raise ValueError(m)
1213

    
1214
        new_attributes[newname] = value
1215

    
1216
    newclass = type(class_name, class_parents, new_attributes)
1217
    return newclass
1218

    
1219

    
1220
def make_synced(prefix='sync', name='SyncedState'):
1221

    
1222
    the_name = name
1223
    the_prefix = prefix
1224

    
1225
    class SyncedState(models.Model):
1226

    
1227
        sync_classname      = the_name
1228
        sync_prefix         = the_prefix
1229
        __metaclass__       = synced_model_metaclass
1230

    
1231
        sync_new_state      = models.BigIntegerField(null=True)
1232
        sync_synced_state   = models.BigIntegerField(null=True)
1233
        STATUS_SYNCED       = 0
1234
        STATUS_PENDING      = 1
1235
        sync_status         = models.IntegerField(db_index=True)
1236

    
1237
        class Meta:
1238
            abstract = True
1239

    
1240
        class NotSynced(Exception):
1241
            pass
1242

    
1243
        def sync_init_state(self, state):
1244
            self.sync_synced_state = state
1245
            self.sync_new_state = state
1246
            self.sync_status = self.STATUS_SYNCED
1247

    
1248
        def sync_get_status(self):
1249
            return self.sync_status
1250

    
1251
        def sync_set_status(self):
1252
            if self.sync_new_state != self.sync_synced_state:
1253
                self.sync_status = self.STATUS_PENDING
1254
            else:
1255
                self.sync_status = self.STATUS_SYNCED
1256

    
1257
        def sync_set_synced(self):
1258
            self.sync_synced_state = self.sync_new_state
1259
            self.sync_status = self.STATUS_SYNCED
1260

    
1261
        def sync_get_synced_state(self):
1262
            return self.sync_synced_state
1263

    
1264
        def sync_set_new_state(self, new_state):
1265
            self.sync_new_state = new_state
1266
            self.sync_set_status()
1267

    
1268
        def sync_get_new_state(self):
1269
            return self.sync_new_state
1270

    
1271
        def sync_set_synced_state(self, synced_state):
1272
            self.sync_synced_state = synced_state
1273
            self.sync_set_status()
1274

    
1275
        def sync_get_pending_objects(self):
1276
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1277
            return self.objects.filter(**kw)
1278

    
1279
        def sync_get_synced_objects(self):
1280
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1281
            return self.objects.filter(**kw)
1282

    
1283
        def sync_verify_get_synced_state(self):
1284
            status = self.sync_get_status()
1285
            state = self.sync_get_synced_state()
1286
            verified = (status == self.STATUS_SYNCED)
1287
            return state, verified
1288

    
1289
        def sync_is_synced(self):
1290
            state, verified = self.sync_verify_get_synced_state()
1291
            return verified
1292

    
1293
    return SyncedState
1294

    
1295
SyncedState = make_synced(prefix='sync', name='SyncedState')
1296

    
1297

    
1298
class ProjectApplication(models.Model):
1299

    
1300
    applicant               =   models.ForeignKey(
1301
                                    AstakosUser,
1302
                                    related_name='projects_applied',
1303
                                    db_index=True)
1304

    
1305
    project                 =   models.ForeignKey(Project,
1306
                                                  related_name='applications')
1307

    
1308
    state                   =   models.CharField(max_length=80,
1309
                                                 default=UNKNOWN)
1310

    
1311
    owner                   =   models.ForeignKey(
1312
                                    AstakosUser,
1313
                                    related_name='projects_owned',
1314
                                    db_index=True)
1315

    
1316
    precursor_application   =   models.OneToOneField('ProjectApplication',
1317
                                                     null=True,
1318
                                                     blank=True,
1319
                                                     db_index=True)
1320

    
1321
    name                    =   models.CharField(max_length=80)
1322
    homepage                =   models.URLField(max_length=255, null=True,
1323
                                                blank=True)
1324
    description             =   models.TextField(null=True)
1325
    start_date              =   models.DateTimeField()
1326
    end_date                =   models.DateTimeField()
1327
    member_join_policy      =   models.ForeignKey(MemberJoinPolicy)
1328
    member_leave_policy     =   models.ForeignKey(MemberLeavePolicy)
1329
    limit_on_members_number =   models.PositiveIntegerField(null=True,
1330
                                                            blank=True)
1331
    resource_grants         =   models.ManyToManyField(
1332
                                    Resource,
1333
                                    null=True,
1334
                                    blank=True,
1335
                                    through='ProjectResourceGrant')
1336
    comments                =   models.TextField(null=True, blank=True)
1337
    issue_date              =   models.DateTimeField()
1338

    
1339
    states_list =   [PENDING, APPROVED, REPLACED, UNKNOWN]
1340
    states      =   dict((k, v) for v, k in enumerate(states_list))
1341

    
1342
    def add_resource_policy(self, service, resource, uplimit, update=True):
1343
        """Raises ObjectDoesNotExist, IntegrityError"""
1344
        resource = Resource.objects.get(service__name=service, name=resource)
1345
        if update:
1346
            ProjectResourceGrant.objects.update_or_create(
1347
                project_application=self,
1348
                resource=resource,
1349
                defaults={'member_capacity': uplimit})
1350
        else:
1351
            q = self.projectresourcegrant_set
1352
            q.create(resource=resource, member_capacity=uplimit)
1353

    
1354
    @property
1355
    def resource_policies(self):
1356
        return self.projectresourcegrant_set.all()
1357

    
1358
    @resource_policies.setter
1359
    def resource_policies(self, policies):
1360
        for p in policies:
1361
            service = p.get('service', None)
1362
            resource = p.get('resource', None)
1363
            uplimit = p.get('uplimit', 0)
1364
            update = p.get('update', True)
1365
            self.add_resource_policy(service, resource, uplimit, update)
1366

    
1367
    @property
1368
    def follower(self):
1369
        try:
1370
            return ProjectApplication.objects.get(precursor_application=self)
1371
        except ProjectApplication.DoesNotExist:
1372
            return
1373

    
1374
    def submit(self, resource_policies, applicant, comments,
1375
               precursor_application=None):
1376

    
1377
        if precursor_application:
1378
            self.precursor_application = precursor_application
1379
            self.owner = precursor_application.owner
1380
        else:
1381
            self.owner = applicant
1382

    
1383
        self.id = None
1384
        self.applicant = applicant
1385
        self.comments = comments
1386
        self.issue_date = datetime.now()
1387
        self.state = PENDING
1388
        self.resource_policies = resource_policies
1389
        self.save()
1390

    
1391
    def _get_project(self):
1392
        precursor = self
1393
        while precursor:
1394
            try:
1395
                project = precursor.project
1396
                return project
1397
            except Project.DoesNotExist:
1398
                pass
1399
            precursor = precursor.precursor_application
1400

    
1401
        return None
1402

    
1403
    def approve(self, approval_user=None):
1404
        """
1405
        If approval_user then during owner membership acceptance
1406
        it is checked whether the request_user is eligible.
1407

1408
        Raises:
1409
            PermissionDenied
1410
        """
1411

    
1412
        if not transaction.is_managed():
1413
            raise AssertionError("NOPE")
1414

    
1415
        new_project_name = self.name
1416
        if self.state != PENDING:
1417
            m = _("cannot approve: project '%s' in state '%s'") % (
1418
                    new_project_name, self.state)
1419
            raise PermissionDenied(m) # invalid argument
1420

    
1421
        now = datetime.now()
1422
        project = self._get_project()
1423
        if project is None:
1424
            try:
1425
                # needs SERIALIZABLE
1426
                conflicting_project = Project.objects.get(name=new_project_name)
1427
                if conflicting_project.is_alive:
1428
                    m = _("cannot approve: project with name '%s' "
1429
                          "already exists (serial: %s)") % (
1430
                            new_project_name, conflicting_project.id)
1431
                    raise PermissionDenied(m) # invalid argument
1432
            except Project.DoesNotExist:
1433
                pass
1434
            project = Project(creation_date=now)
1435

    
1436
        project.application = self
1437

    
1438
        # This will block while syncing,
1439
        # but unblock before setting the membership state.
1440
        # See ProjectMembership.set_sync()
1441
        project.set_membership_pending_sync()
1442

    
1443
        project.last_approval_date = now
1444
        project.save()
1445
        #ProjectMembership.add_to_project(self)
1446
        project.add_member(self.owner)
1447

    
1448
        precursor = self.precursor_application
1449
        while precursor:
1450
            precursor.state = REPLACED
1451
            precursor.save()
1452
            precursor = precursor.precursor_application
1453

    
1454
        self.state = APPROVED
1455
        self.save()
1456

    
1457
        transaction.commit()
1458
        trigger_sync()
1459

    
1460

    
1461
class ProjectResourceGrant(models.Model):
1462

    
1463
    resource                =   models.ForeignKey(Resource)
1464
    project_application     =   models.ForeignKey(ProjectApplication,
1465
                                                  blank=True)
1466
    project_capacity        =   models.BigIntegerField(null=True)
1467
    project_import_limit    =   models.BigIntegerField(null=True)
1468
    project_export_limit    =   models.BigIntegerField(null=True)
1469
    member_capacity         =   models.BigIntegerField(null=True)
1470
    member_import_limit     =   models.BigIntegerField(null=True)
1471
    member_export_limit     =   models.BigIntegerField(null=True)
1472

    
1473
    objects = ExtendedManager()
1474

    
1475
    class Meta:
1476
        unique_together = ("resource", "project_application")
1477

    
1478

    
1479
class Project(models.Model):
1480

    
1481
    application                 =   models.OneToOneField(
1482
                                            ProjectApplication,
1483
                                            related_name='project')
1484
    last_approval_date          =   models.DateTimeField(null=True)
1485

    
1486
    members                     =   models.ManyToManyField(
1487
                                            AstakosUser,
1488
                                            through='ProjectMembership')
1489

    
1490
    termination_start_date      =   models.DateTimeField(null=True)
1491
    termination_date            =   models.DateTimeField(null=True)
1492

    
1493
    creation_date               =   models.DateTimeField()
1494
    name                        =   models.CharField(
1495
                                            max_length=80,
1496
                                            db_index=True,
1497
                                            unique=True)
1498

    
1499
    @property
1500
    def violated_resource_grants(self):
1501
        return False
1502

    
1503
    @property
1504
    def violated_members_number_limit(self):
1505
        application = self.application
1506
        return len(self.approved_members) > application.limit_on_members_number
1507

    
1508
    @property
1509
    def is_terminated(self):
1510
        return bool(self.termination)
1511

    
1512
    @property
1513
    def is_still_approved(self):
1514
        return bool(self.last_approval_date)
1515

    
1516
    @property
1517
    def is_active(self):
1518
        if (self.is_terminated or
1519
            not self.is_still_approved or
1520
            self.violated_resource_grants):
1521
            return False
1522
#         if self.violated_members_number_limit:
1523
#             return False
1524
        return True
1525
    
1526
    @property
1527
    def is_suspended(self):
1528
        if (self.is_terminated or
1529
            self.is_still_approved or
1530
            not self.violated_resource_grants):
1531
            return False
1532
#             if not self.violated_members_number_limit:
1533
#                 return False
1534
        return True
1535

    
1536
    @property
1537
    def is_alive(self):
1538
        return self.is_active or self.is_suspended
1539

    
1540
    @property
1541
    def is_inconsistent(self):
1542
        now = datetime.now()
1543
        if self.creation_date > now:
1544
            return True
1545
        if self.last_approval_date > now:
1546
            return True
1547
        if self.terminaton_date > now:
1548
            return True
1549
        return False
1550

    
1551
    @property
1552
    def approved_memberships(self):
1553
        ACCEPTED = ProjectMembership.ACCEPTED
1554
        PENDING  = ProjectMembership.PENDING
1555
        return self.projectmembership_set.filter(
1556
            Q(state=ACCEPTED) | Q(state=PENDING))
1557

    
1558
    @property
1559
    def approved_members(self):
1560
        return [m.person for m in self.approved_memberships]
1561

    
1562
    def set_membership_pending_sync(self):
1563
        ACCEPTED = ProjectMembership.ACCEPTED
1564
        PENDING  = ProjectMembership.PENDING
1565
        sfu = self.projectmembership_set.select_for_update()
1566
        members = sfu.filter(Q(state=ACCEPTED) | Q(state=PENDING))
1567

    
1568
        for member in members:
1569
            member.state = member.PENDING
1570
            member.save()
1571

    
1572
    def add_member(self, user):
1573
        """
1574
        Raises:
1575
            django.exceptions.PermissionDenied
1576
            astakos.im.models.AstakosUser.DoesNotExist
1577
        """
1578
        if isinstance(user, int):
1579
            user = AstakosUser.objects.get(user=user)
1580

    
1581
        m, created = ProjectMembership.objects.get_or_create(
1582
            person=user, project=self
1583
        )
1584
        m.accept()
1585

    
1586
    def remove_member(self, user):
1587
        """
1588
        Raises:
1589
            django.exceptions.PermissionDenied
1590
            astakos.im.models.AstakosUser.DoesNotExist
1591
            astakos.im.models.ProjectMembership.DoesNotExist
1592
        """
1593
        if isinstance(user, int):
1594
            user = AstakosUser.objects.get(user=user)
1595

    
1596
        m = ProjectMembership.objects.get(person=user, project=self)
1597
        m.remove()
1598

    
1599
    def terminate(self):
1600
        self.termination_start_date = datetime.now()
1601
        self.terminaton_date = None
1602
        self.save()
1603

    
1604
        rejected = self.sync()
1605
        if not rejected:
1606
            self.termination_start_date = None
1607
            self.termination_date = datetime.now()
1608
            self.save()
1609

    
1610
#         try:
1611
#             notification = build_notification(
1612
#                 settings.SERVER_EMAIL,
1613
#                 [self.current_application.owner.email],
1614
#                 _(PROJECT_TERMINATION_SUBJECT) % self.__dict__,
1615
#                 template='im/projects/project_termination_notification.txt',
1616
#                 dictionary={'object':self.current_application}
1617
#             ).send()
1618
#         except NotificationError, e:
1619
#             logger.error(e.messages)
1620

    
1621
    def suspend(self):
1622
        self.last_approval_date = None
1623
        self.save()
1624
        self.sync()
1625

    
1626
#         try:
1627
#             notification = build_notification(
1628
#                 settings.SERVER_EMAIL,
1629
#                 [self.current_application.owner.email],
1630
#                 _(PROJECT_SUSPENSION_SUBJECT) % self.definition.__dict__,
1631
#                 template='im/projects/project_suspension_notification.txt',
1632
#                 dictionary={'object':self.current_application}
1633
#             ).send()
1634
#         except NotificationError, e:
1635
#             logger.error(e.messages)
1636

    
1637

    
1638

    
1639
class ExclusiveOrRaise(object):
1640
    """Context Manager to exclusively execute a critical code section.
1641
       The exclusion must be global.
1642
       (IPC semaphores will not protect across OS,
1643
        DB locks will if it's the same DB)
1644
    """
1645

    
1646
    class Busy(Exception):
1647
        pass
1648

    
1649
    def __init__(self, locked=False):
1650
        init = 0 if locked else 1
1651
        from multiprocessing import Semaphore
1652
        self._sema = Semaphore(init)
1653

    
1654
    def enter(self):
1655
        acquired = self._sema.acquire(False)
1656
        if not acquired:
1657
            raise self.Busy()
1658

    
1659
    def leave(self):
1660
        self._sema.release()
1661

    
1662
    def __enter__(self):
1663
        self.enter()
1664
        return self
1665

    
1666
    def __exit__(self, exc_type, exc_value, exc_traceback):
1667
        self.leave()
1668

    
1669

    
1670
exclusive_or_raise = ExclusiveOrRaise(locked=False)
1671

    
1672

    
1673
class ProjectMembership(models.Model):
1674

    
1675
    person              =   models.ForeignKey(AstakosUser)
1676
    request_date        =   models.DateField(default=datetime.now())
1677
    project             =   models.ForeignKey(Project)
1678

    
1679
    state               =   models.IntegerField(default=0)
1680
    application         =   models.ForeignKey(ProjectApplication, null=True)
1681
    pending_application =   models.ForeignKey(ProjectApplication, null=True)
1682
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1683

    
1684
    acceptance_date     =   models.DateField(null=True, db_index=True)
1685
    leave_request_date  =   models.DateField(null=True)
1686

    
1687
    objects     =   ForUpdateManager()
1688

    
1689
    REQUESTED   =   0
1690
    PENDING     =   1
1691
    ACCEPTED    =   2
1692
    REMOVING    =   3
1693
    REMOVED     =   4
1694

    
1695
    class Meta:
1696
        unique_together = ("person", "project")
1697
        #index_together = [["project", "state"]]
1698

    
1699
    def __str__(self):
1700
        return _("<'%s' membership in project '%s'>") % (
1701
                self.person.username, self.project.application)
1702

    
1703
    __repr__ = __str__
1704

    
1705
    def __init__(self, *args, **kwargs):
1706
        self.state = self.REQUESTED
1707
        super(ProjectMembership, self).__init__(*args, **kwargs)
1708

    
1709
    def _set_history_item(self, reason, date=None):
1710
        if isinstance(reason, basestring):
1711
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1712

    
1713
        history_item = ProjectMembershipHistory(
1714
                            serial=self.id,
1715
                            person=self.person,
1716
                            project=self.project,
1717
                            date=date,
1718
                            reason=reason)
1719
        history_item.save()
1720
        serial = history_item.id
1721

    
1722
    def accept(self):
1723
        if state != self.REQUESTED:
1724
            m = _("%s: attempt to accept in state [%s]") % (self, state)
1725
            raise AssertionError(m)
1726

    
1727
        now = datetime.now()
1728
        self.acceptance_date = now
1729
        self._set_history_item(reason='ACCEPT', date=now)
1730
        self.state = self.PENDING
1731
        self.save()
1732
        trigger_sync()
1733

    
1734
    def remove(self):
1735
        if state != self.ACCEPTED:
1736
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1737
            raise AssertionError(m)
1738

    
1739
        self._set_history_item(reason='REMOVE')
1740
        self.state = self.REMOVING
1741
        self.save()
1742
        trigger_sync()
1743

    
1744
    def reject(self):
1745
        if state != self.REQUESTED:
1746
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1747
            raise AssertionError(m)
1748

    
1749
        # rejected requests don't need sync,
1750
        # because they were never effected
1751
        self._set_history_item(reason='REJECT')
1752
        self.delete()
1753

    
1754
    def get_diff_quotas(self, limits_list=None, remove=False):
1755
        if limits_list is None:
1756
            limits_list = []
1757

    
1758
        append = limits_list.append
1759
        holder = self.person.username
1760
        key = "1"
1761

    
1762
        tmp_grants = {}
1763
        synced_application = self.application
1764
        if synced_application is not None:
1765
            # first, inverse all current limits, and index them by resource name
1766
            cur_grants = synced_application.resource_grants.all()
1767
            f = -1
1768
            for grant in cur_grants:
1769
                name = grant.resource.name
1770
                tmp_grants[name] = QuotaLimits(
1771
                                holder       = holder,
1772
                                resource     = name,
1773
                                capacity     = f * grant.member_capacity,
1774
                                import_limit = f * grant.member_import_limit,
1775
                                export_limit = f * grant.member_export_limit)
1776

    
1777
        if not remove:
1778
            # second, add each new limit to its inverted current
1779
            new_grants = self.pending_application.resource_grants.all()
1780
            for new_grant in new_grants:
1781
                name = grant.resource.name
1782
                cur_grant = tmp_grants.pop(name, None)
1783
                if cur_grant is None:
1784
                    # if limits on a new resource, set 0 current values
1785
                    capacity = 0
1786
                    import_limit = 0
1787
                    export_limit = 0
1788
                else:
1789
                    capacity = cur_grant.capacity
1790
                    import_limit = cur_grant.import_limit
1791
                    export_limit = cur_grant.export_limit
1792

    
1793
                capacity += new_grant.member_capacity
1794
                import_limit += new_grant.member_import_limit
1795
                export_limit += new_grant.member_export_limit
1796

    
1797
                append(QuotaLimits(holder       = holder,
1798
                                   key          = key,
1799
                                   resource     = name,
1800
                                   capacity     = capacity,
1801
                                   import_limit = import_limit,
1802
                                   export_limit = export_limit))
1803

    
1804
        # third, append all the inverted current limits for removed resources
1805
        limits_list.extend(tmp_grants.itervalues())
1806
        return limits_list
1807

    
1808
    def set_sync(self):
1809
        state = self.state
1810
        if state == self.PENDING:
1811
            pending_application = self.pending_application
1812
            if pending_application is None:
1813
                m = _("%s: attempt to sync an empty pending application") % (
1814
                    self, state)
1815
                raise AssertionError(m)
1816
            self.application = pending_application
1817
            self.pending_application = None
1818
            self.pending_serial = None
1819

    
1820
            # project.application may have changed in the meantime,
1821
            # in which case we stay PENDING;
1822
            # we are safe to check due to select_for_update
1823
            if self.application == self.project.application:
1824
                self.state = self.ACCEPTED
1825
            self.save()
1826
        elif state == self.REMOVING:
1827
            self.delete()
1828
        else:
1829
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1830
            raise AssertionError(m)
1831

    
1832
class Serial(models.Model):
1833
    serial  =   models.AutoField(primary_key=True)
1834

    
1835
def new_serial():
1836
    s = Serial.create()
1837
    return s.serial
1838

    
1839
def sync_finish_serials():
1840
    serials_to_ack = set(qh_query_serials([]))
1841
    sfu = ProjectMembership.objects.select_for_update()
1842
    memberships = sfu.filter(pending_serial__isnull=False)
1843

    
1844
    for membership in memberships:
1845
        serial = membership.serial
1846
        # just make sure the project row is selected for update
1847
        project = membership.project
1848
        if serial in serials_to_ack:
1849
            membership.set_sync()
1850

    
1851
    transaction.commit()
1852
    qh_ack_serials(list(serials_to_ack))
1853

    
1854
def sync_projects():
1855
    sync_finish_serials()
1856

    
1857
    PENDING = ProjectMembership.PENDING
1858
    REMOVING = ProjectMembership.REMOVING
1859
    objects = ProjectMembership.objects.select_for_update()
1860

    
1861
    quotas = []
1862

    
1863
    serial = new_serial()
1864

    
1865
    pending = objects.filter(state=PENDING)
1866
    for membership in pending:
1867

    
1868
        if membership.pending_application:
1869
            m = "%s: impossible: pending_application is not None (%s)" % (
1870
                membership, membership.pending_application)
1871
            raise AssertionError(m)
1872
        if membership.pending_serial:
1873
            m = "%s: impossible: pending_serial is not None (%s)" % (
1874
                membership, membership.pending_serial)
1875
            raise AssertionError(m)
1876

    
1877
        membership.pending_application = membership.project.application
1878
        membership.pending_serial = serial
1879
        membership.get_diff_quotas(quotas)
1880
        membership.save()
1881

    
1882
    removing = objects.filter(state=REMOVING)
1883
    for membership in removing:
1884

    
1885
        if membership.pending_application:
1886
            m = ("%s: impossible: removing pending_application is not None (%s)"
1887
                % (membership, membership.pending_application))
1888
            raise AssertionError(m)
1889
        if membership.pending_serial:
1890
            m = "%s: impossible: pending_serial is not None (%s)" % (
1891
                membership, membership.pending_serial)
1892
            raise AssertionError(m)
1893

    
1894
        membership.pending_serial = serial
1895
        membership.get_diff_quotas(quotas, remove=True)
1896
        membership.save()
1897

    
1898
    transaction.commit()
1899
    # ProjectApplication.approve() unblocks here
1900
    # and can set PENDING an already PENDING membership
1901
    # which has been scheduled to sync with the old project.application
1902
    # Need to check in ProjectMembership.set_sync()
1903

    
1904
    qh_add_quota(serial, quotas)
1905
    sync_finish_serials()
1906

    
1907

    
1908
def trigger_sync(retries=3, retry_wait=1.0):
1909
    cursor = connection.cursor()
1910
    locked = True
1911
    try:
1912
        while 1:
1913
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1914
            r = cursor.fetchone()
1915
            if r is None:
1916
                m = "Impossible"
1917
                raise AssertionError(m)
1918
            locked = r[0]
1919
            if locked:
1920
                break
1921

    
1922
            retries -= 1
1923
            if retries <= 0:
1924
                return False
1925
            sleep(retry_wait)
1926

    
1927
        sync_projects()
1928
        return True
1929

    
1930
    finally:
1931
        if locked:
1932
            cursor.execute("SELECT pg_advisory_unlock(1)")
1933
            cursor.fetchall()
1934

    
1935

    
1936
class ProjectMembershipHistory(models.Model):
1937
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1938
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1939

    
1940
    person  =   models.ForeignKey(AstakosUser)
1941
    project =   models.ForeignKey(Project)
1942
    date    =   models.DateField(default=datetime.now)
1943
    reason  =   models.IntegerField()
1944
    serial  =   models.BigIntegerField()
1945
    
1946

    
1947
def filter_queryset_by_property(q, property):
1948
    """
1949
    Incorporate list comprehension for filtering querysets by property
1950
    since Queryset.filter() operates on the database level.
1951
    """
1952
    return (p for p in q if getattr(p, property, False))
1953

    
1954
def get_alive_projects():
1955
    return filter_queryset_by_property(
1956
        Project.objects.all(),
1957
        'is_alive'
1958
    )
1959

    
1960
def get_active_projects():
1961
    return filter_queryset_by_property(
1962
        Project.objects.all(),
1963
        'is_active'
1964
    )
1965

    
1966
def _create_object(model, **kwargs):
1967
    o = model.objects.create(**kwargs)
1968
    o.save()
1969
    return o
1970

    
1971

    
1972
def create_astakos_user(u):
1973
    try:
1974
        AstakosUser.objects.get(user_ptr=u.pk)
1975
    except AstakosUser.DoesNotExist:
1976
        extended_user = AstakosUser(user_ptr_id=u.pk)
1977
        extended_user.__dict__.update(u.__dict__)
1978
        extended_user.save()
1979
        if not extended_user.has_auth_provider('local'):
1980
            extended_user.add_auth_provider('local')
1981
    except BaseException, e:
1982
        logger.exception(e)
1983

    
1984

    
1985
def fix_superusers(sender, **kwargs):
1986
    # Associate superusers with AstakosUser
1987
    admins = User.objects.filter(is_superuser=True)
1988
    for u in admins:
1989
        create_astakos_user(u)
1990
post_syncdb.connect(fix_superusers)
1991

    
1992

    
1993
def user_post_save(sender, instance, created, **kwargs):
1994
    if not created:
1995
        return
1996
    create_astakos_user(instance)
1997
post_save.connect(user_post_save, sender=User)
1998

    
1999

    
2000
# def astakosuser_pre_save(sender, instance, **kwargs):
2001
#     instance.aquarium_report = False
2002
#     instance.new = False
2003
#     try:
2004
#         db_instance = AstakosUser.objects.get(id=instance.id)
2005
#     except AstakosUser.DoesNotExist:
2006
#         # create event
2007
#         instance.aquarium_report = True
2008
#         instance.new = True
2009
#     else:
2010
#         get = AstakosUser.__getattribute__
2011
#         l = filter(lambda f: get(db_instance, f) != get(instance, f),
2012
#                    BILLING_FIELDS)
2013
#         instance.aquarium_report = True if l else False
2014
# pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
2015

    
2016
# def set_default_group(user):
2017
#     try:
2018
#         default = AstakosGroup.objects.get(name='default')
2019
#         Membership(
2020
#             group=default, person=user, date_joined=datetime.now()).save()
2021
#     except AstakosGroup.DoesNotExist, e:
2022
#         logger.exception(e)
2023

    
2024

    
2025
def astakosuser_post_save(sender, instance, created, **kwargs):
2026
#     if instance.aquarium_report:
2027
#         report_user_event(instance, create=instance.new)
2028
    if not created:
2029
        return
2030
#     set_default_group(instance)
2031
    # TODO handle socket.error & IOError
2032
    register_users((instance,))
2033
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2034

    
2035

    
2036
def resource_post_save(sender, instance, created, **kwargs):
2037
    if not created:
2038
        return
2039
    register_resources((instance,))
2040
post_save.connect(resource_post_save, sender=Resource)
2041

    
2042

    
2043
# def on_quota_disturbed(sender, users, **kwargs):
2044
# #     print '>>>', locals()
2045
#     if not users:
2046
#         return
2047
#     send_quota(users)
2048
#
2049
# quota_disturbed = Signal(providing_args=["users"])
2050
# quota_disturbed.connect(on_quota_disturbed)
2051

    
2052

    
2053
# def send_quota_disturbed(sender, instance, **kwargs):
2054
#     users = []
2055
#     extend = users.extend
2056
#     if sender == Membership:
2057
#         if not instance.group.is_enabled:
2058
#             return
2059
#         extend([instance.person])
2060
#     elif sender == AstakosUserQuota:
2061
#         extend([instance.user])
2062
#     elif sender == AstakosGroupQuota:
2063
#         if not instance.group.is_enabled:
2064
#             return
2065
#         extend(instance.group.astakosuser_set.all())
2066
#     elif sender == AstakosGroup:
2067
#         if not instance.is_enabled:
2068
#             return
2069
#     quota_disturbed.send(sender=sender, users=users)
2070
# post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
2071
# post_delete.connect(send_quota_disturbed, sender=Membership)
2072
# post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
2073
# post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
2074
# post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
2075
# post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
2076

    
2077

    
2078
def renew_token(sender, instance, **kwargs):
2079
    if not instance.auth_token:
2080
        instance.renew_token()
2081
pre_save.connect(renew_token, sender=AstakosUser)
2082
pre_save.connect(renew_token, sender=Service)