Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 65360c65

History | View | Annotate | Download (63.3 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
)
54
from django.contrib.contenttypes.models import ContentType
55

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

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

    
85
from astakos.im.notifications import build_notification
86

    
87
import astakos.im.messages as astakos_messages
88

    
89
logger = logging.getLogger(__name__)
90

    
91
DEFAULT_CONTENT_TYPE = None
92
_content_type = None
93

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

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

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

    
108
RESOURCE_SEPARATOR = '.'
109

    
110
inf = float('inf')
111

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

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

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

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

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

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

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

    
156

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

    
161

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

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

    
176

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

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

    
183

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
340
class AstakosUserManager(UserManager):
341

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

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

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

    
367

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

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

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

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

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

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

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

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

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

    
406
    objects = AstakosUserManager()
407

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
645
        return True
646

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

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

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

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

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

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

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

    
679
        pending.delete()
680
        return provider
681

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

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

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

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

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

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

    
713
        return providers
714

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

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

    
726

    
727
class AstakosUserAuthProviderManager(models.Manager):
728

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

    
732

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

    
749
    objects = AstakosUserAuthProviderManager()
750

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

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

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

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

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

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

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

    
782

    
783

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

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

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

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

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

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

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

    
848
    update_or_create = _update_or_create
849

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

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

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

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

    
870

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

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

    
880

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

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

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

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

    
907

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

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

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

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

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

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

    
949

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

    
959
    objects = EmailChangeManager()
960

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

    
965

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

    
973

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

    
983

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1104

    
1105
### PROJECTS ###
1106
################
1107

    
1108

    
1109
class SyncedModel(models.Model):
1110

    
1111
    new_state       = models.BigIntegerField()
1112
    synced_state    = models.BigIntegerField()
1113
    STATUS_SYNCED   = 0
1114
    STATUS_PENDING  = 1
1115
    sync_status     = models.IntegerField(db_index=True)
1116

    
1117
    class Meta:
1118
        abstract = True
1119

    
1120
    class NotSynced(Exception):
1121
        pass
1122

    
1123
    def sync_init_state(self, state):
1124
        self.synced_state = state
1125
        self.new_state = state
1126
        self.sync_status = self.STATUS_SYNCED
1127

    
1128
    def sync_get_status(self):
1129
        return self.sync_status
1130

    
1131
    def sync_set_status(self):
1132
        if self.new_state != self.synced_state:
1133
            self.sync_status = self.STATUS_PENDING
1134
        else:
1135
            self.sync_status = self.STATUS_SYNCED
1136

    
1137
    def sync_set_synced(self):
1138
        self.synced_state = self.new_state
1139
        self.sync_status = self.STATUS_SYNCED
1140

    
1141
    def sync_get_synced_state(self):
1142
        return self.synced_state
1143

    
1144
    def sync_set_new_state(self, new_state):
1145
        self.new_state = new_state
1146
        self.sync_set_status()
1147

    
1148
    def sync_get_new_state(self):
1149
        return self.new_state
1150

    
1151
    def sync_set_synced_state(self, synced_state):
1152
        self.synced_state = synced_state
1153
        self.sync_set_status()
1154

    
1155
    def sync_get_pending_objects(self):
1156
        return self.objects.filter(sync_status=self.STATUS_PENDING)
1157

    
1158
    def sync_get_synced_objects(self):
1159
        return self.objects.filter(sync_status=self.STATUS_SYNCED)
1160

    
1161
    def sync_verify_get_synced_state(self):
1162
        status = self.sync_get_status()
1163
        state = self.sync_get_synced_state()
1164
        verified = (status == self.STATUS_SYNCED)
1165
        return state, verified
1166

    
1167

    
1168
class ProjectResourceGrant(models.Model):
1169

    
1170
    resource = models.ForeignKey(Resource)
1171
    project_application = models.ForeignKey(ProjectApplication, blank=True)
1172
    project_capacity     = models.BigIntegerField(null=True)
1173
    project_import_limit = models.BigIntegerField(null=True)
1174
    project_export_limit = models.BigIntegerField(null=True)
1175
    member_capacity      = models.BigIntegerField(null=True)
1176
    member_import_limit  = models.BigIntegerField(null=True)
1177
    member_export_limit  = models.BigIntegerField(null=True)
1178

    
1179
    objects = ExtendedManager()
