Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (70.4 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
    def get_by_identifier(self, email_or_username, **kwargs):
349
        try:
350
            return self.get(email__iexact=email_or_username, **kwargs)
351
        except AstakosUser.DoesNotExist:
352
            return self.get(username__iexact=email_or_username, **kwargs)
353

    
354
    def user_exists(self, email_or_username, **kwargs):
355
        qemail = Q(email__iexact=email_or_username)
356
        qusername = Q(username__iexact=email_or_username)
357
        return self.filter(qemail | qusername).exists()
358

    
359

    
360
class AstakosUser(User):
361
    """
362
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
363
    """
364
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
365
                                   null=True)
366

    
367
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
368
    #                    AstakosUserProvider model.
369
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
370
                                null=True)
371
    # ex. screen_name for twitter, eppn for shibboleth
372
    third_party_identifier = models.CharField(_('Third-party identifier'),
373
                                              max_length=255, null=True,
374
                                              blank=True)
375

    
376

    
377
    #for invitations
378
    user_level = DEFAULT_USER_LEVEL
379
    level = models.IntegerField(_('Inviter level'), default=user_level)
380
    invitations = models.IntegerField(
381
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
382

    
383
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
384
                                  null=True, blank=True)
385
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
386
    auth_token_expires = models.DateTimeField(
387
        _('Token expiration date'), null=True)
388

    
389
    updated = models.DateTimeField(_('Update date'))
390
    is_verified = models.BooleanField(_('Is verified?'), default=False)
391

    
392
    email_verified = models.BooleanField(_('Email verified?'), default=False)
393

    
394
    has_credits = models.BooleanField(_('Has credits?'), default=False)
395
    has_signed_terms = models.BooleanField(
396
        _('I agree with the terms'), default=False)
397
    date_signed_terms = models.DateTimeField(
398
        _('Signed terms date'), null=True, blank=True)
399

    
400
    activation_sent = models.DateTimeField(
401
        _('Activation sent data'), null=True, blank=True)
402

    
403
    policy = models.ManyToManyField(
404
        Resource, null=True, through='AstakosUserQuota')
405

    
406
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
407

    
408
    astakos_groups = models.ManyToManyField(
409
        AstakosGroup, verbose_name=_('agroups'), blank=True,
410
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
411
        through='Membership')
412

    
413
    __has_signed_terms = False
414
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
415
                                           default=False, db_index=True)
416

    
417
    objects = AstakosUserManager()
418

    
419

    
420
    owner = models.ManyToManyField(
421
        AstakosGroup, related_name='owner', null=True)
422

    
423
    def __init__(self, *args, **kwargs):
424
        super(AstakosUser, self).__init__(*args, **kwargs)
425
        self.__has_signed_terms = self.has_signed_terms
426
        if not self.id:
427
            self.is_active = False
428

    
429
    @property
430
    def realname(self):
431
        return '%s %s' % (self.first_name, self.last_name)
432

    
433
    @realname.setter
434
    def realname(self, value):
435
        parts = value.split(' ')
436
        if len(parts) == 2:
437
            self.first_name = parts[0]
438
            self.last_name = parts[1]
439
        else:
440
            self.last_name = parts[0]
441

    
442
    def add_permission(self, pname):
443
        if self.has_perm(pname):
444
            return
445
        p, created = Permission.objects.get_or_create(
446
                                    codename=pname,
447
                                    name=pname.capitalize(),
448
                                    content_type=get_content_type())
449
        self.user_permissions.add(p)
450

    
451
    def remove_permission(self, pname):
452
        if self.has_perm(pname):
453
            return
454
        p = Permission.objects.get(codename=pname,
455
                                   content_type=get_content_type())
456
        self.user_permissions.remove(p)
457

    
458
    @property
459
    def invitation(self):
460
        try:
461
            return Invitation.objects.get(username=self.email)
462
        except Invitation.DoesNotExist:
463
            return None
464

    
465
    @property
466
    def quota(self):
467
        """Returns a dict with the sum of quota limits per resource"""
468
        d = defaultdict(int)
469
        default_quota = get_default_quota()
470
        d.update(default_quota)
471
        for q in self.policies:
472
            d[q.resource] += q.uplimit or inf
473
        for m in self.projectmembership_set.select_related().all():
474
            if not m.acceptance_date:
475
                continue
476
            p = m.project
477
            if not p.is_active:
478
                continue
479
            grants = p.application.projectresourcegrant_set.all()
480
            for g in grants:
481
                d[str(g.resource)] += g.member_capacity or inf
482
        # TODO set default for remaining
483
        return d
484

    
485
    @property
486
    def policies(self):
487
        return self.astakosuserquota_set.select_related().all()
488

    
489
    @policies.setter
490
    def policies(self, policies):
491
        for p in policies:
492
            service = policies.get('service', None)
493
            resource = policies.get('resource', None)
494
            uplimit = policies.get('uplimit', 0)
495
            update = policies.get('update', True)
496
            self.add_policy(service, resource, uplimit, update)
497

    
498
    def add_policy(self, service, resource, uplimit, update=True):
499
        """Raises ObjectDoesNotExist, IntegrityError"""
500
        resource = Resource.objects.get(service__name=service, name=resource)
501
        if update:
502
            AstakosUserQuota.objects.update_or_create(user=self,
503
                                                      resource=resource,
504
                                                      defaults={'uplimit': uplimit})
505
        else:
506
            q = self.astakosuserquota_set
507
            q.create(resource=resource, uplimit=uplimit)
508

    
509
    def remove_policy(self, service, resource):
510
        """Raises ObjectDoesNotExist, IntegrityError"""
511
        resource = Resource.objects.get(service__name=service, name=resource)
512
        q = self.policies.get(resource=resource).delete()
