Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 85d444db

History | View | Annotate | Download (72.1 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import hashlib
35
import uuid
36
import logging
37
import json
38

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, BILLING_FIELDS,
70
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
71
    SITENAME, SERVICES, MODERATION_ENABLED)
72
from astakos.im import settings as astakos_settings
73
from astakos.im.endpoints.qh import (
74
    register_users, send_quota, register_resources, qh_add_quota, QuotaLimits,
75
    qh_query_serials, qh_ack_serials)
76
from astakos.im import auth_providers
77
#from astakos.im.endpoints.aquarium.producer import report_user_event
78
#from astakos.im.tasks import propagate_groupmembers_quota
79

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

    
83
logger = logging.getLogger(__name__)
84

    
85
DEFAULT_CONTENT_TYPE = None
86
_content_type = None
87

    
88
def get_content_type():
89
    global _content_type
90
    if _content_type is not None:
91
        return _content_type
92

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

    
100
RESOURCE_SEPARATOR = '.'
101

    
102
inf = float('inf')
103

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

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

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

    
125
    def __str__(self):
126
        return self.name
127

    
128
    @property
129
    def resources(self):
130
        return self.resource_set.all()
131

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

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

    
148

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

    
153

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

    
162
    class Meta:
163
        unique_together = ("name", "service")
164

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

    
168

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

    
172
    def __str__(self):
173
        return self.name
174

    
175

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

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

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

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

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

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

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

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

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

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

    
292
    @property
293
    def policies(self):
294
        return self.astakosgroupquota_set.select_related().all()
295

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

    
305
    @property
306
    def owners(self):
307
        return self.owner.all()
308

    
309
    @property
310
    def owner_details(self):
311
        return self.owner.select_related().all()
312

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

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

    
332
class AstakosUserManager(UserManager):
333

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

    
343
    def get_by_email(self, email):
344
        return self.get(email=email)
345

    
346
    def get_by_identifier(self, email_or_username, **kwargs):
347
        try:
348
            return self.get(email__iexact=email_or_username, **kwargs)
349
        except AstakosUser.DoesNotExist:
350
            return self.get(username__iexact=email_or_username, **kwargs)
351

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

    
357

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

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

    
374

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

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

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

    
390
    email_verified = models.BooleanField(_('Email verified?'), default=False)
391

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

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

    
401
    policy = models.ManyToManyField(
402
        Resource, null=True, through='AstakosUserQuota')
403

    
404
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
405

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

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

    
415
    objects = AstakosUserManager()
416

    
417

    
418
    owner = models.ManyToManyField(
419
        AstakosGroup, related_name='owner', null=True)
420

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

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

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

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

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

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

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

    
483
    @property
484
    def policies(self):
485
        return self.astakosuserquota_set.select_related().all()
486

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

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

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

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

    
521
    @property
522
    def extended_groups(self):
523
        return self.membership_set.select_related().all()
524

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

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

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

    
542
        self.update_uuid()
543

    
544
        if self.username != self.email.lower():
545
            # set username
546
            self.username = self.email.lower()
547

    
548
        self.validate_unique_email_isactive()
549

    
550
        super(AstakosUser, self).save(**kwargs)
551

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

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

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

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

    
582
    def __unicode__(self):
583
        return '%s (%s)' % (self.realname, self.email)
584

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

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

    
605
    def email_change_is_pending(self):
606
        return self.emailchanges.count() > 0
607

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

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

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

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

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

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

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

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

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

    
664
        return True
665

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

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

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

    
677
        return True
678

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

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

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

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

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

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

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

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

    
723
        pending.delete()
724
        return provider
725

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

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

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

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

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

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

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

    
761
        return providers
762

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

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

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

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

    
801

    
802
class AstakosUserAuthProviderManager(models.Manager):
803

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

    
807

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

    
826
    objects = AstakosUserAuthProviderManager()
