Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 0e79735c

History | View | Annotate | Download (79.7 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, sleep
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, connection
49
from django.contrib.auth.models import User, UserManager, Group, Permission
50
from django.utils.translation import ugettext as _
51
from django.db import transaction
52
from django.core.exceptions import ValidationError
53
from django.db.models.signals import (
54
    pre_save, post_save, post_syncdb, post_delete)
55
from django.contrib.contenttypes.models import ContentType
56

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

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

    
81
import astakos.im.messages as astakos_messages
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
        q = self.filter(uuid__in=l) if l != None else self
318
        q = self.filter(username__in=l) if l != None else self
319
        return dict(q.values_list('username', 'uuid'))
320

    
321

    
322

    
323
class AstakosUser(User):
324
    """
325
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
326
    """
327
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
328
                                   null=True)
329

    
330
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
331
    #                    AstakosUserProvider model.
332
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
333
                                null=True)
334
    # ex. screen_name for twitter, eppn for shibboleth
335
    third_party_identifier = models.CharField(_('Third-party identifier'),
336
                                              max_length=255, null=True,
337
                                              blank=True)
338

    
339

    
340
    #for invitations
341
    user_level = DEFAULT_USER_LEVEL
342
    level = models.IntegerField(_('Inviter level'), default=user_level)
343
    invitations = models.IntegerField(
344
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
345

    
346
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
347
                                  null=True, blank=True, help_text = _( 'test' ))
348
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
349
    auth_token_expires = models.DateTimeField(
350
        _('Token expiration date'), null=True)
351

    
352
    updated = models.DateTimeField(_('Update date'))
353
    is_verified = models.BooleanField(_('Is verified?'), default=False)
354

    
355
    email_verified = models.BooleanField(_('Email verified?'), default=False)
356

    
357
    has_credits = models.BooleanField(_('Has credits?'), default=False)
358
    has_signed_terms = models.BooleanField(
359
        _('I agree with the terms'), default=False)
360
    date_signed_terms = models.DateTimeField(
361
        _('Signed terms date'), null=True, blank=True)
362

    
363
    activation_sent = models.DateTimeField(
364
        _('Activation sent data'), null=True, blank=True)
365

    
366
    policy = models.ManyToManyField(
367
        Resource, null=True, through='AstakosUserQuota')
368

    
369
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
370

    
371
    __has_signed_terms = False
372
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
373
                                           default=False, db_index=True)
374

    
375
    objects = AstakosUserManager()
376

    
377
    def __init__(self, *args, **kwargs):
378
        super(AstakosUser, self).__init__(*args, **kwargs)
379
        self.__has_signed_terms = self.has_signed_terms
380
        if not self.id:
381
            self.is_active = False
382

    
383
    @property
384
    def realname(self):
385
        return '%s %s' % (self.first_name, self.last_name)
386

    
387
    @realname.setter
388
    def realname(self, value):
389
        parts = value.split(' ')
390
        if len(parts) == 2:
391
            self.first_name = parts[0]
392
            self.last_name = parts[1]
393
        else:
394
            self.last_name = parts[0]
395

    
396
    def add_permission(self, pname):
397
        if self.has_perm(pname):
398
            return
399
        p, created = Permission.objects.get_or_create(
400
                                    codename=pname,
401
                                    name=pname.capitalize(),
402
                                    content_type=get_content_type())
403
        self.user_permissions.add(p)
404

    
405
    def remove_permission(self, pname):
406
        if self.has_perm(pname):
407
            return
408
        p = Permission.objects.get(codename=pname,
409
                                   content_type=get_content_type())
410
        self.user_permissions.remove(p)
411

    
412
    @property
413
    def invitation(self):
414
        try:
415
            return Invitation.objects.get(username=self.email)
416
        except Invitation.DoesNotExist:
417
            return None
418

    
419
    def initial_quotas(self):
420
        quotas = dict(get_default_quota())
421
        for user_quota in self.policies:
422
            resource = user_quota.resource.full_name()
423
            quotas[resource] = user_quota.quota_values()
424
        return quotas
425

    
426
    def all_quotas(self):
427
        quotas = self.initial_quotas()
428

    
429
        objects = self.projectmembership_set.select_related()
430
        memberships = objects.filter(is_active=True)
431
        for membership in memberships:
432
            application = membership.application
433
            if application is None:
434
                m = _("missing application for active membership %s"
435
                      % (membership,))
436
                raise AssertionError(m)
437

    
438
            grants = application.projectresourcegrant_set.all()
439
            for grant in grants:
440
                resource = grant.resource.full_name()
441
                prev = quotas.get(resource, 0)
442
                new = add_quota_values(prev, grant.member_quota_values())
443
                quotas[resource] = new
444
        return quotas
445

    
446
    @property
447
    def policies(self):
448
        return self.astakosuserquota_set.select_related().all()
449

    
450
    @policies.setter
451
    def policies(self, policies):
452
        for p in policies:
453
            p.setdefault('resource', '')
454
            p.setdefault('capacity', 0)
455
            p.setdefault('quantity', 0)
456
            p.setdefault('import_limit', 0)
457
            p.setdefault('export_limit', 0)
458
            p.setdefault('update', True)
459
            self.add_resource_policy(**p)
460

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

    
480
    def remove_resource_policy(self, service, resource):
481
        """Raises ObjectDoesNotExist, IntegrityError"""
482
        resource = Resource.objects.get(service__name=service, name=resource)
483
        q = self.policies.get(resource=resource).delete()
484

    
485
    def update_uuid(self):
486
        while not self.uuid:
487
            uuid_val =  str(uuid.uuid4())
488
            try:
489
                AstakosUser.objects.get(uuid=uuid_val)
490
            except AstakosUser.DoesNotExist, e:
491
                self.uuid = uuid_val
492
        return self.uuid
493

    
494
    def save(self, update_timestamps=True, **kwargs):
495
        if update_timestamps:
496
            if not self.id:
497
                self.date_joined = datetime.now()
498
            self.updated = datetime.now()
499

    
500
        # update date_signed_terms if necessary
501
        if self.__has_signed_terms != self.has_signed_terms:
502
            self.date_signed_terms = datetime.now()
503

    
504
        self.update_uuid()
505

    
506
        if self.username != self.email.lower():
507
            # set username
508
            self.username = self.email.lower()
509

    
510
        super(AstakosUser, self).save(**kwargs)
511

    
512
    def renew_token(self, flush_sessions=False, current_key=None):
513
        md5 = hashlib.md5()
514
        md5.update(settings.SECRET_KEY)
515
        md5.update(self.username)
516
        md5.update(self.realname.encode('ascii', 'ignore'))
517
        md5.update(asctime())
518

    
519
        self.auth_token = b64encode(md5.digest())
520
        self.auth_token_created = datetime.now()
521
        self.auth_token_expires = self.auth_token_created + \
522
                                  timedelta(hours=AUTH_TOKEN_DURATION)
523
        if flush_sessions:
524
            self.flush_sessions(current_key)
525
        msg = 'Token renewed for %s' % self.email
526
        logger.log(LOGGING_LEVEL, msg)
527

    
528
    def flush_sessions(self, current_key=None):
529
        q = self.sessions
530
        if current_key:
531
            q = q.exclude(session_key=current_key)
532

    
533
        keys = q.values_list('session_key', flat=True)
534
        if keys:
535
            msg = 'Flushing sessions: %s' % ','.join(keys)
536
            logger.log(LOGGING_LEVEL, msg, [])
537
        engine = import_module(settings.SESSION_ENGINE)
538
        for k in keys:
539
            s = engine.SessionStore(k)
540
            s.flush()
541

    
542
    def __unicode__(self):
543
        return '%s (%s)' % (self.realname, self.email)
544

    
545
    def conflicting_email(self):
546
        q = AstakosUser.objects.exclude(username=self.username)
