Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 2553efae

History | View | Annotate | Download (59 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
from django.core.exceptions import PermissionDenied
65
from django.views.generic.create_update import lookup_object
66
from django.core.exceptions import ObjectDoesNotExist
67

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

    
82
from astakos.im.notifications import build_notification
83

    
84
import astakos.im.messages as astakos_messages
85

    
86
logger = logging.getLogger(__name__)
87

    
88
DEFAULT_CONTENT_TYPE = None
89
_content_type = None
90

    
91
PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
92

    
93
def get_content_type():
94
    global _content_type
95
    if _content_type is not None:
96
        return _content_type
97

    
98
    try:
99
        content_type = ContentType.objects.get(app_label='im', model='astakosuser')
100
    except:
101
        content_type = DEFAULT_CONTENT_TYPE
102
    _content_type = content_type
103
    return content_type
104

    
105
RESOURCE_SEPARATOR = '.'
106

    
107
inf = float('inf')
108

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

    
119
    def renew_token(self):
120
        md5 = hashlib.md5()
121
        md5.update(self.name.encode('ascii', 'ignore'))
122
        md5.update(self.url.encode('ascii', 'ignore'))
123
        md5.update(asctime())
124

    
125
        self.auth_token = b64encode(md5.digest())
126
        self.auth_token_created = datetime.now()
127
        self.auth_token_expires = self.auth_token_created + \
128
            timedelta(hours=AUTH_TOKEN_DURATION)
129

    
130
    def __str__(self):
131
        return self.name
132

    
133
    @property
134
    def resources(self):
135
        return self.resource_set.all()
136

    
137
    @resources.setter
138
    def resources(self, resources):
139
        for s in resources:
140
            self.resource_set.create(**s)
141

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

    
153

    
154
class ResourceMetadata(models.Model):
155
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
156
    value = models.CharField(_('Value'), max_length=255)
157

    
158

    
159
class Resource(models.Model):
160
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
161
    meta = models.ManyToManyField(ResourceMetadata)
162
    service = models.ForeignKey(Service)
163
    desc = models.TextField(_('Description'), null=True)
164
    unit = models.CharField(_('Name'), null=True, max_length=255)
165
    group = models.CharField(_('Group'), null=True, max_length=255)
166

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

    
170

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

    
174
    def __str__(self):
175
        return self.name
176

    
177

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

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

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

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

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

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

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

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

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

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

    
294
    @property
295
    def policies(self):
296
        return self.astakosgroupquota_set.select_related().all()
297

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

    
307
    @property
308
    def owners(self):
309
        return self.owner.all()
310

    
311
    @property
312
    def owner_details(self):
313
        return self.owner.select_related().all()
314

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

    
320

    
321

    
322
class AstakosUserManager(UserManager):
323

    
324
    def get_auth_provider_user(self, provider, **kwargs):
325
        """
326
        Retrieve AstakosUser instance associated with the specified third party
327
        id.
328
        """
329
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
330
                          kwargs.iteritems()))
331
        return self.get(auth_providers__module=provider, **kwargs)
332

    
333
class AstakosUser(User):
334
    """
335
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
336
    """
337
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
338
                                   null=True)
339

    
340
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
341
    #                    AstakosUserProvider model.
342
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
343
                                null=True)
344
    # ex. screen_name for twitter, eppn for shibboleth
345
    third_party_identifier = models.CharField(_('Third-party identifier'),
346
                                              max_length=255, null=True,
347
                                              blank=True)
348

    
349

    
350
    #for invitations
351
    user_level = DEFAULT_USER_LEVEL
352
    level = models.IntegerField(_('Inviter level'), default=user_level)
353
    invitations = models.IntegerField(
354
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
355

    
356
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
357
                                  null=True, blank=True)
358
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
359
    auth_token_expires = models.DateTimeField(
360
        _('Token expiration date'), null=True)
361

    
362
    updated = models.DateTimeField(_('Update date'))
363
    is_verified = models.BooleanField(_('Is verified?'), default=False)
364

    
365
    email_verified = models.BooleanField(_('Email verified?'), default=False)
366

    
367
    has_credits = models.BooleanField(_('Has credits?'), default=False)
368
    has_signed_terms = models.BooleanField(
369
        _('I agree with the terms'), default=False)
370
    date_signed_terms = models.DateTimeField(
371
        _('Signed terms date'), null=True, blank=True)
372

    
373
    activation_sent = models.DateTimeField(
374
        _('Activation sent data'), null=True, blank=True)
375

    
376
    policy = models.ManyToManyField(
377
        Resource, null=True, through='AstakosUserQuota')
378

    
379
    astakos_groups = models.ManyToManyField(
380
        AstakosGroup, verbose_name=_('agroups'), blank=True,
381
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
382
        through='Membership')
383

    
384
    __has_signed_terms = False
385
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
386
                                           default=False, db_index=True)
387

    
388
    objects = AstakosUserManager()
389

    
390
    owner = models.ManyToManyField(
391
        AstakosGroup, related_name='owner', null=True)
392

    
393
    class Meta:
394
        unique_together = ("provider", "third_party_identifier")
395

    
396
    def __init__(self, *args, **kwargs):
397
        super(AstakosUser, self).__init__(*args, **kwargs)
398
        self.__has_signed_terms = self.has_signed_terms
399
        if not self.id:
400
            self.is_active = False
401

    
402
    @property
403
    def realname(self):
404
        return '%s %s' % (self.first_name, self.last_name)
405

    
406
    @realname.setter
407
    def realname(self, value):
408
        parts = value.split(' ')
409
        if len(parts) == 2:
410
            self.first_name = parts[0]
411
            self.last_name = parts[1]
412
        else:
413
            self.last_name = parts[0]
414

    
415
    def add_permission(self, pname):
416
        if self.has_perm(pname):
417
            return
418
        p, created = Permission.objects.get_or_create(
419
                                    codename=pname,
420
                                    name=pname.capitalize(),
421
                                    content_type=get_content_type())
422
        self.user_permissions.add(p)
423

    
424
    def remove_permission(self, pname):
