Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 73fbaec4

History | View | Annotate | Download (63.4 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, namedtuple
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
from django.contrib.contenttypes.models import ContentType
54

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

    
65
from astakos.im.settings import (
66
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
67
    AUTH_TOKEN_DURATION, BILLING_FIELDS,
68
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
69
    SITENAME, SERVICES)
70
from astakos.im.endpoints.qh import (
71
    register_users, send_quota, register_resources)
72
from astakos.im import auth_providers
73
#from astakos.im.endpoints.aquarium.producer import report_user_event
74
#from astakos.im.tasks import propagate_groupmembers_quota
75

    
76
import astakos.im.messages as astakos_messages
77

    
78
logger = logging.getLogger(__name__)
79

    
80
DEFAULT_CONTENT_TYPE = None
81
_content_type = None
82

    
83
PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
84

    
85
def get_content_type():
86
    global _content_type
87
    if _content_type is not None:
88
        return _content_type
89

    
90
    try:
91
        content_type = ContentType.objects.get(app_label='im', model='astakosuser')
92
    except:
93
        content_type = DEFAULT_CONTENT_TYPE
94
    _content_type = content_type
95
    return content_type
96

    
97
RESOURCE_SEPARATOR = '.'
98

    
99
inf = float('inf')
100

    
101
class Service(models.Model):
102
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
103
    url = models.FilePathField()
104
    icon = models.FilePathField(blank=True)
105
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
106
                                  null=True, blank=True)
107
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
108
    auth_token_expires = models.DateTimeField(
109
        _('Token expiration date'), null=True)
110

    
111
    def renew_token(self):
112
        md5 = hashlib.md5()
113
        md5.update(self.name.encode('ascii', 'ignore'))
114
        md5.update(self.url.encode('ascii', 'ignore'))
115
        md5.update(asctime())
116

    
117
        self.auth_token = b64encode(md5.digest())
118
        self.auth_token_created = datetime.now()
119
        self.auth_token_expires = self.auth_token_created + \
120
            timedelta(hours=AUTH_TOKEN_DURATION)
121

    
122
    def __str__(self):
123
        return self.name
124

    
125
    @property
126
    def resources(self):
127
        return self.resource_set.all()
128

    
129
    @resources.setter
130
    def resources(self, resources):
131
        for s in resources:
132
            self.resource_set.create(**s)
133

    
134
    def add_resource(self, service, resource, uplimit, update=True):
135
        """Raises ObjectDoesNotExist, IntegrityError"""
136
        resource = Resource.objects.get(service__name=service, name=resource)
137
        if update:
138
            AstakosUserQuota.objects.update_or_create(user=self,
139
                                                      resource=resource,
140
                                                      defaults={'uplimit': uplimit})
141
        else:
142
            q = self.astakosuserquota_set
143
            q.create(resource=resource, uplimit=uplimit)
144

    
145

    
146
class ResourceMetadata(models.Model):
147
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
148
    value = models.CharField(_('Value'), max_length=255)
149

    
150

    
151
class Resource(models.Model):
152
    name = models.CharField(_('Name'), max_length=255)
153
    meta = models.ManyToManyField(ResourceMetadata)
154
    service = models.ForeignKey(Service)
155
    desc = models.TextField(_('Description'), null=True)
156
    unit = models.CharField(_('Name'), null=True, max_length=255)
157
    group = models.CharField(_('Group'), null=True, max_length=255)
158
    
159
    class Meta:
160
        unique_together = ("name", "service")
161

    
162
    def __str__(self):
163
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
164

    
165

    
166
class GroupKind(models.Model):
167
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
168

    
169
    def __str__(self):
170
        return self.name
171

    
172

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

    
216
    @property
217
    def is_disabled(self):
218
        if not self.approval_date:
219
            return True
220
        return False
221

    
222
    @property
223
    def is_enabled(self):
224
        if self.is_disabled:
225
            return False
226
        if not self.issue_date:
227
            return False
228
        if not self.expiration_date:
229
            return True
230
        now = datetime.now()
231
        if self.issue_date > now:
232
            return False
233
        if now >= self.expiration_date:
234
            return False
235
        return True
236

    
237
    def enable(self):
238
        if self.is_enabled:
239
            return
240
        self.approval_date = datetime.now()
241
        self.save()
242
        quota_disturbed.send(sender=self, users=self.approved_members)
243
        #propagate_groupmembers_quota.apply_async(
244
        #    args=[self], eta=self.issue_date)
245
        #propagate_groupmembers_quota.apply_async(
246
        #    args=[self], eta=self.expiration_date)
247

    
248
    def disable(self):
249
        if self.is_disabled:
250
            return
251
        self.approval_date = None
252
        self.save()
253
        quota_disturbed.send(sender=self, users=self.approved_members)
254

    
255
    def approve_member(self, person):
256
        m, created = self.membership_set.get_or_create(person=person)
257
        m.approve()
258

    
259
    @property
260
    def members(self):
261
        q = self.membership_set.select_related().all()
262
        return [m.person for m in q]
263

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

    
269
    @property
270
    def quota(self):
271
        d = defaultdict(int)
272
        for q in self.astakosgroupquota_set.select_related().all():
273
            d[q.resource] += q.uplimit or inf
274
        return d
275

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

    
289
    @property
290
    def policies(self):
291
        return self.astakosgroupquota_set.select_related().all()
292

    
293
    @policies.setter
294
    def policies(self, policies):
295
        for p in policies:
296
            service = p.get('service', None)
297
            resource = p.get('resource', None)
298
            uplimit = p.get('uplimit', 0)
299
            update = p.get('update', True)
300
            self.add_policy(service, resource, uplimit, update)
301

    
302
    @property
303
    def owners(self):
304
        return self.owner.all()
305

    
306
    @property
307
    def owner_details(self):
308
        return self.owner.select_related().all()
309

    
310
    @owners.setter
311
    def owners(self, l):
312
        self.owner = l
313
        map(self.approve_member, l)
314

    
315
_default_quota = {}
316
def get_default_quota():
317
    global _default_quota
318
    if _default_quota:
319
        return _default_quota
320
    for s, data in SERVICES.iteritems():