513

    
514
    def update_uuid(self):
515
        while not self.uuid:
516
            uuid_val =  str(uuid.uuid4())
517
            try:
518
                AstakosUser.objects.get(uuid=uuid_val)
519
            except AstakosUser.DoesNotExist, e:
520
                self.uuid = uuid_val
521
        return self.uuid
522

    
523
    @property
524
    def extended_groups(self):
525
        return self.membership_set.select_related().all()
526

    
527
    @extended_groups.setter
528
    def extended_groups(self, groups):
529
        #TODO exceptions
530
        for name in (groups or ()):
531
            group = AstakosGroup.objects.get(name=name)
532
            self.membership_set.create(group=group)
533

    
534
    def save(self, update_timestamps=True, **kwargs):
535
        if update_timestamps:
536
            if not self.id:
537
                self.date_joined = datetime.now()
538
            self.updated = datetime.now()
539

    
540
        # update date_signed_terms if necessary
541
        if self.__has_signed_terms != self.has_signed_terms:
542
            self.date_signed_terms = datetime.now()
543

    
544
        self.update_uuid()
545

    
546
        if self.username != self.email.lower():
547
            # set username
548
            self.username = self.email.lower()
549

    
550
        self.validate_unique_email_isactive()
551

    
552
        super(AstakosUser, self).save(**kwargs)
553

    
554
    def renew_token(self, flush_sessions=False, current_key=None):
555
        md5 = hashlib.md5()
556
        md5.update(settings.SECRET_KEY)
557
        md5.update(self.username)
558
        md5.update(self.realname.encode('ascii', 'ignore'))
559
        md5.update(asctime())
560

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

    
570
    def flush_sessions(self, current_key=None):
571
        q = self.sessions
572
        if current_key:
573
            q = q.exclude(session_key=current_key)
574

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

    
584
    def __unicode__(self):
585
        return '%s (%s)' % (self.realname, self.email)
586

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

    
594
    def validate_unique_email_isactive(self):
595
        """
596
        Implements a unique_together constraint for email and is_active fields.
597
        """
598
        q = AstakosUser.objects.all()
599
        q = q.filter(email = self.email)
600
        if self.id:
601
            q = q.filter(~Q(id = self.id))
602
        if q.count() != 0:
603
            m = 'Another account with the same email = %(email)s & \
604
                is_active = %(is_active)s found.' % self.__dict__
605
            raise ValidationError(m)
606

    
607
    def email_change_is_pending(self):
608
        return self.emailchanges.count() > 0
609

    
610
    def email_change_is_pending(self):
611
        return self.emailchanges.count() > 0
612

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

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

    
637
    def can_login_with_auth_provider(self, provider):
638
        if not self.has_auth_provider(provider):
639
            return False
640
        else:
641
            return auth_providers.get_provider(provider).is_available_for_login()
642

    
643
    def can_add_auth_provider(self, provider, **kwargs):
644
        provider_settings = auth_providers.get_provider(provider)
645

    
646
        if not provider_settings.is_available_for_add():
647
            return False
648

    
649
        if self.has_auth_provider(provider) and \
650
           provider_settings.one_per_user:
651
            return False
652

    
653
        if 'provider_info' in kwargs:
654
            kwargs.pop('provider_info')
655

    
656
        if 'identifier' in kwargs:
657
            try:
658
                # provider with specified params already exist
659
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
660
                                                                   **kwargs)
661
            except AstakosUser.DoesNotExist:
662
                return True
663
            else:
664
                return False
665

    
666
        return True
667

    
668
    def can_remove_auth_provider(self, module):
669
        provider = auth_providers.get_provider(module)
670
        existing = self.get_active_auth_providers()
671
        existing_for_provider = self.get_active_auth_providers(module=module)
672

    
673
        if len(existing) <= 1:
674
            return False
675

    
676
        if len(existing_for_provider) == 1 and provider.is_required():
677
            return False
678

    
679
        return True
680

    
681
    def can_change_password(self):
682
        return self.has_auth_provider('local', auth_backend='astakos')
683

    
684
    def has_required_auth_providers(self):
685
        required = auth_providers.REQUIRED_PROVIDERS
686
        for provider in required:
687
            if not self.has_auth_provider(provider):
688
                return False
689
        return True
690

    
691
    def has_auth_provider(self, provider, **kwargs):
692
        return bool(self.auth_providers.filter(module=provider,
693
                                               **kwargs).count())
694

    
695
    def add_auth_provider(self, provider, **kwargs):
696
        info_data = ''
697
        if 'provider_info' in kwargs:
698
            info_data = kwargs.pop('provider_info')
699
            if isinstance(info_data, dict):
700
                info_data = json.dumps(info_data)
701

    
702
        if self.can_add_auth_provider(provider, **kwargs):
703
            self.auth_providers.create(module=provider, active=True,
704
                                       info_data=info_data,
705
                                       **kwargs)
706
        else:
707
            raise Exception('Cannot add provider')
708

    
709
    def add_pending_auth_provider(self, pending):
710
        """
711
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
712
        the current user.
713
        """
714
        if not isinstance(pending, PendingThirdPartyUser):
715
            pending = PendingThirdPartyUser.objects.get(token=pending)
716

    
717
        provider = self.add_auth_provider(pending.provider,
718
                               identifier=pending.third_party_identifier,
719
                                affiliation=pending.affiliation,
720
                                          provider_info=pending.info)
721

    
722
        if email_re.match(pending.email or '') and pending.email != self.email:
723
            self.additionalmail_set.get_or_create(email=pending.email)
724

    
725
        pending.delete()
726
        return provider
727

    
728
    def remove_auth_provider(self, provider, **kwargs):
729
        self.auth_providers.get(module=provider, **kwargs).delete()