425
        if self.has_perm(pname):
426
            return
427
        p = Permission.objects.get(codename=pname,
428
                                   content_type=get_content_type())
429
        self.user_permissions.remove(p)
430

    
431
    @property
432
    def invitation(self):
433
        try:
434
            return Invitation.objects.get(username=self.email)
435
        except Invitation.DoesNotExist:
436
            return None
437

    
438
    def invite(self, email, realname):
439
        inv = Invitation(inviter=self, username=email, realname=realname)
440
        inv.save()
441
        send_invitation(inv)
442
        self.invitations = max(0, self.invitations - 1)
443
        self.save()
444

    
445
    @property
446
    def quota(self):
447
        """Returns a dict with the sum of quota limits per resource"""
448
        d = defaultdict(int)
449
        for q in self.policies:
450
            d[q.resource] += q.uplimit or inf
451
        for m in self.projectmembership_set.select_related().all():
452
            if not m.acceptance_date:
453
                continue
454
            p = m.project
455
            if not p.is_active:
456
                continue
457
            grants = p.application.definition.projectresourcegrant_set.all()
458
            for g in grants:
459
                d[g.resource] += g.member_limit or inf
460
        # TODO set default for remaining
461
        return d
462

    
463
    @property
464
    def policies(self):
465
        return self.astakosuserquota_set.select_related().all()
466

    
467
    @policies.setter
468
    def policies(self, policies):
469
        for p in policies:
470
            service = policies.get('service', None)
471
            resource = policies.get('resource', None)
472
            uplimit = policies.get('uplimit', 0)
473
            update = policies.get('update', True)
474
            self.add_policy(service, resource, uplimit, update)
475

    
476
    def add_policy(self, service, resource, uplimit, update=True):
477
        """Raises ObjectDoesNotExist, IntegrityError"""
478
        resource = Resource.objects.get(service__name=service, name=resource)
479
        if update:
480
            AstakosUserQuota.objects.update_or_create(user=self,
481
                                                      resource=resource,
482
                                                      defaults={'uplimit': uplimit})
483
        else:
484
            q = self.astakosuserquota_set
485
            q.create(resource=resource, uplimit=uplimit)
486

    
487
    def remove_policy(self, service, resource):
488
        """Raises ObjectDoesNotExist, IntegrityError"""
489
        resource = Resource.objects.get(service__name=service, name=resource)
490
        q = self.policies.get(resource=resource).delete()
491

    
492
    @property
493
    def extended_groups(self):
494
        return self.membership_set.select_related().all()
495

    
496
    @extended_groups.setter
497
    def extended_groups(self, groups):
498
        #TODO exceptions
499
        for name in (groups or ()):
500
            group = AstakosGroup.objects.get(name=name)
501
            self.membership_set.create(group=group)
502

    
503
    def save(self, update_timestamps=True, **kwargs):
504
        if update_timestamps:
505
            if not self.id:
506
                self.date_joined = datetime.now()
507
            self.updated = datetime.now()
508

    
509
        # update date_signed_terms if necessary
510
        if self.__has_signed_terms != self.has_signed_terms:
511
            self.date_signed_terms = datetime.now()
512

    
513
        if not self.id:
514
            # set username
515
            self.username = self.email
516

    
517
        self.validate_unique_email_isactive()
518
        if self.is_active and self.activation_sent:
519
            # reset the activation sent
520
            self.activation_sent = None
521

    
522
        super(AstakosUser, self).save(**kwargs)
523

    
524
    def renew_token(self, flush_sessions=False, current_key=None):
525
        md5 = hashlib.md5()
526
        md5.update(settings.SECRET_KEY)
527
        md5.update(self.username)
528
        md5.update(self.realname.encode('ascii', 'ignore'))
529
        md5.update(asctime())
530

    
531
        self.auth_token = b64encode(md5.digest())
532
        self.auth_token_created = datetime.now()
533
        self.auth_token_expires = self.auth_token_created + \
534
                                  timedelta(hours=AUTH_TOKEN_DURATION)
535
        if flush_sessions:
536
            self.flush_sessions(current_key)
537
        msg = 'Token renewed for %s' % self.email
538
        logger.log(LOGGING_LEVEL, msg)
539

    
540
    def flush_sessions(self, current_key=None):
541
        q = self.sessions
542
        if current_key:
543
            q = q.exclude(session_key=current_key)
544

    
545
        keys = q.values_list('session_key', flat=True)
546
        if keys:
547
            msg = 'Flushing sessions: %s' % ','.join(keys)
548
            logger.log(LOGGING_LEVEL, msg, [])
549
        engine = import_module(settings.SESSION_ENGINE)
550
        for k in keys:
551
            s = engine.SessionStore(k)
552
            s.flush()
553

    
554
    def __unicode__(self):
555
        return '%s (%s)' % (self.realname, self.email)
556

    
557
    def conflicting_email(self):
558
        q = AstakosUser.objects.exclude(username=self.username)
559
        q = q.filter(email__iexact=self.email)
560
        if q.count() != 0:
561
            return True
562
        return False
563

    
564
    def validate_unique_email_isactive(self):
565
        """
566
        Implements a unique_together constraint for email and is_active fields.
567
        """
568
        q = AstakosUser.objects.all()
569
        q = q.filter(email = self.email)
570
        q = q.filter(is_active = self.is_active)
571
        if self.id:
572
            q = q.filter(~Q(id = self.id))
573
        if q.count() != 0:
574
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
575

    
576
    @property
577
    def signed_terms(self):
578
        term = get_latest_terms()
579
        if not term:
580
            return True
581
        if not self.has_signed_terms:
582
            return False
583
        if not self.date_signed_terms:
584
            return False
585
        if self.date_signed_terms < term.date:
586
            self.has_signed_terms = False
587
            self.date_signed_terms = None
588
            self.save()
589
            return False
590
        return True
591

    
592
    def set_invitations_level(self):
593
        """
594
        Update user invitation level
595
        """
596
        level = self.invitation.inviter.level + 1
597
        self.level = level
598
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
599

    
600
    def can_login_with_auth_provider(self, provider):