1180

    
1181
    class Meta:
1182
        unique_together = ("resource", "project_application")
1183

    
1184

    
1185
class ProjectApplication(models.Model):
1186
    states_list = [PENDING, APPROVED, REPLACED, UNKNOWN]
1187
    states = dict((k, v) for v, k in enumerate(states_list))
1188

    
1189
    applicant = models.ForeignKey(
1190
        AstakosUser,
1191
        related_name='my_project_applications',
1192
        db_index=True)
1193
    owner = models.ForeignKey(
1194
        AstakosUser,
1195
        related_name='own_project_applications',
1196
        db_index=True
1197
    )
1198
    precursor_application = models.OneToOneField('ProjectApplication',
1199
        null=True,
1200
        blank=True,
1201
        db_index=True
1202
    )
1203
    state = models.CharField(max_length=80, default=UNKNOWN)
1204

    
1205
    name = models.CharField(max_length=80)
1206
    homepage = models.URLField(max_length=255, null=True, blank=True)
1207
    description = models.TextField(null=True)
1208
    start_date = models.DateTimeField()
1209
    end_date = models.DateTimeField()
1210
    member_join_policy = models.ForeignKey(MemberJoinPolicy)
1211
    member_leave_policy = models.ForeignKey(MemberLeavePolicy)
1212
    limit_on_members_number = models.PositiveIntegerField(null=True,blank=True)
1213
    resource_grants = models.ManyToManyField(
1214
        Resource,
1215
        null=True,
1216
        blank=True,
1217
        through='ProjectResourceGrant'
1218
    )
1219
    comments = models.TextField(null=True, blank=True)
1220
    issue_date = models.DateTimeField()
1221
    
1222
    def add_resource_policy(self, service, resource, uplimit, update=True):
1223
        """Raises ObjectDoesNotExist, IntegrityError"""
1224
        resource = Resource.objects.get(service__name=service, name=resource)
1225
        if update:
1226
            ProjectResourceGrant.objects.update_or_create(
1227
                project_definition=self,
1228
                resource=resource,
1229
                defaults={'member_limit': uplimit}
1230
            )
1231
        else:
1232
            q = self.projectresourcegrant_set
1233
            q.create(resource=resource, member_limit=uplimit)
1234

    
1235
    @property
1236
    def resource_policies(self):
1237
        return self.projectresourcegrant_set.all()
1238

    
1239
    @resource_policies.setter
1240
    def resource_policies(self, policies):
1241
        for p in policies:
1242
            service = p.get('service', None)
1243
            resource = p.get('resource', None)
1244
            uplimit = p.get('uplimit', 0)
1245
            update = p.get('update', True)
1246
            self.add_resource_policy(service, resource, uplimit, update)
1247
    
1248
    @property
1249
    def follower(self):
1250
        try:
1251
            return ProjectApplication.objects.get(precursor_application=self)
1252
        except ProjectApplication.DoesNotExist:
1253
            return
1254

    
1255
    @staticmethod
1256
    def submit_view(definition, resource_policies, applicant, comments,
1257
               precursor_application=None):
1258

    
1259
        application = ProjectApplication()
1260
        if precursor_application:
1261
            application.precursor_application = precursor_application
1262
            application.owner = precursor_application.owner
1263
        else:
1264
            application.owner = applicant
1265

    
1266
        application.definition = definition
1267
        application.definition.id = None
1268
        application.applicant = applicant
1269
        application.comments = comments
1270
        application.issue_date = datetime.now()
1271
        application.state = PENDING
1272
        application.save()
1273
        application.definition.resource_policies = resource_policies
1274
        
1275
        try:
1276
            notification = build_notification(
1277
                settings.SERVER_EMAIL,
1278
                [i[1] for i in settings.ADMINS],
1279
                _(PROJECT_CREATION_SUBJECT) % application.definition.__dict__,
1280
                template='im/projects/project_creation_notification.txt',
1281
                dictionary={'object':application}
1282
            ).send()
1283
        except NotificationError, e:
1284
            logger.error(e.messages)
1285

    
1286
        return application
1287

    
1288
    def _get_project(self):
1289
        precursor = self
1290
        while precursor:
1291
            try:
1292
                project = precursor.project
1293
                return project
1294
            except Project.DoesNotExist:
1295
                pass
