Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (59.9 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.current_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.current_application.id != self.projectapplication.id, q)
1157
        if self.projectapplication.precursor_application:
1158
            q = filter(lambda p: p.current_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
        try:
1234
            notification = build_notification(
1235
                settings.SERVER_EMAIL,
1236
                [i[1] for i in settings.ADMINS],
1237
                _(PROJECT_CREATION_SUBJECT) % application.definition.__dict__,
1238
                template='im/projects/project_creation_notification.txt',
1239
                dictionary={'object':application}
1240
            ).send()
1241
        except NotificationError, e:
1242
            logger.error(e.messages)
1243

    
1244
        return application
1245

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

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

    
1261
        now = datetime.now()
1262
        precursor = self.precursor_application
1263
        try:
1264
            project = precursor.project
1265
        except:
1266
            project = Project()
1267
            project.creation_date = now
1268

    
1269
        project.last_application_approved = self
1270
        project.last_approval_date = now
1271
        project.save()
1272
        project.accept_member(self.owner, approval_user)
1273

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

    
1280
        self.state = APPROVED
1281
        self.save()
1282

    
1283
        if transaction.is_managed():
1284
            transaction.commit()
1285

    
1286
        rejected = self.project.sync()
1287
        
1288
        try:
1289
            notification = build_notification(
1290
                settings.SERVER_EMAIL,
1291
                [self.owner.email],
1292
                _(PROJECT_APPROVED_SUBJECT) % self.definition.__dict__,
1293
                template='im/projects/project_approval_notification.txt',
1294
                dictionary={'object':self}
1295
            ).send()
1296
        except NotificationError, e:
1297
            logger.error(e.messages)
1298

    
1299

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

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

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

    
1458
    def suspend(self):
1459
        self.last_approval_date = None
1460
        self.save()
1461
        self.sync()
1462

    
1463
        try:
1464
            notification = build_notification(
1465
                settings.SERVER_EMAIL,
1466
                [self.current_application.owner.email],
1467
                _(PROJECT_SUSPENSION_SUBJECT) % self.definition.__dict__,
1468
                template='im/projects/project_suspension_notification.txt',
1469
                dictionary={'object':self.current_application}
1470
            ).send()
1471
        except NotificationError, e:
1472
            logger.error(e.messages)
1473

    
1474
class ProjectMembership(models.Model):
1475
    person = models.ForeignKey(AstakosUser)
1476
    project = models.ForeignKey(Project)
1477
    request_date = models.DateField(default=datetime.now())
1478
    acceptance_date = models.DateField(null=True, db_index=True)
1479
    leave_request_date = models.DateField(null=True)
1480

    
1481
    class Meta:
1482
        unique_together = ("person", "project")
1483

    
1484
    def accept(self, delete_on_failure=False, request_user=None):
1485
        """
1486
            Raises:
1487
                django.exception.PermissionDenied
1488
        """
1489
        try:
1490
            if request_user and \
1491
                (not self.project.current_application.owner == request_user and \
1492
                    not request_user.is_superuser):
1493
                raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1494
            if not self.project.is_alive:
1495
                raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % self.project.__dict__)
1496
            if len(self.project.approved_members) + 1 > self.project.definition.limit_on_members_number:
1497
                raise PermissionDenied(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
1498
        except PermissionDenied, e:
1499
            if delete_on_failure:
1500
                self.delete()
1501
            raise
1502
        if self.acceptance_date:
1503
            return
1504
        self.acceptance_date = datetime.now()
1505
        self.save()
1506
        self.sync()
1507

    
1508
        try:
1509
            notification = build_notification(
1510
                settings.SERVER_EMAIL,
1511
                [self.person.email],
1512
                _(PROJECT_MEMBERSHIP_CHANGE_SUBJECT) % self.project.definition.__dict__,
1513
                template='im/projects/project_membership_change_notification.txt',
1514
                dictionary={'object':self.project.current_application, 'action':'accepted'}
1515
            ).send()
1516
        except NotificationError, e:
1517
            logger.error(e.messages)
1518
    
1519
    def reject(self, request_user=None):
1520
        """
1521
            Raises:
1522
                django.exception.PermissionDenied
1523
        """
1524
        if request_user and \
1525
            (not self.project.current_application.owner == request_user and \
1526
                not request_user.is_superuser):
1527
            raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1528
        if not self.project.is_alive:
1529
            raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1530
        history_item = ProjectMembershipHistory(
1531
            person=self.person,
1532
            project=self.project,
1533
            request_date=self.request_date,
1534
            rejection_date=datetime.now()
1535
        )
1536
        self.delete()
1537
        history_item.save()
1538

    
1539
        try:
1540
            notification = build_notification(
1541
                settings.SERVER_EMAIL,
1542
                [self.person.email],
1543
                _(PROJECT_MEMBERSHIP_CHANGE_SUBJECT) % self.project.definition.__dict__,
1544
                template='im/projects/project_membership_change_notification.txt',
1545
                dictionary={'object':self.project.current_application, 'action':'rejected'}
1546
            ).send()
1547
        except NotificationError, e:
1548
            logger.error(e.messages)
1549

    
1550
    def remove(self, request_user=None):
1551
        """
1552
            Raises:
1553
                django.exception.PermissionDenied
1554
        """
1555
        if request_user and \
1556
            (not self.project.current_application.owner == request_user and \
1557
                not request_user.is_superuser):
1558
            raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
1559
        if not self.project.is_alive:
1560
            raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % self.project.__dict__)
1561
        history_item = ProjectMembershipHistory(
1562
            id=self.id,
1563
            person=self.person,
1564
            project=self.project,
1565
            request_date=self.request_date,
1566
            removal_date=datetime.now()
1567
        )
1568
        self.delete()
1569
        history_item.save()
1570
        self.sync()
1571

    
1572
        try:
1573
            notification = build_notification(
1574
                settings.SERVER_EMAIL,
1575
                [self.person.email],
1576
                _(PROJECT_MEMBERSHIP_CHANGE_SUBJECT) % self.project.definition.__dict__,
1577
                template='im/projects/project_membership_change_notification.txt',
1578
                dictionary={'object':self.project.current_application, 'action':'removed'}
1579
            ).send()
1580
        except NotificationError, e:
1581
            logger.error(e.messages)
1582
    
1583
    def leave(self):
1584
        leave_policy = self.project.current_application.definition.member_leave_policy
1585
        if leave_policy == get_auto_accept_leave():
1586
            self.remove()
1587
        else:
1588
            self.leave_request_date = datetime.now()
1589
            self.save()
1590

    
1591
    def sync(self):
1592
        # set membership_dirty flag
1593
        self.project.membership_dirty = True
1594
        self.project.save()
1595
        
1596
        rejected = self.project.sync(specific_members=[self.person])
1597
        if not rejected:
1598
            # if syncing was successful unset membership_dirty flag
1599
            self.membership_dirty = False
1600
            self.project.save()
1601
        
1602

    
1603
class ProjectMembershipHistory(models.Model):
1604
    person = models.ForeignKey(AstakosUser)
1605
    project = models.ForeignKey(Project)
1606
    request_date = models.DateField(default=datetime.now())
1607
    removal_date = models.DateField(null=True)
1608
    rejection_date = models.DateField(null=True)
1609

    
1610

    
1611
def filter_queryset_by_property(q, property):
1612
    """
1613
    Incorporate list comprehension for filtering querysets by property
1614
    since Queryset.filter() operates on the database level.
1615
    """
1616
    return (p for p in q if getattr(p, property, False))
1617

    
1618
def get_alive_projects():
1619
    return filter_queryset_by_property(
1620
        Project.objects.all(),
1621
        'is_alive'
1622
    )
1623

    
1624
def get_active_projects():
1625
    return filter_queryset_by_property(
1626
        Project.objects.all(),
1627
        'is_active'
1628
    )
1629

    
1630
def _create_object(model, **kwargs):
1631
    o = model.objects.create(**kwargs)
1632
    o.save()
1633
    return o
1634

    
1635

    
1636
def create_astakos_user(u):
1637
    try:
1638
        AstakosUser.objects.get(user_ptr=u.pk)
1639
    except AstakosUser.DoesNotExist:
1640
        extended_user = AstakosUser(user_ptr_id=u.pk)
1641
        extended_user.__dict__.update(u.__dict__)
1642
        extended_user.save()
1643
        if not extended_user.has_auth_provider('local'):
1644
            extended_user.add_auth_provider('local')
1645
    except BaseException, e:
1646
        logger.exception(e)
1647

    
1648

    
1649
def fix_superusers(sender, **kwargs):
1650
    # Associate superusers with AstakosUser
1651
    admins = User.objects.filter(is_superuser=True)
1652
    for u in admins:
1653
        create_astakos_user(u)
1654
post_syncdb.connect(fix_superusers)
1655

    
1656

    
1657
def user_post_save(sender, instance, created, **kwargs):
1658
    if not created:
1659
        return
1660
    create_astakos_user(instance)
1661
post_save.connect(user_post_save, sender=User)
1662

    
1663

    
1664
def astakosuser_pre_save(sender, instance, **kwargs):
1665
    instance.aquarium_report = False
1666
    instance.new = False
1667
    try:
1668
        db_instance = AstakosUser.objects.get(id=instance.id)
1669
    except AstakosUser.DoesNotExist:
1670
        # create event
1671
        instance.aquarium_report = True
1672
        instance.new = True
1673
    else:
1674
        get = AstakosUser.__getattribute__
1675
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1676
                   BILLING_FIELDS)
1677
        instance.aquarium_report = True if l else False
1678
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1679

    
1680
def set_default_group(user):
1681
    try:
1682
        default = AstakosGroup.objects.get(name='default')
1683
        Membership(
1684
            group=default, person=user, date_joined=datetime.now()).save()
1685
    except AstakosGroup.DoesNotExist, e:
1686
        logger.exception(e)
1687

    
1688

    
1689
def astakosuser_post_save(sender, instance, created, **kwargs):
1690
    if instance.aquarium_report:
1691
        report_user_event(instance, create=instance.new)
1692
    if not created:
1693
        return
1694
    set_default_group(instance)
1695
    # TODO handle socket.error & IOError
1696
    register_users((instance,))
1697
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1698

    
1699

    
1700
def resource_post_save(sender, instance, created, **kwargs):
1701
    if not created:
1702
        return
1703
    register_resources((instance,))
1704
post_save.connect(resource_post_save, sender=Resource)
1705

    
1706

    
1707
def on_quota_disturbed(sender, users, **kwargs):
1708
#     print '>>>', locals()
1709
    if not users:
1710
        return
1711
    send_quota(users)
1712

    
1713
quota_disturbed = Signal(providing_args=["users"])
1714
quota_disturbed.connect(on_quota_disturbed)
1715

    
1716

    
1717
def send_quota_disturbed(sender, instance, **kwargs):
1718
    users = []
1719
    extend = users.extend
1720
    if sender == Membership:
1721
        if not instance.group.is_enabled:
1722
            return
1723
        extend([instance.person])
1724
    elif sender == AstakosUserQuota:
1725
        extend([instance.user])
1726
    elif sender == AstakosGroupQuota:
1727
        if not instance.group.is_enabled:
1728
            return
1729
        extend(instance.group.astakosuser_set.all())
1730
    elif sender == AstakosGroup:
1731
        if not instance.is_enabled:
1732
            return
1733
    quota_disturbed.send(sender=sender, users=users)
1734
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1735
post_delete.connect(send_quota_disturbed, sender=Membership)
1736
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1737
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1738
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1739
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1740

    
1741

    
1742
def renew_token(sender, instance, **kwargs):
1743
    if not instance.auth_token:
1744
        instance.renew_token()
1745
pre_save.connect(renew_token, sender=AstakosUser)
1746
pre_save.connect(renew_token, sender=Service)
1747

    
1748

    
1749
def check_closed_join_membership_policy(sender, instance, **kwargs):
1750
    if instance.id:
1751
        return
1752
    if instance.person == instance.project.current_application.owner:
1753
        return
1754
    join_policy = instance.project.current_application.definition.member_join_policy
1755
    if join_policy == get_closed_join():
1756
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1757
pre_save.connect(check_closed_join_membership_policy, sender=ProjectMembership)
1758

    
1759

    
1760
def check_auto_accept_join_membership_policy(sender, instance, created, **kwargs):
1761
    if not created:
1762
        return
1763
    join_policy = instance.project.current_application.definition.member_join_policy
1764
    if join_policy == get_auto_accept_join() and not instance.acceptance_date:
1765
        instance.accept()
1766
post_save.connect(check_auto_accept_join_membership_policy, sender=ProjectMembership)