730

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

    
735
    def get_provider_remove_url(self, module, **kwargs):
736
        return reverse('remove_auth_provider', kwargs={
737
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
738

    
739
    def get_activation_url(self, nxt=False):
740
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
741
                                 quote(self.auth_token))
742
        if nxt:
743
            url += "&next=%s" % quote(nxt)
744
        return url
745

    
746
    def get_password_reset_url(self, token_generator=default_token_generator):
747
        return reverse('django.contrib.auth.views.password_reset_confirm',
748
                          kwargs={'uidb36':int_to_base36(self.id),
749
                                  'token':token_generator.make_token(self)})
750

    
751
    def get_auth_providers(self):
752
        return self.auth_providers.all()
753

    
754
    def get_available_auth_providers(self):
755
        """
756
        Returns a list of providers available for user to connect to.
757
        """
758
        providers = []
759
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
760
            if self.can_add_auth_provider(module):
761
                providers.append(provider_settings(self))
762

    
763
        return providers
764

    
765
    def get_active_auth_providers(self, **filters):
766
        providers = []
767
        for provider in self.auth_providers.active(**filters):
768
            if auth_providers.get_provider(provider.module).is_available_for_login():
769
                providers.append(provider)
770
        return providers
771

    
772
    @property
773
    def auth_providers_display(self):
774
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
775

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

    
801
        return mark_safe(message + u' '+ msg_extra)
802

    
803

    
804
class AstakosUserAuthProviderManager(models.Manager):
805

    
806
    def active(self, **filters):
807
        return self.filter(active=True, **filters)
808

    
809

    
810
class AstakosUserAuthProvider(models.Model):
811
    """
812
    Available user authentication methods.
813
    """
814
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
815
                                   null=True, default=None)
816
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
817
    module = models.CharField(_('Provider'), max_length=255, blank=False,
818
                                default='local')
819
    identifier = models.CharField(_('Third-party identifier'),
820
                                              max_length=255, null=True,
821
                                              blank=True)
822
    active = models.BooleanField(default=True)
823
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
824
                                   default='astakos')
825
    info_data = models.TextField(default="", null=True, blank=True)
826
    created = models.DateTimeField('Creation date', auto_now_add=True)
827

    
828
    objects = AstakosUserAuthProviderManager()
829

    
830
    class Meta:
831
        unique_together = (('identifier', 'module', 'user'), )
832
        ordering = ('module', 'created')
833

    
834
    def __init__(self, *args, **kwargs):
835
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
836
        try:
837
            self.info = json.loads(self.info_data)
838
            if not self.info:
839
                self.info = {}
840
        except Exception, e:
841
            self.info = {}
842

    
843
        for key,value in self.info.iteritems():
844
            setattr(self, 'info_%s' % key, value)
845

    
846

    
847
    @property
848
    def settings(self):
849
        return auth_providers.get_provider(self.module)
850

    
851
    @property
852
    def details_display(self):
853
        try:
854
          return self.settings.get_details_tpl_display % self.__dict__
855
        except:
856
          return ''
857

    
858
    @property
859
    def title_display(self):
860
        title_tpl = self.settings.get_title_display
861
        try:
862
            if self.settings.get_user_title_display:
863
                title_tpl = self.settings.get_user_title_display
864
        except Exception, e:
865
            pass
866
        try:
867
          return title_tpl % self.__dict__
868
        except:
869
          return self.settings.get_title_display % self.__dict__
870

    
871
    def can_remove(self):
872
        return self.user.can_remove_auth_provider(self.module)
873

    
874
    def delete(self, *args, **kwargs):
875
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
876
        if self.module == 'local':
877
            self.user.set_unusable_password()
878
            self.user.save()
879
        return ret
880

    
881
    def __repr__(self):
882
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
883

    
884
    def __unicode__(self):
885
        if self.identifier:
886
            return "%s:%s" % (self.module, self.identifier)
887
        if self.auth_backend:
888
            return "%s:%s" % (self.module, self.auth_backend)
889
        return self.module
890

    
891
    def save(self, *args, **kwargs):
892
        self.info_data = json.dumps(self.info)
893
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
894

    
895

    
896
class Membership(models.Model):
897
    person = models.ForeignKey(AstakosUser)
898
    group = models.ForeignKey(AstakosGroup)
899
    date_requested = models.DateField(default=datetime.now(), blank=True)
900
    date_joined = models.DateField(null=True, db_index=True, blank=True)
901

    
902
    class Meta:
903
        unique_together = ("person", "group")
904

    
905
    def save(self, *args, **kwargs):
906
        if not self.id:
907
            if not self.group.moderation_enabled:
908
                self.date_joined = datetime.now()
909
        super(Membership, self).save(*args, **kwargs)
910

    
911
    @property
912
    def is_approved(self):
913
        if self.date_joined:
914
            return True
915
        return False
916

    
917
    def approve(self):
918
        if self.is_approved:
919
            return
920
        if self.group.max_participants:
921
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
922
            'Maximum participant number has been reached.'
923
        self.date_joined = datetime.now()
924
        self.save()
925
        quota_disturbed.send(sender=self, users=(self.person,))
926

    
927
    def disapprove(self):
928
        approved = self.is_approved()
929
        self.delete()
930
        if approved:
931
            quota_disturbed.send(sender=self, users=(self.person,))
932

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

    
960
    update_or_create = _update_or_create
961

    
962
class AstakosGroupQuota(models.Model):
963
    objects = ExtendedManager()
964
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
965
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
966
    resource = models.ForeignKey(Resource)
967
    group = models.ForeignKey(AstakosGroup, blank=True)
968

    
969
    class Meta:
970
        unique_together = ("resource", "group")
971

    
972
class AstakosUserQuota(models.Model):
973
    objects = ExtendedManager()
