Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 71a38edf

History | View | Annotate | Download (51.1 kB)

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

    
34
import hashlib
35
import uuid
36
import logging
37

    
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
    GROUP_CREATION_SUBJECT
70
)
71
from astakos.im.endpoints.qh import (
72
    register_users, send_quota, register_resources
73
)
74
from astakos.im import auth_providers
75
from astakos.im.endpoints.aquarium.producer import report_user_event
76
from astakos.im.functions import send_invitation
77
#from astakos.im.tasks import propagate_groupmembers_quota
78

    
79
from astakos.im.notifications import build_notification
80

    
81
import astakos.im.messages as astakos_messages
82

    
83
logger = logging.getLogger(__name__)
84

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

    
91
RESOURCE_SEPARATOR = '.'
92

    
93
inf = float('inf')
94

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

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

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

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

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

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

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

    
139

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

    
144

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

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

    
156

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

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

    
163

    
164
class AstakosGroup(Group):
165
    kind = models.ForeignKey(GroupKind)
166
    homepage = models.URLField(
167
        _('Homepage Url'), max_length=255, null=True, blank=True)
168
    desc = models.TextField(_('Description'), null=True)
169
    policy = models.ManyToManyField(
170
        Resource,
171
        null=True,
172
        blank=True,
173
        through='AstakosGroupQuota'
174
    )
175
    creation_date = models.DateTimeField(
176
        _('Creation date'),
177
        default=datetime.now()
178
    )
179
    issue_date = models.DateTimeField('Start date', null=True)
180
    expiration_date = models.DateTimeField(
181
        _('Expiration date'),
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
            ProjectResourceGrant.objects.update_or_create(
1052
                project_definition=self,
1053
                resource=resource,
1054
                defaults={'member_limit': uplimit}
1055
            )
1056
        else:
1057
            q = self.projectresourcegrant_set
1058
            q.create(resource=resource, member_limit=uplimit)
1059

    
1060
    @property
1061
    def resource_policies(self):
1062
        return self.projectresourcegrant_set.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
    def get_absolute_url(self):
1074
        return reverse('project_application_detail', args=(self.serial,))
1075

    
1076

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

    
1084
    class Meta:
1085
        unique_together = ("resource", "project_definition")
1086

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

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

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

    
1206
    class Meta:
1207
        unique_together = ("person", "project")
1208

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

    
1216
def get_alive_projects():
1217
    return filter_queryset_by_property(
1218
        Project.objects.all(),
1219
        'is_alive'
1220
    )
1221

    
1222
def get_active_projects():
1223
    return filter_queryset_by_property(
1224
        Project.objects.all(),
1225
        'is_active'
1226
    )
1227

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

    
1242
def _create_object(model, **kwargs):
1243
    o = model.objects.create(**kwargs)
1244
    o.save()
1245
    return o
1246

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

    
1255
def list_applications():
1256
    return ProjectAppication.objects.all()
1257

    
1258
def submit_application(definition, applicant, comments, precursor_application=None, commit=True):
1259
    if precursor_application:
1260
        application = precursor_application.copy()
1261
        application.precursor_application = precursor_application
1262
    else:
1263
        application = ProjectApplication(owner=applicant)
1264
    application.definition = definition
1265
    application.applicant = applicant
1266
    application.comments = comments
1267
    application.issue_date = datetime.now()
1268
    if commit:
1269
        definition.save()
1270
        application.save()
1271
        notification = build_notification(
1272
        settings.SERVER_EMAIL,
1273
        [i[1] for i in settings.ADMINS],
1274
        _(GROUP_CREATION_SUBJECT) % {'group':application.definition.name},
1275
        _('An new project application identified by %(serial)s has been submitted.') % application.__dict__
1276
    )
1277
    notification.send()
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)