1296
            precursor = precursor.precursor_application
1297

    
1298
        return None
1299

    
1300
    def approve(self, approval_user=None):
1301
        """
1302
        If approval_user then during owner membership acceptance
1303
        it is checked whether the request_user is eligible.
1304

1305
        Raises:
1306
            PermissionDenied
1307
        """
1308

    
1309
        if not transaction.is_managed():
1310
            raise AssertionError("NOPE")
1311

    
1312
        new_project_name = self.definition.name
1313
        if self.state != PENDING:
1314
            m = _("cannot approve: project '%s' in state '%s'") % (
1315
                    new_project_name, self.state)
1316
            raise PermissionDenied(m) # invalid argument
1317

    
1318
        now = datetime.now()
1319
        project = self._get_project()
1320
        if project is None:
1321
            try:
1322
                conflicting_project = Project.objects.get(name=new_project_name)
1323
                if conflicting_project.is_alive:
1324
                    m = _("cannot approve: project with name '%s' "
1325
                          "already exists (serial: %s)") % (
1326
                            new_project_name, conflicting_project.id)
1327
                    raise PermissionDenied(m) # invalid argument
1328
            except Project.DoesNotExist:
1329
                pass
1330
            project = Project(creation_date=now)
1331

    
1332
        project.last_application_approved = self
1333
        project.last_approval_date = now
1334
        #ProjectMembership.add_to_project(self)
1335
        project.add_member(self.owner)
1336
        project.save()
1337

    
1338
        precursor = self.precursor_application
1339
        while precursor:
1340
            precursor.state = REPLACED
1341
            precursor.save()
1342
            precursor = precursor.precursor_application
1343

    
1344
        self.state = APPROVED
1345
        self.save()
1346

    
1347
        transaction.commit()
1348
        project.check_sync()
1349

    
1350
    def approve_view():
1351
        rejected = self.project.sync()
1352
        
1353
        try:
1354
            notification = build_notification(
1355
                settings.SERVER_EMAIL,
1356
                [self.owner.email],
1357
                _(PROJECT_APPROVED_SUBJECT) % self.definition.__dict__,
1358
                template='im/projects/project_approval_notification.txt',
1359
                dictionary={'object':self}
1360
            ).send()
1361
        except NotificationError, e:
1362
            logger.error(e.messages)
1363

    
1364

    
1365
class Project(SyncedModel):
1366
    application                 =   models.OneToOneField(
1367
                                            ProjectApplication,
1368
                                            related_name='project',
1369
                                            null=True)
1370
    last_application_approved   =   models.OneToOneField(
1371
                                            ProjectApplication,
1372
                                            related_name='last_project')
1373
    last_approval_date          =   models.DateTimeField(null=True)
1374

    
1375
    members                     =   models.ManyToManyField(
1376
                                            AstakosUser,
1377
                                            through='ProjectMembership')
1378

    
1379
    termination_start_date      =   models.DateTimeField(null=True)
1380
    termination_date            =   models.DateTimeField(null=True)
1381

    
1382
    creation_date               =   models.DateTimeField()
1383
    name                        =   models.CharField(
1384
                                            max_length=80,
1385
                                            db_index=True,
1386
                                            unique=True)
1387

    
1388
    @property
1389
    def current_application(self):
1390
        return self.application or self.last_application_approved
1391
    
1392
    @property
1393
    def violated_resource_grants(self):
1394
        if self.application is None:
1395
            return True
1396
        # do something
1397
        return False
1398
    
1399
    @property
1400
    def violated_members_number_limit(self):
1401
        application = self.application
1402
        if application is None:
1403
            return True
1404
        return len(self.approved_members) <= application.limit_on_members_number
1405
        
1406
    @property
1407
    def is_terminated(self):
1408
        return bool(self.termination)
1409
    
1410
    @property
1411
    def is_still_approved(self):
1412
        return bool(self.last_approval_date)
1413

    
1414
    @property
1415
    def is_active(self):
1416
        if (self.is_terminated or
1417
            not self.is_still_approved or
1418
            self.violated_resource_grants):
1419
            return False
1420
#         if self.violated_members_number_limit:
1421
#             return False
1422
        return True
1423
    
1424
    @property
1425
    def is_suspended(self):
1426
        if (self.is_terminated or
1427
            self.is_still_approved or
1428
            not self.violated_resource_grants):
