Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (86.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
import json
38
import math
39

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

    
48
from django.db import models, IntegrityError, transaction
49
from django.contrib.auth.models import User, UserManager, Group, Permission
50
from django.utils.translation import ugettext as _
51
from django.core.exceptions import ValidationError
52
from django.db.models.signals import (
53
    pre_save, post_save, post_syncdb, post_delete)
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.utils.safestring import mark_safe
64
from django.core.validators import email_re
65
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
66

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
70
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA,
71
    PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES)
72
from astakos.im import settings as astakos_settings
73
from astakos.im.endpoints.qh import (
74
    register_users, send_quotas, qh_check_users, qh_get_quota_limits,
75
    register_services, register_resources, qh_add_quota, QuotaLimits,
76
    qh_query_serials, qh_ack_serials,
77
    QuotaValues, add_quota_values)
78
from astakos.im import auth_providers
79

    
80
import astakos.im.messages as astakos_messages
81
from astakos.im.lock import with_lock
82
from .managers import ForUpdateManager
83

    
84
from synnefo.lib.quotaholder.api import QH_PRACTICALLY_INFINITE
85
from synnefo.lib.db.intdecimalfield import intDecimalField
86

    
87
logger = logging.getLogger(__name__)
88

    
89
DEFAULT_CONTENT_TYPE = None
90
_content_type = None
91

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

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

    
104
RESOURCE_SEPARATOR = '.'
105

    
106
inf = float('inf')
107

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

    
119
    class Meta:
120
        ordering = ('order', )
121

    
122
    def renew_token(self, expiration_date=None):
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
        if expiration_date:
131
            self.auth_token_expires = expiration_date
132
        else:
133
            self.auth_token_expires = None
134

    
135
    def __str__(self):
136
        return self.name
137

    
138
    @property
139
    def resources(self):
140
        return self.resource_set.all()
141

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

    
147

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

    
152
_presentation_data = {}
153
def get_presentation(resource):
154
    global _presentation_data
155
    presentation = _presentation_data.get(resource, {})
156
    if not presentation:
157
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
158
        presentation = resource_presentation.get(resource, {})
159
        _presentation_data[resource] = presentation
160
    return presentation
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
    uplimit = intDecimalField(default=0)
170

    
171
    class Meta:
172
        unique_together = ("service", "name")
173

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

    
177
    def full_name(self):
178
        return str(self)
179

    
180
    @property
181
    def help_text(self):
182
        return get_presentation(str(self)).get('help_text', '')
183

    
184
    @property
185
    def help_text_input_each(self):
186
        return get_presentation(str(self)).get('help_text_input_each', '')
187

    
188
    @property
189
    def is_abbreviation(self):
190
        return get_presentation(str(self)).get('is_abbreviation', False)
191

    
192
    @property
193
    def report_desc(self):
194
        return get_presentation(str(self)).get('report_desc', '')
195

    
196
    @property
197
    def placeholder(self):
198
        return get_presentation(str(self)).get('placeholder', '')
199

    
200
    @property
201
    def verbose_name(self):
202
        return get_presentation(str(self)).get('verbose_name', '')
203

    
204
    @property
205
    def display_name(self):
206
        name = self.verbose_name
207
        if self.is_abbreviation:
208
            name = name.upper()
209
        return name
210

    
211
    @property
212
    def pluralized_display_name(self):
213
        if not self.unit:
214
            return '%ss' % self.display_name
215
        return self.display_name
216

    
217

    
218
def load_service_resources():
219
    ss = []
220
    rs = []
221
    for service_name, data in SERVICES.iteritems():
222
        url = data.get('url')
223
        resources = data.get('resources') or ()
224
        service, created = Service.objects.get_or_create(
225
            name=service_name,
226
            defaults={'url': url}
227
        )
228
        ss.append(service)
229

    
230
        for resource in resources:
231
            try:
232
                resource_name = resource.pop('name', '')
233
                r, created = Resource.objects.get_or_create(
234
                    service=service,
235
                    name=resource_name,
236
                    defaults=resource)
237
                rs.append(r)
238

    
239
            except Exception, e:
240
                print "Cannot create resource ", resource_name
241
                continue
242
    register_services(ss)
243
    register_resources(rs)
244

    
245
def _quota_values(capacity):
246
    return QuotaValues(
247
        quantity = 0,
248
        capacity = capacity,
249
        import_limit = QH_PRACTICALLY_INFINITE,
250
        export_limit = QH_PRACTICALLY_INFINITE)
251

    
252
def get_default_quota():
253
    _DEFAULT_QUOTA = {}
254
    resources = Resource.objects.all()
255
    for resource in resources:
256
        capacity = resource.uplimit
257
        limits = _quota_values(capacity)
258
        _DEFAULT_QUOTA[resource.full_name()] = limits
259

    
260
    return _DEFAULT_QUOTA
261

    
262
def get_resource_names():
263
    _RESOURCE_NAMES = []
264
    resources = Resource.objects.all()
265
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
266
    return _RESOURCE_NAMES
267

    
268

    
269
class AstakosUserManager(UserManager):
270

    
271
    def get_auth_provider_user(self, provider, **kwargs):
272
        """
273
        Retrieve AstakosUser instance associated with the specified third party
274
        id.
275
        """
276
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
277
                          kwargs.iteritems()))
278
        return self.get(auth_providers__module=provider, **kwargs)
279

    
280
    def get_by_email(self, email):
281
        return self.get(email=email)
282

    
283
    def get_by_identifier(self, email_or_username, **kwargs):
284
        try:
285
            return self.get(email__iexact=email_or_username, **kwargs)
286
        except AstakosUser.DoesNotExist:
287
            return self.get(username__iexact=email_or_username, **kwargs)
288

    
289
    def user_exists(self, email_or_username, **kwargs):
290
        qemail = Q(email__iexact=email_or_username)
291
        qusername = Q(username__iexact=email_or_username)
292
        qextra = Q(**kwargs)
293
        return self.filter((qemail | qusername) & qextra).exists()
294

    
295
    def verified_user_exists(self, email_or_username):
296
        return self.user_exists(email_or_username, email_verified=True)
297

    
298
    def verified(self):
299
        return self.filter(email_verified=True)
300

    
301
    def verified(self):
302
        return self.filter(email_verified=True)
303

    
304
    def uuid_catalog(self, l=None):
305
        """
306
        Returns a uuid to username mapping for the uuids appearing in l.
307
        If l is None returns the mapping for all existing users.
308
        """
309
        q = self.filter(uuid__in=l) if l != None else self
310
        return dict(q.values_list('uuid', 'username'))
311

    
312
    def displayname_catalog(self, l=None):
313
        """
314
        Returns a username to uuid mapping for the usernames appearing in l.
315
        If l is None returns the mapping for all existing users.
316
        """
317
        if l is not None:
318
            lmap = dict((x.lower(), x) for x in l)
319
            q = self.filter(username__in=lmap.keys())
320
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
321
        else:
322
            q = self
323
            values = self.values_list('username', 'uuid')
324
        return dict(values)
325

    
326

    
327

    
328
class AstakosUser(User):
329
    """
330
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
331
    """
332
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
333
                                   null=True)
334

    
335
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
336
    #                    AstakosUserProvider model.
337
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
338
                                null=True)
339
    # ex. screen_name for twitter, eppn for shibboleth
340
    third_party_identifier = models.CharField(_('Third-party identifier'),
341
                                              max_length=255, null=True,
342
                                              blank=True)
343

    
344

    
345
    #for invitations
346
    user_level = DEFAULT_USER_LEVEL
347
    level = models.IntegerField(_('Inviter level'), default=user_level)
348
    invitations = models.IntegerField(
349
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
350

    
351
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
352
                                  null=True, blank=True, help_text = _( 'test' ))
353
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
354
    auth_token_expires = models.DateTimeField(
355
        _('Token expiration date'), null=True)
356

    
357
    updated = models.DateTimeField(_('Update date'))
358
    is_verified = models.BooleanField(_('Is verified?'), default=False)
359

    
360
    email_verified = models.BooleanField(_('Email verified?'), default=False)
361

    
362
    has_credits = models.BooleanField(_('Has credits?'), default=False)
363
    has_signed_terms = models.BooleanField(
364
        _('I agree with the terms'), default=False)
365
    date_signed_terms = models.DateTimeField(
366
        _('Signed terms date'), null=True, blank=True)
367

    
368
    activation_sent = models.DateTimeField(
369
        _('Activation sent data'), null=True, blank=True)
370

    
371
    policy = models.ManyToManyField(
372
        Resource, null=True, through='AstakosUserQuota')
373

    
374
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
375

    
376
    __has_signed_terms = False
377
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
378
                                           default=False, db_index=True)
379

    
380
    objects = AstakosUserManager()
381

    
382
    def __init__(self, *args, **kwargs):
383
        super(AstakosUser, self).__init__(*args, **kwargs)
384
        self.__has_signed_terms = self.has_signed_terms
385
        if not self.id:
386
            self.is_active = False
387

    
388
    @property
389
    def realname(self):
390
        return '%s %s' % (self.first_name, self.last_name)
391

    
392
    @realname.setter
393
    def realname(self, value):
394
        parts = value.split(' ')
395
        if len(parts) == 2:
396
            self.first_name = parts[0]
397
            self.last_name = parts[1]
398
        else:
399
            self.last_name = parts[0]
400

    
401
    def add_permission(self, pname):
402
        if self.has_perm(pname):
403
            return
404
        p, created = Permission.objects.get_or_create(
405
                                    codename=pname,
406
                                    name=pname.capitalize(),
407
                                    content_type=get_content_type())
408
        self.user_permissions.add(p)
409

    
410
    def remove_permission(self, pname):
411
        if self.has_perm(pname):
412
            return
413
        p = Permission.objects.get(codename=pname,
414
                                   content_type=get_content_type())
415
        self.user_permissions.remove(p)
