Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (54.2 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
_content_type = None
87

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

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

    
100
RESOURCE_SEPARATOR = '.'
101

    
102
inf = float('inf')
103

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

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

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

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

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

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

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

    
148

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

    
153

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

    
162
    def __str__(self):
163
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
164

    
165

    
166
class GroupKind(models.Model):
167
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
168

    
169
    def __str__(self):
170
        return self.name
171

    
172

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

    
213
    @property
214
    def is_disabled(self):
215
        if not self.approval_date:
216
            return True
217
        return False
218

    
219
    @property
220
    def is_enabled(self):
221
        if self.is_disabled:
222
            return False
223
        if not self.issue_date:
224
            return False
225
        if not self.expiration_date:
226
            return True
227
        now = datetime.now()
228
        if self.issue_date > now:
229
            return False
230
        if now >= self.expiration_date:
231
            return False
232
        return True
233

    
234
    def enable(self):
235
        if self.is_enabled:
236
            return
237
        self.approval_date = datetime.now()
238
        self.save()
239
        quota_disturbed.send(sender=self, users=self.approved_members)
240
        #propagate_groupmembers_quota.apply_async(
241
        #    args=[self], eta=self.issue_date)
242
        #propagate_groupmembers_quota.apply_async(
243
        #    args=[self], eta=self.expiration_date)
244

    
245
    def disable(self):
246
        if self.is_disabled:
247
            return
248
        self.approval_date = None
249
        self.save()
250
        quota_disturbed.send(sender=self, users=self.approved_members)
251

    
252
    def approve_member(self, person):
253
        m, created = self.membership_set.get_or_create(person=person)
254
        m.approve()
255

    
256
    @property
257
    def members(self):
258
        q = self.membership_set.select_related().all()
259
        return [m.person for m in q]
260

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

    
266
    @property
267
    def quota(self):
268
        d = defaultdict(int)
269
        for q in self.astakosgroupquota_set.select_related().all():
270
            d[q.resource] += q.uplimit or inf
271
        return d
272

    
273
    def add_policy(self, service, resource, uplimit, update=True):
274
        """Raises ObjectDoesNotExist, IntegrityError"""
275
        resource = Resource.objects.get(service__name=service, name=resource)
276
        if update:
277
            AstakosGroupQuota.objects.update_or_create(
278
                group=self,
279
                resource=resource,
280
                defaults={'uplimit': uplimit}
281
            )
282
        else:
283
            q = self.astakosgroupquota_set
284
            q.create(resource=resource, uplimit=uplimit)
285

    
286
    @property
287
    def policies(self):
288
        return self.astakosgroupquota_set.select_related().all()
289

    
290
    @policies.setter
291
    def policies(self, policies):
292
        for p in policies:
293
            service = p.get('service', None)
294
            resource = p.get('resource', None)
295
            uplimit = p.get('uplimit', 0)
296
            update = p.get('update', True)
297
            self.add_policy(service, resource, uplimit, update)
298

    
299
    @property
300
    def owners(self):
301
        return self.owner.all()
302

    
303
    @property
304
    def owner_details(self):
305
        return self.owner.select_related().all()
306

    
307
    @owners.setter
308
    def owners(self, l):
309
        self.owner = l
310
        map(self.approve_member, l)
311

    
312

    
313

    
314
class AstakosUserManager(UserManager):
315

    
316
    def get_auth_provider_user(self, provider, **kwargs):
317
        """
318
        Retrieve AstakosUser instance associated with the specified third party
319
        id.
320
        """
321
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
322
                          kwargs.iteritems()))
323
        return self.get(auth_providers__module=provider, **kwargs)
324

    
325
class AstakosUser(User):
326
    """
327
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
328
    """
329
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
330
                                   null=True)
331

    
332
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
333
    #                    AstakosUserProvider model.
334
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
335
                                null=True)
336
    # ex. screen_name for twitter, eppn for shibboleth
337
    third_party_identifier = models.CharField(_('Third-party identifier'),
338
                                              max_length=255, null=True,
339
                                              blank=True)
340

    
341

    
342
    #for invitations
343
    user_level = DEFAULT_USER_LEVEL
344
    level = models.IntegerField(_('Inviter level'), default=user_level)
345
    invitations = models.IntegerField(
346
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
347

    
348
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
349
                                  null=True, blank=True)
350
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
351
    auth_token_expires = models.DateTimeField(
352
        _('Token expiration date'), null=True)
353

    
354
    updated = models.DateTimeField(_('Update date'))
355
    is_verified = models.BooleanField(_('Is verified?'), default=False)
356

    
357
    email_verified = models.BooleanField(_('Email verified?'), default=False)
358

    
359
    has_credits = models.BooleanField(_('Has credits?'), default=False)
360
    has_signed_terms = models.BooleanField(
361
        _('I agree with the terms'), default=False)
362
    date_signed_terms = models.DateTimeField(
363
        _('Signed terms date'), null=True, blank=True)
364

    
365
    activation_sent = models.DateTimeField(
366
        _('Activation sent data'), null=True, blank=True)
367

    
368
    policy = models.ManyToManyField(
369
        Resource, null=True, through='AstakosUserQuota')
370

    
371
    astakos_groups = models.ManyToManyField(
372
        AstakosGroup, verbose_name=_('agroups'), blank=True,
373
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
374
        through='Membership')
375

    
376
    __has_signed_terms = False
377
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
378
                                           default=False, db_index=True)
379

    
380
    objects = AstakosUserManager()
381

    
382
    owner = models.ManyToManyField(
383
        AstakosGroup, related_name='owner', null=True)
384

    
385
    class Meta:
386
        unique_together = ("provider", "third_party_identifier")
387

    
388
    def __init__(self, *args, **kwargs):
389
        super(AstakosUser, self).__init__(*args, **kwargs)
390
        self.__has_signed_terms = self.has_signed_terms
391
        if not self.id:
392
            self.is_active = False
393

    
394
    @property
395
    def realname(self):
396
        return '%s %s' % (self.first_name, self.last_name)
397

    
398
    @realname.setter
399
    def realname(self, value):
400
        parts = value.split(' ')
401
        if len(parts) == 2:
402
            self.first_name = parts[0]
403
            self.last_name = parts[1]
404
        else:
405
            self.last_name = parts[0]
406

    
407
    def add_permission(self, pname):
408
        if self.has_perm(pname):
409
            return
410
        p, created = Permission.objects.get_or_create(
411
                                    codename=pname,
412
                                    name=pname.capitalize(),
413
                                    content_type=get_content_type())
414
        self.user_permissions.add(p)
415

    
416
    def remove_permission(self, pname):
417
        if self.has_perm(pname):
418
            return
419
        p = Permission.objects.get(codename=pname,
420
                                   content_type=get_content_type())
421
        self.user_permissions.remove(p)
422

    
423
    @property
424
    def invitation(self):
425
        try:
426
            return Invitation.objects.get(username=self.email)
427
        except Invitation.DoesNotExist:
428
            return None
429

    
430
    def invite(self, email, realname):
431
        inv = Invitation(inviter=self, username=email, realname=realname)
432
        inv.save()
433
        send_invitation(inv)
434
        self.invitations = max(0, self.invitations - 1)
435
        self.save()
436

    
437
    @property
438
    def quota(self):
439
        """Returns a dict with the sum of quota limits per resource"""
440
        d = defaultdict(int)
441
        for q in self.policies:
442
            d[q.resource] += q.uplimit or inf
443
        for m in self.extended_groups:
444
            if not m.is_approved:
445
                continue
446
            g = m.group
447
            if not g.is_enabled:
448
                continue
449
            for r, uplimit in g.quota.iteritems():
450
                d[r] += uplimit or inf
451
        # TODO set default for remaining
452
        return d
453

    
454
    @property
455
    def policies(self):
456
        return self.astakosuserquota_set.select_related().all()
457

    
458
    @policies.setter
459
    def policies(self, policies):
460
        for p in policies:
461
            service = policies.get('service', None)
462
            resource = policies.get('resource', None)
463
            uplimit = policies.get('uplimit', 0)
464
            update = policies.get('update', True)
465
            self.add_policy(service, resource, uplimit, update)
466

    
467
    def add_policy(self, service, resource, uplimit, update=True):
468
        """Raises ObjectDoesNotExist, IntegrityError"""
469
        resource = Resource.objects.get(service__name=service, name=resource)
470
        if update:
471
            AstakosUserQuota.objects.update_or_create(user=self,
472
                                                      resource=resource,
473
                                                      defaults={'uplimit': uplimit})
474
        else:
475
            q = self.astakosuserquota_set
476
            q.create(resource=resource, uplimit=uplimit)
477

    
478
    def remove_policy(self, service, resource):
479
        """Raises ObjectDoesNotExist, IntegrityError"""
480
        resource = Resource.objects.get(service__name=service, name=resource)
481
        q = self.policies.get(resource=resource).delete()
482

    
483
    @property
484
    def extended_groups(self):
485
        return self.membership_set.select_related().all()
486

    
487
    @extended_groups.setter
488
    def extended_groups(self, groups):
489
        #TODO exceptions
490
        for name in (groups or ()):
491
            group = AstakosGroup.objects.get(name=name)
492
            self.membership_set.create(group=group)
493

    
494
    def save(self, update_timestamps=True, **kwargs):
495
        if update_timestamps:
496
            if not self.id:
497
                self.date_joined = datetime.now()
498
            self.updated = datetime.now()
499

    
500
        # update date_signed_terms if necessary
501
        if self.__has_signed_terms != self.has_signed_terms:
502
            self.date_signed_terms = datetime.now()
503

    
504
        if not self.id:
505
            # set username
506
            self.username = self.email
507

    
508
        self.validate_unique_email_isactive()
509
        if self.is_active and self.activation_sent:
510
            # reset the activation sent
511
            self.activation_sent = None
512

    
513
        super(AstakosUser, self).save(**kwargs)
514

    
515
    def renew_token(self, flush_sessions=False, current_key=None):
516
        md5 = hashlib.md5()
517
        md5.update(settings.SECRET_KEY)
518
        md5.update(self.username)
519
        md5.update(self.realname.encode('ascii', 'ignore'))
520
        md5.update(asctime())
521

    
522
        self.auth_token = b64encode(md5.digest())
523
        self.auth_token_created = datetime.now()
524
        self.auth_token_expires = self.auth_token_created + \
525
                                  timedelta(hours=AUTH_TOKEN_DURATION)
526
        if flush_sessions:
527
            self.flush_sessions(current_key)
528
        msg = 'Token renewed for %s' % self.email
529
        logger.log(LOGGING_LEVEL, msg)
530

    
531
    def flush_sessions(self, current_key=None):
532
        q = self.sessions
533
        if current_key:
534
            q = q.exclude(session_key=current_key)
535

    
536
        keys = q.values_list('session_key', flat=True)
537
        if keys:
538
            msg = 'Flushing sessions: %s' % ','.join(keys)
539
            logger.log(LOGGING_LEVEL, msg, [])
540
        engine = import_module(settings.SESSION_ENGINE)
541
        for k in keys:
542
            s = engine.SessionStore(k)