321
        map(
322
            lambda d:_default_quota.update(
323
                {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
324
            ),
325
            data.get('resources', {})
326
        )
327
    return _default_quota
328

    
329
class AstakosUserManager(UserManager):
330

    
331
    def get_auth_provider_user(self, provider, **kwargs):
332
        """
333
        Retrieve AstakosUser instance associated with the specified third party
334
        id.
335
        """
336
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
337
                          kwargs.iteritems()))
338
        return self.get(auth_providers__module=provider, **kwargs)
339

    
340
class AstakosUser(User):
341
    """
342
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
343
    """
344
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
345
                                   null=True)
346

    
347
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
348
    #                    AstakosUserProvider model.
349
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
350
                                null=True)
351
    # ex. screen_name for twitter, eppn for shibboleth
352
    third_party_identifier = models.CharField(_('Third-party identifier'),
353
                                              max_length=255, null=True,
354
                                              blank=True)
355

    
356

    
357
    #for invitations
358
    user_level = DEFAULT_USER_LEVEL
359
    level = models.IntegerField(_('Inviter level'), default=user_level)
360
    invitations = models.IntegerField(
361
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
362

    
363
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
364
                                  null=True, blank=True)
365
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
366
    auth_token_expires = models.DateTimeField(
367
        _('Token expiration date'), null=True)
368

    
369
    updated = models.DateTimeField(_('Update date'))
370
    is_verified = models.BooleanField(_('Is verified?'), default=False)
371

    
372
    email_verified = models.BooleanField(_('Email verified?'), default=False)
373

    
374
    has_credits = models.BooleanField(_('Has credits?'), default=False)
375
    has_signed_terms = models.BooleanField(
376
        _('I agree with the terms'), default=False)
377
    date_signed_terms = models.DateTimeField(
378
        _('Signed terms date'), null=True, blank=True)
379

    
380
    activation_sent = models.DateTimeField(
381
        _('Activation sent data'), null=True, blank=True)
382

    
383
    policy = models.ManyToManyField(
384
        Resource, null=True, through='AstakosUserQuota')
385

    
386
    astakos_groups = models.ManyToManyField(
387
        AstakosGroup, verbose_name=_('agroups'), blank=True,
388
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
389
        through='Membership')
390

    
391
    __has_signed_terms = False
392
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
393
                                           default=False, db_index=True)
394

    
395
    objects = AstakosUserManager()
396

    
397
    owner = models.ManyToManyField(
398
        AstakosGroup, related_name='owner', null=True)
399

    
400
    class Meta:
401
        unique_together = ("provider", "third_party_identifier")
402

    
403
    def __init__(self, *args, **kwargs):
404
        super(AstakosUser, self).__init__(*args, **kwargs)
405
        self.__has_signed_terms = self.has_signed_terms
406
        if not self.id:
407
            self.is_active = False
408

    
409
    @property
410
    def realname(self):
411
        return '%s %s' % (self.first_name, self.last_name)
412

    
413
    @realname.setter
414
    def realname(self, value):
415
        parts = value.split(' ')
416
        if len(parts) == 2:
417
            self.first_name = parts[0]
418
            self.last_name = parts[1]
419
        else:
420
            self.last_name = parts[0]
421

    
422
    def add_permission(self, pname):
423
        if self.has_perm(pname):
424
            return
425
        p, created = Permission.objects.get_or_create(
426
                                    codename=pname,
427
                                    name=pname.capitalize(),
428
                                    content_type=get_content_type())
429
        self.user_permissions.add(p)
430

    
431
    def remove_permission(self, pname):
432
        if self.has_perm(pname):
433
            return
434
        p = Permission.objects.get(codename=pname,
435
                                   content_type=get_content_type())
436
        self.user_permissions.remove(p)
437

    
438
    @property
439
    def invitation(self):
440
        try:
441
            return Invitation.objects.get(username=self.email)
442
        except Invitation.DoesNotExist:
443
            return None
444

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

    
465
    @property
466
    def policies(self):
467
        return self.astakosuserquota_set.select_related().all()
468

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

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

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

    
494
    @property
495
    def extended_groups(self):
496
        return self.membership_set.select_related().all()
497

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

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

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

    
515
        if not self.id:
516
            # set username
517
            self.username = self.email
518

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

    
524
        super(AstakosUser, self).save(**kwargs)
525

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

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

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

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

    
556
    def __unicode__(self):
557
        return '%s (%s)' % (self.realname, self.email)
558

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

    
566
    def validate_unique_email_isactive(self):
567
        """
568
        Implements a unique_together constraint for email and is_active fields.
569
        """
570
        q = AstakosUser.objects.all()
571
        q = q.filter(email = self.email)
572
        q = q.filter(is_active = self.is_active)
573
        if self.id:
574
            q = q.filter(~Q(id = self.id))
575
        if q.count() != 0:
576
            m = 'Another account with the same email = %(email)s & \
577
                is_active = %(is_active)s found.' % __self__
578
            raise ValidationError(m)
579

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

    
596
    def set_invitations_level(self):
597
        """
598
        Update user invitation level
599
        """
600
        level = self.invitation.inviter.level + 1
601
        self.level = level
602
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
603

    
604
    def can_login_with_auth_provider(self, provider):
605
        if not self.has_auth_provider(provider):
606
            return False
607
        else:
608
            return auth_providers.get_provider(provider).is_available_for_login()
609

    
610
    def can_add_auth_provider(self, provider, **kwargs):
611
        provider_settings = auth_providers.get_provider(provider)
612
        if not provider_settings.is_available_for_login():
613
            return False
614

    
615
        if self.has_auth_provider(provider) and \
616
           provider_settings.one_per_user:
617
            return False
618

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

    
629
        return True
630

    
631
    def can_remove_auth_provider(self, provider):
632
        if len(self.get_active_auth_providers()) <= 1:
633
            return False
634
        return True
635

    
636
    def can_change_password(self):
637
        return self.has_auth_provider('local', auth_backend='astakos')
638

    
639
    def has_auth_provider(self, provider, **kwargs):
640
        return bool(self.auth_providers.filter(module=provider,
641
                                               **kwargs).count())
642

    
643
    def add_auth_provider(self, provider, **kwargs):
644
        if self.can_add_auth_provider(provider, **kwargs):
645
            self.auth_providers.create(module=provider, active=True, **kwargs)
646
        else:
647
            raise Exception('Cannot add provider')
648

    
649
    def add_pending_auth_provider(self, pending):
650
        """
651
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
652
        the current user.
653
        """
654
        if not isinstance(pending, PendingThirdPartyUser):
