Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 9f01cf1d

History | View | Annotate | Download (58.6 kB)

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

    
34
import hashlib
35
import uuid
36
import logging
37

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

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

    
56
from django.dispatch import Signal
57
from django.db.models import Q
58
from django.core.urlresolvers import reverse
59
from django.utils.http import int_to_base36
60
from django.contrib.auth.tokens import default_token_generator
61
from django.conf import settings
62
from django.utils.importlib import import_module
63
from django.core.validators import email_re
64
from django.core.exceptions import PermissionDenied
65
from django.views.generic.create_update import lookup_object
66
from django.core.exceptions import ObjectDoesNotExist
67

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

    
82
from astakos.im.notifications import build_notification
83

    
84
import astakos.im.messages as astakos_messages
85

    
86
logger = logging.getLogger(__name__)
87

    
88
DEFAULT_CONTENT_TYPE = None
89
_content_type = None
90

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

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

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

    
105
RESOURCE_SEPARATOR = '.'
106

    
107
inf = float('inf')
108

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

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

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

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

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

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

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

    
153

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

    
158

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

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

    
170

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

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

    
177

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
320

    
321

    
322
class AstakosUserManager(UserManager):
323

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

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

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

    
349

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

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

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

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

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

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

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

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

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

    
388
    objects = AstakosUserManager()
389

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
625
        return True
626

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

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

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

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

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

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

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

    
659
        pending.delete()
660
        return provider
661

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

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

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

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

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

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

    
693
        return providers
694

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

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

    
706

    
707
class AstakosUserAuthProviderManager(models.Manager):
708

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

    
712

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

    
729
    objects = AstakosUserAuthProviderManager()
730

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

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

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

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

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

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

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

    
762

    
763

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

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

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

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

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

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

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

    
828
    update_or_create = _update_or_create
829

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

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

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

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

    
850

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

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

    
860

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

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

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

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

    
887

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

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

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

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

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

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

    
929

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

    
939
    objects = EmailChangeManager()
940

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

    
945

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

    
953

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

    
963

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1121
    @property
1122
    def resource_policies(self):
1123
        return self.projectresourcegrant_set.all()
1124

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

    
1149

    
1150
class ProjectResourceGrant(models.Model):
1151
    objects = ExtendedManager()
1152
    member_limit = models.BigIntegerField(null=True)
1153
    project_limit = models.BigIntegerField(null=True)
1154
    resource = models.ForeignKey(Resource)
1155
    project_definition = models.ForeignKey(ProjectDefinition, blank=True)
1156

    
1157
    class Meta:
1158
        unique_together = ("resource", "project_definition")
1159

    
1160

    
1161
class ProjectApplication(models.Model):
1162
    states_list = [PENDING, APPROVED, REPLACED, UNKNOWN]
1163
    states = dict((k, v) for k, v in enumerate(states_list))
1164

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

    
1191
    def save(self):
1192
        self.definition.save()
1193
        self.definition = self.definition
1194
        super(ProjectApplication, self).save()
1195

    
1196

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

    
1238
        if create:
1239
            kwargs = {
1240
                'application':self,
1241
                'creation_date':datetime.now(),
1242
                'last_approval_date':datetime.now(),
1243
            }
1244
            project = _create_object(Project, **kwargs)
1245
            project.accept_member(self.owner, approval_user)
1246
        else:
1247
            project = self.precursor_application.project
1248
            project.application = self
1249
            project.last_approval_date = datetime.now()
1250
            project.save()
1251
            self.precursor_application.state = REPLACED
1252
        self.state = APPROVED
1253
        self.save()
1254

    
1255
        notification = build_notification(
1256
            settings.SERVER_EMAIL,
1257
            [self.owner.email],
1258
            _('Project application has been approved on %s alpha2 testing' % SITENAME),
1259
            _('Your application request %(id)s has been apporved.')
1260
        )
1261
        notification.send()
1262

    
1263
        rejected = self.project.sync()
1264
        if rejected:
1265
            # revert to precursor
1266
            project.application = app.precursor_application
1267
            if project.application:
1268
                project.last_approval_date = last_approval_date
1269
                project.save()
1270
            rejected = project.sync()
1271
            if rejected:
1272
                raise Exception(_(astakos_messages.QH_SYNC_ERROR))
1273
        else:
1274
            project.last_application_synced = app
1275
            project.save()
1276

    
1277

    
1278
class Project(models.Model):
1279
    application = models.OneToOneField(ProjectApplication, related_name='project')
1280
    creation_date = models.DateTimeField()
1281
    last_approval_date = models.DateTimeField(null=True)
1282
    termination_start_date = models.DateTimeField(null=True)
1283
    termination_date = models.DateTimeField(null=True)
1284
    members = models.ManyToManyField(AstakosUser, through='ProjectMembership')
1285
    membership_dirty = models.BooleanField(default=False)
1286
    last_application_synced = models.OneToOneField(
1287
        ProjectApplication, related_name='last_project', null=True, blank=True
1288
    )
1289
    
1290
    
1291
    @property
1292
    def definition(self):
1293
        return self.application.definition
1294

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

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

    
1425
    def suspend(self):
1426
        self.last_approval_date = None
1427
        self.save()
1428
        notification = build_notification(
1429
            settings.SERVER_EMAIL,
1430
            [self.application.owner.email],
1431
            _('Project %(name)s has been suspended.') %  self.definition.__dict__,
1432
            _('Project %(name)s has been suspended.') %  self.definition.__dict__
1433
        )
1434
        notification.send()
1435

    
1436
class ProjectMembership(models.Model):
1437
    person = models.ForeignKey(AstakosUser)
1438
    project = models.ForeignKey(Project)
1439
    request_date = models.DateField(default=datetime.now())
1440
    acceptance_date = models.DateField(null=True, db_index=True)
1441
    leave_request_date = models.DateField(null=True)
1442

    
1443
    class Meta:
1444
        unique_together = ("person", "project")
1445

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

    
1543
    def sync(self):
1544
        # set membership_dirty flag
1545
        self.project.membership_dirty = True
1546
        self.project.save()
1547
        
1548
        rejected = self.project.sync(specific_members=[self.person])
1549
        if not rejected:
1550
            # if syncing was successful unset membership_dirty flag
1551
            self.membership_dirty = False
1552
            self.project.save()
1553
        
1554

    
1555
class ProjectMembershipHistory(models.Model):
1556
    person = models.ForeignKey(AstakosUser)
1557
    project = models.ForeignKey(Project)
1558
    request_date = models.DateField(default=datetime.now())
1559
    removal_date = models.DateField(null=True)
1560
    rejection_date = models.DateField(null=True)
1561

    
1562

    
1563
def filter_queryset_by_property(q, property):
1564
    """
1565
    Incorporate list comprehension for filtering querysets by property
1566
    since Queryset.filter() operates on the database level.
1567
    """
1568
    return (p for p in q if getattr(p, property, False))
1569

    
1570
def get_alive_projects():
1571
    return filter_queryset_by_property(
1572
        Project.objects.all(),
1573
        'is_alive'
1574
    )
1575

    
1576
def get_active_projects():
1577
    return filter_queryset_by_property(
1578
        Project.objects.all(),
1579
        'is_active'
1580
    )
1581

    
1582
def _create_object(model, **kwargs):
1583
    o = model.objects.create(**kwargs)
1584
    o.save()
1585
    return o
1586

    
1587

    
1588
def create_astakos_user(u):
1589
    try:
1590
        AstakosUser.objects.get(user_ptr=u.pk)
1591
    except AstakosUser.DoesNotExist:
1592
        extended_user = AstakosUser(user_ptr_id=u.pk)
1593
        extended_user.__dict__.update(u.__dict__)
1594
        extended_user.save()
1595
        if not extended_user.has_auth_provider('local'):
1596
            extended_user.add_auth_provider('local')
1597
    except BaseException, e:
1598
        logger.exception(e)
1599

    
1600

    
1601
def fix_superusers(sender, **kwargs):
1602
    # Associate superusers with AstakosUser