416

    
417
    @property
418
    def invitation(self):
419
        try:
420
            return Invitation.objects.get(username=self.email)
421
        except Invitation.DoesNotExist:
422
            return None
423

    
424
    def initial_quotas(self):
425
        quotas = dict(get_default_quota())
426
        for user_quota in self.policies:
427
            resource = user_quota.resource.full_name()
428
            quotas[resource] = user_quota.quota_values()
429
        return quotas
430

    
431
    def all_quotas(self):
432
        quotas = self.initial_quotas()
433

    
434
        objects = self.projectmembership_set.select_related()
435
        memberships = objects.filter(is_active=True)
436
        for membership in memberships:
437
            application = membership.application
438
            if application is None:
439
                m = _("missing application for active membership %s"
440
                      % (membership,))
441
                raise AssertionError(m)
442

    
443
            grants = application.projectresourcegrant_set.all()
444
            for grant in grants:
445
                resource = grant.resource.full_name()
446
                prev = quotas.get(resource, 0)
447
                new = add_quota_values(prev, grant.member_quota_values())
448
                quotas[resource] = new
449
        return quotas
450

    
451
    @property
452
    def policies(self):
453
        return self.astakosuserquota_set.select_related().all()
454

    
455
    @policies.setter
456
    def policies(self, policies):
457
        for p in policies:
458
            p.setdefault('resource', '')
459
            p.setdefault('capacity', 0)
460
            p.setdefault('quantity', 0)
461
            p.setdefault('import_limit', 0)
462
            p.setdefault('export_limit', 0)
463
            p.setdefault('update', True)
464
            self.add_resource_policy(**p)
465

    
466
    def add_resource_policy(
467
            self, resource, capacity, quantity, import_limit,
468
            export_limit, update=True):
469
        """Raises ObjectDoesNotExist, IntegrityError"""
470
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
471
        resource = Resource.objects.get(service__name=s, name=r)
472
        if update:
473
            AstakosUserQuota.objects.update_or_create(
474
                user=self, resource=resource, defaults={
475
                    'capacity':capacity,
476
                    'quantity': quantity,
477
                    'import_limit':import_limit,
478
                    'export_limit':export_limit})
479
        else:
480
            q = self.astakosuserquota_set
481
            q.create(
482
                resource=resource, capacity=capacity, quanity=quantity,
483
                import_limit=import_limit, export_limit=export_limit)
484

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

    
490
    def update_uuid(self):
491
        while not self.uuid:
492
            uuid_val =  str(uuid.uuid4())
493
            try:
494
                AstakosUser.objects.get(uuid=uuid_val)
495
            except AstakosUser.DoesNotExist, e:
496
                self.uuid = uuid_val
497
        return self.uuid
498

    
499
    def save(self, update_timestamps=True, **kwargs):
500
        if update_timestamps:
501
            if not self.id:
502
                self.date_joined = datetime.now()
503
            self.updated = datetime.now()
504

    
505
        # update date_signed_terms if necessary
506
        if self.__has_signed_terms != self.has_signed_terms:
507
            self.date_signed_terms = datetime.now()
508

    
509
        self.update_uuid()
510

    
511
        if self.username != self.email.lower():
512
            # set username
513
            self.username = self.email.lower()
514

    
515
        super(AstakosUser, self).save(**kwargs)
516

    
517
    def renew_token(self, flush_sessions=False, current_key=None):
518
        md5 = hashlib.md5()
519
        md5.update(settings.SECRET_KEY)
520
        md5.update(self.username)
521
        md5.update(self.realname.encode('ascii', 'ignore'))
522
        md5.update(asctime())
523

    
524
        self.auth_token = b64encode(md5.digest())
525
        self.auth_token_created = datetime.now()
526
        self.auth_token_expires = self.auth_token_created + \
527
                                  timedelta(hours=AUTH_TOKEN_DURATION)
528
        if flush_sessions:
529
            self.flush_sessions(current_key)
530
        msg = 'Token renewed for %s' % self.email
531
        logger.log(LOGGING_LEVEL, msg)
532

    
533
    def flush_sessions(self, current_key=None):
534
        q = self.sessions
535
        if current_key:
536
            q = q.exclude(session_key=current_key)
537

    
538
        keys = q.values_list('session_key', flat=True)
539
        if keys:
540
            msg = 'Flushing sessions: %s' % ','.join(keys)
541
            logger.log(LOGGING_LEVEL, msg, [])
542
        engine = import_module(settings.SESSION_ENGINE)
543
        for k in keys:
544
            s = engine.SessionStore(k)
545
            s.flush()
546

    
547
    def __unicode__(self):
548
        return '%s (%s)' % (self.realname, self.email)
549

    
550
    def conflicting_email(self):
551
        q = AstakosUser.objects.exclude(username=self.username)
552
        q = q.filter(email__iexact=self.email)
553
        if q.count() != 0:
554
            return True
555
        return False
556

    
557
    def email_change_is_pending(self):
558
        return self.emailchanges.count() > 0
559

    
560
    @property
561
    def signed_terms(self):
562
        term = get_latest_terms()
563
        if not term:
564
            return True
565
        if not self.has_signed_terms:
566
            return False
567
        if not self.date_signed_terms:
568
            return False
569
        if self.date_signed_terms < term.date:
570
            self.has_signed_terms = False
571
            self.date_signed_terms = None
572
            self.save()
573
            return False
574
        return True
575

    
576
    def set_invitations_level(self):
577
        """
578
        Update user invitation level
579
        """
580
        level = self.invitation.inviter.level + 1
581
        self.level = level
582
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
583

    
584
    def can_login_with_auth_provider(self, provider):
585
        if not self.has_auth_provider(provider):
586
            return False
587
        else:
588
            return auth_providers.get_provider(provider).is_available_for_login()
589

    
590
    def can_add_auth_provider(self, provider, include_unverified=False, **kwargs):
591
        provider_settings = auth_providers.get_provider(provider)
592

    
593
        if not provider_settings.is_available_for_add():
594
            return False
595

    
596
        if self.has_auth_provider(provider) and \
597
           provider_settings.one_per_user:
598
            return False
599

    
600
        if 'provider_info' in kwargs:
601
            kwargs.pop('provider_info')
602

    
603
        if 'identifier' in kwargs:
604
            try:
605
                # provider with specified params already exist
606
                if not include_unverified:
607
                    kwargs['user__email_verified'] = True
608
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
609
                                                                   **kwargs)
610
            except AstakosUser.DoesNotExist:
611
                return True
612
            else:
613
                return False
614

    
615
        return True
616

    
617
    def can_remove_auth_provider(self, module):
618
        provider = auth_providers.get_provider(module)
619
        existing = self.get_active_auth_providers()
620
        existing_for_provider = self.get_active_auth_providers(module=module)
621

    
622
        if len(existing) <= 1:
623
            return False
624

    
625
        if len(existing_for_provider) == 1 and provider.is_required():
626
            return False
627

    
628
        return provider.is_available_for_remove()
629

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

    
633
    def can_change_email(self):
634
        non_astakos_local = self.get_auth_providers().filter(module='local')
635
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
636
        return non_astakos_local.count() == 0
637

    
638
    def has_required_auth_providers(self):
639
        required = auth_providers.REQUIRED_PROVIDERS
640
        for provider in required:
641
            if not self.has_auth_provider(provider):
642
                return False
643
        return True
644

    
645
    def has_auth_provider(self, provider, **kwargs):
646
        return bool(self.get_auth_providers().filter(module=provider,
647
                                               **kwargs).count())
648

    
649
    def add_auth_provider(self, provider, **kwargs):
650
        info_data = ''
651
        if 'provider_info' in kwargs:
652
            info_data = kwargs.pop('provider_info')
653
            if isinstance(info_data, dict):
654
                info_data = json.dumps(info_data)
655

    
656
        if self.can_add_auth_provider(provider, **kwargs):
657
            if 'identifier' in kwargs:
658
                # clean up third party pending for activation users of the same
659
                # identifier
660
                AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
661
                                                                **kwargs)
662
            self.auth_providers.create(module=provider, active=True,
663
                                       info_data=info_data,
664
                                       **kwargs)
665
        else:
666
            raise Exception('Cannot add provider')
667

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

    
676
        provider = self.add_auth_provider(pending.provider,
677
                               identifier=pending.third_party_identifier,
678
                                affiliation=pending.affiliation,
679
                                          provider_info=pending.info)
680

    
681
        if email_re.match(pending.email or '') and pending.email != self.email:
682
            self.additionalmail_set.get_or_create(email=pending.email)
683

    
684
        pending.delete()
685
        return provider
686

    
687
    def remove_auth_provider(self, provider, **kwargs):
688
        self.get_auth_providers().get(module=provider, **kwargs).delete()
689

    
690
    # user urls
691
    def get_resend_activation_url(self):
692
        return reverse('send_activation', kwargs={'user_id': self.pk})
693

    
694
    def get_provider_remove_url(self, module, **kwargs):