655
            pending = PendingThirdPartyUser.objects.get(token=pending)
656

    
657
        provider = self.add_auth_provider(pending.provider,
658
                               identifier=pending.third_party_identifier)
659

    
660
        if email_re.match(pending.email or '') and pending.email != self.email:
661
            self.additionalmail_set.get_or_create(email=pending.email)
662

    
663
        pending.delete()
664
        return provider
665

    
666
    def remove_auth_provider(self, provider, **kwargs):
667
        self.auth_providers.get(module=provider, **kwargs).delete()
668

    
669
    # user urls
670
    def get_resend_activation_url(self):
671
        return reverse('send_activation', {'user_id': self.pk})
672

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

    
680
    def get_password_reset_url(self, token_generator=default_token_generator):
681
        return reverse('django.contrib.auth.views.password_reset_confirm',
682
                          kwargs={'uidb36':int_to_base36(self.id),
683
                                  'token':token_generator.make_token(self)})
684

    
685
    def get_auth_providers(self):
686
        return self.auth_providers.all()
687

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

    
697
        return providers
698

    
699
    def get_active_auth_providers(self):
700
        providers = []
701
        for provider in self.auth_providers.active():
702
            if auth_providers.get_provider(provider.module).is_available_for_login():
703
                providers.append(provider)
704
        return providers
705

    
706
    @property
707
    def auth_providers_display(self):
708
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
709

    
710

    
711
class AstakosUserAuthProviderManager(models.Manager):
712

    
713
    def active(self):
714
        return self.filter(active=True)
715

    
716

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

    
733
    objects = AstakosUserAuthProviderManager()
734

    
735
    class Meta:
736
        unique_together = (('identifier', 'module', 'user'), )
737

    
738
    @property
739
    def settings(self):
740
        return auth_providers.get_provider(self.module)
741

    
742
    @property
743
    def details_display(self):
744
        return self.settings.details_tpl % self.__dict__
745

    
746
    def can_remove(self):
747
        return self.user.can_remove_auth_provider(self.module)
748

    
749
    def delete(self, *args, **kwargs):
750
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
751
        if self.module == 'local':
752
            self.user.set_unusable_password()
753
            self.user.save()
754
        return ret
755

    
756
    def __repr__(self):
757
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
758

    
759
    def __unicode__(self):
760
        if self.identifier:
761
            return "%s:%s" % (self.module, self.identifier)
762
        if self.auth_backend:
763
            return "%s:%s" % (self.module, self.auth_backend)
764
        return self.module
765

    
766

    
767

    
768
class Membership(models.Model):
769
    person = models.ForeignKey(AstakosUser)
770
    group = models.ForeignKey(AstakosGroup)
771
    date_requested = models.DateField(default=datetime.now(), blank=True)
772
    date_joined = models.DateField(null=True, db_index=True, blank=True)
773

    
774
    class Meta:
775
        unique_together = ("person", "group")
776

    
777
    def save(self, *args, **kwargs):
778
        if not self.id:
779
            if not self.group.moderation_enabled:
780
                self.date_joined = datetime.now()
781
        super(Membership, self).save(*args, **kwargs)
782

    
783
    @property
784
    def is_approved(self):
785
        if self.date_joined:
786
            return True
787
        return False
788

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

    
799
    def disapprove(self):
800
        approved = self.is_approved()
801
        self.delete()
802
        if approved:
803
            quota_disturbed.send(sender=self, users=(self.person,))
804

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

    
832
    update_or_create = _update_or_create
833

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

    
841
    class Meta:
842
        unique_together = ("resource", "group")
843

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

    
851
    class Meta:
852
        unique_together = ("resource", "user")
853

    
854

    
855
class ApprovalTerms(models.Model):
856
    """
857
    Model for approval terms
858
    """
859

    
860
    date = models.DateTimeField(
861
        _('Issue date'), db_index=True, default=datetime.now())
862
    location = models.CharField(_('Terms location'), max_length=255)
863

    
864

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

    
878
    def __init__(self, *args, **kwargs):
879
        super(Invitation, self).__init__(*args, **kwargs)
880
        if not self.id:
881
            self.code = _generate_invitation_code()
882

    
883
    def consume(self):
884
        self.is_consumed = True
885
        self.consumed = datetime.now()
886
        self.save()
887

    
888
    def __unicode__(self):
889
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
890

    
891

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

899
        If the key is valid and has not expired, return the ``User``
900
        after activating.
901

902
        If the key is not valid or has expired, return ``None``.
903

904
        If the key is valid but the ``User`` is already active,
905
        return ``None``.
906

907
        After successful email change the activation record is deleted.
908

909
        Throws ValueError if there is already
910
        """
911
        try:
912
            email_change = self.model.objects.get(
913
                activation_key=activation_key)
914
            if email_change.activation_key_expired():
915
                email_change.delete()
916
                raise EmailChange.DoesNotExist
917
            # is there an active user with this address?
918
            try:
919
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
920
            except AstakosUser.DoesNotExist:
921
                pass
922
            else:
923
                raise ValueError(_('The new email address is reserved.'))
924
            # update user
925
            user = AstakosUser.objects.get(pk=email_change.user_id)
926
            user.email = email_change.new_email_address
927
            user.save()
928
            email_change.delete()
929
            return user
930
        except EmailChange.DoesNotExist:
931
            raise ValueError(_('Invalid activation key.'))
932

    
933

    
934
class EmailChange(models.Model):
935
    new_email_address = models.EmailField(
936
        _(u'new e-mail address'),
937
        help_text=_('Your old email address will be used until you verify your new one.'))
938
    user = models.ForeignKey(
939
        AstakosUser, unique=True, related_name='emailchange_user')
940
    requested_at = models.DateTimeField(default=datetime.now())
941
    activation_key = models.CharField(
942
        max_length=40, unique=True, db_index=True)
943

    
944
    objects = EmailChangeManager()
945

    
946
    def activation_key_expired(self):
947
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
948
        return self.requested_at + expiration_date < datetime.now()
949

    
950

    
951
class AdditionalMail(models.Model):
952
    """
953
    Model for registring invitations
