Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 649f2d36

History | View | Annotate | Download (73.4 kB)

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

    
34
import hashlib
35
import uuid
36
import logging
37
import json
38

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

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

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

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

    
78
import astakos.im.messages as astakos_messages
79
from .managers import ForUpdateManager
80

    
81
from synnefo.lib.quotaholder.api import QH_PRACTICALLY_INFINITE
82
from synnefo.lib.db.intdecimalfield import intDecimalField
83

    
84
logger = logging.getLogger(__name__)
85

    
86
DEFAULT_CONTENT_TYPE = None
87
_content_type = None
88

    
89
def get_content_type():
90
    global _content_type
91
    if _content_type is not None:
92
        return _content_type
93

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

    
101
RESOURCE_SEPARATOR = '.'
102

    
103
inf = float('inf')
104

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

    
116
    class Meta:
117
        ordering = ('order', )
118

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

    
125
        self.auth_token = b64encode(md5.digest())
126
        self.auth_token_created = datetime.now()
127
        if expiration_date:
128
            self.auth_token_expires = expiration_date
129
        else:
130
            self.auth_token_expires = None
131

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

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

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

    
144

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

    
149
_presentation_data = {}
150
def get_presentation(resource):
151
    global _presentation_data
152
    presentation = _presentation_data.get(resource, {})
153
    if not presentation:
154
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
155
        presentation = resource_presentation.get(resource, {})
156
        _presentation_data[resource] = presentation
157
    return presentation
158

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

    
168
    class Meta:
169
        unique_together = ("service", "name")
170

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

    
174
    def full_name(self):
175
        return str(self)
176

    
177
    @property
178
    def help_text(self):
179
        return get_presentation(str(self)).get('help_text', '')
180

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

    
185
    @property
186
    def is_abbreviation(self):
187
        return get_presentation(str(self)).get('is_abbreviation', False)
188

    
189
    @property
190
    def report_desc(self):
191
        return get_presentation(str(self)).get('report_desc', '')
192

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

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

    
201

    
202
def load_service_resources():
203
    ss = []
204
    rs = []
205
    for service_name, data in SERVICES.iteritems():
206
        url = data.get('url')
207
        resources = data.get('resources') or ()
208
        service, created = Service.objects.get_or_create(
209
            name=service_name,
210
            defaults={'url': url}
211
        )
212
        ss.append(service)
213

    
214
        for resource in resources:
215
            try:
216
                resource_name = resource.pop('name', '')
217
                r, created = Resource.objects.get_or_create(
218
                    service=service,
219
                    name=resource_name,
220
                    defaults=resource)
221
                rs.append(r)
222

    
223
            except Exception, e:
224
                print "Cannot create resource ", resource_name
225
                continue
226
    register_services(ss)
227
    register_resources(rs)
228

    
229
def _quota_values(capacity):
230
    return QuotaValues(
231
        quantity = 0,
232
        capacity = capacity,
233
        import_limit = QH_PRACTICALLY_INFINITE,
234
        export_limit = QH_PRACTICALLY_INFINITE)
235

    
236
def get_default_quota():
237
    _DEFAULT_QUOTA = {}
238
    resources = Resource.objects.all()
239
    for resource in resources:
240
        capacity = resource.uplimit
241
        limits = _quota_values(capacity)
242
        _DEFAULT_QUOTA[resource.full_name()] = limits
243

    
244
    return _DEFAULT_QUOTA
245

    
246
def get_resource_names():
247
    _RESOURCE_NAMES = []
248
    resources = Resource.objects.all()
249
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
250
    return _RESOURCE_NAMES
251

    
252

    
253
class AstakosUserManager(UserManager):
254

    
255
    def get_auth_provider_user(self, provider, **kwargs):
256
        """
257
        Retrieve AstakosUser instance associated with the specified third party
258
        id.
259
        """
260
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
261
                          kwargs.iteritems()))
262
        return self.get(auth_providers__module=provider, **kwargs)
263

    
264
    def get_by_email(self, email):
265
        return self.get(email=email)
266

    
267
    def get_by_identifier(self, email_or_username, **kwargs):
268
        try:
269
            return self.get(email__iexact=email_or_username, **kwargs)
270
        except AstakosUser.DoesNotExist:
271
            return self.get(username__iexact=email_or_username, **kwargs)
272

    
273
    def user_exists(self, email_or_username, **kwargs):
274
        qemail = Q(email__iexact=email_or_username)
275
        qusername = Q(username__iexact=email_or_username)
276
        qextra = Q(**kwargs)
277
        return self.filter((qemail | qusername) & qextra).exists()
278

    
279
    def verified_user_exists(self, email_or_username):
280
        return self.user_exists(email_or_username, email_verified=True)
281

    
282
    def verified(self):
283
        return self.filter(email_verified=True)
284

    
285
    def verified(self):
286
        return self.filter(email_verified=True)
287

    
288

    
289
class AstakosUser(User):
290
    """
291
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
292
    """
293
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
294
                                   null=True)
295

    
296
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
297
    #                    AstakosUserProvider model.
298
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
299
                                null=True)
300
    # ex. screen_name for twitter, eppn for shibboleth
301
    third_party_identifier = models.CharField(_('Third-party identifier'),
302
                                              max_length=255, null=True,
303
                                              blank=True)
304

    
305

    
306
    #for invitations
307
    user_level = DEFAULT_USER_LEVEL
308
    level = models.IntegerField(_('Inviter level'), default=user_level)
309
    invitations = models.IntegerField(
310
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
311

    
312
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
313
                                  null=True, blank=True)
314
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
315
    auth_token_expires = models.DateTimeField(
316
        _('Token expiration date'), null=True)
317

    
318
    updated = models.DateTimeField(_('Update date'))
319
    is_verified = models.BooleanField(_('Is verified?'), default=False)
320

    
321
    email_verified = models.BooleanField(_('Email verified?'), default=False)
322

    
323
    has_credits = models.BooleanField(_('Has credits?'), default=False)
324
    has_signed_terms = models.BooleanField(
325
        _('I agree with the terms'), default=False)
326
    date_signed_terms = models.DateTimeField(
327
        _('Signed terms date'), null=True, blank=True)
328

    
329
    activation_sent = models.DateTimeField(
330
        _('Activation sent data'), null=True, blank=True)
331

    
332
    policy = models.ManyToManyField(
333
        Resource, null=True, through='AstakosUserQuota')
334

    
335
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
336

    
337
    __has_signed_terms = False
338
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
339
                                           default=False, db_index=True)
340

    
341
    objects = AstakosUserManager()
342

    
343
    def __init__(self, *args, **kwargs):
344
        super(AstakosUser, self).__init__(*args, **kwargs)
345
        self.__has_signed_terms = self.has_signed_terms
346
        if not self.id:
347
            self.is_active = False
348

    
349
    @property
350
    def realname(self):
351
        return '%s %s' % (self.first_name, self.last_name)
352

    
353
    @realname.setter
354
    def realname(self, value):
355
        parts = value.split(' ')
356
        if len(parts) == 2:
357
            self.first_name = parts[0]
358
            self.last_name = parts[1]
359
        else:
360
            self.last_name = parts[0]
361

    
362
    def add_permission(self, pname):
363
        if self.has_perm(pname):
364
            return
365
        p, created = Permission.objects.get_or_create(
366
                                    codename=pname,
367
                                    name=pname.capitalize(),
368
                                    content_type=get_content_type())
369
        self.user_permissions.add(p)
370

    
371
    def remove_permission(self, pname):
372
        if self.has_perm(pname):
373
            return
374
        p = Permission.objects.get(codename=pname,
375
                                   content_type=get_content_type())
376
        self.user_permissions.remove(p)
377

    
378
    @property
379
    def invitation(self):
380
        try:
381
            return Invitation.objects.get(username=self.email)
382
        except Invitation.DoesNotExist:
383
            return None
384

    
385
    def initial_quotas(self):
386
        quotas = dict(get_default_quota())
387
        for user_quota in self.policies:
388
            resource = user_quota.resource.full_name()
389
            quotas[resource] = user_quota.quota_values()
390
        return quotas
391

    
392
    def all_quotas(self):
393
        quotas = self.initial_quotas()
394

    
395
        objects = self.projectmembership_set.select_related()
396
        memberships = objects.filter(is_active=True)
397
        for membership in memberships:
398
            application = membership.application
399

    
400
            grants = application.projectresourcegrant_set.all()
401
            for grant in grants:
402
                resource = grant.resource.full_name()
403
                prev = quotas.get(resource, 0)
404
                new = add_quota_values(prev, grant.member_quota_values())
405
                quotas[resource] = new
406
        return quotas
407

    
408
    @property
409
    def policies(self):
410
        return self.astakosuserquota_set.select_related().all()
411

    
412
    @policies.setter
413
    def policies(self, policies):
414
        for p in policies:
415
            p.setdefault('resource', '')
416
            p.setdefault('capacity', 0)
417
            p.setdefault('quantity', 0)
418
            p.setdefault('import_limit', 0)
419
            p.setdefault('export_limit', 0)
420
            p.setdefault('update', True)
421
            self.add_resource_policy(**p)
422

    
423
    def add_resource_policy(
424
            self, resource, capacity, quantity, import_limit,
425
            export_limit, update=True):
426
        """Raises ObjectDoesNotExist, IntegrityError"""
427
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
428
        resource = Resource.objects.get(service__name=s, name=r)
429
        if update:
430
            AstakosUserQuota.objects.update_or_create(
431
                user=self, resource=resource, defaults={
432
                    'capacity':capacity,
433
                    'quantity': quantity,
434
                    'import_limit':import_limit,
435
                    'export_limit':export_limit})
436
        else:
437
            q = self.astakosuserquota_set
438
            q.create(
439
                resource=resource, capacity=capacity, quanity=quantity,
440
                import_limit=import_limit, export_limit=export_limit)
441

    
442
    def remove_resource_policy(self, service, resource):
443
        """Raises ObjectDoesNotExist, IntegrityError"""
444
        resource = Resource.objects.get(service__name=service, name=resource)
445
        q = self.policies.get(resource=resource).delete()
446

    
447
    def update_uuid(self):
448
        while not self.uuid:
449
            uuid_val =  str(uuid.uuid4())
450
            try:
451
                AstakosUser.objects.get(uuid=uuid_val)
452
            except AstakosUser.DoesNotExist, e:
453
                self.uuid = uuid_val
454
        return self.uuid
455

    
456
    def save(self, update_timestamps=True, **kwargs):
457
        if update_timestamps:
458
            if not self.id:
459
                self.date_joined = datetime.now()
460
            self.updated = datetime.now()
461

    
462
        # update date_signed_terms if necessary
463
        if self.__has_signed_terms != self.has_signed_terms:
464
            self.date_signed_terms = datetime.now()
465

    
466
        self.update_uuid()
467

    
468
        if self.username != self.email.lower():
469
            # set username
470
            self.username = self.email.lower()
471

    
472
        super(AstakosUser, self).save(**kwargs)
473

    
474
    def renew_token(self, flush_sessions=False, current_key=None):
475
        md5 = hashlib.md5()
476
        md5.update(settings.SECRET_KEY)
477
        md5.update(self.username)
478
        md5.update(self.realname.encode('ascii', 'ignore'))
479
        md5.update(asctime())
480

    
481
        self.auth_token = b64encode(md5.digest())
482
        self.auth_token_created = datetime.now()
483
        self.auth_token_expires = self.auth_token_created + \
484
                                  timedelta(hours=AUTH_TOKEN_DURATION)
485
        if flush_sessions:
486
            self.flush_sessions(current_key)
487
        msg = 'Token renewed for %s' % self.email
488
        logger.log(LOGGING_LEVEL, msg)
489

    
490
    def flush_sessions(self, current_key=None):
491
        q = self.sessions
492
        if current_key:
493
            q = q.exclude(session_key=current_key)
494

    
495
        keys = q.values_list('session_key', flat=True)
496
        if keys:
497
            msg = 'Flushing sessions: %s' % ','.join(keys)
498
            logger.log(LOGGING_LEVEL, msg, [])
499
        engine = import_module(settings.SESSION_ENGINE)
500
        for k in keys:
501
            s = engine.SessionStore(k)
502
            s.flush()
503

    
504
    def __unicode__(self):
505
        return '%s (%s)' % (self.realname, self.email)
506

    
507
    def conflicting_email(self):
508
        q = AstakosUser.objects.exclude(username=self.username)
509
        q = q.filter(email__iexact=self.email)
510
        if q.count() != 0:
511
            return True
512
        return False
513

    
514
    def email_change_is_pending(self):
515
        return self.emailchanges.count() > 0
516

    
517
    def email_change_is_pending(self):
518
        return self.emailchanges.count() > 0
519

    
520
    @property
521
    def signed_terms(self):
522
        term = get_latest_terms()
523
        if not term:
524
            return True
525
        if not self.has_signed_terms:
526
            return False
527
        if not self.date_signed_terms:
528
            return False
529
        if self.date_signed_terms < term.date:
530
            self.has_signed_terms = False
531
            self.date_signed_terms = None
532
            self.save()
533
            return False
534
        return True
535

    
536
    def set_invitations_level(self):
537
        """
538
        Update user invitation level
539
        """
540
        level = self.invitation.inviter.level + 1
541
        self.level = level
542
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
543

    
544
    def can_login_with_auth_provider(self, provider):
545
        if not self.has_auth_provider(provider):
546
            return False
547
        else:
548
            return auth_providers.get_provider(provider).is_available_for_login()
549

    
550
    def can_add_auth_provider(self, provider, **kwargs):
551
        provider_settings = auth_providers.get_provider(provider)
552

    
553
        if not provider_settings.is_available_for_add():
554
            return False
555

    
556
        if self.has_auth_provider(provider) and \
557
           provider_settings.one_per_user:
558
            return False
559

    
560
        if 'provider_info' in kwargs:
561
            kwargs.pop('provider_info')
562

    
563
        if 'identifier' in kwargs:
564
            try:
565
                # provider with specified params already exist
566
                kwargs['user__email_verified'] = True
567
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
568
                                                                   **kwargs)
569
            except AstakosUser.DoesNotExist:
570
                return True
571
            else:
572
                return False
573

    
574
        return True
575

    
576
    def can_remove_auth_provider(self, module):
577
        provider = auth_providers.get_provider(module)
578
        existing = self.get_active_auth_providers()
579
        existing_for_provider = self.get_active_auth_providers(module=module)
580

    
581
        if len(existing) <= 1:
582
            return False
583

    
584
        if len(existing_for_provider) == 1 and provider.is_required():
585
            return False
586

    
587
        return True
588

    
589
    def can_change_password(self):
590
        return self.has_auth_provider('local', auth_backend='astakos')
591

    
592
    def can_change_email(self):
593
        non_astakos_local = self.get_auth_providers().filter(module='local')
594
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
595
        return non_astakos_local.count() == 0
596

    
597
    def has_required_auth_providers(self):
598
        required = auth_providers.REQUIRED_PROVIDERS
599
        for provider in required:
600
            if not self.has_auth_provider(provider):
601
                return False
602
        return True
603

    
604
    def has_auth_provider(self, provider, **kwargs):
605
        return bool(self.auth_providers.filter(module=provider,
606
                                               **kwargs).count())
607

    
608
    def add_auth_provider(self, provider, **kwargs):
609
        info_data = ''
610
        if 'provider_info' in kwargs:
611
            info_data = kwargs.pop('provider_info')
612
            if isinstance(info_data, dict):
613
                info_data = json.dumps(info_data)
614

    
615
        if self.can_add_auth_provider(provider, **kwargs):
616
            AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
617
                                                                **kwargs)
618
            self.auth_providers.create(module=provider, active=True,
619
                                       info_data=info_data,
620
                                       **kwargs)
621
        else:
622
            raise Exception('Cannot add provider')
623

    
624
    def add_pending_auth_provider(self, pending):
625
        """
626
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
627
        the current user.
628
        """
629
        if not isinstance(pending, PendingThirdPartyUser):
630
            pending = PendingThirdPartyUser.objects.get(token=pending)
631

    
632
        provider = self.add_auth_provider(pending.provider,
633
                               identifier=pending.third_party_identifier,
634
                                affiliation=pending.affiliation,
635
                                          provider_info=pending.info)
636

    
637
        if email_re.match(pending.email or '') and pending.email != self.email:
638
            self.additionalmail_set.get_or_create(email=pending.email)
639

    
640
        pending.delete()
641
        return provider
642

    
643
    def remove_auth_provider(self, provider, **kwargs):
644
        self.auth_providers.get(module=provider, **kwargs).delete()
645

    
646
    # user urls
647
    def get_resend_activation_url(self):
