Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (59.7 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, SERVICES
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)
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
    class Meta:
168
        unique_together = ("name", "service")
169

    
170
    def __str__(self):
171
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
172

    
173

    
174
class GroupKind(models.Model):
175
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
176

    
177
    def __str__(self):
178
        return self.name
179

    
180

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

    
224
    @property
225
    def is_disabled(self):
226
        if not self.approval_date:
227
            return True
228
        return False
229

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

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

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

    
263
    def approve_member(self, person):
264
        m, created = self.membership_set.get_or_create(person=person)
265
        m.approve()
266

    
267
    @property
268
    def members(self):
269
        q = self.membership_set.select_related().all()
270
        return [m.person for m in q]
271

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

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

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

    
297
    @property
298
    def policies(self):
299
        return self.astakosgroupquota_set.select_related().all()
300

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

    
310
    @property
311
    def owners(self):
312
        return self.owner.all()
313

    
314
    @property
315
    def owner_details(self):
316
        return self.owner.select_related().all()
317

    
318
    @owners.setter
319
    def owners(self, l):
320
        self.owner = l
321
        map(self.approve_member, l)
322

    
323
_default_quota = {}
324
def get_default_quota():
325
    global _default_quota
326
    if _default_quota:
327
        return _default_quota
328
    for s, data in SERVICES.iteritems():
329
        map(
330
            lambda d:_default_quota.update(
331
                {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
332
            ),
333
            data.get('resources', {})
334
        )
335
    return _default_quota
336

    
337
class AstakosUserManager(UserManager):
338

    
339
    def get_auth_provider_user(self, provider, **kwargs):
340
        """
341
        Retrieve AstakosUser instance associated with the specified third party
342
        id.
343
        """
344
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
345
                          kwargs.iteritems()))
346
        return self.get(auth_providers__module=provider, **kwargs)
347

    
348
class AstakosUser(User):
349
    """
350
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
351
    """
352
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
353
                                   null=True)
354

    
355
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
356
    #                    AstakosUserProvider model.
357
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
358
                                null=True)
359
    # ex. screen_name for twitter, eppn for shibboleth
360
    third_party_identifier = models.CharField(_('Third-party identifier'),
361
                                              max_length=255, null=True,
362
                                              blank=True)
363

    
364

    
365
    #for invitations
366
    user_level = DEFAULT_USER_LEVEL
367
    level = models.IntegerField(_('Inviter level'), default=user_level)
368
    invitations = models.IntegerField(
369
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
370

    
371
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
372
                                  null=True, blank=True)
373
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
374
    auth_token_expires = models.DateTimeField(
375
        _('Token expiration date'), null=True)
376

    
377
    updated = models.DateTimeField(_('Update date'))
378
    is_verified = models.BooleanField(_('Is verified?'), default=False)
379

    
380
    email_verified = models.BooleanField(_('Email verified?'), default=False)
381

    
382
    has_credits = models.BooleanField(_('Has credits?'), default=False)
383
    has_signed_terms = models.BooleanField(
384
        _('I agree with the terms'), default=False)
385
    date_signed_terms = models.DateTimeField(
386
        _('Signed terms date'), null=True, blank=True)
387

    
388
    activation_sent = models.DateTimeField(
389
        _('Activation sent data'), null=True, blank=True)
390

    
391
    policy = models.ManyToManyField(
392
        Resource, null=True, through='AstakosUserQuota')
393

    
394
    astakos_groups = models.ManyToManyField(
395
        AstakosGroup, verbose_name=_('agroups'), blank=True,
396
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
397
        through='Membership')
398

    
399
    __has_signed_terms = False
400
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
401
                                           default=False, db_index=True)
402

    
403
    objects = AstakosUserManager()
404

    
405
    owner = models.ManyToManyField(
406
        AstakosGroup, related_name='owner', null=True)
407

    
408
    class Meta:
409
        unique_together = ("provider", "third_party_identifier")
410

    
411
    def __init__(self, *args, **kwargs):
412
        super(AstakosUser, self).__init__(*args, **kwargs)
413
        self.__has_signed_terms = self.has_signed_terms
414
        if not self.id:
415
            self.is_active = False
416

    
417
    @property
418
    def realname(self):
419
        return '%s %s' % (self.first_name, self.last_name)
420

    
421
    @realname.setter
422
    def realname(self, value):
423
        parts = value.split(' ')
424
        if len(parts) == 2:
425
            self.first_name = parts[0]
426
            self.last_name = parts[1]
427
        else:
428
            self.last_name = parts[0]
429

    
430
    def add_permission(self, pname):
431
        if self.has_perm(pname):
432
            return
433
        p, created = Permission.objects.get_or_create(
434
                                    codename=pname,
435
                                    name=pname.capitalize(),
436
                                    content_type=get_content_type())
437
        self.user_permissions.add(p)
438

    
439
    def remove_permission(self, pname):
440
        if self.has_perm(pname):
441
            return
442
        p = Permission.objects.get(codename=pname,
443
                                   content_type=get_content_type())
444
        self.user_permissions.remove(p)
445

    
446
    @property
447
    def invitation(self):
448
        try:
449
            return Invitation.objects.get(username=self.email)
450
        except Invitation.DoesNotExist:
451
            return None
452

    
453
    def invite(self, email, realname):
454
        inv = Invitation(inviter=self, username=email, realname=realname)
455
        inv.save()
456
        send_invitation(inv)
457
        self.invitations = max(0, self.invitations - 1)
458
        self.save()
459

    
460
    @property
461
    def quota(self):
462
        """Returns a dict with the sum of quota limits per resource"""
463
        d = defaultdict(int)
464
        default_quota = get_default_quota()
465
        d.update(default_quota)
466
        for q in self.policies:
467
            d[q.resource] += q.uplimit or inf
468
        for m in self.projectmembership_set.select_related().all():
469
            if not m.acceptance_date:
470
                continue
471
            p = m.project
472
            if not p.is_active:
473
                continue
474
            grants = p.application.definition.projectresourcegrant_set.all()
475
            for g in grants:
476
                d[str(g.resource)] += g.member_limit or inf