954
    """
955
    owner = models.ForeignKey(AstakosUser)
956
    email = models.EmailField()
957

    
958

    
959
def _generate_invitation_code():
960
    while True:
961
        code = randint(1, 2L ** 63 - 1)
962
        try:
963
            Invitation.objects.get(code=code)
964
            # An invitation with this code already exists, try again
965
        except Invitation.DoesNotExist:
966
            return code
967

    
968

    
969
def get_latest_terms():
970
    try:
971
        term = ApprovalTerms.objects.order_by('-id')[0]
972
        return term
973
    except IndexError:
974
        pass
975
    return None
976

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

    
991
    class Meta:
992
        unique_together = ("provider", "third_party_identifier")
993

    
994
    @property
995
    def realname(self):
996
        return '%s %s' %(self.first_name, self.last_name)
997

    
998
    @realname.setter
999
    def realname(self, value):
1000
        parts = value.split(' ')
1001
        if len(parts) == 2:
1002
            self.first_name = parts[0]
1003
            self.last_name = parts[1]
1004
        else:
1005
            self.last_name = parts[0]
1006

    
1007
    def save(self, **kwargs):
1008
        if not self.id:
1009
            # set username
1010
            while not self.username:
1011
                username =  uuid.uuid4().hex[:30]
1012
                try:
1013
                    AstakosUser.objects.get(username = username)
1014
                except AstakosUser.DoesNotExist, e:
1015
                    self.username = username
1016
        super(PendingThirdPartyUser, self).save(**kwargs)
1017

    
1018
    def generate_token(self):
1019
        self.password = self.third_party_identifier
1020
        self.last_login = datetime.now()
1021
        self.token = default_token_generator.make_token(self)
1022

    
1023
class SessionCatalog(models.Model):
1024
    session_key = models.CharField(_('session key'), max_length=40)
1025
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1026

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

    
1031
    def __str__(self):
1032
        return self.policy
1033

    
1034
class MemberLeavePolicy(models.Model):
1035
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1036
    description = models.CharField(_('Description'), max_length=80)
1037

    
1038
    def __str__(self):
1039
        return self.policy
1040

    
1041
_auto_accept_join = False
1042
def get_auto_accept_join():
1043
    global _auto_accept_join
1044
    if _auto_accept_join is not False:
1045
        return _auto_accept_join
1046
    try:
1047
        auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
1048
    except:
1049
        auto_accept = None
1050
    _auto_accept_join = auto_accept
1051
    return auto_accept
1052

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

    
1065
_auto_accept_leave = False
1066
def get_auto_accept_leave():
1067
    global _auto_accept_leave
1068
    if _auto_accept_leave is not False:
1069
        return _auto_accept_leave
1070
    try:
1071
        auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
1072
    except:
1073
        auto_accept = None
1074
    _auto_accept_leave = auto_accept
1075
    return auto_accept
1076

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

    
1089

    
1090
### PROJECTS ###
1091
################
1092

    
1093

    
1094
class SyncedModel(models.Model):
1095

    
1096
    new_state       = models.BigIntegerField()
1097
    synced_state    = models.BigIntegerField()
1098
    STATUS_SYNCED   = 0
1099
    STATUS_PENDING  = 1
1100
    sync_status     = models.IntegerField(db_index=True)
1101

    
1102
    class Meta:
1103
        abstract = True
1104

    
1105
    class NotSynced(Exception):
1106
        pass
1107

    
1108
    def sync_init_state(self, state):
1109
        self.synced_state = state
1110
        self.new_state = state
1111
        self.sync_status = self.STATUS_SYNCED
1112

    
1113
    def sync_get_status(self):
1114
        return self.sync_status
1115

    
1116
    def sync_set_status(self):
1117
        if self.new_state != self.synced_state:
1118
            self.sync_status = self.STATUS_PENDING
1119
        else:
1120
            self.sync_status = self.STATUS_SYNCED
1121

    
1122
    def sync_set_synced(self):
1123
        self.synced_state = self.new_state
1124
        self.sync_status = self.STATUS_SYNCED
1125

    
1126
    def sync_get_synced_state(self):
1127
        return self.synced_state
1128

    
1129
    def sync_set_new_state(self, new_state):
1130
        self.new_state = new_state
1131
        self.sync_set_status()
1132

    
1133
    def sync_get_new_state(self):
1134
        return self.new_state
1135

    
1136
    def sync_set_synced_state(self, synced_state):
1137
        self.synced_state = synced_state
1138
        self.sync_set_status()
1139

    
1140
    def sync_get_pending_objects(self):
1141
        return self.objects.filter(sync_status=self.STATUS_PENDING)
1142

    
1143
    def sync_get_synced_objects(self):
1144
        return self.objects.filter(sync_status=self.STATUS_SYNCED)
1145

    
1146
    def sync_verify_get_synced_state(self):
1147
        status = self.sync_get_status()
1148
        state = self.sync_get_synced_state()
1149
        verified = (status == self.STATUS_SYNCED)
1150
        return state, verified
1151

    
1152
class ProjectApplication(models.Model):
1153
    states_list = [PENDING, APPROVED, REPLACED, UNKNOWN]
1154
    states = dict((k, v) for v, k in enumerate(states_list))
1155

    
1156
    applicant = models.ForeignKey(
1157
        AstakosUser,
1158
        related_name='my_project_applications',
1159
        db_index=True)
1160
    owner = models.ForeignKey(
1161
        AstakosUser,
1162
        related_name='own_project_applications',
1163
        db_index=True
1164
    )
1165
    precursor_application = models.OneToOneField('ProjectApplication',
1166
        null=True,
1167
        blank=True,
1168
        db_index=True
1169
    )
1170
    state = models.CharField(max_length=80, default=UNKNOWN)
1171

    
1172
    name = models.CharField(max_length=80)
1173
    homepage = models.URLField(max_length=255, null=True, blank=True)
1174
    description = models.TextField(null=True)
1175
    start_date = models.DateTimeField()
1176
    end_date = models.DateTimeField()
1177
    member_join_policy = models.ForeignKey(MemberJoinPolicy)
1178
    member_leave_policy = models.ForeignKey(MemberLeavePolicy)
1179
    limit_on_members_number = models.PositiveIntegerField(null=True,blank=True)
1180
    resource_grants = models.ManyToManyField(
1181
        Resource,
1182
        null=True,
1183
        blank=True,
1184
        through='ProjectResourceGrant'
1185
    )
1186
    comments = models.TextField(null=True, blank=True)
1187
    issue_date = models.DateTimeField()
1188
    
1189
    def add_resource_policy(self, service, resource, uplimit, update=True):
1190
        """Raises ObjectDoesNotExist, IntegrityError"""
1191
        resource = Resource.objects.get(service__name=service, name=resource)
1192
        if update:
1193
            ProjectResourceGrant.objects.update_or_create(
1194
                project_application=self,
1195
                resource=resource,
1196
                defaults={'member_capacity': uplimit}
1197
            )
1198
        else:
1199
            q = self.projectresourcegrant_set
1200
            q.create(resource=resource, member_capacity=uplimit)
1201

    
1202
    @property
1203
    def resource_policies(self):
1204
        return self.projectresourcegrant_set.all()
1205

    
1206
    @resource_policies.setter
1207
    def resource_policies(self, policies):
1208
        for p in policies:
1209
            service = p.get('service', None)
1210
            resource = p.get('resource', None)
1211
            uplimit = p.get('uplimit', 0)
1212
            update = p.get('update', True)
1213
            self.add_resource_policy(service, resource, uplimit, update)
1214
    
1215
    @property
1216
    def follower(self):
1217
        try:
1218
            return ProjectApplication.objects.get(precursor_application=self)
1219
        except ProjectApplication.DoesNotExist:
1220
            return
1221

    
1222
    def submit(self, resource_policies, applicant, comments,
1223
               precursor_application=None):
1224

    
1225
        if precursor_application:
1226
            self.precursor_application = precursor_application
1227
            self.owner = precursor_application.owner
1228
        else:
1229
            self.owner = applicant
1230

    
1231
        self.id = None
1232
        self.applicant = applicant
1233
        self.comments = comments
1234
        self.issue_date = datetime.now()
1235
        self.state = PENDING
1236
        self.save()
1237
        self.resource_policies = resource_policies
1238

    
1239
    def _get_project(self):
1240
        precursor = self
1241
        while precursor:
1242
            try:
1243
                project = precursor.project
1244
                return project
1245
            except Project.DoesNotExist:
1246
                pass
1247
            precursor = precursor.precursor_application
1248

    
1249
        return None
1250

    
1251
    def approve(self, approval_user=None):
1252
        """