601
        if not self.has_auth_provider(provider):
602
            return False
603
        else:
604
            return auth_providers.get_provider(provider).is_available_for_login()
605

    
606
    def can_add_auth_provider(self, provider, **kwargs):
607
        provider_settings = auth_providers.get_provider(provider)
608
        if not provider_settings.is_available_for_login():
609
            return False
610

    
611
        if self.has_auth_provider(provider) and \
612
           provider_settings.one_per_user:
613
            return False
614

    
615
        if 'identifier' in kwargs:
616
            try:
617
                # provider with specified params already exist
618
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
619
                                                                   **kwargs)
620
            except AstakosUser.DoesNotExist:
621
                return True
622
            else:
623
                return False
624

    
625
        return True
626

    
627
    def can_remove_auth_provider(self, provider):
628
        if len(self.get_active_auth_providers()) <= 1:
629
            return False
630
        return True
631

    
632
    def can_change_password(self):
633
        return self.has_auth_provider('local', auth_backend='astakos')
634

    
635
    def has_auth_provider(self, provider, **kwargs):
636
        return bool(self.auth_providers.filter(module=provider,
637
                                               **kwargs).count())
638

    
639
    def add_auth_provider(self, provider, **kwargs):
640
        if self.can_add_auth_provider(provider, **kwargs):
641
            self.auth_providers.create(module=provider, active=True, **kwargs)
642
        else:
643
            raise Exception('Cannot add provider')
644

    
645
    def add_pending_auth_provider(self, pending):
646
        """
647
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
648
        the current user.
649
        """
650
        if not isinstance(pending, PendingThirdPartyUser):
651
            pending = PendingThirdPartyUser.objects.get(token=pending)
652

    
653
        provider = self.add_auth_provider(pending.provider,
654
                               identifier=pending.third_party_identifier)
655

    
656
        if email_re.match(pending.email or '') and pending.email != self.email:
657
            self.additionalmail_set.get_or_create(email=pending.email)
658

    
659
        pending.delete()
660
        return provider
661

    
662
    def remove_auth_provider(self, provider, **kwargs):
663
        self.auth_providers.get(module=provider, **kwargs).delete()
664

    
665
    # user urls
666
    def get_resend_activation_url(self):
667
        return reverse('send_activation', {'user_id': self.pk})
668

    
669
    def get_activation_url(self, nxt=False):
670
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
671
                                 quote(self.auth_token))
672
        if nxt:
673
            url += "&next=%s" % quote(nxt)
674
        return url
675

    
676
    def get_password_reset_url(self, token_generator=default_token_generator):
677
        return reverse('django.contrib.auth.views.password_reset_confirm',
678
                          kwargs={'uidb36':int_to_base36(self.id),
679
                                  'token':token_generator.make_token(self)})
680

    
681
    def get_auth_providers(self):
682
        return self.auth_providers.all()
683

    
684
    def get_available_auth_providers(self):
685
        """
686
        Returns a list of providers available for user to connect to.
687
        """
688
        providers = []
689
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
690
            if self.can_add_auth_provider(module):
691
                providers.append(provider_settings(self))
692

    
693
        return providers
694

    
695
    def get_active_auth_providers(self):
696
        providers = []
697
        for provider in self.auth_providers.active():
698
            if auth_providers.get_provider(provider.module).is_available_for_login():
699
                providers.append(provider)
700
        return providers
701

    
702
    @property
703
    def auth_providers_display(self):
704
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
705

    
706

    
707
class AstakosUserAuthProviderManager(models.Manager):
708

    
709
    def active(self):
710
        return self.filter(active=True)
711

    
712

    
713
class AstakosUserAuthProvider(models.Model):
714
    """
715
    Available user authentication methods.
716
    """
717
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
718
                                   null=True, default=None)
719
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
720
    module = models.CharField(_('Provider'), max_length=255, blank=False,
721
                                default='local')
722
    identifier = models.CharField(_('Third-party identifier'),
723
                                              max_length=255, null=True,
724
                                              blank=True)
725
    active = models.BooleanField(default=True)
726
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
727
                                   default='astakos')
728

    
729
    objects = AstakosUserAuthProviderManager()
730

    
731
    class Meta:
732
        unique_together = (('identifier', 'module', 'user'), )
733

    
734
    @property
735
    def settings(self):
736
        return auth_providers.get_provider(self.module)
737

    
738
    @property
739
    def details_display(self):
740
        return self.settings.details_tpl % self.__dict__
741

    
742
    def can_remove(self):
743
        return self.user.can_remove_auth_provider(self.module)
744

    
745
    def delete(self, *args, **kwargs):
746
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
747
        if self.module == 'local':
748
            self.user.set_unusable_password()
749
            self.user.save()
750
        return ret
751

    
752
    def __repr__(self):
753
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
754

    
755
    def __unicode__(self):
756
        if self.identifier:
757
            return "%s:%s" % (self.module, self.identifier)
758
        if self.auth_backend:
759
            return "%s:%s" % (self.module, self.auth_backend)
760
        return self.module
761

    
762

    
763

    
764
class Membership(models.Model):
765
    person = models.ForeignKey(AstakosUser)
766
    group = models.ForeignKey(AstakosGroup)
767
    date_requested = models.DateField(default=datetime.now(), blank=True)
768
    date_joined = models.DateField(null=True, db_index=True, blank=True)
769

    
770
    class Meta:
771
        unique_together = ("person", "group")
772

    
773
    def save(self, *args, **kwargs):
774
        if not self.id:
775
            if not self.group.moderation_enabled:
776
                self.date_joined = datetime.now()
777
        super(Membership, self).save(*args, **kwargs)
778

    
779
    @property
780
    def is_approved(self):
781
        if self.date_joined:
782
            return True
783
        return False
784

    
785
    def approve(self):
786
        if self.is_approved:
787
            return
788
        if self.group.max_participants:
789
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
790
            'Maximum participant number has been reached.'
791
        self.date_joined = datetime.now()
792
        self.save()
793
        quota_disturbed.send(sender=self, users=(self.person,))
794

    
795
    def disapprove(self):
