Statistics
| Branch: | Tag: | Revision:

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

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

    
643
        if not provider_settings.is_available_for_add():
644
            return False
645

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

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

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

    
663
        return True
664

    
665
    def can_remove_auth_provider(self, module):
666
        provider = auth_providers.get_provider(module)
667
        existing = self.get_active_auth_providers()
668
        existing_for_provider = self.get_active_auth_providers(module=module)
669

    
670
        if len(existing) <= 1:
671
            return False
672

    
673
        if len(existing_for_provider) == 1 and provider.is_required():
674
            return False
675

    
676
        return True
677

    
678
    def can_change_password(self):
679
        return self.has_auth_provider('local', auth_backend='astakos')
680

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

    
688
    def has_auth_provider(self, provider, **kwargs):
689
        return bool(self.auth_providers.filter(module=provider,
690
                                               **kwargs).count())
691

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

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

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

    
714
        provider = self.add_auth_provider(pending.provider,
715
                               identifier=pending.third_party_identifier,
716
                                affiliation=pending.affiliation,
717
                                          provider_info=pending.info)
718

    
719
        if email_re.match(pending.email or '') and pending.email != self.email:
720
            self.additionalmail_set.get_or_create(email=pending.email)
721

    
722
        pending.delete()
723
        return provider
724

    
725
    def remove_auth_provider(self, provider, **kwargs):
726
        self.auth_providers.get(module=provider, **kwargs).delete()
727

    
728
    # user urls
729
    def get_resend_activation_url(self):
730
        return reverse('send_activation', kwargs={'user_id': self.pk})
731

    
732
    def get_provider_remove_url(self, module, **kwargs):