695
        return reverse('remove_auth_provider', kwargs={
696
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
697

    
698
    def get_activation_url(self, nxt=False):
699
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
700
                                 quote(self.auth_token))
701
        if nxt:
702
            url += "&next=%s" % quote(nxt)
703
        return url
704

    
705
    def get_password_reset_url(self, token_generator=default_token_generator):
706
        return reverse('django.contrib.auth.views.password_reset_confirm',
707
                          kwargs={'uidb36':int_to_base36(self.id),
708
                                  'token':token_generator.make_token(self)})
709

    
710
    def get_auth_providers(self):
711
        return self.auth_providers
712

    
713
    def get_available_auth_providers(self):
714
        """
715
        Returns a list of providers available for user to connect to.
716
        """
717
        providers = []
718
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
719
            if self.can_add_auth_provider(module):
720
                providers.append(provider_settings(self))
721

    
722
        modules = astakos_settings.IM_MODULES
723
        def key(p):
724
            if not p.module in modules:
725
                return 100
726
            return modules.index(p.module)
727
        providers = sorted(providers, key=key)
728
        return providers
729

    
730
    def get_active_auth_providers(self, **filters):
731
        providers = []
732
        for provider in self.get_auth_providers().active(**filters):
733
            if auth_providers.get_provider(provider.module).is_available_for_login():
734
                providers.append(provider)
735

    
736
        modules = astakos_settings.IM_MODULES
737
        def key(p):
738
            if not p.module in modules:
739
                return 100
740
            return modules.index(p.module)
741
        providers = sorted(providers, key=key)
742
        return providers
743

    
744
    @property
745
    def auth_providers_display(self):
746
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
747

    
748
    def get_inactive_message(self):
749
        msg_extra = ''
750
        message = ''
751
        if self.activation_sent:
752
            if self.email_verified:
753
                message = _(astakos_messages.ACCOUNT_INACTIVE)
754
            else:
755
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
756
                if astakos_settings.MODERATION_ENABLED:
757
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
758
                else:
759
                    url = self.get_resend_activation_url()
760
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
761
                                u' ' + \
762
                                _('<a href="%s">%s?</a>') % (url,
763
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
764
        else:
765
            if astakos_settings.MODERATION_ENABLED:
766
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
767
            else:
768
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
769
                url = self.get_resend_activation_url()
770
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
771
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
772

    
773
        return mark_safe(message + u' '+ msg_extra)
774

    
775
    def owns_application(self, application):
776
        return application.owner == self
777

    
778
    def owns_project(self, project):
779
        return project.application.owner == self
780

    
781
    def is_associated(self, project):
782
        try:
783
            m = ProjectMembership.objects.get(person=self, project=project)
784
            return m.state in ProjectMembership.ASSOCIATED_STATES
785
        except ProjectMembership.DoesNotExist:
786
            return False
787

    
788
    def get_membership(self, project):
789
        try:
790
            return ProjectMembership.objects.get(
791
                project=project,
792
                person=self)
793
        except ProjectMembership.DoesNotExist:
794
            return None
795

    
796
    def membership_display(self, project):
797
        m = self.get_membership(project)
798
        if m is None:
799
            return _('Not a member')
800
        else:
801
            return m.user_friendly_state_display()
802

    
803
    def non_owner_can_view(self, maybe_project):
804
        if maybe_project is None:
805
            return False
806
        project = maybe_project
807
        if self.is_associated(project):
808
            return True
809
        if project.is_deactivated():
810
            return False
811
        return True
812

    
813

    
814
class AstakosUserAuthProviderManager(models.Manager):
815

    
816
    def active(self, **filters):
817
        return self.filter(active=True, **filters)
818

    
819
    def remove_unverified_providers(self, provider, **filters):
820
        try:
821
            existing = self.filter(module=provider, user__email_verified=False, **filters)
822
            for p in existing:
823
                p.user.delete()
824
        except:
825
            pass
826

    
827

    
828

    
829
class AstakosUserAuthProvider(models.Model):
830
    """
831
    Available user authentication methods.
832
    """
833
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
834
                                   null=True, default=None)
835
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
836
    module = models.CharField(_('Provider'), max_length=255, blank=False,
837
                                default='local')
838
    identifier = models.CharField(_('Third-party identifier'),
839
                                              max_length=255, null=True,
840
                                              blank=True)
841
    active = models.BooleanField(default=True)
842
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
843
                                   default='astakos')
844
    info_data = models.TextField(default="", null=True, blank=True)
845
    created = models.DateTimeField('Creation date', auto_now_add=True)
846

    
847
    objects = AstakosUserAuthProviderManager()
848

    
849
    class Meta:
850
        unique_together = (('identifier', 'module', 'user'), )
851
        ordering = ('module', 'created')
852

    
853
    def __init__(self, *args, **kwargs):
854
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
855
        try:
856
            self.info = json.loads(self.info_data)
857
            if not self.info:
858
                self.info = {}
859
        except Exception, e:
860
            self.info = {}
861

    
862
        for key,value in self.info.iteritems():
863
            setattr(self, 'info_%s' % key, value)
864

    
865

    
866
    @property
867
    def settings(self):
868
        return auth_providers.get_provider(self.module)
869

    
870
    @property
871
    def details_display(self):
872
        try:
873
            params = self.user.__dict__
874
            params.update(self.__dict__)
875
            return self.settings.get_details_tpl_display % params
876
        except:
877
            return ''
878

    
879
    @property
880
    def title_display(self):
881
        title_tpl = self.settings.get_title_display
882
        try:
883
            if self.settings.get_user_title_display:
884
                title_tpl = self.settings.get_user_title_display
885
        except Exception, e:
886
            pass
887
        try:
888
          return title_tpl % self.__dict__
889
        except:
890
          return self.settings.get_title_display % self.__dict__
891

    
892
    def can_remove(self):
893
        return self.user.can_remove_auth_provider(self.module)
894

    
895
    def delete(self, *args, **kwargs):
896
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
897
        if self.module == 'local':
898
            self.user.set_unusable_password()
899
            self.user.save()
900
        return ret
901

    
902
    def __repr__(self):
903
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
904

    
905
    def __unicode__(self):
906
        if self.identifier:
907
            return "%s:%s" % (self.module, self.identifier)
908
        if self.auth_backend:
909
            return "%s:%s" % (self.module, self.auth_backend)
910
        return self.module
911

    
912
    def save(self, *args, **kwargs):
913
        self.info_data = json.dumps(self.info)
914
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
915

    
916

    
917
class ExtendedManager(models.Manager):
918
    def _update_or_create(self, **kwargs):
919
        assert kwargs, \
920
            'update_or_create() must be passed at least one keyword argument'
921
        obj, created = self.get_or_create(**kwargs)
922
        defaults = kwargs.pop('defaults', {})
923
        if created:
924
            return obj, True, False
925
        else:
926
            try:
927
                params = dict(
928
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
929
                params.update(defaults)
930
                for attr, val in params.items():
931
                    if hasattr(obj, attr):
932
                        setattr(obj, attr, val)
933
                sid = transaction.savepoint()
934
                obj.save(force_update=True)
935
                transaction.savepoint_commit(sid)
936
                return obj, False, True
937
            except IntegrityError, e:
938
                transaction.savepoint_rollback(sid)
939
                try:
940
                    return self.get(**kwargs), False, False
941
                except self.model.DoesNotExist:
942
                    raise e
943

    
944
    update_or_create = _update_or_create
945

    
946

    
947
class AstakosUserQuota(models.Model):
948
    objects = ExtendedManager()
949
    capacity = intDecimalField()
950
    quantity = intDecimalField(default=0)
951
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
952
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
953
    resource = models.ForeignKey(Resource)
954
    user = models.ForeignKey(AstakosUser)
955

    
956
    class Meta:
957
        unique_together = ("resource", "user")
958

    
959
    def quota_values(self):
960
        return QuotaValues(
961
            quantity = self.quantity,
962
            capacity = self.capacity,
963
            import_limit = self.import_limit,
964
            export_limit = self.export_limit)
965

    
966

    
967
class ApprovalTerms(models.Model):
968
    """
969
    Model for approval terms
970
    """
971

    
972
    date = models.DateTimeField(
973
        _('Issue date'), db_index=True, auto_now_add=True)
974
    location = models.CharField(_('Terms location'), max_length=255)
975

    
976

    
977
class Invitation(models.Model):
978
    """
979
    Model for registring invitations
980
    """
981
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
982
                                null=True)
983
    realname = models.CharField(_('Real name'), max_length=255)
984
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
985
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
986
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
987
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
988
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
989

    
990
    def __init__(self, *args, **kwargs):
991
        super(Invitation, self).__init__(*args, **kwargs)
992
        if not self.id:
993
            self.code = _generate_invitation_code()
994

    
995
    def consume(self):
996
        self.is_consumed = True
997
        self.consumed = datetime.now()
998
        self.save()
999

    
1000
    def __unicode__(self):
1001
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1002

    
1003

    
1004
class EmailChangeManager(models.Manager):
1005

    
1006
    @transaction.commit_on_success
1007
    def change_email(self, activation_key):
1008
        """
1009
        Validate an activation key and change the corresponding
1010
        ``User`` if valid.
1011

1012
        If the key is valid and has not expired, return the ``User``
1013
        after activating.
1014

1015
        If the key is not valid or has expired, return ``None``.
1016

1017
        If the key is valid but the ``User`` is already active,
1018
        return ``None``.
1019

1020
        After successful email change the activation record is deleted.
1021

1022
        Throws ValueError if there is already
1023
        """
1024
        try:
1025
            email_change = self.model.objects.get(
1026
                activation_key=activation_key)
1027
            if email_change.activation_key_expired():
1028
                email_change.delete()
1029
                raise EmailChange.DoesNotExist
1030
            # is there an active user with this address?
1031
            try:
1032
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1033
            except AstakosUser.DoesNotExist:
1034
                pass
1035
            else:
1036
                raise ValueError(_('The new email address is reserved.'))
1037
            # update user
1038
            user = AstakosUser.objects.get(pk=email_change.user_id)
1039
            old_email = user.email
1040
            user.email = email_change.new_email_address
1041
            user.save()
1042
            email_change.delete()
1043
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1044
                                                          user.email)
1045
            logger.log(LOGGING_LEVEL, msg)
1046
            return user
1047
        except EmailChange.DoesNotExist:
1048
            raise ValueError(_('Invalid activation key.'))
1049

    
1050

    
1051
class EmailChange(models.Model):
1052
    new_email_address = models.EmailField(
1053
        _(u'new e-mail address'),
1054
        help_text=_('Provide a new email address. Until you verify the new '
1055
                    'address by following the activation link that will be '
1056
                    'sent to it, your old email address will remain active.'))