796
        approved = self.is_approved()
797
        self.delete()
798
        if approved:
799
            quota_disturbed.send(sender=self, users=(self.person,))
800

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

    
828
    update_or_create = _update_or_create
829

    
830
class AstakosGroupQuota(models.Model):
831
    objects = ExtendedManager()
832
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
833
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
834
    resource = models.ForeignKey(Resource)
835
    group = models.ForeignKey(AstakosGroup, blank=True)
836

    
837
    class Meta:
838
        unique_together = ("resource", "group")
839

    
840
class AstakosUserQuota(models.Model):
841
    objects = ExtendedManager()
842
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
843
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
844
    resource = models.ForeignKey(Resource)
845
    user = models.ForeignKey(AstakosUser)
846

    
847
    class Meta:
848
        unique_together = ("resource", "user")
849

    
850

    
851
class ApprovalTerms(models.Model):
852
    """
853
    Model for approval terms
854
    """
855

    
856
    date = models.DateTimeField(
857
        _('Issue date'), db_index=True, default=datetime.now())
858
    location = models.CharField(_('Terms location'), max_length=255)
859

    
860

    
861
class Invitation(models.Model):
862
    """
863
    Model for registring invitations
864
    """
865
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
866
                                null=True)
867
    realname = models.CharField(_('Real name'), max_length=255)
868
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
869
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
870
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
871
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
872
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
873

    
874
    def __init__(self, *args, **kwargs):
875
        super(Invitation, self).__init__(*args, **kwargs)
876
        if not self.id:
877
            self.code = _generate_invitation_code()
878

    
879
    def consume(self):
880
        self.is_consumed = True
881
        self.consumed = datetime.now()
882
        self.save()
883

    
884
    def __unicode__(self):
885
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
886

    
887

    
888
class EmailChangeManager(models.Manager):
889
    @transaction.commit_on_success
890
    def change_email(self, activation_key):
891
        """
892
        Validate an activation key and change the corresponding
893
        ``User`` if valid.
894

895
        If the key is valid and has not expired, return the ``User``
896
        after activating.
897

898
        If the key is not valid or has expired, return ``None``.
899

900
        If the key is valid but the ``User`` is already active,
901
        return ``None``.
902

903
        After successful email change the activation record is deleted.
904

905
        Throws ValueError if there is already
906
        """
907
        try:
908
            email_change = self.model.objects.get(
909
                activation_key=activation_key)
910
            if email_change.activation_key_expired():
911
                email_change.delete()
912
                raise EmailChange.DoesNotExist
913
            # is there an active user with this address?
914
            try:
915
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
916
            except AstakosUser.DoesNotExist:
917
                pass
918
            else:
919
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
920
            # update user
921
            user = AstakosUser.objects.get(pk=email_change.user_id)
922
            user.email = email_change.new_email_address
923
            user.save()
924
            email_change.delete()
925
            return user
926
        except EmailChange.DoesNotExist:
927
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
928

    
929

    
930
class EmailChange(models.Model):
931
    new_email_address = models.EmailField(_(u'new e-mail address'),
932
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
933
    user = models.ForeignKey(
934
        AstakosUser, unique=True, related_name='emailchange_user')
935
    requested_at = models.DateTimeField(default=datetime.now())
936
    activation_key = models.CharField(
937
        max_length=40, unique=True, db_index=True)
938

    
939
    objects = EmailChangeManager()
940

    
941
    def activation_key_expired(self):
942
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
943
        return self.requested_at + expiration_date < datetime.now()
944

    
945

    
946
class AdditionalMail(models.Model):
947
    """
948
    Model for registring invitations
949
    """
950
    owner = models.ForeignKey(AstakosUser)
951
    email = models.EmailField()
952

    
953

    
954
def _generate_invitation_code():
955
    while True:
956
        code = randint(1, 2L ** 63 - 1)
957
        try:
958
            Invitation.objects.get(code=code)
959
            # An invitation with this code already exists, try again
960
        except Invitation.DoesNotExist:
961
            return code
962

    
963

    
964
def get_latest_terms():
965
    try:
966
        term = ApprovalTerms.objects.order_by('-id')[0]
967
        return term
968
    except IndexError:
969
        pass
970
    return None
971

    
972
class PendingThirdPartyUser(models.Model):
973
    """
974
    Model for registring successful third party user authentications
975
    """
976
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
977
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
978
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
979
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
980
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
981
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
982
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
983
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
984
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
985

    
986
    class Meta:
987
        unique_together = ("provider", "third_party_identifier")
988

    
989
    @property
990
    def realname(self):
991
        return '%s %s' %(self.first_name, self.last_name)
992

    
993
    @realname.setter
994
    def realname(self, value):
995
        parts = value.split(' ')
996
        if len(parts) == 2:
997
            self.first_name = parts[0]
998
            self.last_name = parts[1]
999
        else:
1000
            self.last_name = parts[0]
1001

    
1002
    def save(self, **kwargs):
1003
        if not self.id:
1004
            # set username
1005
            while not self.username:
1006
                username =  uuid.uuid4().hex[:30]
1007
                try:
1008
                    AstakosUser.objects.get(username = username)
1009
                except AstakosUser.DoesNotExist, e:
1010
                    self.username = username
1011
        super(PendingThirdPartyUser, self).save(**kwargs)
1012

    
1013
    def generate_token(self):
1014
        self.password = self.third_party_identifier
1015
        self.last_login = datetime.now()
1016
        self.token = default_token_generator.make_token(self)
1017

    
1018
class SessionCatalog(models.Model):
1019
    session_key = models.CharField(_('session key'), max_length=40)
1020
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1021

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

    
1026
    def __str__(self):
1027
        return self.policy
1028

    
1029
class MemberLeavePolicy(models.Model):
1030
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1031
    description = models.CharField(_('Description'), max_length=80)
1032

    
1033
    def __str__(self):
1034
        return self.policy
1035

    
1036
_auto_accept_join = False
1037
def get_auto_accept_join():
1038
    global _auto_accept_join
1039
    if _auto_accept_join is not False:
1040
        return _auto_accept_join
1041
    try:
1042
        auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
1043
    except:
1044
        auto_accept = None
1045
    _auto_accept_join = auto_accept
1046
    return auto_accept
1047

    
1048
_closed_join = False
1049
def get_closed_join():
1050
    global _closed_join
1051
    if _closed_join is not False:
1052
        return _closed_join
1053
    try:
1054
        closed = MemberJoinPolicy.objects.get(policy='closed')
1055
    except:
1056
        closed = None
1057
    _closed_join = closed
1058
    return closed
1059

    
1060
_auto_accept_leave = False
1061
def get_auto_accept_leave():
1062
    global _auto_accept_leave
1063
    if _auto_accept_leave is not False:
1064
        return _auto_accept_leave
1065
    try:
1066
        auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
1067
    except:
1068
        auto_accept = None
1069
    _auto_accept_leave = auto_accept
1070
    return auto_accept
1071

    
1072
_closed_leave = False
1073
def get_closed_leave():
1074
    global _closed_leave
1075
    if _closed_leave is not False:
1076
        return _closed_leave
1077
    try:
1078
        closed = MemberLeavePolicy.objects.get(policy='closed')
1079
    except:
1080
        closed = None
1081
    _closed_leave = closed
1082
    return closeds
1083

    
1084
class ProjectDefinition(models.Model):
1085
    name = models.CharField(max_length=80)
1086
    homepage = models.URLField(max_length=255, null=True, blank=True)
1087
    description = models.TextField(null=True)
1088
    start_date = models.DateTimeField()
1089
    end_date = models.DateTimeField()
1090
    member_join_policy = models.ForeignKey(MemberJoinPolicy)
1091
    member_leave_policy = models.ForeignKey(MemberLeavePolicy)
1092
    limit_on_members_number = models.PositiveIntegerField(null=True,blank=True)
1093
    resource_grants = models.ManyToManyField(
1094
        Resource,
1095
        null=True,
1096
        blank=True,
1097
        through='ProjectResourceGrant'
1098
    )
1099
    
1100
    @property
1101
    def violated_resource_grants(self):
1102
        return False
1103
    
1104
    def add_resource_policy(self, service, resource, uplimit, update=True):
1105
        """Raises ObjectDoesNotExist, IntegrityError"""
1106
        resource = Resource.objects.get(service__name=service, name=resource)
1107
        if update:
1108
            ProjectResourceGrant.objects.update_or_create(
1109
                project_definition=self,
1110
                resource=resource,
1111
                defaults={'member_limit': uplimit}
1112
            )
1113
        else:
1114
            q = self.projectresourcegrant_set
1115
            q.create(resource=resource, member_limit=uplimit)
1116

    
1117
    @property
1118
    def resource_policies(self):
1119
        return self.projectresourcegrant_set.all()
1120

    
1121
    @resource_policies.setter
1122
    def resource_policies(self, policies):
1123
        for p in policies:
1124
            service = p.get('service', None)
1125
            resource = p.get('resource', None)
1126
            uplimit = p.get('uplimit', 0)
1127
            update = p.get('update', True)
1128
            self.add_resource_policy(service, resource, uplimit, update)
1129
    
1130
    def validate_name(self):
1131
        """
1132
        Validate name uniqueness among all active projects.
1133
        """
1134
        alive_projects = list(get_alive_projects())
1135
        q = filter(
1136
            lambda p: p.definition.name == self.name and \
1137
                p.application.id != self.projectapplication.id,
1138
            alive_projects
1139
        )
1140
        if q:
1141
            raise ValidationError(
1142
                {'name': [_(astakos_messages.UNIQUE_PROJECT_NAME_CONSTRAIN_ERR)]}
1143
            )
1144

    
1145

    
1146
class ProjectResourceGrant(models.Model):
1147
    objects = ExtendedManager()
1148
    member_limit = models.BigIntegerField(null=True)
1149
    project_limit = models.BigIntegerField(null=True)
1150
    resource = models.ForeignKey(Resource)
1151
    project_definition = models.ForeignKey(ProjectDefinition, blank=True)
1152

    
1153
    class Meta:
1154
        unique_together = ("resource", "project_definition")
1155

    
1156

    
1157
class ProjectApplication(models.Model):
1158
    states_list = [PENDING, APPROVED, REPLACED, UNKNOWN]
1159
    states = dict((k, v) for k, v in enumerate(states_list))
1160

    
1161
    applicant = models.ForeignKey(
1162
        AstakosUser,
1163
        related_name='my_project_applications',
1164
        db_index=True)
1165
    owner = models.ForeignKey(
1166
        AstakosUser,
1167
        related_name='own_project_applications',
1168
        db_index=True
1169
    )
1170
    comments = models.TextField(null=True, blank=True)
1171
    definition = models.OneToOneField(ProjectDefinition)
1172
    issue_date = models.DateTimeField()
1173
    precursor_application = models.OneToOneField('ProjectApplication',
1174
        null=True,
1175
        blank=True,
1176
        db_index=True
1177
    )
1178
    state = models.CharField(max_length=80, default=UNKNOWN)
1179
    
1180
    @property
1181
    def follower(self):
1182
        try:
1183
            return ProjectApplication.objects.get(precursor_application=self)
1184
        except ProjectApplication.DoesNotExist:
1185
            return
1186

    
1187
    def save(self):
1188
        self.definition.save()
1189
        self.definition = self.definition
1190
        super(ProjectApplication, self).save()
1191

    
1192

    
1193
    @staticmethod
1194
    def submit(definition, resource_policies, applicant, comments, precursor_application=None, commit=True):
1195
        application = None
1196
        if precursor_application:
1197
            precursor_application_id = precursor_application.id
1198
            application = precursor_application
1199
            application.id = None
1200
        else:
1201
            application = ProjectApplication(owner=applicant)
1202
        application.definition = definition
1203
        application.definition.id = None
1204
        application.applicant = applicant
1205
        application.comments = comments
1206
        application.issue_date = datetime.now()
1207
        application.state = PENDING
1208
        if commit:
1209
            application.save()
1210
            application.definition.resource_policies = resource_policies
1211
            # better implementation ???
1212
            if precursor_application:
1213
                try:
1214
                    precursor = ProjectApplication.objects.get(id=precursor_application_id)
1215
                except:
1216
                    pass
1217
                application.precursor_application = precursor
1218
                application.save()
1219
        else:
1220
            notification = build_notification(
1221
                settings.SERVER_EMAIL,
1222
                [i[1] for i in settings.ADMINS],
1223
                _(GROUP_CREATION_SUBJECT) % {'group':application.definition.name},
1224
                _('An new project application identified by %(id)s has been submitted.') % application.__dict__
1225
            )
1226
            notification.send()
1227
        return application
1228
        
1229
    def approve(self, approval_user=None):
1230
        """
1231
        If approval_user then during owner membership acceptance
1232
        it is checked whether the request_user is eligible.
1233
        
1234
        Raises:
1235
            ValidationError: if there is other alive project with the same name
1236
            
1237
        """
1238
        if self.state != PENDING:
1239
            return
1240
        create = False
1241
        try:
1242
            self.precursor_application.project
1243
        except:
1244
            create = True
1245

    
1246
        if create:
1247
            kwargs = {
1248
                'application':self,
1249
                'creation_date':datetime.now(),
1250
                'last_approval_date':datetime.now(),
1251
            }
1252
            project = _create_object(Project, **kwargs)
1253
            project.accept_member(self.owner, approval_user)
1254
        else:
1255
            project = self.precursor_application.project
1256
            project.application = self
1257
            project.last_approval_date = datetime.now()
1258
            project.save()
1259
            self.precursor_application.state = REPLACED
1260
        self.definition.validate_name()
1261
        self.state = APPROVED
1262
        self.save()
1263

    
1264
        notification = build_notification(
1265
            settings.SERVER_EMAIL,
1266
            [self.owner.email],
1267
            _('Project application has been approved on %s alpha2 testing' % SITENAME),
1268
            _('Your application request %(id)s has been apporved.')
1269
        )
1270
        notification.send()
1271

    
1272
        rejected = self.project.sync()
1273
        if rejected:
1274
            # revert to precursor
1275
            project.application = app.precursor_application
1276
            if project.application:
1277
                project.last_approval_date = last_approval_date
1278
                project.save()
1279
            rejected = project.sync()
1280
            if rejected:
1281
                raise Exception(_(astakos_messages.QH_SYNC_ERROR))
1282
        else:
1283
            project.last_application_synced = app
1284
            project.save()
1285

    
1286

    
1287
class Project(models.Model):
1288
    application = models.OneToOneField(ProjectApplication, related_name='project')
1289
    creation_date = models.DateTimeField()
1290
    last_approval_date = models.DateTimeField(null=True)
1291
    termination_start_date = models.DateTimeField(null=True)
1292
    termination_date = models.DateTimeField(null=True)
1293
    members = models.ManyToManyField(AstakosUser, through='ProjectMembership')
1294
    membership_dirty = models.BooleanField(default=False)
1295
    last_application_synced = models.OneToOneField(
1296
        ProjectApplication, related_name='last_project', null=True, blank=True
1297
    )
1298
    
1299
    
1300
    @property
1301
    def definition(self):
1302
        return self.application.definition
1303

    
1304
    @property
1305
    def violated_members_number_limit(self):
1306
        return len(self.approved_members) <= self.definition.limit_on_members_number
1307
        
1308
    @property
1309
    def is_active(self):
1310
        if not self.last_approval_date:
1311
            return False
1312
        if self.termination_date:
1313
            return False
1314
        if self.definition.violated_resource_grants:
1315
            return False
1316
#         if self.violated_members_number_limit:
1317
#             return False
1318
        return True
1319
    
1320
    @property
1321
    def is_terminated(self):
1322
        if not self.termination_date:
1323
            return False
1324
        return True
1325
    
1326
    @property
1327
    def is_suspended(self):
1328
        if not self.termination_date:
1329
            return False
1330
        if not self.last_approval_date:
1331
            if not self.definition.violated_resource_grants:
1332
                return False
1333
#             if not self.violated_members_number_limit:
1334
#                 return False
1335
        return True
1336
    
1337
    @property
1338
    def is_alive(self):
1339
        return self.is_active or self.is_suspended
1340
    
1341
    @property
1342
    def is_inconsistent(self):
1343
        now = datetime.now()
1344
        if self.creation_date > now:
1345
            return True
1346
        if self.last_approval_date > now:
1347
            return True
1348
        if self.terminaton_date > now:
1349
            return True
1350
        return False
1351
    
1352
    @property
1353
    def is_synchronized(self):
1354
        return self.last_application_synced == self.application and \
1355
            not self.membership_dirty and \
1356
            (not self.termination_start_date or termination_date)
1357
    
1358
    @property
1359
    def approved_members(self):
1360
        return [m.person for m in self.projectmembership_set.filter(~Q(acceptance_date=None))]
1361
        
1362
    def sync(self, specific_members=()):
1363
        if self.is_synchronized:
1364
            return
1365
        members = specific_members or self.approved_members
1366
        c, rejected = send_quota(self.approved_members)
1367
        return rejected
1368
    
1369
    def accept_member(self, user, request_user=None):
1370
        """
1371
        Raises:
1372
            django.exceptions.PermissionDenied
1373
            astakos.im.models.AstakosUser.DoesNotExist
1374
        """
1375
        if isinstance(user, int):
1376
            try:
1377
                user = lookup_object(AstakosUser, user, None, None)
1378
            except Http404:
1379
                raise AstakosUser.DoesNotExist()
1380
        m, created = ProjectMembership.objects.get_or_create(
1381
            person=user, project=self
1382
        )
1383
        m.accept(delete_on_failure=created, request_user=None)
1384

    
1385
    def reject_member(self, user, request_user=None):
1386
        """
1387
        Raises:
1388
            django.exceptions.PermissionDenied
1389
            astakos.im.models.AstakosUser.DoesNotExist
1390
            astakos.im.models.ProjectMembership.DoesNotExist
1391
        """
1392
        if isinstance(user, int):
1393
            try:
1394
                user = lookup_object(AstakosUser, user, None, None)
1395
            except Http404:
1396
                raise AstakosUser.DoesNotExist()
1397
        m = ProjectMembership.objects.get(person=user, project=self)
1398
        m.reject()
1399
        
1400
    def remove_member(self, user, request_user=None):
1401
        """
1402
        Raises:
1403
            django.exceptions.PermissionDenied
1404
            astakos.im.models.AstakosUser.DoesNotExist
1405
            astakos.im.models.ProjectMembership.DoesNotExist
1406
        """
1407
        if isinstance(user, int):
1408
            try:
1409
                user = lookup_object(AstakosUser, user, None, None)
1410
            except Http404:
1411
                raise AstakosUser.DoesNotExist()
1412
        m = ProjectMembership.objects.get(person=user, project=self)
1413
        m.remove()
1414
    
1415
    def terminate(self):
1416
        self.termination_start_date = datetime.now()
1417
        self.terminaton_date = None
1418
        self.save()
1419
        
1420
        rejected = self.sync()
1421
        if not rejected:
1422
            self.termination_start_date = None
1423
            self.terminaton_date = datetime.now()
1424
            self.save()
1425
            
1426
            notification = build_notification(
1427
                settings.SERVER_EMAIL,
1428
                [self.application.owner.email],
1429
                _('Project %(name)s has been terminated.') %  self.definition.__dict__,
1430
                _('Project %(name)s has been terminated.') %  self.definition.__dict__
1431
            )
1432
            notification.send()
1433

    
1434
    def suspend(self):
1435
        self.last_approval_date = None
1436
        self.save()
1437
        notification = build_notification(
1438
            settings.SERVER_EMAIL,
1439
            [self.application.owner.email],
1440
            _('Project %(name)s has been suspended.') %  self.definition.__dict__,
1441
            _('Project %(name)s has been suspended.') %  self.definition.__dict__
1442
        )
1443
        notification.send()
1444

    
1445
class ProjectMembership(models.Model):
1446
    person = models.ForeignKey(AstakosUser)
1447
    project = models.ForeignKey(Project)
1448
    request_date = models.DateField(default=datetime.now())
1449
    acceptance_date = models.DateField(null=True, db_index=True)
1450
    leave_request_date = models.DateField(null=True)
1451

    
1452
    class Meta:
1453
        unique_together = ("person", "project")
1454

    
1455
    def accept(self, delete_on_failure=False, request_user=None):
1456
        """
1457
            Raises:
1458
                django.exception.PermissionDenied
1459
                astakos.im.notifications.NotificationError
1460
        """
1461
        try:
1462
            if request_user and \
1463
                (not self.project.application.owner == request_user and \
1464
                    not request_user.is_superuser):
1465
                raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1466
            if not self.project.is_alive:
1467
                raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % self.project.__dict__)