477
        # TODO set default for remaining
478
        return d
479

    
480
    @property
481
    def policies(self):
482
        return self.astakosuserquota_set.select_related().all()
483

    
484
    @policies.setter
485
    def policies(self, policies):
486
        for p in policies:
487
            service = policies.get('service', None)
488
            resource = policies.get('resource', None)
489
            uplimit = policies.get('uplimit', 0)
490
            update = policies.get('update', True)
491
            self.add_policy(service, resource, uplimit, update)
492

    
493
    def add_policy(self, service, resource, uplimit, update=True):
494
        """Raises ObjectDoesNotExist, IntegrityError"""
495
        resource = Resource.objects.get(service__name=service, name=resource)
496
        if update:
497
            AstakosUserQuota.objects.update_or_create(user=self,
498
                                                      resource=resource,
499
                                                      defaults={'uplimit': uplimit})
500
        else:
501
            q = self.astakosuserquota_set
502
            q.create(resource=resource, uplimit=uplimit)
503

    
504
    def remove_policy(self, service, resource):
505
        """Raises ObjectDoesNotExist, IntegrityError"""
506
        resource = Resource.objects.get(service__name=service, name=resource)
507
        q = self.policies.get(resource=resource).delete()
508

    
509
    @property
510
    def extended_groups(self):
511
        return self.membership_set.select_related().all()
512

    
513
    @extended_groups.setter
514
    def extended_groups(self, groups):
515
        #TODO exceptions
516
        for name in (groups or ()):
517
            group = AstakosGroup.objects.get(name=name)
518
            self.membership_set.create(group=group)
519

    
520
    def save(self, update_timestamps=True, **kwargs):
521
        if update_timestamps:
522
            if not self.id:
523
                self.date_joined = datetime.now()
524
            self.updated = datetime.now()
525

    
526
        # update date_signed_terms if necessary
527
        if self.__has_signed_terms != self.has_signed_terms:
528
            self.date_signed_terms = datetime.now()
529

    
530
        if not self.id:
531
            # set username
532
            self.username = self.email
533

    
534
        self.validate_unique_email_isactive()
535
        if self.is_active and self.activation_sent:
536
            # reset the activation sent
537
            self.activation_sent = None
538

    
539
        super(AstakosUser, self).save(**kwargs)
540

    
541
    def renew_token(self, flush_sessions=False, current_key=None):
542
        md5 = hashlib.md5()
543
        md5.update(settings.SECRET_KEY)
544
        md5.update(self.username)
545
        md5.update(self.realname.encode('ascii', 'ignore'))
546
        md5.update(asctime())
547

    
548
        self.auth_token = b64encode(md5.digest())
549
        self.auth_token_created = datetime.now()
550
        self.auth_token_expires = self.auth_token_created + \
551
                                  timedelta(hours=AUTH_TOKEN_DURATION)
552
        if flush_sessions:
553
            self.flush_sessions(current_key)
554
        msg = 'Token renewed for %s' % self.email
555
        logger.log(LOGGING_LEVEL, msg)
556

    
557
    def flush_sessions(self, current_key=None):
558
        q = self.sessions
559
        if current_key:
560
            q = q.exclude(session_key=current_key)
561

    
562
        keys = q.values_list('session_key', flat=True)
563
        if keys:
564
            msg = 'Flushing sessions: %s' % ','.join(keys)
565
            logger.log(LOGGING_LEVEL, msg, [])
566
        engine = import_module(settings.SESSION_ENGINE)
567
        for k in keys:
568
            s = engine.SessionStore(k)
569
            s.flush()
570

    
571
    def __unicode__(self):
572
        return '%s (%s)' % (self.realname, self.email)
573

    
574
    def conflicting_email(self):
575
        q = AstakosUser.objects.exclude(username=self.username)
576
        q = q.filter(email__iexact=self.email)
577
        if q.count() != 0:
578
            return True
579
        return False
580

    
581
    def validate_unique_email_isactive(self):
582
        """
583
        Implements a unique_together constraint for email and is_active fields.
584
        """
585
        q = AstakosUser.objects.all()
586
        q = q.filter(email = self.email)
587
        q = q.filter(is_active = self.is_active)
588
        if self.id:
589
            q = q.filter(~Q(id = self.id))
590
        if q.count() != 0:
591
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
592

    
593
    @property
594
    def signed_terms(self):
595
        term = get_latest_terms()
596
        if not term:
597
            return True
598
        if not self.has_signed_terms:
599
            return False
600
        if not self.date_signed_terms:
601
            return False
602
        if self.date_signed_terms < term.date:
603
            self.has_signed_terms = False
604
            self.date_signed_terms = None
605
            self.save()
606
            return False
607
        return True
608

    
609
    def set_invitations_level(self):
610
        """
611
        Update user invitation level
612
        """
613
        level = self.invitation.inviter.level + 1
614
        self.level = level
615
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
616

    
617
    def can_login_with_auth_provider(self, provider):
618
        if not self.has_auth_provider(provider):
619
            return False
620
        else:
621
            return auth_providers.get_provider(provider).is_available_for_login()
622

    
623
    def can_add_auth_provider(self, provider, **kwargs):
624
        provider_settings = auth_providers.get_provider(provider)
625
        if not provider_settings.is_available_for_login():
626
            return False
627

    
628
        if self.has_auth_provider(provider) and \
629
           provider_settings.one_per_user:
630
            return False
631

    
632
        if 'identifier' in kwargs:
633
            try:
634
                # provider with specified params already exist
635
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
636
                                                                   **kwargs)
637
            except AstakosUser.DoesNotExist:
638
                return True
639
            else:
640
                return False
641

    
642
        return True
643

    
644
    def can_remove_auth_provider(self, provider):
645
        if len(self.get_active_auth_providers()) <= 1:
646
            return False
647
        return True
648

    
649
    def can_change_password(self):
650
        return self.has_auth_provider('local', auth_backend='astakos')
651

    
652
    def has_auth_provider(self, provider, **kwargs):
653
        return bool(self.auth_providers.filter(module=provider,
654
                                               **kwargs).count())