547
        q = q.filter(email__iexact=self.email)
548
        if q.count() != 0:
549
            return True
550
        return False
551

    
552
    def email_change_is_pending(self):
553
        return self.emailchanges.count() > 0
554

    
555
    def email_change_is_pending(self):
556
        return self.emailchanges.count() > 0
557

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

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

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

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

    
591
        if not provider_settings.is_available_for_add():
592
            return False
593

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

    
598
        if 'provider_info' in kwargs:
599
            kwargs.pop('provider_info')
600

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

    
614
        return True
615

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

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

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

    
627
        return True
628

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

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

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

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

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

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

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

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

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

    
683
        pending.delete()
684
        return provider
685

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

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

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

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

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

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

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

    
721
        return providers
722

    
723
    def get_active_auth_providers(self, **filters):
724
        providers = []
725
        for provider in self.get_auth_providers().active(**filters):
726
            if auth_providers.get_provider(provider.module).is_available_for_login():
727
                providers.append(provider)
728
        return providers
729

    
730
    @property
731
    def auth_providers_display(self):
732
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
733

    
734
    def get_inactive_message(self):
735
        msg_extra = ''
736
        message = ''
737
        if self.activation_sent:
738
            if self.email_verified:
739
                message = _(astakos_messages.ACCOUNT_INACTIVE)
740
            else:
741
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
742
                if astakos_settings.MODERATION_ENABLED:
743
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
744
                else:
745
                    url = self.get_resend_activation_url()
746
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
747
                                u' ' + \
748
                                _('<a href="%s">%s?</a>') % (url,
749
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
750
        else:
751
            if astakos_settings.MODERATION_ENABLED:
752
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
753
            else:
754
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
755
                url = self.get_resend_activation_url()
756
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
757
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
758

    
759
        return mark_safe(message + u' '+ msg_extra)
760

    
761
    def owns_application(self, application):
762
        return application.owner == self
763

    
764
    def owns_project(self, project):
765
        return project.application.owner == self
766

    
767
    def is_project_member(self, project_or_application):
768
        return self.get_status_in_project(project_or_application) in \
769
                                        ProjectMembership.ASSOCIATED_STATES
770

    
771
    def is_project_accepted_member(self, project_or_application):
772
        return self.get_status_in_project(project_or_application) in \
773
                                            ProjectMembership.ACCEPTED_STATES
774

    
775
    def get_status_in_project(self, project_or_application):
776
        application = project_or_application
777
        if isinstance(project_or_application, Project):
778
            application = project_or_application.project
779
        return application.user_status(self)
780

    
781
    def get_membership(self, project):
782
        try:
783
            return ProjectMembership.objects.get(
784
                project=project,
785
                person=self)
786
        except ProjectMembership.DoesNotExist:
787
            return None
788

    
789
class AstakosUserAuthProviderManager(models.Manager):
790

    
791
    def active(self, **filters):
792
        return self.filter(active=True, **filters)
793

    
794
    def remove_unverified_providers(self, provider, **filters):
795
        try:
796
            existing = self.filter(module=provider, user__email_verified=False, **filters)
797
            for p in existing:
798
                p.user.delete()
799
        except:
800
            pass
801

    
802

    
803

    
804
class AstakosUserAuthProvider(models.Model):
805
    """
806
    Available user authentication methods.
807
    """
808
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
809
                                   null=True, default=None)
810
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
811
    module = models.CharField(_('Provider'), max_length=255, blank=False,
812
                                default='local')
813
    identifier = models.CharField(_('Third-party identifier'),
814
                                              max_length=255, null=True,
815
                                              blank=True)
816
    active = models.BooleanField(default=True)
817
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
818
                                   default='astakos')
819
    info_data = models.TextField(default="", null=True, blank=True)
820
    created = models.DateTimeField('Creation date', auto_now_add=True)
821

    
822
    objects = AstakosUserAuthProviderManager()
823

    
824
    class Meta:
825
        unique_together = (('identifier', 'module', 'user'), )
826
        ordering = ('module', 'created')
827

    
828
    def __init__(self, *args, **kwargs):
829
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
830
        try:
831
            self.info = json.loads(self.info_data)
832
            if not self.info:
833
                self.info = {}
834
        except Exception, e:
835
            self.info = {}
836

    
837
        for key,value in self.info.iteritems():
838
            setattr(self, 'info_%s' % key, value)
839

    
840

    
841
    @property
842
    def settings(self):
843
        return auth_providers.get_provider(self.module)
844

    
845
    @property
846
    def details_display(self):
847
        try:
848
          return self.settings.get_details_tpl_display % self.__dict__
849
        except:
850
          return ''
851

    
852
    @property
853
    def title_display(self):
854
        title_tpl = self.settings.get_title_display
855
        try:
856
            if self.settings.get_user_title_display:
857
                title_tpl = self.settings.get_user_title_display
858
        except Exception, e:
859
            pass
860
        try:
861
          return title_tpl % self.__dict__
862
        except:
863
          return self.settings.get_title_display % self.__dict__
864

    
865
    def can_remove(self):
866
        return self.user.can_remove_auth_provider(self.module)
867

    
868
    def delete(self, *args, **kwargs):
869
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
870
        if self.module == 'local':
871
            self.user.set_unusable_password()
872
            self.user.save()
873
        return ret
874

    
875
    def __repr__(self):
876
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
877

    
878
    def __unicode__(self):
879
        if self.identifier:
880
            return "%s:%s" % (self.module, self.identifier)
881
        if self.auth_backend:
882
            return "%s:%s" % (self.module, self.auth_backend)
883
        return self.module
884

    
885
    def save(self, *args, **kwargs):
886
        self.info_data = json.dumps(self.info)
887
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
888

    
889

    
890
class ExtendedManager(models.Manager):
891
    def _update_or_create(self, **kwargs):
892
        assert kwargs, \
893
            'update_or_create() must be passed at least one keyword argument'
894
        obj, created = self.get_or_create(**kwargs)
895
        defaults = kwargs.pop('defaults', {})
896
        if created:
897
            return obj, True, False
898
        else:
899
            try:
900
                params = dict(
901
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
902
                params.update(defaults)
903
                for attr, val in params.items():
904
                    if hasattr(obj, attr):
905
                        setattr(obj, attr, val)
906
                sid = transaction.savepoint()
907
                obj.save(force_update=True)
908
                transaction.savepoint_commit(sid)
909
                return obj, False, True
910
            except IntegrityError, e:
911
                transaction.savepoint_rollback(sid)
912
                try:
913
                    return self.get(**kwargs), False, False
914
                except self.model.DoesNotExist:
915
                    raise e
916

    
917
    update_or_create = _update_or_create
918

    
919

    
920
class AstakosUserQuota(models.Model):
921
    objects = ExtendedManager()
922
    capacity = intDecimalField()
923
    quantity = intDecimalField(default=0)
924
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
925
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
926
    resource = models.ForeignKey(Resource)
927
    user = models.ForeignKey(AstakosUser)
928

    
929
    class Meta:
930
        unique_together = ("resource", "user")
931

    
932
    def quota_values(self):
933
        return QuotaValues(
934
            quantity = self.quantity,
935
            capacity = self.capacity,
936
            import_limit = self.import_limit,
937
            export_limit = self.export_limit)
938

    
939

    
940
class ApprovalTerms(models.Model):
941
    """
942
    Model for approval terms
943
    """
944

    
945
    date = models.DateTimeField(
946
        _('Issue date'), db_index=True, auto_now_add=True)
947
    location = models.CharField(_('Terms location'), max_length=255)
948

    
949

    
950
class Invitation(models.Model):
951
    """
952
    Model for registring invitations
953
    """
954
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
955
                                null=True)
956
    realname = models.CharField(_('Real name'), max_length=255)
957
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
958
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
959
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
960
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
961
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
962

    
963
    def __init__(self, *args, **kwargs):
