Statistics
| Branch: | Tag: | Revision:

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

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
         null=True
183
    )
184
    moderation_enabled = models.BooleanField(
185
        _('Moderated membership?'),
186
        default=True
187
    )
188
    approval_date = models.DateTimeField(
189
        _('Activation date'),
190
        null=True,
191
        blank=True
192
    )
193
    estimated_participants = models.PositiveIntegerField(
194
        _('Estimated #members'),
195
        null=True,
196
        blank=True,
197
    )
198
    max_participants = models.PositiveIntegerField(
199
        _('Maximum numder of participants'),
200
        null=True,
201
        blank=True
202
    )
203

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
303

    
304

    
305
class AstakosUserManager(UserManager):
306

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

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

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

    
332

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

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

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

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

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

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

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

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

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

    
371
    objects = AstakosUserManager()
372

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
606
        return True
607

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

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

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

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

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

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

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

    
640
        pending.delete()
641
        return provider
642

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

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

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

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

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

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

    
674
        return providers
675

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

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

    
687

    
688
class AstakosUserAuthProviderManager(models.Manager):
689

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

    
693

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

    
710
    objects = AstakosUserAuthProviderManager()
711

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

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

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

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

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

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

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

    
743

    
744

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

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

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

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

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

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

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

    
809
    update_or_create = _update_or_create
810

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

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

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

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

    
831

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

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

    
841

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

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

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

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

    
868

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

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

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

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

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

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

    
910

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

    
920
    objects = EmailChangeManager()
921

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

    
926

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

    
934

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

    
944

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1073

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1322

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

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

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

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

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

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

    
1415

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

    
1422

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

    
1428

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

    
1437

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

    
1453

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

    
1463

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

    
1469

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

    
1488

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

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

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

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

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

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