974
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
975
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
976
    resource = models.ForeignKey(Resource)
977
    user = models.ForeignKey(AstakosUser)
978

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

    
982

    
983
class ApprovalTerms(models.Model):
984
    """
985
    Model for approval terms
986
    """
987

    
988
    date = models.DateTimeField(
989
        _('Issue date'), db_index=True, default=datetime.now())
990
    location = models.CharField(_('Terms location'), max_length=255)
991

    
992

    
993
class Invitation(models.Model):
994
    """
995
    Model for registring invitations
996
    """
997
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
998
                                null=True)
999
    realname = models.CharField(_('Real name'), max_length=255)
1000
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1001
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1002
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1003
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1004
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1005

    
1006
    def __init__(self, *args, **kwargs):
1007
        super(Invitation, self).__init__(*args, **kwargs)
1008
        if not self.id:
1009
            self.code = _generate_invitation_code()
1010

    
1011
    def consume(self):
1012
        self.is_consumed = True
1013
        self.consumed = datetime.now()
1014
        self.save()
1015

    
1016
    def __unicode__(self):
1017
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1018

    
1019

    
1020
class EmailChangeManager(models.Manager):
1021

    
1022
    @transaction.commit_on_success
1023
    def change_email(self, activation_key):
1024
        """
1025
        Validate an activation key and change the corresponding
1026
        ``User`` if valid.
1027

1028
        If the key is valid and has not expired, return the ``User``
1029
        after activating.
1030

1031
        If the key is not valid or has expired, return ``None``.
1032

1033
        If the key is valid but the ``User`` is already active,
1034
        return ``None``.
1035

1036
        After successful email change the activation record is deleted.
1037

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

    
1066

    
1067
class EmailChange(models.Model):
1068
    new_email_address = models.EmailField(
1069
        _(u'new e-mail address'),
1070
        help_text=_('Your old email address will be used until you verify your new one.'))
1071
    user = models.ForeignKey(
1072
        AstakosUser, unique=True, related_name='emailchanges')
1073
    requested_at = models.DateTimeField(default=datetime.now())
1074
    activation_key = models.CharField(
1075
        max_length=40, unique=True, db_index=True)
1076

    
1077
    objects = EmailChangeManager()
1078

    
1079
    def get_url(self):
1080
        return reverse('email_change_confirm',
1081
                      kwargs={'activation_key': self.activation_key})
1082

    
1083
    def activation_key_expired(self):
1084
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1085
        return self.requested_at + expiration_date < datetime.now()
1086

    
1087

    
1088
class AdditionalMail(models.Model):
1089
    """
1090
    Model for registring invitations
1091
    """
1092
    owner = models.ForeignKey(AstakosUser)
1093
    email = models.EmailField()
1094

    
1095

    
1096
def _generate_invitation_code():
1097
    while True:
1098
        code = randint(1, 2L ** 63 - 1)
1099
        try:
1100
            Invitation.objects.get(code=code)
1101
            # An invitation with this code already exists, try again
1102
        except Invitation.DoesNotExist:
1103
            return code
1104

    
1105

    
1106
def get_latest_terms():
1107
    try:
1108
        term = ApprovalTerms.objects.order_by('-id')[0]
1109
        return term
1110
    except IndexError:
1111
        pass
1112
    return None
1113

    
1114
class PendingThirdPartyUser(models.Model):
1115
    """
1116
    Model for registring successful third party user authentications