648
        return reverse('send_activation', kwargs={'user_id': self.pk})
649

    
650
    def get_provider_remove_url(self, module, **kwargs):
651
        return reverse('remove_auth_provider', kwargs={
652
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
653

    
654
    def get_activation_url(self, nxt=False):
655
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
656
                                 quote(self.auth_token))
657
        if nxt:
658
            url += "&next=%s" % quote(nxt)
659
        return url
660

    
661
    def get_password_reset_url(self, token_generator=default_token_generator):
662
        return reverse('django.contrib.auth.views.password_reset_confirm',
663
                          kwargs={'uidb36':int_to_base36(self.id),
664
                                  'token':token_generator.make_token(self)})
665

    
666
    def get_auth_providers(self):
667
        return self.auth_providers.all()
668

    
669
    def get_available_auth_providers(self):
670
        """
671
        Returns a list of providers available for user to connect to.
672
        """
673
        providers = []
674
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
675
            if self.can_add_auth_provider(module):
676
                providers.append(provider_settings(self))
677

    
678
        return providers
679

    
680
    def get_active_auth_providers(self, **filters):
681
        providers = []
682
        for provider in self.auth_providers.active(**filters):
683
            if auth_providers.get_provider(provider.module).is_available_for_login():
684
                providers.append(provider)
685
        return providers
686

    
687
    @property
688
    def auth_providers_display(self):
689
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
690

    
691
    def get_inactive_message(self):
692
        msg_extra = ''
693
        message = ''
694
        if self.activation_sent:
695
            if self.email_verified:
696
                message = _(astakos_messages.ACCOUNT_INACTIVE)
697
            else:
698
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
699
                if astakos_settings.MODERATION_ENABLED:
700
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
701
                else:
702
                    url = self.get_resend_activation_url()
703
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
704
                                u' ' + \
705
                                _('<a href="%s">%s?</a>') % (url,
706
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
707
        else:
708
            if astakos_settings.MODERATION_ENABLED:
709
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
710
            else:
711
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
712
                url = self.get_resend_activation_url()
713
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
714
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
715

    
716
        return mark_safe(message + u' '+ msg_extra)
717

    
718
    def owns_project(self, project):
719
        return project.owner == self
720

    
721
    def is_project_member(self, project_or_application):
722
        return self.get_status_in_project(project_or_application) in \
723
                                        ProjectMembership.ASSOCIATED_STATES
724

    
725
    def is_project_accepted_member(self, project_or_application):
726
        return self.get_status_in_project(project_or_application) in \
727
                                            ProjectMembership.ACCEPTED_STATES
728

    
729
    def get_status_in_project(self, project_or_application):
730
        application = project_or_application
731
        if isinstance(project_or_application, Project):
732
            application = project_or_application.project
733
        return application.user_status(self)
734

    
735

    
736
class AstakosUserAuthProviderManager(models.Manager):
737

    
738
    def active(self, **filters):
739
        return self.filter(active=True, **filters)
740

    
741
    def remove_unverified_providers(self, provider, **filters):
742
        try:
743
            existing = self.filter(module=provider, user__email_verified=False, **filters)
744
            for p in existing:
745
                p.user.delete()
746
        except:
747
            pass
748

    
749

    
750

    
751
class AstakosUserAuthProvider(models.Model):
752
    """
753
    Available user authentication methods.
754
    """
755
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
756
                                   null=True, default=None)
757
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
758
    module = models.CharField(_('Provider'), max_length=255, blank=False,
759
                                default='local')
760
    identifier = models.CharField(_('Third-party identifier'),
761
                                              max_length=255, null=True,
762
                                              blank=True)
763
    active = models.BooleanField(default=True)
764
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
765
                                   default='astakos')
766
    info_data = models.TextField(default="", null=True, blank=True)
767
    created = models.DateTimeField('Creation date', auto_now_add=True)
768

    
769
    objects = AstakosUserAuthProviderManager()
770

    
771
    class Meta:
772
        unique_together = (('identifier', 'module', 'user'), )
773
        ordering = ('module', 'created')
774

    
775
    def __init__(self, *args, **kwargs):
776
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
777
        try:
778
            self.info = json.loads(self.info_data)
779
            if not self.info:
780
                self.info = {}
781
        except Exception, e:
782
            self.info = {}
783

    
784
        for key,value in self.info.iteritems():
785
            setattr(self, 'info_%s' % key, value)
786

    
787

    
788
    @property
789
    def settings(self):
790
        return auth_providers.get_provider(self.module)
791

    
792
    @property
793
    def details_display(self):
794
        try:
795
          return self.settings.get_details_tpl_display % self.__dict__
796
        except:
797
          return ''
798

    
799
    @property
800
    def title_display(self):
801
        title_tpl = self.settings.get_title_display
802
        try:
803
            if self.settings.get_user_title_display:
804
                title_tpl = self.settings.get_user_title_display
805
        except Exception, e:
806
            pass
807
        try:
808
          return title_tpl % self.__dict__
809
        except:
810
          return self.settings.get_title_display % self.__dict__
811

    
812
    def can_remove(self):
813
        return self.user.can_remove_auth_provider(self.module)
814

    
815
    def delete(self, *args, **kwargs):
816
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
817
        if self.module == 'local':
818
            self.user.set_unusable_password()
819
            self.user.save()
820
        return ret
821

    
822
    def __repr__(self):
823
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
824

    
825
    def __unicode__(self):
826
        if self.identifier:
827
            return "%s:%s" % (self.module, self.identifier)
828
        if self.auth_backend:
829
            return "%s:%s" % (self.module, self.auth_backend)
830
        return self.module
831

    
832
    def save(self, *args, **kwargs):
833
        self.info_data = json.dumps(self.info)
834
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
835

    
836

    
837
class ExtendedManager(models.Manager):
838
    def _update_or_create(self, **kwargs):
839
        assert kwargs, \
840
            'update_or_create() must be passed at least one keyword argument'
841
        obj, created = self.get_or_create(**kwargs)
842
        defaults = kwargs.pop('defaults', {})
843
        if created:
844
            return obj, True, False
845
        else:
846
            try:
847
                params = dict(
848
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
849
                params.update(defaults)
850
                for attr, val in params.items():
851
                    if hasattr(obj, attr):
852
                        setattr(obj, attr, val)
853
                sid = transaction.savepoint()
854
                obj.save(force_update=True)
855
                transaction.savepoint_commit(sid)
856
                return obj, False, True
857
            except IntegrityError, e:
858
                transaction.savepoint_rollback(sid)
859
                try:
860
                    return self.get(**kwargs), False, False
861
                except self.model.DoesNotExist:
862
                    raise e
863

    
864
    update_or_create = _update_or_create
865

    
866

    
867
class AstakosUserQuota(models.Model):
868
    objects = ExtendedManager()
869
    capacity = intDecimalField()
870
    quantity = intDecimalField(default=0)
871
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
872
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
873
    resource = models.ForeignKey(Resource)
874
    user = models.ForeignKey(AstakosUser)
875

    
876
    class Meta:
877
        unique_together = ("resource", "user")
878

    
879
    def quota_values(self):
880
        return QuotaValues(
881
            quantity = self.quantity,
882
            capacity = self.capacity,
883
            import_limit = self.import_limit,
884
            export_limit = self.export_limit)
885

    
886

    
887
class ApprovalTerms(models.Model):
888
    """
889
    Model for approval terms
890
    """
891

    
892
    date = models.DateTimeField(
893
        _('Issue date'), db_index=True, auto_now_add=True)
894
    location = models.CharField(_('Terms location'), max_length=255)
895

    
896

    
897
class Invitation(models.Model):
898
    """
899
    Model for registring invitations
900
    """
901
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
902
                                null=True)
903
    realname = models.CharField(_('Real name'), max_length=255)
904
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
905
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
906
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
907
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
908
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
909

    
910
    def __init__(self, *args, **kwargs):
911
        super(Invitation, self).__init__(*args, **kwargs)
912
        if not self.id:
913
            self.code = _generate_invitation_code()
914

    
915
    def consume(self):
916
        self.is_consumed = True
917
        self.consumed = datetime.now()
918
        self.save()
919

    
920
    def __unicode__(self):
921
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
922

    
923

    
924
class EmailChangeManager(models.Manager):
925

    
926
    @transaction.commit_on_success
927
    def change_email(self, activation_key):