1603
    admins = User.objects.filter(is_superuser=True)
1604
    for u in admins:
1605
        create_astakos_user(u)
1606
post_syncdb.connect(fix_superusers)
1607

    
1608

    
1609
def user_post_save(sender, instance, created, **kwargs):
1610
    if not created:
1611
        return
1612
    create_astakos_user(instance)
1613
post_save.connect(user_post_save, sender=User)
1614

    
1615

    
1616
def astakosuser_pre_save(sender, instance, **kwargs):
1617
    instance.aquarium_report = False
1618
    instance.new = False
1619
    try:
1620
        db_instance = AstakosUser.objects.get(id=instance.id)
1621
    except AstakosUser.DoesNotExist:
1622
        # create event
1623
        instance.aquarium_report = True
1624
        instance.new = True
1625
    else:
1626
        get = AstakosUser.__getattribute__
1627
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1628
                   BILLING_FIELDS)
1629
        instance.aquarium_report = True if l else False
1630
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1631

    
1632
def set_default_group(user):
1633
    try:
1634
        default = AstakosGroup.objects.get(name='default')
1635
        Membership(
1636
            group=default, person=user, date_joined=datetime.now()).save()
1637
    except AstakosGroup.DoesNotExist, e:
1638
        logger.exception(e)
1639

    
1640

    
1641
def astakosuser_post_save(sender, instance, created, **kwargs):
1642
    if instance.aquarium_report:
1643
        report_user_event(instance, create=instance.new)
1644
    if not created:
1645
        return
1646
    set_default_group(instance)
1647
    # TODO handle socket.error & IOError
1648
    register_users((instance,))
1649
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1650

    
1651

    
1652
def resource_post_save(sender, instance, created, **kwargs):
1653
    if not created:
1654
        return
1655
    register_resources((instance,))
1656
post_save.connect(resource_post_save, sender=Resource)
1657

    
1658

    
1659
def on_quota_disturbed(sender, users, **kwargs):
1660
#     print '>>>', locals()
1661
    if not users:
1662
        return
1663
    send_quota(users)
1664

    
1665
quota_disturbed = Signal(providing_args=["users"])
1666
quota_disturbed.connect(on_quota_disturbed)
1667

    
1668

    
1669
def send_quota_disturbed(sender, instance, **kwargs):
1670
    users = []
1671
    extend = users.extend
1672
    if sender == Membership:
1673
        if not instance.group.is_enabled:
1674
            return
1675
        extend([instance.person])
1676
    elif sender == AstakosUserQuota:
1677
        extend([instance.user])
1678
    elif sender == AstakosGroupQuota:
1679
        if not instance.group.is_enabled:
1680
            return
1681
        extend(instance.group.astakosuser_set.all())
1682
    elif sender == AstakosGroup:
1683
        if not instance.is_enabled:
1684
            return
1685
    quota_disturbed.send(sender=sender, users=users)
1686
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1687
post_delete.connect(send_quota_disturbed, sender=Membership)
1688
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1689
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1690
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1691
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1692

    
1693

    
1694
def renew_token(sender, instance, **kwargs):
1695
    if not instance.auth_token:
1696
        instance.renew_token()
1697
pre_save.connect(renew_token, sender=AstakosUser)
1698
pre_save.connect(renew_token, sender=Service)
1699

    
1700

    
1701
def check_closed_join_membership_policy(sender, instance, **kwargs):
1702
    if instance.id:
1703
        return
1704
    join_policy = instance.project.application.definition.member_join_policy
1705
    if join_policy == get_closed_join():
1706
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1707
pre_save.connect(check_closed_join_membership_policy, sender=ProjectMembership)
1708

    
1709

    
1710
def check_auto_accept_join_membership_policy(sender, instance, created, **kwargs):
1711
    if not created:
1712
        return
1713
    join_policy = instance.project.application.definition.member_join_policy
1714
    if join_policy == get_auto_accept_join():
1715
        instance.accept()
1716
post_save.connect(check_auto_accept_join_membership_policy, sender=ProjectMembership)