1468
            if self.project.definition.member_join_policy == 'closed':
1469
                raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1470
            if len(self.project.approved_members) + 1 > self.project.definition.limit_on_members_number:
1471
                raise PermissionDenied(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
1472
        except PermissionDenied, e:
1473
            if delete_on_failure:
1474
                m.delete()
1475
            raise
1476
        if self.acceptance_date:
1477
            return
1478
        self.acceptance_date = datetime.now()
1479
        self.save()
1480
        notification = build_notification(
1481
            settings.SERVER_EMAIL,
1482
            [self.person.email],
1483
            _('Your membership on project %(name)s has been accepted.') % self.project.definition.__dict__,
1484
            _('Your membership on project %(name)s has been accepted.') % self.project.definition.__dict__
1485
        ).send()
1486
        self.sync()
1487
    
1488
    def reject(self, request_user=None):
1489
        """
1490
            Raises:
1491
                django.exception.PermissionDenied,
1492
                astakos.im.notifications.NotificationError
1493
        """
1494
        if request_user and \
1495
            (not self.project.application.owner == request_user and \
1496
                not request_user.is_superuser):
1497
            raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1498
        if not self.project.is_alive:
1499
            raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1500
        history_item = ProjectMembershipHistory(
1501
            person=self.person,
1502
            project=self.project,
1503
            request_date=self.request_date,
1504
            rejection_date=datetime.now()
1505
        )
1506
        self.delete()
1507
        history_item.save()
1508
        notification = build_notification(
1509
            settings.SERVER_EMAIL,
1510
            [self.person.email],
1511
            _('Your membership on project %(name)s has been rejected.') % self.project.definition.__dict__,
1512
            _('Your membership on project %(name)s has been rejected.') % self.project.definition.__dict__
1513
        ).send()
1514
    
1515
    def remove(self, request_user=None):
1516
        """
1517
            Raises:
1518
                django.exception.PermissionDenied
1519
                astakos.im.notifications.NotificationError
1520
        """
1521
        if request_user and \
1522
            (not self.project.application.owner == request_user and \
1523
                not request_user.is_superuser):
1524
            raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1525
        if not self.project.is_alive:
1526
            raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % self.project.__dict__)
