Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (59.1 kB)

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

    
34
import hashlib
35
import uuid
36
import logging
37

    
38
from time import asctime
39
from datetime import datetime, timedelta
40
from base64 import b64encode
41
from urlparse import urlparse
42
from urllib import quote
43
from random import randint
44
from collections import defaultdict
45

    
46
from django.db import models, IntegrityError
47
from django.contrib.auth.models import User, UserManager, Group, Permission
48
from django.utils.translation import ugettext as _
49
from django.db import transaction
50
from django.core.exceptions import ValidationError
51
from django.db.models.signals import (
52
    pre_save, post_save, post_syncdb, post_delete
53
)
54
from django.contrib.contenttypes.models import ContentType
55

    
56
from django.dispatch import Signal
57
from django.db.models import Q
58
from django.core.urlresolvers import reverse
59
from django.utils.http import int_to_base36
60
from django.contrib.auth.tokens import default_token_generator
61
from django.conf import settings
62
from django.utils.importlib import import_module
63
from django.core.validators import email_re
64
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
    SITENAME, SERVICES,
73
    PROJECT_CREATION_SUBJECT, PROJECT_APPROVED_SUBJECT,
74
    PROJECT_TERMINATION_SUBJECT, PROJECT_SUSPENSION_SUBJECT,
75
    PROJECT_MEMBERSHIP_CHANGE_SUBJECT
76
)
77
from astakos.im.endpoints.qh import (
78
    register_users, send_quota, register_resources
79
)
80
from astakos.im import auth_providers
81
from astakos.im.endpoints.aquarium.producer import report_user_event
82
from astakos.im.functions import send_invitation
83
#from astakos.im.tasks import propagate_groupmembers_quota
84

    
85
from astakos.im.notifications import build_notification
86

    
87
import astakos.im.messages as astakos_messages
88

    
89
logger = logging.getLogger(__name__)
90

    
91
DEFAULT_CONTENT_TYPE = None
92
_content_type = None
93

    
94
PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
95

    
96
def get_content_type():
97
    global _content_type
98
    if _content_type is not None:
99
        return _content_type
100

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

    
108
RESOURCE_SEPARATOR = '.'
109

    
110
inf = float('inf')
111

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

    
122
    def renew_token(self):
123
        md5 = hashlib.md5()
124
        md5.update(self.name.encode('ascii', 'ignore'))
125
        md5.update(self.url.encode('ascii', 'ignore'))
126
        md5.update(asctime())
127

    
128
        self.auth_token = b64encode(md5.digest())
129
        self.auth_token_created = datetime.now()
130
        self.auth_token_expires = self.auth_token_created + \
131
            timedelta(hours=AUTH_TOKEN_DURATION)
132

    
133
    def __str__(self):
134
        return self.name
135

    
136
    @property
137
    def resources(self):
138
        return self.resource_set.all()
139

    
140
    @resources.setter
141
    def resources(self, resources):
142
        for s in resources:
143
            self.resource_set.create(**s)
144

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

    
156

    
157
class ResourceMetadata(models.Model):
158
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
159
    value = models.CharField(_('Value'), max_length=255)
160

    
161

    
162
class Resource(models.Model):
163
    name = models.CharField(_('Name'), max_length=255)
164
    meta = models.ManyToManyField(ResourceMetadata)
165
    service = models.ForeignKey(Service)
166
    desc = models.TextField(_('Description'), null=True)
167
    unit = models.CharField(_('Name'), null=True, max_length=255)
168
    group = models.CharField(_('Group'), null=True, max_length=255)
169
    
170
    class Meta:
171
        unique_together = ("name", "service")
172

    
173
    def __str__(self):
174
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
175

    
176

    
177
class GroupKind(models.Model):
178
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
179

    
180
    def __str__(self):
181
        return self.name
182

    
183

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

    
227
    @property
228
    def is_disabled(self):
229
        if not self.approval_date:
230
            return True
231
        return False
232

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

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

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

    
266
    def approve_member(self, person):