1117
    """
1118
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1119
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1120
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1121
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
1122
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
1123
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
1124
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1125
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1126
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1127
    info = models.TextField(default="", null=True, blank=True)
1128

    
1129
    class Meta:
1130
        unique_together = ("provider", "third_party_identifier")
1131

    
1132
    def get_user_instance(self):
1133
        d = self.__dict__
1134
        d.pop('_state', None)
1135
        d.pop('id', None)
1136
        d.pop('token', None)
1137
        d.pop('created', None)
1138
        d.pop('info', None)
1139
        user = AstakosUser(**d)
1140

    
1141
        return user
1142

    
1143
    @property
1144
    def realname(self):
1145
        return '%s %s' %(self.first_name, self.last_name)
1146

    
1147
    @realname.setter
1148
    def realname(self, value):
1149
        parts = value.split(' ')
1150
        if len(parts) == 2:
1151
            self.first_name = parts[0]
1152
            self.last_name = parts[1]
1153
        else:
1154
            self.last_name = parts[0]
1155

    
1156
    def save(self, **kwargs):
1157
        if not self.id:
1158
            # set username
1159
            while not self.username:
1160
                username =  uuid.uuid4().hex[:30]
1161
                try:
1162
                    AstakosUser.objects.get(username = username)
1163
                except AstakosUser.DoesNotExist, e:
1164
                    self.username = username
1165
        super(PendingThirdPartyUser, self).save(**kwargs)
1166

    
1167
    def generate_token(self):
1168
        self.password = self.third_party_identifier
1169
        self.last_login = datetime.now()
1170
        self.token = default_token_generator.make_token(self)
1171

    
1172
class SessionCatalog(models.Model):
1173
    session_key = models.CharField(_('session key'), max_length=40)
1174
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1175

    
1176
class MemberJoinPolicy(models.Model):
1177
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1178
    description = models.CharField(_('Description'), max_length=80)
1179

    
1180
    def __str__(self):
1181
        return self.policy
1182

    
1183
class MemberLeavePolicy(models.Model):
1184
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1185
    description = models.CharField(_('Description'), max_length=80)
1186

    
1187
    def __str__(self):
1188
        return self.policy
1189

    
1190
_auto_accept_join = False
1191
def get_auto_accept_join():
1192
    global _auto_accept_join
1193
    if _auto_accept_join is not False:
1194
        return _auto_accept_join
1195
    try:
1196
        auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
1197
    except:
1198
        auto_accept = None
1199
    _auto_accept_join = auto_accept
1200
    return auto_accept
1201

    
1202
_closed_join = False
1203
def get_closed_join():
1204
    global _closed_join
1205
    if _closed_join is not False:
1206
        return _closed_join
1207
    try:
1208
        closed = MemberJoinPolicy.objects.get(policy='closed')
1209
    except:
1210
        closed = None
1211
    _closed_join = closed
1212
    return closed
1213

    
1214
_auto_accept_leave = False
1215
def get_auto_accept_leave():
1216
    global _auto_accept_leave
1217
    if _auto_accept_leave is not False:
1218
        return _auto_accept_leave
1219
    try:
1220
        auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
1221
    except:
1222
        auto_accept = None
1223
    _auto_accept_leave = auto_accept
1224
    return auto_accept
1225

    
1226
_closed_leave = False
1227
def get_closed_leave():
1228
    global _closed_leave
1229
    if _closed_leave is not False:
1230
        return _closed_leave
1231
    try:
1232
        closed = MemberLeavePolicy.objects.get(policy='closed')
1233
    except:
1234
        closed = None
1235
    _closed_leave = closed
1236
    return closeds
1237

    
1238

    
1239
### PROJECTS ###
1240
################
1241

    
1242

    
1243
def synced_model_metaclass(class_name, class_parents, class_attributes):
1244

    
1245
    new_attributes = {}
1246
    sync_attributes = {}
1247

    
1248
    for name, value in class_attributes.iteritems():
1249
        sync, underscore, rest = name.partition('_')
1250
        if sync == 'sync' and underscore == '_':
1251
            sync_attributes[rest] = value
1252
        else:
1253
            new_attributes[name] = value
1254

    
1255
    if 'prefix' not in sync_attributes:
1256
        m = ("you did not specify a 'sync_prefix' attribute "
1257
             "in class '%s'" % (class_name,))
1258
        raise ValueError(m)
1259

    
1260
    prefix = sync_attributes.pop('prefix')
1261
    class_name = sync_attributes.pop('classname', prefix + '_model')
1262

    
1263
    for name, value in sync_attributes.iteritems():
1264
        newname = prefix + '_' + name
1265
        if newname in new_attributes:
1266
            m = ("class '%s' was specified with prefix '%s' "
1267
                 "but it already has an attribute named '%s'"
1268
                 % (class_name, prefix, newname))
1269
            raise ValueError(m)
1270

    
1271
        new_attributes[newname] = value
1272

    
1273
    newclass = type(class_name, class_parents, new_attributes)
1274
    return newclass
1275

    
1276

    
1277
def make_synced(prefix='sync', name='SyncedState'):
1278

    
1279
    the_name = name
1280
    the_prefix = prefix
1281

    
1282
    class SyncedState(models.Model):
1283

    
1284
        sync_classname      = the_name
1285
        sync_prefix         = the_prefix
1286
        __metaclass__       = synced_model_metaclass
1287

    
1288
        sync_new_state      = models.BigIntegerField(null=True)
1289
        sync_synced_state   = models.BigIntegerField(null=True)
1290
        STATUS_SYNCED       = 0
1291
        STATUS_PENDING      = 1
1292
        sync_status         = models.IntegerField(db_index=True)
1293

    
1294
        class Meta:
1295
            abstract = True
1296

    
1297
        class NotSynced(Exception):
1298
            pass
1299

    
1300
        def sync_init_state(self, state):
1301
            self.sync_synced_state = state
1302
            self.sync_new_state = state
1303
            self.sync_status = self.STATUS_SYNCED
1304

    
1305
        def sync_get_status(self):
1306
            return self.sync_status
1307

    
1308
        def sync_set_status(self):
1309
            if self.sync_new_state != self.sync_synced_state:
1310
                self.sync_status = self.STATUS_PENDING
1311
            else:
1312
                self.sync_status = self.STATUS_SYNCED
1313

    
1314
        def sync_set_synced(self):
1315
            self.sync_synced_state = self.sync_new_state
1316
            self.sync_status = self.STATUS_SYNCED
1317

    
1318
        def sync_get_synced_state(self):
1319
            return self.sync_synced_state
1320

    
1321
        def sync_set_new_state(self, new_state):
1322
            self.sync_new_state = new_state
1323
            self.sync_set_status()
1324

    
1325
        def sync_get_new_state(self):
1326
            return self.sync_new_state
1327

    
1328
        def sync_set_synced_state(self, synced_state):
1329
            self.sync_synced_state = synced_state
1330
            self.sync_set_status()
1331

    
1332
        def sync_get_pending_objects(self):
1333
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1334
            return self.objects.filter(**kw)
1335

    
1336
        def sync_get_synced_objects(self):
1337
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1338
            return self.objects.filter(**kw)
1339

    
1340
        def sync_verify_get_synced_state(self):
1341
            status = self.sync_get_status()
1342
            state = self.sync_get_synced_state()
1343
            verified = (status == self.STATUS_SYNCED)
1344
            return state, verified
1345

    
1346
        def sync_is_synced(self):
1347
            state, verified = self.sync_verify_get_synced_state()
1348
            return verified
1349

    
1350
    return SyncedState
1351

    
1352
SyncedState = make_synced(prefix='sync', name='SyncedState')
1353

    
1354

    
1355
class ProjectApplication(models.Model):
1356

    
1357
    applicant               =   models.ForeignKey(
1358
                                    AstakosUser,
1359
                                    related_name='projects_applied',
1360
                                    db_index=True)
1361

    
1362
    state                   =   models.CharField(max_length=80,
1363
                                                default=UNKNOWN)
1364

    
1365
    owner                   =   models.ForeignKey(
1366
                                    AstakosUser,
1367
                                    related_name='projects_owned',
1368
                                    db_index=True)
1369

    
1370
    precursor_application   =   models.OneToOneField('ProjectApplication',
1371
                                                     null=True,
1372
                                                     blank=True,
1373
                                                     db_index=True)
1374

    
1375
    name                    =   models.CharField(max_length=80)
1376
    homepage                =   models.URLField(max_length=255, null=True,
1377
                                                blank=True)
1378
    description             =   models.TextField(null=True, blank=True)
1379
    start_date              =   models.DateTimeField()
1380
    end_date                =   models.DateTimeField()
1381
    member_join_policy      =   models.ForeignKey(MemberJoinPolicy)
1382
    member_leave_policy     =   models.ForeignKey(MemberLeavePolicy)
1383
    limit_on_members_number =   models.PositiveIntegerField(null=True,
1384
                                                            blank=True)
1385
    resource_grants         =   models.ManyToManyField(
1386
                                    Resource,
1387
                                    null=True,
1388
                                    blank=True,
1389
                                    through='ProjectResourceGrant')
1390
    comments                =   models.TextField(null=True, blank=True)
1391
    issue_date              =   models.DateTimeField()
1392

    
1393
    states_list =   [PENDING, APPROVED, REPLACED, UNKNOWN]
1394
    states      =   dict((k, v) for v, k in enumerate(states_list))
1395

    
1396
    def add_resource_policy(self, service, resource, uplimit):
1397
        """Raises ObjectDoesNotExist, IntegrityError"""
1398
        q = self.projectresourcegrant_set
1399
        resource = Resource.objects.get(service__name=service, name=resource)
1400
        q.create(resource=resource, member_capacity=uplimit)
1401

    
1402
    @property
1403
    def resource_policies(self):
1404
        return self.projectresourcegrant_set.all()
1405

    
1406
    @resource_policies.setter
1407
    def resource_policies(self, policies):
1408
        for p in policies:
1409
            service = p.get('service', None)
1410
            resource = p.get('resource', None)
1411
            uplimit = p.get('uplimit', 0)
1412
            self.add_resource_policy(service, resource, uplimit)
1413

    
1414
    @property
1415
    def follower(self):
1416
        try:
1417
            return ProjectApplication.objects.get(precursor_application=self)
1418
        except ProjectApplication.DoesNotExist:
1419
            return
1420

    
1421
    def submit(self, resource_policies, applicant, comments,
1422
               precursor_application=None):
1423

    
1424
        if precursor_application:
1425
            self.precursor_application = precursor_application
1426
            self.owner = precursor_application.owner
1427
        else:
1428
            self.owner = applicant
1429

    
1430
        self.id = None
1431
        self.applicant = applicant
1432
        self.comments = comments
1433
        self.issue_date = datetime.now()
1434
        self.state = PENDING
1435
        self.save()
1436
        self.resource_policies = resource_policies
1437

    
1438
    def _get_project(self):
1439
        precursor = self
1440
        while precursor:
1441
            try:
1442
                project = precursor.project
1443
                return project
1444
            except Project.DoesNotExist:
1445
                pass
1446
            precursor = precursor.precursor_application
1447

    
1448
        return None
1449

    
1450
    def approve(self, approval_user=None):
1451
        """