543
            s.flush()
544

    
545
    def __unicode__(self):
546
        return '%s (%s)' % (self.realname, self.email)
547

    
548
    def conflicting_email(self):
549
        q = AstakosUser.objects.exclude(username=self.username)
550
        q = q.filter(email__iexact=self.email)
551
        if q.count() != 0:
552
            return True
553
        return False
554

    
555
    def validate_unique_email_isactive(self):
556
        """
557
        Implements a unique_together constraint for email and is_active fields.
558
        """
559
        q = AstakosUser.objects.all()
560
        q = q.filter(email = self.email)
561
        q = q.filter(is_active = self.is_active)
562
        if self.id:
563
            q = q.filter(~Q(id = self.id))
564
        if q.count() != 0:
565
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
566

    
567
    @property
568
    def signed_terms(self):
569
        term = get_latest_terms()
570
        if not term:
571
            return True
572
        if not self.has_signed_terms:
573
            return False
574
        if not self.date_signed_terms:
575
            return False
576
        if self.date_signed_terms < term.date:
577
            self.has_signed_terms = False
578
            self.date_signed_terms = None
579
            self.save()
580
            return False
581
        return True
582

    
583
    def set_invitations_level(self):
584
        """
585
        Update user invitation level
586
        """
587
        level = self.invitation.inviter.level + 1
588
        self.level = level
589
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
590

    
591
    def can_login_with_auth_provider(self, provider):
592
        if not self.has_auth_provider(provider):
593
            return False
594
        else:
595
            return auth_providers.get_provider(provider).is_available_for_login()
596

    
597
    def can_add_auth_provider(self, provider, **kwargs):
598
        provider_settings = auth_providers.get_provider(provider)
599
        if not provider_settings.is_available_for_login():
600
            return False
601

    
602
        if self.has_auth_provider(provider) and \
603
           provider_settings.one_per_user:
604
            return False
605

    
606
        if 'identifier' in kwargs:
607
            try:
608
                # provider with specified params already exist
609
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
610
                                                                   **kwargs)
611
            except AstakosUser.DoesNotExist:
612
                return True
613
            else:
614
                return False
615

    
616
        return True
617

    
618
    def can_remove_auth_provider(self, provider):
619
        if len(self.get_active_auth_providers()) <= 1:
620
            return False
621
        return True
622

    
623
    def can_change_password(self):
624
        return self.has_auth_provider('local', auth_backend='astakos')
625

    
626
    def has_auth_provider(self, provider, **kwargs):
627
        return bool(self.auth_providers.filter(module=provider,
628
                                               **kwargs).count())
629

    
630
    def add_auth_provider(self, provider, **kwargs):
631
        if self.can_add_auth_provider(provider, **kwargs):
632
            self.auth_providers.create(module=provider, active=True, **kwargs)
633
        else:
634
            raise Exception('Cannot add provider')
635

    
636
    def add_pending_auth_provider(self, pending):
637
        """
638
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
639
        the current user.
640
        """
641
        if not isinstance(pending, PendingThirdPartyUser):
642
            pending = PendingThirdPartyUser.objects.get(token=pending)
643

    
644
        provider = self.add_auth_provider(pending.provider,
645
                               identifier=pending.third_party_identifier)
646

    
647
        if email_re.match(pending.email or '') and pending.email != self.email:
648
            self.additionalmail_set.get_or_create(email=pending.email)
649

    
650
        pending.delete()
651
        return provider
652

    
653
    def remove_auth_provider(self, provider, **kwargs):
654
        self.auth_providers.get(module=provider, **kwargs).delete()
655

    
656
    # user urls
657
    def get_resend_activation_url(self):
658
        return reverse('send_activation', {'user_id': self.pk})
659

    
660
    def get_activation_url(self, nxt=False):
661
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
662
                                 quote(self.auth_token))
663
        if nxt:
664
            url += "&next=%s" % quote(nxt)
665
        return url
666

    
667
    def get_password_reset_url(self, token_generator=default_token_generator):
668
        return reverse('django.contrib.auth.views.password_reset_confirm',
669
                          kwargs={'uidb36':int_to_base36(self.id),
670
                                  'token':token_generator.make_token(self)})
671

    
672
    def get_auth_providers(self):
673
        return self.auth_providers.all()
674

    
675
    def get_available_auth_providers(self):
676
        """
677
        Returns a list of providers available for user to connect to.
678
        """
679
        providers = []
680
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
681
            if self.can_add_auth_provider(module):
682
                providers.append(provider_settings(self))
683

    
684
        return providers
685

    
686
    def get_active_auth_providers(self):
687
        providers = []
688
        for provider in self.auth_providers.active():
689
            if auth_providers.get_provider(provider.module).is_available_for_login():
690
                providers.append(provider)
691
        return providers
692

    
693
    @property
694
    def auth_providers_display(self):
695
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
696

    
697

    
698
class AstakosUserAuthProviderManager(models.Manager):
699

    
700
    def active(self):
701
        return self.filter(active=True)
702

    
703

    
704
class AstakosUserAuthProvider(models.Model):
705
    """
706
    Available user authentication methods.
707
    """
708
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
709
                                   null=True, default=None)
710
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
711
    module = models.CharField(_('Provider'), max_length=255, blank=False,
712
                                default='local')
713
    identifier = models.CharField(_('Third-party identifier'),
714
                                              max_length=255, null=True,
715
                                              blank=True)
716
    active = models.BooleanField(default=True)
717
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
718
                                   default='astakos')
719

    
720
    objects = AstakosUserAuthProviderManager()
721

    
722
    class Meta:
723
        unique_together = (('identifier', 'module', 'user'), )