1057
    user = models.ForeignKey(
1058
        AstakosUser, unique=True, related_name='emailchanges')
1059
    requested_at = models.DateTimeField(auto_now_add=True)
1060
    activation_key = models.CharField(
1061
        max_length=40, unique=True, db_index=True)
1062

    
1063
    objects = EmailChangeManager()
1064

    
1065
    def get_url(self):
1066
        return reverse('email_change_confirm',
1067
                      kwargs={'activation_key': self.activation_key})
1068

    
1069
    def activation_key_expired(self):
1070
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1071
        return self.requested_at + expiration_date < datetime.now()
1072

    
1073

    
1074
class AdditionalMail(models.Model):
1075
    """
1076
    Model for registring invitations
1077
    """
1078
    owner = models.ForeignKey(AstakosUser)
1079
    email = models.EmailField()
1080

    
1081

    
1082
def _generate_invitation_code():
1083
    while True:
1084
        code = randint(1, 2L ** 63 - 1)
1085
        try:
1086
            Invitation.objects.get(code=code)
1087
            # An invitation with this code already exists, try again
1088
        except Invitation.DoesNotExist:
1089
            return code
1090

    
1091

    
1092
def get_latest_terms():
1093
    try:
1094
        term = ApprovalTerms.objects.order_by('-id')[0]
1095
        return term
1096
    except IndexError:
1097
        pass
1098
    return None
1099

    
1100
class PendingThirdPartyUser(models.Model):
1101
    """
1102
    Model for registring successful third party user authentications
1103
    """
1104
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1105
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1106
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1107
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1108
                                  null=True)
1109
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1110
                                 null=True)
1111
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1112
                                   null=True)
1113
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1114
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1115
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1116
    info = models.TextField(default="", null=True, blank=True)
1117

    
1118
    class Meta:
1119
        unique_together = ("provider", "third_party_identifier")
1120

    
1121
    def get_user_instance(self):
1122
        d = self.__dict__
1123
        d.pop('_state', None)
1124
        d.pop('id', None)
1125
        d.pop('token', None)
1126
        d.pop('created', None)
1127
        d.pop('info', None)
1128
        user = AstakosUser(**d)
1129

    
1130
        return user
1131

    
1132
    @property
1133
    def realname(self):
1134
        return '%s %s' %(self.first_name, self.last_name)
1135

    
1136
    @realname.setter
1137
    def realname(self, value):
1138
        parts = value.split(' ')
1139
        if len(parts) == 2:
1140
            self.first_name = parts[0]
1141
            self.last_name = parts[1]
1142
        else:
1143
            self.last_name = parts[0]
1144

    
1145
    def save(self, **kwargs):
1146
        if not self.id:
1147
            # set username
1148
            while not self.username:
1149
                username =  uuid.uuid4().hex[:30]
1150
                try:
1151
                    AstakosUser.objects.get(username = username)
1152
                except AstakosUser.DoesNotExist, e:
1153
                    self.username = username
1154
        super(PendingThirdPartyUser, self).save(**kwargs)
1155

    
1156
    def generate_token(self):
1157
        self.password = self.third_party_identifier
1158
        self.last_login = datetime.now()
1159
        self.token = default_token_generator.make_token(self)
1160

    
1161
class SessionCatalog(models.Model):
1162
    session_key = models.CharField(_('session key'), max_length=40)
1163
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1164

    
1165

    
1166
### PROJECTS ###
1167
################
1168

    
1169
def synced_model_metaclass(class_name, class_parents, class_attributes):
1170

    
1171
    new_attributes = {}
1172
    sync_attributes = {}
1173

    
1174
    for name, value in class_attributes.iteritems():
1175
        sync, underscore, rest = name.partition('_')
1176
        if sync == 'sync' and underscore == '_':
1177
            sync_attributes[rest] = value
1178
        else:
1179
            new_attributes[name] = value
1180

    
1181
    if 'prefix' not in sync_attributes:
1182
        m = ("you did not specify a 'sync_prefix' attribute "
1183
             "in class '%s'" % (class_name,))
1184
        raise ValueError(m)
1185

    
1186
    prefix = sync_attributes.pop('prefix')
1187
    class_name = sync_attributes.pop('classname', prefix + '_model')
1188

    
1189
    for name, value in sync_attributes.iteritems():
1190
        newname = prefix + '_' + name
1191
        if newname in new_attributes:
1192
            m = ("class '%s' was specified with prefix '%s' "
1193
                 "but it already has an attribute named '%s'"
1194
                 % (class_name, prefix, newname))
1195
            raise ValueError(m)
1196

    
1197
        new_attributes[newname] = value
1198

    
1199
    newclass = type(class_name, class_parents, new_attributes)
1200
    return newclass
1201

    
1202

    
1203
def make_synced(prefix='sync', name='SyncedState'):
1204

    
1205
    the_name = name
1206
    the_prefix = prefix
1207

    
1208
    class SyncedState(models.Model):
1209

    
1210
        sync_classname      = the_name
1211
        sync_prefix         = the_prefix
1212
        __metaclass__       = synced_model_metaclass
1213

    
1214
        sync_new_state      = models.BigIntegerField(null=True)
1215
        sync_synced_state   = models.BigIntegerField(null=True)
1216
        STATUS_SYNCED       = 0
1217
        STATUS_PENDING      = 1
1218
        sync_status         = models.IntegerField(db_index=True)
1219

    
1220
        class Meta:
1221
            abstract = True
1222

    
1223
        class NotSynced(Exception):
1224
            pass
1225

    
1226
        def sync_init_state(self, state):
1227
            self.sync_synced_state = state
1228
            self.sync_new_state = state
1229
            self.sync_status = self.STATUS_SYNCED
1230

    
1231
        def sync_get_status(self):
1232
            return self.sync_status
1233

    
1234
        def sync_set_status(self):
1235
            if self.sync_new_state != self.sync_synced_state:
1236
                self.sync_status = self.STATUS_PENDING
1237
            else:
1238
                self.sync_status = self.STATUS_SYNCED
1239

    
1240
        def sync_set_synced(self):
1241
            self.sync_synced_state = self.sync_new_state
1242
            self.sync_status = self.STATUS_SYNCED
1243

    
1244
        def sync_get_synced_state(self):
1245
            return self.sync_synced_state
1246

    
1247
        def sync_set_new_state(self, new_state):
1248
            self.sync_new_state = new_state
1249
            self.sync_set_status()
1250

    
1251
        def sync_get_new_state(self):
1252
            return self.sync_new_state
1253

    
1254
        def sync_set_synced_state(self, synced_state):
1255
            self.sync_synced_state = synced_state
1256
            self.sync_set_status()
1257

    
1258
        def sync_get_pending_objects(self):
1259
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1260
            return self.objects.filter(**kw)
1261

    
1262
        def sync_get_synced_objects(self):
1263
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1264
            return self.objects.filter(**kw)
1265

    
1266
        def sync_verify_get_synced_state(self):
1267
            status = self.sync_get_status()
1268
            state = self.sync_get_synced_state()
1269
            verified = (status == self.STATUS_SYNCED)
1270
            return state, verified
1271

    
1272
        def sync_is_synced(self):
1273
            state, verified = self.sync_verify_get_synced_state()
1274
            return verified
1275

    
1276
    return SyncedState
1277

    
1278
SyncedState = make_synced(prefix='sync', name='SyncedState')
1279

    
1280

    
1281
class ChainManager(ForUpdateManager):
1282

    
1283
    def search_by_name(self, *search_strings):
1284
        projects = Project.objects.search_by_name(*search_strings)
1285
        chains = [p.id for p in projects]
1286
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1287
        apps = (app for app in apps if app.is_latest())
1288
        app_chains = [app.chain for app in apps if app.chain not in chains]
1289
        return chains + app_chains
1290

    
1291
    def all_full_state(self):
1292
        d = {}
1293
        chains = self.all()
1294
        for chain in chains:
1295
            d[chain.pk] = chain.full_state()
1296
        return d
1297

    
1298
    def of_project(self, project):
1299
        if project is None:
1300
            return None
1301
        try:
1302
            return self.get(chain=project.id)
1303
        except Chain.DoesNotExist:
1304
            raise AssertionError('project with no chain')
1305

    
1306

    
1307
class Chain(models.Model):
1308
    chain  =   models.AutoField(primary_key=True)
1309

    
1310
    def __str__(self):
1311
        return "%s" % (self.chain,)
1312

    
1313
    objects = ChainManager()
1314

    
1315
    PENDING            = 0
1316
    DENIED             = 3
1317
    DISMISSED          = 4
1318
    CANCELLED          = 5
1319

    
1320
    APPROVED           = 10
1321
    APPROVED_PENDING   = 11
1322
    SUSPENDED          = 12
1323
    SUSPENDED_PENDING  = 13
1324
    TERMINATED         = 14
1325
    TERMINATED_PENDING = 15
1326

    
1327
    PENDING_STATES = [PENDING,
1328
                      APPROVED_PENDING,
1329
                      SUSPENDED_PENDING,
1330
                      TERMINATED_PENDING,
1331
                      ]
1332

    
1333
    SKIP_STATES = [DISMISSED,
1334
                   CANCELLED,
1335
                   TERMINATED]
1336

    
1337
    STATE_DISPLAY = {
1338
        PENDING            : _("Pending"),
1339
        DENIED             : _("Denied"),
1340
        DISMISSED          : _("Dismissed"),
1341
        CANCELLED          : _("Cancelled"),
1342
        APPROVED           : _("Active"),
1343
        APPROVED_PENDING   : _("Active - Pending"),
1344
        SUSPENDED          : _("Suspended"),
1345
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1346
        TERMINATED         : _("Terminated"),
1347
        TERMINATED_PENDING : _("Terminated - Pending"),
1348
        }
1349

    
1350

    
1351
    @classmethod
1352
    def _chain_state(cls, project_state, app_state):