928
        """
929
        Validate an activation key and change the corresponding
930
        ``User`` if valid.
931

932
        If the key is valid and has not expired, return the ``User``
933
        after activating.
934

935
        If the key is not valid or has expired, return ``None``.
936

937
        If the key is valid but the ``User`` is already active,
938
        return ``None``.
939

940
        After successful email change the activation record is deleted.
941

942
        Throws ValueError if there is already
943
        """
944
        try:
945
            email_change = self.model.objects.get(
946
                activation_key=activation_key)
947
            if email_change.activation_key_expired():
948
                email_change.delete()
949
                raise EmailChange.DoesNotExist
950
            # is there an active user with this address?
951
            try:
952
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
953
            except AstakosUser.DoesNotExist:
954
                pass
955
            else:
956
                raise ValueError(_('The new email address is reserved.'))
957
            # update user
958
            user = AstakosUser.objects.get(pk=email_change.user_id)
959
            old_email = user.email
960
            user.email = email_change.new_email_address
961
            user.save()
962
            email_change.delete()
963
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
964
                                                          user.email)
965
            logger.log(LOGGING_LEVEL, msg)
966
            return user
967
        except EmailChange.DoesNotExist:
968
            raise ValueError(_('Invalid activation key.'))
969

    
970

    
971
class EmailChange(models.Model):
972
    new_email_address = models.EmailField(
973
        _(u'new e-mail address'),
974
        help_text=_('Your old email address will be used until you verify your new one.'))
975
    user = models.ForeignKey(
976
        AstakosUser, unique=True, related_name='emailchanges')
977
    requested_at = models.DateTimeField(auto_now_add=True)
978
    activation_key = models.CharField(
979
        max_length=40, unique=True, db_index=True)
980

    
981
    objects = EmailChangeManager()
982

    
983
    def get_url(self):
984
        return reverse('email_change_confirm',
985
                      kwargs={'activation_key': self.activation_key})
986

    
987
    def activation_key_expired(self):
988
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
989
        return self.requested_at + expiration_date < datetime.now()
990

    
991

    
992
class AdditionalMail(models.Model):
993
    """
994
    Model for registring invitations
995
    """
996
    owner = models.ForeignKey(AstakosUser)
997
    email = models.EmailField()
998

    
999

    
1000
def _generate_invitation_code():
1001
    while True:
1002
        code = randint(1, 2L ** 63 - 1)
1003
        try:
1004
            Invitation.objects.get(code=code)
1005
            # An invitation with this code already exists, try again
1006
        except Invitation.DoesNotExist:
1007
            return code
1008

    
1009

    
1010
def get_latest_terms():
1011
    try:
1012
        term = ApprovalTerms.objects.order_by('-id')[0]
1013
        return term
1014
    except IndexError:
1015
        pass
1016
    return None
1017

    
1018
class PendingThirdPartyUser(models.Model):
1019
    """
1020
    Model for registring successful third party user authentications
1021
    """
1022
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1023
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1024
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1025
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1026
                                  null=True)
1027
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1028
                                 null=True)
1029
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1030
                                   null=True)
1031
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1032
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1033
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1034
    info = models.TextField(default="", null=True, blank=True)
1035

    
1036
    class Meta:
1037
        unique_together = ("provider", "third_party_identifier")
1038

    
1039
    def get_user_instance(self):
1040
        d = self.__dict__
1041
        d.pop('_state', None)
1042
        d.pop('id', None)
1043
        d.pop('token', None)
1044
        d.pop('created', None)
1045
        d.pop('info', None)
1046
        user = AstakosUser(**d)
1047

    
1048
        return user
1049

    
1050
    @property
1051
    def realname(self):
1052
        return '%s %s' %(self.first_name, self.last_name)
1053

    
1054
    @realname.setter
1055
    def realname(self, value):
1056
        parts = value.split(' ')
1057
        if len(parts) == 2:
1058
            self.first_name = parts[0]
1059
            self.last_name = parts[1]
1060
        else:
1061
            self.last_name = parts[0]
1062

    
1063
    def save(self, **kwargs):
1064
        if not self.id:
1065
            # set username
1066
            while not self.username:
1067
                username =  uuid.uuid4().hex[:30]
1068
                try:
1069
                    AstakosUser.objects.get(username = username)
1070
                except AstakosUser.DoesNotExist, e:
1071
                    self.username = username
1072
        super(PendingThirdPartyUser, self).save(**kwargs)
1073

    
1074
    def generate_token(self):
1075
        self.password = self.third_party_identifier
1076
        self.last_login = datetime.now()
1077
        self.token = default_token_generator.make_token(self)
1078

    
1079
class SessionCatalog(models.Model):
1080
    session_key = models.CharField(_('session key'), max_length=40)
1081
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1082

    
1083

    
1084
### PROJECTS ###
1085
################
1086

    
1087
def synced_model_metaclass(class_name, class_parents, class_attributes):
1088

    
1089
    new_attributes = {}
1090
    sync_attributes = {}
1091

    
1092
    for name, value in class_attributes.iteritems():
1093
        sync, underscore, rest = name.partition('_')
1094
        if sync == 'sync' and underscore == '_':
1095
            sync_attributes[rest] = value
1096
        else:
1097
            new_attributes[name] = value
1098

    
1099
    if 'prefix' not in sync_attributes:
1100
        m = ("you did not specify a 'sync_prefix' attribute "
1101
             "in class '%s'" % (class_name,))
1102
        raise ValueError(m)
1103

    
1104
    prefix = sync_attributes.pop('prefix')
1105
    class_name = sync_attributes.pop('classname', prefix + '_model')
1106

    
1107
    for name, value in sync_attributes.iteritems():
1108
        newname = prefix + '_' + name
1109
        if newname in new_attributes:
1110
            m = ("class '%s' was specified with prefix '%s' "
1111
                 "but it already has an attribute named '%s'"
1112
                 % (class_name, prefix, newname))
1113
            raise ValueError(m)
1114

    
1115
        new_attributes[newname] = value
1116

    
1117
    newclass = type(class_name, class_parents, new_attributes)
1118
    return newclass
1119

    
1120

    
1121
def make_synced(prefix='sync', name='SyncedState'):
1122

    
1123
    the_name = name
1124
    the_prefix = prefix
1125

    
1126
    class SyncedState(models.Model):
1127

    
1128
        sync_classname      = the_name
1129
        sync_prefix         = the_prefix
1130
        __metaclass__       = synced_model_metaclass
1131

    
1132
        sync_new_state      = models.BigIntegerField(null=True)
1133
        sync_synced_state   = models.BigIntegerField(null=True)
1134
        STATUS_SYNCED       = 0
1135
        STATUS_PENDING      = 1
1136
        sync_status         = models.IntegerField(db_index=True)
1137

    
1138
        class Meta:
1139
            abstract = True
1140

    
1141
        class NotSynced(Exception):
1142
            pass
1143

    
1144
        def sync_init_state(self, state):
1145
            self.sync_synced_state = state
1146
            self.sync_new_state = state
1147
            self.sync_status = self.STATUS_SYNCED
1148

    
1149
        def sync_get_status(self):
1150
            return self.sync_status
1151

    
1152
        def sync_set_status(self):
1153
            if self.sync_new_state != self.sync_synced_state:
1154
                self.sync_status = self.STATUS_PENDING
1155
            else:
1156
                self.sync_status = self.STATUS_SYNCED
1157

    
1158
        def sync_set_synced(self):
1159
            self.sync_synced_state = self.sync_new_state
1160
            self.sync_status = self.STATUS_SYNCED
1161

    
1162
        def sync_get_synced_state(self):
1163
            return self.sync_synced_state
1164

    
1165
        def sync_set_new_state(self, new_state):
1166
            self.sync_new_state = new_state
1167
            self.sync_set_status()
1168

    
1169
        def sync_get_new_state(self):
1170
            return self.sync_new_state
1171

    
1172
        def sync_set_synced_state(self, synced_state):
1173
            self.sync_synced_state = synced_state
1174
            self.sync_set_status()
1175

    
1176
        def sync_get_pending_objects(self):
1177
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1178
            return self.objects.filter(**kw)
1179

    
1180
        def sync_get_synced_objects(self):
1181
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1182
            return self.objects.filter(**kw)
1183

    
1184
        def sync_verify_get_synced_state(self):
1185
            status = self.sync_get_status()
1186
            state = self.sync_get_synced_state()
1187
            verified = (status == self.STATUS_SYNCED)
1188
            return state, verified
1189

    
1190
        def sync_is_synced(self):