827

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

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

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

    
844

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

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

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

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

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

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

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

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

    
893

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

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

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

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

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

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

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

    
958
    update_or_create = _update_or_create
959

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

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

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

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

    
980

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

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

    
990

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

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

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

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

    
1017

    
1018
class EmailChangeManager(models.Manager):
1019

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

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

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

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

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

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

    
1064

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

    
1075
    objects = EmailChangeManager()
1076

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

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

    
1085

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

    
1093

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

    
1103

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

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

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

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

    
1139
        return user
1140

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1236

    
1237
### PROJECTS ###
1238
################
1239

    
1240

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

    
1243
    new_attributes = {}
1244
    sync_attributes = {}
1245

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

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

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

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

    
1269
        new_attributes[newname] = value
1270

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

    
1274

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

    
1277
    the_name = name
1278
    the_prefix = prefix
1279

    
1280
    class SyncedState(models.Model):
1281

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

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

    
1292
        class Meta:
1293
            abstract = True
1294

    
1295
        class NotSynced(Exception):
1296
            pass
1297

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

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

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

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

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

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

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

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

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

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

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

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

    
1348
    return SyncedState
1349

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

    
1352

    
1353
class ProjectApplication(models.Model):
1354
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1355
    applicant               =   models.ForeignKey(
1356
                                    AstakosUser,
1357
                                    related_name='projects_applied',
1358
                                    db_index=True)
1359

    
1360
    state                   =   models.CharField(max_length=80,
1361
                                                default=UNKNOWN)
1362

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

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

    
1373
    name                    =   models.CharField(max_length=80, help_text=" The Project's name should be in a domain format. The domain shouldn't neccessarily exist in the real world but is helpful to imply a structure. e.g.: myproject.mylab.ntua.gr or myservice.myteam.myorganization ",)