1353
        s = CHAIN_STATE.get((project_state, app_state), None)
1354
        if s is None:
1355
            raise AssertionError('inconsistent chain state')
1356
        return s
1357

    
1358
    @classmethod
1359
    def chain_state(cls, project, app):
1360
        p_state = project.state if project else None
1361
        return cls._chain_state(p_state, app.state)
1362

    
1363
    @classmethod
1364
    def state_display(cls, s):
1365
        if s is None:
1366
            return _("Unknown")
1367
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1368

    
1369
    def last_application(self):
1370
        return self.chained_apps.order_by('-id')[0]
1371

    
1372
    def get_project(self):
1373
        try:
1374
            return self.chained_project
1375
        except Project.DoesNotExist:
1376
            return None
1377

    
1378
    def get_elements(self):
1379
        project = self.get_project()
1380
        app = self.last_application()
1381
        return project, app
1382

    
1383
    def full_state(self):
1384
        project, app = self.get_elements()
1385
        s = self.chain_state(project, app)
1386
        return s, project, app
1387

    
1388
def new_chain():
1389
    c = Chain.objects.create()
1390
    return c
1391

    
1392

    
1393
class ProjectApplicationManager(ForUpdateManager):
1394

    
1395
    def user_visible_projects(self, *filters, **kw_filters):
1396
        model = self.model
1397
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1398

    
1399
    def user_visible_by_chain(self, flt):
1400
        model = self.model
1401
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1402
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1403
        by_chain = dict(pending.annotate(models.Max('id')))
1404
        by_chain.update(approved.annotate(models.Max('id')))
1405
        return self.filter(flt, id__in=by_chain.values())
1406

    
1407
    def user_accessible_projects(self, user):
1408
        """
1409
        Return projects accessed by specified user.
1410
        """
1411
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1412
                               Q(project__projectmembership__person=user)
1413

    
1414
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1415

    
1416
    def search_by_name(self, *search_strings):
1417
        q = Q()
1418
        for s in search_strings:
1419
            q = q | Q(name__icontains=s)
1420
        return self.filter(q)
1421

    
1422
    def latest_of_chain(self, chain_id):
1423
        try:
1424
            return self.filter(chain=chain_id).order_by('-id')[0]
1425
        except IndexError:
1426
            return None
1427

    
1428

    
1429
class ProjectApplication(models.Model):
1430
    applicant               =   models.ForeignKey(
1431
                                    AstakosUser,
1432
                                    related_name='projects_applied',
1433
                                    db_index=True)
1434

    
1435
    PENDING     =    0
1436
    APPROVED    =    1
1437
    REPLACED    =    2
1438
    DENIED      =    3
1439
    DISMISSED   =    4
1440
    CANCELLED   =    5
1441

    
1442
    state                   =   models.IntegerField(default=PENDING,
1443
                                                    db_index=True)
1444

    
1445
    owner                   =   models.ForeignKey(
1446
                                    AstakosUser,
1447
                                    related_name='projects_owned',
1448
                                    db_index=True)
1449

    
1450
    chain                   =   models.ForeignKey(Chain,
1451
                                                  related_name='chained_apps',
1452
                                                  db_column='chain')
1453
    precursor_application   =   models.ForeignKey('ProjectApplication',
1454
                                                  null=True,
1455
                                                  blank=True)
1456

    
1457
    name                    =   models.CharField(max_length=80)
1458
    homepage                =   models.URLField(max_length=255, null=True,
1459
                                                verify_exists=False)
1460
    description             =   models.TextField(null=True, blank=True)
1461
    start_date              =   models.DateTimeField(null=True, blank=True)
1462
    end_date                =   models.DateTimeField()
1463
    member_join_policy      =   models.IntegerField()
1464
    member_leave_policy     =   models.IntegerField()
1465
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1466
    resource_grants         =   models.ManyToManyField(
1467
                                    Resource,
1468
                                    null=True,
1469
                                    blank=True,
1470
                                    through='ProjectResourceGrant')
1471
    comments                =   models.TextField(null=True, blank=True)
1472
    issue_date              =   models.DateTimeField(auto_now_add=True)
1473
    response_date           =   models.DateTimeField(null=True, blank=True)
1474

    
1475
    objects                 =   ProjectApplicationManager()
1476

    
1477
    # Compiled queries
1478
    Q_PENDING  = Q(state=PENDING)
1479
    Q_APPROVED = Q(state=APPROVED)
1480
    Q_DENIED   = Q(state=DENIED)
1481

    
1482
    class Meta:
1483
        unique_together = ("chain", "id")
1484

    
1485
    def __unicode__(self):
1486
        return "%s applied by %s" % (self.name, self.applicant)
1487

    
1488
    # TODO: Move to a more suitable place
1489
    APPLICATION_STATE_DISPLAY = {
1490
        PENDING  : _('Pending review'),
1491
        APPROVED : _('Approved'),
1492
        REPLACED : _('Replaced'),
1493
        DENIED   : _('Denied'),
1494
        DISMISSED: _('Dismissed'),
1495
        CANCELLED: _('Cancelled')
1496
    }
1497

    
1498
    def get_project(self):
1499
        try:
1500
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1501
            return Project
1502
        except Project.DoesNotExist, e:
1503
            return None
1504

    
1505
    def state_display(self):
1506
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1507

    
1508
    def project_state_display(self):
1509
        try:
1510
            project = self.project
1511
            return project.state_display()
1512
        except Project.DoesNotExist:
1513
            return self.state_display()
1514

    
1515
    def add_resource_policy(self, service, resource, uplimit):
1516
        """Raises ObjectDoesNotExist, IntegrityError"""
1517
        q = self.projectresourcegrant_set
1518
        resource = Resource.objects.get(service__name=service, name=resource)
1519
        q.create(resource=resource, member_capacity=uplimit)
1520

    
1521
    def members_count(self):
1522
        return self.project.approved_memberships.count()
1523

    
1524
    @property
1525
    def grants(self):
1526
        return self.projectresourcegrant_set.values(
1527
            'member_capacity', 'resource__name', 'resource__service__name')
1528

    
1529
    @property
1530
    def resource_policies(self):
1531
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1532

    
1533
    @resource_policies.setter
1534
    def resource_policies(self, policies):
1535
        for p in policies:
1536
            service = p.get('service', None)
1537
            resource = p.get('resource', None)
1538
            uplimit = p.get('uplimit', 0)
1539
            self.add_resource_policy(service, resource, uplimit)
1540

    
1541
    def pending_modifications_incl_me(self):
1542
        q = self.chained_applications()
1543
        q = q.filter(Q(state=self.PENDING))
1544
        return q
1545

    
1546
    def last_pending_incl_me(self):
1547
        try:
1548
            return self.pending_modifications_incl_me().order_by('-id')[0]
1549
        except IndexError:
1550
            return None
1551

    
1552
    def pending_modifications(self):
1553
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1554

    
1555
    def last_pending(self):
1556
        try:
1557
            return self.pending_modifications().order_by('-id')[0]
1558
        except IndexError:
1559
            return None
1560

    
1561
    def is_modification(self):
1562
        # if self.state != self.PENDING:
1563
        #     return False
1564
        parents = self.chained_applications().filter(id__lt=self.id)
1565
        parents = parents.filter(state__in=[self.APPROVED])
1566
        return parents.count() > 0
1567

    
1568
    def chained_applications(self):
1569
        return ProjectApplication.objects.filter(chain=self.chain)
1570

    
1571
    def is_latest(self):
1572
        return self.chained_applications().order_by('-id')[0] == self
1573

    
1574
    def has_pending_modifications(self):
1575
        return bool(self.last_pending())
1576

    
1577
    def is_applied(self):
1578
        try:
1579
            self.project
1580
            return True
1581
        except Project.DoesNotExist:
1582
            return False
1583

    
1584
    def get_project(self):
1585
        try:
1586
            return Project.objects.get(id=self.chain)
1587
        except Project.DoesNotExist:
1588
            return None
1589

    
1590
    def project_exists(self):
1591
        return self.get_project() is not None
1592

    
1593
    def _get_project_for_update(self):
1594
        try:
1595
            objects = Project.objects.select_for_update()
1596
            project = objects.get(id=self.chain)
1597
            return project
1598
        except Project.DoesNotExist:
1599
            return None
1600

    
1601
    def can_cancel(self):
1602
        return self.state == self.PENDING
1603

    
1604
    def cancel(self):
1605
        if not self.can_cancel():
1606
            m = _("cannot cancel: application '%s' in state '%s'") % (
1607
                    self.id, self.state)
1608
            raise AssertionError(m)
1609

    
1610
        self.state = self.CANCELLED
1611
        self.save()
1612

    
1613
    def can_dismiss(self):
1614
        return self.state == self.DENIED
1615

    
1616
    def dismiss(self):
1617
        if not self.can_dismiss():
1618
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1619
                    self.id, self.state)
1620
            raise AssertionError(m)
1621

    
1622
        self.state = self.DISMISSED
1623
        self.save()
1624

    
1625
    def can_deny(self):
1626
        return self.state == self.PENDING
1627

    
1628
    def deny(self):
1629
        if not self.can_deny():
1630
            m = _("cannot deny: application '%s' in state '%s'") % (
1631
                    self.id, self.state)
1632
            raise AssertionError(m)
1633

    
1634
        self.state = self.DENIED
1635
        self.response_date = datetime.now()
1636
        self.save()
1637

    
1638
    def can_approve(self):
1639
        return self.state == self.PENDING
1640

    
1641
    def approve(self, approval_user=None):
1642
        """
1643
        If approval_user then during owner membership acceptance
1644
        it is checked whether the request_user is eligible.
1645

1646
        Raises:
1647
            PermissionDenied
1648
        """
1649

    
1650
        if not transaction.is_managed():
1651
            raise AssertionError("NOPE")
1652

    
1653
        new_project_name = self.name