267
        m, created = self.membership_set.get_or_create(person=person)
268
        m.approve()
269

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

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

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

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

    
300
    @property
301
    def policies(self):
302
        return self.astakosgroupquota_set.select_related().all()
303

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

    
313
    @property
314
    def owners(self):
315
        return self.owner.all()
316

    
317
    @property
318
    def owner_details(self):
319
        return self.owner.select_related().all()
320

    
321
    @owners.setter
322
    def owners(self, l):
323
        self.owner = l
324
        map(self.approve_member, l)
325

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

    
340
class AstakosUserManager(UserManager):
341

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

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

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

    
367

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

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

    
380
    updated = models.DateTimeField(_('Update date'))
381
    is_verified = models.BooleanField(_('Is verified?'), default=False)
382

    
383
    email_verified = models.BooleanField(_('Email verified?'), default=False)
384

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

    
391
    activation_sent = models.DateTimeField(
392
        _('Activation sent data'), null=True, blank=True)
393

    
394
    policy = models.ManyToManyField(
395
        Resource, null=True, through='AstakosUserQuota')
396

    
397
    astakos_groups = models.ManyToManyField(
398
        AstakosGroup, verbose_name=_('agroups'), blank=True,
399
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
400
        through='Membership')
401

    
402
    __has_signed_terms = False
403
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
404
                                           default=False, db_index=True)
405

    
406
    objects = AstakosUserManager()
407

    
408
    owner = models.ManyToManyField(
409
        AstakosGroup, related_name='owner', null=True)
410

    
411
    class Meta:
412
        unique_together = ("provider", "third_party_identifier")
413

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

    
420
    @property
421
    def realname(self):
422
        return '%s %s' % (self.first_name, self.last_name)
423

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

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

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

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

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

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

    
483
    @property
484
    def policies(self):
485
        return self.astakosuserquota_set.select_related().all()
486

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

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

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

    
512
    @property
513
    def extended_groups(self):
514
        return self.membership_set.select_related().all()
515

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

    
523
    def save(self, update_timestamps=True, **kwargs):
524
        if update_timestamps:
525
            if not self.id:
526
                self.date_joined = datetime.now()
527
            self.updated = datetime.now()
528

    
529
        # update date_signed_terms if necessary
530
        if self.__has_signed_terms != self.has_signed_terms:
531
            self.date_signed_terms = datetime.now()
532

    
533
        if not self.id:
534
            # set username
535
            self.username = self.email
536

    
537
        self.validate_unique_email_isactive()
538
        if self.is_active and self.activation_sent:
539
            # reset the activation sent
540
            self.activation_sent = None
541

    
542
        super(AstakosUser, self).save(**kwargs)
543

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

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

    
560
    def flush_sessions(self, current_key=None):
561
        q = self.sessions
562
        if current_key:
563
            q = q.exclude(session_key=current_key)
564

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

    
574
    def __unicode__(self):
575
        return '%s (%s)' % (self.realname, self.email)
576

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

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

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

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

    
620
    def can_login_with_auth_provider(self, provider):
621
        if not self.has_auth_provider(provider):
622
            return False
623
        else:
624
            return auth_providers.get_provider(provider).is_available_for_login()
625

    
626
    def can_add_auth_provider(self, provider, **kwargs):
627
        provider_settings = auth_providers.get_provider(provider)
628
        if not provider_settings.is_available_for_login():
629
            return False
630

    
631
        if self.has_auth_provider(provider) and \
632
           provider_settings.one_per_user:
633
            return False
634

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

    
645
        return True
646

    
647
    def can_remove_auth_provider(self, provider):
648
        if len(self.get_active_auth_providers()) <= 1:
649
            return False
650
        return True
651

    
652
    def can_change_password(self):
653
        return self.has_auth_provider('local', auth_backend='astakos')
654

    
655
    def has_auth_provider(self, provider, **kwargs):
656
        return bool(self.auth_providers.filter(module=provider,
657
                                               **kwargs).count())