1253
        If approval_user then during owner membership acceptance
1254
        it is checked whether the request_user is eligible.
1255

1256
        Raises:
1257
            PermissionDenied
1258
        """
1259

    
1260
        if not transaction.is_managed():
1261
            raise AssertionError("NOPE")
1262

    
1263
        new_project_name = self.name
1264
        if self.state != PENDING:
1265
            m = _("cannot approve: project '%s' in state '%s'") % (
1266
                    new_project_name, self.state)
1267
            raise PermissionDenied(m) # invalid argument
1268

    
1269
        now = datetime.now()
1270
        project = self._get_project()
1271
        if project is None:
1272
            try:
1273
                conflicting_project = Project.objects.get(name=new_project_name)
1274
                if conflicting_project.is_alive:
1275
                    m = _("cannot approve: project with name '%s' "
1276
                          "already exists (serial: %s)") % (
1277
                            new_project_name, conflicting_project.id)
1278
                    raise PermissionDenied(m) # invalid argument
1279
            except Project.DoesNotExist:
1280
                pass
1281
            project = Project(creation_date=now)
1282

    
1283
        project.last_application_approved = self
1284
        project.last_approval_date = now
1285
        project.save()
1286
        #ProjectMembership.add_to_project(self)
1287
        project.add_member(self.owner)
1288

    
1289
        precursor = self.precursor_application
1290
        while precursor:
1291
            precursor.state = REPLACED
1292
            precursor.save()
1293
            precursor = precursor.precursor_application
1294

    
1295
        self.state = APPROVED
1296
        self.save()
1297

    
1298
        transaction.commit()
1299
        project.check_sync()
1300

    
1301
class ProjectResourceGrant(models.Model):
1302

    
1303
    resource = models.ForeignKey(Resource)
1304
    project_application = models.ForeignKey(ProjectApplication, blank=True)
1305
    project_capacity     = models.BigIntegerField(null=True)
1306
    project_import_limit = models.BigIntegerField(null=True)
1307
    project_export_limit = models.BigIntegerField(null=True)
1308
    member_capacity      = models.BigIntegerField(null=True)
1309
    member_import_limit  = models.BigIntegerField(null=True)
1310
    member_export_limit  = models.BigIntegerField(null=True)
1311

    
1312
    objects = ExtendedManager()
1313

    
1314
    class Meta:
1315
        unique_together = ("resource", "project_application")
1316

    
1317
class Project(SyncedModel):
1318
    application                 =   models.OneToOneField(
1319
                                            ProjectApplication,
1320
                                            related_name='project',
1321
                                            null=True)
1322
    last_application_approved   =   models.OneToOneField(
1323
                                            ProjectApplication,
1324
                                            related_name='last_project')
1325
    last_approval_date          =   models.DateTimeField(null=True)
1326

    
1327
    members                     =   models.ManyToManyField(
1328
                                            AstakosUser,
1329
                                            through='ProjectMembership')
1330

    
1331
    termination_start_date      =   models.DateTimeField(null=True)
1332
    termination_date            =   models.DateTimeField(null=True)
1333

    
1334
    creation_date               =   models.DateTimeField()
1335
    name                        =   models.CharField(
1336
                                            max_length=80,
1337
                                            db_index=True,
1338
                                            unique=True)
1339
    state = models.CharField(max_length=80, default=UNKNOWN)
1340

    
1341
    @property
1342
    def current_application(self):
1343
        return self.application or self.last_application_approved
1344
    
1345
    @property
1346
    def violated_resource_grants(self):
1347
        if self.application is None:
1348
            return True
1349
        # do something
1350
        return False
1351
    
1352
    @property
1353
    def violated_members_number_limit(self):
1354
        application = self.application
1355
        if application is None:
1356
            return True
1357
        return len(self.approved_members) > application.limit_on_members_number
1358
        
1359
    @property
1360
    def is_terminated(self):
1361
        return bool(self.termination)
1362
    
1363
    @property
1364
    def is_still_approved(self):
1365
        return bool(self.last_approval_date)
1366

    
1367
    @property
1368
    def is_active(self):
1369
        if (self.is_terminated or
1370
            not self.is_still_approved or
1371
            self.violated_resource_grants):
1372
            return False
1373
#         if self.violated_members_number_limit:
1374
#             return False
1375
        return True
1376
    
1377
    @property
1378
    def is_suspended(self):
1379
        if (self.is_terminated or
1380
            self.is_still_approved or
1381
            not self.violated_resource_grants):
1382
            return False
1383
#             if not self.violated_members_number_limit:
1384
#                 return False
1385
        return True
1386
    
1387
    @property
1388
    def is_alive(self):
1389
        return self.is_active or self.is_suspended
1390
    
1391
    @property
1392
    def is_inconsistent(self):
1393
        now = datetime.now()
1394
        if self.creation_date > now:
1395
            return True
1396
        if self.last_approval_date > now:
1397
            return True
1398
        if self.terminaton_date > now:
1399
            return True
1400
        return False
1401
    
1402
    @property
1403
    def approved_members(self):
1404
        return [m.person for m in self.projectmembership_set.filter(~Q(acceptance_date=None))]
1405

    
1406
    def sync(self, specific_members=()):
1407
        if self.is_synchronized:
1408
            return
1409
        members = specific_members or self.approved_members
1410
        rejected = send_quota(self.approved_members)
1411
        if not rejected:
1412
            self.application = self.last_application_approved
1413
            self.save()
1414
        return rejected
1415

    
1416
    def set_pending_membership_sync(self):
1417
        self.membership_dirty = True
1418
        self.save()
1419

    
1420
    def set_state(self):
1421
        PROJECT_SYNCHRONIZED = 0
1422
        PROJECT_SYNC_PENDING_MEMBERSHIP = (1 << 0)
1423
        PROJECT_SYNC_PENDING_DEFINITION = (1 << 1)
1424
        PROJECT_SYNC_PENDING = (PROJECT_SYNC_PENDING_DEFINITION | 
1425
                                PROJECT_SYNC_PENDING_MEMBERSHIP)
1426

    
1427
        oldstate = self.state
1428
        state = PROJECT_SYNCHRONIZED
1429

    
1430
        if self.last_application_approved != self.application:
1431
            state |= PROJECT_SYNC_PENDING_DEFINITION
1432

    
1433
        if self.membership_dirty:
1434
            state |= PROJECT_SYNC_PENDING_MEMBERSHIP
1435

    
1436
        if oldstate != state:
1437
            self.state = state
1438
            self.save()
1439
        return state
1440

    
1441
    def check_sync(self, hint=None):
1442
        state = self.set_state()
1443
        if state: # needs syncing
1444
            if self.sync_membership():
1445
                self.set_sta
1446

    
1447
    def sync_membership(self, members=None):
1448
        members = members if members is not None else self.approved_members
1449
        rejected = send_quota(members)
1450
        success = not rejected
1451
        if success:
1452
            self.members
1453
        return success
1454

    
1455
    def add_member(self, user):
1456
        """