1654
        if not self.can_approve():
1655
            m = _("cannot approve: project '%s' in state '%s'") % (
1656
                    new_project_name, self.state)
1657
            raise AssertionError(m) # invalid argument
1658

    
1659
        now = datetime.now()
1660
        project = self._get_project_for_update()
1661

    
1662
        try:
1663
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1664
            conflicting_project = Project.objects.get(q)
1665
            if (conflicting_project != project):
1666
                m = (_("cannot approve: project with name '%s' "
1667
                       "already exists (id: %s)") % (
1668
                        new_project_name, conflicting_project.id))
1669
                raise PermissionDenied(m) # invalid argument
1670
        except Project.DoesNotExist:
1671
            pass
1672

    
1673
        new_project = False
1674
        if project is None:
1675
            new_project = True
1676
            project = Project(id=self.chain)
1677

    
1678
        project.name = new_project_name
1679
        project.application = self
1680
        project.last_approval_date = now
1681
        if not new_project:
1682
            project.is_modified = True
1683

    
1684
        project.save()
1685

    
1686
        self.state = self.APPROVED
1687
        self.response_date = now
1688
        self.save()
1689

    
1690
    @property
1691
    def member_join_policy_display(self):
1692
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1693

    
1694
    @property
1695
    def member_leave_policy_display(self):
1696
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1697

    
1698
class ProjectResourceGrant(models.Model):
1699

    
1700
    resource                =   models.ForeignKey(Resource)
1701
    project_application     =   models.ForeignKey(ProjectApplication,
1702
                                                  null=True)
1703
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1704
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1705
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1706
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1707
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1708
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1709

    
1710
    objects = ExtendedManager()
1711

    
1712
    class Meta:
1713
        unique_together = ("resource", "project_application")
1714

    
1715
    def member_quota_values(self):
1716
        return QuotaValues(
1717
            quantity = 0,
1718
            capacity = self.member_capacity,
1719
            import_limit = self.member_import_limit,
1720
            export_limit = self.member_export_limit)
1721

    
1722
    def display_member_capacity(self):
1723
        if self.member_capacity:
1724
            if self.resource.unit:
1725
                return ProjectResourceGrant.display_filesize(
1726
                    self.member_capacity)
1727
            else:
1728
                if math.isinf(self.member_capacity):
1729
                    return 'Unlimited'
1730
                else:
1731
                    return self.member_capacity
1732
        else:
1733
            return 'Unlimited'
1734

    
1735
    def __str__(self):
1736
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1737
                                        self.display_member_capacity())
1738

    
1739
    @classmethod
1740
    def display_filesize(cls, value):
1741
        try:
1742
            value = float(value)
1743
        except:
1744
            return
1745
        else:
1746
            if math.isinf(value):
1747
                return 'Unlimited'
1748
            if value > 1:
1749
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1750
                                [0, 0, 0, 0, 0, 0])
1751
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1752
                quotient = float(value) / 1024**exponent
1753
                unit, value_decimals = unit_list[exponent]
1754
                format_string = '{0:.%sf} {1}' % (value_decimals)
1755
                return format_string.format(quotient, unit)
1756
            if value == 0:
1757
                return '0 bytes'
1758
            if value == 1:
1759
                return '1 byte'
1760
            else:
1761
               return '0'
1762

    
1763

    
1764
class ProjectManager(ForUpdateManager):
1765

    
1766
    def terminated_projects(self):
1767
        q = self.model.Q_TERMINATED
1768
        return self.filter(q)
1769

    
1770
    def not_terminated_projects(self):
1771
        q = ~self.model.Q_TERMINATED
1772
        return self.filter(q)
1773

    
1774
    def terminating_projects(self):
1775
        q = self.model.Q_TERMINATED & Q(is_active=True)
1776
        return self.filter(q)
1777

    
1778
    def deactivated_projects(self):
1779
        q = self.model.Q_DEACTIVATED
1780
        return self.filter(q)
1781

    
1782
    def deactivating_projects(self):
1783
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1784
        return self.filter(q)
1785

    
1786
    def modified_projects(self):
1787
        return self.filter(is_modified=True)
1788

    
1789
    def reactivating_projects(self):
1790
        return self.filter(state=Project.APPROVED, is_active=False)
1791

    
1792
    def expired_projects(self):
1793
        q = (~Q(state=Project.TERMINATED) &
1794
              Q(application__end_date__lt=datetime.now()))
1795
        return self.filter(q)
1796

    
1797
    def search_by_name(self, *search_strings):
1798
        q = Q()
1799
        for s in search_strings:
1800
            q = q | Q(name__icontains=s)
1801
        return self.filter(q)
1802

    
1803

    
1804
class Project(models.Model):
1805

    
1806
    id                          =   models.OneToOneField(Chain,
1807
                                                      related_name='chained_project',
1808
                                                      db_column='id',
1809
                                                      primary_key=True)
1810

    
1811
    application                 =   models.OneToOneField(
1812
                                            ProjectApplication,
1813
                                            related_name='project')
1814
    last_approval_date          =   models.DateTimeField(null=True)
1815

    
1816
    members                     =   models.ManyToManyField(
1817
                                            AstakosUser,
1818
                                            through='ProjectMembership')
1819

    
1820
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1821
    deactivation_date           =   models.DateTimeField(null=True)
1822

    
1823
    creation_date               =   models.DateTimeField(auto_now_add=True)
1824
    name                        =   models.CharField(
1825
                                            max_length=80,
1826
                                            null=True,
1827
                                            db_index=True,
1828
                                            unique=True)
1829

    
1830
    APPROVED    = 1
1831
    SUSPENDED   = 10
1832
    TERMINATED  = 100
1833

    
1834
    is_modified                 =   models.BooleanField(default=False,
1835
                                                        db_index=True)
1836
    is_active                   =   models.BooleanField(default=True,
1837
                                                        db_index=True)
1838
    state                       =   models.IntegerField(default=APPROVED,
1839
                                                        db_index=True)
1840

    
1841
    objects     =   ProjectManager()
1842

    
1843
    # Compiled queries
1844
    Q_TERMINATED  = Q(state=TERMINATED)
1845
    Q_SUSPENDED   = Q(state=SUSPENDED)
1846
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1847

    
1848
    def __str__(self):
1849
        return _("<project %s '%s'>") % (self.id, self.application.name)
1850

    
1851
    __repr__ = __str__
1852

    
1853
    STATE_DISPLAY = {
1854
        APPROVED   : 'Active',
1855
        SUSPENDED  : 'Suspended',
1856
        TERMINATED : 'Terminated'
1857
        }
1858

    
1859
    def state_display(self):
1860
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1861

    
1862
    def admin_state_display(self):
1863
        s = self.state_display()
1864
        if self.sync_pending():
1865
            s += ' (sync pending)'
1866
        return s
1867

    
1868
    def sync_pending(self):
1869
        if self.state != self.APPROVED:
1870
            return self.is_active
1871
        return not self.is_active or self.is_modified
1872

    
1873
    def expiration_info(self):
1874
        return (str(self.id), self.name, self.state_display(),
1875
                str(self.application.end_date))
1876

    
1877
    def is_deactivated(self, reason=None):
1878
        if reason is not None:
1879
            return self.state == reason
1880

    
1881
        return self.state != self.APPROVED
1882

    
1883
    def is_deactivating(self, reason=None):
1884
        if not self.is_active:
1885
            return False
1886

    
1887
        return self.is_deactivated(reason)
1888

    
1889
    def is_deactivated_strict(self, reason=None):
1890
        if self.is_active:
1891
            return False
1892

    
1893
        return self.is_deactivated(reason)
1894

    
1895
    ### Deactivation calls
1896

    
1897
    def deactivate(self):
1898
        self.deactivation_date = datetime.now()
1899
        self.is_active = False
1900

    
1901
    def reactivate(self):
1902
        self.deactivation_date = None
1903
        self.is_active = True
1904

    
1905
    def terminate(self):
1906
        self.deactivation_reason = 'TERMINATED'
1907
        self.state = self.TERMINATED
1908
        self.name = None
1909
        self.save()
1910

    
1911
    def suspend(self):
1912
        self.deactivation_reason = 'SUSPENDED'
1913
        self.state = self.SUSPENDED
1914
        self.save()
1915

    
1916
    def resume(self):
1917
        self.deactivation_reason = None
1918
        self.state = self.APPROVED
1919
        self.save()
1920

    
1921
    ### Logical checks
1922

    
1923
    def is_inconsistent(self):
1924
        now = datetime.now()
1925
        dates = [self.creation_date,
1926
                 self.last_approval_date,
1927
                 self.deactivation_date]
1928
        return any([date > now for date in dates])
1929

    
1930
    def is_active_strict(self):
1931
        return self.is_active and self.state == self.APPROVED
1932

    
1933
    def is_approved(self):
1934
        return self.state == self.APPROVED
1935

    
1936
    @property
1937
    def is_alive(self):
1938
        return not self.is_terminated
1939

    
1940
    @property
1941
    def is_terminated(self):
1942
        return self.is_deactivated(self.TERMINATED)
1943

    
1944
    @property
1945
    def is_suspended(self):
1946
        return self.is_deactivated(self.SUSPENDED)
1947

    
1948
    def violates_resource_grants(self):
1949
        return False
1950

    
1951
    def violates_members_limit(self, adding=0):
1952
        application = self.application
1953
        limit = application.limit_on_members_number
1954
        if limit is None:
1955
            return False
1956
        return (len(self.approved_members) + adding > limit)
1957

    
1958

    
1959
    ### Other
1960

    
1961
    def count_pending_memberships(self):
1962
        memb_set = self.projectmembership_set
1963
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1964
        return memb_count
1965

    
1966
    def members_count(self):
1967
        return self.approved_memberships.count()
1968

    
1969
    @property
1970
    def approved_memberships(self):