733
        return reverse('remove_auth_provider', kwargs={
734
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
735

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

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

    
748
    def get_auth_providers(self):
749
        return self.auth_providers.all()
750

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

    
760
        return providers
761

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

    
769
    @property
770
    def auth_providers_display(self):
771
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
772

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

    
798
        return mark_safe(message + u' '+ msg_extra)
799

    
800

    
801
class AstakosUserAuthProviderManager(models.Manager):
802

    
803
    def active(self, **filters):
804
        return self.filter(active=True, **filters)
805

    
806

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

    
825
    objects = AstakosUserAuthProviderManager()
826

    
827
    class Meta:
828
        unique_together = (('identifier', 'module', 'user'), )
829
        ordering = ('module', 'created')
830

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

    
840
        for key,value in self.info.iteritems():
841
            setattr(self, 'info_%s' % key, value)
842

    
843

    
844
    @property
845
    def settings(self):
846
        return auth_providers.get_provider(self.module)
847

    
848
    @property
849
    def details_display(self):
850
        try:
851
          return self.settings.get_details_tpl_display % self.__dict__
852
        except:
853
          return ''
854

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

    
868
    def can_remove(self):
869
        return self.user.can_remove_auth_provider(self.module)
870

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

    
878
    def __repr__(self):
879
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
880

    
881
    def __unicode__(self):
882
        if self.identifier:
883
            return "%s:%s" % (self.module, self.identifier)
884
        if self.auth_backend:
885
            return "%s:%s" % (self.module, self.auth_backend)
886
        return self.module
887

    
888
    def save(self, *args, **kwargs):
889
        self.info_data = json.dumps(self.info)
890
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
891

    
892

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

    
899
    class Meta:
900
        unique_together = ("person", "group")
901

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

    
908
    @property
909
    def is_approved(self):
910
        if self.date_joined:
911
            return True
912
        return False
913

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

    
924
    def disapprove(self):
925
        approved = self.is_approved()
926
        self.delete()
927
        if approved:
928
            quota_disturbed.send(sender=self, users=(self.person,))
929

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

    
957
    update_or_create = _update_or_create
958

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

    
966
    class Meta:
967
        unique_together = ("resource", "group")
968

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

    
976
    class Meta:
977
        unique_together = ("resource", "user")
978

    
979

    
980
class ApprovalTerms(models.Model):
981
    """
982
    Model for approval terms
983
    """
984

    
985
    date = models.DateTimeField(
986
        _('Issue date'), db_index=True, default=datetime.now())
987
    location = models.CharField(_('Terms location'), max_length=255)
988

    
989

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

    
1003
    def __init__(self, *args, **kwargs):
1004
        super(Invitation, self).__init__(*args, **kwargs)
1005
        if not self.id:
1006
            self.code = _generate_invitation_code()
1007

    
1008
    def consume(self):
1009
        self.is_consumed = True
1010
        self.consumed = datetime.now()
1011
        self.save()
1012

    
1013
    def __unicode__(self):
1014
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1015

    
1016

    
1017
class EmailChangeManager(models.Manager):
1018

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

1025
        If the key is valid and has not expired, return the ``User``
1026
        after activating.
1027

1028
        If the key is not valid or has expired, return ``None``.
1029

1030
        If the key is valid but the ``User`` is already active,
1031
        return ``None``.
1032

1033
        After successful email change the activation record is deleted.
1034

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

    
1063

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

    
1074
    objects = EmailChangeManager()
1075

    
1076
    def get_url(self):
1077
        return reverse('email_change_confirm',
1078
                      kwargs={'activation_key': self.activation_key})
1079

    
1080
    def activation_key_expired(self):
1081
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1082
        return self.requested_at + expiration_date < datetime.now()
1083

    
1084

    
1085
class AdditionalMail(models.Model):
1086
    """
1087
    Model for registring invitations
1088
    """
1089
    owner = models.ForeignKey(AstakosUser)
1090
    email = models.EmailField()
1091

    
1092

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

    
1102

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

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

    
1126
    class Meta:
1127
        unique_together = ("provider", "third_party_identifier")
1128

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

    
1138
        return user
1139

    
1140
    @property
1141
    def realname(self):
1142
        return '%s %s' %(self.first_name, self.last_name)
1143

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

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

    
1164
    def generate_token(self):
1165
        self.password = self.third_party_identifier
1166
        self.last_login = datetime.now()
1167
        self.token = default_token_generator.make_token(self)
1168

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

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

    
1177
    def __str__(self):
1178
        return self.policy
1179

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

    
1184
    def __str__(self):
1185
        return self.policy
1186

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

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

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

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

    
1235

    
1236
### PROJECTS ###
1237
################
1238

    
1239

    
1240
def synced_model_metaclass(class_name, class_parents, class_attributes):
1241

    
1242
    new_attributes = {}
1243
    sync_attributes = {}
1244

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

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

    
1257
    prefix = sync_attributes.pop('prefix')
1258
    class_name = sync_attributes.pop('classname', prefix + '_model')
1259

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

    
1268
        new_attributes[newname] = value
1269

    
1270
    newclass = type(class_name, class_parents, new_attributes)
1271
    return newclass
1272

    
1273

    
1274
def make_synced(prefix='sync', name='SyncedState'):
1275

    
1276
    the_name = name
1277
    the_prefix = prefix
1278

    
1279
    class SyncedState(models.Model):
1280

    
1281
        sync_classname      = the_name
1282
        sync_prefix         = the_prefix
1283
        __metaclass__       = synced_model_metaclass
1284

    
1285
        sync_new_state      = models.BigIntegerField(null=True)
1286
        sync_synced_state   = models.BigIntegerField(null=True)
1287
        STATUS_SYNCED       = 0
1288
        STATUS_PENDING      = 1
1289
        sync_status         = models.IntegerField(db_index=True)
1290

    
1291
        class Meta:
1292
            abstract = True
1293

    
1294
        class NotSynced(Exception):
1295
            pass
1296

    
1297
        def sync_init_state(self, state):
1298
            self.sync_synced_state = state
1299
            self.sync_new_state = state
1300
            self.sync_status = self.STATUS_SYNCED
1301

    
1302
        def sync_get_status(self):
1303
            return self.sync_status
1304

    
1305
        def sync_set_status(self):
1306
            if self.sync_new_state != self.sync_synced_state:
1307
                self.sync_status = self.STATUS_PENDING
1308
            else:
1309
                self.sync_status = self.STATUS_SYNCED
1310

    
1311
        def sync_set_synced(self):
1312
            self.sync_synced_state = self.sync_new_state
1313
            self.sync_status = self.STATUS_SYNCED
1314

    
1315
        def sync_get_synced_state(self):
1316
            return self.sync_synced_state
1317

    
1318
        def sync_set_new_state(self, new_state):
1319
            self.sync_new_state = new_state
1320
            self.sync_set_status()
1321

    
1322
        def sync_get_new_state(self):
1323
            return self.sync_new_state
1324

    
1325
        def sync_set_synced_state(self, synced_state):
1326
            self.sync_synced_state = synced_state
1327
            self.sync_set_status()
1328

    
1329
        def sync_get_pending_objects(self):
1330
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1331
            return self.objects.filter(**kw)
1332

    
1333
        def sync_get_synced_objects(self):
1334
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1335
            return self.objects.filter(**kw)
1336

    
1337
        def sync_verify_get_synced_state(self):
1338
            status = self.sync_get_status()
1339
            state = self.sync_get_synced_state()
1340
            verified = (status == self.STATUS_SYNCED)
1341
            return state, verified
1342

    
1343
        def sync_is_synced(self):
1344
            state, verified = self.sync_verify_get_synced_state()
1345
            return verified
1346

    
1347
    return SyncedState
1348

    
1349
SyncedState = make_synced(prefix='sync', name='SyncedState')
1350

    
1351

    
1352
class ProjectApplication(models.Model):
1353

    
1354
    applicant               =   models.ForeignKey(
1355
                                    AstakosUser,
1356
                                    related_name='projects_applied',
1357
                                    db_index=True)
1358

    
1359
    project                 =   models.ForeignKey('Project',
1360
                                                  related_name='applications',
1361
                                                  null=True)
1362

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1449
        return None
1450

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

1456
        Raises:
1457
            PermissionDenied
1458
        """
1459

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

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

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

    
1484
        project.application = self
1485

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

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

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

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

    
1505
        transaction.commit()
1506
        trigger_sync()
1507

    
1508

    
1509
class ProjectResourceGrant(models.Model):
1510

    
1511
    resource                =   models.ForeignKey(Resource)
1512
    project_application     =   models.ForeignKey(ProjectApplication,
1513
                                                  null=True)
1514
    project_capacity        =   models.BigIntegerField(null=True)
1515
    project_import_limit    =   models.BigIntegerField(null=True)
1516
    project_export_limit    =   models.BigIntegerField(null=True)
1517
    member_capacity         =   models.BigIntegerField(null=True)
1518
    member_import_limit     =   models.BigIntegerField(null=True)
1519
    member_export_limit     =   models.BigIntegerField(null=True)
1520

    
1521
    objects = ExtendedManager()
1522

    
1523
    class Meta:
1524
        unique_together = ("resource", "project_application")
1525

    
1526

    
1527
class Project(models.Model):
1528

    
1529
    application                 =   models.OneToOneField(
1530
                                            ProjectApplication,
1531
                                            related_name='app_project')
1532
    last_approval_date          =   models.DateTimeField(null=True)
1533

    
1534
    members                     =   models.ManyToManyField(
1535
                                            AstakosUser,
1536
                                            through='ProjectMembership')
1537

    
1538
    termination_start_date      =   models.DateTimeField(null=True)
1539
    termination_date            =   models.DateTimeField(null=True)
1540

    
1541
    creation_date               =   models.DateTimeField()
1542
    name                        =   models.CharField(
1543
                                            max_length=80,
1544
                                            db_index=True,
1545
                                            unique=True)
1546

    
1547
    @property
1548
    def violated_resource_grants(self):
1549
        return False
1550

    
1551
    @property
1552
    def violated_members_number_limit(self):
1553
        application = self.application
1554
        return len(self.approved_members) > application.limit_on_members_number
1555

    
1556
    @property
1557
    def is_terminated(self):
1558
        return bool(self.termination)
1559

    
1560
    @property
1561
    def is_still_approved(self):
1562
        return bool(self.last_approval_date)
1563

    
1564
    @property
1565
    def is_active(self):
1566
        if (self.is_terminated or
1567
            not self.is_still_approved or
1568
            self.violated_resource_grants):
1569
            return False
1570
#         if self.violated_members_number_limit:
1571
#             return False
1572
        return True
1573
    
1574
    @property
1575
    def is_suspended(self):
1576
        if (self.is_terminated or
1577
            self.is_still_approved or
1578
            not self.violated_resource_grants):
1579
            return False
1580
#             if not self.violated_members_number_limit:
1581
#                 return False
1582
        return True
1583

    
1584
    @property
1585
    def is_alive(self):
1586
        return self.is_active or self.is_suspended
1587

    
1588
    @property
1589
    def is_inconsistent(self):
1590
        now = datetime.now()
1591
        if self.creation_date > now:
1592
            return True
1593
        if self.last_approval_date > now:
1594
            return True
1595
        if self.terminaton_date > now:
1596
            return True
1597
        return False
1598

    
1599
    @property
1600
    def approved_memberships(self):
1601
        ACCEPTED = ProjectMembership.ACCEPTED
1602
        PENDING  = ProjectMembership.PENDING
1603
        return self.projectmembership_set.filter(
1604
            Q(state=ACCEPTED) | Q(state=PENDING))
1605

    
1606
    @property
1607
    def approved_members(self):
1608
        return [m.person for m in self.approved_memberships]
1609

    
1610
    def set_membership_pending_sync(self):
1611
        ACCEPTED = ProjectMembership.ACCEPTED
1612
        PENDING  = ProjectMembership.PENDING
1613
        sfu = self.projectmembership_set.select_for_update()
1614
        members = sfu.filter(Q(state=ACCEPTED) | Q(state=PENDING))
1615

    
1616
        for member in members:
1617
            member.state = member.PENDING
1618
            member.save()
1619

    
1620
    def add_member(self, user):
1621
        """
1622
        Raises:
1623
            django.exceptions.PermissionDenied
1624
            astakos.im.models.AstakosUser.DoesNotExist
1625
        """
1626
        if isinstance(user, int):
1627
            user = AstakosUser.objects.get(user=user)
1628

    
1629
        m, created = ProjectMembership.objects.get_or_create(
1630
            person=user, project=self
1631
        )
1632
        m.accept()
1633

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

    
1644
        m = ProjectMembership.objects.get(person=user, project=self)
1645
        m.remove()
1646

    
1647
    def terminate(self):
1648
        self.termination_start_date = datetime.now()
1649
        self.terminaton_date = None
1650
        self.save()
1651

    
1652
        rejected = self.sync()
1653
        if not rejected:
1654
            self.termination_start_date = None
1655
            self.termination_date = datetime.now()
1656
            self.save()
1657

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

    
1669
    def suspend(self):
1670
        self.last_approval_date = None
1671
        self.save()
1672
        self.sync()
1673

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

    
1685

    
1686

    
1687
class ExclusiveOrRaise(object):
1688
    """Context Manager to exclusively execute a critical code section.
1689
       The exclusion must be global.
1690
       (IPC semaphores will not protect across OS,
1691
        DB locks will if it's the same DB)
1692
    """
1693

    
1694
    class Busy(Exception):
1695
        pass
1696

    
1697
    def __init__(self, locked=False):
1698
        init = 0 if locked else 1
1699
        from multiprocessing import Semaphore
1700
        self._sema = Semaphore(init)
1701

    
1702
    def enter(self):
1703
        acquired = self._sema.acquire(False)
1704
        if not acquired:
1705
            raise self.Busy()
1706

    
1707
    def leave(self):
1708
        self._sema.release()
1709

    
1710
    def __enter__(self):
1711
        self.enter()
1712
        return self
1713

    
1714
    def __exit__(self, exc_type, exc_value, exc_traceback):
1715
        self.leave()
1716

    
1717

    
1718
exclusive_or_raise = ExclusiveOrRaise(locked=False)
1719

    
1720

    
1721
class ProjectMembership(models.Model):
1722

    
1723
    person              =   models.ForeignKey(AstakosUser)
1724
    request_date        =   models.DateField(default=datetime.now())
1725
    project             =   models.ForeignKey(Project)
1726

    
1727
    state               =   models.IntegerField(default=0)
1728
    application         =   models.ForeignKey(
1729
                                ProjectApplication,
1730
                                null=True,
1731
                                related_name='memberships')
1732
    pending_application =   models.ForeignKey(
1733
                                ProjectApplication,
1734
                                null=True,
1735
                                related_name='pending_memebrships')
1736
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1737

    
1738
    acceptance_date     =   models.DateField(null=True, db_index=True)
1739
    leave_request_date  =   models.DateField(null=True)
1740

    
1741
    objects     =   ForUpdateManager()
1742

    
1743
    REQUESTED   =   0
1744
    PENDING     =   1
1745
    ACCEPTED    =   2
1746
    REMOVING    =   3
1747
    REMOVED     =   4
1748

    
1749
    class Meta:
1750
        unique_together = ("person", "project")
1751
        #index_together = [["project", "state"]]
1752

    
1753
    def __str__(self):
1754
        return _("<'%s' membership in project '%s'>") % (
1755
                self.person.username, self.project.application)
1756

    
1757
    __repr__ = __str__
1758

    
1759
    def __init__(self, *args, **kwargs):
1760
        self.state = self.REQUESTED
1761
        super(ProjectMembership, self).__init__(*args, **kwargs)
1762

    
1763
    def _set_history_item(self, reason, date=None):
1764
        if isinstance(reason, basestring):
1765
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1766

    
1767
        history_item = ProjectMembershipHistory(
1768
                            serial=self.id,
1769
                            person=self.person,
1770
                            project=self.project,
1771
                            date=date,
1772
                            reason=reason)
1773
        history_item.save()
1774
        serial = history_item.id
1775

    
1776
    def accept(self):
1777
        state = self.state
1778
        if state != self.REQUESTED:
1779
            m = _("%s: attempt to accept in state [%s]") % (self, state)
1780
            raise AssertionError(m)
1781

    
1782
        now = datetime.now()
1783
        self.acceptance_date = now
1784
        self._set_history_item(reason='ACCEPT', date=now)
1785
        self.state = self.PENDING
1786
        self.save()
1787
        trigger_sync()
1788

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

    
1794
        self._set_history_item(reason='REMOVE')
1795
        self.state = self.REMOVING
1796
        self.save()
1797
        trigger_sync()
1798

    
1799
    def reject(self):
1800
        if state != self.REQUESTED:
1801
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1802
            raise AssertionError(m)
1803

    
1804
        # rejected requests don't need sync,
1805
        # because they were never effected
1806
        self._set_history_item(reason='REJECT')
1807
        self.delete()
1808

    
1809
    def get_diff_quotas(self, limits_list=None, remove=False):
1810
        if limits_list is None:
1811
            limits_list = []
1812

    
1813
        append = limits_list.append
1814
        holder = self.person.username
1815
        key = "1"
1816

    
1817
        tmp_grants = {}
1818
        synced_application = self.application
1819
        if synced_application is not None:
1820
            # first, inverse all current limits, and index them by resource name
1821
            cur_grants = synced_application.resource_grants.all()
1822
            f = -1
1823
            for grant in cur_grants:
1824
                name = grant.resource.name
1825
                tmp_grants[name] = QuotaLimits(
1826
                                holder       = holder,
1827
                                resource     = name,
1828
                                capacity     = f * grant.member_capacity,
1829
                                import_limit = f * grant.member_import_limit,
1830
                                export_limit = f * grant.member_export_limit)
1831

    
1832
        if not remove:
1833
            # second, add each new limit to its inverted current
1834
            new_grants = self.pending_application.resource_grants.all()
1835
            for new_grant in new_grants:
1836
                name = grant.resource.name
1837
                cur_grant = tmp_grants.pop(name, None)
1838
                if cur_grant is None:
1839
                    # if limits on a new resource, set 0 current values
1840
                    capacity = 0
1841
                    import_limit = 0
1842
                    export_limit = 0
1843
                else:
1844
                    capacity = cur_grant.capacity
1845
                    import_limit = cur_grant.import_limit
1846
                    export_limit = cur_grant.export_limit
1847

    
1848
                capacity += new_grant.member_capacity
1849
                import_limit += new_grant.member_import_limit
1850
                export_limit += new_grant.member_export_limit
1851

    
1852
                append(QuotaLimits(holder       = holder,
1853
                                   key          = key,
1854
                                   resource     = name,
1855
                                   capacity     = capacity,
1856
                                   import_limit = import_limit,
1857
                                   export_limit = export_limit))
1858

    
1859
        # third, append all the inverted current limits for removed resources
1860
        limits_list.extend(tmp_grants.itervalues())
1861
        return limits_list
1862

    
1863
    def set_sync(self):
1864
        state = self.state
1865
        if state == self.PENDING:
1866
            pending_application = self.pending_application
1867
            if pending_application is None:
1868
                m = _("%s: attempt to sync an empty pending application") % (
1869
                    self, state)
1870
                raise AssertionError(m)
1871
            self.application = pending_application
1872
            self.pending_application = None
1873
            self.pending_serial = None
1874

    
1875
            # project.application may have changed in the meantime,
1876
            # in which case we stay PENDING;
1877
            # we are safe to check due to select_for_update
1878
            if self.application == self.project.application:
1879
                self.state = self.ACCEPTED
1880
            self.save()
1881
        elif state == self.REMOVING:
1882
            self.delete()
1883
        else:
1884
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1885
            raise AssertionError(m)
1886

    
1887
class Serial(models.Model):
1888
    serial  =   models.AutoField(primary_key=True)
1889

    
1890
def new_serial():
1891
    s = Serial.objects.create()
1892
    return s.serial
1893

    
1894
def sync_finish_serials():
1895
    serials_to_ack = set(qh_query_serials([]))
1896
    sfu = ProjectMembership.objects.select_for_update()
1897
    memberships = sfu.filter(pending_serial__isnull=False)
1898

    
1899
    for membership in memberships:
1900
        serial = membership.pending_serial
1901
        # just make sure the project row is selected for update
1902
        project = membership.project
1903
        if serial in serials_to_ack:
1904
            membership.set_sync()
1905

    
1906
    transaction.commit()
1907
    qh_ack_serials(list(serials_to_ack))
1908

    
1909
def sync_projects():
1910
    sync_finish_serials()
1911

    
1912
    PENDING = ProjectMembership.PENDING
1913
    REMOVING = ProjectMembership.REMOVING
1914
    objects = ProjectMembership.objects.select_for_update()
1915

    
1916
    quotas = []
1917

    
1918
    serial = new_serial()
1919

    
1920
    pending = objects.filter(state=PENDING)
1921
    for membership in pending:
1922

    
1923
        if membership.pending_application:
1924
            m = "%s: impossible: pending_application is not None (%s)" % (
1925
                membership, membership.pending_application)
1926
            raise AssertionError(m)
1927
        if membership.pending_serial:
1928
            m = "%s: impossible: pending_serial is not None (%s)" % (
1929
                membership, membership.pending_serial)
1930
            raise AssertionError(m)
1931

    
1932
        membership.pending_application = membership.project.application
1933
        membership.pending_serial = serial
1934
        membership.get_diff_quotas(quotas)
1935
        membership.save()
1936

    
1937
    removing = objects.filter(state=REMOVING)
1938
    for membership in removing:
1939

    
1940
        if membership.pending_application:
1941
            m = ("%s: impossible: removing pending_application is not None (%s)"
1942
                % (membership, membership.pending_application))
1943
            raise AssertionError(m)
1944
        if membership.pending_serial:
1945
            m = "%s: impossible: pending_serial is not None (%s)" % (
1946
                membership, membership.pending_serial)
1947
            raise AssertionError(m)
1948

    
1949
        membership.pending_serial = serial
1950
        membership.get_diff_quotas(quotas, remove=True)
1951
        membership.save()
1952

    
1953
    transaction.commit()
1954
    # ProjectApplication.approve() unblocks here
1955
    # and can set PENDING an already PENDING membership
1956
    # which has been scheduled to sync with the old project.application
1957
    # Need to check in ProjectMembership.set_sync()
1958

    
1959
    qh_add_quota(serial, quotas)
1960
    sync_finish_serials()
1961

    
1962

    
1963
def trigger_sync(retries=3, retry_wait=1.0):
1964
    cursor = connection.cursor()
1965
    locked = True
1966
    try:
1967
        while 1:
1968
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1969
            r = cursor.fetchone()
1970
            if r is None:
1971
                m = "Impossible"
1972
                raise AssertionError(m)
1973
            locked = r[0]
1974
            if locked:
1975
                break
1976

    
1977
            retries -= 1
1978
            if retries <= 0:
1979
                return False
1980
            sleep(retry_wait)
1981

    
1982
        sync_projects()
1983
        return True
1984

    
1985
    finally:
1986
        if locked:
1987
            cursor.execute("SELECT pg_advisory_unlock(1)")
1988
            cursor.fetchall()
1989

    
1990

    
1991
class ProjectMembershipHistory(models.Model):
1992
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1993
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1994

    
1995
    person  =   models.ForeignKey(AstakosUser)
1996
    project =   models.ForeignKey(Project)
1997
    date    =   models.DateField(default=datetime.now)
1998
    reason  =   models.IntegerField()
1999
    serial  =   models.BigIntegerField()
2000
    
2001

    
2002
def filter_queryset_by_property(q, property):
2003
    """
2004
    Incorporate list comprehension for filtering querysets by property
2005
    since Queryset.filter() operates on the database level.
2006
    """
2007
    return (p for p in q if getattr(p, property, False))
2008

    
2009
def get_alive_projects():
2010
    return filter_queryset_by_property(
2011
        Project.objects.all(),
2012
        'is_alive'
2013
    )
2014

    
2015
def get_active_projects():
2016
    return filter_queryset_by_property(
2017
        Project.objects.all(),
2018
        'is_active'
2019
    )
2020

    
2021
def _create_object(model, **kwargs):
2022
    o = model.objects.create(**kwargs)
2023
    o.save()
2024
    return o
2025

    
2026

    
2027
def create_astakos_user(u):
2028
    try:
2029
        AstakosUser.objects.get(user_ptr=u.pk)
2030
    except AstakosUser.DoesNotExist:
2031
        extended_user = AstakosUser(user_ptr_id=u.pk)
2032
        extended_user.__dict__.update(u.__dict__)
2033
        extended_user.save()
2034
        if not extended_user.has_auth_provider('local'):
2035
            extended_user.add_auth_provider('local')
2036
    except BaseException, e:
2037
        logger.exception(e)
2038

    
2039

    
2040
def fix_superusers(sender, **kwargs):
2041
    # Associate superusers with AstakosUser
2042
    admins = User.objects.filter(is_superuser=True)
2043
    for u in admins:
2044
        create_astakos_user(u)
2045
post_syncdb.connect(fix_superusers)
2046

    
2047

    
2048
def user_post_save(sender, instance, created, **kwargs):
2049
    if not created:
2050
        return
2051
    create_astakos_user(instance)
2052
post_save.connect(user_post_save, sender=User)
2053

    
2054

    
2055
# def astakosuser_pre_save(sender, instance, **kwargs):
2056
#     instance.aquarium_report = False
2057
#     instance.new = False
2058
#     try:
2059
#         db_instance = AstakosUser.objects.get(id=instance.id)
2060
#     except AstakosUser.DoesNotExist:
2061
#         # create event
2062
#         instance.aquarium_report = True
2063
#         instance.new = True
2064
#     else:
2065
#         get = AstakosUser.__getattribute__
2066
#         l = filter(lambda f: get(db_instance, f) != get(instance, f),
2067
#                    BILLING_FIELDS)
2068
#         instance.aquarium_report = True if l else False
2069
# pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
2070

    
2071
# def set_default_group(user):
2072
#     try:
2073
#         default = AstakosGroup.objects.get(name='default')
2074
#         Membership(
2075
#             group=default, person=user, date_joined=datetime.now()).save()
2076
#     except AstakosGroup.DoesNotExist, e:
2077
#         logger.exception(e)
2078

    
2079

    
2080
def astakosuser_post_save(sender, instance, created, **kwargs):
2081
#     if instance.aquarium_report:
2082
#         report_user_event(instance, create=instance.new)
2083
    if not created:
2084
        return
2085
#     set_default_group(instance)
2086
    # TODO handle socket.error & IOError
2087
    register_users((instance,))
2088
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2089

    
2090

    
2091
def resource_post_save(sender, instance, created, **kwargs):
2092
    if not created:
2093
        return
2094
    register_resources((instance,))
2095
post_save.connect(resource_post_save, sender=Resource)
2096

    
2097

    
2098
# def on_quota_disturbed(sender, users, **kwargs):
2099
# #     print '>>>', locals()
2100
#     if not users:
2101
#         return
2102
#     send_quota(users)
2103
#
2104
# quota_disturbed = Signal(providing_args=["users"])
2105
# quota_disturbed.connect(on_quota_disturbed)
2106

    
2107

    
2108
# def send_quota_disturbed(sender, instance, **kwargs):
2109
#     users = []
2110
#     extend = users.extend
2111
#     if sender == Membership:
2112
#         if not instance.group.is_enabled:
2113
#             return
2114
#         extend([instance.person])
2115
#     elif sender == AstakosUserQuota:
2116
#         extend([instance.user])
2117
#     elif sender == AstakosGroupQuota:
2118
#         if not instance.group.is_enabled:
2119
#             return
2120
#         extend(instance.group.astakosuser_set.all())
2121
#     elif sender == AstakosGroup:
2122
#         if not instance.is_enabled:
2123
#             return
2124
#     quota_disturbed.send(sender=sender, users=users)
2125
# post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
2126
# post_delete.connect(send_quota_disturbed, sender=Membership)
2127
# post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
2128
# post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
2129
# post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
2130
# post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
2131

    
2132

    
2133
def renew_token(sender, instance, **kwargs):
2134
    if not instance.auth_token:
2135
        instance.renew_token()
2136
pre_save.connect(renew_token, sender=AstakosUser)
2137
pre_save.connect(renew_token, sender=Service)