1452
        If approval_user then during owner membership acceptance
1453
        it is checked whether the request_user is eligible.
1454

1455
        Raises:
1456
            PermissionDenied
1457
        """
1458

    
1459
        if not transaction.is_managed():
1460
            raise AssertionError("NOPE")
1461

    
1462
        new_project_name = self.name
1463
        if self.state != PENDING:
1464
            m = _("cannot approve: project '%s' in state '%s'") % (
1465
                    new_project_name, self.state)
1466
            raise PermissionDenied(m) # invalid argument
1467

    
1468
        now = datetime.now()
1469
        project = self._get_project()
1470
        if project is None:
1471
            try:
1472
                # needs SERIALIZABLE
1473
                conflicting_project = Project.objects.get(name=new_project_name)
1474
                if conflicting_project.is_alive:
1475
                    m = _("cannot approve: project with name '%s' "
1476
                          "already exists (serial: %s)") % (
1477
                            new_project_name, conflicting_project.id)
1478
                    raise PermissionDenied(m) # invalid argument
1479
            except Project.DoesNotExist:
1480
                pass
1481
            project = Project(creation_date=now, name=new_project_name)
1482

    
1483
        project.application = self
1484

    
1485
        # This will block while syncing,
1486
        # but unblock before setting the membership state.
1487
        # See ProjectMembership.set_sync()
1488
        project.set_membership_pending_sync()
1489

    
1490
        project.last_approval_date = now
1491
        project.save()
1492
        #ProjectMembership.add_to_project(self)
1493
        project.add_member(self.owner)
1494

    
1495
        precursor = self.precursor_application
1496
        while precursor:
1497
            precursor.state = REPLACED
1498
            precursor.save()
1499
            precursor = precursor.precursor_application
1500

    
1501
        self.state = APPROVED
1502
        self.save()
1503

    
1504

    
1505
class ProjectResourceGrant(models.Model):
1506

    
1507
    resource                =   models.ForeignKey(Resource)
1508
    project_application     =   models.ForeignKey(ProjectApplication,
1509
                                                  null=True)
1510
    project_capacity        =   models.BigIntegerField(null=True)
1511
    project_import_limit    =   models.BigIntegerField(null=True)
1512
    project_export_limit    =   models.BigIntegerField(null=True)
1513
    member_capacity         =   models.BigIntegerField(null=True)
1514
    member_import_limit     =   models.BigIntegerField(null=True)
1515
    member_export_limit     =   models.BigIntegerField(null=True)
1516

    
1517
    objects = ExtendedManager()
1518

    
1519
    class Meta:
1520
        unique_together = ("resource", "project_application")
1521

    
1522

    
1523
class Project(models.Model):
1524

    
1525
    application                 =   models.OneToOneField(
1526
                                            ProjectApplication,
1527
                                            related_name='project')
1528
    last_approval_date          =   models.DateTimeField(null=True)
1529

    
1530
    members                     =   models.ManyToManyField(
1531
                                            AstakosUser,
1532
                                            through='ProjectMembership')
1533

    
1534
    termination_start_date      =   models.DateTimeField(null=True)
1535
    termination_date            =   models.DateTimeField(null=True)
1536

    
1537
    creation_date               =   models.DateTimeField()
1538
    name                        =   models.CharField(
1539
                                            max_length=80,
1540
                                            db_index=True,
1541
                                            unique=True)
1542

    
1543
    @property
1544
    def violated_resource_grants(self):
1545
        return False
1546

    
1547
    @property
1548
    def violated_members_number_limit(self):
1549
        application = self.application
1550
        return len(self.approved_members) > application.limit_on_members_number
1551

    
1552
    @property
1553
    def is_terminated(self):
1554
        return bool(self.termination_date)
1555

    
1556
    @property
1557
    def is_still_approved(self):
1558
        return bool(self.last_approval_date)
1559

    
1560
    @property
1561
    def is_active(self):
1562
        if (self.is_terminated or
1563
            not self.is_still_approved or
1564
            self.violated_resource_grants):
1565
            return False
1566
#         if self.violated_members_number_limit:
1567
#             return False
1568
        return True
1569

    
1570
    @property
1571
    def is_suspended(self):
1572
        if (self.is_terminated or
1573
            self.is_still_approved or
1574
            not self.violated_resource_grants):
1575
            return False
1576
#             if not self.violated_members_number_limit:
1577
#                 return False
1578
        return True
1579

    
1580
    @property
1581
    def is_alive(self):
1582
        return self.is_active or self.is_suspended
1583

    
1584
    @property
1585
    def is_inconsistent(self):
1586
        now = datetime.now()
1587
        if self.creation_date > now:
1588
            return True
1589
        if self.last_approval_date > now:
1590
            return True
1591
        if self.terminaton_date > now:
1592
            return True
1593
        return False
1594

    
1595
    @property
1596
    def approved_memberships(self):
1597
        ACCEPTED = ProjectMembership.ACCEPTED
1598
        PENDING  = ProjectMembership.PENDING
1599
        return self.projectmembership_set.filter(
1600
            Q(state=ACCEPTED) | Q(state=PENDING))
1601

    
1602
    @property
1603
    def approved_members(self):
1604
        return [m.person for m in self.approved_memberships]
1605

    
1606
    def set_membership_pending_sync(self):
1607
        ACCEPTED = ProjectMembership.ACCEPTED
1608
        PENDING  = ProjectMembership.PENDING
1609
        sfu = self.projectmembership_set.select_for_update()
1610
        members = sfu.filter(Q(state=ACCEPTED) | Q(state=PENDING))
1611

    
1612
        for member in members:
1613
            member.state = member.PENDING
1614
            member.save()
1615

    
1616
    def add_member(self, user):
1617
        """