964
        super(Invitation, self).__init__(*args, **kwargs)
965
        if not self.id:
966
            self.code = _generate_invitation_code()
967

    
968
    def consume(self):
969
        self.is_consumed = True
970
        self.consumed = datetime.now()
971
        self.save()
972

    
973
    def __unicode__(self):
974
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
975

    
976

    
977
class EmailChangeManager(models.Manager):
978

    
979
    @transaction.commit_on_success
980
    def change_email(self, activation_key):
981
        """
982
        Validate an activation key and change the corresponding
983
        ``User`` if valid.
984

985
        If the key is valid and has not expired, return the ``User``
986
        after activating.
987

988
        If the key is not valid or has expired, return ``None``.
989

990
        If the key is valid but the ``User`` is already active,
991
        return ``None``.
992

993
        After successful email change the activation record is deleted.
994

995
        Throws ValueError if there is already
996
        """
997
        try:
998
            email_change = self.model.objects.get(
999
                activation_key=activation_key)
1000
            if email_change.activation_key_expired():
1001
                email_change.delete()
1002
                raise EmailChange.DoesNotExist
1003
            # is there an active user with this address?
1004
            try:
1005
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1006
            except AstakosUser.DoesNotExist:
1007
                pass
1008
            else:
1009
                raise ValueError(_('The new email address is reserved.'))
1010
            # update user
1011
            user = AstakosUser.objects.get(pk=email_change.user_id)
1012
            old_email = user.email
1013
            user.email = email_change.new_email_address
1014
            user.save()
1015
            email_change.delete()
1016
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1017
                                                          user.email)
1018
            logger.log(LOGGING_LEVEL, msg)
1019
            return user
1020
        except EmailChange.DoesNotExist:
1021
            raise ValueError(_('Invalid activation key.'))
1022

    
1023

    
1024
class EmailChange(models.Model):
1025
    new_email_address = models.EmailField(
1026
        _(u'new e-mail address'),
1027
        help_text=_('Your old email address will be used until you verify your new one.'))
1028
    user = models.ForeignKey(
1029
        AstakosUser, unique=True, related_name='emailchanges')
1030
    requested_at = models.DateTimeField(auto_now_add=True)
1031
    activation_key = models.CharField(
1032
        max_length=40, unique=True, db_index=True)
1033

    
1034
    objects = EmailChangeManager()
1035

    
1036
    def get_url(self):
1037
        return reverse('email_change_confirm',
1038
                      kwargs={'activation_key': self.activation_key})
1039

    
1040
    def activation_key_expired(self):
1041
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1042
        return self.requested_at + expiration_date < datetime.now()
1043

    
1044

    
1045
class AdditionalMail(models.Model):
1046
    """
1047
    Model for registring invitations
1048
    """
1049
    owner = models.ForeignKey(AstakosUser)
1050
    email = models.EmailField()
1051

    
1052

    
1053
def _generate_invitation_code():
1054
    while True:
1055
        code = randint(1, 2L ** 63 - 1)
1056
        try:
1057
            Invitation.objects.get(code=code)
1058
            # An invitation with this code already exists, try again
1059
        except Invitation.DoesNotExist:
1060
            return code
1061

    
1062

    
1063
def get_latest_terms():
1064
    try:
1065
        term = ApprovalTerms.objects.order_by('-id')[0]
1066
        return term
1067
    except IndexError:
1068
        pass
1069
    return None
1070

    
1071
class PendingThirdPartyUser(models.Model):
1072
    """
1073
    Model for registring successful third party user authentications
1074
    """
1075
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1076
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1077
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1078
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1079
                                  null=True)
1080
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1081
                                 null=True)
1082
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1083
                                   null=True)
1084
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1085
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1086
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1087
    info = models.TextField(default="", null=True, blank=True)
1088

    
1089
    class Meta:
1090
        unique_together = ("provider", "third_party_identifier")
1091

    
1092
    def get_user_instance(self):
1093
        d = self.__dict__
1094
        d.pop('_state', None)
1095
        d.pop('id', None)
1096
        d.pop('token', None)
1097
        d.pop('created', None)
1098
        d.pop('info', None)
1099
        user = AstakosUser(**d)
1100

    
1101
        return user
1102

    
1103
    @property
1104
    def realname(self):
1105
        return '%s %s' %(self.first_name, self.last_name)
1106

    
1107
    @realname.setter
1108
    def realname(self, value):
1109
        parts = value.split(' ')
1110
        if len(parts) == 2:
1111
            self.first_name = parts[0]
1112
            self.last_name = parts[1]
1113
        else:
1114
            self.last_name = parts[0]
1115

    
1116
    def save(self, **kwargs):
1117
        if not self.id:
1118
            # set username
1119
            while not self.username:
1120
                username =  uuid.uuid4().hex[:30]
1121
                try:
1122
                    AstakosUser.objects.get(username = username)
1123
                except AstakosUser.DoesNotExist, e:
1124
                    self.username = username
1125
        super(PendingThirdPartyUser, self).save(**kwargs)
1126

    
1127
    def generate_token(self):
1128
        self.password = self.third_party_identifier
1129
        self.last_login = datetime.now()
1130
        self.token = default_token_generator.make_token(self)
1131

    
1132
class SessionCatalog(models.Model):
1133
    session_key = models.CharField(_('session key'), max_length=40)
1134
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1135

    
1136

    
1137
### PROJECTS ###
1138
################
1139

    
1140
def synced_model_metaclass(class_name, class_parents, class_attributes):
1141

    
1142
    new_attributes = {}
1143
    sync_attributes = {}
1144

    
1145
    for name, value in class_attributes.iteritems():
1146
        sync, underscore, rest = name.partition('_')
1147
        if sync == 'sync' and underscore == '_':
1148
            sync_attributes[rest] = value
1149
        else:
1150
            new_attributes[name] = value
1151

    
1152
    if 'prefix' not in sync_attributes:
1153
        m = ("you did not specify a 'sync_prefix' attribute "
1154
             "in class '%s'" % (class_name,))
1155
        raise ValueError(m)
1156

    
1157
    prefix = sync_attributes.pop('prefix')
1158
    class_name = sync_attributes.pop('classname', prefix + '_model')
1159

    
1160
    for name, value in sync_attributes.iteritems():
1161
        newname = prefix + '_' + name
1162
        if newname in new_attributes:
1163
            m = ("class '%s' was specified with prefix '%s' "
1164
                 "but it already has an attribute named '%s'"
1165
                 % (class_name, prefix, newname))
1166
            raise ValueError(m)
1167

    
1168
        new_attributes[newname] = value
1169

    
1170
    newclass = type(class_name, class_parents, new_attributes)
1171
    return newclass
1172

    
1173

    
1174
def make_synced(prefix='sync', name='SyncedState'):
1175

    
1176
    the_name = name
1177
    the_prefix = prefix
1178

    
1179
    class SyncedState(models.Model):
1180

    
1181
        sync_classname      = the_name
1182
        sync_prefix         = the_prefix
1183
        __metaclass__       = synced_model_metaclass
1184

    
1185
        sync_new_state      = models.BigIntegerField(null=True)
1186
        sync_synced_state   = models.BigIntegerField(null=True)
1187
        STATUS_SYNCED       = 0
1188
        STATUS_PENDING      = 1
1189
        sync_status         = models.IntegerField(db_index=True)
1190

    
1191
        class Meta:
1192
            abstract = True
1193

    
1194
        class NotSynced(Exception):
1195
            pass
1196

    
1197
        def sync_init_state(self, state):
1198
            self.sync_synced_state = state
1199
            self.sync_new_state = state
1200
            self.sync_status = self.STATUS_SYNCED
1201

    
1202
        def sync_get_status(self):
1203
            return self.sync_status
1204

    
1205
        def sync_set_status(self):