658

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

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

    
673
        provider = self.add_auth_provider(pending.provider,
674
                               identifier=pending.third_party_identifier)
675

    
676
        if email_re.match(pending.email or '') and pending.email != self.email:
677
            self.additionalmail_set.get_or_create(email=pending.email)
678

    
679
        pending.delete()
680
        return provider
681

    
682
    def remove_auth_provider(self, provider, **kwargs):
683
        self.auth_providers.get(module=provider, **kwargs).delete()
684

    
685
    # user urls
686
    def get_resend_activation_url(self):
687
        return reverse('send_activation', {'user_id': self.pk})
688

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

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

    
701
    def get_auth_providers(self):
702
        return self.auth_providers.all()
703

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

    
713
        return providers
714

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

    
722
    @property
723
    def auth_providers_display(self):
724
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
725

    
726

    
727
class AstakosUserAuthProviderManager(models.Manager):
728

    
729
    def active(self):
730
        return self.filter(active=True)
731

    
732

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

    
749
    objects = AstakosUserAuthProviderManager()
750

    
751
    class Meta:
752
        unique_together = (('identifier', 'module', 'user'), )
753

    
754
    @property
755
    def settings(self):
756
        return auth_providers.get_provider(self.module)
757

    
758
    @property
759
    def details_display(self):
760
        return self.settings.details_tpl % self.__dict__
761

    
762
    def can_remove(self):
763
        return self.user.can_remove_auth_provider(self.module)
764

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

    
772
    def __repr__(self):
773
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
774

    
775
    def __unicode__(self):
776
        if self.identifier:
777
            return "%s:%s" % (self.module, self.identifier)
778
        if self.auth_backend:
779
            return "%s:%s" % (self.module, self.auth_backend)
780
        return self.module
781

    
782

    
783

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

    
790
    class Meta:
791
        unique_together = ("person", "group")
792

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

    
799
    @property
800
    def is_approved(self):
801
        if self.date_joined:
802
            return True
803
        return False
804

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

    
815
    def disapprove(self):
816
        approved = self.is_approved()
817
        self.delete()
818
        if approved:
819
            quota_disturbed.send(sender=self, users=(self.person,))
820

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

    
848
    update_or_create = _update_or_create
849

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

    
857
    class Meta:
858
        unique_together = ("resource", "group")
859

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

    
867
    class Meta:
868
        unique_together = ("resource", "user")
869

    
870

    
871
class ApprovalTerms(models.Model):
872
    """
873
    Model for approval terms
874
    """
875

    
876
    date = models.DateTimeField(
877
        _('Issue date'), db_index=True, default=datetime.now())
878
    location = models.CharField(_('Terms location'), max_length=255)
879

    
880

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

    
894
    def __init__(self, *args, **kwargs):
895
        super(Invitation, self).__init__(*args, **kwargs)
896
        if not self.id:
897
            self.code = _generate_invitation_code()
898

    
899
    def consume(self):
900
        self.is_consumed = True
901
        self.consumed = datetime.now()
902
        self.save()
903

    
904
    def __unicode__(self):
905
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
906

    
907

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

915
        If the key is valid and has not expired, return the ``User``
916
        after activating.
917

918
        If the key is not valid or has expired, return ``None``.
919

920
        If the key is valid but the ``User`` is already active,
921
        return ``None``.
922

923
        After successful email change the activation record is deleted.
924

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

    
949

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

    
959
    objects = EmailChangeManager()
960

    
961
    def activation_key_expired(self):
962
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
963
        return self.requested_at + expiration_date < datetime.now()
964

    
965

    
966
class AdditionalMail(models.Model):
967
    """
968
    Model for registring invitations
969
    """
970
    owner = models.ForeignKey(AstakosUser)
971
    email = models.EmailField()
972

    
973

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

    
983

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

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

    
1006
    class Meta:
1007
        unique_together = ("provider", "third_party_identifier")
1008

    
1009
    @property
1010
    def realname(self):