655

    
656
    def add_auth_provider(self, provider, **kwargs):
657
        if self.can_add_auth_provider(provider, **kwargs):
658
            self.auth_providers.create(module=provider, active=True, **kwargs)
659
        else:
660
            raise Exception('Cannot add provider')
661

    
662
    def add_pending_auth_provider(self, pending):
663
        """
664
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
665
        the current user.
666
        """
667
        if not isinstance(pending, PendingThirdPartyUser):
668
            pending = PendingThirdPartyUser.objects.get(token=pending)
669

    
670
        provider = self.add_auth_provider(pending.provider,
671
                               identifier=pending.third_party_identifier)
672

    
673
        if email_re.match(pending.email or '') and pending.email != self.email:
674
            self.additionalmail_set.get_or_create(email=pending.email)
675

    
676
        pending.delete()
677
        return provider
678

    
679
    def remove_auth_provider(self, provider, **kwargs):
680
        self.auth_providers.get(module=provider, **kwargs).delete()
681

    
682
    # user urls
683
    def get_resend_activation_url(self):
684
        return reverse('send_activation', {'user_id': self.pk})
685

    
686
    def get_activation_url(self, nxt=False):
687
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
688
                                 quote(self.auth_token))
689
        if nxt:
690
            url += "&next=%s" % quote(nxt)
691
        return url
692

    
693
    def get_password_reset_url(self, token_generator=default_token_generator):
694
        return reverse('django.contrib.auth.views.password_reset_confirm',
695
                          kwargs={'uidb36':int_to_base36(self.id),
696
                                  'token':token_generator.make_token(self)})
697

    
698
    def get_auth_providers(self):
699
        return self.auth_providers.all()
700

    
701
    def get_available_auth_providers(self):
702
        """
703
        Returns a list of providers available for user to connect to.
704
        """
705
        providers = []
706
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
707
            if self.can_add_auth_provider(module):
708
                providers.append(provider_settings(self))
709

    
710
        return providers
711

    
712
    def get_active_auth_providers(self):
713
        providers = []
714
        for provider in self.auth_providers.active():
715
            if auth_providers.get_provider(provider.module).is_available_for_login():
716
                providers.append(provider)
717
        return providers
718

    
719
    @property
720
    def auth_providers_display(self):
721
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
722

    
723

    
724
class AstakosUserAuthProviderManager(models.Manager):
725

    
726
    def active(self):
727
        return self.filter(active=True)
728

    
729

    
730
class AstakosUserAuthProvider(models.Model):
731
    """
732
    Available user authentication methods.
733
    """
734
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
735
                                   null=True, default=None)
736
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
737
    module = models.CharField(_('Provider'), max_length=255, blank=False,
738
                                default='local')
739
    identifier = models.CharField(_('Third-party identifier'),
740
                                              max_length=255, null=True,
741
                                              blank=True)
742
    active = models.BooleanField(default=True)
743
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
744
                                   default='astakos')
745

    
746
    objects = AstakosUserAuthProviderManager()
747

    
748
    class Meta:
749
        unique_together = (('identifier', 'module', 'user'), )
750

    
751
    @property
752
    def settings(self):
753
        return auth_providers.get_provider(self.module)
754

    
755
    @property
756
    def details_display(self):
757
        return self.settings.details_tpl % self.__dict__
758

    
759
    def can_remove(self):
760
        return self.user.can_remove_auth_provider(self.module)
761

    
762
    def delete(self, *args, **kwargs):
763
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
764
        if self.module == 'local':
765
            self.user.set_unusable_password()
766
            self.user.save()
767
        return ret
768

    
769
    def __repr__(self):
770
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
771

    
772
    def __unicode__(self):
773
        if self.identifier:
774
            return "%s:%s" % (self.module, self.identifier)
775
        if self.auth_backend:
776
            return "%s:%s" % (self.module, self.auth_backend)
777
        return self.module
778

    
779

    
780

    
781
class Membership(models.Model):
782
    person = models.ForeignKey(AstakosUser)
783
    group = models.ForeignKey(AstakosGroup)
784
    date_requested = models.DateField(default=datetime.now(), blank=True)
785
    date_joined = models.DateField(null=True, db_index=True, blank=True)
786

    
787
    class Meta:
788
        unique_together = ("person", "group")
789

    
790
    def save(self, *args, **kwargs):
791
        if not self.id:
792
            if not self.group.moderation_enabled:
793
                self.date_joined = datetime.now()
794
        super(Membership, self).save(*args, **kwargs)
795

    
796
    @property
797
    def is_approved(self):
798
        if self.date_joined:
799
            return True
800
        return False
801

    
802
    def approve(self):
803
        if self.is_approved:
804
            return
805
        if self.group.max_participants:
806
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
807
            'Maximum participant number has been reached.'
808
        self.date_joined = datetime.now()
809
        self.save()
810
        quota_disturbed.send(sender=self, users=(self.person,))
811

    
812
    def disapprove(self):
813
        approved = self.is_approved()
814
        self.delete()
815
        if approved:
816
            quota_disturbed.send(sender=self, users=(self.person,))
817

    
818
class ExtendedManager(models.Manager):
819
    def _update_or_create(self, **kwargs):
820
        assert kwargs, \
821
            'update_or_create() must be passed at least one keyword argument'
822
        obj, created = self.get_or_create(**kwargs)
823
        defaults = kwargs.pop('defaults', {})
824
        if created:
825
            return obj, True, False
826
        else:
827
            try:
828
                params = dict(
829
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
830
                params.update(defaults)
831
                for attr, val in params.items():
832
                    if hasattr(obj, attr):
833
                        setattr(obj, attr, val)
834
                sid = transaction.savepoint()
835
                obj.save(force_update=True)
836
                transaction.savepoint_commit(sid)
837
                return obj, False, True
838
            except IntegrityError, e:
839
                transaction.savepoint_rollback(sid)
840
                try:
841
                    return self.get(**kwargs), False, False
842
                except self.model.DoesNotExist:
843
                    raise e
844

    
845
    update_or_create = _update_or_create
846

    
847
class AstakosGroupQuota(models.Model):
848
    objects = ExtendedManager()
849
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
850
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
851
    resource = models.ForeignKey(Resource)
852
    group = models.ForeignKey(AstakosGroup, blank=True)
853

    
854
    class Meta:
855
        unique_together = ("resource", "group")
856

    
857
class AstakosUserQuota(models.Model):
858
    objects = ExtendedManager()
859
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
860
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
861
    resource = models.ForeignKey(Resource)
862
    user = models.ForeignKey(AstakosUser)
863

    
864
    class Meta:
865
        unique_together = ("resource", "user")
866

    
867

    
868
class ApprovalTerms(models.Model):
869
    """
870
    Model for approval terms
871
    """
872

    
873
    date = models.DateTimeField(
874
        _('Issue date'), db_index=True, default=datetime.now())
875
    location = models.CharField(_('Terms location'), max_length=255)
876

    
877

    
878
class Invitation(models.Model):
879
    """
880
    Model for registring invitations
881
    """
882
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
883
                                null=True)
884
    realname = models.CharField(_('Real name'), max_length=255)
885
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
886
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
887
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
888
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
889
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
890

    
891
    def __init__(self, *args, **kwargs):
892
        super(Invitation, self).__init__(*args, **kwargs)
893
        if not self.id:
894
            self.code = _generate_invitation_code()
895

    
896
    def consume(self):
897
        self.is_consumed = True
898
        self.consumed = datetime.now()
899
        self.save()
900

    
901
    def __unicode__(self):
902
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
903

    
904

    
905
class EmailChangeManager(models.Manager):
906
    @transaction.commit_on_success
907
    def change_email(self, activation_key):
908
        """
909
        Validate an activation key and change the corresponding
910
        ``User`` if valid.
911

912
        If the key is valid and has not expired, return the ``User``
913
        after activating.
914

915
        If the key is not valid or has expired, return ``None``.
916

917
        If the key is valid but the ``User`` is already active,
918
        return ``None``.
919

920
        After successful email change the activation record is deleted.
921

922
        Throws ValueError if there is already
923
        """
924
        try:
925
            email_change = self.model.objects.get(
926
                activation_key=activation_key)
927
            if email_change.activation_key_expired():
928
                email_change.delete()
929
                raise EmailChange.DoesNotExist
930
            # is there an active user with this address?
931
            try:
932
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
933
            except AstakosUser.DoesNotExist:
934
                pass
935
            else:
936
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
937
            # update user
938
            user = AstakosUser.objects.get(pk=email_change.user_id)
939
            user.email = email_change.new_email_address
940
            user.save()
941
            email_change.delete()
942
            return user
943
        except EmailChange.DoesNotExist:
944
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
945

    
946

    
947
class EmailChange(models.Model):
948
    new_email_address = models.EmailField(_(u'new e-mail address'),
949
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
950
    user = models.ForeignKey(
951
        AstakosUser, unique=True, related_name='emailchange_user')
952
    requested_at = models.DateTimeField(default=datetime.now())
953
    activation_key = models.CharField(
954
        max_length=40, unique=True, db_index=True)
955

    
956
    objects = EmailChangeManager()
957

    
958
    def activation_key_expired(self):
959
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
960
        return self.requested_at + expiration_date < datetime.now()
961

    
962

    
963
class AdditionalMail(models.Model):
964
    """
965
    Model for registring invitations
966
    """
967
    owner = models.ForeignKey(AstakosUser)
968
    email = models.EmailField()
969

    
970

    
971
def _generate_invitation_code():
972
    while True:
973
        code = randint(1, 2L ** 63 - 1)
974
        try:
975
            Invitation.objects.get(code=code)
976
            # An invitation with this code already exists, try again
977
        except Invitation.DoesNotExist:
978
            return code
979

    
980

    
981
def get_latest_terms():
982
    try:
983
        term = ApprovalTerms.objects.order_by('-id')[0]
984
        return term
985
    except IndexError:
986
        pass
987
    return None
988

    
989
class PendingThirdPartyUser(models.Model):
990
    """
991
    Model for registring successful third party user authentications
992
    """
993
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
994
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
995
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
996
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
997
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
998
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
999
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1000
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1001
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1002

    
1003
    class Meta:
1004
        unique_together = ("provider", "third_party_identifier")
1005

    
1006
    @property
1007
    def realname(self):
1008
        return '%s %s' %(self.first_name, self.last_name)
1009

    
1010
    @realname.setter
1011
    def realname(self, value):
1012
        parts = value.split(' ')
1013
        if len(parts) == 2:
1014
            self.first_name = parts[0]
1015
            self.last_name = parts[1]
1016
        else:
1017
            self.last_name = parts[0]
1018

    
1019
    def save(self, **kwargs):
1020
        if not self.id:
1021
            # set username
1022
            while not self.username:
1023
                username =  uuid.uuid4().hex[:30]
1024
                try:
1025
                    AstakosUser.objects.get(username = username)
1026
                except AstakosUser.DoesNotExist, e:
1027
                    self.username = username
1028
        super(PendingThirdPartyUser, self).save(**kwargs)
1029

    
1030
    def generate_token(self):
1031
        self.password = self.third_party_identifier
1032
        self.last_login = datetime.now()
1033
        self.token = default_token_generator.make_token(self)
1034

    
1035
class SessionCatalog(models.Model):
1036
    session_key = models.CharField(_('session key'), max_length=40)
1037
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1038

    
1039
class MemberJoinPolicy(models.Model):
1040
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1041
    description = models.CharField(_('Description'), max_length=80)
1042

    
1043
    def __str__(self):
1044
        return self.policy
1045

    
1046
class MemberLeavePolicy(models.Model):
1047
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1048
    description = models.CharField(_('Description'), max_length=80)
1049

    
1050
    def __str__(self):
1051
        return self.policy
1052

    
1053
_auto_accept_join = False
1054
def get_auto_accept_join():
1055
    global _auto_accept_join
1056
    if _auto_accept_join is not False:
1057
        return _auto_accept_join
1058
    try:
1059
        auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
1060
    except:
1061
        auto_accept = None
1062
    _auto_accept_join = auto_accept
1063
    return auto_accept
1064

    
1065
_closed_join = False
1066
def get_closed_join():
1067
    global _closed_join
1068
    if _closed_join is not False:
1069
        return _closed_join
1070
    try:
1071
        closed = MemberJoinPolicy.objects.get(policy='closed')
1072
    except:
1073
        closed = None
1074
    _closed_join = closed
1075
    return closed
1076

    
1077
_auto_accept_leave = False
1078
def get_auto_accept_leave():
1079
    global _auto_accept_leave
1080
    if _auto_accept_leave is not False:
1081
        return _auto_accept_leave
1082
    try:
1083
        auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
1084
    except:
1085
        auto_accept = None
1086
    _auto_accept_leave = auto_accept
1087
    return auto_accept
1088

    
1089
_closed_leave = False
1090
def get_closed_leave():
1091
    global _closed_leave
1092
    if _closed_leave is not False:
1093
        return _closed_leave
1094
    try:
1095
        closed = MemberLeavePolicy.objects.get(policy='closed')
1096
    except:
1097
        closed = None
1098
    _closed_leave = closed
1099
    return closeds
1100

    
1101
class ProjectDefinition(models.Model):
1102
    name = models.CharField(max_length=80)
1103
    homepage = models.URLField(max_length=255, null=True, blank=True)
1104
    description = models.TextField(null=True)
1105
    start_date = models.DateTimeField()
1106
    end_date = models.DateTimeField()
1107
    member_join_policy = models.ForeignKey(MemberJoinPolicy)
1108
    member_leave_policy = models.ForeignKey(MemberLeavePolicy)
1109
    limit_on_members_number = models.PositiveIntegerField(null=True,blank=True)
1110
    resource_grants = models.ManyToManyField(
1111
        Resource,
1112
        null=True,
1113
        blank=True,
1114
        through='ProjectResourceGrant'
1115
    )
1116
    
1117
    @property
1118
    def violated_resource_grants(self):
1119
        return False
1120
    
1121
    def add_resource_policy(self, service, resource, uplimit, update=True):
1122
        """Raises ObjectDoesNotExist, IntegrityError"""
1123
        resource = Resource.objects.get(service__name=service, name=resource)
1124
        if update:
1125
            ProjectResourceGrant.objects.update_or_create(
1126
                project_definition=self,
1127
                resource=resource,
1128
                defaults={'member_limit': uplimit}
1129
            )
1130
        else:
1131
            q = self.projectresourcegrant_set
1132
            q.create(resource=resource, member_limit=uplimit)
1133

    
1134
    @property
1135
    def resource_policies(self):
1136
        return self.projectresourcegrant_set.all()
1137

    
1138
    @resource_policies.setter
1139
    def resource_policies(self, policies):
1140
        for p in policies:
1141
            service = p.get('service', None)
1142
            resource = p.get('resource', None)
1143
            uplimit = p.get('uplimit', 0)
1144
            update = p.get('update', True)
1145
            self.add_resource_policy(service, resource, uplimit, update)
1146
    
1147
    def validate_name(self):
1148
        """
1149
        Validate name uniqueness among all active projects.
1150
        """
1151
        alive_projects = list(get_alive_projects())
1152
        q = filter(
1153
            lambda p: p.definition.name == self.name and \
1154
                p.application.id != self.projectapplication.id,
1155
            alive_projects
1156
        )
1157
        if q:
1158
            raise ValidationError(
1159
                {'name': [_(astakos_messages.UNIQUE_PROJECT_NAME_CONSTRAIN_ERR)]}
1160
            )
1161

    
1162

    
1163
class ProjectResourceGrant(models.Model):
1164
    objects = ExtendedManager()
1165
    member_limit = models.BigIntegerField(null=True)
1166
    project_limit = models.BigIntegerField(null=True)
1167
    resource = models.ForeignKey(Resource)
1168
    project_definition = models.ForeignKey(ProjectDefinition, blank=True)
1169

    
1170
    class Meta:
1171
        unique_together = ("resource", "project_definition")
1172

    
1173

    
1174
class ProjectApplication(models.Model):
1175
    states_list = [PENDING, APPROVED, REPLACED, UNKNOWN]
1176
    states = dict((k, v) for k, v in enumerate(states_list))
1177

    
1178
    applicant = models.ForeignKey(
1179
        AstakosUser,
1180
        related_name='my_project_applications',
1181
        db_index=True)
1182
    owner = models.ForeignKey(
1183
        AstakosUser,
1184
        related_name='own_project_applications',
1185
        db_index=True
1186
    )
1187
    comments = models.TextField(null=True, blank=True)
1188
    definition = models.OneToOneField(ProjectDefinition)
1189
    issue_date = models.DateTimeField()
1190
    precursor_application = models.OneToOneField('ProjectApplication',
1191
        null=True,
1192
        blank=True,
1193
        db_index=True
1194
    )
1195
    state = models.CharField(max_length=80, default=UNKNOWN)
1196
    
1197
    @property
1198
    def follower(self):
1199
        try:
1200
            return ProjectApplication.objects.get(precursor_application=self)
1201
        except ProjectApplication.DoesNotExist:
1202
            return
1203

    
1204
    def save(self):
1205
        self.definition.save()
1206
        self.definition = self.definition
1207
        super(ProjectApplication, self).save()
1208

    
1209

    
1210
    @staticmethod
1211
    def submit(definition, resource_policies, applicant, comments, precursor_application=None, commit=True):
1212
        application = None
1213
        if precursor_application:
1214
            precursor_application_id = precursor_application.id
1215
            application = precursor_application
1216
            application.id = None
1217
            application.precursor_application = None
1218
        else:
1219
            application = ProjectApplication(owner=applicant)
1220
        application.definition = definition
1221
        application.definition.id = None
1222
        application.applicant = applicant
1223
        application.comments = comments
1224
        application.issue_date = datetime.now()
1225
        application.state = PENDING
1226
        if commit:
1227
            application.save()
1228
            application.definition.resource_policies = resource_policies
1229
            # better implementation ???
1230
            if precursor_application:
1231
                try:
1232
                    precursor = ProjectApplication.objects.get(id=precursor_application_id)
1233
                except:
1234
                    pass
1235
                application.precursor_application = precursor
1236
                application.save()
1237
        else:
1238
            notification = build_notification(
1239
                settings.SERVER_EMAIL,
1240
                [i[1] for i in settings.ADMINS],
1241
                _(GROUP_CREATION_SUBJECT) % {'group':application.definition.name},
1242
                _('An new project application identified by %(id)s has been submitted.') % application.__dict__
1243
            )
1244
            notification.send()
1245
        return application
1246
        
1247
    def approve(self, approval_user=None):
1248
        """
1249
        If approval_user then during owner membership acceptance
1250
        it is checked whether the request_user is eligible.
1251
        
1252
        Raises:
1253
            ValidationError: if there is other alive project with the same name
1254
            
1255
        """
1256
        if self.state != PENDING:
1257
            return
1258
        create = False
1259
        try:
1260
            self.precursor_application.project
1261
        except:
1262
            create = True
1263

    
1264
        if create:
1265
            kwargs = {
1266
                'application':self,
1267
                'creation_date':datetime.now(),
1268
                'last_approval_date':datetime.now(),
1269
            }
1270
            project = _create_object(Project, **kwargs)
1271
            project.accept_member(self.owner, approval_user)
1272
        else:
1273
            project = self.precursor_application.project
1274
            project.application = self
1275
            project.last_approval_date = datetime.now()
1276
            project.save()
1277
        precursor = self.precursor_application
1278
        while precursor:
1279
            precursor.state = REPLACED
1280
            precursor.save()
1281
            precursor = precursor.precursor_application
1282
        self.state = APPROVED
1283
        self.save()
1284
        
1285
#         self.definition.validate_name()
1286

    
1287
        notification = build_notification(
1288
            settings.SERVER_EMAIL,
1289
            [self.owner.email],
1290
            _('Project application has been approved on %s alpha2 testing' % SITENAME),
1291
            _('Your application request %(id)s has been apporved.')
1292
        )
1293
        notification.send()
1294

    
1295
        rejected = self.project.sync()
1296
        if rejected:
1297
            # revert to precursor
1298
            project.application = app.precursor_application
1299
            if project.application:
1300
                project.last_approval_date = last_approval_date
1301
                project.save()
1302
            rejected = project.sync()
1303
            if rejected:
1304
                raise Exception(_(astakos_messages.QH_SYNC_ERROR))
1305
        else:
1306
            project.last_application_synced = self
1307
            project.save()
1308

    
1309

    
1310
class Project(models.Model):
1311
    application = models.OneToOneField(ProjectApplication, related_name='project')
1312
    creation_date = models.DateTimeField()
1313
    last_approval_date = models.DateTimeField(null=True)
1314
    termination_start_date = models.DateTimeField(null=True)
1315
    termination_date = models.DateTimeField(null=True)
1316
    members = models.ManyToManyField(AstakosUser, through='ProjectMembership')
1317
    membership_dirty = models.BooleanField(default=False)
1318
    last_application_synced = models.OneToOneField(
1319
        ProjectApplication, related_name='last_project', null=True, blank=True
1320
    )
1321
    
1322
    
1323
    @property
1324
    def definition(self):
1325
        return self.application.definition
1326

    
1327
    @property
1328
    def violated_members_number_limit(self):
1329
        return len(self.approved_members) <= self.definition.limit_on_members_number
1330
        
1331
    @property
1332
    def is_active(self):
1333
        if not self.last_approval_date:
1334
            return False
1335
        if self.termination_date:
1336
            return False
1337
        if self.definition.violated_resource_grants:
1338
            return False
1339
#         if self.violated_members_number_limit:
1340
#             return False
1341
        return True
1342
    
1343
    @property
1344
    def is_terminated(self):
1345
        if not self.termination_date:
1346
            return False
1347
        return True
1348
    
1349
    @property
1350
    def is_suspended(self):
1351
        if self.termination_date:
1352
            return False
1353
        if self.last_approval_date:
1354
            if not self.definition.violated_resource_grants:
1355
                return False
1356
#             if not self.violated_members_number_limit:
1357
#                 return False
1358
        return True
1359
    
1360
    @property
1361
    def is_alive(self):
1362
        return self.is_active or self.is_suspended
1363
    
1364
    @property
1365
    def is_inconsistent(self):
1366
        now = datetime.now()
1367
        if self.creation_date > now:
1368
            return True
1369
        if self.last_approval_date > now:
1370
            return True
1371
        if self.terminaton_date > now:
1372
            return True
1373
        return False
1374
    
1375
    @property
1376
    def is_synchronized(self):
1377
        return self.last_application_synced == self.application and \
1378
            not self.membership_dirty and \
1379
            (not self.termination_start_date or termination_date)
1380
    
1381
    @property
1382
    def approved_members(self):
1383
        return [m.person for m in self.projectmembership_set.filter(~Q(acceptance_date=None))]
1384
        
1385
    def sync(self, specific_members=()):
1386
        if self.is_synchronized:
1387
            return
1388
        members = specific_members or self.approved_members
1389
        c, rejected = send_quota(self.approved_members)
1390
        return rejected
1391
    
1392
    def accept_member(self, user, request_user=None):
1393
        """
1394
        Raises:
1395
            django.exceptions.PermissionDenied
1396
            astakos.im.models.AstakosUser.DoesNotExist
1397
        """
1398
        if isinstance(user, int):
1399
            try:
1400
                user = lookup_object(AstakosUser, user, None, None)
1401
            except Http404:
1402
                raise AstakosUser.DoesNotExist()
1403
        m, created = ProjectMembership.objects.get_or_create(
1404
            person=user, project=self
1405
        )
1406
        m.accept(delete_on_failure=created, request_user=None)
1407

    
1408
    def reject_member(self, user, request_user=None):
1409
        """
1410
        Raises:
1411
            django.exceptions.PermissionDenied
1412
            astakos.im.models.AstakosUser.DoesNotExist
1413
            astakos.im.models.ProjectMembership.DoesNotExist
1414
        """
1415
        if isinstance(user, int):
1416
            try:
1417
                user = lookup_object(AstakosUser, user, None, None)
1418
            except Http404:
1419
                raise AstakosUser.DoesNotExist()
1420
        m = ProjectMembership.objects.get(person=user, project=self)
1421
        m.reject()
1422
        
1423
    def remove_member(self, user, request_user=None):
1424
        """
1425
        Raises:
1426
            django.exceptions.PermissionDenied
1427
            astakos.im.models.AstakosUser.DoesNotExist
1428
            astakos.im.models.ProjectMembership.DoesNotExist
1429
        """
1430
        if isinstance(user, int):
1431
            try:
1432
                user = lookup_object(AstakosUser, user, None, None)
1433
            except Http404:
1434
                raise AstakosUser.DoesNotExist()
1435
        m = ProjectMembership.objects.get(person=user, project=self)
1436
        m.remove()
1437
    
1438
    def terminate(self):
1439
        self.termination_start_date = datetime.now()
1440
        self.terminaton_date = None
1441
        self.save()
1442
        
1443
        rejected = self.sync()
1444
        if not rejected:
1445
            self.termination_start_date = None
1446
            self.terminaton_date = datetime.now()
1447
            self.save()
1448
            
1449
            notification = build_notification(
1450
                settings.SERVER_EMAIL,
1451
                [self.application.owner.email],
1452
                _('Project %(name)s has been terminated.') %  self.definition.__dict__,
1453
                _('Project %(name)s has been terminated.') %  self.definition.__dict__
1454
            )
1455
            notification.send()
1456

    
1457
    def suspend(self):
1458
        self.last_approval_date = None
1459
        self.save()
1460
        notification = build_notification(
1461
            settings.SERVER_EMAIL,
1462
            [self.application.owner.email],
1463
            _('Project %(name)s has been suspended.') %  self.definition.__dict__,
1464
            _('Project %(name)s has been suspended.') %  self.definition.__dict__
1465
        )
1466
        notification.send()
1467

    
1468
class ProjectMembership(models.Model):
1469
    person = models.ForeignKey(AstakosUser)
1470
    project = models.ForeignKey(Project)
1471
    request_date = models.DateField(default=datetime.now())
1472
    acceptance_date = models.DateField(null=True, db_index=True)
1473
    leave_request_date = models.DateField(null=True)
1474

    
1475
    class Meta:
1476
        unique_together = ("person", "project")
1477

    
1478
    def accept(self, delete_on_failure=False, request_user=None):
1479
        """
1480
            Raises:
1481
                django.exception.PermissionDenied
1482
                astakos.im.notifications.NotificationError
1483
        """
1484
        try:
1485
            if request_user and \
1486
                (not self.project.application.owner == request_user and \
1487
                    not request_user.is_superuser):
1488
                raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1489
            if not self.project.is_alive:
1490
                raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % self.project.__dict__)