1206
            if self.sync_new_state != self.sync_synced_state:
1207
                self.sync_status = self.STATUS_PENDING
1208
            else:
1209
                self.sync_status = self.STATUS_SYNCED
1210

    
1211
        def sync_set_synced(self):
1212
            self.sync_synced_state = self.sync_new_state
1213
            self.sync_status = self.STATUS_SYNCED
1214

    
1215
        def sync_get_synced_state(self):
1216
            return self.sync_synced_state
1217

    
1218
        def sync_set_new_state(self, new_state):
1219
            self.sync_new_state = new_state
1220
            self.sync_set_status()
1221

    
1222
        def sync_get_new_state(self):
1223
            return self.sync_new_state
1224

    
1225
        def sync_set_synced_state(self, synced_state):
1226
            self.sync_synced_state = synced_state
1227
            self.sync_set_status()
1228

    
1229
        def sync_get_pending_objects(self):
1230
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1231
            return self.objects.filter(**kw)
1232

    
1233
        def sync_get_synced_objects(self):
1234
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1235
            return self.objects.filter(**kw)
1236

    
1237
        def sync_verify_get_synced_state(self):
1238
            status = self.sync_get_status()
1239
            state = self.sync_get_synced_state()
1240
            verified = (status == self.STATUS_SYNCED)
1241
            return state, verified
1242

    
1243
        def sync_is_synced(self):
1244
            state, verified = self.sync_verify_get_synced_state()
1245
            return verified
1246

    
1247
    return SyncedState
1248

    
1249
SyncedState = make_synced(prefix='sync', name='SyncedState')
1250

    
1251

    
1252
class ProjectApplicationManager(ForUpdateManager):
1253

    
1254
    def user_visible_projects(self, *filters, **kw_filters):
1255
        model = self.model
1256
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1257

    
1258
    def user_visible_by_chain(self, flt):
1259
        model = self.model
1260
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1261
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1262
        by_chain = dict(pending.annotate(models.Max('id')))
1263
        by_chain.update(approved.annotate(models.Max('id')))
1264
        return self.filter(flt, id__in=by_chain.values())
1265

    
1266
    def user_accessible_projects(self, user):
1267
        """
1268
        Return projects accessed by specified user.
1269
        """
1270
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1271
                               Q(project__projectmembership__person=user)
1272

    
1273
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1274

    
1275
    def search_by_name(self, *search_strings):
1276
        q = Q()
1277
        for s in search_strings:
1278
            q = q | Q(name__icontains=s)
1279
        return self.filter(q)
1280

    
1281
    def latest_of_chain(self, chain_id):
1282
        try:
1283
            return self.filter(chain=chain_id).order_by('-id')[0]
1284
        except IndexError:
1285
            return None
1286

    
1287
USER_STATUS_DISPLAY = {
1288
      0: _('Join requested'),
1289
      1: _('Accepted member'),
1290
     10: _('Suspended'),
1291
    100: _('Terminated'),
1292
    200: _('Removed'),
1293
     -1: _('Not a member'),
1294
}
1295

    
1296

    
1297
class Chain(models.Model):
1298
    chain  =   models.AutoField(primary_key=True)
1299

    
1300
def new_chain():
1301
    c = Chain.objects.create()
1302
    chain = c.chain
1303
    c.delete()
1304
    return chain
1305

    
1306

    
1307
class ProjectApplication(models.Model):
1308
    applicant               =   models.ForeignKey(
1309
                                    AstakosUser,
1310
                                    related_name='projects_applied',
1311
                                    db_index=True)
1312

    
1313
    PENDING     =    0
1314
    APPROVED    =    1
1315
    REPLACED    =    2
1316
    DENIED      =    3
1317
    DISMISSED   =    4
1318
    CANCELLED   =    5
1319

    
1320
    state                   =   models.IntegerField(default=PENDING,
1321
                                                    db_index=True)
1322

    
1323
    owner                   =   models.ForeignKey(
1324
                                    AstakosUser,
1325
                                    related_name='projects_owned',
1326
                                    db_index=True)
1327

    
1328
    chain                   =   models.IntegerField()
1329
    precursor_application   =   models.ForeignKey('ProjectApplication',
1330
                                                  null=True,
1331
                                                  blank=True)
1332

    
1333
    name                    =   models.CharField(max_length=80)
1334
    homepage                =   models.URLField(max_length=255, null=True)
1335
    description             =   models.TextField(null=True, blank=True)
1336
    start_date              =   models.DateTimeField(null=True, blank=True)
1337
    end_date                =   models.DateTimeField()
1338
    member_join_policy      =   models.IntegerField()
1339
    member_leave_policy     =   models.IntegerField()
1340
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1341
    resource_grants         =   models.ManyToManyField(
1342
                                    Resource,
1343
                                    null=True,
1344
                                    blank=True,
1345
                                    through='ProjectResourceGrant')
1346
    comments                =   models.TextField(null=True, blank=True)
1347
    issue_date              =   models.DateTimeField(auto_now_add=True)
1348
    response_date           =   models.DateTimeField(null=True, blank=True)
1349

    
1350
    objects                 =   ProjectApplicationManager()
1351

    
1352
    # Compiled queries
1353
    Q_PENDING  = Q(state=PENDING)
1354
    Q_APPROVED = Q(state=APPROVED)
1355
    Q_DENIED   = Q(state=DENIED)
1356

    
1357
    class Meta:
1358
        unique_together = ("chain", "id")
1359

    
1360
    def __unicode__(self):
1361
        return "%s applied by %s" % (self.name, self.applicant)
1362

    
1363
    # TODO: Move to a more suitable place
1364
    APPLICATION_STATE_DISPLAY = {
1365
        PENDING  : _('Pending review'),
1366
        APPROVED : _('Active'),
1367
        REPLACED : _('Replaced'),
1368
        DENIED   : _('Denied'),
1369
        DISMISSED: _('Dismissed'),
1370
        CANCELLED: _('Cancelled')
1371
    }
1372

    
1373
    def get_project(self):
1374
        try:
1375
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1376
            return Project
1377
        except Project.DoesNotExist, e:
1378
            return None
1379

    
1380
    def state_display(self):
1381
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1382

    
1383
    def add_resource_policy(self, service, resource, uplimit):
1384
        """Raises ObjectDoesNotExist, IntegrityError"""
1385
        q = self.projectresourcegrant_set
1386
        resource = Resource.objects.get(service__name=service, name=resource)
1387
        q.create(resource=resource, member_capacity=uplimit)
1388

    
1389
    def user_status(self, user):
1390
        try:
1391
            project = self.get_project()
1392
            if not project:
1393
                return -1
1394
            membership = project.projectmembership_set
1395
            membership = membership.exclude(state=ProjectMembership.REMOVED)
1396
            membership = membership.get(person=user)
1397
            status = membership.state
1398
        except ProjectMembership.DoesNotExist:
1399
            return -1
1400

    
1401
        return status
1402

    
1403
    def user_status_display(self, user):
1404
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1405

    
1406
    def members_count(self):
1407
        return self.project.approved_memberships.count()
1408

    
1409
    @property
1410
    def grants(self):
1411
        return self.projectresourcegrant_set.values(
1412
            'member_capacity', 'resource__name', 'resource__service__name')
1413

    
1414
    @property
1415
    def resource_policies(self):
1416
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1417

    
1418
    @resource_policies.setter
1419
    def resource_policies(self, policies):
1420
        for p in policies:
1421
            service = p.get('service', None)
1422
            resource = p.get('resource', None)
1423
            uplimit = p.get('uplimit', 0)
1424
            self.add_resource_policy(service, resource, uplimit)
1425

    
1426
    def pending_modifications(self):
1427
        q = self.chained_applications()
1428
        q = q.filter(~Q(id=self.id) & Q(state=self.PENDING))