1191
            state, verified = self.sync_verify_get_synced_state()
1192
            return verified
1193

    
1194
    return SyncedState
1195

    
1196
SyncedState = make_synced(prefix='sync', name='SyncedState')
1197

    
1198

    
1199
class ProjectApplicationManager(ForUpdateManager):
1200

    
1201
    def user_visible_projects(self, *filters, **kw_filters):
1202
        return self.filter(Q(state=ProjectApplication.PENDING)|\
1203
                           Q(state=ProjectApplication.APPROVED))
1204

    
1205
    def user_visible_by_chain(self, *filters, **kw_filters):
1206
        Q_PENDING = Q(state=ProjectApplication.PENDING)
1207
        Q_APPROVED = Q(state=ProjectApplication.APPROVED)
1208
        pending = self.filter(Q_PENDING).values_list('chain')
1209
        approved = self.filter(Q_APPROVED).values_list('chain')
1210
        by_chain = dict(pending.annotate(models.Max('id')))
1211
        by_chain.update(approved.annotate(models.Max('id')))
1212
        return self.filter(id__in=by_chain.values())
1213

    
1214
    def user_accessible_projects(self, user):
1215
        """
1216
        Return projects accessed by specified user.
1217
        """
1218
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1219
                               Q(project__projectmembership__person=user)
1220

    
1221
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1222

    
1223
    def search_by_name(self, *search_strings):
1224
        q = Q()
1225
        for s in search_strings:
1226
            q = q | Q(name__icontains=s)
1227
        return self.filter(q)
1228

    
1229

    
1230
USER_STATUS_DISPLAY = {
1231
      0: _('Join requested'),
1232
      1: _('Accepted member'),
1233
     10: _('Suspended'),
1234
    100: _('Terminated'),
1235
    200: _('Removed'),
1236
     -1: _('Not a member'),
1237
}
1238

    
1239

    
1240
class Chain(models.Model):
1241
    chain  =   models.AutoField(primary_key=True)
1242

    
1243
def new_chain():
1244
    c = Chain.objects.create()
1245
    chain = c.chain
1246
    c.delete()
1247
    return chain
1248

    
1249

    
1250
class ProjectApplication(models.Model):
1251
    applicant               =   models.ForeignKey(
1252
                                    AstakosUser,
1253
                                    related_name='projects_applied',
1254
                                    db_index=True)
1255

    
1256
    PENDING     =    0
1257
    APPROVED    =    1
1258
    REPLACED    =    2
1259
    DENIED      =    3
1260
    DISMISSED   =    4
1261
    CANCELLED   =    5
1262

    
1263
    state                   =   models.IntegerField(default=PENDING)
1264

    
1265
    owner                   =   models.ForeignKey(
1266
                                    AstakosUser,
1267
                                    related_name='projects_owned',
1268
                                    db_index=True)
1269

    
1270
    chain                   =   models.IntegerField()
1271
    precursor_application   =   models.ForeignKey('ProjectApplication',
1272
                                                  null=True,
1273
                                                  blank=True)
1274

    
1275
    name                    =   models.CharField(max_length=80)
1276
    homepage                =   models.URLField(max_length=255, null=True)
1277
    description             =   models.TextField(null=True, blank=True)
1278
    start_date              =   models.DateTimeField(null=True, blank=True)
1279
    end_date                =   models.DateTimeField()
1280
    member_join_policy      =   models.IntegerField()
1281
    member_leave_policy     =   models.IntegerField()
1282
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1283
    resource_grants         =   models.ManyToManyField(
1284
                                    Resource,
1285
                                    null=True,
1286
                                    blank=True,
1287
                                    through='ProjectResourceGrant')
1288
    comments                =   models.TextField(null=True, blank=True)
1289
    issue_date              =   models.DateTimeField(auto_now_add=True)
1290
    response_date           =   models.DateTimeField(null=True, blank=True)
1291

    
1292
    objects                 =   ProjectApplicationManager()
1293

    
1294
    class Meta:
1295
        unique_together = ("chain", "id")
1296

    
1297
    def __unicode__(self):
1298
        return "%s applied by %s" % (self.name, self.applicant)
1299

    
1300
    # TODO: Move to a more suitable place
1301
    PROJECT_STATE_DISPLAY = {
1302
        PENDING  : _('Pending review'),
1303
        APPROVED : _('Active'),
1304
        REPLACED : _('Replaced'),
1305
        DENIED   : _('Denied'),
1306
        DISMISSED: _('Dismissed'),
1307
        CANCELLED: _('Cancelled')
1308
    }
1309

    
1310
    def get_project(self):
1311
        try:
1312
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1313
            return Project
1314
        except Project.DoesNotExist, e:
1315
            return None
1316

    
1317
    def state_display(self):
1318
        return self.PROJECT_STATE_DISPLAY.get(self.state, _('Unknown'))
1319

    
1320
    def add_resource_policy(self, service, resource, uplimit):
1321
        """Raises ObjectDoesNotExist, IntegrityError"""
1322
        q = self.projectresourcegrant_set
1323
        resource = Resource.objects.get(service__name=service, name=resource)
1324
        q.create(resource=resource, member_capacity=uplimit)
1325

    
1326
    def user_status(self, user):
1327
        try:
1328
            project = self.get_project()
1329
            if not project:
1330
                return -1
1331
            membership = project.projectmembership_set
1332
            membership = membership.exclude(state=ProjectMembership.REMOVED)
1333
            membership = membership.get(person=user)
1334
            status = membership.state
1335
        except ProjectMembership.DoesNotExist:
1336
            return -1
1337

    
1338
        return status
1339

    
1340
    def user_status_display(self, user):
1341
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1342

    
1343
    def members_count(self):
1344
        return self.project.approved_memberships.count()
1345

    
1346
    @property
1347
    def grants(self):
1348
        return self.projectresourcegrant_set.values(
1349
            'member_capacity', 'resource__name', 'resource__service__name')
1350

    
1351
    @property
1352
    def resource_policies(self):
1353
        return self.projectresourcegrant_set.all()
1354

    
1355
    @resource_policies.setter
1356
    def resource_policies(self, policies):
1357
        for p in policies:
1358
            service = p.get('service', None)
1359
            resource = p.get('resource', None)
1360
            uplimit = p.get('uplimit', 0)
1361
            self.add_resource_policy(service, resource, uplimit)
1362

    
1363
    @property
1364
    def follower(self):
1365
        try:
1366
            return ProjectApplication.objects.get(precursor_application=self)
1367
        except ProjectApplication.DoesNotExist:
1368
            return
1369

    
1370
    def followers(self):
1371
        followers = self.chained_applications()
1372
        followers = followers.exclude(id=self.pk).filter(state=self.PENDING)
1373
        followers = followers.order_by('id')
1374
        return followers
1375

    
1376
    def last_follower(self):
1377
        try:
1378
            return self.followers().order_by('-id')[0]
1379
        except IndexError:
1380
            return None
1381

    
1382
    def is_modification(self):
1383
        parents = self.chained_applications().filter(id__lt=self.id)
1384
        parents = parents.filter(state__in=[self.APPROVED])
1385
        return parents.count() > 0
1386

    
1387
    def chained_applications(self):
1388
        return ProjectApplication.objects.filter(chain=self.chain)
1389

    
1390
    def has_pending_modifications(self):
1391
        return bool(self.last_follower())
1392

    
1393
    def get_project(self):
1394
        try:
1395
            return Project.objects.get(id=self.chain)
1396
        except Project.DoesNotExist:
1397
            return None
1398

    
1399
    def _get_project_for_update(self):
1400
        try:
1401
            objects = Project.objects.select_for_update()
1402
            project = objects.get(id=self.chain)
1403
            return project
1404
        except Project.DoesNotExist:
1405
            return None
1406

    
1407
    def cancel(self):
1408
        if self.state != self.PENDING:
1409
            m = _("cannot cancel: application '%s' in state '%s'") % (
1410
                    self.id, self.state)
1411
            raise AssertionError(m)
1412

    
1413
        self.state = self.CANCELLED
1414
        self.save()
1415

    
1416
    def dismiss(self):
1417
        if self.state != self.DENIED:
1418
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1419
                    self.id, self.state)
1420
            raise AssertionError(m)
1421

    
1422
        self.state = self.DISMISSED
1423
        self.save()
1424

    
1425
    def deny(self):
1426
        if self.state != self.PENDING:
1427
            m = _("cannot deny: application '%s' in state '%s'") % (
1428
                    self.id, self.state)