1491
            if self.project.definition.member_join_policy == 'closed':
1492
                raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1493
            if len(self.project.approved_members) + 1 > self.project.definition.limit_on_members_number:
1494
                raise PermissionDenied(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
1495
        except PermissionDenied, e:
1496
            if delete_on_failure:
1497
                m.delete()
1498
            raise
1499
        if self.acceptance_date:
1500
            return
1501
        self.acceptance_date = datetime.now()
1502
        self.save()
1503
        notification = build_notification(
1504
            settings.SERVER_EMAIL,
1505
            [self.person.email],
1506
            _('Your membership on project %(name)s has been accepted.') % self.project.definition.__dict__,
1507
            _('Your membership on project %(name)s has been accepted.') % self.project.definition.__dict__
1508
        ).send()
1509
        self.sync()
1510
    
1511
    def reject(self, request_user=None):
1512
        """
1513
            Raises:
1514
                django.exception.PermissionDenied,
1515
                astakos.im.notifications.NotificationError
1516
        """
1517
        if request_user and \
1518
            (not self.project.application.owner == request_user and \
1519
                not request_user.is_superuser):
1520
            raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1521
        if not self.project.is_alive:
1522
            raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1523
        history_item = ProjectMembershipHistory(
1524
            person=self.person,
1525
            project=self.project,
1526
            request_date=self.request_date,
1527
            rejection_date=datetime.now()
1528
        )
1529
        self.delete()
1530
        history_item.save()
1531
        notification = build_notification(
1532
            settings.SERVER_EMAIL,
1533
            [self.person.email],
1534
            _('Your membership on project %(name)s has been rejected.') % self.project.definition.__dict__,
1535
            _('Your membership on project %(name)s has been rejected.') % self.project.definition.__dict__
1536
        ).send()
1537
    
1538
    def remove(self, request_user=None):
1539
        """
1540
            Raises:
1541
                django.exception.PermissionDenied
1542
                astakos.im.notifications.NotificationError
1543
        """
1544
        if request_user and \
1545
            (not self.project.application.owner == request_user and \
1546
                not request_user.is_superuser):
1547
            raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1548
        if not self.project.is_alive:
1549
            raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % self.project.__dict__)