1429
        q = q.order_by('id')
1430
        return q
1431

    
1432
    def last_pending(self):
1433
        try:
1434
            return self.pending_modifications().order_by('-id')[0]
1435
        except IndexError:
1436
            return None
1437

    
1438
    def is_modification(self):
1439
        if self.state != self.PENDING:
1440
            return False
1441
        parents = self.chained_applications().filter(id__lt=self.id)
1442
        parents = parents.filter(state__in=[self.APPROVED])
1443
        return parents.count() > 0
1444

    
1445
    def chained_applications(self):
1446
        return ProjectApplication.objects.filter(chain=self.chain)
1447

    
1448
    def has_pending_modifications(self):
1449
        return bool(self.last_pending())
1450

    
1451
    def get_project(self):
1452
        try:
1453
            return Project.objects.get(id=self.chain)
1454
        except Project.DoesNotExist:
1455
            return None
1456

    
1457
    def _get_project_for_update(self):
1458
        try:
1459
            objects = Project.objects.select_for_update()
1460
            project = objects.get(id=self.chain)
1461
            return project
1462
        except Project.DoesNotExist:
1463
            return None
1464

    
1465
    def can_cancel(self):
1466
        return self.state == self.PENDING
1467

    
1468
    def cancel(self):
1469
        if not self.can_cancel():
1470
            m = _("cannot cancel: application '%s' in state '%s'") % (
1471
                    self.id, self.state)
1472
            raise AssertionError(m)
1473

    
1474
        self.state = self.CANCELLED
1475
        self.save()
1476

    
1477
    def can_dismiss(self):
1478
        return self.state == self.DENIED
1479

    
1480
    def dismiss(self):
1481
        if not self.can_dismiss():
1482
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1483
                    self.id, self.state)
1484
            raise AssertionError(m)
1485

    
1486
        self.state = self.DISMISSED
1487
        self.save()
1488

    
1489
    def can_deny(self):
1490
        return self.state == self.PENDING
1491

    
1492
    def deny(self):
1493
        if not self.can_deny():
1494
            m = _("cannot deny: application '%s' in state '%s'") % (
1495
                    self.id, self.state)
1496
            raise AssertionError(m)
1497

    
1498
        self.state = self.DENIED
1499
        self.response_date = datetime.now()
1500
        self.save()
1501

    
1502
    def can_approve(self):
1503
        return self.state == self.PENDING
1504

    
1505
    def approve(self, approval_user=None):
1506
        """
1507
        If approval_user then during owner membership acceptance
1508
        it is checked whether the request_user is eligible.
1509

1510
        Raises:
1511
            PermissionDenied
1512
        """
1513

    
1514
        if not transaction.is_managed():
1515
            raise AssertionError("NOPE")
1516

    
1517
        new_project_name = self.name
1518
        if not self.can_approve():
1519
            m = _("cannot approve: project '%s' in state '%s'") % (
1520
                    new_project_name, self.state)
1521
            raise AssertionError(m) # invalid argument
1522

    
1523
        now = datetime.now()
1524
        project = self._get_project_for_update()
1525

    
1526
        try:
1527
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1528
            conflicting_project = Project.objects.get(q)
1529
            if (conflicting_project != project):
1530
                m = (_("cannot approve: project with name '%s' "
1531
                       "already exists (serial: %s)") % (
1532
                        new_project_name, conflicting_project.id))
1533
                raise PermissionDenied(m) # invalid argument
1534
        except Project.DoesNotExist:
1535
            pass
1536

    
1537
        new_project = False
1538
        if project is None:
1539
            new_project = True
1540
            project = Project(id=self.chain)
1541

    
1542
        project.name = new_project_name
1543
        project.application = self
1544
        project.last_approval_date = now
1545
        if not new_project:
1546
            project.is_modified = True
1547

    
1548
        project.save()
1549

    
1550
        self.state = self.APPROVED
1551
        self.response_date = now
1552
        self.save()
1553

    
1554
    @property
1555
    def member_join_policy_display(self):
1556
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1557

    
1558
    @property
1559
    def member_leave_policy_display(self):
1560
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1561

    
1562
class ProjectResourceGrant(models.Model):
1563

    
1564
    resource                =   models.ForeignKey(Resource)
1565
    project_application     =   models.ForeignKey(ProjectApplication,
1566
                                                  null=True)
1567
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1568
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1569
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1570
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1571
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1572
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1573

    
1574
    objects = ExtendedManager()
1575

    
1576
    class Meta:
1577
        unique_together = ("resource", "project_application")
1578

    
1579
    def member_quota_values(self):
1580
        return QuotaValues(
1581
            quantity = 0,
1582
            capacity = self.member_capacity,
1583
            import_limit = self.member_import_limit,
1584
            export_limit = self.member_export_limit)
1585

    
1586
    def display_member_capacity(self):
1587
        if self.member_capacity:
1588
            if self.resource.unit:
1589
                return ProjectResourceGrant.display_filesize(
1590
                    self.member_capacity)
1591
            else:
1592
                if math.isinf(self.member_capacity):
1593
                    return 'Unlimited'
1594
                else:
1595
                    return self.member_capacity
1596
        else:
1597
            return 'Unlimited'
1598

    
1599
    def __str__(self):
1600
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1601
                                        self.display_member_capacity())
1602

    
1603
    @classmethod
1604
    def display_filesize(cls, value):
1605
        try:
1606
            value = float(value)
1607
        except:
1608
            return
1609
        else:
1610
            if math.isinf(value):
1611
                return 'Unlimited'
1612
            if value > 1:
1613
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1614
                                [0, 0, 0, 0, 0, 0])
1615
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1616
                quotient = float(value) / 1024**exponent
1617
                unit, value_decimals = unit_list[exponent]
1618
                format_string = '{0:.%sf} {1}' % (value_decimals)
1619
                return format_string.format(quotient, unit)
1620
            if value == 0:
1621
                return '0 bytes'
1622
            if value == 1:
1623
                return '1 byte'
1624
            else:
1625
               return '0'
1626

    
1627

    
1628
class ProjectManager(ForUpdateManager):
1629

    
1630
    def terminated_projects(self):
1631
        q = self.model.Q_TERMINATED
1632
        return self.filter(q)
1633

    
1634
    def not_terminated_projects(self):
1635
        q = ~self.model.Q_TERMINATED
1636
        return self.filter(q)
1637

    
1638
    def terminating_projects(self):
1639
        q = self.model.Q_TERMINATED & Q(is_active=True)
1640
        return self.filter(q)
1641

    
1642
    def deactivated_projects(self):
1643
        q = self.model.Q_DEACTIVATED
1644
        return self.filter(q)
1645

    
1646
    def deactivating_projects(self):
1647
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1648
        return self.filter(q)
1649

    
1650
    def modified_projects(self):
1651
        return self.filter(is_modified=True)
1652

    
1653
    def reactivating_projects(self):
1654
        return self.filter(state=Project.APPROVED, is_active=False)
1655

    
1656
    def expired_projects(self):
1657
        q = (~Q(state=Project.TERMINATED) &
1658
              Q(application__end_date__lt=datetime.now()))
1659
        return self.filter(q)
1660

    
1661

    
1662
class Project(models.Model):
1663

    
1664
    application                 =   models.OneToOneField(
1665
                                            ProjectApplication,
1666
                                            related_name='project')
1667
    last_approval_date          =   models.DateTimeField(null=True)
1668

    
1669
    members                     =   models.ManyToManyField(
1670
                                            AstakosUser,
1671
                                            through='ProjectMembership')
1672

    
1673
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1674
    deactivation_date           =   models.DateTimeField(null=True)
1675

    
1676
    creation_date               =   models.DateTimeField(auto_now_add=True)
1677
    name                        =   models.CharField(
1678
                                            max_length=80,
1679
                                            db_index=True,
1680
                                            unique=True)