1429
            return False
1430
#             if not self.violated_members_number_limit:
1431
#                 return False
1432
        return True
1433
    
1434
    @property
1435
    def is_alive(self):
1436
        return self.is_active or self.is_suspended
1437
    
1438
    @property
1439
    def is_inconsistent(self):
1440
        now = datetime.now()
1441
        if self.creation_date > now:
1442
            return True
1443
        if self.last_approval_date > now:
1444
            return True
1445
        if self.terminaton_date > now:
1446
            return True
1447
        return False
1448
    
1449
    @property
1450
    def approved_members(self):
1451
        return [m.person for m in self.projectmembership_set.filter(~Q(acceptance_date=None))]
1452

    
1453
    def sync(self, specific_members=()):
1454
        if self.is_synchronized:
1455
            return
1456
        members = specific_members or self.approved_members
1457
        c, rejected = send_quota(self.approved_members)
1458
        if not rejected:
1459
            self.application = self.last_application_approved
1460
            self.save()
1461
        return rejected
1462

    
1463
    def set_pending_membership_sync(self):
1464
        self.membership_dirty = True
1465
        self.save()
1466

    
1467
    def set_state(self):
1468
        PROJECT_SYNCHRONIZED = 0
1469
        PROJECT_SYNC_PENDING_MEMBERSHIP = (1 << 0)
1470
        PROJECT_SYNC_PENDING_DEFINITION = (1 << 1)
1471
        PROJECT_SYNC_PENDING = (PROJECT_SYNC_PENDING_DEFINITION | 
1472
                                PROJECT_SYNC_PENDING_MEMBERSHIP)
1473

    
1474
        oldstate = self.state
1475
        state = PROJECT_SYNCHRONIZED
1476

    
1477
        if self.last_application_approved != self.application:
1478
            state |= PROJECT_SYNC_PENDING_DEFINITION
1479

    
1480
        if self.membership_dirty:
1481
            state |= PROJECT_SYNC_PENDING_MEMBERSHIP
1482

    
1483
        if oldstate != state:
1484
            self.state = state
1485
            self.save()
1486
        return state
1487

    
1488
    def check_sync(self, hint=None):
1489
        state = self.set_state()
1490
        if state: # needs syncing
1491
            if self.sync_membership():
1492
                self.set_sta
1493

    
1494
    def sync_membership(self, members=None):
1495
        members = members if members is not None else self.approved_members
1496
        rejected = send_quota(members)
1497
        success = not rejected
1498
        if success:
1499
            self.members
1500
        return success
1501

    
1502
    def add_member(self, user):
1503
        """
1504
        Raises:
1505
            django.exceptions.PermissionDenied
1506
            astakos.im.models.AstakosUser.DoesNotExist
1507
        """
1508
        if isinstance(user, int):
1509
            user = AstakosUser.objects.get(user=user)
1510

    
1511
        m, created = ProjectMembership.objects.get_or_create(
1512
            person=user, project=self
1513
        )
1514
        m.accept()
1515

    
1516
    def remove_member(self, user):
1517
        """
1518
        Raises:
1519
            django.exceptions.PermissionDenied
1520
            astakos.im.models.AstakosUser.DoesNotExist
1521
            astakos.im.models.ProjectMembership.DoesNotExist
1522
        """
1523
        if isinstance(user, int):
1524
            user = AstakosUser.objects.get(user=user)
1525

    
1526
        m = ProjectMembership.objects.get(person=user, project=self)
1527
        m.remove()
1528

    
1529
    def terminate(self):
1530
        self.termination_start_date = datetime.now()
1531
        self.terminaton_date = None
1532
        self.save()
1533
        
1534
        rejected = self.sync()
1535
        if not rejected:
1536
            self.termination_start_date = None
1537
            self.termination_date = datetime.now()
1538
            self.save()
1539
            
1540
        try:
1541
            notification = build_notification(
1542
                settings.SERVER_EMAIL,
1543
                [self.current_application.owner.email],
1544
                _(PROJECT_TERMINATION_SUBJECT) % self.definition.__dict__,
1545
                template='im/projects/project_termination_notification.txt',
1546
                dictionary={'object':self.current_application}
1547
            ).send()
1548
        except NotificationError, e:
1549
            logger.error(e.messages)