1011
        return '%s %s' %(self.first_name, self.last_name)
1012

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

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

    
1033
    def generate_token(self):
1034
        self.password = self.third_party_identifier
1035
        self.last_login = datetime.now()
1036
        self.token = default_token_generator.make_token(self)
1037

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

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

    
1046
    def __str__(self):
1047
        return self.policy
1048

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

    
1053
    def __str__(self):
1054
        return self.policy
1055

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

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

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

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

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

    
1137
    @property
1138
    def resource_policies(self):
1139
        return self.projectresourcegrant_set.all()
1140

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

    
1165

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

    
1173
    class Meta:
1174
        unique_together = ("resource", "project_definition")
1175

    
1176

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

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

    
1207
    def save(self):
1208
        self.definition.save()
1209
        self.definition = self.definition
1210
        super(ProjectApplication, self).save()
1211

    
1212

    
1213
    @staticmethod
1214
    def submit(definition, resource_policies, applicant, comments,
1215
               precursor_application=None):
1216

    
1217
        application = ProjectApplication()
1218
        if precursor_application:
1219
            application.precursor_application = precursor_application
1220
            application.owner = precursor_application.owner
1221
        else:
1222
            application.owner = applicant
1223

    
1224
        application.definition = definition
1225
        application.definition.id = None
1226
        application.applicant = applicant
1227
        application.comments = comments
1228
        application.issue_date = datetime.now()
1229
        application.state = PENDING
1230
        application.save()
1231
        application.definition.resource_policies = resource_policies
1232

    
1233
        notification = build_notification(
1234
            settings.SERVER_EMAIL,
1235
            [i[1] for i in settings.ADMINS],
1236
            _(PROJECT_CREATION_SUBJECT) % application.definition.__dict__,
1237
            template='im/projects/project_creation_notification.txt',
1238
            dictionary={'object':application}
1239
        )
1240
        notification.send()
1241
        return application
1242

    
1243
    def approve(self, approval_user=None):
1244
        """
1245
        If approval_user then during owner membership acceptance
1246
        it is checked whether the request_user is eligible.
1247

1248
        Raises:
1249
            ValidationError: if there is other alive project with the same name
1250

1251
        """
1252
        try:
1253
            self.definition.validate_name()
1254
        except ValidationError, e:
1255
            raise PermissionDenied(e.messages[0])
1256
        if self.state != PENDING:
1257
            raise PermissionDenied(_(PROJECT_ALREADY_ACTIVE))
1258

    
1259
        now = datetime.now()
1260
        precursor = self.precursor_application
1261
        try:
1262
            project = precursor.project
1263
        except:
1264
            project = Project()
1265
            project.creation_date = now
1266
            project.accept_member(self.owner, approval_user)
1267

    
1268
        project.last_application_approved = self
1269
        project.last_approval_date = now
1270
        project.save()
1271

    
1272
        p = precursor
1273
        while p:
1274
            p.state = REPLACED
1275
            p.save()
1276
            p = p.precursor_application
1277

    
1278
        self.state = APPROVED
1279
        self.save()
1280

    
1281
        transaction.commit()
1282

    
1283
        notification = build_notification(
1284
            settings.SERVER_EMAIL,
1285
            [self.owner.email],
1286
            _(PROJECT_APPROVED_SUBJECT) % self.definition.__dict__,
1287
            template='im/projects/project_approval_notification.txt',
1288
            dictionary={'object':self}
1289
        )
1290
        notification.send()
1291

    
1292
        rejected = self.project.sync()
1293
        if not rejected:
1294
            project.application = self
1295
            project.save()
1296

    
1297

    
1298
class Project(models.Model):
1299
    application = models.OneToOneField(
1300
        ProjectApplication, related_name='project', null=True, blank=True)
1301
    creation_date = models.DateTimeField()
1302
    last_approval_date = models.DateTimeField(null=True)
1303
    termination_start_date = models.DateTimeField(null=True)
1304
    termination_date = models.DateTimeField(null=True)