1971
        query = ProjectMembership.Q_ACCEPTED_STATES
1972
        return self.projectmembership_set.filter(query)
1973

    
1974
    @property
1975
    def approved_members(self):
1976
        return [m.person for m in self.approved_memberships]
1977

    
1978
    def add_member(self, user):
1979
        """
1980
        Raises:
1981
            django.exceptions.PermissionDenied
1982
            astakos.im.models.AstakosUser.DoesNotExist
1983
        """
1984
        if isinstance(user, int):
1985
            user = AstakosUser.objects.get(user=user)
1986

    
1987
        m, created = ProjectMembership.objects.get_or_create(
1988
            person=user, project=self
1989
        )
1990
        m.accept()
1991

    
1992
    def remove_member(self, user):
1993
        """
1994
        Raises:
1995
            django.exceptions.PermissionDenied
1996
            astakos.im.models.AstakosUser.DoesNotExist
1997
            astakos.im.models.ProjectMembership.DoesNotExist
1998
        """
1999
        if isinstance(user, int):
2000
            user = AstakosUser.objects.get(user=user)
2001

    
2002
        m = ProjectMembership.objects.get(person=user, project=self)
2003
        m.remove()
2004

    
2005

    
2006
CHAIN_STATE = {
2007
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2008
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2009
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2010
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2011
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2012

    
2013
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2014
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2015
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2016
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2017
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2018

    
2019
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2020
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2021
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2022
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2023
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2024

    
2025
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2026
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2027
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2028
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2029
    }
2030

    
2031

    
2032
class PendingMembershipError(Exception):
2033
    pass
2034

    
2035

    
2036
class ProjectMembershipManager(ForUpdateManager):
2037

    
2038
    def any_accepted(self):
2039
        q = (Q(state=ProjectMembership.ACCEPTED) |
2040
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
2041
        return self.filter(q)
2042

    
2043
    def requested(self):
2044
        return self.filter(state=ProjectMembership.REQUESTED)
2045

    
2046
    def suspended(self):
2047
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2048

    
2049
class ProjectMembership(models.Model):
2050

    
2051
    person              =   models.ForeignKey(AstakosUser)
2052
    request_date        =   models.DateField(auto_now_add=True)
2053
    project             =   models.ForeignKey(Project)
2054

    
2055
    REQUESTED           =   0
2056
    ACCEPTED            =   1
2057
    # User deactivation
2058
    USER_SUSPENDED      =   10
2059
    # Project deactivation
2060
    PROJECT_DEACTIVATED =   100
2061

    
2062
    REMOVED             =   200
2063

    
2064
    ASSOCIATED_STATES   =   set([REQUESTED,
2065
                                 ACCEPTED,
2066
                                 USER_SUSPENDED,
2067
                                 PROJECT_DEACTIVATED])
2068

    
2069
    ACCEPTED_STATES     =   set([ACCEPTED,
2070
                                 USER_SUSPENDED,
2071
                                 PROJECT_DEACTIVATED])
2072

    
2073
    state               =   models.IntegerField(default=REQUESTED,
2074
                                                db_index=True)
2075
    is_pending          =   models.BooleanField(default=False, db_index=True)
2076
    is_active           =   models.BooleanField(default=False, db_index=True)
2077
    application         =   models.ForeignKey(
2078
                                ProjectApplication,
2079
                                null=True,
2080
                                related_name='memberships')
2081
    pending_application =   models.ForeignKey(
2082
                                ProjectApplication,
2083
                                null=True,
2084
                                related_name='pending_memberships')
2085
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2086

    
2087
    acceptance_date     =   models.DateField(null=True, db_index=True)
2088
    leave_request_date  =   models.DateField(null=True)
2089

    
2090
    objects     =   ProjectMembershipManager()
2091

    
2092
    # Compiled queries
2093
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2094

    
2095
    MEMBERSHIP_STATE_DISPLAY = {
2096
        REQUESTED           : _('Requested'),
2097
        ACCEPTED            : _('Accepted'),
2098
        USER_SUSPENDED      : _('Suspended'),
2099
        PROJECT_DEACTIVATED : _('Accepted'), # sic
2100
        REMOVED             : _('Pending removal'),
2101
        }
2102

    
2103
    USER_FRIENDLY_STATE_DISPLAY = {
2104
        REQUESTED           : _('Join requested'),
2105
        ACCEPTED            : _('Accepted member'),
2106
        USER_SUSPENDED      : _('Suspended member'),
2107
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
2108
        REMOVED             : _('Pending removal'),
2109
        }
2110

    
2111
    def state_display(self):
2112
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2113

    
2114
    def user_friendly_state_display(self):
2115
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2116

    
2117
    def get_combined_state(self):
2118
        return self.state, self.is_active, self.is_pending
2119

    
2120
    class Meta:
2121
        unique_together = ("person", "project")
2122
        #index_together = [["project", "state"]]
2123

    
2124
    def __str__(self):
2125
        return _("<'%s' membership in '%s'>") % (
2126
                self.person.username, self.project)
2127

    
2128
    __repr__ = __str__
2129

    
2130
    def __init__(self, *args, **kwargs):
2131
        self.state = self.REQUESTED
2132
        super(ProjectMembership, self).__init__(*args, **kwargs)
2133

    
2134
    def _set_history_item(self, reason, date=None):
2135
        if isinstance(reason, basestring):
2136
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2137

    
2138
        history_item = ProjectMembershipHistory(
2139
                            serial=self.id,
2140
                            person=self.person_id,
2141
                            project=self.project_id,
2142
                            date=date or datetime.now(),
2143
                            reason=reason)
2144
        history_item.save()
2145
        serial = history_item.id
2146

    
2147
    def can_accept(self):
2148
        return self.state == self.REQUESTED
2149

    
2150
    def accept(self):
2151
        if self.is_pending:
2152
            m = _("%s: attempt to accept while is pending") % (self,)
2153
            raise AssertionError(m)
2154

    
2155
        if not self.can_accept():
2156
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2157
            raise AssertionError(m)
2158

    
2159
        now = datetime.now()
2160
        self.acceptance_date = now
2161
        self._set_history_item(reason='ACCEPT', date=now)
2162
        if self.project.is_approved():
2163
            self.state = self.ACCEPTED
2164
            self.is_pending = True
2165
        else:
2166
            self.state = self.PROJECT_DEACTIVATED
2167

    
2168
        self.save()
2169

    
2170
    def can_leave(self):
2171
        return self.can_remove()
2172

    
2173
    def can_remove(self):
2174
        return self.state in self.ACCEPTED_STATES
2175

    
2176
    def remove(self):
2177
        if self.is_pending:
2178
            m = _("%s: attempt to remove while is pending") % (self,)
2179
            raise AssertionError(m)
2180

    
2181
        if not self.can_remove():
2182
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2183
            raise AssertionError(m)
2184

    
2185
        self._set_history_item(reason='REMOVE')
2186
        self.state = self.REMOVED
2187
        self.is_pending = True
2188
        self.save()
2189

    
2190
    def can_reject(self):
2191
        return self.state == self.REQUESTED
2192

    
2193
    def reject(self):
2194
        if self.is_pending:
2195
            m = _("%s: attempt to reject while is pending") % (self,)
2196
            raise AssertionError(m)
2197

    
2198
        if not self.can_reject():
2199
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2200
            raise AssertionError(m)
2201

    
2202
        # rejected requests don't need sync,
2203
        # because they were never effected
2204
        self._set_history_item(reason='REJECT')
2205
        self.delete()
2206

    
2207
    def can_cancel(self):
2208
        return self.state == self.REQUESTED
2209

    
2210
    def cancel(self):
2211
        if self.is_pending:
2212
            m = _("%s: attempt to cancel while is pending") % (self,)
2213
            raise AssertionError(m)
2214

    
2215
        if not self.can_cancel():
2216
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2217
            raise AssertionError(m)
2218

    
2219
        # rejected requests don't need sync,
2220
        # because they were never effected
2221
        self._set_history_item(reason='CANCEL')
2222
        self.delete()
2223

    
2224
    def get_diff_quotas(self, sub_list=None, add_list=None):
2225
        if sub_list is None:
2226
            sub_list = []
2227

    
2228
        if add_list is None:
2229
            add_list = []
2230

    
2231
        sub_append = sub_list.append
2232
        add_append = add_list.append
2233
        holder = self.person.uuid
2234

    
2235
        synced_application = self.application
2236
        if synced_application is not None:
2237
            cur_grants = synced_application.projectresourcegrant_set.all()
2238
            for grant in cur_grants:
2239
                sub_append(QuotaLimits(
2240
                               holder       = holder,
2241
                               resource     = str(grant.resource),
2242
                               capacity     = grant.member_capacity,
2243
                               import_limit = grant.member_import_limit,
2244
                               export_limit = grant.member_export_limit))
2245

    
2246
        pending_application = self.pending_application
2247
        if pending_application is not None:
2248
            new_grants = pending_application.projectresourcegrant_set.all()
2249
            for new_grant in new_grants:
2250
                add_append(QuotaLimits(
2251
                               holder       = holder,
2252
                               resource     = str(new_grant.resource),
2253
                               capacity     = new_grant.member_capacity,
2254
                               import_limit = new_grant.member_import_limit,
2255
                               export_limit = new_grant.member_export_limit))
2256

    
2257
        return (sub_list, add_list)
2258

    
2259
    def set_sync(self):
2260
        if not self.is_pending:
2261
            m = _("%s: attempt to sync a non pending membership") % (self,)
2262
            raise AssertionError(m)
2263

    
2264
        state = self.state
2265
        if state == self.ACCEPTED:
2266
            pending_application = self.pending_application
2267
            if pending_application is None:
2268
                m = _("%s: attempt to sync an empty pending application") % (
2269
                    self,)
2270
                raise AssertionError(m)
2271

    
2272
            self.application = pending_application
2273
            self.is_active = True
2274

    
2275
            self.pending_application = None
2276
            self.pending_serial = None
2277

    
2278
            # project.application may have changed in the meantime,
2279
            # in which case we stay PENDING;
2280
            # we are safe to check due to select_for_update
2281
            if self.application == self.project.application:
2282
                self.is_pending = False
2283
            self.save()
2284

    
2285
        elif state == self.PROJECT_DEACTIVATED:
2286
            if self.pending_application:
2287
                m = _("%s: attempt to sync in state '%s' "
2288
                      "with a pending application") % (self, state)
2289
                raise AssertionError(m)
2290

    
2291
            self.application = None
2292
            self.is_active = False
2293
            self.pending_serial = None
2294
            self.is_pending = False
2295
            self.save()
2296

    
2297
        elif state == self.REMOVED:
2298
            self.delete()
2299

    
2300
        else:
2301
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2302
            raise AssertionError(m)
2303

    
2304
    def reset_sync(self):
2305
        if not self.is_pending:
2306
            m = _("%s: attempt to reset a non pending membership") % (self,)
2307
            raise AssertionError(m)
2308

    
2309
        state = self.state
2310
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
2311
            self.pending_application = None
2312
            self.pending_serial = None
2313
            self.save()
2314
        else:
2315
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2316
            raise AssertionError(m)
2317

    
2318
class Serial(models.Model):
2319
    serial  =   models.AutoField(primary_key=True)
2320

    
2321
def new_serial():
2322
    s = Serial.objects.create()
2323
    serial = s.serial
2324
    s.delete()
2325
    return serial
2326

    
2327
def sync_finish_serials(serials_to_ack=None):
2328
    if serials_to_ack is None:
2329
        serials_to_ack = qh_query_serials([])
2330

    
2331
    serials_to_ack = set(serials_to_ack)
2332
    sfu = ProjectMembership.objects.select_for_update()
2333
    memberships = list(sfu.filter(pending_serial__isnull=False))
2334

    
2335
    if memberships:
2336
        for membership in memberships:
2337
            serial = membership.pending_serial
2338
            if serial in serials_to_ack:
2339
                membership.set_sync()
2340
            else:
2341
                membership.reset_sync()
2342

    
2343
        transaction.commit()
2344

    
2345
    qh_ack_serials(list(serials_to_ack))
2346
    return len(memberships)
2347

    
2348
def pre_sync_projects(sync=True):
2349
    ACCEPTED = ProjectMembership.ACCEPTED
2350
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2351
    psfu = Project.objects.select_for_update()
2352

    
2353
    modified = list(psfu.modified_projects())
2354
    if sync:
2355
        for project in modified:
2356
            objects = project.projectmembership_set.select_for_update()
2357

    
2358
            memberships = objects.filter(state=ACCEPTED)
2359
            for membership in memberships:
2360
                membership.is_pending = True
2361
                membership.save()
2362

    
2363
    reactivating = list(psfu.reactivating_projects())
2364
    if sync:
2365
        for project in reactivating:
2366
            objects = project.projectmembership_set.select_for_update()
2367

    
2368
            memberships = objects.filter(state=PROJECT_DEACTIVATED)
2369
            for membership in memberships:
2370
                membership.is_pending = True
2371
                membership.state = ACCEPTED
2372
                membership.save()
2373

    
2374
    deactivating = list(psfu.deactivating_projects())
2375
    if sync:
2376
        for project in deactivating:
2377
            objects = project.projectmembership_set.select_for_update()
2378

    
2379
            # Note: we keep a user-level deactivation
2380
            # (e.g. USER_SUSPENDED) intact
2381
            memberships = objects.filter(state=ACCEPTED)
2382
            for membership in memberships:
2383
                membership.is_pending = True
2384
                membership.state = PROJECT_DEACTIVATED
2385
                membership.save()
2386

    
2387
    return (modified, reactivating, deactivating)
2388

    
2389
def do_sync_projects():
2390

    
2391
    ACCEPTED = ProjectMembership.ACCEPTED
2392
    objects = ProjectMembership.objects.select_for_update()
2393

    
2394
    sub_quota, add_quota = [], []
2395

    
2396
    serial = new_serial()
2397

    
2398
    pending = objects.filter(is_pending=True)
2399
    for membership in pending:
2400

    
2401
        if membership.pending_application:
2402
            m = "%s: impossible: pending_application is not None (%s)" % (
2403
                membership, membership.pending_application)
2404
            raise AssertionError(m)
2405
        if membership.pending_serial:
2406
            m = "%s: impossible: pending_serial is not None (%s)" % (
2407
                membership, membership.pending_serial)
2408
            raise AssertionError(m)
2409

    
2410
        if membership.state == ACCEPTED:
2411
            membership.pending_application = membership.project.application
2412

    
2413
        membership.pending_serial = serial
2414
        membership.get_diff_quotas(sub_quota, add_quota)
2415
        membership.save()
2416

    
2417
    transaction.commit()
2418
    # ProjectApplication.approve() unblocks here
2419
    # and can set PENDING an already PENDING membership
2420
    # which has been scheduled to sync with the old project.application
2421
    # Need to check in ProjectMembership.set_sync()
2422

    
2423
    r = qh_add_quota(serial, sub_quota, add_quota)
2424
    if r:
2425
        m = "cannot sync serial: %d" % serial
2426
        raise RuntimeError(m)
2427

    
2428
    return serial
2429

    
2430
def post_sync_projects():
2431
    ACCEPTED = ProjectMembership.ACCEPTED
2432
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2433
    psfu = Project.objects.select_for_update()
2434

    
2435
    modified = psfu.modified_projects()
2436
    for project in modified:
2437
        objects = project.projectmembership_set.select_for_update()
2438

    
2439
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2440
        if not memberships:
2441
            project.is_modified = False
2442
            project.save()
2443

    
2444
    reactivating = psfu.reactivating_projects()
2445
    for project in reactivating:
2446
        objects = project.projectmembership_set.select_for_update()
2447
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2448
                                          Q(is_pending=True)))