1550

    
1551
    def suspend(self):
1552
        self.last_approval_date = None
1553
        self.save()
1554
        self.sync()
1555

    
1556
        try:
1557
            notification = build_notification(
1558
                settings.SERVER_EMAIL,
1559
                [self.current_application.owner.email],
1560
                _(PROJECT_SUSPENSION_SUBJECT) % self.definition.__dict__,
1561
                template='im/projects/project_suspension_notification.txt',
1562
                dictionary={'object':self.current_application}
1563
            ).send()
1564
        except NotificationError, e:
1565
            logger.error(e.messages)
1566

    
1567

    
1568
QuotaLimits = namedtuple('QuotaLimits', ('holder',
1569
                                         'capacity',
1570
                                         'import_limit',
1571
                                         'export_limit'))
1572

    
1573

    
1574

    
1575
class ExclusiveOrRaise(object):
1576
    """Context Manager to exclusively execute a critical code section.
1577
       The exclusion must be global.
1578
       (IPC semaphores will not protect across OS,
1579
        DB locks will if it's the same DB)
1580
    """
1581

    
1582
    class Busy(Exception):
1583
        pass
1584

    
1585
    def __init__(self, locked=False):
1586
        init = 0 if locked else 1
1587
        from multiprocess import Semaphore
1588
        self._sema = Semaphore(init)
1589

    
1590
    def enter(self):
1591
        acquired = self._sema.acquire(False)
1592
        if not acquired:
1593
            raise self.Busy()
1594

    
1595
    def leave(self):
1596
        self._sema.release()
1597

    
1598
    def __enter__(self):
1599
        self.enter()
1600
        return self
1601

    
1602
    def __exit__(self, exc_type, exc_value, exc_traceback):
1603
        self.leave()
1604

    
1605

    
1606
exclusive_or_raise = ExclusiveOrRaise(locked=False)
1607

    
1608

    
1609
class ProjectMembership(SyncedModel):
1610
    person = models.ForeignKey(AstakosUser)
1611
    project = models.ForeignKey(Project)
1612
    request_date = models.DateField(default=datetime.now())
1613

    
1614
    acceptance_date = models.DateField(null=True, db_index=True)
1615
    leave_request_date = models.DateField(null=True)
1616

    
1617
    REQUESTED   =   0
1618
    ACCEPTED    =   1
1619
    REMOVED     =   2
1620
    REJECTED    =   3   # never seen, because .delete()
1621

    
1622
    class Meta:
1623
        unique_together = ("person", "project")
1624

    
1625
    def __str__(self):
1626
        return _("<'%s' membership in project '%s'>") % (
1627
                self.person.username, self.project.application)
1628

    
1629
    __repr__ = __str__
1630

    
1631
    def __init__(self, *args, **kwargs):
1632
        self.sync_init_state(self.REQUEST)
1633
        super(ProjectMembership, self).__init__(*args, **kwargs)
1634

    
1635
    def _set_history_item(self, reason, date=None):
1636
        if isinstance(reason, basestring):
1637
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1638

    
1639
        history_item = ProjectMembershipHistory(
1640
                            serial=self.id,
1641
                            person=self.person,
1642
                            project=self.project,
1643
                            date=date,
1644
                            reason=reason)
1645
        history_item.save()
1646
        serial = history_item.id
1647

    
1648
    def accept(self):
1649
        state, verified = self.sync_verify_get_synced_state()
1650
        if not verified:
1651
            new_state = self.sync_get_new_state()
1652
            m = _("%s: cannot accept: not synched (%s -> %s)") % (
1653
                    self, state, new_state)
1654
            raise self.NotSynced(m)
1655

    
1656
        if state != self.REQUESTED:
1657
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1658
            raise AssertionError(m)
1659

    
1660
        now = datetime.now()
1661
        self.acceptance_date = now
1662
        self._set_history_item(reason='ACCEPT', date=now)
1663
        self.sync_set_new_state(self.ACCEPTED)
1664
        self.save()
1665

    
1666
    def remove(self):
1667
        state, verified = self.sync_verify_get_synced_state()
1668
        if not verified:
1669
            new_state = self.sync_get_new_state()
1670
            m = _("%s: cannot remove: not synched (%s -> %s)") % (
1671
                    self, state, new_state)
1672
            raise self.NotSynced(m)