1457
        Raises:
1458
            django.exceptions.PermissionDenied
1459
            astakos.im.models.AstakosUser.DoesNotExist
1460
        """
1461
        if isinstance(user, int):
1462
            user = AstakosUser.objects.get(user=user)
1463

    
1464
        m, created = ProjectMembership.objects.get_or_create(
1465
            person=user, project=self
1466
        )
1467
        m.accept()
1468

    
1469
    def remove_member(self, user):
1470
        """
1471
        Raises:
1472
            django.exceptions.PermissionDenied
1473
            astakos.im.models.AstakosUser.DoesNotExist
1474
            astakos.im.models.ProjectMembership.DoesNotExist
1475
        """
1476
        if isinstance(user, int):
1477
            user = AstakosUser.objects.get(user=user)
1478

    
1479
        m = ProjectMembership.objects.get(person=user, project=self)
1480
        m.remove()
1481

    
1482
    def terminate(self):
1483
        self.termination_start_date = datetime.now()
1484
        self.terminaton_date = None
1485
        self.save()
1486
        
1487
        rejected = self.sync()
1488
        if not rejected:
1489
            self.termination_start_date = None
1490
            self.termination_date = datetime.now()
1491
            self.save()
1492
            
1493
#         try:
1494
#             notification = build_notification(
1495
#                 settings.SERVER_EMAIL,
1496
#                 [self.current_application.owner.email],
1497
#                 _(PROJECT_TERMINATION_SUBJECT) % self.__dict__,
1498
#                 template='im/projects/project_termination_notification.txt',
1499
#                 dictionary={'object':self.current_application}
1500
#             ).send()
1501
#         except NotificationError, e:
1502
#             logger.error(e.messages)
1503

    
1504
    def suspend(self):
1505
        self.last_approval_date = None
1506
        self.save()
1507
        self.sync()
1508

    
1509
#         try:
1510
#             notification = build_notification(
1511
#                 settings.SERVER_EMAIL,
1512
#                 [self.current_application.owner.email],
1513
#                 _(PROJECT_SUSPENSION_SUBJECT) % self.definition.__dict__,
1514
#                 template='im/projects/project_suspension_notification.txt',
1515
#                 dictionary={'object':self.current_application}
1516
#             ).send()
1517
#         except NotificationError, e:
1518
#             logger.error(e.messages)
1519

    
1520

    
1521
QuotaLimits = namedtuple('QuotaLimits', ('holder',
1522
                                         'capacity',
1523
                                         'import_limit',
1524
                                         'export_limit'))
1525

    
1526

    
1527

    
1528
class ExclusiveOrRaise(object):
1529
    """Context Manager to exclusively execute a critical code section.
1530
       The exclusion must be global.