1527
        history_item = ProjectMembershipHistory(
1528
            id=self.id,
1529
            person=self.person,
1530
            project=self.project,
1531
            request_date=self.request_date,
1532
            removal_date=datetime.now()
1533
        )
1534
        self.delete()
1535
        history_item.save()
1536
        notification = build_notification(
1537
            settings.SERVER_EMAIL,
1538
            [self.person.email],
1539
            _('Your membership on project %(name)s has been removed.') % self.project.definition.__dict__,
1540
            _('Your membership on project %(name)s has been removed.') % self.project.definition.__dict__
1541
        ).send()
1542
        self.sync()
1543
    
1544
    def leave(self):
1545
        leave_policy = self.project.application.definition.member_leave_policy
1546
        if leave_policy == get_auto_accept_leave():
1547
            self.remove()
1548
        else:
1549
            self.leave_request_date = datetime.now()
1550
            self.save()
1551

    
1552
    def sync(self):
1553
        # set membership_dirty flag
1554
        self.project.membership_dirty = True
1555
        self.project.save()
1556
        
1557
        rejected = self.project.sync(specific_members=[self.person])
1558
        if not rejected:
1559
            # if syncing was successful unset membership_dirty flag
1560
            self.membership_dirty = False
1561
            self.project.save()
1562
        