2449
        if not memberships:
2450
            project.reactivate()
2451
            project.save()
2452

    
2453
    deactivating = psfu.deactivating_projects()
2454
    for project in deactivating:
2455
        objects = project.projectmembership_set.select_for_update()
2456

    
2457
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2458
                                          Q(is_pending=True)))
2459
        if not memberships:
2460
            project.deactivate()
2461
            project.save()
2462

    
2463
    transaction.commit()
2464

    
2465
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2466
    @with_lock(retries, retry_wait)
2467
    def _sync_projects(sync):
2468
        sync_finish_serials()
2469
        # Informative only -- no select_for_update()
2470
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2471

    
2472
        projects_log = pre_sync_projects(sync)
2473
        if sync:
2474
            serial = do_sync_projects()
2475
            sync_finish_serials([serial])
2476
            post_sync_projects()
2477

    
2478
        return (pending, projects_log)
2479
    return _sync_projects(sync)
2480

    
2481
def all_users_quotas(users):
2482
    quotas = {}
2483
    for user in users:
2484
        quotas[user.uuid] = user.all_quotas()
2485
    return quotas
2486

    
2487
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2488
    @with_lock(retries, retry_wait)
2489
    def _sync_users(users, sync):
2490
        sync_finish_serials()
2491

    
2492
        existing, nonexisting = qh_check_users(users)
2493
        resources = get_resource_names()
2494
        registered_quotas = qh_get_quota_limits(existing, resources)
2495
        astakos_quotas = all_users_quotas(users)
2496

    
2497
        if sync:
2498
            r = register_users(nonexisting)
2499
            r = send_quotas(astakos_quotas)
2500

    
2501
        return (existing, nonexisting, registered_quotas, astakos_quotas)
2502
    return _sync_users(users, sync)
2503

    
2504
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2505
    users = AstakosUser.objects.filter(is_active=True)
2506
    return sync_users(users, sync, retries, retry_wait)
2507

    
2508
class ProjectMembershipHistory(models.Model):
2509
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2510
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2511

    
2512
    person  =   models.BigIntegerField()
2513
    project =   models.BigIntegerField()
2514
    date    =   models.DateField(auto_now_add=True)
2515
    reason  =   models.IntegerField()
2516
    serial  =   models.BigIntegerField()
2517

    
2518
### SIGNALS ###
2519
################
2520

    
2521
def create_astakos_user(u):
2522
    try:
2523
        AstakosUser.objects.get(user_ptr=u.pk)
2524
    except AstakosUser.DoesNotExist:
2525
        extended_user = AstakosUser(user_ptr_id=u.pk)
2526
        extended_user.__dict__.update(u.__dict__)
2527
        extended_user.save()
2528
        if not extended_user.has_auth_provider('local'):
2529
            extended_user.add_auth_provider('local')
2530
    except BaseException, e:
2531
        logger.exception(e)
2532

    
2533

    
2534
def fix_superusers(sender, **kwargs):
2535
    # Associate superusers with AstakosUser
2536
    admins = User.objects.filter(is_superuser=True)
2537
    for u in admins:
2538
        create_astakos_user(u)
2539
post_syncdb.connect(fix_superusers)
2540

    
2541

    
2542
def user_post_save(sender, instance, created, **kwargs):
2543
    if not created:
2544
        return
2545
    create_astakos_user(instance)
2546
post_save.connect(user_post_save, sender=User)
2547

    
2548
def astakosuser_post_save(sender, instance, created, **kwargs):
2549
    pass
2550

    
2551
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2552

    
2553
def resource_post_save(sender, instance, created, **kwargs):
2554
    pass
2555

    
2556
post_save.connect(resource_post_save, sender=Resource)
2557

    
2558
def renew_token(sender, instance, **kwargs):
2559
    if not instance.auth_token:
2560
        instance.renew_token()
2561
pre_save.connect(renew_token, sender=AstakosUser)
2562
pre_save.connect(renew_token, sender=Service)
2563