Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 8ed8ea10

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

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

    
46
from django.db import models, IntegrityError
47
from django.contrib.auth.models import User, UserManager, Group, Permission
48
from django.utils.translation import ugettext as _
49
from django.db import transaction
50
from django.core.exceptions import ValidationError
51
from django.db.models.signals import (
52
    pre_save, post_save, post_syncdb, post_delete
53
)
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.core.validators import email_re
64

    
65
from astakos.im.settings import (
66
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
67
    AUTH_TOKEN_DURATION, BILLING_FIELDS,
68
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
69
)
70
from astakos.im.endpoints.qh import (
71
    register_users, send_quota, register_resources
72
)
73
from astakos.im import auth_providers
74
from astakos.im.endpoints.aquarium.producer import report_user_event
75
from astakos.im.functions import send_invitation
76
#from astakos.im.tasks import propagate_groupmembers_quota
77

    
78
from astakos.im.notifications import build_notification
79

    
80
import astakos.im.messages as astakos_messages
81

    
82
logger = logging.getLogger(__name__)
83

    
84
DEFAULT_CONTENT_TYPE = None
85
try:
86
    content_type = ContentType.objects.get(app_label='im', model='astakosuser')
87
except:
88
    content_type = DEFAULT_CONTENT_TYPE
89

    
90
RESOURCE_SEPARATOR = '.'
91

    
92
inf = float('inf')
93

    
94
class Service(models.Model):
95
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
96
    url = models.FilePathField()
97
    icon = models.FilePathField(blank=True)
98
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
99
                                  null=True, blank=True)
100
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
101
    auth_token_expires = models.DateTimeField(
102
        _('Token expiration date'), null=True)
103

    
104
    def renew_token(self):
105
        md5 = hashlib.md5()
106
        md5.update(self.name.encode('ascii', 'ignore'))
107
        md5.update(self.url.encode('ascii', 'ignore'))
108
        md5.update(asctime())
109

    
110
        self.auth_token = b64encode(md5.digest())
111
        self.auth_token_created = datetime.now()
112
        self.auth_token_expires = self.auth_token_created + \
113
            timedelta(hours=AUTH_TOKEN_DURATION)
114

    
115
    def __str__(self):
116
        return self.name
117

    
118
    @property
119
    def resources(self):
120
        return self.resource_set.all()
121

    
122
    @resources.setter
123
    def resources(self, resources):
124
        for s in resources:
125
            self.resource_set.create(**s)
126

    
127
    def add_resource(self, service, resource, uplimit, update=True):
128
        """Raises ObjectDoesNotExist, IntegrityError"""
129
        resource = Resource.objects.get(service__name=service, name=resource)
130
        if update:
131
            AstakosUserQuota.objects.update_or_create(user=self,
132
                                                      resource=resource,
133
                                                      defaults={'uplimit': uplimit})
134
        else:
135
            q = self.astakosuserquota_set
136
            q.create(resource=resource, uplimit=uplimit)
137

    
138

    
139
class ResourceMetadata(models.Model):
140
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
141
    value = models.CharField(_('Value'), max_length=255)
142

    
143

    
144
class Resource(models.Model):
145
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
146
    meta = models.ManyToManyField(ResourceMetadata)
147
    service = models.ForeignKey(Service)
148
    desc = models.TextField(_('Description'), null=True)
149
    unit = models.CharField(_('Name'), null=True, max_length=255)
150
    group = models.CharField(_('Group'), null=True, max_length=255)
151

    
152
    def __str__(self):
153
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
154

    
155

    
156
class GroupKind(models.Model):
157
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
158

    
159
    def __str__(self):
160
        return self.name
161

    
162

    
163
class AstakosGroup(Group):
164
    kind = models.ForeignKey(GroupKind)
165
    homepage = models.URLField(
166
        _('Homepage Url'), max_length=255, null=True, blank=True)
167
    desc = models.TextField(_('Description'), null=True)
168
    policy = models.ManyToManyField(
169
        Resource,
170
        null=True,
171
        blank=True,
172
        through='AstakosGroupQuota'
173
    )
174
    creation_date = models.DateTimeField(
175
        _('Creation date'),
176
        default=datetime.now()
177
    )
178
    issue_date = models.DateTimeField('Start date', null=True)
179
    expiration_date = models.DateTimeField(
180
        _('Expiration date'),
181
        null=True,
182
    )
183
    moderation_enabled = models.BooleanField(
184
        _('Moderated membership?'),
185
        default=True
186
    )
187
    approval_date = models.DateTimeField(
188
        _('Activation date'),
189
        null=True,
190
        blank=True
191
    )
192
    estimated_participants = models.PositiveIntegerField(
193
        _('Estimated #members'),
194
        null=True,
195
        blank=True,
196
    )
197
    max_participants = models.PositiveIntegerField(
198
        _('Maximum numder of participants'),
199
        null=True,
200
        blank=True
201
    )
202

    
203
    @property
204
    def is_disabled(self):
205
        if not self.approval_date:
206
            return True
207
        return False
208

    
209
    @property
210
    def is_enabled(self):
211
        if self.is_disabled:
212
            return False
213
        if not self.issue_date:
214
            return False
215
        if not self.expiration_date:
216
            return True
217
        now = datetime.now()
218
        if self.issue_date > now:
219
            return False
220
        if now >= self.expiration_date:
221
            return False
222
        return True
223

    
224
    def enable(self):
225
        if self.is_enabled:
226
            return
227
        self.approval_date = datetime.now()
228
        self.save()
229
        quota_disturbed.send(sender=self, users=self.approved_members)
230
        #propagate_groupmembers_quota.apply_async(
231
        #    args=[self], eta=self.issue_date)
232
        #propagate_groupmembers_quota.apply_async(
233
        #    args=[self], eta=self.expiration_date)
234

    
235
    def disable(self):
236
        if self.is_disabled:
237
            return
238
        self.approval_date = None
239
        self.save()
240
        quota_disturbed.send(sender=self, users=self.approved_members)
241

    
242
    def approve_member(self, person):