1429
            raise AssertionError(m)
1430

    
1431
        self.state = self.DENIED
1432
        self.response_date = datetime.now()
1433
        self.save()
1434

    
1435
    def approve(self, approval_user=None):
1436
        """
1437
        If approval_user then during owner membership acceptance
1438
        it is checked whether the request_user is eligible.
1439

1440
        Raises:
1441
            PermissionDenied
1442
        """
1443

    
1444
        if not transaction.is_managed():
1445
            raise AssertionError("NOPE")
1446

    
1447
        new_project_name = self.name
1448
        if self.state != self.PENDING:
1449
            m = _("cannot approve: project '%s' in state '%s'") % (
1450
                    new_project_name, self.state)
1451
            raise PermissionDenied(m) # invalid argument
1452

    
1453
        now = datetime.now()
1454
        project = self._get_project_for_update()
1455

    
1456
        try:
1457
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1458
            conflicting_project = Project.objects.get(q)
1459
            if (conflicting_project != project):
1460
                m = (_("cannot approve: project with name '%s' "
1461
                       "already exists (serial: %s)") % (
1462
                        new_project_name, conflicting_project.id))
1463
                raise PermissionDenied(m) # invalid argument
1464
        except Project.DoesNotExist:
1465
            pass
1466

    
1467
        new_project = False
1468
        if project is None:
1469
            new_project = True
1470
            project = Project(id=self.chain)
1471

    
1472
        project.name = new_project_name
1473
        project.application = self
1474
        project.last_approval_date = now
1475
        if not new_project:
1476
            project.is_modified = True
1477

    
1478
        project.save()
1479

    
1480
        self.state = self.APPROVED
1481
        self.response_date = now
1482
        self.save()
1483

    
1484
def submit_application(**kw):
1485

    
1486
    resource_policies = kw.pop('resource_policies', None)
1487
    application = ProjectApplication(**kw)
1488

    
1489
    precursor = kw['precursor_application']
1490

    
1491
    if precursor is None:
1492
        application.chain = new_chain()
1493
    else:
1494
        application.chain = precursor.chain
1495
        if precursor.state == ProjectApplication.PENDING:
1496
            precursor.state = ProjectApplication.REPLACED
1497
            precursor.save()
1498

    
1499
    application.save()
1500
    application.resource_policies = resource_policies
1501
    return application
1502

    
1503
class ProjectResourceGrant(models.Model):
1504

    
1505
    resource                =   models.ForeignKey(Resource)
1506
    project_application     =   models.ForeignKey(ProjectApplication,
1507
                                                  null=True)
1508
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1509
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1510
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1511
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1512
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1513
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1514

    
1515
    objects = ExtendedManager()
1516

    
1517
    class Meta:
1518
        unique_together = ("resource", "project_application")
1519

    
1520
    def member_quota_values(self):
1521
        return QuotaValues(
1522
            quantity = 0,
1523
            capacity = self.member_capacity,
1524
            import_limit = self.member_import_limit,
1525
            export_limit = self.member_export_limit)
1526

    
1527

    
1528
class ProjectManager(ForUpdateManager):
1529

    
1530
    def _q_terminated(self):
1531
        return Q(state=Project.TERMINATED)
1532
    def _q_suspended(self):
1533
        return Q(state=Project.SUSPENDED)
1534
    def _q_deactivated(self):
1535
        return self._q_terminated() | self._q_suspended()
1536

    
1537
    def terminated_projects(self):
1538
        q = self._q_terminated()
1539
        return self.filter(q)
1540

    
1541
    def not_terminated_projects(self):
1542
        q = ~self._q_terminated()
1543
        return self.filter(q)
1544

    
1545
    def terminating_projects(self):
1546
        q = self._q_terminated() & Q(is_active=True)
1547
        return self.filter(q)
1548

    
1549
    def deactivated_projects(self):
1550
        q = self._q_deactivated()
1551
        return self.filter(q)
1552

    
1553
    def deactivating_projects(self):
1554
        q = self._q_deactivated() & Q(is_active=True)
1555
        return self.filter(q)
1556

    
1557
    def modified_projects(self):
1558
        return self.filter(is_modified=True)
1559

    
1560
    def reactivating_projects(self):
1561
        return self.filter(state=Project.APPROVED, is_active=False)
1562

    
1563
class Project(models.Model):
1564

    
1565
    application                 =   models.OneToOneField(
1566
                                            ProjectApplication,
1567
                                            related_name='project')
1568
    last_approval_date          =   models.DateTimeField(null=True)
1569

    
1570
    members                     =   models.ManyToManyField(
1571
                                            AstakosUser,
1572
                                            through='ProjectMembership')
1573

    
1574
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1575
    deactivation_date           =   models.DateTimeField(null=True)
1576

    
1577
    creation_date               =   models.DateTimeField(auto_now_add=True)
1578
    name                        =   models.CharField(
1579
                                            max_length=80,
1580
                                            db_index=True,
1581
                                            unique=True)
1582

    
1583
    APPROVED    = 1
1584
    SUSPENDED   = 10
1585
    TERMINATED  = 100
1586

    
1587
    is_modified                 =   models.BooleanField(default=False,
1588
                                                        db_index=True)
1589
    is_active                   =   models.BooleanField(default=True,
1590
                                                        db_index=True)
1591
    state                       =   models.IntegerField(default=APPROVED,
1592
                                                        db_index=True)
1593

    
1594
    objects     =   ProjectManager()
1595

    
1596
    def __str__(self):
1597
        return _("<project %s '%s'>") % (self.id, self.application.name)
1598

    
1599
    __repr__ = __str__
1600

    
1601
    def is_deactivated(self, reason=None):
1602
        if reason is not None:
1603
            return self.state == reason
1604

    
1605
        return self.state != self.APPROVED
1606

    
1607
    def is_deactivating(self, reason=None):
1608
        if not self.is_active:
1609
            return False
1610

    
1611
        return self.is_deactivated(reason)
1612

    
1613
    def is_deactivated_strict(self, reason=None):
1614
        if self.is_active:
1615
            return False
1616

    
1617
        return self.is_deactivated(reason)
1618

    
1619
    ### Deactivation calls
1620

    
1621
    def deactivate(self):
1622
        self.deactivation_date = datetime.now()
1623
        self.is_active = False
1624

    
1625
    def reactivate(self):
1626
        self.deactivation_date = None
1627
        self.is_active = True
1628

    
1629
    def terminate(self):
1630
        self.deactivation_reason = 'TERMINATED'
1631
        self.state = self.TERMINATED
1632
        self.save()
1633

    
1634
    def suspend(self):
1635
        self.deactivation_reason = 'SUSPENDED'
1636
        self.state = self.SUSPENDED
1637
        self.save()
1638

    
1639
    def resume(self):
1640
        self.deactivation_reason = None
1641
        self.state = self.APPROVED
1642
        self.save()
1643

    
1644
    ### Logical checks
1645

    
1646
    def is_inconsistent(self):
1647
        now = datetime.now()
1648
        dates = [self.creation_date,
1649
                 self.last_approval_date,
1650
                 self.deactivation_date]
1651
        return any([date > now for date in dates])
1652

    
1653
    def is_active_strict(self):
1654
        return self.is_active and self.state == self.APPROVED
1655

    
1656
    def is_approved(self):
1657
        return self.state == self.APPROVED
1658

    
1659
    @property
1660
    def is_alive(self):
1661
        return self.is_active_strict()
1662

    
1663
    @property
1664
    def is_terminated(self):
1665
        return self.is_deactivated(self.TERMINATED)
1666

    
1667
    @property
1668
    def is_suspended(self):
1669
        return self.is_deactivated(self.SUSPENDED)
1670

    
1671
    def violates_resource_grants(self):
1672
        return False
1673

    
1674
    def violates_members_limit(self, adding=0):
1675
        application = self.application
1676
        limit = application.limit_on_members_number
1677
        if limit is None:
1678
            return False
1679
        return (len(self.approved_members) + adding > limit)
1680

    
1681

    
1682
    ### Other
1683

    
1684
    def count_pending_memberships(self):
1685
        memb_set = self.projectmembership_set
1686
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1687
        return memb_count
1688

    
1689
    @property
1690
    def approved_memberships(self):
1691
        query = ProjectMembership.query_approved()
1692
        return self.projectmembership_set.filter(query)
1693

    
1694
    @property
1695
    def approved_members(self):