1563

    
1564
class ProjectMembershipHistory(models.Model):
1565
    person = models.ForeignKey(AstakosUser)
1566
    project = models.ForeignKey(Project)
1567
    request_date = models.DateField(default=datetime.now())
1568
    removal_date = models.DateField(null=True)
1569
    rejection_date = models.DateField(null=True)
1570

    
1571

    
1572
def filter_queryset_by_property(q, property):
1573
    """
1574
    Incorporate list comprehension for filtering querysets by property
1575
    since Queryset.filter() operates on the database level.
1576
    """
1577
    return (p for p in q if getattr(p, property, False))
1578

    
1579
def get_alive_projects():
1580
    return filter_queryset_by_property(
1581
        Project.objects.all(),
1582
        'is_alive'
1583
    )
1584

    
1585
def get_active_projects():
1586
    return filter_queryset_by_property(
1587
        Project.objects.all(),
1588
        'is_active'
1589
    )
1590

    
1591
def _create_object(model, **kwargs):
1592
    o = model.objects.create(**kwargs)
1593
    o.save()
1594
    return o
1595

    
1596

    
1597
def create_astakos_user(u):
1598
    try:
1599
        AstakosUser.objects.get(user_ptr=u.pk)
1600
    except AstakosUser.DoesNotExist:
1601
        extended_user = AstakosUser(user_ptr_id=u.pk)
1602
        extended_user.__dict__.update(u.__dict__)
1603
        extended_user.save()
1604
        if not extended_user.has_auth_provider('local'):
1605
            extended_user.add_auth_provider('local')
1606
    except BaseException, e:
1607
        logger.exception(e)
1608

    
1609

    
1610
def fix_superusers(sender, **kwargs):
1611
    # Associate superusers with AstakosUser
1612
    admins = User.objects.filter(is_superuser=True)
1613
    for u in admins:
1614
        create_astakos_user(u)
1615
post_syncdb.connect(fix_superusers)
1616

    
1617

    
1618
def user_post_save(sender, instance, created, **kwargs):
1619
    if not created:
1620
        return
1621
    create_astakos_user(instance)
1622
post_save.connect(user_post_save, sender=User)
1623

    
1624

    
1625
def astakosuser_pre_save(sender, instance, **kwargs):
1626
    instance.aquarium_report = False
1627
    instance.new = False
1628
    try:
1629
        db_instance = AstakosUser.objects.get(id=instance.id)
1630
    except AstakosUser.DoesNotExist:
1631
        # create event
1632
        instance.aquarium_report = True
1633
        instance.new = True
1634
    else:
1635
        get = AstakosUser.__getattribute__
1636
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1637
                   BILLING_FIELDS)
1638
        instance.aquarium_report = True if l else False
1639
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1640

    
1641
def set_default_group(user):
1642
    try:
1643
        default = AstakosGroup.objects.get(name='default')
1644
        Membership(
1645
            group=default, person=user, date_joined=datetime.now()).save()
1646
    except AstakosGroup.DoesNotExist, e:
1647
        logger.exception(e)
1648

    
1649

    
1650
def astakosuser_post_save(sender, instance, created, **kwargs):
1651
    if instance.aquarium_report:
1652
        report_user_event(instance, create=instance.new)
1653
    if not created:
1654
        return
1655
    set_default_group(instance)
1656
    # TODO handle socket.error & IOError
1657
    register_users((instance,))
1658
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1659

    
1660

    
1661
def resource_post_save(sender, instance, created, **kwargs):
1662
    if not created:
1663
        return
1664
    register_resources((instance,))
1665
post_save.connect(resource_post_save, sender=Resource)
1666

    
1667

    
1668
def on_quota_disturbed(sender, users, **kwargs):
1669
#     print '>>>', locals()
1670
    if not users:
1671
        return
1672
    send_quota(users)
1673

    
1674
quota_disturbed = Signal(providing_args=["users"])
1675
quota_disturbed.connect(on_quota_disturbed)
1676

    
1677

    
1678
def send_quota_disturbed(sender, instance, **kwargs):
1679
    users = []
1680
    extend = users.extend
1681
    if sender == Membership:
1682
        if not instance.group.is_enabled:
1683
            return
1684
        extend([instance.person])
1685
    elif sender == AstakosUserQuota:
1686
        extend([instance.user])
1687
    elif sender == AstakosGroupQuota:
1688
        if not instance.group.is_enabled:
1689
            return
1690
        extend(instance.group.astakosuser_set.all())
1691
    elif sender == AstakosGroup:
1692
        if not instance.is_enabled:
1693
            return
1694
    quota_disturbed.send(sender=sender, users=users)
1695
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1696
post_delete.connect(send_quota_disturbed, sender=Membership)
1697
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1698
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1699
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1700
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1701

    
1702

    
1703
def renew_token(sender, instance, **kwargs):
1704
    if not instance.auth_token:
1705
        instance.renew_token()
1706
pre_save.connect(renew_token, sender=AstakosUser)
1707
pre_save.connect(renew_token, sender=Service)
1708

    
1709

    
1710
def check_closed_join_membership_policy(sender, instance, **kwargs):
1711
    if instance.id:
1712
        return
1713
    join_policy = instance.project.application.definition.member_join_policy
1714
    if join_policy == get_closed_join():
1715
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1716
pre_save.connect(check_closed_join_membership_policy, sender=ProjectMembership)
1717

    
1718

    
1719
def check_auto_accept_join_membership_policy(sender, instance, created, **kwargs):
1720
    if not created:
1721
        return
1722
    join_policy = instance.project.application.definition.member_join_policy
1723
    if join_policy == get_auto_accept_join():
1724
        instance.accept()
1725
post_save.connect(check_auto_accept_join_membership_policy, sender=ProjectMembership)