1681

    
1682
    APPROVED    = 1
1683
    SUSPENDED   = 10
1684
    TERMINATED  = 100
1685

    
1686
    is_modified                 =   models.BooleanField(default=False,
1687
                                                        db_index=True)
1688
    is_active                   =   models.BooleanField(default=True,
1689
                                                        db_index=True)
1690
    state                       =   models.IntegerField(default=APPROVED,
1691
                                                        db_index=True)
1692

    
1693
    objects     =   ProjectManager()
1694

    
1695
    # Compiled queries
1696
    Q_TERMINATED  = Q(state=TERMINATED)
1697
    Q_SUSPENDED   = Q(state=SUSPENDED)
1698
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1699

    
1700
    def __str__(self):
1701
        return _("<project %s '%s'>") % (self.id, self.application.name)
1702

    
1703
    __repr__ = __str__
1704

    
1705
    STATE_DISPLAY = {
1706
        APPROVED   : 'APPROVED',
1707
        SUSPENDED  : 'SUSPENDED',
1708
        TERMINATED : 'TERMINATED'
1709
        }
1710

    
1711
    def state_display(self):
1712
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1713

    
1714
    def expiration_info(self):
1715
        return (str(self.id), self.name, self.state_display(),
1716
                str(self.application.end_date))
1717

    
1718
    def is_deactivated(self, reason=None):
1719
        if reason is not None:
1720
            return self.state == reason
1721

    
1722
        return self.state != self.APPROVED
1723

    
1724
    def is_deactivating(self, reason=None):
1725
        if not self.is_active:
1726
            return False
1727

    
1728
        return self.is_deactivated(reason)
1729

    
1730
    def is_deactivated_strict(self, reason=None):
1731
        if self.is_active:
1732
            return False
1733

    
1734
        return self.is_deactivated(reason)
1735

    
1736
    ### Deactivation calls
1737

    
1738
    def deactivate(self):
1739
        self.deactivation_date = datetime.now()
1740
        self.is_active = False
1741

    
1742
    def reactivate(self):
1743
        self.deactivation_date = None
1744
        self.is_active = True
1745

    
1746
    def terminate(self):
1747
        self.deactivation_reason = 'TERMINATED'
1748
        self.state = self.TERMINATED
1749
        self.save()
1750

    
1751
    def suspend(self):
1752
        self.deactivation_reason = 'SUSPENDED'
1753
        self.state = self.SUSPENDED
1754
        self.save()
1755

    
1756
    def resume(self):
1757
        self.deactivation_reason = None
1758
        self.state = self.APPROVED
1759
        self.save()
1760

    
1761
    ### Logical checks
1762

    
1763
    def is_inconsistent(self):
1764
        now = datetime.now()
1765
        dates = [self.creation_date,
1766
                 self.last_approval_date,
1767
                 self.deactivation_date]
1768
        return any([date > now for date in dates])
1769

    
1770
    def is_active_strict(self):
1771
        return self.is_active and self.state == self.APPROVED
1772

    
1773
    def is_approved(self):
1774
        return self.state == self.APPROVED
1775

    
1776
    @property
1777
    def is_alive(self):
1778
        return not self.is_terminated
1779

    
1780
    @property
1781
    def is_terminated(self):
1782
        return self.is_deactivated(self.TERMINATED)
1783

    
1784
    @property
1785
    def is_suspended(self):
1786
        return self.is_deactivated(self.SUSPENDED)
1787

    
1788
    def violates_resource_grants(self):
1789
        return False
1790

    
1791
    def violates_members_limit(self, adding=0):
1792
        application = self.application
1793
        limit = application.limit_on_members_number
1794
        if limit is None:
1795
            return False
1796
        return (len(self.approved_members) + adding > limit)
1797

    
1798

    
1799
    ### Other
1800

    
1801
    def count_pending_memberships(self):
1802
        memb_set = self.projectmembership_set
1803
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1804
        return memb_count
1805

    
1806
    @property
1807
    def approved_memberships(self):
1808
        query = ProjectMembership.Q_ACCEPTED_STATES
1809
        return self.projectmembership_set.filter(query)
1810

    
1811
    @property
1812
    def approved_members(self):
1813
        return [m.person for m in self.approved_memberships]
1814

    
1815
    def add_member(self, user):
1816
        """
1817
        Raises:
1818
            django.exceptions.PermissionDenied
1819
            astakos.im.models.AstakosUser.DoesNotExist
1820
        """
1821
        if isinstance(user, int):
1822
            user = AstakosUser.objects.get(user=user)
1823

    
1824
        m, created = ProjectMembership.objects.get_or_create(
1825
            person=user, project=self
1826
        )
1827
        m.accept()
1828

    
1829
    def remove_member(self, user):
1830
        """
1831
        Raises:
1832
            django.exceptions.PermissionDenied
1833
            astakos.im.models.AstakosUser.DoesNotExist
1834
            astakos.im.models.ProjectMembership.DoesNotExist
1835
        """
1836
        if isinstance(user, int):
1837
            user = AstakosUser.objects.get(user=user)
1838

    
1839
        m = ProjectMembership.objects.get(person=user, project=self)
1840
        m.remove()
1841

    
1842

    
1843
class PendingMembershipError(Exception):
1844
    pass
1845

    
1846

    
1847
class ProjectMembershipManager(ForUpdateManager):
1848
    pass
1849

    
1850
class ProjectMembership(models.Model):
1851

    
1852
    person              =   models.ForeignKey(AstakosUser)
1853
    request_date        =   models.DateField(auto_now_add=True)
1854
    project             =   models.ForeignKey(Project)
1855

    
1856
    REQUESTED           =   0
1857
    ACCEPTED            =   1
1858
    # User deactivation
1859
    USER_SUSPENDED      =   10
1860
    # Project deactivation
1861
    PROJECT_DEACTIVATED =   100
1862

    
1863
    REMOVED             =   200
1864

    
1865
    ASSOCIATED_STATES   =   set([REQUESTED,
1866
                                 ACCEPTED,
1867
                                 USER_SUSPENDED,
1868
                                 PROJECT_DEACTIVATED])
1869

    
1870
    ACCEPTED_STATES     =   set([ACCEPTED,
1871
                                 USER_SUSPENDED,
1872
                                 PROJECT_DEACTIVATED])
1873

    
1874
    state               =   models.IntegerField(default=REQUESTED,
1875
                                                db_index=True)
1876
    is_pending          =   models.BooleanField(default=False, db_index=True)
1877
    is_active           =   models.BooleanField(default=False, db_index=True)
1878
    application         =   models.ForeignKey(
1879
                                ProjectApplication,
1880
                                null=True,
1881
                                related_name='memberships')
1882
    pending_application =   models.ForeignKey(
1883
                                ProjectApplication,
1884
                                null=True,
1885
                                related_name='pending_memberships')
1886
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1887

    
1888
    acceptance_date     =   models.DateField(null=True, db_index=True)
1889
    leave_request_date  =   models.DateField(null=True)
1890

    
1891
    objects     =   ProjectMembershipManager()
1892

    
1893
    # Compiled queries
1894
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1895

    
1896
    def get_combined_state(self):
1897
        return self.state, self.is_active, self.is_pending
1898

    
1899
    class Meta:
1900
        unique_together = ("person", "project")
1901
        #index_together = [["project", "state"]]
1902

    
1903
    def __str__(self):
1904
        return _("<'%s' membership in '%s'>") % (
1905
                self.person.username, self.project)
1906

    
1907
    __repr__ = __str__
1908

    
1909
    def __init__(self, *args, **kwargs):
1910
        self.state = self.REQUESTED
1911
        super(ProjectMembership, self).__init__(*args, **kwargs)
1912

    
1913
    def _set_history_item(self, reason, date=None):
1914
        if isinstance(reason, basestring):