724

    
725
    @property
726
    def settings(self):
727
        return auth_providers.get_provider(self.module)
728

    
729
    @property
730
    def details_display(self):
731
        return self.settings.details_tpl % self.__dict__
732

    
733
    def can_remove(self):
734
        return self.user.can_remove_auth_provider(self.module)
735

    
736
    def delete(self, *args, **kwargs):
737
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
738
        if self.module == 'local':
739
            self.user.set_unusable_password()
740
            self.user.save()
741
        return ret
742

    
743
    def __repr__(self):
744
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
745

    
746
    def __unicode__(self):
747
        if self.identifier:
748
            return "%s:%s" % (self.module, self.identifier)
749
        if self.auth_backend:
750
            return "%s:%s" % (self.module, self.auth_backend)
751
        return self.module
752

    
753

    
754

    
755
class Membership(models.Model):
756
    person = models.ForeignKey(AstakosUser)
757
    group = models.ForeignKey(AstakosGroup)
758
    date_requested = models.DateField(default=datetime.now(), blank=True)
759
    date_joined = models.DateField(null=True, db_index=True, blank=True)
760

    
761
    class Meta:
762
        unique_together = ("person", "group")
763

    
764
    def save(self, *args, **kwargs):
765
        if not self.id:
766
            if not self.group.moderation_enabled:
767
                self.date_joined = datetime.now()
768
        super(Membership, self).save(*args, **kwargs)
769

    
770
    @property
771
    def is_approved(self):
772
        if self.date_joined:
773
            return True
774
        return False
775

    
776
    def approve(self):
777
        if self.is_approved:
778
            return
779
        if self.group.max_participants:
780
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
781
            'Maximum participant number has been reached.'
782
        self.date_joined = datetime.now()
783
        self.save()
784
        quota_disturbed.send(sender=self, users=(self.person,))
785

    
786
    def disapprove(self):
787
        approved = self.is_approved()
788
        self.delete()
789
        if approved:
790
            quota_disturbed.send(sender=self, users=(self.person,))
791

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

    
819
    update_or_create = _update_or_create
820

    
821
class AstakosGroupQuota(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
    group = models.ForeignKey(AstakosGroup, blank=True)
827

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

    
831
class AstakosUserQuota(models.Model):
832
    objects = ExtendedManager()
833
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
834
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
835
    resource = models.ForeignKey(Resource)
836
    user = models.ForeignKey(AstakosUser)
837

    
838
    class Meta:
839
        unique_together = ("resource", "user")
840

    
841

    
842
class ApprovalTerms(models.Model):
843
    """
844
    Model for approval terms
845
    """
846

    
847
    date = models.DateTimeField(
848
        _('Issue date'), db_index=True, default=datetime.now())
849
    location = models.CharField(_('Terms location'), max_length=255)
850

    
851

    
852
class Invitation(models.Model):
853
    """
854
    Model for registring invitations
855
    """
856
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
857
                                null=True)
858
    realname = models.CharField(_('Real name'), max_length=255)
859
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
860
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
861
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
862
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
863
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
864

    
865
    def __init__(self, *args, **kwargs):
866
        super(Invitation, self).__init__(*args, **kwargs)
867
        if not self.id:
868
            self.code = _generate_invitation_code()
869

    
870
    def consume(self):
871
        self.is_consumed = True
872
        self.consumed = datetime.now()
873
        self.save()
874

    
875
    def __unicode__(self):
876
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
877

    
878

    
879
class EmailChangeManager(models.Manager):
880
    @transaction.commit_on_success
881
    def change_email(self, activation_key):
882
        """
883
        Validate an activation key and change the corresponding
884
        ``User`` if valid.
885

886
        If the key is valid and has not expired, return the ``User``
887
        after activating.
888

889
        If the key is not valid or has expired, return ``None``.
890

891
        If the key is valid but the ``User`` is already active,
892
        return ``None``.
893

894
        After successful email change the activation record is deleted.
895

896
        Throws ValueError if there is already
897
        """
898
        try:
899
            email_change = self.model.objects.get(
900
                activation_key=activation_key)
901
            if email_change.activation_key_expired():
902
                email_change.delete()
903
                raise EmailChange.DoesNotExist
904
            # is there an active user with this address?
905
            try:
906
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
907
            except AstakosUser.DoesNotExist:
908
                pass
909
            else:
910
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
911
            # update user
912
            user = AstakosUser.objects.get(pk=email_change.user_id)
913
            user.email = email_change.new_email_address
914
            user.save()
915
            email_change.delete()
916
            return user
917
        except EmailChange.DoesNotExist:
918
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
919

    
920

    
921
class EmailChange(models.Model):
922
    new_email_address = models.EmailField(_(u'new e-mail address'),
923
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
924
    user = models.ForeignKey(
925
        AstakosUser, unique=True, related_name='emailchange_user')
926
    requested_at = models.DateTimeField(default=datetime.now())
927
    activation_key = models.CharField(
928
        max_length=40, unique=True, db_index=True)
929

    
930
    objects = EmailChangeManager()
931

    
932
    def activation_key_expired(self):
933
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
934
        return self.requested_at + expiration_date < datetime.now()
935

    
936

    
937
class AdditionalMail(models.Model):
938
    """
939
    Model for registring invitations
940
    """
941
    owner = models.ForeignKey(AstakosUser)
942
    email = models.EmailField()
943

    
944

    
945
def _generate_invitation_code():
946
    while True:
947
        code = randint(1, 2L ** 63 - 1)
948
        try:
949
            Invitation.objects.get(code=code)
950
            # An invitation with this code already exists, try again
951
        except Invitation.DoesNotExist:
952
            return code
953

    
954

    
955
def get_latest_terms():
956
    try:
957
        term = ApprovalTerms.objects.order_by('-id')[0]
958
        return term
959
    except IndexError:
960
        pass
961
    return None
962

    
963
class PendingThirdPartyUser(models.Model):
964
    """
965
    Model for registring successful third party user authentications
966
    """
967
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
968
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
969
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
970
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
971
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
972
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
973
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
974
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
975
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
976

    
977
    class Meta:
978
        unique_together = ("provider", "third_party_identifier")
979

    
980
    @property
981
    def realname(self):
982
        return '%s %s' %(self.first_name, self.last_name)
983

    
984
    @realname.setter
985
    def realname(self, value):
986
        parts = value.split(' ')
987
        if len(parts) == 2:
988
            self.first_name = parts[0]
989
            self.last_name = parts[1]
990
        else:
991
            self.last_name = parts[0]
992

    
993
    def save(self, **kwargs):
994
        if not self.id:
995
            # set username
996
            while not self.username:
997
                username =  uuid.uuid4().hex[:30]
998
                try:
999
                    AstakosUser.objects.get(username = username)
1000
                except AstakosUser.DoesNotExist, e:
1001
                    self.username = username
1002
        super(PendingThirdPartyUser, self).save(**kwargs)
1003

    
1004
    def generate_token(self):
1005
        self.password = self.third_party_identifier
1006
        self.last_login = datetime.now()
1007
        self.token = default_token_generator.make_token(self)
1008

    
1009
class SessionCatalog(models.Model):
1010
    session_key = models.CharField(_('session key'), max_length=40)
1011
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1012

    
1013
class MemberAcceptPolicy(models.Model):
1014
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1015
    description = models.CharField(_('Description'), max_length=80)
1016

    
1017
    def __str__(self):
1018
        return self.policy
1019

    
1020
class MemberRejectPolicy(models.Model):
1021
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1022
    description = models.CharField(_('Description'), max_length=80)
1023

    
1024
    def __str__(self):
1025
        return self.policy
1026

    
1027
_auto_accept = False
1028
def get_auto_accept():
1029
    global _auto_accept
1030
    if _auto_accept is not False:
1031
        return _auto_accept
1032
    try:
1033
        auto_accept = MemberAcceptPolicy.objects.get(policy='auto_accept')
1034
    except:
1035
        auto_accept = None
1036
    _auto_accept = auto_accept
1037
    return auto_accept
1038

    
1039
class ProjectDefinition(models.Model):
1040
    name = models.CharField(max_length=80)
1041
    homepage = models.URLField(max_length=255, null=True, blank=True)
1042
    description = models.TextField(null=True)
1043
    start_date = models.DateTimeField()
1044
    end_date = models.DateTimeField()
1045
    member_accept_policy = models.ForeignKey(MemberAcceptPolicy)
1046
    limit_on_members_number = models.PositiveIntegerField(null=True,blank=True)
1047
    resource_grants = models.ManyToManyField(
1048
        Resource,
1049
        null=True,
1050
        blank=True,
1051
        through='ProjectResourceGrant'
1052
    )
1053
    
1054
    def save(self):
1055
        self.validate_name()
1056
        super(ProjectDefinition, self).save()
1057
        
1058
    @property
1059
    def violated_resource_grants(self):
1060
        return False
1061
    
1062
    def add_resource_policy(self, service, resource, uplimit, update=True):
1063
        """Raises ObjectDoesNotExist, IntegrityError"""
1064
        resource = Resource.objects.get(service__name=service, name=resource)
1065
        if update:
1066
            ProjectResourceGrant.objects.update_or_create(
1067
                project_definition=self,
1068
                resource=resource,
1069
                defaults={'member_limit': uplimit}
1070
            )
1071
        else:
1072
            q = self.projectresourcegrant_set
1073
            q.create(resource=resource, member_limit=uplimit)
1074

    
1075
    @property
1076
    def resource_policies(self):
1077
        return self.projectresourcegrant_set.all()
1078

    
1079
    @resource_policies.setter
1080
    def resource_policies(self, policies):
1081
        for p in policies:
1082
            service = p.get('service', None)
1083
            resource = p.get('resource', None)
1084
            uplimit = p.get('uplimit', 0)
1085
            update = p.get('update', True)
1086
            self.add_resource_policy(service, resource, uplimit, update)
1087

    
1088
class ProjectResourceGrant(models.Model):
1089
    objects = ExtendedManager()
1090
    member_limit = models.BigIntegerField(null=True)
1091
    project_limit = models.BigIntegerField(null=True)
1092
    resource = models.ForeignKey(Resource)
1093
    project_definition = models.ForeignKey(ProjectDefinition, blank=True)
1094

    
1095
    class Meta:
1096
        unique_together = ("resource", "project_definition")
1097

    
1098
class ProjectApplication(models.Model):
1099
    serial = models.CharField(
1100
        primary_key=True,
1101
        max_length=30,
1102
        unique=True,
1103
        default=uuid.uuid4().hex[:30]
1104
    )
1105
    applicant = models.ForeignKey(
1106
        AstakosUser,
1107
        related_name='my_project_applications',
1108
        db_index=True)
1109
    owner = models.ForeignKey(
1110
        AstakosUser,
1111
        related_name='own_project_applications',
1112
        db_index=True
1113
    )
1114
    comments = models.TextField(null=True, blank=True)
1115
    definition = models.OneToOneField(ProjectDefinition)
1116
    issue_date = models.DateTimeField()
1117
    precursor_application = models.OneToOneField('ProjectApplication',
1118
        null=True,
1119
        blank=True
1120
    )
1121

    
1122
class Project(models.Model):
1123
    serial = models.CharField(
1124
        _('username'),
1125
        primary_key=True,
1126
        max_length=30,
1127
        unique=True,
1128
        default=uuid.uuid4().hex[:30]
1129
    )
1130
    application = models.OneToOneField(ProjectApplication, related_name='project')
1131
    creation_date = models.DateTimeField()
1132
    last_approval_date = models.DateTimeField(null=True)
1133
    termination_start_date = models.DateTimeField(null=True)
1134
    termination_date = models.DateTimeField(null=True)
1135
    members = models.ManyToManyField(AstakosUser, through='ProjectMembership')
1136
    membership_dirty = models.BooleanField(default=False)
1137
    last_application_synced = models.OneToOneField(
1138
        ProjectApplication, related_name='last_project', null=True, blank=True
1139
    )
1140
    
1141
    @property
1142
    def definition(self):
1143
        return self.application.definition
1144

    
1145
    @property
1146
    def violated_members_number_limit(self):
1147
        return len(self.approved_members) <= self.definition.limit_on_members_number
1148

    
1149
    @property
1150
    def is_valid(self):
1151
        try:
1152
            self.application.definition.validate_name()
1153
        except ValidationError:
1154
            return False
1155
        else:
1156
            return True
1157
        
1158
    @property
1159
    def is_active(self):
1160
        if not self.is_valid:
1161
            return False
1162
        if not self.last_approval_date:
1163
            return False
1164
        if self.termination_date:
1165
            return False
1166
        if self.definition.violated_resource_grants:
1167
            return False
1168
#         if self.violated_members_number_limit:
1169
#             return False
1170
        return True
1171
    
1172
    @property
1173
    def is_terminated(self):
1174
        if not self.is_valid:
1175
            return False
1176
        if not self.termination_date:
1177
            return False
1178
        return True
1179
    
1180
    @property
1181
    def is_suspended(self):
1182
        if not self.is_valid:
1183
            return False
1184
        if not self.termination_date:
1185
            return False
1186
        if not self.last_approval_date:
1187
            if not self.definition.violated_resource_grants:
1188
                return False
1189
#             if not self.violated_members_number_limit:
1190
#                 return False
1191
        return True
1192
    
1193
    @property
1194
    def is_alive(self):
1195
        return self.is_active or self.is_suspended
1196
    
1197
    @property
1198
    def is_inconsistent(self):
1199
        now = datetime.now()
1200
        if self.creation_date > now:
1201
            return True
1202
        if self.last_approval_date > now:
1203
            return True
1204
        if self.terminaton_date > now:
1205
            return True
1206
        return False
1207
    
1208
    @property
1209
    def is_synchronized(self):
1210
        return self.last_application_synced == self.application and \
1211
            not self.membership_dirty and \
1212
            (not self.termination_start_date or termination_date)
1213
    
1214
    @property
1215
    def approved_members(self):
1216
        return [m.person for m in self.projectmembership_set.filter(is_accepted=True)]
1217
        
1218
    def sync(self, specific_members=()):
1219
        if self.is_synchornized():
1220
            return
1221
        members = specific_members or self.approved_members
1222
        c, rejected = send_quota(members)
1223
        return rejected
1224
    
1225
    def add_member(self, user, request_user=None):
1226
        if isinstance(user, int):
1227
            user = _lookup_object(AstakosUser, id=user)
1228
        if request_user and \
1229
            (not self.owner == request_user and not request_user.is_superuser):
1230
            raise Exception(_(astakos_messages.NOT_ALLOWED))
1231
        if not self.is_alive:
1232
            raise Exception(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1233
        if self.definition.member_accept_policy == 'closed':
1234
            raise Exception(_(astakos_messages.MEMBER_ACCEPT_POLICY_CLOSED))
1235
        if len(self.approved_members) + 1 > self.limit_on_members_number:
1236
            raise Exception(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
1237
        created, m = ProjectMembership.objects.get_or_create(
1238
            person=user, project=project
1239
        )
1240
        if m.is_accepted:
1241
            return
1242
        if created:
1243
            m.issue_date = datetime.now()
1244
        
1245
        m.is_accepted = True
1246
        m.decision_date = datetime.now()
1247
        m.save()
1248
        
1249
        # set membership_dirty flag
1250
        self.membership_dirty = True
1251
        self.save()
1252
        
1253
        rejected = self.sync([user])
1254
        if not rejected:
1255
            # if syncing was successful unset membership_dirty flag
1256
            self.membership_dirty = False
1257
            self.save()
1258
        
1259
        notification = build_notification(
1260
            settings.SERVER_EMAIL,
1261
            [user.email],
1262
            _('Your membership on project %(name)s has been accepted.') % project.definition.__dict__,
1263
            _('Your membership on project %(name)s has been accepted.') % project.definition.__dict__
1264
        )
1265
    
1266
    def remove_member(self, user, request_user=None):
1267
        if user.is_digit():
1268
            user = _lookup_object(AstakosUser, id=user)
1269
        if request_user and \
1270
            (not self.owner == request_user and not request_user.is_superuser):
1271
            raise Exception(_(astakos_messages.NOT_ALLOWED))
1272
        if not self.is_alive:
1273
            raise Exception(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1274
        m = _lookup_object(ProjectMembership, person=user, project=project)
1275
        if not m.is_accepted:
1276
            return
1277
        m.is_accepted = False
1278
        m.decision_date = datetime.now()
1279
        m.save()
1280
        
1281
        # set membership_dirty flag
1282
        self.membership_dirty = True
1283
        self.save()
1284
        
1285
        rejected = self.sync([user])
1286
        if not rejected:
1287
            # if syncing was successful unset membership_dirty flag
1288
            self.membership_dirty = False
1289
            self.save()
1290
            
1291
        notification = build_notification(
1292
            settings.SERVER_EMAIL,
1293
            [user.email],
1294
            _('Your membership on project %(name)s has been removed.') % project.definition.__dict__,
1295
            _('Your membership on project %(name)s has been removed.') % project.definition.__dict__
1296
        )
1297
        notification.send()
1298
            
1299

    
1300
    def validate_name(self):
1301
        """
1302
        Validate name uniqueness among all active projects.
1303
        """
1304
        alive_projects = list(get_alive_projects())
1305
        q = filter(
1306
            lambda p: p.definition.name == self.definition.name and \
1307
                p.application.serial != self.application.serial,
1308
            alive_projects
1309
        )
1310
        if q:
1311
            raise ValidationError(
1312
                {'name': [_(astakos_messages.UNIQUE_PROJECT_NAME_CONSTRAIN_ERR)]}
1313
            )
1314
    
1315
    @classmethod
1316
    def submit(definition, applicant, comments, precursor_application=None, commit=True):
1317
        if precursor_application and precursor_application.project.is_valid:
1318
            application = precursor_application.copy()
1319
            application.precursor_application = precursor_application
1320
        else:
1321
            application = ProjectApplication(owner=applicant)
1322
        application.definition = definition
1323
        application.applicant = applicant
1324
        application.comments = comments
1325
        application.issue_date = datetime.now()
1326
        if commit:
1327
            definition.save()
1328
            application.save()
1329
        if applicant.is_superuser():
1330
            self.approve_application()
1331
        notification = build_notification(
1332
            settings.SERVER_EMAIL,
1333
            [i[1] for i in settings.ADMINS],
1334
            _(GROUP_CREATION_SUBJECT) % {'group':application.definition.name},
1335
            _('An new project application identified by %(serial)s has been submitted.') % application.__dict__
1336
        )
1337
        notification.send()
1338
        return application
1339
    
1340
    def approve(self, approval_user=None):
1341
        """
1342
        If approval_user then during owner membership acceptance
1343
        it is checked whether the request_user is eligible.
1344
        """
1345
        if not self.precursor_application:
1346
            kwargs = {
1347
                'application':self,
1348
                'creation_date':datetime.now(),
1349
                'last_approval_date':datetime.now(),
1350
            }
1351
            project = _create_object(Project, **kwargs)
1352
            project.add_member(self.owner, approval_user)
1353
        else:
1354
            project = self.precursor_application.project
1355
            last_approval_date = project.last_approval_date
1356
            if project.is_valid:
1357
                project.application = app
1358
                project.last_approval_date = datetime.now()
1359
                project.save()
1360
            else:
1361
                raise Exception(_(astakos_messages.INVALID_PROJECT) % project.__dict__)
1362
        
1363
        rejected = self.sync()
1364
        if rejected:
1365
            # revert to precursor
1366
            project.appication = app.precursor_application
1367
            if project.application:
1368
                project.last_approval_date = last_approval_date
1369
                project.save()
1370
            rejected = synchonize_project(project.serial)
1371
            if rejected:
1372
                raise Exception(_(astakos_messages.QH_SYNC_ERROR))
1373
        else:
1374
            project.last_application_synced = app
1375
            project.save()
1376
            sender, recipients, subject, message
1377
            notification = build_notification(
1378
                settings.SERVER_EMAIL,
1379
                [project.owner.email],
1380
                _('Project application has been approved on %s alpha2 testing' % SITENAME),
1381
                _('Your application request %(serial)s has been apporved.')
1382
            )
1383
            notification.send()
1384
    
1385
    def terminate(self):
1386
        self.termination_start_date = datetime.now()
1387
        self.terminaton_date = None
1388
        self.save()
1389
        
1390
        rejected = self.sync()
1391
        if not rejected:
1392
            self.termination_start_date = None
1393
            self.terminaton_date = datetime.now()
1394
            self.save()
1395
            
1396
            notification = build_notification(
1397
                settings.SERVER_EMAIL,
1398
                [self.application.owner.email],
1399
                _('Project %(name)s has been terminated.') %  self.definition.__dict__,
1400
                _('Project %(name)s has been terminated.') %  self.definition.__dict__
1401
            )
1402
            notification.send()
1403

    
1404
    def suspend(self):
1405
        self.last_approval_date = None
1406
        self.save()
1407
        notification = build_notification(
1408
            settings.SERVER_EMAIL,
1409
            [self.application.owner.email],
1410
            _('Project %(name)s has been suspended.') %  self.definition.__dict__,
1411
            _('Project %(name)s has been suspended.') %  self.definition.__dict__
1412
        )
1413
        notification.send()
1414

    
1415

    
1416

    
1417
class ProjectMembership(models.Model):
1418
    person = models.ForeignKey(AstakosUser)
1419
    project = models.ForeignKey(Project)
1420
    issue_date = models.DateField(default=datetime.now())
1421
    decision_date = models.DateField(null=True, db_index=True)
1422
    is_accepted = models.BooleanField(default=False)
1423

    
1424
    class Meta:
1425
        unique_together = ("person", "project")
1426

    
1427
def filter_queryset_by_property(q, property):
1428
    """
1429
    Incorporate list comprehension for filtering querysets by property
1430
    since Queryset.filter() operates on the database level.
1431
    """
1432
    return (p for p in q if getattr(p, property, False))
1433

    
1434
def get_alive_projects():
1435
    return filter_queryset_by_property(
1436
        Project.objects.all(),
1437
        'is_alive'
1438
    )
1439

    
1440
def get_active_projects():
1441
    return filter_queryset_by_property(
1442
        Project.objects.all(),
1443
        'is_active'
1444
    )
1445

    
1446
def _lookup_object(model, **kwargs):
1447
    """
1448
    Returns an object of the specific model matching the given lookup
1449
    parameters.
1450
    """
1451
    if not kwargs:
1452
        raise MissingIdentifier
1453
    try:
1454
        return model.objects.get(**kwargs)
1455
    except model.DoesNotExist:
1456
        raise ItemNotExists(model._meta.verbose_name, **kwargs)
1457
    except model.MultipleObjectsReturned:
1458
        raise MultipleItemsExist(model._meta.verbose_name, **kwargs)
1459

    
1460
def _create_object(model, **kwargs):
1461
    o = model.objects.create(**kwargs)
1462
    o.save()
1463
    return o
1464

    
1465
def _update_object(model, id, save=True, **kwargs):
1466
    o = self._lookup_object(model, id=id)
1467
    if kwargs:
1468
        o.__dict__.update(kwargs)
1469
    if save:
1470
        o.save()
1471
    return o
1472

    
1473
def list_applications():
1474
    return ProjectApplication.objects.all()
1475

    
1476

    
1477
def list_projects(filter_property=None):
1478
    if filter_property:
1479
        return filter_queryset_by_property(
1480
            Project.objects.all(),
1481
            filter_property
1482
        )
1483
    return Project.objects.all()
1484

    
1485

    
1486
def synchonize_project(serial):
1487
    project = _lookup_object(Project, serial=serial)
1488
    return project.sync()
1489

    
1490

    
1491
def create_astakos_user(u):
1492
    try:
1493
        AstakosUser.objects.get(user_ptr=u.pk)
1494
    except AstakosUser.DoesNotExist:
1495
        extended_user = AstakosUser(user_ptr_id=u.pk)
1496
        extended_user.__dict__.update(u.__dict__)
1497
        extended_user.save()
1498
        if not extended_user.has_auth_provider('local'):
1499
            extended_user.add_auth_provider('local')
1500
    except BaseException, e:
1501
        logger.exception(e)
1502

    
1503

    
1504
def fix_superusers(sender, **kwargs):
1505
    # Associate superusers with AstakosUser
1506
    admins = User.objects.filter(is_superuser=True)
1507
    for u in admins:
1508
        create_astakos_user(u)
1509

    
1510

    
1511
def user_post_save(sender, instance, created, **kwargs):
1512
    if not created:
1513
        return
1514
    create_astakos_user(instance)
1515

    
1516

    
1517
def set_default_group(user):
1518
    try:
1519
        default = AstakosGroup.objects.get(name='default')
1520
        Membership(
1521
            group=default, person=user, date_joined=datetime.now()).save()
1522
    except AstakosGroup.DoesNotExist, e:
1523
        logger.exception(e)
1524

    
1525

    
1526
def astakosuser_pre_save(sender, instance, **kwargs):
1527
    instance.aquarium_report = False
1528
    instance.new = False
1529
    try:
1530
        db_instance = AstakosUser.objects.get(id=instance.id)
1531
    except AstakosUser.DoesNotExist:
1532
        # create event
1533
        instance.aquarium_report = True
1534
        instance.new = True
1535
    else:
1536
        get = AstakosUser.__getattribute__
1537
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1538
                   BILLING_FIELDS)
1539
        instance.aquarium_report = True if l else False
1540

    
1541

    
1542
def astakosuser_post_save(sender, instance, created, **kwargs):
1543
    if instance.aquarium_report:
1544
        report_user_event(instance, create=instance.new)
1545
    if not created:
1546
        return
1547
    set_default_group(instance)
1548
    # TODO handle socket.error & IOError
1549
    register_users((instance,))
1550

    
1551

    
1552
def resource_post_save(sender, instance, created, **kwargs):
1553
    if not created:
1554
        return
1555
    register_resources((instance,))
1556

    
1557

    
1558
def send_quota_disturbed(sender, instance, **kwargs):
1559
    users = []
1560
    extend = users.extend
1561
    if sender == Membership:
1562
        if not instance.group.is_enabled:
1563
            return
1564
        extend([instance.person])
1565
    elif sender == AstakosUserQuota:
1566
        extend([instance.user])
1567
    elif sender == AstakosGroupQuota:
1568
        if not instance.group.is_enabled:
1569
            return
1570
        extend(instance.group.astakosuser_set.all())
1571
    elif sender == AstakosGroup:
1572
        if not instance.is_enabled:
1573
            return
1574
    quota_disturbed.send(sender=sender, users=users)
1575

    
1576

    
1577
def on_quota_disturbed(sender, users, **kwargs):
1578
#     print '>>>', locals()
1579
    if not users:
1580
        return
1581
    send_quota(users)
1582

    
1583
def renew_token(sender, instance, **kwargs):
1584
    if not instance.auth_token:
1585
        instance.renew_token()
1586

    
1587
post_syncdb.connect(fix_superusers)
1588
post_save.connect(user_post_save, sender=User)
1589
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1590
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1591
post_save.connect(resource_post_save, sender=Resource)
1592

    
1593
quota_disturbed = Signal(providing_args=["users"])
1594
quota_disturbed.connect(on_quota_disturbed)
1595

    
1596
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1597
post_delete.connect(send_quota_disturbed, sender=Membership)
1598
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1599
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1600
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1601
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1602

    
1603
pre_save.connect(renew_token, sender=AstakosUser)
1604
pre_save.connect(renew_token, sender=Service)