1673

    
1674
        if state != self.ACCEPTED:
1675
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1676
            raise AssertionError(m)
1677

    
1678
        serial = self._set_history_item(reason='REMOVE')
1679
        self.sync_set_new_state(self.REMOVED)
1680
        self.save()
1681

    
1682
    def reject(self):
1683
        state, verified = self.sync_verify_get_synced_state()
1684
        if not verified:
1685
            new_state = self.sync_get_new_state()
1686
            m = _("%s: cannot reject: not synched (%s -> %s)") % (
1687
                    self, state, new_state))
1688
            raise self.NotSynced(m)
1689

    
1690
        if state != self.REQUESTED:
1691
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1692
            raise AssertionError(m)
1693

    
1694
        # rejected requests don't need sync,
1695
        # because they were never effected
1696
        self._set_history_item(reason='REJECT')
1697
        self.delete()
1698

    
1699
    def get_quotas(self, limits_list=None, factor=1):
1700
        holder = self.person.username
1701
        if limits_list is None:
1702
            limits_list = []
1703
        append = limits_list.append
1704
        all_grants = self.project.application.resource_grants.all()
1705
        for grant in all_grants:
1706
            append(QuotaLimits(holder       = holder,
1707
                               resource     = grant.resource.name,
1708
                               capacity     = factor * grant.member_capacity,
1709
                               import_limit = factor * grant.member_import_limit,
1710
                               export_limit = factor * grant.member_export_limit))
1711
        return limits_list
1712

    
1713
    def do_sync(self):
1714
        state = self.sync_get_synced_state()
1715
        new_state = self.sync_get_new_state()
1716

    
1717
        if state == self.REQUESTED and new_state == self.ACCEPTED:
1718
            factor = 1
1719
        elif state == self.ACCEPTED and new_state == self.REMOVED:
1720
            factor = -1
1721
        else:
1722
            m = _("%s: sync: called on invalid state ('%s' -> '%s')") % (
1723
                    self, state, new_state)
1724
            raise AssertionError(m)
1725

    
1726
        quotas = self.get_quotas(factor=factor)
1727
        try:
1728
            failure = add_quotas(quotas)
1729
            if failure:
1730
                m = "%s: sync: add_quotas failed" % (self,)
1731
                raise RuntimeError(m)
1732
        except Exception:
1733
            raise
1734
        else:
1735
            self.sync_set_synced()
1736

    
1737
        if new_state == self.REMOVED:
1738
            self.delete()
1739

    
1740
    def sync(self):
1741
        with exclusive_or_raise:
1742
            self.do_sync()
1743

    
1744

    
1745
class ProjectMembershipHistory(models.Model):
1746
    reasons_list = ['ACCEPT', 'REJECT', 'REMOVE']
1747
    reasons = dict((k, v) for v, k in enumerate(reasons_list))
1748
    person = models.ForeignKey(AstakosUser)
1749
    project = models.ForeignKey(Project)
1750
    date = models.DateField(default=datetime.now)
1751
    reason = models.IntegerField()
1752
    serial = models.BigIntegerField()
1753

    
1754

    
1755
def filter_queryset_by_property(q, property):
1756
    """
1757
    Incorporate list comprehension for filtering querysets by property
1758
    since Queryset.filter() operates on the database level.
1759
    """
1760
    return (p for p in q if getattr(p, property, False))
1761

    
1762
def get_alive_projects():
1763
    return filter_queryset_by_property(
1764
        Project.objects.all(),
1765
        'is_alive'
1766
    )
1767

    
1768
def get_active_projects():
1769
    return filter_queryset_by_property(
1770
        Project.objects.all(),
1771
        'is_active'
1772
    )
1773

    
1774
def _create_object(model, **kwargs):
1775
    o = model.objects.create(**kwargs)
1776
    o.save()
1777
    return o
1778

    
1779

    
1780
def create_astakos_user(u):
1781
    try:
1782
        AstakosUser.objects.get(user_ptr=u.pk)
1783
    except AstakosUser.DoesNotExist:
1784
        extended_user = AstakosUser(user_ptr_id=u.pk)
1785
        extended_user.__dict__.update(u.__dict__)
1786
        extended_user.save()
1787
        if not extended_user.has_auth_provider('local'):
1788
            extended_user.add_auth_provider('local')
1789
    except BaseException, e:
1790
        logger.exception(e)
1791

    
1792

    
1793
def fix_superusers(sender, **kwargs):
1794
    # Associate superusers with AstakosUser
1795
    admins = User.objects.filter(is_superuser=True)
1796
    for u in admins:
1797
        create_astakos_user(u)
1798
post_syncdb.connect(fix_superusers)
1799

    
1800

    
1801
def user_post_save(sender, instance, created, **kwargs):
1802
    if not created:
1803
        return
1804
    create_astakos_user(instance)
1805
post_save.connect(user_post_save, sender=User)
1806

    
1807

    
1808
def astakosuser_pre_save(sender, instance, **kwargs):
1809
    instance.aquarium_report = False
1810
    instance.new = False
1811
    try:
1812
        db_instance = AstakosUser.objects.get(id=instance.id)
1813
    except AstakosUser.DoesNotExist:
1814
        # create event
1815
        instance.aquarium_report = True
1816
        instance.new = True
1817
    else:
1818
        get = AstakosUser.__getattribute__
1819
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1820
                   BILLING_FIELDS)
1821
        instance.aquarium_report = True if l else False
1822
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1823

    
1824
def set_default_group(user):
1825
    try:
1826
        default = AstakosGroup.objects.get(name='default')
1827
        Membership(
1828
            group=default, person=user, date_joined=datetime.now()).save()
1829
    except AstakosGroup.DoesNotExist, e:
1830
        logger.exception(e)
1831

    
1832

    
1833
def astakosuser_post_save(sender, instance, created, **kwargs):
1834
    if instance.aquarium_report:
1835
        report_user_event(instance, create=instance.new)
1836
    if not created:
1837
        return
1838
    set_default_group(instance)
1839
    # TODO handle socket.error & IOError
1840
    register_users((instance,))
1841
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1842

    
1843

    
1844
def resource_post_save(sender, instance, created, **kwargs):
1845
    if not created:
1846
        return
1847
    register_resources((instance,))
1848
post_save.connect(resource_post_save, sender=Resource)
1849

    
1850

    
1851
def on_quota_disturbed(sender, users, **kwargs):
1852
#     print '>>>', locals()
1853
    if not users:
1854
        return
1855
    send_quota(users)
1856

    
1857
quota_disturbed = Signal(providing_args=["users"])
1858
quota_disturbed.connect(on_quota_disturbed)
1859

    
1860

    
1861
def send_quota_disturbed(sender, instance, **kwargs):
1862
    users = []
1863
    extend = users.extend
1864
    if sender == Membership:
1865
        if not instance.group.is_enabled:
1866
            return
1867
        extend([instance.person])
1868
    elif sender == AstakosUserQuota:
1869
        extend([instance.user])
1870
    elif sender == AstakosGroupQuota:
1871
        if not instance.group.is_enabled:
1872
            return
1873
        extend(instance.group.astakosuser_set.all())
1874
    elif sender == AstakosGroup:
1875
        if not instance.is_enabled:
1876
            return
1877
    quota_disturbed.send(sender=sender, users=users)
1878
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1879
post_delete.connect(send_quota_disturbed, sender=Membership)
1880
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1881
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1882
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1883
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1884

    
1885

    
1886
def renew_token(sender, instance, **kwargs):
1887
    if not instance.auth_token:
1888
        instance.renew_token()
1889
pre_save.connect(renew_token, sender=AstakosUser)
1890
pre_save.connect(renew_token, sender=Service)
1891

    
1892

    
1893
def check_closed_join_membership_policy(sender, instance, **kwargs):
1894
    if instance.id:
1895
        return
1896
    if instance.person == instance.project.current_application.owner:
1897
        return
1898
    join_policy = instance.project.current_application.definition.member_join_policy
1899
    if join_policy == get_closed_join():
1900
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1901
pre_save.connect(check_closed_join_membership_policy, sender=ProjectMembership)
1902

    
1903

    
1904
def check_auto_accept_join_membership_policy(sender, instance, created, **kwargs):
1905
    if not created:
1906
        return
1907
    join_policy = instance.project.current_application.definition.member_join_policy
1908
    if join_policy == get_auto_accept_join() and not instance.acceptance_date:
1909
        instance.accept()
1910
post_save.connect(check_auto_accept_join_membership_policy, sender=ProjectMembership)