1915
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1916

    
1917
        history_item = ProjectMembershipHistory(
1918
                            serial=self.id,
1919
                            person=self.person_id,
1920
                            project=self.project_id,
1921
                            date=date or datetime.now(),
1922
                            reason=reason)
1923
        history_item.save()
1924
        serial = history_item.id
1925

    
1926
    def can_accept(self):
1927
        return self.state == self.REQUESTED
1928

    
1929
    def accept(self):
1930
        if self.is_pending:
1931
            m = _("%s: attempt to accept while is pending") % (self,)
1932
            raise AssertionError(m)
1933

    
1934
        if not self.can_accept():
1935
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
1936
            raise AssertionError(m)
1937

    
1938
        now = datetime.now()
1939
        self.acceptance_date = now
1940
        self._set_history_item(reason='ACCEPT', date=now)
1941
        if self.project.is_approved():
1942
            self.state = self.ACCEPTED
1943
            self.is_pending = True
1944
        else:
1945
            self.state = self.PROJECT_DEACTIVATED
1946

    
1947
        self.save()
1948

    
1949
    def can_leave(self):
1950
        return self.can_remove()
1951

    
1952
    def can_remove(self):
1953
        return self.state in self.ACCEPTED_STATES
1954

    
1955
    def remove(self):
1956
        if self.is_pending:
1957
            m = _("%s: attempt to remove while is pending") % (self,)
1958
            raise AssertionError(m)
1959

    
1960
        if not self.can_remove():
1961
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
1962
            raise AssertionError(m)
1963

    
1964
        self._set_history_item(reason='REMOVE')
1965
        self.state = self.REMOVED
1966
        self.is_pending = True
1967
        self.save()
1968

    
1969
    def can_reject(self):
1970
        return self.state == self.REQUESTED
1971

    
1972
    def reject(self):
1973
        if self.is_pending:
1974
            m = _("%s: attempt to reject while is pending") % (self,)
1975
            raise AssertionError(m)
1976

    
1977
        if not self.can_reject():
1978
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
1979
            raise AssertionError(m)
1980

    
1981
        # rejected requests don't need sync,
1982
        # because they were never effected
1983
        self._set_history_item(reason='REJECT')
1984
        self.delete()
1985

    
1986
    def can_cancel(self):
1987
        return self.state == self.REQUESTED
1988

    
1989
    def cancel(self):
1990
        if self.is_pending:
1991
            m = _("%s: attempt to cancel while is pending") % (self,)
1992
            raise AssertionError(m)
1993

    
1994
        if not self.can_cancel():
1995
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
1996
            raise AssertionError(m)
1997

    
1998
        # rejected requests don't need sync,
1999
        # because they were never effected
2000
        self._set_history_item(reason='CANCEL')
2001
        self.delete()
2002

    
2003
    def get_diff_quotas(self, sub_list=None, add_list=None):
2004
        if sub_list is None:
2005
            sub_list = []
2006

    
2007
        if add_list is None:
2008
            add_list = []
2009

    
2010
        sub_append = sub_list.append
2011
        add_append = add_list.append
2012
        holder = self.person.uuid
2013

    
2014
        synced_application = self.application
2015
        if synced_application is not None:
2016
            cur_grants = synced_application.projectresourcegrant_set.all()
2017
            for grant in cur_grants:
2018
                sub_append(QuotaLimits(
2019
                               holder       = holder,
2020
                               resource     = str(grant.resource),
2021
                               capacity     = grant.member_capacity,
2022
                               import_limit = grant.member_import_limit,
2023
                               export_limit = grant.member_export_limit))
2024

    
2025
        pending_application = self.pending_application
2026
        if pending_application is not None:
2027
            new_grants = pending_application.projectresourcegrant_set.all()
2028
            for new_grant in new_grants:
2029
                add_append(QuotaLimits(
2030
                               holder       = holder,
2031
                               resource     = str(new_grant.resource),
2032
                               capacity     = new_grant.member_capacity,
2033
                               import_limit = new_grant.member_import_limit,
2034
                               export_limit = new_grant.member_export_limit))
2035

    
2036
        return (sub_list, add_list)
2037

    
2038
    def set_sync(self):
2039
        if not self.is_pending:
2040
            m = _("%s: attempt to sync a non pending membership") % (self,)
2041
            raise AssertionError(m)
2042

    
2043
        state = self.state
2044
        if state == self.ACCEPTED:
2045
            pending_application = self.pending_application
2046
            if pending_application is None:
2047
                m = _("%s: attempt to sync an empty pending application") % (
2048
                    self,)
2049
                raise AssertionError(m)
2050

    
2051
            self.application = pending_application
2052
            self.is_active = True
2053

    
2054
            self.pending_application = None
2055
            self.pending_serial = None
2056

    
2057
            # project.application may have changed in the meantime,
2058
            # in which case we stay PENDING;
2059
            # we are safe to check due to select_for_update
2060
            if self.application == self.project.application:
2061
                self.is_pending = False
2062
            self.save()
2063

    
2064
        elif state == self.PROJECT_DEACTIVATED:
2065
            if self.pending_application:
2066
                m = _("%s: attempt to sync in state '%s' "
2067
                      "with a pending application") % (self, state)
2068
                raise AssertionError(m)
2069

    
2070
            self.application = None
2071
            self.is_active = False
2072
            self.pending_serial = None
2073
            self.is_pending = False
2074
            self.save()
2075

    
2076
        elif state == self.REMOVED:
2077
            self.delete()
2078

    
2079
        else:
2080
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2081
            raise AssertionError(m)
2082

    
2083
    def reset_sync(self):
2084
        if not self.is_pending:
2085
            m = _("%s: attempt to reset a non pending membership") % (self,)
2086
            raise AssertionError(m)
2087

    
2088
        state = self.state
2089
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
2090
            self.pending_application = None
2091
            self.pending_serial = None
2092
            self.save()
2093
        else:
2094
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2095
            raise AssertionError(m)
2096

    
2097
class Serial(models.Model):
2098
    serial  =   models.AutoField(primary_key=True)
2099

    
2100
def new_serial():
2101
    s = Serial.objects.create()
2102
    serial = s.serial
2103
    s.delete()
2104
    return serial
2105

    
2106
def sync_finish_serials(serials_to_ack=None):
2107
    if serials_to_ack is None:
2108
        serials_to_ack = qh_query_serials([])
2109

    
2110
    serials_to_ack = set(serials_to_ack)
2111
    sfu = ProjectMembership.objects.select_for_update()
2112
    memberships = list(sfu.filter(pending_serial__isnull=False))
2113

    
2114
    if memberships:
2115
        for membership in memberships:
2116
            serial = membership.pending_serial
2117
            if serial in serials_to_ack:
2118
                membership.set_sync()
2119
            else:
2120
                membership.reset_sync()
2121

    
2122
        transaction.commit()
2123

    
2124
    qh_ack_serials(list(serials_to_ack))
2125
    return len(memberships)
2126

    
2127
def pre_sync_projects(sync=True):
2128
    ACCEPTED = ProjectMembership.ACCEPTED
2129
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2130
    psfu = Project.objects.select_for_update()
2131

    
2132
    modified = list(psfu.modified_projects())
2133
    if sync:
2134
        for project in modified:
2135
            objects = project.projectmembership_set.select_for_update()
2136

    
2137
            memberships = objects.filter(state=ACCEPTED)
2138
            for membership in memberships:
2139
                membership.is_pending = True
2140
                membership.save()
2141

    
2142
    reactivating = list(psfu.reactivating_projects())
2143
    if sync:
2144
        for project in reactivating:
2145
            objects = project.projectmembership_set.select_for_update()
2146

    
2147
            memberships = objects.filter(state=PROJECT_DEACTIVATED)
2148
            for membership in memberships:
2149
                membership.is_pending = True
2150
                membership.state = ACCEPTED