1531
       (IPC semaphores will not protect across OS,
1532
        DB locks will if it's the same DB)
1533
    """
1534

    
1535
    class Busy(Exception):
1536
        pass
1537

    
1538
    def __init__(self, locked=False):
1539
        init = 0 if locked else 1
1540
        from multiprocessing import Semaphore
1541
        self._sema = Semaphore(init)
1542

    
1543
    def enter(self):
1544
        acquired = self._sema.acquire(False)
1545
        if not acquired:
1546
            raise self.Busy()
1547

    
1548
    def leave(self):
1549
        self._sema.release()
1550

    
1551
    def __enter__(self):
1552
        self.enter()
1553
        return self
1554

    
1555
    def __exit__(self, exc_type, exc_value, exc_traceback):
1556
        self.leave()
1557

    
1558

    
1559
exclusive_or_raise = ExclusiveOrRaise(locked=False)
1560

    
1561

    
1562
class ProjectMembership(SyncedModel):
1563
    person = models.ForeignKey(AstakosUser)
1564
    project = models.ForeignKey(Project)
1565
    request_date = models.DateField(default=datetime.now())
1566

    
1567
    acceptance_date = models.DateField(null=True, db_index=True)
1568
    leave_request_date = models.DateField(null=True)
1569

    
1570
    REQUESTED   =   0
1571
    ACCEPTED    =   1
1572
    REMOVED     =   2
1573
    REJECTED    =   3   # never seen, because .delete()
1574
    REPLACED    =   4   # when the project definition is replaced
1575
                        # spontaneously goes back to ACCEPTED when synced
1576

    
1577
    class Meta:
1578
        unique_together = ("person", "project")
1579

    
1580
    def __str__(self):
1581
        return _("<'%s' membership in project '%s'>") % (
1582
                self.person.username, self.project.application)
1583

    
1584
    __repr__ = __str__
1585

    
1586
    def __init__(self, *args, **kwargs):
1587
        self.sync_init_state(self.REQUEST)
1588
        super(ProjectMembership, self).__init__(*args, **kwargs)
1589

    
1590
    def _set_history_item(self, reason, date=None):
1591
        if isinstance(reason, basestring):
1592
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1593

    
1594
        history_item = ProjectMembershipHistory(
1595
                            serial=self.id,
1596
                            person=self.person,
1597
                            project=self.project,
1598
                            date=date,
1599
                            reason=reason)
1600
        history_item.save()
1601
        serial = history_item.id
1602

    
1603
    def accept(self):
1604
        state, verified = self.sync_verify_get_synced_state()
1605
        if not verified:
1606
            new_state = self.sync_get_new_state()
1607
            m = _("%s: cannot accept: not synched (%s -> %s)") % (
1608
                    self, state, new_state)
1609
            raise self.NotSynced(m)
1610

    
1611
        if state != self.REQUESTED:
1612
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1613
            raise AssertionError(m)
1614

    
1615
        now = datetime.now()
1616
        self.acceptance_date = now
1617
        self._set_history_item(reason='ACCEPT', date=now)
1618
        self.sync_set_new_state(self.ACCEPTED)
1619
        self.save()
1620

    
1621
    def remove(self):
1622
        state, verified = self.sync_verify_get_synced_state()
1623
        if not verified:
1624
            new_state = self.sync_get_new_state()
1625
            m = _("%s: cannot remove: not synched (%s -> %s)") % (
1626
                    self, state, new_state)
1627
            raise self.NotSynced(m)
1628

    
1629
        if state != self.ACCEPTED:
1630
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1631
            raise AssertionError(m)
1632

    
1633
        serial = self._set_history_item(reason='REMOVE')
1634
        self.sync_set_new_state(self.REMOVED)
1635
        self.save()
1636

    
1637
    def reject(self):
1638
        state, verified = self.sync_verify_get_synced_state()
1639
        if not verified:
1640
            new_state = self.sync_get_new_state()
1641
            m = _("%s: cannot reject: not synched (%s -> %s)" % (
1642
                    self, state, new_state))
1643
            raise self.NotSynced(m)
1644

    
1645
        if state != self.REQUESTED:
1646
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1647
            raise AssertionError(m)
1648

    
1649
        # rejected requests don't need sync,
1650
        # because they were never effected
1651
        self._set_history_item(reason='REJECT')
1652
        self.delete()
1653

    
1654
    def get_quotas(self, limits_list=None, factor=1):
1655
        if limits_list is None:
1656
            limits_list = []
1657
        append = limits_list.append
1658
        holder = self.person.username
1659
        all_grants = self.project.application.resource_grants.all()
1660
        for grant in all_grants:
1661
            append(QuotaLimits(holder       = holder,
1662
                               resource     = grant.resource.name,
1663
                               capacity     = factor * grant.member_capacity,
1664
                               import_limit = factor * grant.member_import_limit,
1665
                               export_limit = factor * grant.member_export_limit))
1666
        return limits_list
1667

    
1668
    def get_diff_quotas(self, limits_list=None, factor=1):
1669
        if limits_list is None:
1670
            limits_list = []
1671

    
1672
        append = limits_list.append
1673
        holder = self.person.username
1674

    
1675
        # first, inverse all current limits, and index them by resource name
1676
        cur_grants = self.project.application.resource_grants.all()
1677
        f = factor * -1
1678
        tmp_grants = {}
1679
        for grant in cur_grants:
1680
            name = grant.resource.name
1681
            tmp_grants[name] = QuotaLimits(
1682
                            holder       = holder,
1683
                            resource     = name,
1684
                            capacity     = f * grant.member_capacity,
1685
                            import_limit = f * grant.member_import_limit,
1686
                            export_limit = f * grant.member_export_limit)
1687

    
1688
        # second, add each new limit to its inversed current
1689
        new_grants = self.project.new_application.resource_grants.all()
1690
        for new_grant in new_grants:
1691
            name = grant.resource.name
1692
            cur_grant = tmp_grants.pop(name, None)
1693
            if cur_grant is None:
1694
                # if limits on a new resource, set 0 current values
1695
                capacity = 0
1696
                import_limit = 0
1697
                export_limit = 0
1698
            else:
1699
                capacity = cur_grant.capacity
1700
                import_limit = cur_grant.import_limit
1701
                export_limit = cur_grant.export_limit
1702

    
1703
            capacity += new_grant.member_capacity
1704
            import_limit += new_grant.member_import_limit
1705
            export_limit += new_grant.member_export_limit
1706

    
1707
            append(QuotaLimits(holder       = holder,
1708
                               resource     = name,
1709
                               capacity     = capacity,
1710
                               import_limit = import_limit,
1711
                               export_limit = export_limit))
1712

    
1713
        # third, append all the inversed current limits for removed resources
1714
        limits_list.extend(tmp_grants.itervalues())
1715
        return limits_list
1716

    
1717
    def do_sync(self):
1718
        state = self.sync_get_synced_state()
1719
        new_state = self.sync_get_new_state()
1720

    
1721
        if state == self.REQUESTED and new_state == self.ACCEPTED:
1722
            quotas = self.get_quotas(factor=1)
1723
        elif state == self.ACCEPTED and new_state == self.REMOVED:
1724
            quotas = self.get_quotas(factor=-1)
1725
        elif state == self.ACCEPTED and new_state == self.REPLACED:
1726
            quotas = self.get_diff_quotas(factor=1)
1727
        else:
1728
            m = _("%s: sync: called on invalid state ('%s' -> '%s')") % (
1729
                    self, state, new_state)
1730
            raise AssertionError(m)
1731

    
1732
        quotas = self.get_quotas(factor=factor)
1733
        try:
1734
            failure = add_quotas(quotas)
1735
            if failure:
1736
                m = "%s: sync: add_quotas failed" % (self,)
1737
                raise RuntimeError(m)
1738
        except Exception:
1739
            raise
1740
        else:
1741
            self.sync_set_synced()
1742

    
1743
        # some states have instant side-effects/transitions
1744
        if new_state == self.REMOVED:
1745
            self.delete()
1746
        elif new_state == self.REPLACED:
1747
            self.sync_init_state(self.ACCEPTED)
1748

    
1749
    def sync(self):
1750
        with exclusive_or_raise:
1751
            self.do_sync()
1752

    
1753

    
1754
class ProjectMembershipHistory(models.Model):
1755
    reasons_list = ['ACCEPT', 'REJECT', 'REMOVE']
1756
    reasons = dict((k, v) for v, k in enumerate(reasons_list))
1757
    person = models.ForeignKey(AstakosUser)
1758
    project = models.ForeignKey(Project)
1759
    date = models.DateField(default=datetime.now)
1760
    reason = models.IntegerField()
1761
    serial = models.BigIntegerField()
1762

    
1763

    
1764
def filter_queryset_by_property(q, property):
1765
    """
1766
    Incorporate list comprehension for filtering querysets by property
1767
    since Queryset.filter() operates on the database level.
1768
    """
1769
    return (p for p in q if getattr(p, property, False))
1770

    
1771
def get_alive_projects():
1772
    return filter_queryset_by_property(
1773
        Project.objects.all(),
1774
        'is_alive'
1775
    )
1776

    
1777
def get_active_projects():
1778
    return filter_queryset_by_property(
1779
        Project.objects.all(),
1780
        'is_active'
1781
    )
1782

    
1783
def _create_object(model, **kwargs):
1784
    o = model.objects.create(**kwargs)
1785
    o.save()
1786
    return o
1787

    
1788

    
1789
def create_astakos_user(u):
1790
    try:
1791
        AstakosUser.objects.get(user_ptr=u.pk)
1792
    except AstakosUser.DoesNotExist:
1793
        extended_user = AstakosUser(user_ptr_id=u.pk)
1794
        extended_user.__dict__.update(u.__dict__)
1795
        extended_user.save()
1796
        if not extended_user.has_auth_provider('local'):
1797
            extended_user.add_auth_provider('local')
1798
    except BaseException, e:
1799
        logger.exception(e)
1800

    
1801

    
1802
def fix_superusers(sender, **kwargs):
1803
    # Associate superusers with AstakosUser
1804
    admins = User.objects.filter(is_superuser=True)
1805
    for u in admins:
1806
        create_astakos_user(u)
1807
post_syncdb.connect(fix_superusers)
1808

    
1809

    
1810
def user_post_save(sender, instance, created, **kwargs):
1811
    if not created:
1812
        return
1813
    create_astakos_user(instance)
1814
post_save.connect(user_post_save, sender=User)
1815

    
1816

    
1817
# def astakosuser_pre_save(sender, instance, **kwargs):
1818
#     instance.aquarium_report = False
1819
#     instance.new = False
1820
#     try:
1821
#         db_instance = AstakosUser.objects.get(id=instance.id)
1822
#     except AstakosUser.DoesNotExist:
1823
#         # create event
1824
#         instance.aquarium_report = True
1825
#         instance.new = True
1826
#     else:
1827
#         get = AstakosUser.__getattribute__
1828
#         l = filter(lambda f: get(db_instance, f) != get(instance, f),
1829
#                    BILLING_FIELDS)
1830
#         instance.aquarium_report = True if l else False
1831
# pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1832

    
1833
# def set_default_group(user):
1834
#     try:
1835
#         default = AstakosGroup.objects.get(name='default')
1836
#         Membership(
1837
#             group=default, person=user, date_joined=datetime.now()).save()
1838
#     except AstakosGroup.DoesNotExist, e:
1839
#         logger.exception(e)
1840

    
1841

    
1842
def astakosuser_post_save(sender, instance, created, **kwargs):
1843
#     if instance.aquarium_report:
1844
#         report_user_event(instance, create=instance.new)
1845
    if not created:
1846
        return
1847
#     set_default_group(instance)
1848
    # TODO handle socket.error & IOError
1849
    register_users((instance,))
1850
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1851

    
1852

    
1853
def resource_post_save(sender, instance, created, **kwargs):
1854
    if not created:
1855
        return
1856
    register_resources((instance,))
1857
post_save.connect(resource_post_save, sender=Resource)
1858

    
1859

    
1860
# def on_quota_disturbed(sender, users, **kwargs):
1861
# #     print '>>>', locals()
1862
#     if not users:
1863
#         return
1864
#     send_quota(users)
1865
# 
1866
# quota_disturbed = Signal(providing_args=["users"])
1867
# quota_disturbed.connect(on_quota_disturbed)
1868

    
1869

    
1870
# def send_quota_disturbed(sender, instance, **kwargs):
1871
#     users = []
1872
#     extend = users.extend
1873
#     if sender == Membership:
1874
#         if not instance.group.is_enabled:
1875
#             return
1876
#         extend([instance.person])
1877
#     elif sender == AstakosUserQuota:
1878
#         extend([instance.user])
1879
#     elif sender == AstakosGroupQuota:
1880
#         if not instance.group.is_enabled:
1881
#             return
1882
#         extend(instance.group.astakosuser_set.all())
1883
#     elif sender == AstakosGroup:
1884
#         if not instance.is_enabled:
1885
#             return
1886
#     quota_disturbed.send(sender=sender, users=users)
1887
# post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1888
# post_delete.connect(send_quota_disturbed, sender=Membership)
1889
# post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1890
# post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1891
# post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1892
# post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1893

    
1894

    
1895
def renew_token(sender, instance, **kwargs):
1896
    if not instance.auth_token:
1897
        instance.renew_token()
1898
pre_save.connect(renew_token, sender=AstakosUser)
1899
pre_save.connect(renew_token, sender=Service)