1618
        Raises:
1619
            django.exceptions.PermissionDenied
1620
            astakos.im.models.AstakosUser.DoesNotExist
1621
        """
1622
        if isinstance(user, int):
1623
            user = AstakosUser.objects.get(user=user)
1624

    
1625
        m, created = ProjectMembership.objects.get_or_create(
1626
            person=user, project=self
1627
        )
1628
        m.accept()
1629

    
1630
    def remove_member(self, user):
1631
        """
1632
        Raises:
1633
            django.exceptions.PermissionDenied
1634
            astakos.im.models.AstakosUser.DoesNotExist
1635
            astakos.im.models.ProjectMembership.DoesNotExist
1636
        """
1637
        if isinstance(user, int):
1638
            user = AstakosUser.objects.get(user=user)
1639

    
1640
        m = ProjectMembership.objects.get(person=user, project=self)
1641
        m.remove()
1642

    
1643
    def terminate(self):
1644
        self.termination_start_date = datetime.now()
1645
        self.terminaton_date = None
1646
        self.save()
1647

    
1648
        rejected = self.sync()
1649
        if not rejected:
1650
            self.termination_start_date = None
1651
            self.termination_date = datetime.now()
1652
            self.save()
1653

    
1654
#         try:
1655
#             notification = build_notification(
1656
#                 settings.SERVER_EMAIL,
1657
#                 [self.current_application.owner.email],
1658
#                 _(PROJECT_TERMINATION_SUBJECT) % self.__dict__,
1659
#                 template='im/projects/project_termination_notification.txt',
1660
#                 dictionary={'object':self.current_application}
1661
#             ).send()
1662
#         except NotificationError, e:
1663
#             logger.error(e.messages)
1664

    
1665
    def suspend(self):
1666
        self.last_approval_date = None
1667
        self.save()
1668
        self.sync()
1669

    
1670
#         try:
1671
#             notification = build_notification(
1672
#                 settings.SERVER_EMAIL,
1673
#                 [self.current_application.owner.email],
1674
#                 _(PROJECT_SUSPENSION_SUBJECT) % self.definition.__dict__,
1675
#                 template='im/projects/project_suspension_notification.txt',
1676
#                 dictionary={'object':self.current_application}
1677
#             ).send()
1678
#         except NotificationError, e:
1679
#             logger.error(e.messages)
1680

    
1681

    
1682
class ProjectMembership(models.Model):
1683

    
1684
    person              =   models.ForeignKey(AstakosUser)
1685
    request_date        =   models.DateField(default=datetime.now())
1686
    project             =   models.ForeignKey(Project)
1687

    
1688
    state               =   models.IntegerField(default=0)
1689
    application         =   models.ForeignKey(
1690
                                ProjectApplication,
1691
                                null=True,
1692
                                related_name='memberships')
1693
    pending_application =   models.ForeignKey(
1694
                                ProjectApplication,
1695
                                null=True,
1696
                                related_name='pending_memebrships')
1697
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1698

    
1699
    acceptance_date     =   models.DateField(null=True, db_index=True)
1700
    leave_request_date  =   models.DateField(null=True)
1701

    
1702
    objects     =   ForUpdateManager()
1703

    
1704
    REQUESTED   =   0
1705
    PENDING     =   1
1706
    ACCEPTED    =   2
1707
    REMOVING    =   3
1708
    REMOVED     =   4
1709

    
1710
    class Meta:
1711
        unique_together = ("person", "project")
1712
        #index_together = [["project", "state"]]
1713

    
1714
    def __str__(self):
1715
        return _("<'%s' membership in project '%s'>") % (
1716
                self.person.username, self.project.application)
1717

    
1718
    __repr__ = __str__
1719

    
1720
    def __init__(self, *args, **kwargs):
1721
        self.state = self.REQUESTED
1722
        super(ProjectMembership, self).__init__(*args, **kwargs)
1723

    
1724
    def _set_history_item(self, reason, date=None):
1725
        if isinstance(reason, basestring):
1726
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1727

    
1728
        history_item = ProjectMembershipHistory(
1729
                            serial=self.id,
1730
                            person=self.person,
1731
                            project=self.project,
1732
                            date=date,
1733
                            reason=reason)
1734
        history_item.save()
1735
        serial = history_item.id
1736

    
1737
    def accept(self):
1738
        state = self.state
1739
        if state != self.REQUESTED:
1740
            m = _("%s: attempt to accept in state [%s]") % (self, state)
1741
            raise AssertionError(m)
1742

    
1743
        now = datetime.now()
1744
        self.acceptance_date = now
1745
        self._set_history_item(reason='ACCEPT', date=now)
1746
        self.state = self.PENDING
1747
        self.save()
1748

    
1749
    def remove(self):
1750
        if state != self.ACCEPTED:
1751
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1752
            raise AssertionError(m)
1753

    
1754
        self._set_history_item(reason='REMOVE')
1755
        self.state = self.REMOVING
1756
        self.save()
1757

    
1758
    def reject(self):
1759
        if state != self.REQUESTED:
1760
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1761
            raise AssertionError(m)
1762

    
1763
        # rejected requests don't need sync,
1764
        # because they were never effected
1765
        self._set_history_item(reason='REJECT')
1766
        self.delete()
1767

    
1768
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1769
        if sub_list is None:
1770
            sub_list = []
1771

    
1772
        if add_list is None:
1773
            add_list = []
1774

    
1775
        sub_append = sub_list.append
1776
        add_append = add_list.append
1777
        holder = self.person.username
1778

    
1779
        synced_application = self.application
1780
        if synced_application is not None:
1781
            # first, inverse all current limits, and index them by resource name
1782
            cur_grants = synced_application.resource_grants.all()
1783
            for grant in cur_grants:
1784
                sub_append(QuotaLimits(
1785
                               holder       = holder,
1786
                               resource     = grant.resource.name,
1787
                               capacity     = grant.member_capacity,
1788
                               import_limit = grant.member_import_limit,
1789
                               export_limit = grant.member_export_limit))
1790

    
1791
        if not remove:
1792
            # second, add each new limit to its inverted current
1793
            new_grants = self.pending_application.projectresourcegrant_set.all()
1794
            for new_grant in new_grants:
1795
                add_append(QuotaLimits(
1796
                               holder       = holder,
1797
                               resource     = new_grant.resource.name,
1798
                               capacity     = new_grant.capacity,
1799
                               import_limit = new_grant.import_limit,
1800
                               export_limit = new_grant.export_limit))
1801

    
1802
        return (sub_list, add_list)
1803

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

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

    
1828
class Serial(models.Model):
1829
    serial  =   models.AutoField(primary_key=True)
1830

    
1831
def new_serial():
1832
    s = Serial.objects.create()
1833
    serial = s.serial
1834
    s.delete()
1835
    return serial
1836

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

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

    
1849
    transaction.commit()
1850
    qh_ack_serials(list(serials_to_ack))
1851

    
1852
def sync_projects():
1853
    sync_finish_serials()
1854

    
1855
    PENDING = ProjectMembership.PENDING
1856
    REMOVING = ProjectMembership.REMOVING
1857
    objects = ProjectMembership.objects.select_for_update()
1858

    
1859
    sub_quota, add_quota = [], []
1860

    
1861
    serial = new_serial()
1862

    
1863
    pending = objects.filter(state=PENDING)
1864
    for membership in pending:
1865

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

    
1875
        membership.pending_application = membership.project.application
1876
        membership.pending_serial = serial
1877
        membership.get_diff_quotas(sub_quota, add_quota)
1878
        membership.save()
1879

    
1880
    removing = objects.filter(state=REMOVING)
1881
    for membership in removing:
1882

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

    
1892
        membership.pending_serial = serial
1893
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1894
        membership.save()
1895

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

    
1902
    qh_add_quota(serial, sub_quota, add_quota)
1903
    sync_finish_serials()
1904

    
1905

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

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

    
1925
        sync_projects()
1926
        return True
1927

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

    
1933

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

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

    
1944

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

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

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

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

    
1969

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

    
1982

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

    
1990

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

    
1997

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

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

    
2022

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

    
2033

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

    
2040

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

    
2050

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

    
2075

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