1374
    homepage                =   models.URLField(max_length=255, null=True,
1375
                                                blank=True,help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",)
1376
    description             =   models.TextField(null=True, blank=True,help_text= "Please provide a short but descriptive abstract of your Project, so that anyone searching can quickly understand what this Project is about. ")
1377
    start_date              =   models.DateTimeField(help_text= "Here you specify the date you want your Project to start granting its resources. Its members will get the resources coming from this Project on this exact date.")
1378
    end_date                =   models.DateTimeField(help_text= "Here you specify the date you want your Project to cease. This means that after this date all members will no longer be able to allocate resources from this Project.  ")
1379
    member_join_policy      =   models.ForeignKey(MemberJoinPolicy)
1380
    member_leave_policy     =   models.ForeignKey(MemberLeavePolicy)
1381
    limit_on_members_number =   models.PositiveIntegerField(null=True,
1382
                                                            blank=True,help_text= "Here you specify the number of members this Project is going to have. This means that this number of people will be granted the resources you will specify in the next step. This can be '1' if you are the only one wanting to get resources. ")
1383
    resource_grants         =   models.ManyToManyField(
1384
                                    Resource,
1385
                                    null=True,
1386
                                    blank=True,
1387
                                    through='ProjectResourceGrant')
1388
    comments                =   models.TextField(null=True, blank=True)
1389
    issue_date              =   models.DateTimeField()
1390

    
1391
    def add_resource_policy(self, service, resource, uplimit):
1392
        """Raises ObjectDoesNotExist, IntegrityError"""
1393
        q = self.projectresourcegrant_set
1394
        resource = Resource.objects.get(service__name=service, name=resource)
1395
        q.create(resource=resource, member_capacity=uplimit)
1396

    
1397
    
1398
    @property
1399
    def grants(self):
1400
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1401
            
1402
    @property
1403
    def resource_policies(self):
1404
        return self.projectresourcegrant_set.all()
1405

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

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

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

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

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

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

    
1448
        return None
1449

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

1455
        Raises:
1456
            PermissionDenied
1457
        """
1458

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

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

    
1468
        now = datetime.now()
1469
        project = self._get_project()
1470

    
1471
        try:
1472
            # needs SERIALIZABLE
1473
            conflicting_project = Project.objects.get(name=new_project_name)
1474
            if (conflicting_project.is_alive and
1475
                conflicting_project != project):
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

    
1483
        if project is None:
1484
            project = Project(creation_date=now)
1485

    
1486
        project.name = new_project_name
1487
        project.application = self
1488

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

    
1494
        project.last_approval_date = now
1495
        project.save()
1496
        #ProjectMembership.add_to_project(self)
1497
        project.add_member(self.owner)
1498

    
1499
        precursor = self.precursor_application
1500
        while precursor:
1501
            precursor.state = self.REPLACED
1502
            precursor.save()
1503
            precursor = precursor.precursor_application
1504

    
1505
        self.state = self.APPROVED
1506
        self.save()
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='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_date)
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.application.owner.email],
1662
#                 _(PROJECT_TERMINATION_SUBJECT) % self.__dict__,
1663
#                 template='im/projects/project_termination_notification.txt',
1664
#                 dictionary={'object':self.application}
1665
#             ).send()
1666
#         except NotificationError, e:
1667
#             logger.error(e.message)
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.application.owner.email],
1678
#                 _(PROJECT_SUSPENSION_SUBJECT) % self.__dict__,
1679
#                 template='im/projects/project_suspension_notification.txt',
1680
#                 dictionary={'object':self.application}
1681
#             ).send()
1682
#         except NotificationError, e:
1683
#             logger.error(e.message)
1684

    
1685

    
1686
class ProjectMembership(models.Model):
1687

    
1688
    person              =   models.ForeignKey(AstakosUser)
1689
    request_date        =   models.DateField(default=datetime.now())
1690
    project             =   models.ForeignKey(Project)
1691

    
1692
    state               =   models.IntegerField(default=0)
1693
    application         =   models.ForeignKey(
1694
                                ProjectApplication,
1695
                                null=True,
1696
                                related_name='memberships')
1697
    pending_application =   models.ForeignKey(
1698
                                ProjectApplication,
1699
                                null=True,
1700
                                related_name='pending_memebrships')
1701
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1702

    
1703
    acceptance_date     =   models.DateField(null=True, db_index=True)
1704
    leave_request_date  =   models.DateField(null=True)
1705

    
1706
    objects     =   ForUpdateManager()
1707

    
1708
    REQUESTED   =   0
1709
    PENDING     =   1
1710
    ACCEPTED    =   2
1711
    REMOVING    =   3
1712
    REMOVED     =   4
1713

    
1714
    class Meta:
1715
        unique_together = ("person", "project")
1716
        #index_together = [["project", "state"]]
1717

    
1718
    def __str__(self):
1719
        return _("<'%s' membership in project '%s'>") % (
1720
                self.person.username, self.project.application)
1721

    
1722
    __repr__ = __str__
1723

    
1724
    def __init__(self, *args, **kwargs):
1725
        self.state = self.REQUESTED
1726
        super(ProjectMembership, self).__init__(*args, **kwargs)
1727

    
1728
    def _set_history_item(self, reason, date=None):
1729
        if isinstance(reason, basestring):
1730
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1731

    
1732
        history_item = ProjectMembershipHistory(
1733
                            serial=self.id,
1734
                            person=self.person,
1735
                            project=self.project,
1736
                            date=date or datetime.now(),
1737
                            reason=reason)
1738
        history_item.save()
1739
        serial = history_item.id
1740

    
1741
    def accept(self):
1742
        state = self.state
1743
        if state != self.REQUESTED:
1744
            m = _("%s: attempt to accept in state [%s]") % (self, state)
1745
            raise AssertionError(m)
1746

    
1747
        now = datetime.now()
1748
        self.acceptance_date = now
1749
        self._set_history_item(reason='ACCEPT', date=now)
1750
        self.state = self.PENDING
1751
        self.save()
1752

    
1753
    def remove(self):
1754
        state = self.state
1755
        if state != self.ACCEPTED:
1756
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1757
            raise AssertionError(m)
1758

    
1759
        self._set_history_item(reason='REMOVE')
1760
        self.state = self.REMOVING
1761
        self.save()
1762

    
1763
    def reject(self):
1764
        state = self.state
1765
        if state != self.REQUESTED:
1766
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1767
            raise AssertionError(m)
1768

    
1769
        # rejected requests don't need sync,
1770
        # because they were never effected
1771
        self._set_history_item(reason='REJECT')
1772
        self.delete()
1773

    
1774
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1775
        if sub_list is None:
1776
            sub_list = []
1777

    
1778
        if add_list is None:
1779
            add_list = []
1780

    
1781
        sub_append = sub_list.append
1782
        add_append = add_list.append
1783
        holder = self.person.uuid
1784

    
1785
        synced_application = self.application
1786
        if synced_application is not None:
1787
            cur_grants = synced_application.projectresourcegrant_set.all()
1788
            for grant in cur_grants:
1789
                sub_append(QuotaLimits(
1790
                               holder       = holder,
1791
                               resource     = str(grant.resource),
1792
                               capacity     = grant.member_capacity,
1793
                               import_limit = grant.member_import_limit,
1794
                               export_limit = grant.member_export_limit))
1795

    
1796
        if not remove:
1797
            new_grants = self.pending_application.projectresourcegrant_set.all()
1798
            for new_grant in new_grants:
1799
                add_append(QuotaLimits(
1800
                               holder       = holder,
1801
                               resource     = str(new_grant.resource),
1802
                               capacity     = new_grant.member_capacity,
1803
                               import_limit = new_grant.member_import_limit,
1804
                               export_limit = new_grant.member_export_limit))
1805

    
1806
        return (sub_list, add_list)
1807

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

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

    
1832
    def reset_sync(self):
1833
        state = self.state
1834
        if state in [self.PENDING, self.REMOVING]:
1835
            self.pending_application = None
1836
            self.pending_serial = None
1837
            self.save()
1838
        else:
1839
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1840
            raise AssertionError(m)
1841

    
1842
class Serial(models.Model):
1843
    serial  =   models.AutoField(primary_key=True)
1844

    
1845
def new_serial():
1846
    s = Serial.objects.create()
1847
    serial = s.serial
1848
    s.delete()
1849
    return serial
1850

    
1851
def sync_finish_serials(serials_to_ack=None):
1852
    if serials_to_ack is None:
1853
        serials_to_ack = qh_query_serials([])
1854

    
1855
    serials_to_ack = set(serials_to_ack)
1856
    sfu = ProjectMembership.objects.select_for_update()
1857
    memberships = list(sfu.filter(pending_serial__isnull=False))
1858

    
1859
    if memberships:
1860
        for membership in memberships:
1861
            serial = membership.pending_serial
1862
            # just make sure the project row is selected for update
1863
            project = membership.project
1864
            if serial in serials_to_ack:
1865
                membership.set_sync()
1866
            else:
1867
                membership.reset_sync()
1868

    
1869
        transaction.commit()
1870

    
1871
    qh_ack_serials(list(serials_to_ack))
1872
    return len(memberships)
1873

    
1874
def sync_projects():
1875
    sync_finish_serials()
1876

    
1877
    PENDING = ProjectMembership.PENDING
1878
    REMOVING = ProjectMembership.REMOVING
1879
    objects = ProjectMembership.objects.select_for_update()
1880

    
1881
    sub_quota, add_quota = [], []
1882

    
1883
    serial = new_serial()
1884

    
1885
    pending = objects.filter(state=PENDING)
1886
    for membership in pending:
1887

    
1888
        if membership.pending_application:
1889
            m = "%s: impossible: pending_application is not None (%s)" % (
1890
                membership, membership.pending_application)
1891
            raise AssertionError(m)
1892
        if membership.pending_serial:
1893
            m = "%s: impossible: pending_serial is not None (%s)" % (
1894
                membership, membership.pending_serial)
1895
            raise AssertionError(m)
1896

    
1897
        membership.pending_application = membership.project.application
1898
        membership.pending_serial = serial
1899
        membership.get_diff_quotas(sub_quota, add_quota)
1900
        membership.save()
1901

    
1902
    removing = objects.filter(state=REMOVING)
1903
    for membership in removing:
1904

    
1905
        if membership.pending_application:
1906
            m = ("%s: impossible: removing pending_application is not None (%s)"
1907
                % (membership, membership.pending_application))
1908
            raise AssertionError(m)
1909
        if membership.pending_serial:
1910
            m = "%s: impossible: pending_serial is not None (%s)" % (
1911
                membership, membership.pending_serial)
1912
            raise AssertionError(m)
1913

    
1914
        membership.pending_serial = serial
1915
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1916
        membership.save()
1917

    
1918
    transaction.commit()
1919
    # ProjectApplication.approve() unblocks here
1920
    # and can set PENDING an already PENDING membership
1921
    # which has been scheduled to sync with the old project.application
1922
    # Need to check in ProjectMembership.set_sync()
1923

    
1924
    r = qh_add_quota(serial, sub_quota, add_quota)
1925
    if r:
1926
        m = "cannot sync serial: %d" % serial
1927
        raise RuntimeError(m)
1928

    
1929
    sync_finish_serials([serial])
1930

    
1931

    
1932
def trigger_sync(retries=3, retry_wait=1.0):
1933
    cursor = connection.cursor()
1934
    locked = True
1935
    try:
1936
        while 1:
1937
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1938
            r = cursor.fetchone()
1939
            if r is None:
1940
                m = "Impossible"
1941
                raise AssertionError(m)
1942
            locked = r[0]
1943
            if locked:
1944
                break
1945

    
1946
            retries -= 1
1947
            if retries <= 0:
1948
                return False
1949
            sleep(retry_wait)
1950

    
1951
        transaction.commit()
1952
        sync_projects()
1953
        return True
1954

    
1955
    finally:
1956
        if locked:
1957
            cursor.execute("SELECT pg_advisory_unlock(1)")
1958
            cursor.fetchall()
1959

    
1960

    
1961
class ProjectMembershipHistory(models.Model):
1962
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1963
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1964

    
1965
    person  =   models.ForeignKey(AstakosUser)
1966
    project =   models.ForeignKey(Project)
1967
    date    =   models.DateField(default=datetime.now)
1968
    reason  =   models.IntegerField()
1969
    serial  =   models.BigIntegerField()
1970

    
1971

    
1972
def filter_queryset_by_property(q, property):
1973
    """
1974
    Incorporate list comprehension for filtering querysets by property
1975
    since Queryset.filter() operates on the database level.
1976
    """
1977
    return (p for p in q if getattr(p, property, False))
1978

    
1979
def get_alive_projects():
1980
    return filter_queryset_by_property(
1981
        Project.objects.all(),
1982
        'is_alive'
1983
    )
1984

    
1985
def get_active_projects():
1986
    return filter_queryset_by_property(
1987
        Project.objects.all(),
1988
        'is_active'
1989
    )
1990

    
1991
def _create_object(model, **kwargs):
1992
    o = model.objects.create(**kwargs)
1993
    o.save()
1994
    return o
1995

    
1996

    
1997
def create_astakos_user(u):
1998
    try:
1999
        AstakosUser.objects.get(user_ptr=u.pk)
2000
    except AstakosUser.DoesNotExist:
2001
        extended_user = AstakosUser(user_ptr_id=u.pk)
2002
        extended_user.__dict__.update(u.__dict__)
2003
        extended_user.save()
2004
        if not extended_user.has_auth_provider('local'):
2005
            extended_user.add_auth_provider('local')
2006
    except BaseException, e:
2007
        logger.exception(e)
2008

    
2009

    
2010
def fix_superusers(sender, **kwargs):
2011
    # Associate superusers with AstakosUser
2012
    admins = User.objects.filter(is_superuser=True)
2013
    for u in admins:
2014
        create_astakos_user(u)
2015
post_syncdb.connect(fix_superusers)
2016

    
2017

    
2018
def user_post_save(sender, instance, created, **kwargs):
2019
    if not created:
2020
        return
2021
    create_astakos_user(instance)
2022
post_save.connect(user_post_save, sender=User)
2023

    
2024

    
2025
# def astakosuser_pre_save(sender, instance, **kwargs):
2026
#     instance.aquarium_report = False
2027
#     instance.new = False
2028
#     try:
2029
#         db_instance = AstakosUser.objects.get(id=instance.id)
2030
#     except AstakosUser.DoesNotExist:
2031
#         # create event
2032
#         instance.aquarium_report = True
2033
#         instance.new = True
2034
#     else:
2035
#         get = AstakosUser.__getattribute__
2036
#         l = filter(lambda f: get(db_instance, f) != get(instance, f),
2037
#                    BILLING_FIELDS)
2038
#         instance.aquarium_report = True if l else False
2039
# pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
2040

    
2041
# def set_default_group(user):
2042
#     try:
2043
#         default = AstakosGroup.objects.get(name='default')
2044
#         Membership(
2045
#             group=default, person=user, date_joined=datetime.now()).save()
2046
#     except AstakosGroup.DoesNotExist, e:
2047
#         logger.exception(e)
2048

    
2049

    
2050
def astakosuser_post_save(sender, instance, created, **kwargs):
2051
#     if instance.aquarium_report:
2052
#         report_user_event(instance, create=instance.new)
2053
    if not created:
2054
        return
2055
#     set_default_group(instance)
2056
    # TODO handle socket.error & IOError
2057
    register_users((instance,))
2058
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2059

    
2060

    
2061
def resource_post_save(sender, instance, created, **kwargs):
2062
    if not created:
2063
        return
2064
    register_resources((instance,))
2065
post_save.connect(resource_post_save, sender=Resource)
2066

    
2067

    
2068
# def on_quota_disturbed(sender, users, **kwargs):
2069
# #     print '>>>', locals()
2070
#     if not users:
2071
#         return
2072
#     send_quota(users)
2073
#
2074
# quota_disturbed = Signal(providing_args=["users"])
2075
# quota_disturbed.connect(on_quota_disturbed)
2076

    
2077

    
2078
# def send_quota_disturbed(sender, instance, **kwargs):
2079
#     users = []
2080
#     extend = users.extend
2081
#     if sender == Membership:
2082
#         if not instance.group.is_enabled:
2083
#             return
2084
#         extend([instance.person])
2085
#     elif sender == AstakosUserQuota:
2086
#         extend([instance.user])
2087
#     elif sender == AstakosGroupQuota:
2088
#         if not instance.group.is_enabled:
2089
#             return
2090
#         extend(instance.group.astakosuser_set.all())
2091
#     elif sender == AstakosGroup:
2092
#         if not instance.is_enabled:
2093
#             return
2094
#     quota_disturbed.send(sender=sender, users=users)
2095
# post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
2096
# post_delete.connect(send_quota_disturbed, sender=Membership)
2097
# post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
2098
# post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
2099
# post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
2100
# post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
2101

    
2102

    
2103
def renew_token(sender, instance, **kwargs):
2104
    if not instance.auth_token:
2105
        instance.renew_token()
2106
pre_save.connect(renew_token, sender=AstakosUser)
2107
pre_save.connect(renew_token, sender=Service)
2108