1696
        return [m.person for m in self.approved_memberships]
1697

    
1698
    def add_member(self, user):
1699
        """
1700
        Raises:
1701
            django.exceptions.PermissionDenied
1702
            astakos.im.models.AstakosUser.DoesNotExist
1703
        """
1704
        if isinstance(user, int):
1705
            user = AstakosUser.objects.get(user=user)
1706

    
1707
        m, created = ProjectMembership.objects.get_or_create(
1708
            person=user, project=self
1709
        )
1710
        m.accept()
1711

    
1712
    def remove_member(self, user):
1713
        """
1714
        Raises:
1715
            django.exceptions.PermissionDenied
1716
            astakos.im.models.AstakosUser.DoesNotExist
1717
            astakos.im.models.ProjectMembership.DoesNotExist
1718
        """
1719
        if isinstance(user, int):
1720
            user = AstakosUser.objects.get(user=user)
1721

    
1722
        m = ProjectMembership.objects.get(person=user, project=self)
1723
        m.remove()
1724

    
1725

    
1726
class PendingMembershipError(Exception):
1727
    pass
1728

    
1729

    
1730
class ProjectMembershipManager(ForUpdateManager):
1731
    pass
1732

    
1733
class ProjectMembership(models.Model):
1734

    
1735
    person              =   models.ForeignKey(AstakosUser)
1736
    request_date        =   models.DateField(auto_now_add=True)
1737
    project             =   models.ForeignKey(Project)
1738

    
1739
    REQUESTED           =   0
1740
    ACCEPTED            =   1
1741
    # User deactivation
1742
    USER_SUSPENDED      =   10
1743
    # Project deactivation
1744
    PROJECT_DEACTIVATED =   100
1745

    
1746
    REMOVED             =   200
1747

    
1748
    ASSOCIATED_STATES   =   set([REQUESTED,
1749
                                 ACCEPTED,
1750
                                 USER_SUSPENDED,
1751
                                 PROJECT_DEACTIVATED])
1752

    
1753
    ACCEPTED_STATES     =   set([ACCEPTED,
1754
                                 USER_SUSPENDED,
1755
                                 PROJECT_DEACTIVATED])
1756

    
1757
    state               =   models.IntegerField(default=REQUESTED,
1758
                                                db_index=True)
1759
    is_pending          =   models.BooleanField(default=False, db_index=True)
1760
    is_active           =   models.BooleanField(default=False, db_index=True)
1761
    application         =   models.ForeignKey(
1762
                                ProjectApplication,
1763
                                null=True,
1764
                                related_name='memberships')
1765
    pending_application =   models.ForeignKey(
1766
                                ProjectApplication,
1767
                                null=True,
1768
                                related_name='pending_memebrships')
1769
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1770

    
1771
    acceptance_date     =   models.DateField(null=True, db_index=True)
1772
    leave_request_date  =   models.DateField(null=True)
1773

    
1774
    objects     =   ProjectMembershipManager()
1775

    
1776

    
1777
    def get_combined_state(self):
1778
        return self.state, self.is_active, self.is_pending
1779

    
1780
    @classmethod
1781
    def query_approved(cls):
1782
        return (~Q(state=cls.REQUESTED) &
1783
                ~Q(state=cls.REMOVED))
1784

    
1785
    class Meta:
1786
        unique_together = ("person", "project")
1787
        #index_together = [["project", "state"]]
1788

    
1789
    def __str__(self):
1790
        return _("<'%s' membership in '%s'>") % (
1791
                self.person.username, self.project)
1792

    
1793
    __repr__ = __str__
1794

    
1795
    def __init__(self, *args, **kwargs):
1796
        self.state = self.REQUESTED
1797
        super(ProjectMembership, self).__init__(*args, **kwargs)
1798

    
1799
    def _set_history_item(self, reason, date=None):
1800
        if isinstance(reason, basestring):
1801
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1802

    
1803
        history_item = ProjectMembershipHistory(
1804
                            serial=self.id,
1805
                            person=self.person_id,
1806
                            project=self.project_id,
1807
                            date=date or datetime.now(),
1808
                            reason=reason)
1809
        history_item.save()
1810
        serial = history_item.id
1811

    
1812
    def accept(self):
1813
        if self.is_pending:
1814
            m = _("%s: attempt to accept while is pending") % (self,)
1815
            raise AssertionError(m)
1816

    
1817
        state = self.state
1818
        if state != self.REQUESTED:
1819
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1820
            raise AssertionError(m)
1821

    
1822
        now = datetime.now()
1823
        self.acceptance_date = now
1824
        self._set_history_item(reason='ACCEPT', date=now)
1825
        if self.project.is_approved():
1826
            self.state = self.ACCEPTED
1827
            self.is_pending = True
1828
        else:
1829
            self.state = self.PROJECT_DEACTIVATED
1830

    
1831
        self.save()
1832

    
1833
    def remove(self):
1834
        if self.is_pending:
1835
            m = _("%s: attempt to remove while is pending") % (self,)
1836
            raise AssertionError(m)
1837

    
1838
        state = self.state
1839
        if state not in self.ACCEPTED_STATES:
1840
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1841
            raise AssertionError(m)
1842

    
1843
        self._set_history_item(reason='REMOVE')
1844
        self.state = self.REMOVED
1845
        self.is_pending = True
1846
        self.save()
1847

    
1848
    def reject(self):
1849
        if self.is_pending:
1850
            m = _("%s: attempt to reject while is pending") % (self,)
1851
            raise AssertionError(m)
1852

    
1853
        state = self.state
1854
        if state != self.REQUESTED:
1855
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1856
            raise AssertionError(m)
1857

    
1858
        # rejected requests don't need sync,
1859
        # because they were never effected
1860
        self._set_history_item(reason='REJECT')
1861
        self.delete()
1862

    
1863
    def get_diff_quotas(self, sub_list=None, add_list=None):
1864
        if sub_list is None:
1865
            sub_list = []
1866

    
1867
        if add_list is None:
1868
            add_list = []
1869

    
1870
        sub_append = sub_list.append
1871
        add_append = add_list.append
1872
        holder = self.person.uuid
1873

    
1874
        synced_application = self.application
1875
        if synced_application is not None:
1876
            cur_grants = synced_application.projectresourcegrant_set.all()
1877
            for grant in cur_grants:
1878
                sub_append(QuotaLimits(
1879
                               holder       = holder,
1880
                               resource     = str(grant.resource),
1881
                               capacity     = grant.member_capacity,
1882
                               import_limit = grant.member_import_limit,
1883
                               export_limit = grant.member_export_limit))
1884

    
1885
        pending_application = self.pending_application
1886
        if pending_application is not None:
1887
            new_grants = pending_application.projectresourcegrant_set.all()
1888
            for new_grant in new_grants:
1889
                add_append(QuotaLimits(
1890
                               holder       = holder,
1891
                               resource     = str(new_grant.resource),
1892
                               capacity     = new_grant.member_capacity,
1893
                               import_limit = new_grant.member_import_limit,
1894
                               export_limit = new_grant.member_export_limit))
1895

    
1896
        return (sub_list, add_list)
1897

    
1898
    def set_sync(self):
1899
        if not self.is_pending:
1900
            m = _("%s: attempt to sync a non pending membership") % (self,)
1901
            raise AssertionError(m)
1902

    
1903
        state = self.state
1904
        if state == self.ACCEPTED:
1905
            pending_application = self.pending_application
1906
            if pending_application is None:
1907
                m = _("%s: attempt to sync an empty pending application") % (
1908
                    self,)
1909
                raise AssertionError(m)
1910

    
1911
            self.application = pending_application
1912
            self.is_active = True
1913

    
1914
            self.pending_application = None
1915
            self.pending_serial = None
1916

    
1917
            # project.application may have changed in the meantime,
1918
            # in which case we stay PENDING;
1919
            # we are safe to check due to select_for_update
1920
            if self.application == self.project.application:
1921
                self.is_pending = False
1922
            self.save()
1923

    
1924
        elif state == self.PROJECT_DEACTIVATED:
1925
            if self.pending_application:
1926
                m = _("%s: attempt to sync in state '%s' "
1927
                      "with a pending application") % (self, state)
1928
                raise AssertionError(m)
1929

    
1930
            self.application = None
1931
            self.pending_serial = None
1932
            self.is_pending = False
1933
            self.save()
1934

    
1935
        elif state == self.REMOVED:
1936
            self.delete()
1937

    
1938
        else:
1939
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1940
            raise AssertionError(m)
1941

    
1942
    def reset_sync(self):
1943
        if not self.is_pending:
1944
            m = _("%s: attempt to reset a non pending membership") % (self,)
1945
            raise AssertionError(m)
1946

    
1947
        state = self.state
1948
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
1949
            self.pending_application = None
1950
            self.pending_serial = None
1951
            self.save()
1952
        else:
1953
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1954
            raise AssertionError(m)
1955

    
1956
class Serial(models.Model):
1957
    serial  =   models.AutoField(primary_key=True)
1958

    
1959
def new_serial():
1960
    s = Serial.objects.create()
1961
    serial = s.serial
1962
    s.delete()
1963
    return serial
1964

    
1965
def sync_finish_serials(serials_to_ack=None):
1966
    if serials_to_ack is None:
1967
        serials_to_ack = qh_query_serials([])
1968

    
1969
    serials_to_ack = set(serials_to_ack)
1970
    sfu = ProjectMembership.objects.select_for_update()
1971
    memberships = list(sfu.filter(pending_serial__isnull=False))
1972

    
1973
    if memberships:
1974
        for membership in memberships:
1975
            serial = membership.pending_serial
1976
            if serial in serials_to_ack:
1977
                membership.set_sync()
1978
            else:
1979
                membership.reset_sync()
1980

    
1981
        transaction.commit()
1982

    
1983
    qh_ack_serials(list(serials_to_ack))
1984
    return len(memberships)
1985

    
1986
def pre_sync():
1987
    ACCEPTED = ProjectMembership.ACCEPTED
1988
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
1989
    psfu = Project.objects.select_for_update()
1990

    
1991
    modified = psfu.modified_projects()
1992
    for project in modified:
1993
        objects = project.projectmembership_set.select_for_update()
1994

    
1995
        memberships = objects.filter(state=ACCEPTED)
1996
        for membership in memberships:
1997
            membership.is_pending = True
1998
            membership.save()
1999

    
2000
    reactivating = psfu.reactivating_projects()
2001
    for project in reactivating:
2002
        objects = project.projectmembership_set.select_for_update()
2003
        memberships = objects.filter(state=PROJECT_DEACTIVATED)
2004
        for membership in memberships:
2005
            membership.is_pending = True
2006
            membership.state = ACCEPTED
2007
            membership.save()
2008

    
2009
    deactivating = psfu.deactivating_projects()
2010
    for project in deactivating:
2011
        objects = project.projectmembership_set.select_for_update()
2012

    
2013
        # Note: we keep a user-level deactivation (e.g. USER_SUSPENDED) intact
2014
        memberships = objects.filter(state=ACCEPTED)
2015
        for membership in memberships:
2016
            membership.is_pending = True
2017
            membership.state = PROJECT_DEACTIVATED
2018
            membership.save()
2019

    
2020
def do_sync():
2021

    
2022
    ACCEPTED = ProjectMembership.ACCEPTED
2023
    objects = ProjectMembership.objects.select_for_update()
2024

    
2025
    sub_quota, add_quota = [], []
2026

    
2027
    serial = new_serial()
2028

    
2029
    pending = objects.filter(is_pending=True)
2030
    for membership in pending:
2031

    
2032
        if membership.pending_application:
2033
            m = "%s: impossible: pending_application is not None (%s)" % (
2034
                membership, membership.pending_application)
2035
            raise AssertionError(m)
2036
        if membership.pending_serial:
2037
            m = "%s: impossible: pending_serial is not None (%s)" % (
2038
                membership, membership.pending_serial)
2039
            raise AssertionError(m)
2040

    
2041
        if membership.state == ACCEPTED:
2042
            membership.pending_application = membership.project.application
2043

    
2044
        membership.pending_serial = serial
2045
        membership.get_diff_quotas(sub_quota, add_quota)
2046
        membership.save()
2047

    
2048
    transaction.commit()
2049
    # ProjectApplication.approve() unblocks here
2050
    # and can set PENDING an already PENDING membership
2051
    # which has been scheduled to sync with the old project.application
2052
    # Need to check in ProjectMembership.set_sync()
2053

    
2054
    r = qh_add_quota(serial, sub_quota, add_quota)
2055
    if r:
2056
        m = "cannot sync serial: %d" % serial
2057
        raise RuntimeError(m)
2058

    
2059
    return serial
2060

    
2061
def post_sync():
2062
    ACCEPTED = ProjectMembership.ACCEPTED
2063
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2064
    psfu = Project.objects.select_for_update()
2065

    
2066
    modified = psfu.modified_projects()
2067
    for project in modified:
2068
        objects = project.projectmembership_set.select_for_update()
2069

    
2070
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2071
        if not memberships:
2072
            project.is_modified = False
2073
            project.save()
2074

    
2075
    reactivating = psfu.reactivating_projects()
2076
    for project in reactivating:
2077
        objects = project.projectmembership_set.select_for_update()
2078
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2079
                                          Q(is_pending=True)))
2080
        if not memberships:
2081
            project.reactivate()
2082
            project.save()
2083

    
2084
    deactivating = psfu.deactivating_projects()
2085
    for project in deactivating:
2086
        objects = project.projectmembership_set.select_for_update()
2087

    
2088
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2089
                                          Q(is_pending=True)))
2090
        if not memberships:
2091
            project.deactivate()
2092
            project.save()
2093

    
2094
    transaction.commit()
2095

    
2096
def sync_projects():
2097
    sync_finish_serials()
2098
    pre_sync()
2099
    serial = do_sync()
2100
    sync_finish_serials([serial])
2101
    post_sync()
2102

    
2103
def trigger_sync(retries=3, retry_wait=1.0):
2104
    transaction.commit()
2105

    
2106
    cursor = connection.cursor()
2107
    locked = True
2108
    try:
2109
        while 1:
2110
            cursor.execute("SELECT pg_try_advisory_lock(1)")
2111
            r = cursor.fetchone()
2112
            if r is None:
2113
                m = "Impossible"
2114
                raise AssertionError(m)
2115
            locked = r[0]
2116
            if locked:
2117
                break
2118

    
2119
            retries -= 1
2120
            if retries <= 0:
2121
                return False
2122
            sleep(retry_wait)
2123

    
2124
        sync_projects()
2125
        return True
2126

    
2127
    finally:
2128
        if locked:
2129
            cursor.execute("SELECT pg_advisory_unlock(1)")
2130
            cursor.fetchall()
2131

    
2132

    
2133
class ProjectMembershipHistory(models.Model):
2134
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2135
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2136

    
2137
    person  =   models.BigIntegerField()
2138
    project =   models.BigIntegerField()
2139
    date    =   models.DateField(auto_now_add=True)
2140
    reason  =   models.IntegerField()
2141
    serial  =   models.BigIntegerField()
2142

    
2143
### SIGNALS ###
2144
################
2145

    
2146
def create_astakos_user(u):
2147
    try:
2148
        AstakosUser.objects.get(user_ptr=u.pk)
2149
    except AstakosUser.DoesNotExist:
2150
        extended_user = AstakosUser(user_ptr_id=u.pk)
2151
        extended_user.__dict__.update(u.__dict__)
2152
        extended_user.save()
2153
        if not extended_user.has_auth_provider('local'):
2154
            extended_user.add_auth_provider('local')
2155
    except BaseException, e:
2156
        logger.exception(e)
2157

    
2158

    
2159
def fix_superusers(sender, **kwargs):
2160
    # Associate superusers with AstakosUser
2161
    admins = User.objects.filter(is_superuser=True)
2162
    for u in admins:
2163
        create_astakos_user(u)
2164
post_syncdb.connect(fix_superusers)
2165

    
2166

    
2167
def user_post_save(sender, instance, created, **kwargs):
2168
    if not created:
2169
        return
2170
    create_astakos_user(instance)
2171
post_save.connect(user_post_save, sender=User)
2172

    
2173
def astakosuser_post_save(sender, instance, created, **kwargs):
2174
    pass
2175

    
2176
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2177

    
2178
def resource_post_save(sender, instance, created, **kwargs):
2179
    pass
2180

    
2181
post_save.connect(resource_post_save, sender=Resource)
2182

    
2183
def renew_token(sender, instance, **kwargs):
2184
    if not instance.auth_token:
2185
        instance.renew_token()
2186
pre_save.connect(renew_token, sender=AstakosUser)
2187
pre_save.connect(renew_token, sender=Service)
2188