243
        m, created = self.membership_set.get_or_create(person=person)
244
        m.approve()
245

    
246
    @property
247
    def members(self):
248
        q = self.membership_set.select_related().all()
249
        return [m.person for m in q]
250

    
251
    @property
252
    def approved_members(self):
253
        q = self.membership_set.select_related().all()
254
        return [m.person for m in q if m.is_approved]
255

    
256
    @property
257
    def quota(self):
258
        d = defaultdict(int)
259
        for q in self.astakosgroupquota_set.select_related().all():
260
            d[q.resource] += q.uplimit or inf
261
        return d
262

    
263
    def add_policy(self, service, resource, uplimit, update=True):
264
        """Raises ObjectDoesNotExist, IntegrityError"""
265
        resource = Resource.objects.get(service__name=service, name=resource)
266
        if update:
267
            AstakosGroupQuota.objects.update_or_create(
268
                group=self,
269
                resource=resource,
270
                defaults={'uplimit': uplimit}
271
            )
272
        else:
273
            q = self.astakosgroupquota_set
274
            q.create(resource=resource, uplimit=uplimit)
275

    
276
    @property
277
    def policies(self):
278
        return self.astakosgroupquota_set.select_related().all()
279

    
280
    @policies.setter
281
    def policies(self, policies):
282
        for p in policies:
283
            service = p.get('service', None)
284
            resource = p.get('resource', None)
285
            uplimit = p.get('uplimit', 0)
286
            update = p.get('update', True)
287
            self.add_policy(service, resource, uplimit, update)
288

    
289
    @property
290
    def owners(self):
291
        return self.owner.all()
292

    
293
    @property
294
    def owner_details(self):
295
        return self.owner.select_related().all()
296

    
297
    @owners.setter
298
    def owners(self, l):
299
        self.owner = l
300
        map(self.approve_member, l)
301

    
302

    
303

    
304
class AstakosUserManager(UserManager):
305

    
306
    def get_auth_provider_user(self, provider, **kwargs):
307
        """
308
        Retrieve AstakosUser instance associated with the specified third party
309
        id.
310
        """
311
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
312
                          kwargs.iteritems()))
313
        return self.get(auth_providers__module=provider, **kwargs)
314

    
315
class AstakosUser(User):
316
    """
317
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
318
    """
319
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
320
                                   null=True)
321

    
322
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
323
    #                    AstakosUserProvider model.
324
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
325
                                null=True)
326
    # ex. screen_name for twitter, eppn for shibboleth
327
    third_party_identifier = models.CharField(_('Third-party identifier'),
328
                                              max_length=255, null=True,
329
                                              blank=True)
330

    
331

    
332
    #for invitations
333
    user_level = DEFAULT_USER_LEVEL
334
    level = models.IntegerField(_('Inviter level'), default=user_level)
335
    invitations = models.IntegerField(
336
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
337

    
338
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
339
                                  null=True, blank=True)
340
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
341
    auth_token_expires = models.DateTimeField(
342
        _('Token expiration date'), null=True)
343

    
344
    updated = models.DateTimeField(_('Update date'))
345
    is_verified = models.BooleanField(_('Is verified?'), default=False)
346

    
347
    email_verified = models.BooleanField(_('Email verified?'), default=False)
348

    
349
    has_credits = models.BooleanField(_('Has credits?'), default=False)
350
    has_signed_terms = models.BooleanField(
351
        _('I agree with the terms'), default=False)
352
    date_signed_terms = models.DateTimeField(
353
        _('Signed terms date'), null=True, blank=True)
354

    
355
    activation_sent = models.DateTimeField(
356
        _('Activation sent data'), null=True, blank=True)
357

    
358
    policy = models.ManyToManyField(
359
        Resource, null=True, through='AstakosUserQuota')
360

    
361
    astakos_groups = models.ManyToManyField(
362
        AstakosGroup, verbose_name=_('agroups'), blank=True,
363
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
364
        through='Membership')
365

    
366
    __has_signed_terms = False
367
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
368
                                           default=False, db_index=True)
369

    
370
    objects = AstakosUserManager()
371

    
372
    owner = models.ManyToManyField(
373
        AstakosGroup, related_name='owner', null=True)
374

    
375
    class Meta:
376
        unique_together = ("provider", "third_party_identifier")
377

    
378
    def __init__(self, *args, **kwargs):
379
        super(AstakosUser, self).__init__(*args, **kwargs)
380
        self.__has_signed_terms = self.has_signed_terms
381
        if not self.id:
382
            self.is_active = False
383

    
384
    @property
385
    def realname(self):
386
        return '%s %s' % (self.first_name, self.last_name)
387

    
388
    @realname.setter
389
    def realname(self, value):
390
        parts = value.split(' ')
391
        if len(parts) == 2:
392
            self.first_name = parts[0]
393
            self.last_name = parts[1]
394
        else:
395
            self.last_name = parts[0]
396

    
397
    def add_permission(self, pname):
398
        if self.has_perm(pname):
399
            return
400
        p, created = Permission.objects.get_or_create(codename=pname,
401
                                                      name=pname.capitalize(),
402
                                                      content_type=content_type)
403
        self.user_permissions.add(p)
404

    
405
    def remove_permission(self, pname):
406
        if self.has_perm(pname):
407
            return
408
        p = Permission.objects.get(codename=pname,
409
                                   content_type=content_type)
410
        self.user_permissions.remove(p)
411

    
412
    @property
413
    def invitation(self):
414
        try:
415
            return Invitation.objects.get(username=self.email)
416
        except Invitation.DoesNotExist:
417
            return None
418

    
419
    def invite(self, email, realname):
420
        inv = Invitation(inviter=self, username=email, realname=realname)
421
        inv.save()
422
        send_invitation(inv)
423
        self.invitations = max(0, self.invitations - 1)
424
        self.save()
425

    
426
    @property
427
    def quota(self):
428
        """Returns a dict with the sum of quota limits per resource"""
429
        d = defaultdict(int)
430
        for q in self.policies:
431
            d[q.resource] += q.uplimit or inf
432
        for m in self.extended_groups:
433
            if not m.is_approved:
434
                continue
435
            g = m.group
436
            if not g.is_enabled:
437
                continue
438
            for r, uplimit in g.quota.iteritems():
439
                d[r] += uplimit or inf
440
        # TODO set default for remaining
441
        return d
442

    
443
    @property
444
    def policies(self):
445
        return self.astakosuserquota_set.select_related().all()
446

    
447
    @policies.setter
448
    def policies(self, policies):
449
        for p in policies:
450
            service = policies.get('service', None)
451
            resource = policies.get('resource', None)
452
            uplimit = policies.get('uplimit', 0)
453
            update = policies.get('update', True)
454
            self.add_policy(service, resource, uplimit, update)
455

    
456
    def add_policy(self, service, resource, uplimit, update=True):
457
        """Raises ObjectDoesNotExist, IntegrityError"""
458
        resource = Resource.objects.get(service__name=service, name=resource)
459
        if update:
460
            AstakosUserQuota.objects.update_or_create(user=self,
461
                                                      resource=resource,
462
                                                      defaults={'uplimit': uplimit})
463
        else:
464
            q = self.astakosuserquota_set
465
            q.create(resource=resource, uplimit=uplimit)
466

    
467
    def remove_policy(self, service, resource):
468
        """Raises ObjectDoesNotExist, IntegrityError"""
469
        resource = Resource.objects.get(service__name=service, name=resource)
470
        q = self.policies.get(resource=resource).delete()
471

    
472
    @property
473
    def extended_groups(self):
474
        return self.membership_set.select_related().all()
475

    
476
    @extended_groups.setter
477
    def extended_groups(self, groups):
478
        #TODO exceptions
479
        for name in (groups or ()):
480
            group = AstakosGroup.objects.get(name=name)
481
            self.membership_set.create(group=group)
482

    
483
    def save(self, update_timestamps=True, **kwargs):
484
        if update_timestamps:
485
            if not self.id:
486
                self.date_joined = datetime.now()
487
            self.updated = datetime.now()
488

    
489
        # update date_signed_terms if necessary
490
        if self.__has_signed_terms != self.has_signed_terms:
491
            self.date_signed_terms = datetime.now()
492

    
493
        if not self.id:
494
            # set username
495
            self.username = self.email
496

    
497
        self.validate_unique_email_isactive()
498
        if self.is_active and self.activation_sent:
499
            # reset the activation sent
500
            self.activation_sent = None
501

    
502
        super(AstakosUser, self).save(**kwargs)
503

    
504
    def renew_token(self, flush_sessions=False, current_key=None):
505
        md5 = hashlib.md5()
506
        md5.update(settings.SECRET_KEY)
507
        md5.update(self.username)
508
        md5.update(self.realname.encode('ascii', 'ignore'))
509
        md5.update(asctime())
510

    
511
        self.auth_token = b64encode(md5.digest())
512
        self.auth_token_created = datetime.now()
513
        self.auth_token_expires = self.auth_token_created + \
514
                                  timedelta(hours=AUTH_TOKEN_DURATION)
515
        if flush_sessions:
516
            self.flush_sessions(current_key)
517
        msg = 'Token renewed for %s' % self.email
518
        logger.log(LOGGING_LEVEL, msg)
519

    
520
    def flush_sessions(self, current_key=None):
521
        q = self.sessions
522
        if current_key:
523
            q = q.exclude(session_key=current_key)
524

    
525
        keys = q.values_list('session_key', flat=True)
526
        if keys:
527
            msg = 'Flushing sessions: %s' % ','.join(keys)
528
            logger.log(LOGGING_LEVEL, msg, [])
529
        engine = import_module(settings.SESSION_ENGINE)
530
        for k in keys:
531
            s = engine.SessionStore(k)
532
            s.flush()
533

    
534
    def __unicode__(self):
535
        return '%s (%s)' % (self.realname, self.email)
536

    
537
    def conflicting_email(self):
538
        q = AstakosUser.objects.exclude(username=self.username)
539
        q = q.filter(email__iexact=self.email)
540
        if q.count() != 0:
541
            return True
542
        return False
543

    
544
    def validate_unique_email_isactive(self):
545
        """
546
        Implements a unique_together constraint for email and is_active fields.
547
        """
548
        q = AstakosUser.objects.all()
549
        q = q.filter(email = self.email)
550
        q = q.filter(is_active = self.is_active)
551
        if self.id:
552
            q = q.filter(~Q(id = self.id))
553
        if q.count() != 0:
554
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
555

    
556
    @property
557
    def signed_terms(self):
558
        term = get_latest_terms()
559
        if not term:
560
            return True
561
        if not self.has_signed_terms:
562
            return False
563
        if not self.date_signed_terms:
564
            return False
565
        if self.date_signed_terms < term.date:
566
            self.has_signed_terms = False
567
            self.date_signed_terms = None
568
            self.save()
569
            return False
570
        return True
571

    
572
    def set_invitations_level(self):
573
        """
574
        Update user invitation level
575
        """
576
        level = self.invitation.inviter.level + 1
577
        self.level = level
578
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
579

    
580
    def can_login_with_auth_provider(self, provider):
581
        if not self.has_auth_provider(provider):
582
            return False
583
        else:
584
            return auth_providers.get_provider(provider).is_available_for_login()
585

    
586
    def can_add_auth_provider(self, provider, **kwargs):
587
        provider_settings = auth_providers.get_provider(provider)
588
        if not provider_settings.is_available_for_login():
589
            return False
590

    
591
        if self.has_auth_provider(provider) and \
592
           provider_settings.one_per_user:
593
            return False
594

    
595
        if 'identifier' in kwargs:
596
            try:
597
                # provider with specified params already exist
598
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
599
                                                                   **kwargs)
600
            except AstakosUser.DoesNotExist:
601
                return True
602
            else:
603
                return False
604

    
605
        return True
606

    
607
    def can_remove_auth_provider(self, provider):
608
        if len(self.get_active_auth_providers()) <= 1:
609
            return False
610
        return True
611

    
612
    def can_change_password(self):
613
        return self.has_auth_provider('local', auth_backend='astakos')
614

    
615
    def has_auth_provider(self, provider, **kwargs):
616
        return bool(self.auth_providers.filter(module=provider,
617
                                               **kwargs).count())
618

    
619
    def add_auth_provider(self, provider, **kwargs):
620
        if self.can_add_auth_provider(provider, **kwargs):
621
            self.auth_providers.create(module=provider, active=True, **kwargs)
622
        else:
623
            raise Exception('Cannot add provider')
624

    
625
    def add_pending_auth_provider(self, pending):
626
        """
627
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
628
        the current user.
629
        """
630
        if not isinstance(pending, PendingThirdPartyUser):
631
            pending = PendingThirdPartyUser.objects.get(token=pending)
632

    
633
        provider = self.add_auth_provider(pending.provider,
634
                               identifier=pending.third_party_identifier)
635

    
636
        if email_re.match(pending.email or '') and pending.email != self.email:
637
            self.additionalmail_set.get_or_create(email=pending.email)
638

    
639
        pending.delete()
640
        return provider
641

    
642
    def remove_auth_provider(self, provider, **kwargs):
643
        self.auth_providers.get(module=provider, **kwargs).delete()
644

    
645
    # user urls
646
    def get_resend_activation_url(self):
647
        return reverse('send_activation', {'user_id': self.pk})
648

    
649
    def get_activation_url(self, nxt=False):
650
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
651
                                 quote(self.auth_token))
652
        if nxt:
653
            url += "&next=%s" % quote(nxt)
654
        return url
655

    
656
    def get_password_reset_url(self, token_generator=default_token_generator):
657
        return reverse('django.contrib.auth.views.password_reset_confirm',
658
                          kwargs={'uidb36':int_to_base36(self.id),
659
                                  'token':token_generator.make_token(self)})
660

    
661
    def get_auth_providers(self):
662
        return self.auth_providers.all()
663

    
664
    def get_available_auth_providers(self):
665
        """
666
        Returns a list of providers available for user to connect to.
667
        """
668
        providers = []
669
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
670
            if self.can_add_auth_provider(module):
671
                providers.append(provider_settings(self))
672

    
673
        return providers
674

    
675
    def get_active_auth_providers(self):
676
        providers = []
677
        for provider in self.auth_providers.active():
678
            if auth_providers.get_provider(provider.module).is_available_for_login():
679
                providers.append(provider)
680
        return providers
681

    
682
    @property
683
    def auth_providers_display(self):
684
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
685

    
686

    
687
class AstakosUserAuthProviderManager(models.Manager):
688

    
689
    def active(self):
690
        return self.filter(active=True)
691

    
692

    
693
class AstakosUserAuthProvider(models.Model):
694
    """
695
    Available user authentication methods.
696
    """
697
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
698
                                   null=True, default=None)
699
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
700
    module = models.CharField(_('Provider'), max_length=255, blank=False,
701
                                default='local')
702
    identifier = models.CharField(_('Third-party identifier'),
703
                                              max_length=255, null=True,
704
                                              blank=True)
705
    active = models.BooleanField(default=True)
706
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
707
                                   default='astakos')
708

    
709
    objects = AstakosUserAuthProviderManager()
710

    
711
    class Meta:
712
        unique_together = (('identifier', 'module', 'user'), )
713

    
714
    @property
715
    def settings(self):
716
        return auth_providers.get_provider(self.module)
717

    
718
    @property
719
    def details_display(self):
720
        return self.settings.details_tpl % self.__dict__
721

    
722
    def can_remove(self):
723
        return self.user.can_remove_auth_provider(self.module)
724

    
725
    def delete(self, *args, **kwargs):
726
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
727
        if self.module == 'local':
728
            self.user.set_unusable_password()
729
            self.user.save()
730
        return ret
731

    
732
    def __repr__(self):
733
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
734

    
735
    def __unicode__(self):
736
        if self.identifier:
737
            return "%s:%s" % (self.module, self.identifier)
738
        if self.auth_backend:
739
            return "%s:%s" % (self.module, self.auth_backend)
740
        return self.module
741

    
742

    
743

    
744
class Membership(models.Model):
745
    person = models.ForeignKey(AstakosUser)
746
    group = models.ForeignKey(AstakosGroup)
747
    date_requested = models.DateField(default=datetime.now(), blank=True)
748
    date_joined = models.DateField(null=True, db_index=True, blank=True)
749

    
750
    class Meta:
751
        unique_together = ("person", "group")
752

    
753
    def save(self, *args, **kwargs):
754
        if not self.id:
755
            if not self.group.moderation_enabled:
756
                self.date_joined = datetime.now()
757
        super(Membership, self).save(*args, **kwargs)
758

    
759
    @property
760
    def is_approved(self):
761
        if self.date_joined:
762
            return True
763
        return False
764

    
765
    def approve(self):
766
        if self.is_approved:
767
            return
768
        if self.group.max_participants:
769
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
770
            'Maximum participant number has been reached.'
771
        self.date_joined = datetime.now()
772
        self.save()
773
        quota_disturbed.send(sender=self, users=(self.person,))
774

    
775
    def disapprove(self):
776
        approved = self.is_approved()
777
        self.delete()
778
        if approved:
779
            quota_disturbed.send(sender=self, users=(self.person,))
780

    
781
class ExtendedManager(models.Manager):
782
    def _update_or_create(self, **kwargs):
783
        assert kwargs, \
784
            'update_or_create() must be passed at least one keyword argument'
785
        obj, created = self.get_or_create(**kwargs)
786
        defaults = kwargs.pop('defaults', {})
787
        if created:
788
            return obj, True, False
789
        else:
790
            try:
791
                params = dict(
792
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
793
                params.update(defaults)
794
                for attr, val in params.items():
795
                    if hasattr(obj, attr):
796
                        setattr(obj, attr, val)
797
                sid = transaction.savepoint()
798
                obj.save(force_update=True)
799
                transaction.savepoint_commit(sid)
800
                return obj, False, True
801
            except IntegrityError, e:
802
                transaction.savepoint_rollback(sid)
803
                try:
804
                    return self.get(**kwargs), False, False
805
                except self.model.DoesNotExist:
806
                    raise e
807

    
808
    update_or_create = _update_or_create
809

    
810
class AstakosGroupQuota(models.Model):
811
    objects = ExtendedManager()
812
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
813
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
814
    resource = models.ForeignKey(Resource)
815
    group = models.ForeignKey(AstakosGroup, blank=True)
816

    
817
    class Meta:
818
        unique_together = ("resource", "group")
819

    
820
class AstakosUserQuota(models.Model):
821
    objects = ExtendedManager()
822
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
823
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
824
    resource = models.ForeignKey(Resource)
825
    user = models.ForeignKey(AstakosUser)
826

    
827
    class Meta:
828
        unique_together = ("resource", "user")
829

    
830

    
831
class ApprovalTerms(models.Model):
832
    """
833
    Model for approval terms
834
    """
835

    
836
    date = models.DateTimeField(
837
        _('Issue date'), db_index=True, default=datetime.now())
838
    location = models.CharField(_('Terms location'), max_length=255)
839

    
840

    
841
class Invitation(models.Model):
842
    """
843
    Model for registring invitations
844
    """
845
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
846
                                null=True)
847
    realname = models.CharField(_('Real name'), max_length=255)
848
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
849
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
850
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
851
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
852
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
853

    
854
    def __init__(self, *args, **kwargs):
855
        super(Invitation, self).__init__(*args, **kwargs)
856
        if not self.id:
857
            self.code = _generate_invitation_code()
858

    
859
    def consume(self):
860
        self.is_consumed = True
861
        self.consumed = datetime.now()
862
        self.save()
863

    
864
    def __unicode__(self):
865
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
866

    
867

    
868
class EmailChangeManager(models.Manager):
869
    @transaction.commit_on_success
870
    def change_email(self, activation_key):
871
        """
872
        Validate an activation key and change the corresponding
873
        ``User`` if valid.
874

875
        If the key is valid and has not expired, return the ``User``
876
        after activating.
877

878
        If the key is not valid or has expired, return ``None``.
879

880
        If the key is valid but the ``User`` is already active,
881
        return ``None``.
882

883
        After successful email change the activation record is deleted.
884

885
        Throws ValueError if there is already
886
        """
887
        try:
888
            email_change = self.model.objects.get(
889
                activation_key=activation_key)
890
            if email_change.activation_key_expired():
891
                email_change.delete()
892
                raise EmailChange.DoesNotExist
893
            # is there an active user with this address?
894
            try:
895
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
896
            except AstakosUser.DoesNotExist:
897
                pass
898
            else:
899
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
900
            # update user
901
            user = AstakosUser.objects.get(pk=email_change.user_id)
902
            user.email = email_change.new_email_address
903
            user.save()
904
            email_change.delete()
905
            return user
906
        except EmailChange.DoesNotExist:
907
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
908

    
909

    
910
class EmailChange(models.Model):
911
    new_email_address = models.EmailField(_(u'new e-mail address'),
912
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
913
    user = models.ForeignKey(
914
        AstakosUser, unique=True, related_name='emailchange_user')
915
    requested_at = models.DateTimeField(default=datetime.now())
916
    activation_key = models.CharField(
917
        max_length=40, unique=True, db_index=True)
918

    
919
    objects = EmailChangeManager()
920

    
921
    def activation_key_expired(self):
922
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
923
        return self.requested_at + expiration_date < datetime.now()
924

    
925

    
926
class AdditionalMail(models.Model):
927
    """
928
    Model for registring invitations
929
    """
930
    owner = models.ForeignKey(AstakosUser)
931
    email = models.EmailField()
932

    
933

    
934
def _generate_invitation_code():
935
    while True:
936
        code = randint(1, 2L ** 63 - 1)
937
        try:
938
            Invitation.objects.get(code=code)
939
            # An invitation with this code already exists, try again
940
        except Invitation.DoesNotExist:
941
            return code
942

    
943

    
944
def get_latest_terms():
945
    try:
946
        term = ApprovalTerms.objects.order_by('-id')[0]
947
        return term
948
    except IndexError:
949
        pass
950
    return None
951

    
952
class PendingThirdPartyUser(models.Model):
953
    """
954
    Model for registring successful third party user authentications
955
    """
956
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
957
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
958
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
959
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
960
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
961
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
962
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
963
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
964
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
965

    
966
    class Meta:
967
        unique_together = ("provider", "third_party_identifier")
968

    
969
    @property
970
    def realname(self):
971
        return '%s %s' %(self.first_name, self.last_name)
972

    
973
    @realname.setter
974
    def realname(self, value):
975
        parts = value.split(' ')
976
        if len(parts) == 2:
977
            self.first_name = parts[0]
978
            self.last_name = parts[1]
979
        else:
980
            self.last_name = parts[0]
981

    
982
    def save(self, **kwargs):
983
        if not self.id:
984
            # set username
985
            while not self.username:
986
                username =  uuid.uuid4().hex[:30]
987
                try:
988
                    AstakosUser.objects.get(username = username)
989
                except AstakosUser.DoesNotExist, e:
990
                    self.username = username
991
        super(PendingThirdPartyUser, self).save(**kwargs)
992

    
993
    def generate_token(self):
994
        self.password = self.third_party_identifier
995
        self.last_login = datetime.now()
996
        self.token = default_token_generator.make_token(self)
997

    
998
class SessionCatalog(models.Model):
999
    session_key = models.CharField(_('session key'), max_length=40)
1000
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1001

    
1002
class MemberAcceptPolicy(models.Model):
1003
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1004
    description = models.CharField(_('Description'), max_length=80)
1005

    
1006
    def __str__(self):
1007
        return self.policy
1008

    
1009
try:
1010
    auto_accept = MemberAcceptPolicy.objects.get(policy='auto_accept')
1011
except:
1012
    auto_accept = None
1013

    
1014
class ProjectDefinition(models.Model):
1015
    name = models.CharField(max_length=80)
1016
    homepage = models.URLField(max_length=255, null=True, blank=True)
1017
    description = models.TextField(null=True)
1018
    start_date = models.DateTimeField()
1019
    end_date = models.DateTimeField()
1020
    member_accept_policy = models.ForeignKey(MemberAcceptPolicy)
1021
    limit_on_members_number = models.PositiveIntegerField(null=True,blank=True)
1022
    resource_grants = models.ManyToManyField(
1023
        Resource,
1024
        null=True,
1025
        blank=True,
1026
        through='ProjectResourceGrant'
1027
    )
1028
    
1029
    def save(self):
1030
        self.validate_name()
1031
        super(ProjectDefinition, self).save()
1032
    
1033
    def validate_name(self):
1034
        """
1035
        Validate name uniqueness among all active projects.
1036
        """
1037
        alive_projects = list(get_alive_projects())
1038
        q = filter(lambda p: p.definition.name==self.name, alive_projects)
1039
        if q:
1040
            raise ValidationError({'name': [_(astakos_messages.UNIQUE_PROJECT_NAME_CONSTRAIN_ERR)]})
1041
    
1042
    @property
1043
    def violated_resource_grants(self):
1044
        return False
1045
    
1046
    def add_resource_policy(self, service, resource, uplimit, update=True):
1047
        """Raises ObjectDoesNotExist, IntegrityError"""
1048
        resource = Resource.objects.get(service__name=service, name=resource)
1049
        if update:
1050
            ResourceGrant.objects.update_or_create(
1051
                project=self,
1052
                resource=resource,
1053
                defaults={'uplimit': uplimit}
1054
            )
1055
        else:
1056
            q = self.resource_grants_set
1057
            q.create(resource=resource, uplimit=uplimit)
1058

    
1059
    @property
1060
    def resource_policies(self):
1061
        return self.resource_grants_set.select_related().all()
1062

    
1063
    @resource_policies.setter
1064
    def resource_policies(self, policies):
1065
        for p in policies:
1066
            service = p.get('service', None)
1067
            resource = p.get('resource', None)
1068
            uplimit = p.get('uplimit', 0)
1069
            update = p.get('update', True)
1070
            self.add_resource_policy(service, resource, uplimit, update)
1071

    
1072

    
1073
class ProjectResourceGrant(models.Model):
1074
    objects = ExtendedManager()
1075
    member_limit = models.BigIntegerField(null=True)
1076
    project_limit = models.BigIntegerField(null=True)
1077
    resource = models.ForeignKey(Resource)
1078
    project_definition = models.ForeignKey(ProjectDefinition, blank=True)
1079

    
1080
    class Meta:
1081
        unique_together = ("resource", "project_definition")
1082

    
1083
class ProjectApplication(models.Model):
1084
    serial = models.CharField(
1085
        primary_key=True,
1086
        max_length=30,
1087
        unique=True,
1088
        default=uuid.uuid4().hex[:30]
1089
    )
1090
    applicant = models.ForeignKey(AstakosUser, related_name='my_project_applications')
1091
    owner = models.ForeignKey(AstakosUser, related_name='own_project_applications')
1092
    comments = models.TextField(null=True, blank=True)
1093
    definition = models.OneToOneField(ProjectDefinition)
1094
    issue_date = models.DateTimeField()
1095
    precursor_application = models.OneToOneField('ProjectApplication',
1096
        null=True,
1097
        blank=True
1098
    )
1099

    
1100
class Project(models.Model):
1101
    serial = models.CharField(
1102
        _('username'),
1103
        primary_key=True,
1104
        max_length=30,
1105
        unique=True,
1106
        default=uuid.uuid4().hex[:30]
1107
    )
1108
    application = models.OneToOneField(ProjectApplication, related_name='project')
1109
    creation_date = models.DateTimeField()
1110
    last_approval_date = models.DateTimeField()
1111
    termination_date = models.DateTimeField()
1112
    members = models.ManyToManyField(AstakosUser, through='ProjectMembership')
1113
    last_synced_application = models.OneToOneField(
1114
        ProjectApplication, related_name='last_project', null=True, blank=True
1115
    )
1116
    
1117
    @property
1118
    def definition(self):
1119
        return self.application.definition
1120
    
1121
    @property
1122
    def is_valid(self):
1123
        try:
1124
            self.application.definition.validate_name()
1125
        except ValidationError:
1126
            return False
1127
        else:
1128
            return True
1129
    
1130
    @property
1131
    def is_active(self):
1132
        if not self.is_valid:
1133
            return False
1134
        if not self.last_approval_date:
1135
            return False
1136
        if self.termination_date:
1137
            return False
1138
        if self.definition.violated_resource_grants:
1139
            return False
1140
        return True
1141
    
1142
    @property
1143
    def is_terminated(self):
1144
        if not self.is_valid:
1145
            return False
1146
        if not self.termination_date:
1147
            return False
1148
        return True
1149
    
1150
    @property
1151
    def is_suspended(self):
1152
        if not self.is_valid:
1153
            return False
1154
        if not self.termination_date:
1155
            return False
1156
        if not self.last_approval_date:
1157
            if not self.definition.violated_resource_grants:
1158
                return False
1159
        return True
1160
    
1161
    @property
1162
    def is_alive(self):
1163
        return self.is_active or self.is_suspended
1164
    
1165
    @property
1166
    def is_inconsistent(self):
1167
        now = datetime.now()
1168
        if self.creation_date > now:
1169
            return True
1170
        if self.last_approval_date > now:
1171
            return True
1172
        if self.terminaton_date > now:
1173
            return True
1174
        return False
1175
    
1176
    @property
1177
    def approved_members(self):
1178
        return self.members.filter(is_accepted=True)
1179
    
1180
    def suspend(self):
1181
        self.last_approval_date = None
1182
        self.save()
1183
    
1184
    def terminate(self):
1185
        self.terminaton_date = datetime.now()
1186
        self.save()
1187
    
1188
    def sync(self):
1189
        c, rejected = send_quota(self.approved_members)
1190
        return rejected
1191

    
1192
class ProjectMembership(models.Model):
1193
    person = models.ForeignKey(AstakosUser)
1194
    project = models.ForeignKey(Project)
1195
    issue_date = models.DateField(default=datetime.now())
1196
    decision_date = models.DateField(null=True, db_index=True)
1197
    is_accepted = models.BooleanField(
1198
        _('Whether the membership application is accepted'),
1199
        default=False
1200
    )
1201

    
1202
    class Meta:
1203
        unique_together = ("person", "project")
1204

    
1205
def filter_queryset_by_property(q, property):
1206
    """
1207
    Incorporate list comprehension for filtering querysets by property
1208
    since Queryset.filter() operates on the database level.
1209
    """
1210
    return (p for p in q if getattr(p, property, False))
1211

    
1212
def get_alive_projects():
1213
    return filter_queryset_by_property(
1214
        Project.objects.all(),
1215
        'is_alive'
1216
    )
1217

    
1218
def get_active_projects():
1219
    return filter_queryset_by_property(
1220
        Project.objects.all(),
1221
        'is_active'
1222
    )
1223

    
1224
def _lookup_object(model, **kwargs):
1225
    """
1226
    Returns an object of the specific model matching the given lookup
1227
    parameters.
1228
    """
1229
    if not kwargs:
1230
        raise MissingIdentifier
1231
    try:
1232
        return model.objects.get(**kwargs)
1233
    except model.DoesNotExist:
1234
        raise ItemNotExists(model._meta.verbose_name, **kwargs)
1235
    except model.MultipleObjectsReturned:
1236
        raise MultipleItemsExist(model._meta.verbose_name, **kwargs)
1237

    
1238
def _create_object(model, **kwargs):
1239
    o = model.objects.create(**kwargs)
1240
    o.save()
1241
    return o
1242

    
1243
def _update_object(model, id, save=True, **kwargs):
1244
    o = self._lookup_object(model, id=id)
1245
    if kwargs:
1246
        o.__dict__.update(kwargs)
1247
    if save:
1248
        o.save()
1249
    return o
1250

    
1251
def submit_application(**kwargs):
1252
    app = self._create_object(ProjectApplication, **kwargs)
1253
    notification = build_notification(
1254
        settings.SERVER_EMAIL,
1255
        [settings.ADMINS],
1256
        _(GROUP_CREATION_SUBJECT) % {'group':app.definition.name},
1257
        _('An new project application identified by %(serial)s has been submitted.') % app.serial
1258
    )
1259
    notification.send()
1260

    
1261
def list_applications():
1262
    return ProjectAppication.objects.all()
1263

    
1264
def create_application(definition, applicant, comments, precursor_application=None, commit=True):
1265
    if precursor_application:
1266
        application = precursor_application.copy()
1267
        application.precursor_application = precursor_application
1268
    else:
1269
        application = ProjectApplication(owner=applicant)
1270
    application.definition = definition
1271
    application.applicant = applicant
1272
    application.comments = comments
1273
    application.issue_date = datetime.now()
1274
    if commit:
1275
        definition.save()
1276
        application.save()
1277
    return application
1278
    
1279
def approve_application(serial):
1280
    app = _lookup_object(ProjectAppication, serial=serial)
1281
    notify = False
1282
    if not app.precursor_application:
1283
        kwargs = {
1284
            'application':app,
1285
            'creation_date':datetime.now(),
1286
            'last_approval_date':datetime.now(),
1287
        }
1288
        project = _create_object(Project, **kwargs)
1289
    else:
1290
        project = app.precursor_application.project
1291
        last_approval_date = project.last_approval_date
1292
        if project.is_valid:
1293
            project.application = app
1294
            project.last_approval_date = datetime.now()
1295
            project.save()
1296
        else:
1297
            raise Exception(_(astakos_messages.INVALID_PROJECT) % project.__dict__)
1298
    
1299
    rejected = synchonize_project(project.serial)
1300
    if rejected:
1301
        # revert to precursor
1302
        project.appication = app.precursor_application
1303
        if project.application:
1304
            project.last_approval_date = last_approval_date
1305
        project.save()
1306
        rejected = synchonize_project(project.serial)
1307
        if rejected:
1308
            raise Exception(_(astakos_messages.QH_SYNC_ERROR))
1309
    else:
1310
        project.last_application_synced = app
1311
        project.save()
1312
        sender, recipients, subject, message
1313
        notification = build_notification(
1314
            settings.SERVER_EMAIL,
1315
            [project.owner.email],
1316
            _('Project application has been approved on %s alpha2 testing' % SITENAME),
1317
            _('Your application request %(serial)s has been apporved.')
1318
        )
1319
        notification.send()
1320

    
1321

    
1322
def list_projects(filter_property=None):
1323
    if filter_property:
1324
        return filter_queryset_by_property(
1325
            Project.objects.all(),
1326
            filter_property
1327
        )
1328
    return Project.objects.all()
1329

    
1330
def add_project_member(serial, user_id, request_user):
1331
    project = _lookup_object(Project, serial=serial)
1332
    user = _lookup_object(AstakosUser, id=user_id)
1333
    if not project.owner == request_user:
1334
        raise Exception(_(astakos_messages.NOT_PROJECT_OWNER))
1335
    
1336
    if not project.is_alive:
1337
        raise Exception(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1338
    if len(project.members) + 1 > project.limit_on_members_number:
1339
        raise Exception(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
1340
    m = self._lookup_object(ProjectMembership, person=user, project=project)
1341
    if m.is_accepted:
1342
        return
1343
    m.is_accepted = True
1344
    m.decision_date = datetime.now()
1345
    m.save()
1346
    notification = build_notification(
1347
        settings.SERVER_EMAIL,
1348
        [user.email],
1349
        _('Your membership on project %(name)s has been accepted.') % project.definition.__dict__, 
1350
        _('Your membership on project %(name)s has been accepted.') % project.definition.__dict__,
1351
    )
1352
    notification.send()
1353

    
1354
def remove_project_member(serial, user_id, request_user):
1355
    project = _lookup_object(Project, serial=serial)
1356
    if not project.is_alive:
1357
        raise Exception(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1358
    if not project.owner == request_user:
1359
        raise Exception(_(astakos_messages.NOT_PROJECT_OWNER))
1360
    user = self.lookup_user(user_id)
1361
    m = _lookup_object(ProjectMembership, person=user, project=project)
1362
    if not m.is_accepted:
1363
        return
1364
    m.is_accepted = False
1365
    m.decision_date = datetime.now()
1366
    m.save()
1367
    notification = build_notification(
1368
        settings.SERVER_EMAIL,
1369
        [user.email],
1370
        _('Your membership on project %(name)s has been removed.') % project.definition.__dict__,
1371
        _('Your membership on project %(name)s has been removed.') % project.definition.__dict__
1372
    )
1373
    notification.send()    
1374

    
1375
def suspend_project(serial):
1376
    project = _lookup_object(Project, serial=serial)
1377
    project.suspend()
1378
    notification = build_notification(
1379
        settings.SERVER_EMAIL,
1380
        [project.owner.email],
1381
        _('Project %(name)s has been suspended.') %  project.definition.__dict__,
1382
        _('Project %(name)s has been suspended.') %  project.definition.__dict__
1383
    )
1384
    notification.send()
1385

    
1386
def terminate_project(serial):
1387
    project = _lookup_object(Project, serial=serial)
1388
    project.termination()
1389
    notification = build_notification(
1390
        settings.SERVER_EMAIL,
1391
        [project.owner.email],
1392
        _('Project %(name)s has been terminated.') %  project.definition.__dict__,
1393
        _('Project %(name)s has been terminated.') %  project.definition.__dict__
1394
    )
1395
    notification.send()
1396

    
1397
def synchonize_project(serial):
1398
    project = _lookup_object(Project, serial=serial)
1399
    if project.app != project.last_application_synced:
1400
        return project.sync()
1401
     
1402
def create_astakos_user(u):
1403
    try:
1404
        AstakosUser.objects.get(user_ptr=u.pk)
1405
    except AstakosUser.DoesNotExist:
1406
        extended_user = AstakosUser(user_ptr_id=u.pk)
1407
        extended_user.__dict__.update(u.__dict__)
1408
        extended_user.save()
1409
        if not extended_user.has_auth_provider('local'):
1410
            extended_user.add_auth_provider('local')
1411
    except BaseException, e:
1412
        logger.exception(e)
1413

    
1414

    
1415
def fix_superusers(sender, **kwargs):
1416
    # Associate superusers with AstakosUser
1417
    admins = User.objects.filter(is_superuser=True)
1418
    for u in admins:
1419
        create_astakos_user(u)
1420

    
1421

    
1422
def user_post_save(sender, instance, created, **kwargs):
1423
    if not created:
1424
        return
1425
    create_astakos_user(instance)
1426

    
1427

    
1428
def set_default_group(user):
1429
    try:
1430
        default = AstakosGroup.objects.get(name='default')
1431
        Membership(
1432
            group=default, person=user, date_joined=datetime.now()).save()
1433
    except AstakosGroup.DoesNotExist, e:
1434
        logger.exception(e)
1435

    
1436

    
1437
def astakosuser_pre_save(sender, instance, **kwargs):
1438
    instance.aquarium_report = False
1439
    instance.new = False
1440
    try:
1441
        db_instance = AstakosUser.objects.get(id=instance.id)
1442
    except AstakosUser.DoesNotExist:
1443
        # create event
1444
        instance.aquarium_report = True
1445
        instance.new = True
1446
    else:
1447
        get = AstakosUser.__getattribute__
1448
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1449
                   BILLING_FIELDS)
1450
        instance.aquarium_report = True if l else False
1451

    
1452

    
1453
def astakosuser_post_save(sender, instance, created, **kwargs):
1454
    if instance.aquarium_report:
1455
        report_user_event(instance, create=instance.new)
1456
    if not created:
1457
        return
1458
    set_default_group(instance)
1459
    # TODO handle socket.error & IOError
1460
    register_users((instance,))
1461

    
1462

    
1463
def resource_post_save(sender, instance, created, **kwargs):
1464
    if not created:
1465
        return
1466
    register_resources((instance,))
1467

    
1468

    
1469
def send_quota_disturbed(sender, instance, **kwargs):
1470
    users = []
1471
    extend = users.extend
1472
    if sender == Membership:
1473
        if not instance.group.is_enabled:
1474
            return
1475
        extend([instance.person])
1476
    elif sender == AstakosUserQuota:
1477
        extend([instance.user])
1478
    elif sender == AstakosGroupQuota:
1479
        if not instance.group.is_enabled:
1480
            return
1481
        extend(instance.group.astakosuser_set.all())
1482
    elif sender == AstakosGroup:
1483
        if not instance.is_enabled:
1484
            return
1485
    quota_disturbed.send(sender=sender, users=users)
1486

    
1487

    
1488
def on_quota_disturbed(sender, users, **kwargs):
1489
#     print '>>>', locals()
1490
    if not users:
1491
        return
1492
    send_quota(users)
1493

    
1494
def renew_token(sender, instance, **kwargs):
1495
    if not instance.auth_token:
1496
        instance.renew_token()
1497

    
1498
post_syncdb.connect(fix_superusers)
1499
post_save.connect(user_post_save, sender=User)
1500
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1501
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1502
post_save.connect(resource_post_save, sender=Resource)
1503

    
1504
quota_disturbed = Signal(providing_args=["users"])
1505
quota_disturbed.connect(on_quota_disturbed)
1506

    
1507
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1508
post_delete.connect(send_quota_disturbed, sender=Membership)
1509
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1510
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1511
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1512
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1513

    
1514
pre_save.connect(renew_token, sender=AstakosUser)
1515
pre_save.connect(renew_token, sender=Service)