1305
    members = models.ManyToManyField(AstakosUser, through='ProjectMembership')
1306
    membership_dirty = models.BooleanField(default=False)
1307
    last_application_approved = models.OneToOneField(
1308
        ProjectApplication, related_name='last_project')
1309
    
1310
    
1311
    @property
1312
    def definition(self):
1313
        return self.application.definition
1314

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

    
1398
    def reject_member(self, user, request_user=None):
1399
        """
1400
        Raises:
1401
            django.exceptions.PermissionDenied
1402
            astakos.im.models.AstakosUser.DoesNotExist
1403
            astakos.im.models.ProjectMembership.DoesNotExist
1404
        """
1405
        if isinstance(user, int):
1406
            try:
1407
                user = lookup_object(AstakosUser, user, None, None)
1408
            except Http404:
1409
                raise AstakosUser.DoesNotExist()
1410
        m = ProjectMembership.objects.get(person=user, project=self)
1411
        m.reject()
1412
        
1413
    def remove_member(self, user, request_user=None):
1414
        """
1415
        Raises:
1416
            django.exceptions.PermissionDenied
1417
            astakos.im.models.AstakosUser.DoesNotExist
1418
            astakos.im.models.ProjectMembership.DoesNotExist
1419
        """
1420
        if isinstance(user, int):
1421
            try:
1422
                user = lookup_object(AstakosUser, user, None, None)
1423
            except Http404:
1424
                raise AstakosUser.DoesNotExist()
1425
        m = ProjectMembership.objects.get(person=user, project=self)
1426
        m.remove()
1427
    
1428
    def terminate(self):
1429
        self.termination_start_date = datetime.now()
1430
        self.terminaton_date = None
1431
        self.save()
1432
        
1433
        rejected = self.sync()
1434
        if not rejected:
1435
            self.termination_start_date = None
1436
            self.termination_date = datetime.now()
1437
            self.save()
1438
            
1439
        notification = build_notification(
1440
            settings.SERVER_EMAIL,
1441
            [self.application.owner.email],
1442
            _(PROJECT_TERMINATION_SUBJECT) % self.definition.__dict__,
1443
            template='im/projects/project_termination_notification.txt',
1444
            dictionary={'object':self.application}
1445
        )
1446
        notification.send()
1447

    
1448
    def suspend(self):
1449
        self.last_approval_date = None
1450
        self.save()
1451
        self.sync()
1452
        notification = build_notification(
1453
            settings.SERVER_EMAIL,
1454
            [self.application.owner.email],
1455
            _(PROJECT_SUSPENSION_SUBJECT) % self.definition.__dict__,
1456
            template='im/projects/project_suspension_notification.txt',
1457
            dictionary={'object':self.application}
1458
        )
1459
        notification.send()
1460

    
1461
class ProjectMembership(models.Model):
1462
    person = models.ForeignKey(AstakosUser)
1463
    project = models.ForeignKey(Project)
1464
    request_date = models.DateField(default=datetime.now())
1465
    acceptance_date = models.DateField(null=True, db_index=True)
1466
    leave_request_date = models.DateField(null=True)
1467

    
1468
    class Meta:
1469
        unique_together = ("person", "project")
1470

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

    
1569
    def sync(self):
1570
        # set membership_dirty flag
1571
        self.project.membership_dirty = True
1572
        self.project.save()
1573
        
1574
        rejected = self.project.sync(specific_members=[self.person])
1575
        if not rejected:
1576
            # if syncing was successful unset membership_dirty flag
1577
            self.membership_dirty = False
1578
            self.project.save()
1579
        
1580

    
1581
class ProjectMembershipHistory(models.Model):
1582
    person = models.ForeignKey(AstakosUser)
1583
    project = models.ForeignKey(Project)
1584
    request_date = models.DateField(default=datetime.now())
1585
    removal_date = models.DateField(null=True)
1586
    rejection_date = models.DateField(null=True)