1550
        history_item = ProjectMembershipHistory(
1551
            id=self.id,
1552
            person=self.person,
1553
            project=self.project,
1554
            request_date=self.request_date,
1555
            removal_date=datetime.now()
1556
        )
1557
        self.delete()
1558
        history_item.save()
1559
        notification = build_notification(
1560
            settings.SERVER_EMAIL,
1561
            [self.person.email],
1562
            _('Your membership on project %(name)s has been removed.') % self.project.definition.__dict__,
1563
            _('Your membership on project %(name)s has been removed.') % self.project.definition.__dict__
1564
        ).send()
1565
        self.sync()
1566
    
1567
    def leave(self):
1568
        leave_policy = self.project.application.definition.member_leave_policy
1569
        if leave_policy == get_auto_accept_leave():
1570
            self.remove()
1571
        else:
1572
            self.leave_request_date = datetime.now()
1573
            self.save()
1574

    
1575
    def sync(self):
1576
        # set membership_dirty flag
1577
        self.project.membership_dirty = True
1578
        self.project.save()
1579
        
1580
        rejected = self.project.sync(specific_members=[self.person])
1581
        if not rejected:
1582
            # if syncing was successful unset membership_dirty flag
1583
            self.membership_dirty = False
1584
            self.project.save()
1585
        
