Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 5200e864

History | View | Annotate | Download (71.8 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
    @property
611
    def signed_terms(self):
612
        term = get_latest_terms()
613
        if not term:
614
            return True
615
        if not self.has_signed_terms:
616
            return False
617
        if not self.date_signed_terms:
618
            return False
619
        if self.date_signed_terms < term.date:
620
            self.has_signed_terms = False
621
            self.date_signed_terms = None
622
            self.save()
623
            return False
624
        return True
625

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

    
634
    def can_login_with_auth_provider(self, provider):
635
        if not self.has_auth_provider(provider):
636
            return False
637
        else:
638
            return auth_providers.get_provider(provider).is_available_for_login()
639

    
640
    def can_add_auth_provider(self, provider, **kwargs):
641
        provider_settings = auth_providers.get_provider(provider)
642
        if not provider_settings.is_available_for_login():
643
            return False
644

    
645
        if self.has_auth_provider(provider) and \
646
           provider_settings.one_per_user:
647
            return False
648

    
649
        if 'provider_info' in kwargs:
650
            kwargs.pop('provider_info')
651

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

    
662
        return True
663

    
664
    def can_remove_auth_provider(self, provider):
665
        if len(self.get_active_auth_providers()) <= 1:
666
            return False
667
        return True
668

    
669
    def can_change_password(self):
670
        return self.has_auth_provider('local', auth_backend='astakos')
671

    
672
    def has_auth_provider(self, provider, **kwargs):
673
        return bool(self.auth_providers.filter(module=provider,
674
                                               **kwargs).count())
675

    
676
    def add_auth_provider(self, provider, **kwargs):
677
        info_data = ''
678
        if 'provider_info' in kwargs:
679
            info_data = kwargs.pop('provider_info')
680
            if isinstance(info_data, dict):
681
                info_data = json.dumps(info_data)
682

    
683
        if self.can_add_auth_provider(provider, **kwargs):
684
            self.auth_providers.create(module=provider, active=True,
685
                                       info_data=info_data,
686
                                       **kwargs)
687
        else:
688
            raise Exception('Cannot add provider')
689

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

    
698
        provider = self.add_auth_provider(pending.provider,
699
                               identifier=pending.third_party_identifier,
700
                                affiliation=pending.affiliation,
701
                                          provider_info=pending.info)
702

    
703
        if email_re.match(pending.email or '') and pending.email != self.email:
704
            self.additionalmail_set.get_or_create(email=pending.email)
705

    
706
        pending.delete()
707
        return provider
708

    
709
    def remove_auth_provider(self, provider, **kwargs):
710
        self.auth_providers.get(module=provider, **kwargs).delete()
711

    
712
    # user urls
713
    def get_resend_activation_url(self):
714
        return reverse('send_activation', kwargs={'user_id': self.pk})
715

    
716
    def get_provider_remove_url(self, module, **kwargs):
717
        return reverse('remove_auth_provider', kwargs={
718
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
719

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

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

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

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

    
744
        return providers
745

    
746
    def get_active_auth_providers(self):
747
        providers = []
748
        for provider in self.auth_providers.active():
749
            if auth_providers.get_provider(provider.module).is_available_for_login():
750
                providers.append(provider)
751
        return providers
752

    
753
    @property
754
    def auth_providers_display(self):
755
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
756

    
757
    def get_inactive_message(self):
758
        msg_extra = ''
759
        message = ''
760
        if self.activation_sent:
761
            if self.email_verified:
762
                message = _(astakos_messages.ACCOUNT_INACTIVE)
763
            else:
764
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
765
                if MODERATION_ENABLED:
766
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
767
                else:
768
                    url = self.get_resend_activation_url()
769
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
770
                                u' ' + \
771
                                _('<a href="%s">%s?</a>') % (url,
772
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
773
        else:
774
            if MODERATION_ENABLED:
775
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
776
            else:
777
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
778
                url = self.get_resend_activation_url()
779
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
780
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
781

    
782
        return mark_safe(message + u' '+ msg_extra)
783

    
784

    
785
class AstakosUserAuthProviderManager(models.Manager):
786

    
787
    def active(self):
788
        return self.filter(active=True)
789

    
790

    
791
class AstakosUserAuthProvider(models.Model):
792
    """
793
    Available user authentication methods.
794
    """
795
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
796
                                   null=True, default=None)
797
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
798
    module = models.CharField(_('Provider'), max_length=255, blank=False,
799
                                default='local')
800
    identifier = models.CharField(_('Third-party identifier'),
801
                                              max_length=255, null=True,
802
                                              blank=True)
803
    active = models.BooleanField(default=True)
804
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
805
                                   default='astakos')
806
    info_data = models.TextField(default="", null=True, blank=True)
807
    created = models.DateTimeField('Creation date', auto_now_add=True)
808

    
809
    objects = AstakosUserAuthProviderManager()
810

    
811
    class Meta:
812
        unique_together = (('identifier', 'module', 'user'), )
813
        ordering = ('module', 'created')
814

    
815
    def __init__(self, *args, **kwargs):
816
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
817
        try:
818
            self.info = json.loads(self.info_data)
819
            if not self.info:
820
                self.info = {}
821
        except Exception, e:
822
            self.info = {}
823

    
824
        for key,value in self.info.iteritems():
825
            setattr(self, 'info_%s' % key, value)
826

    
827

    
828
    @property
829
    def settings(self):
830
        return auth_providers.get_provider(self.module)
831

    
832
    @property
833
    def details_display(self):
834
        try:
835
          return self.settings.get_details_tpl_display % self.__dict__
836
        except:
837
          return ''
838

    
839
    @property
840
    def title_display(self):
841
        title_tpl = self.settings.get_title_display
842
        try:
843
            if self.settings.get_user_title_display:
844
                title_tpl = self.settings.get_user_title_display
845
        except Exception, e:
846
            pass
847
        try:
848
          return title_tpl % self.__dict__
849
        except:
850
          return self.settings.get_title_display % self.__dict__
851

    
852
    def can_remove(self):
853
        return self.user.can_remove_auth_provider(self.module)
854

    
855
    def delete(self, *args, **kwargs):
856
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
857
        if self.module == 'local':
858
            self.user.set_unusable_password()
859
            self.user.save()
860
        return ret
861

    
862
    def __repr__(self):
863
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
864

    
865
    def __unicode__(self):
866
        if self.identifier:
867
            return "%s:%s" % (self.module, self.identifier)
868
        if self.auth_backend:
869
            return "%s:%s" % (self.module, self.auth_backend)
870
        return self.module
871

    
872
    def save(self, *args, **kwargs):
873
        self.info_data = json.dumps(self.info)
874
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
875

    
876

    
877
class Membership(models.Model):
878
    person = models.ForeignKey(AstakosUser)
879
    group = models.ForeignKey(AstakosGroup)
880
    date_requested = models.DateField(default=datetime.now(), blank=True)
881
    date_joined = models.DateField(null=True, db_index=True, blank=True)
882

    
883
    class Meta:
884
        unique_together = ("person", "group")
885

    
886
    def save(self, *args, **kwargs):
887
        if not self.id:
888
            if not self.group.moderation_enabled:
889
                self.date_joined = datetime.now()
890
        super(Membership, self).save(*args, **kwargs)
891

    
892
    @property
893
    def is_approved(self):
894
        if self.date_joined:
895
            return True
896
        return False
897

    
898
    def approve(self):
899
        if self.is_approved:
900
            return
901
        if self.group.max_participants:
902
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
903
            'Maximum participant number has been reached.'
904
        self.date_joined = datetime.now()
905
        self.save()
906
        quota_disturbed.send(sender=self, users=(self.person,))
907

    
908
    def disapprove(self):
909
        approved = self.is_approved()
910
        self.delete()
911
        if approved:
912
            quota_disturbed.send(sender=self, users=(self.person,))
913

    
914
class ExtendedManager(models.Manager):
915
    def _update_or_create(self, **kwargs):
916
        assert kwargs, \
917
            'update_or_create() must be passed at least one keyword argument'
918
        obj, created = self.get_or_create(**kwargs)
919
        defaults = kwargs.pop('defaults', {})
920
        if created:
921
            return obj, True, False
922
        else:
923
            try:
924
                params = dict(
925
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
926
                params.update(defaults)
927
                for attr, val in params.items():
928
                    if hasattr(obj, attr):
929
                        setattr(obj, attr, val)
930
                sid = transaction.savepoint()
931
                obj.save(force_update=True)
932
                transaction.savepoint_commit(sid)
933
                return obj, False, True
934
            except IntegrityError, e:
935
                transaction.savepoint_rollback(sid)
936
                try:
937
                    return self.get(**kwargs), False, False
938
                except self.model.DoesNotExist:
939
                    raise e
940

    
941
    update_or_create = _update_or_create
942

    
943
class AstakosGroupQuota(models.Model):
944
    objects = ExtendedManager()
945
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
946
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
947
    resource = models.ForeignKey(Resource)
948
    group = models.ForeignKey(AstakosGroup, blank=True)
949

    
950
    class Meta:
951
        unique_together = ("resource", "group")
952

    
953
class AstakosUserQuota(models.Model):
954
    objects = ExtendedManager()
955
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
956
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
957
    resource = models.ForeignKey(Resource)
958
    user = models.ForeignKey(AstakosUser)
959

    
960
    class Meta:
961
        unique_together = ("resource", "user")
962

    
963

    
964
class ApprovalTerms(models.Model):
965
    """
966
    Model for approval terms
967
    """
968

    
969
    date = models.DateTimeField(
970
        _('Issue date'), db_index=True, default=datetime.now())
971
    location = models.CharField(_('Terms location'), max_length=255)
972

    
973

    
974
class Invitation(models.Model):
975
    """
976
    Model for registring invitations
977
    """
978
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
979
                                null=True)
980
    realname = models.CharField(_('Real name'), max_length=255)
981
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
982
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
983
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
984
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
985
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
986

    
987
    def __init__(self, *args, **kwargs):
988
        super(Invitation, self).__init__(*args, **kwargs)
989
        if not self.id:
990
            self.code = _generate_invitation_code()
991

    
992
    def consume(self):
993
        self.is_consumed = True
994
        self.consumed = datetime.now()
995
        self.save()
996

    
997
    def __unicode__(self):
998
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
999

    
1000

    
1001
class EmailChangeManager(models.Manager):
1002

    
1003
    @transaction.commit_on_success
1004
    def change_email(self, activation_key):
1005
        """
1006
        Validate an activation key and change the corresponding
1007
        ``User`` if valid.
1008

1009
        If the key is valid and has not expired, return the ``User``
1010
        after activating.
1011

1012
        If the key is not valid or has expired, return ``None``.
1013

1014
        If the key is valid but the ``User`` is already active,
1015
        return ``None``.
1016

1017
        After successful email change the activation record is deleted.
1018

1019
        Throws ValueError if there is already
1020
        """
1021
        try:
1022
            email_change = self.model.objects.get(
1023
                activation_key=activation_key)
1024
            if email_change.activation_key_expired():
1025
                email_change.delete()
1026
                raise EmailChange.DoesNotExist
1027
            # is there an active user with this address?
1028
            try:
1029
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1030
            except AstakosUser.DoesNotExist:
1031
                pass
1032
            else:
1033
                raise ValueError(_('The new email address is reserved.'))
1034
            # update user
1035
            user = AstakosUser.objects.get(pk=email_change.user_id)
1036
            old_email = user.email
1037
            user.email = email_change.new_email_address
1038
            user.save()
1039
            email_change.delete()
1040
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1041
                                                          user.email)
1042
            logger.log(LOGGING_LEVEL, msg)
1043
            return user
1044
        except EmailChange.DoesNotExist:
1045
            raise ValueError(_('Invalid activation key.'))
1046

    
1047

    
1048
class EmailChange(models.Model):
1049
    new_email_address = models.EmailField(
1050
        _(u'new e-mail address'),
1051
        help_text=_('Your old email address will be used until you verify your new one.'))
1052
    user = models.ForeignKey(
1053
        AstakosUser, unique=True, related_name='emailchanges')
1054
    requested_at = models.DateTimeField(default=datetime.now())
1055
    activation_key = models.CharField(
1056
        max_length=40, unique=True, db_index=True)
1057

    
1058
    objects = EmailChangeManager()
1059

    
1060
    def get_url(self):
1061
        return reverse('email_change_confirm',
1062
                      kwargs={'activation_key': self.activation_key})
1063

    
1064
    def activation_key_expired(self):
1065
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1066
        return self.requested_at + expiration_date < datetime.now()
1067

    
1068

    
1069
class AdditionalMail(models.Model):
1070
    """
1071
    Model for registring invitations
1072
    """
1073
    owner = models.ForeignKey(AstakosUser)
1074
    email = models.EmailField()
1075

    
1076

    
1077
def _generate_invitation_code():
1078
    while True:
1079
        code = randint(1, 2L ** 63 - 1)
1080
        try:
1081
            Invitation.objects.get(code=code)
1082
            # An invitation with this code already exists, try again
1083
        except Invitation.DoesNotExist:
1084
            return code
1085

    
1086

    
1087
def get_latest_terms():
1088
    try:
1089
        term = ApprovalTerms.objects.order_by('-id')[0]
1090
        return term
1091
    except IndexError:
1092
        pass
1093
    return None
1094

    
1095
class PendingThirdPartyUser(models.Model):
1096
    """
1097
    Model for registring successful third party user authentications
1098
    """
1099
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1100
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1101
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1102
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
1103
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
1104
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
1105
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1106
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1107
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1108
    info = models.TextField(default="", null=True, blank=True)
1109

    
1110
    class Meta:
1111
        unique_together = ("provider", "third_party_identifier")
1112

    
1113
    def get_user_instance(self):
1114
        d = self.__dict__
1115
        d.pop('_state', None)
1116
        d.pop('id', None)
1117
        d.pop('token', None)
1118
        d.pop('created', None)
1119
        d.pop('info', None)
1120
        user = AstakosUser(**d)
1121

    
1122
        return user
1123

    
1124
    @property
1125
    def realname(self):
1126
        return '%s %s' %(self.first_name, self.last_name)
1127

    
1128
    @realname.setter
1129
    def realname(self, value):
1130
        parts = value.split(' ')
1131
        if len(parts) == 2:
1132
            self.first_name = parts[0]
1133
            self.last_name = parts[1]
1134
        else:
1135
            self.last_name = parts[0]
1136

    
1137
    def save(self, **kwargs):
1138
        if not self.id:
1139
            # set username
1140
            while not self.username:
1141
                username =  uuid.uuid4().hex[:30]
1142
                try:
1143
                    AstakosUser.objects.get(username = username)
1144
                except AstakosUser.DoesNotExist, e:
1145
                    self.username = username
1146
        super(PendingThirdPartyUser, self).save(**kwargs)
1147

    
1148
    def generate_token(self):
1149
        self.password = self.third_party_identifier
1150
        self.last_login = datetime.now()
1151
        self.token = default_token_generator.make_token(self)
1152

    
1153
class SessionCatalog(models.Model):
1154
    session_key = models.CharField(_('session key'), max_length=40)
1155
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1156

    
1157
class MemberJoinPolicy(models.Model):
1158
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1159
    description = models.CharField(_('Description'), max_length=80)
1160

    
1161
    def __str__(self):
1162
        return self.policy
1163

    
1164
class MemberLeavePolicy(models.Model):
1165
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1166
    description = models.CharField(_('Description'), max_length=80)
1167

    
1168
    def __str__(self):
1169
        return self.policy
1170

    
1171
_auto_accept_join = False
1172
def get_auto_accept_join():
1173
    global _auto_accept_join
1174
    if _auto_accept_join is not False:
1175
        return _auto_accept_join
1176
    try:
1177
        auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
1178
    except:
1179
        auto_accept = None
1180
    _auto_accept_join = auto_accept
1181
    return auto_accept
1182

    
1183
_closed_join = False
1184
def get_closed_join():
1185
    global _closed_join
1186
    if _closed_join is not False:
1187
        return _closed_join
1188
    try:
1189
        closed = MemberJoinPolicy.objects.get(policy='closed')
1190
    except:
1191
        closed = None
1192
    _closed_join = closed
1193
    return closed
1194

    
1195
_auto_accept_leave = False
1196
def get_auto_accept_leave():
1197
    global _auto_accept_leave
1198
    if _auto_accept_leave is not False:
1199
        return _auto_accept_leave
1200
    try:
1201
        auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
1202
    except:
1203
        auto_accept = None
1204
    _auto_accept_leave = auto_accept
1205
    return auto_accept
1206

    
1207
_closed_leave = False
1208
def get_closed_leave():
1209
    global _closed_leave
1210
    if _closed_leave is not False:
1211
        return _closed_leave
1212
    try:
1213
        closed = MemberLeavePolicy.objects.get(policy='closed')
1214
    except:
1215
        closed = None
1216
    _closed_leave = closed
1217
    return closeds
1218

    
1219

    
1220
### PROJECTS ###
1221
################
1222

    
1223

    
1224
def synced_model_metaclass(class_name, class_parents, class_attributes):
1225

    
1226
    new_attributes = {}
1227
    sync_attributes = {}
1228

    
1229
    for name, value in class_attributes.iteritems():
1230
        sync, underscore, rest = name.partition('_')
1231
        if sync == 'sync' and underscore == '_':
1232
            sync_attributes[rest] = value
1233
        else:
1234
            new_attributes[name] = value
1235

    
1236
    if 'prefix' not in sync_attributes:
1237
        m = ("you did not specify a 'sync_prefix' attribute "
1238
             "in class '%s'" % (class_name,))
1239
        raise ValueError(m)
1240

    
1241
    prefix = sync_attributes.pop('prefix')
1242
    class_name = sync_attributes.pop('classname', prefix + '_model')
1243

    
1244
    for name, value in sync_attributes.iteritems():
1245
        newname = prefix + '_' + name
1246
        if newname in new_attributes:
1247
            m = ("class '%s' was specified with prefix '%s' "
1248
                 "but it already has an attribute named '%s'"
1249
                 % (class_name, prefix, newname))
1250
            raise ValueError(m)
1251

    
1252
        new_attributes[newname] = value
1253

    
1254
    newclass = type(class_name, class_parents, new_attributes)
1255
    return newclass
1256

    
1257

    
1258
def make_synced(prefix='sync', name='SyncedState'):
1259

    
1260
    the_name = name
1261
    the_prefix = prefix
1262

    
1263
    class SyncedState(models.Model):
1264

    
1265
        sync_classname      = the_name
1266
        sync_prefix         = the_prefix
1267
        __metaclass__       = synced_model_metaclass
1268

    
1269
        sync_new_state      = models.BigIntegerField(null=True)
1270
        sync_synced_state   = models.BigIntegerField(null=True)
1271
        STATUS_SYNCED       = 0
1272
        STATUS_PENDING      = 1
1273
        sync_status         = models.IntegerField(db_index=True)
1274

    
1275
        class Meta:
1276
            abstract = True
1277

    
1278
        class NotSynced(Exception):
1279
            pass
1280

    
1281
        def sync_init_state(self, state):
1282
            self.sync_synced_state = state
1283
            self.sync_new_state = state
1284
            self.sync_status = self.STATUS_SYNCED
1285

    
1286
        def sync_get_status(self):
1287
            return self.sync_status
1288

    
1289
        def sync_set_status(self):
1290
            if self.sync_new_state != self.sync_synced_state:
1291
                self.sync_status = self.STATUS_PENDING
1292
            else:
1293
                self.sync_status = self.STATUS_SYNCED
1294

    
1295
        def sync_set_synced(self):
1296
            self.sync_synced_state = self.sync_new_state
1297
            self.sync_status = self.STATUS_SYNCED
1298

    
1299
        def sync_get_synced_state(self):
1300
            return self.sync_synced_state
1301

    
1302
        def sync_set_new_state(self, new_state):
1303
            self.sync_new_state = new_state
1304
            self.sync_set_status()
1305

    
1306
        def sync_get_new_state(self):
1307
            return self.sync_new_state
1308

    
1309
        def sync_set_synced_state(self, synced_state):
1310
            self.sync_synced_state = synced_state
1311
            self.sync_set_status()
1312

    
1313
        def sync_get_pending_objects(self):
1314
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1315
            return self.objects.filter(**kw)
1316

    
1317
        def sync_get_synced_objects(self):
1318
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1319
            return self.objects.filter(**kw)
1320

    
1321
        def sync_verify_get_synced_state(self):
1322
            status = self.sync_get_status()
1323
            state = self.sync_get_synced_state()
1324
            verified = (status == self.STATUS_SYNCED)
1325
            return state, verified
1326

    
1327
        def sync_is_synced(self):
1328
            state, verified = self.sync_verify_get_synced_state()
1329
            return verified
1330

    
1331
    return SyncedState
1332

    
1333
SyncedState = make_synced(prefix='sync', name='SyncedState')
1334

    
1335

    
1336
class ProjectApplication(models.Model):
1337

    
1338
    applicant               =   models.ForeignKey(
1339
                                    AstakosUser,
1340
                                    related_name='projects_applied',
1341
                                    db_index=True)
1342

    
1343
    project                 =   models.ForeignKey('Project',
1344
                                                  related_name='applications',
1345
                                                  null=True)
1346

    
1347
    state                   =   models.CharField(max_length=80,
1348
                                                 default=UNKNOWN)
1349

    
1350
    owner                   =   models.ForeignKey(
1351
                                    AstakosUser,
1352
                                    related_name='projects_owned',
1353
                                    db_index=True)
1354

    
1355
    precursor_application   =   models.OneToOneField('ProjectApplication',
1356
                                                     null=True,
1357
                                                     blank=True,
1358
                                                     db_index=True)
1359

    
1360
    name                    =   models.CharField(max_length=80)
1361
    homepage                =   models.URLField(max_length=255, null=True,
1362
                                                blank=True)
1363
    description             =   models.TextField(null=True, blank=True)
1364
    start_date              =   models.DateTimeField()
1365
    end_date                =   models.DateTimeField()
1366
    member_join_policy      =   models.ForeignKey(MemberJoinPolicy)
1367
    member_leave_policy     =   models.ForeignKey(MemberLeavePolicy)
1368
    limit_on_members_number =   models.PositiveIntegerField(null=True,
1369
                                                            blank=True)
1370
    resource_grants         =   models.ManyToManyField(
1371
                                    Resource,
1372
                                    null=True,
1373
                                    blank=True,
1374
                                    through='ProjectResourceGrant')
1375
    comments                =   models.TextField(null=True, blank=True)
1376
    issue_date              =   models.DateTimeField()
1377

    
1378
    states_list =   [PENDING, APPROVED, REPLACED, UNKNOWN]
1379
    states      =   dict((k, v) for v, k in enumerate(states_list))
1380

    
1381
    def add_resource_policy(self, service, resource, uplimit, update=True):
1382
        """Raises ObjectDoesNotExist, IntegrityError"""
1383
        resource = Resource.objects.get(service__name=service, name=resource)
1384
        if update:
1385
            ProjectResourceGrant.objects.update_or_create(
1386
                project_application=self,
1387
                resource=resource,
1388
                defaults={'member_capacity': uplimit})
1389
        else:
1390
            q = self.projectresourcegrant_set
1391
            q.create(resource=resource, member_capacity=uplimit)
1392

    
1393
    @property
1394
    def resource_policies(self):
1395
        return self.projectresourcegrant_set.all()
1396

    
1397
    @resource_policies.setter
1398
    def resource_policies(self, policies):
1399
        for p in policies:
1400
            service = p.get('service', None)
1401
            resource = p.get('resource', None)
1402
            uplimit = p.get('uplimit', 0)
1403
            update = p.get('update', True)
1404
            self.add_resource_policy(service, resource, uplimit, update)
1405

    
1406
    @property
1407
    def follower(self):
1408
        try:
1409
            return ProjectApplication.objects.get(precursor_application=self)
1410
        except ProjectApplication.DoesNotExist:
1411
            return
1412

    
1413
    def submit(self, resource_policies, applicant, comments,
1414
               precursor_application=None):
1415

    
1416
        if precursor_application:
1417
            self.precursor_application = precursor_application
1418
            self.owner = precursor_application.owner
1419
        else:
1420
            self.owner = applicant
1421

    
1422
        self.id = None
1423
        self.applicant = applicant
1424
        self.comments = comments
1425
        self.issue_date = datetime.now()
1426
        self.state = PENDING
1427
        self.resource_policies = resource_policies
1428
        self.save()
1429

    
1430
    def _get_project(self):
1431
        precursor = self
1432
        while precursor:
1433
            try:
1434
                project = precursor.project
1435
                return project
1436
            except Project.DoesNotExist:
1437
                pass
1438
            precursor = precursor.precursor_application
1439

    
1440
        return None
1441

    
1442
    def approve(self, approval_user=None):
1443
        """
1444
        If approval_user then during owner membership acceptance
1445
        it is checked whether the request_user is eligible.
1446

1447
        Raises:
1448
            PermissionDenied
1449
        """
1450

    
1451
        if not transaction.is_managed():
1452
            raise AssertionError("NOPE")
1453

    
1454
        new_project_name = self.name
1455
        if self.state != PENDING:
1456
            m = _("cannot approve: project '%s' in state '%s'") % (
1457
                    new_project_name, self.state)
1458
            raise PermissionDenied(m) # invalid argument
1459

    
1460
        now = datetime.now()
1461
        project = self._get_project()
1462
        if project is None:
1463
            try:
1464
                # needs SERIALIZABLE
1465
                conflicting_project = Project.objects.get(name=new_project_name)
1466
                if conflicting_project.is_alive:
1467
                    m = _("cannot approve: project with name '%s' "
1468
                          "already exists (serial: %s)") % (
1469
                            new_project_name, conflicting_project.id)
1470
                    raise PermissionDenied(m) # invalid argument
1471
            except Project.DoesNotExist:
1472
                pass
1473
            project = Project(creation_date=now)
1474

    
1475
        project.application = self
1476

    
1477
        # This will block while syncing,
1478
        # but unblock before setting the membership state.
1479
        # See ProjectMembership.set_sync()
1480
        project.set_membership_pending_sync()
1481

    
1482
        project.last_approval_date = now
1483
        project.save()
1484
        #ProjectMembership.add_to_project(self)
1485
        project.add_member(self.owner)
1486

    
1487
        precursor = self.precursor_application
1488
        while precursor:
1489
            precursor.state = REPLACED
1490
            precursor.save()
1491
            precursor = precursor.precursor_application
1492

    
1493
        self.state = APPROVED
1494
        self.save()
1495

    
1496
        transaction.commit()
1497
        trigger_sync()
1498

    
1499

    
1500
class ProjectResourceGrant(models.Model):
1501

    
1502
    resource                =   models.ForeignKey(Resource)
1503
    project_application     =   models.ForeignKey(ProjectApplication,
1504
                                                  null=True)
1505
    project_capacity        =   models.BigIntegerField(null=True)
1506
    project_import_limit    =   models.BigIntegerField(null=True)
1507
    project_export_limit    =   models.BigIntegerField(null=True)
1508
    member_capacity         =   models.BigIntegerField(null=True)
1509
    member_import_limit     =   models.BigIntegerField(null=True)
1510
    member_export_limit     =   models.BigIntegerField(null=True)
1511

    
1512
    objects = ExtendedManager()
1513

    
1514
    class Meta:
1515
        unique_together = ("resource", "project_application")
1516

    
1517

    
1518
class Project(models.Model):
1519

    
1520
    application                 =   models.OneToOneField(
1521
                                            ProjectApplication,
1522
                                            related_name='app_project')
1523
    last_approval_date          =   models.DateTimeField(null=True)
1524

    
1525
    members                     =   models.ManyToManyField(
1526
                                            AstakosUser,
1527
                                            through='ProjectMembership')
1528

    
1529
    termination_start_date      =   models.DateTimeField(null=True)
1530
    termination_date            =   models.DateTimeField(null=True)
1531

    
1532
    creation_date               =   models.DateTimeField()
1533
    name                        =   models.CharField(
1534
                                            max_length=80,
1535
                                            db_index=True,
1536
                                            unique=True)
1537

    
1538
    @property
1539
    def violated_resource_grants(self):
1540
        return False
1541

    
1542
    @property
1543
    def violated_members_number_limit(self):
1544
        application = self.application
1545
        return len(self.approved_members) > application.limit_on_members_number
1546

    
1547
    @property
1548
    def is_terminated(self):
1549
        return bool(self.termination)
1550

    
1551
    @property
1552
    def is_still_approved(self):
1553
        return bool(self.last_approval_date)
1554

    
1555
    @property
1556
    def is_active(self):
1557
        if (self.is_terminated or
1558
            not self.is_still_approved or
1559
            self.violated_resource_grants):
1560
            return False
1561
#         if self.violated_members_number_limit:
1562
#             return False
1563
        return True
1564
    
1565
    @property
1566
    def is_suspended(self):
1567
        if (self.is_terminated or
1568
            self.is_still_approved or
1569
            not self.violated_resource_grants):
1570
            return False
1571
#             if not self.violated_members_number_limit:
1572
#                 return False
1573
        return True
1574

    
1575
    @property
1576
    def is_alive(self):
1577
        return self.is_active or self.is_suspended
1578

    
1579
    @property
1580
    def is_inconsistent(self):
1581
        now = datetime.now()
1582
        if self.creation_date > now:
1583
            return True
1584
        if self.last_approval_date > now:
1585
            return True
1586
        if self.terminaton_date > now:
1587
            return True
1588
        return False
1589

    
1590
    @property
1591
    def approved_memberships(self):
1592
        ACCEPTED = ProjectMembership.ACCEPTED
1593
        PENDING  = ProjectMembership.PENDING
1594
        return self.projectmembership_set.filter(
1595
            Q(state=ACCEPTED) | Q(state=PENDING))
1596

    
1597
    @property
1598
    def approved_members(self):
1599
        return [m.person for m in self.approved_memberships]
1600

    
1601
    def set_membership_pending_sync(self):
1602
        ACCEPTED = ProjectMembership.ACCEPTED
1603
        PENDING  = ProjectMembership.PENDING
1604
        sfu = self.projectmembership_set.select_for_update()
1605
        members = sfu.filter(Q(state=ACCEPTED) | Q(state=PENDING))
1606

    
1607
        for member in members:
1608
            member.state = member.PENDING
1609
            member.save()
1610

    
1611
    def add_member(self, user):
1612
        """
1613
        Raises:
1614
            django.exceptions.PermissionDenied
1615
            astakos.im.models.AstakosUser.DoesNotExist
1616
        """
1617
        if isinstance(user, int):
1618
            user = AstakosUser.objects.get(user=user)
1619

    
1620
        m, created = ProjectMembership.objects.get_or_create(
1621
            person=user, project=self
1622
        )
1623
        m.accept()
1624

    
1625
    def remove_member(self, user):
1626
        """
1627
        Raises:
1628
            django.exceptions.PermissionDenied
1629
            astakos.im.models.AstakosUser.DoesNotExist
1630
            astakos.im.models.ProjectMembership.DoesNotExist
1631
        """
1632
        if isinstance(user, int):
1633
            user = AstakosUser.objects.get(user=user)
1634

    
1635
        m = ProjectMembership.objects.get(person=user, project=self)
1636
        m.remove()
1637

    
1638
    def terminate(self):
1639
        self.termination_start_date = datetime.now()
1640
        self.terminaton_date = None
1641
        self.save()
1642

    
1643
        rejected = self.sync()
1644
        if not rejected:
1645
            self.termination_start_date = None
1646
            self.termination_date = datetime.now()
1647
            self.save()
1648

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

    
1660
    def suspend(self):
1661
        self.last_approval_date = None
1662
        self.save()
1663
        self.sync()
1664

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

    
1676

    
1677

    
1678
class ExclusiveOrRaise(object):
1679
    """Context Manager to exclusively execute a critical code section.
1680
       The exclusion must be global.
1681
       (IPC semaphores will not protect across OS,
1682
        DB locks will if it's the same DB)
1683
    """
1684

    
1685
    class Busy(Exception):
1686
        pass
1687

    
1688
    def __init__(self, locked=False):
1689
        init = 0 if locked else 1
1690
        from multiprocessing import Semaphore
1691
        self._sema = Semaphore(init)
1692

    
1693
    def enter(self):
1694
        acquired = self._sema.acquire(False)
1695
        if not acquired:
1696
            raise self.Busy()
1697

    
1698
    def leave(self):
1699
        self._sema.release()
1700

    
1701
    def __enter__(self):
1702
        self.enter()
1703
        return self
1704

    
1705
    def __exit__(self, exc_type, exc_value, exc_traceback):
1706
        self.leave()
1707

    
1708

    
1709
exclusive_or_raise = ExclusiveOrRaise(locked=False)
1710

    
1711

    
1712
class ProjectMembership(models.Model):
1713

    
1714
    person              =   models.ForeignKey(AstakosUser)
1715
    request_date        =   models.DateField(default=datetime.now())
1716
    project             =   models.ForeignKey(Project)
1717

    
1718
    state               =   models.IntegerField(default=0)
1719
    application         =   models.ForeignKey(
1720
                                ProjectApplication,
1721
                                null=True,
1722
                                related_name='memberships')
1723
    pending_application =   models.ForeignKey(
1724
                                ProjectApplication,
1725
                                null=True,
1726
                                related_name='pending_memebrships')
1727
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1728

    
1729
    acceptance_date     =   models.DateField(null=True, db_index=True)
1730
    leave_request_date  =   models.DateField(null=True)
1731

    
1732
    objects     =   ForUpdateManager()
1733

    
1734
    REQUESTED   =   0
1735
    PENDING     =   1
1736
    ACCEPTED    =   2
1737
    REMOVING    =   3
1738
    REMOVED     =   4
1739

    
1740
    class Meta:
1741
        unique_together = ("person", "project")
1742
        #index_together = [["project", "state"]]
1743

    
1744
    def __str__(self):
1745
        return _("<'%s' membership in project '%s'>") % (
1746
                self.person.username, self.project.application)
1747

    
1748
    __repr__ = __str__
1749

    
1750
    def __init__(self, *args, **kwargs):
1751
        self.state = self.REQUESTED
1752
        super(ProjectMembership, self).__init__(*args, **kwargs)
1753

    
1754
    def _set_history_item(self, reason, date=None):
1755
        if isinstance(reason, basestring):
1756
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1757

    
1758
        history_item = ProjectMembershipHistory(
1759
                            serial=self.id,
1760
                            person=self.person,
1761
                            project=self.project,
1762
                            date=date,
1763
                            reason=reason)
1764
        history_item.save()
1765
        serial = history_item.id
1766

    
1767
    def accept(self):
1768
        state = self.state
1769
        if state != self.REQUESTED:
1770
            m = _("%s: attempt to accept in state [%s]") % (self, state)
1771
            raise AssertionError(m)
1772

    
1773
        now = datetime.now()
1774
        self.acceptance_date = now
1775
        self._set_history_item(reason='ACCEPT', date=now)
1776
        self.state = self.PENDING
1777
        self.save()
1778
        trigger_sync()
1779

    
1780
    def remove(self):
1781
        if state != self.ACCEPTED:
1782
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1783
            raise AssertionError(m)
1784

    
1785
        self._set_history_item(reason='REMOVE')
1786
        self.state = self.REMOVING
1787
        self.save()
1788
        trigger_sync()
1789

    
1790
    def reject(self):
1791
        if state != self.REQUESTED:
1792
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1793
            raise AssertionError(m)
1794

    
1795
        # rejected requests don't need sync,
1796
        # because they were never effected
1797
        self._set_history_item(reason='REJECT')
1798
        self.delete()
1799

    
1800
    def get_diff_quotas(self, limits_list=None, remove=False):
1801
        if limits_list is None:
1802
            limits_list = []
1803

    
1804
        append = limits_list.append
1805
        holder = self.person.username
1806
        key = "1"
1807

    
1808
        tmp_grants = {}
1809
        synced_application = self.application
1810
        if synced_application is not None:
1811
            # first, inverse all current limits, and index them by resource name
1812
            cur_grants = synced_application.resource_grants.all()
1813
            f = -1
1814
            for grant in cur_grants:
1815
                name = grant.resource.name
1816
                tmp_grants[name] = QuotaLimits(
1817
                                holder       = holder,
1818
                                resource     = name,
1819
                                capacity     = f * grant.member_capacity,
1820
                                import_limit = f * grant.member_import_limit,
1821
                                export_limit = f * grant.member_export_limit)
1822

    
1823
        if not remove:
1824
            # second, add each new limit to its inverted current
1825
            new_grants = self.pending_application.resource_grants.all()
1826
            for new_grant in new_grants:
1827
                name = grant.resource.name
1828
                cur_grant = tmp_grants.pop(name, None)
1829
                if cur_grant is None:
1830
                    # if limits on a new resource, set 0 current values
1831
                    capacity = 0
1832
                    import_limit = 0
1833
                    export_limit = 0
1834
                else:
1835
                    capacity = cur_grant.capacity
1836
                    import_limit = cur_grant.import_limit
1837
                    export_limit = cur_grant.export_limit
1838

    
1839
                capacity += new_grant.member_capacity
1840
                import_limit += new_grant.member_import_limit
1841
                export_limit += new_grant.member_export_limit
1842

    
1843
                append(QuotaLimits(holder       = holder,
1844
                                   key          = key,
1845
                                   resource     = name,
1846
                                   capacity     = capacity,
1847
                                   import_limit = import_limit,
1848
                                   export_limit = export_limit))
1849

    
1850
        # third, append all the inverted current limits for removed resources
1851
        limits_list.extend(tmp_grants.itervalues())
1852
        return limits_list
1853

    
1854
    def set_sync(self):
1855
        state = self.state
1856
        if state == self.PENDING:
1857
            pending_application = self.pending_application
1858
            if pending_application is None:
1859
                m = _("%s: attempt to sync an empty pending application") % (
1860
                    self, state)
1861
                raise AssertionError(m)
1862
            self.application = pending_application
1863
            self.pending_application = None
1864
            self.pending_serial = None
1865

    
1866
            # project.application may have changed in the meantime,
1867
            # in which case we stay PENDING;
1868
            # we are safe to check due to select_for_update
1869
            if self.application == self.project.application:
1870
                self.state = self.ACCEPTED
1871
            self.save()
1872
        elif state == self.REMOVING:
1873
            self.delete()
1874
        else:
1875
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1876
            raise AssertionError(m)
1877

    
1878
class Serial(models.Model):
1879
    serial  =   models.AutoField(primary_key=True)
1880

    
1881
def new_serial():
1882
    s = Serial.objects.create()
1883
    return s.serial
1884

    
1885
def sync_finish_serials():
1886
    serials_to_ack = set(qh_query_serials([]))
1887
    sfu = ProjectMembership.objects.select_for_update()
1888
    memberships = sfu.filter(pending_serial__isnull=False)
1889

    
1890
    for membership in memberships:
1891
        serial = membership.pending_serial
1892
        # just make sure the project row is selected for update
1893
        project = membership.project
1894
        if serial in serials_to_ack:
1895
            membership.set_sync()
1896

    
1897
    transaction.commit()
1898
    qh_ack_serials(list(serials_to_ack))
1899

    
1900
def sync_projects():
1901
    sync_finish_serials()
1902

    
1903
    PENDING = ProjectMembership.PENDING
1904
    REMOVING = ProjectMembership.REMOVING
1905
    objects = ProjectMembership.objects.select_for_update()
1906

    
1907
    quotas = []
1908

    
1909
    serial = new_serial()
1910

    
1911
    pending = objects.filter(state=PENDING)
1912
    for membership in pending:
1913

    
1914
        if membership.pending_application:
1915
            m = "%s: impossible: pending_application is not None (%s)" % (
1916
                membership, membership.pending_application)
1917
            raise AssertionError(m)
1918
        if membership.pending_serial:
1919
            m = "%s: impossible: pending_serial is not None (%s)" % (
1920
                membership, membership.pending_serial)
1921
            raise AssertionError(m)
1922

    
1923
        membership.pending_application = membership.project.application
1924
        membership.pending_serial = serial
1925
        membership.get_diff_quotas(quotas)
1926
        membership.save()
1927

    
1928
    removing = objects.filter(state=REMOVING)
1929
    for membership in removing:
1930

    
1931
        if membership.pending_application:
1932
            m = ("%s: impossible: removing pending_application is not None (%s)"
1933
                % (membership, membership.pending_application))
1934
            raise AssertionError(m)
1935
        if membership.pending_serial:
1936
            m = "%s: impossible: pending_serial is not None (%s)" % (
1937
                membership, membership.pending_serial)
1938
            raise AssertionError(m)
1939

    
1940
        membership.pending_serial = serial
1941
        membership.get_diff_quotas(quotas, remove=True)
1942
        membership.save()
1943

    
1944
    transaction.commit()
1945
    # ProjectApplication.approve() unblocks here
1946
    # and can set PENDING an already PENDING membership
1947
    # which has been scheduled to sync with the old project.application
1948
    # Need to check in ProjectMembership.set_sync()
1949

    
1950
    qh_add_quota(serial, quotas)
1951
    sync_finish_serials()
1952

    
1953

    
1954
def trigger_sync(retries=3, retry_wait=1.0):
1955
    cursor = connection.cursor()
1956
    locked = True
1957
    try:
1958
        while 1:
1959
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1960
            r = cursor.fetchone()
1961
            if r is None:
1962
                m = "Impossible"
1963
                raise AssertionError(m)
1964
            locked = r[0]
1965
            if locked:
1966
                break
1967

    
1968
            retries -= 1
1969
            if retries <= 0:
1970
                return False
1971
            sleep(retry_wait)
1972

    
1973
        sync_projects()
1974
        return True
1975

    
1976
    finally:
1977
        if locked:
1978
            cursor.execute("SELECT pg_advisory_unlock(1)")
1979
            cursor.fetchall()
1980

    
1981

    
1982
class ProjectMembershipHistory(models.Model):
1983
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1984
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1985

    
1986
    person  =   models.ForeignKey(AstakosUser)
1987
    project =   models.ForeignKey(Project)
1988
    date    =   models.DateField(default=datetime.now)
1989
    reason  =   models.IntegerField()
1990
    serial  =   models.BigIntegerField()
1991
    
1992

    
1993
def filter_queryset_by_property(q, property):
1994
    """
1995
    Incorporate list comprehension for filtering querysets by property
1996
    since Queryset.filter() operates on the database level.
1997
    """
1998
    return (p for p in q if getattr(p, property, False))
1999

    
2000
def get_alive_projects():
2001
    return filter_queryset_by_property(
2002
        Project.objects.all(),
2003
        'is_alive'
2004
    )
2005

    
2006
def get_active_projects():
2007
    return filter_queryset_by_property(
2008
        Project.objects.all(),
2009
        'is_active'
2010
    )
2011

    
2012
def _create_object(model, **kwargs):
2013
    o = model.objects.create(**kwargs)
2014
    o.save()
2015
    return o
2016

    
2017

    
2018
def create_astakos_user(u):
2019
    try:
2020
        AstakosUser.objects.get(user_ptr=u.pk)
2021
    except AstakosUser.DoesNotExist:
2022
        extended_user = AstakosUser(user_ptr_id=u.pk)
2023
        extended_user.__dict__.update(u.__dict__)
2024
        extended_user.save()
2025
        if not extended_user.has_auth_provider('local'):
2026
            extended_user.add_auth_provider('local')
2027
    except BaseException, e:
2028
        logger.exception(e)
2029

    
2030

    
2031
def fix_superusers(sender, **kwargs):
2032
    # Associate superusers with AstakosUser
2033
    admins = User.objects.filter(is_superuser=True)
2034
    for u in admins:
2035
        create_astakos_user(u)
2036
post_syncdb.connect(fix_superusers)
2037

    
2038

    
2039
def user_post_save(sender, instance, created, **kwargs):
2040
    if not created:
2041
        return
2042
    create_astakos_user(instance)
2043
post_save.connect(user_post_save, sender=User)
2044

    
2045

    
2046
# def astakosuser_pre_save(sender, instance, **kwargs):
2047
#     instance.aquarium_report = False
2048
#     instance.new = False
2049
#     try:
2050
#         db_instance = AstakosUser.objects.get(id=instance.id)
2051
#     except AstakosUser.DoesNotExist:
2052
#         # create event
2053
#         instance.aquarium_report = True
2054
#         instance.new = True
2055
#     else:
2056
#         get = AstakosUser.__getattribute__
2057
#         l = filter(lambda f: get(db_instance, f) != get(instance, f),
2058
#                    BILLING_FIELDS)
2059
#         instance.aquarium_report = True if l else False
2060
# pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
2061

    
2062
# def set_default_group(user):
2063
#     try:
2064
#         default = AstakosGroup.objects.get(name='default')
2065
#         Membership(
2066
#             group=default, person=user, date_joined=datetime.now()).save()
2067
#     except AstakosGroup.DoesNotExist, e:
2068
#         logger.exception(e)
2069

    
2070

    
2071
def astakosuser_post_save(sender, instance, created, **kwargs):
2072
#     if instance.aquarium_report:
2073
#         report_user_event(instance, create=instance.new)
2074
    if not created:
2075
        return
2076
#     set_default_group(instance)
2077
    # TODO handle socket.error & IOError
2078
    register_users((instance,))
2079
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2080

    
2081

    
2082
def resource_post_save(sender, instance, created, **kwargs):
2083
    if not created:
2084
        return
2085
    register_resources((instance,))
2086
post_save.connect(resource_post_save, sender=Resource)
2087

    
2088

    
2089
# def on_quota_disturbed(sender, users, **kwargs):
2090
# #     print '>>>', locals()
2091
#     if not users:
2092
#         return
2093
#     send_quota(users)
2094
#
2095
# quota_disturbed = Signal(providing_args=["users"])
2096
# quota_disturbed.connect(on_quota_disturbed)
2097

    
2098

    
2099
# def send_quota_disturbed(sender, instance, **kwargs):
2100
#     users = []
2101
#     extend = users.extend
2102
#     if sender == Membership:
2103
#         if not instance.group.is_enabled:
2104
#             return
2105
#         extend([instance.person])
2106
#     elif sender == AstakosUserQuota:
2107
#         extend([instance.user])
2108
#     elif sender == AstakosGroupQuota:
2109
#         if not instance.group.is_enabled:
2110
#             return
2111
#         extend(instance.group.astakosuser_set.all())
2112
#     elif sender == AstakosGroup:
2113
#         if not instance.is_enabled:
2114
#             return
2115
#     quota_disturbed.send(sender=sender, users=users)
2116
# post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
2117
# post_delete.connect(send_quota_disturbed, sender=Membership)
2118
# post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
2119
# post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
2120
# post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
2121
# post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
2122

    
2123

    
2124
def renew_token(sender, instance, **kwargs):
2125
    if not instance.auth_token:
2126
        instance.renew_token()
2127
pre_save.connect(renew_token, sender=AstakosUser)
2128
pre_save.connect(renew_token, sender=Service)