1587

    
1588

    
1589
def filter_queryset_by_property(q, property):
1590
    """
1591
    Incorporate list comprehension for filtering querysets by property
1592
    since Queryset.filter() operates on the database level.
1593
    """
1594
    return (p for p in q if getattr(p, property, False))
1595

    
1596
def get_alive_projects():
1597
    return filter_queryset_by_property(
1598
        Project.objects.all(),
1599
        'is_alive'
1600
    )
1601

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

    
1608
def _create_object(model, **kwargs):
1609
    o = model.objects.create(**kwargs)
1610
    o.save()
1611
    return o
1612

    
1613

    
1614
def create_astakos_user(u):
1615
    try:
1616
        AstakosUser.objects.get(user_ptr=u.pk)
1617
    except AstakosUser.DoesNotExist:
1618
        extended_user = AstakosUser(user_ptr_id=u.pk)
1619
        extended_user.__dict__.update(u.__dict__)
1620
        extended_user.save()
1621
        if not extended_user.has_auth_provider('local'):
1622
            extended_user.add_auth_provider('local')
1623
    except BaseException, e:
1624
        logger.exception(e)
1625

    
1626

    
1627
def fix_superusers(sender, **kwargs):
1628
    # Associate superusers with AstakosUser
1629
    admins = User.objects.filter(is_superuser=True)
1630
    for u in admins:
1631
        create_astakos_user(u)
1632
post_syncdb.connect(fix_superusers)
1633

    
1634

    
1635
def user_post_save(sender, instance, created, **kwargs):
1636
    if not created:
1637
        return
1638
    create_astakos_user(instance)
1639
post_save.connect(user_post_save, sender=User)
1640

    
1641

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

    
1658
def set_default_group(user):
1659
    try:
1660
        default = AstakosGroup.objects.get(name='default')
1661
        Membership(
1662
            group=default, person=user, date_joined=datetime.now()).save()
1663
    except AstakosGroup.DoesNotExist, e:
1664
        logger.exception(e)
1665

    
1666

    
1667
def astakosuser_post_save(sender, instance, created, **kwargs):
1668
    if instance.aquarium_report:
1669
        report_user_event(instance, create=instance.new)
1670
    if not created:
1671
        return
1672
    set_default_group(instance)
1673
    # TODO handle socket.error & IOError
1674
    register_users((instance,))
1675
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1676

    
1677

    
1678
def resource_post_save(sender, instance, created, **kwargs):
1679
    if not created:
1680
        return
1681
    register_resources((instance,))
1682
post_save.connect(resource_post_save, sender=Resource)
1683

    
1684

    
1685
def on_quota_disturbed(sender, users, **kwargs):
1686
#     print '>>>', locals()
1687
    if not users:
1688
        return
1689
    send_quota(users)
1690

    
1691
quota_disturbed = Signal(providing_args=["users"])
1692
quota_disturbed.connect(on_quota_disturbed)
1693

    
1694

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

    
1719

    
1720
def renew_token(sender, instance, **kwargs):
1721
    if not instance.auth_token:
1722
        instance.renew_token()
1723
pre_save.connect(renew_token, sender=AstakosUser)
1724
pre_save.connect(renew_token, sender=Service)
1725

    
1726

    
1727
def check_closed_join_membership_policy(sender, instance, **kwargs):
1728
    if instance.id:
1729
        return
1730
    if instance.person == instance.project.application.owner:
1731
        return
1732
    join_policy = instance.project.application.definition.member_join_policy
1733
    if join_policy == get_closed_join():
1734
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1735
pre_save.connect(check_closed_join_membership_policy, sender=ProjectMembership)
1736

    
1737

    
1738
def check_auto_accept_join_membership_policy(sender, instance, created, **kwargs):
1739
    if not created:
1740
        return
1741
    join_policy = instance.project.application.definition.member_join_policy
1742
    if join_policy == get_auto_accept_join() and not instance.acceptance_date:
1743
        instance.accept()
1744
post_save.connect(check_auto_accept_join_membership_policy, sender=ProjectMembership)