1586

    
1587
class ProjectMembershipHistory(models.Model):
1588
    person = models.ForeignKey(AstakosUser)
1589
    project = models.ForeignKey(Project)
1590
    request_date = models.DateField(default=datetime.now())
1591
    removal_date = models.DateField(null=True)
1592
    rejection_date = models.DateField(null=True)
1593

    
1594

    
1595
def filter_queryset_by_property(q, property):
1596
    """
1597
    Incorporate list comprehension for filtering querysets by property
1598
    since Queryset.filter() operates on the database level.
1599
    """
1600
    return (p for p in q if getattr(p, property, False))
1601

    
1602
def get_alive_projects():
1603
    return filter_queryset_by_property(
1604
        Project.objects.all(),
1605
        'is_alive'
1606
    )
1607

    
1608
def get_active_projects():
1609
    return filter_queryset_by_property(
1610
        Project.objects.all(),
1611
        'is_active'
1612
    )
1613

    
1614
def _create_object(model, **kwargs):
1615
    o = model.objects.create(**kwargs)
1616
    o.save()
1617
    return o
1618

    
1619

    
1620
def create_astakos_user(u):
1621
    try:
1622
        AstakosUser.objects.get(user_ptr=u.pk)
1623
    except AstakosUser.DoesNotExist:
1624
        extended_user = AstakosUser(user_ptr_id=u.pk)
1625
        extended_user.__dict__.update(u.__dict__)
1626
        extended_user.save()
1627
        if not extended_user.has_auth_provider('local'):
1628
            extended_user.add_auth_provider('local')
1629
    except BaseException, e:
1630
        logger.exception(e)
1631

    
1632

    
1633
def fix_superusers(sender, **kwargs):
1634
    # Associate superusers with AstakosUser
1635
    admins = User.objects.filter(is_superuser=True)
1636
    for u in admins:
1637
        create_astakos_user(u)
1638
post_syncdb.connect(fix_superusers)
1639

    
1640

    
1641
def user_post_save(sender, instance, created, **kwargs):
1642
    if not created:
1643
        return
1644
    create_astakos_user(instance)
1645
post_save.connect(user_post_save, sender=User)
1646

    
1647

    
1648
def astakosuser_pre_save(sender, instance, **kwargs):
1649
    instance.aquarium_report = False
1650
    instance.new = False
1651
    try:
1652
        db_instance = AstakosUser.objects.get(id=instance.id)
1653
    except AstakosUser.DoesNotExist:
1654
        # create event
1655
        instance.aquarium_report = True
1656
        instance.new = True
1657
    else:
1658
        get = AstakosUser.__getattribute__
1659
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1660
                   BILLING_FIELDS)
1661
        instance.aquarium_report = True if l else False
1662
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1663

    
1664
def set_default_group(user):
1665
    try:
1666
        default = AstakosGroup.objects.get(name='default')
1667
        Membership(
1668
            group=default, person=user, date_joined=datetime.now()).save()
1669
    except AstakosGroup.DoesNotExist, e:
1670
        logger.exception(e)
1671

    
1672

    
1673
def astakosuser_post_save(sender, instance, created, **kwargs):
1674
    if instance.aquarium_report:
1675
        report_user_event(instance, create=instance.new)
1676
    if not created:
1677
        return
1678
    set_default_group(instance)
1679
    # TODO handle socket.error & IOError
1680
    register_users((instance,))
1681
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1682

    
1683

    
1684
def resource_post_save(sender, instance, created, **kwargs):
1685
    if not created:
1686
        return
1687
    register_resources((instance,))
1688
post_save.connect(resource_post_save, sender=Resource)
1689

    
1690

    
1691
def on_quota_disturbed(sender, users, **kwargs):
1692
#     print '>>>', locals()
1693
    if not users:
1694
        return
1695
    send_quota(users)
1696

    
1697
quota_disturbed = Signal(providing_args=["users"])
1698
quota_disturbed.connect(on_quota_disturbed)
1699

    
1700

    
1701
def send_quota_disturbed(sender, instance, **kwargs):
1702
    users = []
1703
    extend = users.extend
1704
    if sender == Membership:
1705
        if not instance.group.is_enabled:
1706
            return
1707
        extend([instance.person])
1708
    elif sender == AstakosUserQuota:
1709
        extend([instance.user])
1710
    elif sender == AstakosGroupQuota:
1711
        if not instance.group.is_enabled:
1712
            return
1713
        extend(instance.group.astakosuser_set.all())
1714
    elif sender == AstakosGroup:
1715
        if not instance.is_enabled:
1716
            return
1717
    quota_disturbed.send(sender=sender, users=users)
1718
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1719
post_delete.connect(send_quota_disturbed, sender=Membership)
1720
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1721
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1722
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1723
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1724

    
1725

    
1726
def renew_token(sender, instance, **kwargs):
1727
    if not instance.auth_token:
1728
        instance.renew_token()
1729
pre_save.connect(renew_token, sender=AstakosUser)
1730
pre_save.connect(renew_token, sender=Service)
1731

    
1732

    
1733
def check_closed_join_membership_policy(sender, instance, **kwargs):
1734
    if instance.id:
1735
        return
1736
    join_policy = instance.project.application.definition.member_join_policy
1737
    if join_policy == get_closed_join():
1738
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1739
pre_save.connect(check_closed_join_membership_policy, sender=ProjectMembership)
1740

    
1741

    
1742
def check_auto_accept_join_membership_policy(sender, instance, created, **kwargs):
1743
    if not created:
1744
        return
1745
    join_policy = instance.project.application.definition.member_join_policy
1746
    if join_policy == get_auto_accept_join():
1747
        instance.accept()
1748
post_save.connect(check_auto_accept_join_membership_policy, sender=ProjectMembership)