2151
                membership.save()
2152

    
2153
    deactivating = list(psfu.deactivating_projects())
2154
    if sync:
2155
        for project in deactivating:
2156
            objects = project.projectmembership_set.select_for_update()
2157

    
2158
            # Note: we keep a user-level deactivation
2159
            # (e.g. USER_SUSPENDED) intact
2160
            memberships = objects.filter(state=ACCEPTED)
2161
            for membership in memberships:
2162
                membership.is_pending = True
2163
                membership.state = PROJECT_DEACTIVATED
2164
                membership.save()
2165

    
2166
    return (modified, reactivating, deactivating)
2167

    
2168
def do_sync_projects():
2169

    
2170
    ACCEPTED = ProjectMembership.ACCEPTED
2171
    objects = ProjectMembership.objects.select_for_update()
2172

    
2173
    sub_quota, add_quota = [], []
2174

    
2175
    serial = new_serial()
2176

    
2177
    pending = objects.filter(is_pending=True)
2178
    for membership in pending:
2179

    
2180
        if membership.pending_application:
2181
            m = "%s: impossible: pending_application is not None (%s)" % (
2182
                membership, membership.pending_application)
2183
            raise AssertionError(m)
2184
        if membership.pending_serial:
2185
            m = "%s: impossible: pending_serial is not None (%s)" % (
2186
                membership, membership.pending_serial)
2187
            raise AssertionError(m)
2188

    
2189
        if membership.state == ACCEPTED:
2190
            membership.pending_application = membership.project.application
2191

    
2192
        membership.pending_serial = serial
2193
        membership.get_diff_quotas(sub_quota, add_quota)
2194
        membership.save()
2195

    
2196
    transaction.commit()
2197
    # ProjectApplication.approve() unblocks here
2198
    # and can set PENDING an already PENDING membership
2199
    # which has been scheduled to sync with the old project.application
2200
    # Need to check in ProjectMembership.set_sync()
2201

    
2202
    r = qh_add_quota(serial, sub_quota, add_quota)
2203
    if r:
2204
        m = "cannot sync serial: %d" % serial
2205
        raise RuntimeError(m)
2206

    
2207
    return serial
2208

    
2209
def post_sync_projects():
2210
    ACCEPTED = ProjectMembership.ACCEPTED
2211
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2212
    psfu = Project.objects.select_for_update()
2213

    
2214
    modified = psfu.modified_projects()
2215
    for project in modified:
2216
        objects = project.projectmembership_set.select_for_update()
2217

    
2218
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2219
        if not memberships:
2220
            project.is_modified = False
2221
            project.save()
2222

    
2223
    reactivating = psfu.reactivating_projects()
2224
    for project in reactivating:
2225
        objects = project.projectmembership_set.select_for_update()
2226
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2227
                                          Q(is_pending=True)))
2228
        if not memberships:
2229
            project.reactivate()
2230
            project.save()
2231

    
2232
    deactivating = psfu.deactivating_projects()
2233
    for project in deactivating:
2234
        objects = project.projectmembership_set.select_for_update()
2235

    
2236
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2237
                                          Q(is_pending=True)))
2238
        if not memberships:
2239
            project.deactivate()
2240
            project.save()
2241

    
2242
    transaction.commit()
2243

    
2244
def _sync_projects(sync):
2245
    sync_finish_serials()
2246
    # Informative only -- no select_for_update()
2247
    pending = list(ProjectMembership.objects.filter(is_pending=True))
2248

    
2249
    projects_log = pre_sync_projects(sync)
2250
    if sync:
2251
        serial = do_sync_projects()
2252
        sync_finish_serials([serial])
2253
        post_sync_projects()
2254

    
2255
    return (pending, projects_log)
2256

    
2257
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2258
    return lock_sync(_sync_projects,
2259
                     args=[sync],
2260
                     retries=retries,
2261
                     retry_wait=retry_wait)
2262

    
2263

    
2264
def all_users_quotas(users):
2265
    quotas = {}
2266
    for user in users:
2267
        quotas[user.uuid] = user.all_quotas()
2268
    return quotas
2269

    
2270
def _sync_users(users, sync):
2271
    sync_finish_serials()
2272

    
2273
    existing, nonexisting = qh_check_users(users)
2274
    resources = get_resource_names()
2275
    registered_quotas = qh_get_quota_limits(existing, resources)
2276
    astakos_quotas = all_users_quotas(users)
2277

    
2278
    if sync:
2279
        r = register_users(nonexisting)
2280
        r = send_quotas(astakos_quotas)
2281

    
2282
    return (existing, nonexisting, registered_quotas, astakos_quotas)
2283

    
2284
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2285
    return lock_sync(_sync_users,
2286
                     args=[users, sync],
2287
                     retries=retries,
2288
                     retry_wait=retry_wait)
2289

    
2290
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2291
    users = AstakosUser.objects.filter(is_active=True)
2292
    return sync_users(users, sync, retries=retries, retry_wait=retry_wait)
2293

    
2294
def lock_sync(func, args=[], kwargs={}, retries=3, retry_wait=1.0):
2295
    transaction.commit()
2296

    
2297
    cursor = connection.cursor()
2298
    locked = True
2299
    try:
2300
        while 1:
2301
            cursor.execute("SELECT pg_try_advisory_lock(1)")
2302
            r = cursor.fetchone()
2303
            if r is None:
2304
                m = "Impossible"
2305
                raise AssertionError(m)
2306
            locked = r[0]
2307
            if locked:
2308
                break
2309

    
2310
            retries -= 1
2311
            if retries <= 0:
2312
                return False
2313
            sleep(retry_wait)
2314

    
2315
        return func(*args, **kwargs)
2316

    
2317
    finally:
2318
        if locked:
2319
            cursor.execute("SELECT pg_advisory_unlock(1)")
2320
            cursor.fetchall()
2321

    
2322

    
2323
class ProjectMembershipHistory(models.Model):
2324
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2325
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2326

    
2327
    person  =   models.BigIntegerField()
2328
    project =   models.BigIntegerField()
2329
    date    =   models.DateField(auto_now_add=True)
2330
    reason  =   models.IntegerField()
2331
    serial  =   models.BigIntegerField()
2332

    
2333
### SIGNALS ###
2334
################
2335

    
2336
def create_astakos_user(u):
2337
    try:
2338
        AstakosUser.objects.get(user_ptr=u.pk)
2339
    except AstakosUser.DoesNotExist:
2340
        extended_user = AstakosUser(user_ptr_id=u.pk)
2341
        extended_user.__dict__.update(u.__dict__)
2342
        extended_user.save()
2343
        if not extended_user.has_auth_provider('local'):
2344
            extended_user.add_auth_provider('local')
2345
    except BaseException, e:
2346
        logger.exception(e)
2347

    
2348

    
2349
def fix_superusers(sender, **kwargs):
2350
    # Associate superusers with AstakosUser
2351
    admins = User.objects.filter(is_superuser=True)
2352
    for u in admins:
2353
        create_astakos_user(u)
2354
post_syncdb.connect(fix_superusers)
2355

    
2356

    
2357
def user_post_save(sender, instance, created, **kwargs):
2358
    if not created:
2359
        return
2360
    create_astakos_user(instance)
2361
post_save.connect(user_post_save, sender=User)
2362

    
2363
def astakosuser_post_save(sender, instance, created, **kwargs):
2364
    pass
2365

    
2366
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2367

    
2368
def resource_post_save(sender, instance, created, **kwargs):
2369
    pass
2370

    
2371
post_save.connect(resource_post_save, sender=Resource)
2372

    
2373
def renew_token(sender, instance, **kwargs):
2374
    if not instance.auth_token:
2375
        instance.renew_token()
2376
pre_save.connect(renew_token, sender=AstakosUser)
2377
pre_save.connect(renew_token, sender=Service)
2378