Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 7eadc230

History | View | Annotate | Download (73.9 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
            if application is None:
400
                m = _("missing application for active membership %s"
401
                      % (membership,))
402
                raise AssertionError(m)
403

    
404
            grants = application.projectresourcegrant_set.all()
405
            for grant in grants:
406
                resource = grant.resource.full_name()
407
                prev = quotas.get(resource, 0)
408
                new = add_quota_values(prev, grant.member_quota_values())
409
                quotas[resource] = new
410
        return quotas
411

    
412
    @property
413
    def policies(self):
414
        return self.astakosuserquota_set.select_related().all()
415

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

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

    
446
    def remove_resource_policy(self, service, resource):
447
        """Raises ObjectDoesNotExist, IntegrityError"""
448
        resource = Resource.objects.get(service__name=service, name=resource)
449
        q = self.policies.get(resource=resource).delete()
450

    
451
    def update_uuid(self):
452
        while not self.uuid:
453
            uuid_val =  str(uuid.uuid4())
454
            try:
455
                AstakosUser.objects.get(uuid=uuid_val)
456
            except AstakosUser.DoesNotExist, e:
457
                self.uuid = uuid_val
458
        return self.uuid
459

    
460
    def save(self, update_timestamps=True, **kwargs):
461
        if update_timestamps:
462
            if not self.id:
463
                self.date_joined = datetime.now()
464
            self.updated = datetime.now()
465

    
466
        # update date_signed_terms if necessary
467
        if self.__has_signed_terms != self.has_signed_terms:
468
            self.date_signed_terms = datetime.now()
469

    
470
        self.update_uuid()
471

    
472
        if self.username != self.email.lower():
473
            # set username
474
            self.username = self.email.lower()
475

    
476
        super(AstakosUser, self).save(**kwargs)
477

    
478
    def renew_token(self, flush_sessions=False, current_key=None):
479
        md5 = hashlib.md5()
480
        md5.update(settings.SECRET_KEY)
481
        md5.update(self.username)
482
        md5.update(self.realname.encode('ascii', 'ignore'))
483
        md5.update(asctime())
484

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

    
494
    def flush_sessions(self, current_key=None):
495
        q = self.sessions
496
        if current_key:
497
            q = q.exclude(session_key=current_key)
498

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

    
508
    def __unicode__(self):
509
        return '%s (%s)' % (self.realname, self.email)
510

    
511
    def conflicting_email(self):
512
        q = AstakosUser.objects.exclude(username=self.username)
513
        q = q.filter(email__iexact=self.email)
514
        if q.count() != 0:
515
            return True
516
        return False
517

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

    
521
    def email_change_is_pending(self):
522
        return self.emailchanges.count() > 0
523

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

    
540
    def set_invitations_level(self):
541
        """
542
        Update user invitation level
543
        """
544
        level = self.invitation.inviter.level + 1
545
        self.level = level
546
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
547

    
548
    def can_login_with_auth_provider(self, provider):
549
        if not self.has_auth_provider(provider):
550
            return False
551
        else:
552
            return auth_providers.get_provider(provider).is_available_for_login()
553

    
554
    def can_add_auth_provider(self, provider, **kwargs):
555
        provider_settings = auth_providers.get_provider(provider)
556

    
557
        if not provider_settings.is_available_for_add():
558
            return False
559

    
560
        if self.has_auth_provider(provider) and \
561
           provider_settings.one_per_user:
562
            return False
563

    
564
        if 'provider_info' in kwargs:
565
            kwargs.pop('provider_info')
566

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

    
578
        return True
579

    
580
    def can_remove_auth_provider(self, module):
581
        provider = auth_providers.get_provider(module)
582
        existing = self.get_active_auth_providers()
583
        existing_for_provider = self.get_active_auth_providers(module=module)
584

    
585
        if len(existing) <= 1:
586
            return False
587

    
588
        if len(existing_for_provider) == 1 and provider.is_required():
589
            return False
590

    
591
        return True
592

    
593
    def can_change_password(self):
594
        return self.has_auth_provider('local', auth_backend='astakos')
595

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

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

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

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

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

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

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

    
639
        pending.delete()
640
        return provider
641

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

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

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

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

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

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

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

    
677
        return providers
678

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

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

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

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

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

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

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

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

    
734

    
735
class AstakosUserAuthProviderManager(models.Manager):
736

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

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

    
748

    
749

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

    
768
    objects = AstakosUserAuthProviderManager()
769

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

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

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

    
786

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

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

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

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

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

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

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

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

    
835

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

    
863
    update_or_create = _update_or_create
864

    
865

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

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

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

    
885

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

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

    
895

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

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

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

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

    
922

    
923
class EmailChangeManager(models.Manager):
924

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

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

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

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

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

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

    
969

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

    
980
    objects = EmailChangeManager()
981

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

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

    
990

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

    
998

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

    
1008

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

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

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

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

    
1047
        return user
1048

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

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

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

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

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

    
1082

    
1083
### PROJECTS ###
1084
################
1085

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

    
1088
    new_attributes = {}
1089
    sync_attributes = {}
1090

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

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

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

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

    
1114
        new_attributes[newname] = value
1115

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

    
1119

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

    
1122
    the_name = name
1123
    the_prefix = prefix
1124

    
1125
    class SyncedState(models.Model):
1126

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

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

    
1137
        class Meta:
1138
            abstract = True
1139

    
1140
        class NotSynced(Exception):
1141
            pass
1142

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

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

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

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

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

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

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

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

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

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

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

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

    
1193
    return SyncedState
1194

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

    
1197

    
1198
class ProjectApplicationManager(ForUpdateManager):
1199

    
1200
    def user_visible_projects(self, *filters, **kw_filters):
1201
        model = self.model
1202
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1203

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

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

    
1219
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1220

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

    
1227

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

    
1237

    
1238
class Chain(models.Model):
1239
    chain  =   models.AutoField(primary_key=True)
1240

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

    
1247

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

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

    
1261
    state                   =   models.IntegerField(default=PENDING,
1262
                                                    db_index=True)
1263

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

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

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

    
1291
    objects                 =   ProjectApplicationManager()
1292

    
1293
    # Compiled queries
1294
    Q_PENDING  = Q(state=PENDING)
1295
    Q_APPROVED = Q(state=APPROVED)
1296

    
1297
    class Meta:
1298
        unique_together = ("chain", "id")
1299

    
1300
    def __unicode__(self):
1301
        return "%s applied by %s" % (self.name, self.applicant)
1302

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

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

    
1320
    def state_display(self):
1321
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1322

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

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

    
1341
        return status
1342

    
1343
    def user_status_display(self, user):
1344
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1345

    
1346
    def members_count(self):
1347
        return self.project.approved_memberships.count()
1348

    
1349
    @property
1350
    def grants(self):
1351
        return self.projectresourcegrant_set.values(
1352
            'member_capacity', 'resource__name', 'resource__service__name')
1353

    
1354
    @property
1355
    def resource_policies(self):
1356
        return self.projectresourcegrant_set.all()
1357

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

    
1366
    @property
1367
    def follower(self):
1368
        try:
1369
            return ProjectApplication.objects.get(precursor_application=self)
1370
        except ProjectApplication.DoesNotExist:
1371
            return
1372

    
1373
    def followers(self):
1374
        followers = self.chained_applications()
1375
        followers = followers.exclude(id=self.pk).filter(state=self.PENDING)
1376
        followers = followers.order_by('id')
1377
        return followers
1378

    
1379
    def last_follower(self):
1380
        try:
1381
            return self.followers().order_by('-id')[0]
1382
        except IndexError:
1383
            return None
1384

    
1385
    def is_modification(self):
1386
        parents = self.chained_applications().filter(id__lt=self.id)
1387
        parents = parents.filter(state__in=[self.APPROVED])
1388
        return parents.count() > 0
1389

    
1390
    def chained_applications(self):
1391
        return ProjectApplication.objects.filter(chain=self.chain)
1392

    
1393
    def has_pending_modifications(self):
1394
        return bool(self.last_follower())
1395

    
1396
    def get_project(self):
1397
        try:
1398
            return Project.objects.get(id=self.chain)
1399
        except Project.DoesNotExist:
1400
            return None
1401

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

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

    
1416
        self.state = self.CANCELLED
1417
        self.save()
1418

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

    
1425
        self.state = self.DISMISSED
1426
        self.save()
1427

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

    
1434
        self.state = self.DENIED
1435
        self.response_date = datetime.now()
1436
        self.save()
1437

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

1443
        Raises:
1444
            PermissionDenied
1445
        """
1446

    
1447
        if not transaction.is_managed():
1448
            raise AssertionError("NOPE")
1449

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

    
1456
        now = datetime.now()
1457
        project = self._get_project_for_update()
1458

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

    
1470
        new_project = False
1471
        if project is None:
1472
            new_project = True
1473
            project = Project(id=self.chain)
1474

    
1475
        project.name = new_project_name
1476
        project.application = self
1477
        project.last_approval_date = now
1478
        if not new_project:
1479
            project.is_modified = True
1480

    
1481
        project.save()
1482

    
1483
        self.state = self.APPROVED
1484
        self.response_date = now
1485
        self.save()
1486

    
1487
def submit_application(**kw):
1488

    
1489
    resource_policies = kw.pop('resource_policies', None)
1490
    application = ProjectApplication(**kw)
1491

    
1492
    precursor = kw['precursor_application']
1493

    
1494
    if precursor is None:
1495
        application.chain = new_chain()
1496
    else:
1497
        application.chain = precursor.chain
1498
        if precursor.state == ProjectApplication.PENDING:
1499
            precursor.state = ProjectApplication.REPLACED
1500
            precursor.save()
1501

    
1502
    application.save()
1503
    application.resource_policies = resource_policies
1504
    return application
1505

    
1506
class ProjectResourceGrant(models.Model):
1507

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

    
1518
    objects = ExtendedManager()
1519

    
1520
    class Meta:
1521
        unique_together = ("resource", "project_application")
1522

    
1523
    def member_quota_values(self):
1524
        return QuotaValues(
1525
            quantity = 0,
1526
            capacity = self.member_capacity,
1527
            import_limit = self.member_import_limit,
1528
            export_limit = self.member_export_limit)
1529

    
1530

    
1531
class ProjectManager(ForUpdateManager):
1532

    
1533
    def terminated_projects(self):
1534
        q = self.model.Q_TERMINATED
1535
        return self.filter(q)
1536

    
1537
    def not_terminated_projects(self):
1538
        q = ~self.model.Q_TERMINATED
1539
        return self.filter(q)
1540

    
1541
    def terminating_projects(self):
1542
        q = self.model.Q_TERMINATED & Q(is_active=True)
1543
        return self.filter(q)
1544

    
1545
    def deactivated_projects(self):
1546
        q = self.model.Q_DEACTIVATED
1547
        return self.filter(q)
1548

    
1549
    def deactivating_projects(self):
1550
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1551
        return self.filter(q)
1552

    
1553
    def modified_projects(self):
1554
        return self.filter(is_modified=True)
1555

    
1556
    def reactivating_projects(self):
1557
        return self.filter(state=Project.APPROVED, is_active=False)
1558

    
1559
    def expired_projects(self):
1560
        q = (~Q(state=Project.TERMINATED) &
1561
              Q(application__end_date__lt=datetime.now()))
1562
        return self.filter(q)
1563

    
1564

    
1565
class Project(models.Model):
1566

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

    
1572
    members                     =   models.ManyToManyField(
1573
                                            AstakosUser,
1574
                                            through='ProjectMembership')
1575

    
1576
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1577
    deactivation_date           =   models.DateTimeField(null=True)
1578

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

    
1585
    APPROVED    = 1
1586
    SUSPENDED   = 10
1587
    TERMINATED  = 100
1588

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

    
1596
    objects     =   ProjectManager()
1597

    
1598
    # Compiled queries
1599
    Q_TERMINATED  = Q(state=TERMINATED)
1600
    Q_SUSPENDED   = Q(state=SUSPENDED)
1601
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1602

    
1603
    def __str__(self):
1604
        return _("<project %s '%s'>") % (self.id, self.application.name)
1605

    
1606
    __repr__ = __str__
1607

    
1608
    STATE_DISPLAY = {
1609
        APPROVED   : 'APPROVED',
1610
        SUSPENDED  : 'SUSPENDED',
1611
        TERMINATED : 'TERMINATED'
1612
        }
1613

    
1614
    def state_display(self):
1615
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1616

    
1617
    def expiration_info(self):
1618
        return (str(self.id), self.name, self.state_display(),
1619
                str(self.application.end_date))
1620

    
1621
    def is_deactivated(self, reason=None):
1622
        if reason is not None:
1623
            return self.state == reason
1624

    
1625
        return self.state != self.APPROVED
1626

    
1627
    def is_deactivating(self, reason=None):
1628
        if not self.is_active:
1629
            return False
1630

    
1631
        return self.is_deactivated(reason)
1632

    
1633
    def is_deactivated_strict(self, reason=None):
1634
        if self.is_active:
1635
            return False
1636

    
1637
        return self.is_deactivated(reason)
1638

    
1639
    ### Deactivation calls
1640

    
1641
    def deactivate(self):
1642
        self.deactivation_date = datetime.now()
1643
        self.is_active = False
1644

    
1645
    def reactivate(self):
1646
        self.deactivation_date = None
1647
        self.is_active = True
1648

    
1649
    def terminate(self):
1650
        self.deactivation_reason = 'TERMINATED'
1651
        self.state = self.TERMINATED
1652
        self.save()
1653

    
1654
    def suspend(self):
1655
        self.deactivation_reason = 'SUSPENDED'
1656
        self.state = self.SUSPENDED
1657
        self.save()
1658

    
1659
    def resume(self):
1660
        self.deactivation_reason = None
1661
        self.state = self.APPROVED
1662
        self.save()
1663

    
1664
    ### Logical checks
1665

    
1666
    def is_inconsistent(self):
1667
        now = datetime.now()
1668
        dates = [self.creation_date,
1669
                 self.last_approval_date,
1670
                 self.deactivation_date]
1671
        return any([date > now for date in dates])
1672

    
1673
    def is_active_strict(self):
1674
        return self.is_active and self.state == self.APPROVED
1675

    
1676
    def is_approved(self):
1677
        return self.state == self.APPROVED
1678

    
1679
    @property
1680
    def is_alive(self):
1681
        return not self.is_terminated
1682

    
1683
    @property
1684
    def is_terminated(self):
1685
        return self.is_deactivated(self.TERMINATED)
1686

    
1687
    @property
1688
    def is_suspended(self):
1689
        return self.is_deactivated(self.SUSPENDED)
1690

    
1691
    def violates_resource_grants(self):
1692
        return False
1693

    
1694
    def violates_members_limit(self, adding=0):
1695
        application = self.application
1696
        limit = application.limit_on_members_number
1697
        if limit is None:
1698
            return False
1699
        return (len(self.approved_members) + adding > limit)
1700

    
1701

    
1702
    ### Other
1703

    
1704
    def count_pending_memberships(self):
1705
        memb_set = self.projectmembership_set
1706
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1707
        return memb_count
1708

    
1709
    @property
1710
    def approved_memberships(self):
1711
        query = ProjectMembership.Q_ACCEPTED_STATES
1712
        return self.projectmembership_set.filter(query)
1713

    
1714
    @property
1715
    def approved_members(self):
1716
        return [m.person for m in self.approved_memberships]
1717

    
1718
    def add_member(self, user):
1719
        """
1720
        Raises:
1721
            django.exceptions.PermissionDenied
1722
            astakos.im.models.AstakosUser.DoesNotExist
1723
        """
1724
        if isinstance(user, int):
1725
            user = AstakosUser.objects.get(user=user)
1726

    
1727
        m, created = ProjectMembership.objects.get_or_create(
1728
            person=user, project=self
1729
        )
1730
        m.accept()
1731

    
1732
    def remove_member(self, user):
1733
        """
1734
        Raises:
1735
            django.exceptions.PermissionDenied
1736
            astakos.im.models.AstakosUser.DoesNotExist
1737
            astakos.im.models.ProjectMembership.DoesNotExist
1738
        """
1739
        if isinstance(user, int):
1740
            user = AstakosUser.objects.get(user=user)
1741

    
1742
        m = ProjectMembership.objects.get(person=user, project=self)
1743
        m.remove()
1744

    
1745

    
1746
class PendingMembershipError(Exception):
1747
    pass
1748

    
1749

    
1750
class ProjectMembershipManager(ForUpdateManager):
1751
    pass
1752

    
1753
class ProjectMembership(models.Model):
1754

    
1755
    person              =   models.ForeignKey(AstakosUser)
1756
    request_date        =   models.DateField(auto_now_add=True)
1757
    project             =   models.ForeignKey(Project)
1758

    
1759
    REQUESTED           =   0
1760
    ACCEPTED            =   1
1761
    # User deactivation
1762
    USER_SUSPENDED      =   10
1763
    # Project deactivation
1764
    PROJECT_DEACTIVATED =   100
1765

    
1766
    REMOVED             =   200
1767

    
1768
    ASSOCIATED_STATES   =   set([REQUESTED,
1769
                                 ACCEPTED,
1770
                                 USER_SUSPENDED,
1771
                                 PROJECT_DEACTIVATED])
1772

    
1773
    ACCEPTED_STATES     =   set([ACCEPTED,
1774
                                 USER_SUSPENDED,
1775
                                 PROJECT_DEACTIVATED])
1776

    
1777
    state               =   models.IntegerField(default=REQUESTED,
1778
                                                db_index=True)
1779
    is_pending          =   models.BooleanField(default=False, db_index=True)
1780
    is_active           =   models.BooleanField(default=False, db_index=True)
1781
    application         =   models.ForeignKey(
1782
                                ProjectApplication,
1783
                                null=True,
1784
                                related_name='memberships')
1785
    pending_application =   models.ForeignKey(
1786
                                ProjectApplication,
1787
                                null=True,
1788
                                related_name='pending_memberships')
1789
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1790

    
1791
    acceptance_date     =   models.DateField(null=True, db_index=True)
1792
    leave_request_date  =   models.DateField(null=True)
1793

    
1794
    objects     =   ProjectMembershipManager()
1795

    
1796
    # Compiled queries
1797
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1798

    
1799
    def get_combined_state(self):
1800
        return self.state, self.is_active, self.is_pending
1801

    
1802
    class Meta:
1803
        unique_together = ("person", "project")
1804
        #index_together = [["project", "state"]]
1805

    
1806
    def __str__(self):
1807
        return _("<'%s' membership in '%s'>") % (
1808
                self.person.username, self.project)
1809

    
1810
    __repr__ = __str__
1811

    
1812
    def __init__(self, *args, **kwargs):
1813
        self.state = self.REQUESTED
1814
        super(ProjectMembership, self).__init__(*args, **kwargs)
1815

    
1816
    def _set_history_item(self, reason, date=None):
1817
        if isinstance(reason, basestring):
1818
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1819

    
1820
        history_item = ProjectMembershipHistory(
1821
                            serial=self.id,
1822
                            person=self.person_id,
1823
                            project=self.project_id,
1824
                            date=date or datetime.now(),
1825
                            reason=reason)
1826
        history_item.save()
1827
        serial = history_item.id
1828

    
1829
    def accept(self):
1830
        if self.is_pending:
1831
            m = _("%s: attempt to accept while is pending") % (self,)
1832
            raise AssertionError(m)
1833

    
1834
        state = self.state
1835
        if state != self.REQUESTED:
1836
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1837
            raise AssertionError(m)
1838

    
1839
        now = datetime.now()
1840
        self.acceptance_date = now
1841
        self._set_history_item(reason='ACCEPT', date=now)
1842
        if self.project.is_approved():
1843
            self.state = self.ACCEPTED
1844
            self.is_pending = True
1845
        else:
1846
            self.state = self.PROJECT_DEACTIVATED
1847

    
1848
        self.save()
1849

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

    
1855
        state = self.state
1856
        if state not in self.ACCEPTED_STATES:
1857
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1858
            raise AssertionError(m)
1859

    
1860
        self._set_history_item(reason='REMOVE')
1861
        self.state = self.REMOVED
1862
        self.is_pending = True
1863
        self.save()
1864

    
1865
    def reject(self):
1866
        if self.is_pending:
1867
            m = _("%s: attempt to reject while is pending") % (self,)
1868
            raise AssertionError(m)
1869

    
1870
        state = self.state
1871
        if state != self.REQUESTED:
1872
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1873
            raise AssertionError(m)
1874

    
1875
        # rejected requests don't need sync,
1876
        # because they were never effected
1877
        self._set_history_item(reason='REJECT')
1878
        self.delete()
1879

    
1880
    def get_diff_quotas(self, sub_list=None, add_list=None):
1881
        if sub_list is None:
1882
            sub_list = []
1883

    
1884
        if add_list is None:
1885
            add_list = []
1886

    
1887
        sub_append = sub_list.append
1888
        add_append = add_list.append
1889
        holder = self.person.uuid
1890

    
1891
        synced_application = self.application
1892
        if synced_application is not None:
1893
            cur_grants = synced_application.projectresourcegrant_set.all()
1894
            for grant in cur_grants:
1895
                sub_append(QuotaLimits(
1896
                               holder       = holder,
1897
                               resource     = str(grant.resource),
1898
                               capacity     = grant.member_capacity,
1899
                               import_limit = grant.member_import_limit,
1900
                               export_limit = grant.member_export_limit))
1901

    
1902
        pending_application = self.pending_application
1903
        if pending_application is not None:
1904
            new_grants = pending_application.projectresourcegrant_set.all()
1905
            for new_grant in new_grants:
1906
                add_append(QuotaLimits(
1907
                               holder       = holder,
1908
                               resource     = str(new_grant.resource),
1909
                               capacity     = new_grant.member_capacity,
1910
                               import_limit = new_grant.member_import_limit,
1911
                               export_limit = new_grant.member_export_limit))
1912

    
1913
        return (sub_list, add_list)
1914

    
1915
    def set_sync(self):
1916
        if not self.is_pending:
1917
            m = _("%s: attempt to sync a non pending membership") % (self,)
1918
            raise AssertionError(m)
1919

    
1920
        state = self.state
1921
        if state == self.ACCEPTED:
1922
            pending_application = self.pending_application
1923
            if pending_application is None:
1924
                m = _("%s: attempt to sync an empty pending application") % (
1925
                    self,)
1926
                raise AssertionError(m)
1927

    
1928
            self.application = pending_application
1929
            self.is_active = True
1930

    
1931
            self.pending_application = None
1932
            self.pending_serial = None
1933

    
1934
            # project.application may have changed in the meantime,
1935
            # in which case we stay PENDING;
1936
            # we are safe to check due to select_for_update
1937
            if self.application == self.project.application:
1938
                self.is_pending = False
1939
            self.save()
1940

    
1941
        elif state == self.PROJECT_DEACTIVATED:
1942
            if self.pending_application:
1943
                m = _("%s: attempt to sync in state '%s' "
1944
                      "with a pending application") % (self, state)
1945
                raise AssertionError(m)
1946

    
1947
            self.application = None
1948
            self.is_active = False
1949
            self.pending_serial = None
1950
            self.is_pending = False
1951
            self.save()
1952

    
1953
        elif state == self.REMOVED:
1954
            self.delete()
1955

    
1956
        else:
1957
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1958
            raise AssertionError(m)
1959

    
1960
    def reset_sync(self):
1961
        if not self.is_pending:
1962
            m = _("%s: attempt to reset a non pending membership") % (self,)
1963
            raise AssertionError(m)
1964

    
1965
        state = self.state
1966
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
1967
            self.pending_application = None
1968
            self.pending_serial = None
1969
            self.save()
1970
        else:
1971
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1972
            raise AssertionError(m)
1973

    
1974
class Serial(models.Model):
1975
    serial  =   models.AutoField(primary_key=True)
1976

    
1977
def new_serial():
1978
    s = Serial.objects.create()
1979
    serial = s.serial
1980
    s.delete()
1981
    return serial
1982

    
1983
def sync_finish_serials(serials_to_ack=None):
1984
    if serials_to_ack is None:
1985
        serials_to_ack = qh_query_serials([])
1986

    
1987
    serials_to_ack = set(serials_to_ack)
1988
    sfu = ProjectMembership.objects.select_for_update()
1989
    memberships = list(sfu.filter(pending_serial__isnull=False))
1990

    
1991
    if memberships:
1992
        for membership in memberships:
1993
            serial = membership.pending_serial
1994
            if serial in serials_to_ack:
1995
                membership.set_sync()
1996
            else:
1997
                membership.reset_sync()
1998

    
1999
        transaction.commit()
2000

    
2001
    qh_ack_serials(list(serials_to_ack))
2002
    return len(memberships)
2003

    
2004
def pre_sync():
2005
    ACCEPTED = ProjectMembership.ACCEPTED
2006
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2007
    psfu = Project.objects.select_for_update()
2008

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

    
2013
        memberships = objects.filter(state=ACCEPTED)
2014
        for membership in memberships:
2015
            membership.is_pending = True
2016
            membership.save()
2017

    
2018
    reactivating = psfu.reactivating_projects()
2019
    for project in reactivating:
2020
        objects = project.projectmembership_set.select_for_update()
2021
        memberships = objects.filter(state=PROJECT_DEACTIVATED)
2022
        for membership in memberships:
2023
            membership.is_pending = True
2024
            membership.state = ACCEPTED
2025
            membership.save()
2026

    
2027
    deactivating = psfu.deactivating_projects()
2028
    for project in deactivating:
2029
        objects = project.projectmembership_set.select_for_update()
2030

    
2031
        # Note: we keep a user-level deactivation (e.g. USER_SUSPENDED) intact
2032
        memberships = objects.filter(state=ACCEPTED)
2033
        for membership in memberships:
2034
            membership.is_pending = True
2035
            membership.state = PROJECT_DEACTIVATED
2036
            membership.save()
2037

    
2038
def do_sync():
2039

    
2040
    ACCEPTED = ProjectMembership.ACCEPTED
2041
    objects = ProjectMembership.objects.select_for_update()
2042

    
2043
    sub_quota, add_quota = [], []
2044

    
2045
    serial = new_serial()
2046

    
2047
    pending = objects.filter(is_pending=True)
2048
    for membership in pending:
2049

    
2050
        if membership.pending_application:
2051
            m = "%s: impossible: pending_application is not None (%s)" % (
2052
                membership, membership.pending_application)
2053
            raise AssertionError(m)
2054
        if membership.pending_serial:
2055
            m = "%s: impossible: pending_serial is not None (%s)" % (
2056
                membership, membership.pending_serial)
2057
            raise AssertionError(m)
2058

    
2059
        if membership.state == ACCEPTED:
2060
            membership.pending_application = membership.project.application
2061

    
2062
        membership.pending_serial = serial
2063
        membership.get_diff_quotas(sub_quota, add_quota)
2064
        membership.save()
2065

    
2066
    transaction.commit()
2067
    # ProjectApplication.approve() unblocks here
2068
    # and can set PENDING an already PENDING membership
2069
    # which has been scheduled to sync with the old project.application
2070
    # Need to check in ProjectMembership.set_sync()
2071

    
2072
    r = qh_add_quota(serial, sub_quota, add_quota)
2073
    if r:
2074
        m = "cannot sync serial: %d" % serial
2075
        raise RuntimeError(m)
2076

    
2077
    return serial
2078

    
2079
def post_sync():
2080
    ACCEPTED = ProjectMembership.ACCEPTED
2081
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2082
    psfu = Project.objects.select_for_update()
2083

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

    
2088
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2089
        if not memberships:
2090
            project.is_modified = False
2091
            project.save()
2092

    
2093
    reactivating = psfu.reactivating_projects()
2094
    for project in reactivating:
2095
        objects = project.projectmembership_set.select_for_update()
2096
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2097
                                          Q(is_pending=True)))
2098
        if not memberships:
2099
            project.reactivate()
2100
            project.save()
2101

    
2102
    deactivating = psfu.deactivating_projects()
2103
    for project in deactivating:
2104
        objects = project.projectmembership_set.select_for_update()
2105

    
2106
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2107
                                          Q(is_pending=True)))
2108
        if not memberships:
2109
            project.deactivate()
2110
            project.save()
2111

    
2112
    transaction.commit()
2113

    
2114
def sync_projects():
2115
    sync_finish_serials()
2116
    pre_sync()
2117
    serial = do_sync()
2118
    sync_finish_serials([serial])
2119
    post_sync()
2120

    
2121
def trigger_sync(retries=3, retry_wait=1.0):
2122
    transaction.commit()
2123

    
2124
    cursor = connection.cursor()
2125
    locked = True
2126
    try:
2127
        while 1:
2128
            cursor.execute("SELECT pg_try_advisory_lock(1)")
2129
            r = cursor.fetchone()
2130
            if r is None:
2131
                m = "Impossible"
2132
                raise AssertionError(m)
2133
            locked = r[0]
2134
            if locked:
2135
                break
2136

    
2137
            retries -= 1
2138
            if retries <= 0:
2139
                return False
2140
            sleep(retry_wait)
2141

    
2142
        sync_projects()
2143
        return True
2144

    
2145
    finally:
2146
        if locked:
2147
            cursor.execute("SELECT pg_advisory_unlock(1)")
2148
            cursor.fetchall()
2149

    
2150

    
2151
class ProjectMembershipHistory(models.Model):
2152
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2153
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2154

    
2155
    person  =   models.BigIntegerField()
2156
    project =   models.BigIntegerField()
2157
    date    =   models.DateField(auto_now_add=True)
2158
    reason  =   models.IntegerField()
2159
    serial  =   models.BigIntegerField()
2160

    
2161
### SIGNALS ###
2162
################
2163

    
2164
def create_astakos_user(u):
2165
    try:
2166
        AstakosUser.objects.get(user_ptr=u.pk)
2167
    except AstakosUser.DoesNotExist:
2168
        extended_user = AstakosUser(user_ptr_id=u.pk)
2169
        extended_user.__dict__.update(u.__dict__)
2170
        extended_user.save()
2171
        if not extended_user.has_auth_provider('local'):
2172
            extended_user.add_auth_provider('local')
2173
    except BaseException, e:
2174
        logger.exception(e)
2175

    
2176

    
2177
def fix_superusers(sender, **kwargs):
2178
    # Associate superusers with AstakosUser
2179
    admins = User.objects.filter(is_superuser=True)
2180
    for u in admins:
2181
        create_astakos_user(u)
2182
post_syncdb.connect(fix_superusers)
2183

    
2184

    
2185
def user_post_save(sender, instance, created, **kwargs):
2186
    if not created:
2187
        return
2188
    create_astakos_user(instance)
2189
post_save.connect(user_post_save, sender=User)
2190

    
2191
def astakosuser_post_save(sender, instance, created, **kwargs):
2192
    pass
2193

    
2194
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2195

    
2196
def resource_post_save(sender, instance, created, **kwargs):
2197
    pass
2198

    
2199
post_save.connect(resource_post_save, sender=Resource)
2200

    
2201
def renew_token(sender, instance, **kwargs):
2202
    if not instance.auth_token:
2203
        instance.renew_token()
2204
pre_save.connect(renew_token, sender=AstakosUser)
2205
pre_save.connect(renew_token, sender=Service)
2206