Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (73.7 kB)

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

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

    
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, help_text = _( 'test' ))
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, include_unverified=False, **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
                if not include_unverified:
567
                    kwargs['user__email_verified'] = True
568
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
569
                                                                   **kwargs)
570
            except AstakosUser.DoesNotExist:
571
                return True
572
            else:
573
                return False
574

    
575
        return True
576

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

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

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

    
588
        return True
589

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

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

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

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

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

    
616
        if self.can_add_auth_provider(provider, **kwargs):
617
            if 'identifier' in kwargs:
618
                # clean up third party pending for activation users of the same
619
                # identifier
620
                AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
621
                                                                **kwargs)
622
            self.auth_providers.create(module=provider, active=True,
623
                                       info_data=info_data,
624
                                       **kwargs)
625
        else:
626
            raise Exception('Cannot add provider')
627

    
628
    def add_pending_auth_provider(self, pending):
629
        """
630
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
631
        the current user.
632
        """
633
        if not isinstance(pending, PendingThirdPartyUser):
634
            pending = PendingThirdPartyUser.objects.get(token=pending)
635

    
636
        provider = self.add_auth_provider(pending.provider,
637
                               identifier=pending.third_party_identifier,
638
                                affiliation=pending.affiliation,
639
                                          provider_info=pending.info)
640

    
641
        if email_re.match(pending.email or '') and pending.email != self.email:
642
            self.additionalmail_set.get_or_create(email=pending.email)
643

    
644
        pending.delete()
645
        return provider
646

    
647
    def remove_auth_provider(self, provider, **kwargs):
648
        self.get_auth_providers().get(module=provider, **kwargs).delete()
649

    
650
    # user urls
651
    def get_resend_activation_url(self):
652
        return reverse('send_activation', kwargs={'user_id': self.pk})
653

    
654
    def get_provider_remove_url(self, module, **kwargs):
655
        return reverse('remove_auth_provider', kwargs={
656
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
657

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

    
665
    def get_password_reset_url(self, token_generator=default_token_generator):
666
        return reverse('django.contrib.auth.views.password_reset_confirm',
667
                          kwargs={'uidb36':int_to_base36(self.id),
668
                                  'token':token_generator.make_token(self)})
669

    
670
    def get_auth_providers(self):
671
        return self.auth_providers
672

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

    
682
        return providers
683

    
684
    def get_active_auth_providers(self, **filters):
685
        providers = []
686
        for provider in self.get_auth_providers().active(**filters):
687
            if auth_providers.get_provider(provider.module).is_available_for_login():
688
                providers.append(provider)
689
        return providers
690

    
691
    @property
692
    def auth_providers_display(self):
693
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
694

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

    
720
        return mark_safe(message + u' '+ msg_extra)
721

    
722
    def owns_project(self, project):
723
        return project.owner == self
724

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

    
729
    def is_project_accepted_member(self, project_or_application):
730
        return self.get_status_in_project(project_or_application) in \
731
                                            ProjectMembership.ACCEPTED_STATES
732

    
733
    def get_status_in_project(self, project_or_application):
734
        application = project_or_application
735
        if isinstance(project_or_application, Project):
736
            application = project_or_application.project
737
        return application.user_status(self)
738

    
739

    
740
class AstakosUserAuthProviderManager(models.Manager):
741

    
742
    def active(self, **filters):
743
        return self.filter(active=True, **filters)
744

    
745
    def remove_unverified_providers(self, provider, **filters):
746
        try:
747
            existing = self.filter(module=provider, user__email_verified=False, **filters)
748
            for p in existing:
749
                p.user.delete()
750
        except:
751
            pass
752

    
753

    
754

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

    
773
    objects = AstakosUserAuthProviderManager()
774

    
775
    class Meta:
776
        unique_together = (('identifier', 'module', 'user'), )
777
        ordering = ('module', 'created')
778

    
779
    def __init__(self, *args, **kwargs):
780
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
781
        try:
782
            self.info = json.loads(self.info_data)
783
            if not self.info:
784
                self.info = {}
785
        except Exception, e:
786
            self.info = {}
787

    
788
        for key,value in self.info.iteritems():
789
            setattr(self, 'info_%s' % key, value)
790

    
791

    
792
    @property
793
    def settings(self):
794
        return auth_providers.get_provider(self.module)
795

    
796
    @property
797
    def details_display(self):
798
        try:
799
          return self.settings.get_details_tpl_display % self.__dict__
800
        except:
801
          return ''
802

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

    
816
    def can_remove(self):
817
        return self.user.can_remove_auth_provider(self.module)
818

    
819
    def delete(self, *args, **kwargs):
820
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
821
        if self.module == 'local':
822
            self.user.set_unusable_password()
823
            self.user.save()
824
        return ret
825

    
826
    def __repr__(self):
827
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
828

    
829
    def __unicode__(self):
830
        if self.identifier:
831
            return "%s:%s" % (self.module, self.identifier)
832
        if self.auth_backend:
833
            return "%s:%s" % (self.module, self.auth_backend)
834
        return self.module
835

    
836
    def save(self, *args, **kwargs):
837
        self.info_data = json.dumps(self.info)
838
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
839

    
840

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

    
868
    update_or_create = _update_or_create
869

    
870

    
871
class AstakosUserQuota(models.Model):
872
    objects = ExtendedManager()
873
    capacity = intDecimalField()
874
    quantity = intDecimalField(default=0)
875
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
876
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
877
    resource = models.ForeignKey(Resource)
878
    user = models.ForeignKey(AstakosUser)
879

    
880
    class Meta:
881
        unique_together = ("resource", "user")
882

    
883
    def quota_values(self):
884
        return QuotaValues(
885
            quantity = self.quantity,
886
            capacity = self.capacity,
887
            import_limit = self.import_limit,
888
            export_limit = self.export_limit)
889

    
890

    
891
class ApprovalTerms(models.Model):
892
    """
893
    Model for approval terms
894
    """
895

    
896
    date = models.DateTimeField(
897
        _('Issue date'), db_index=True, auto_now_add=True)
898
    location = models.CharField(_('Terms location'), max_length=255)
899

    
900

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

    
914
    def __init__(self, *args, **kwargs):
915
        super(Invitation, self).__init__(*args, **kwargs)
916
        if not self.id:
917
            self.code = _generate_invitation_code()
918

    
919
    def consume(self):
920
        self.is_consumed = True
921
        self.consumed = datetime.now()
922
        self.save()
923

    
924
    def __unicode__(self):
925
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
926

    
927

    
928
class EmailChangeManager(models.Manager):
929

    
930
    @transaction.commit_on_success
931
    def change_email(self, activation_key):
932
        """
933
        Validate an activation key and change the corresponding
934
        ``User`` if valid.
935

936
        If the key is valid and has not expired, return the ``User``
937
        after activating.
938

939
        If the key is not valid or has expired, return ``None``.
940

941
        If the key is valid but the ``User`` is already active,
942
        return ``None``.
943

944
        After successful email change the activation record is deleted.
945

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

    
974

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

    
985
    objects = EmailChangeManager()
986

    
987
    def get_url(self):
988
        return reverse('email_change_confirm',
989
                      kwargs={'activation_key': self.activation_key})
990

    
991
    def activation_key_expired(self):
992
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
993
        return self.requested_at + expiration_date < datetime.now()
994

    
995

    
996
class AdditionalMail(models.Model):
997
    """
998
    Model for registring invitations
999
    """
1000
    owner = models.ForeignKey(AstakosUser)
1001
    email = models.EmailField()
1002

    
1003

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

    
1013

    
1014
def get_latest_terms():
1015
    try:
1016
        term = ApprovalTerms.objects.order_by('-id')[0]
1017
        return term
1018
    except IndexError:
1019
        pass
1020
    return None
1021

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

    
1040
    class Meta:
1041
        unique_together = ("provider", "third_party_identifier")
1042

    
1043
    def get_user_instance(self):
1044
        d = self.__dict__
1045
        d.pop('_state', None)
1046
        d.pop('id', None)
1047
        d.pop('token', None)
1048
        d.pop('created', None)
1049
        d.pop('info', None)
1050
        user = AstakosUser(**d)
1051

    
1052
        return user
1053

    
1054
    @property
1055
    def realname(self):
1056
        return '%s %s' %(self.first_name, self.last_name)
1057

    
1058
    @realname.setter
1059
    def realname(self, value):
1060
        parts = value.split(' ')
1061
        if len(parts) == 2:
1062
            self.first_name = parts[0]
1063
            self.last_name = parts[1]
1064
        else:
1065
            self.last_name = parts[0]
1066

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

    
1078
    def generate_token(self):
1079
        self.password = self.third_party_identifier
1080
        self.last_login = datetime.now()
1081
        self.token = default_token_generator.make_token(self)
1082

    
1083
class SessionCatalog(models.Model):
1084
    session_key = models.CharField(_('session key'), max_length=40)
1085
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1086

    
1087

    
1088
### PROJECTS ###
1089
################
1090

    
1091
def synced_model_metaclass(class_name, class_parents, class_attributes):
1092

    
1093
    new_attributes = {}
1094
    sync_attributes = {}
1095

    
1096
    for name, value in class_attributes.iteritems():
1097
        sync, underscore, rest = name.partition('_')
1098
        if sync == 'sync' and underscore == '_':
1099
            sync_attributes[rest] = value
1100
        else:
1101
            new_attributes[name] = value
1102

    
1103
    if 'prefix' not in sync_attributes:
1104
        m = ("you did not specify a 'sync_prefix' attribute "
1105
             "in class '%s'" % (class_name,))
1106
        raise ValueError(m)
1107

    
1108
    prefix = sync_attributes.pop('prefix')
1109
    class_name = sync_attributes.pop('classname', prefix + '_model')
1110

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

    
1119
        new_attributes[newname] = value
1120

    
1121
    newclass = type(class_name, class_parents, new_attributes)
1122
    return newclass
1123

    
1124

    
1125
def make_synced(prefix='sync', name='SyncedState'):
1126

    
1127
    the_name = name
1128
    the_prefix = prefix
1129

    
1130
    class SyncedState(models.Model):
1131

    
1132
        sync_classname      = the_name
1133
        sync_prefix         = the_prefix
1134
        __metaclass__       = synced_model_metaclass
1135

    
1136
        sync_new_state      = models.BigIntegerField(null=True)
1137
        sync_synced_state   = models.BigIntegerField(null=True)
1138
        STATUS_SYNCED       = 0
1139
        STATUS_PENDING      = 1
1140
        sync_status         = models.IntegerField(db_index=True)
1141

    
1142
        class Meta:
1143
            abstract = True
1144

    
1145
        class NotSynced(Exception):
1146
            pass
1147

    
1148
        def sync_init_state(self, state):
1149
            self.sync_synced_state = state
1150
            self.sync_new_state = state
1151
            self.sync_status = self.STATUS_SYNCED
1152

    
1153
        def sync_get_status(self):
1154
            return self.sync_status
1155

    
1156
        def sync_set_status(self):
1157
            if self.sync_new_state != self.sync_synced_state:
1158
                self.sync_status = self.STATUS_PENDING
1159
            else:
1160
                self.sync_status = self.STATUS_SYNCED
1161

    
1162
        def sync_set_synced(self):
1163
            self.sync_synced_state = self.sync_new_state
1164
            self.sync_status = self.STATUS_SYNCED
1165

    
1166
        def sync_get_synced_state(self):
1167
            return self.sync_synced_state
1168

    
1169
        def sync_set_new_state(self, new_state):
1170
            self.sync_new_state = new_state
1171
            self.sync_set_status()
1172

    
1173
        def sync_get_new_state(self):
1174
            return self.sync_new_state
1175

    
1176
        def sync_set_synced_state(self, synced_state):
1177
            self.sync_synced_state = synced_state
1178
            self.sync_set_status()
1179

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

    
1184
        def sync_get_synced_objects(self):
1185
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1186
            return self.objects.filter(**kw)
1187

    
1188
        def sync_verify_get_synced_state(self):
1189
            status = self.sync_get_status()
1190
            state = self.sync_get_synced_state()
1191
            verified = (status == self.STATUS_SYNCED)
1192
            return state, verified
1193

    
1194
        def sync_is_synced(self):
1195
            state, verified = self.sync_verify_get_synced_state()
1196
            return verified
1197

    
1198
    return SyncedState
1199

    
1200
SyncedState = make_synced(prefix='sync', name='SyncedState')
1201

    
1202

    
1203
class ProjectApplicationManager(ForUpdateManager):
1204

    
1205
    def user_visible_projects(self, *filters, **kw_filters):
1206
        return self.filter(Q(state=ProjectApplication.PENDING)|\
1207
                           Q(state=ProjectApplication.APPROVED))
1208

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

    
1218
    def user_accessible_projects(self, user):
1219
        """
1220
        Return projects accessed by specified user.
1221
        """
1222
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1223
                               Q(project__projectmembership__person=user)
1224

    
1225
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1226

    
1227
    def search_by_name(self, *search_strings):
1228
        q = Q()
1229
        for s in search_strings:
1230
            q = q | Q(name__icontains=s)
1231
        return self.filter(q)
1232

    
1233

    
1234
USER_STATUS_DISPLAY = {
1235
      0: _('Join requested'),
1236
      1: _('Accepted member'),
1237
     10: _('Suspended'),
1238
    100: _('Terminated'),
1239
    200: _('Removed'),
1240
     -1: _('Not a member'),
1241
}
1242

    
1243

    
1244
class Chain(models.Model):
1245
    chain  =   models.AutoField(primary_key=True)
1246

    
1247
def new_chain():
1248
    c = Chain.objects.create()
1249
    chain = c.chain
1250
    c.delete()
1251
    return chain
1252

    
1253

    
1254
class ProjectApplication(models.Model):
1255
    applicant               =   models.ForeignKey(
1256
                                    AstakosUser,
1257
                                    related_name='projects_applied',
1258
                                    db_index=True)
1259

    
1260
    PENDING     =    0
1261
    APPROVED    =    1
1262
    REPLACED    =    2
1263
    DENIED      =    3
1264
    DISMISSED   =    4
1265
    CANCELLED   =    5
1266

    
1267
    state                   =   models.IntegerField(default=PENDING)
1268

    
1269
    owner                   =   models.ForeignKey(
1270
                                    AstakosUser,
1271
                                    related_name='projects_owned',
1272
                                    db_index=True)
1273

    
1274
    chain                   =   models.IntegerField()
1275
    precursor_application   =   models.ForeignKey('ProjectApplication',
1276
                                                  null=True,
1277
                                                  blank=True)
1278

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

    
1296
    objects                 =   ProjectApplicationManager()
1297

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

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

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

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

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

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

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

    
1342
        return status
1343

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1444
        Raises:
1445
            PermissionDenied
1446
        """
1447

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

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

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

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

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

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

    
1482
        project.save()
1483

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

    
1488
def submit_application(**kw):
1489

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

    
1493
    precursor = kw['precursor_application']
1494

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

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

    
1507
class ProjectResourceGrant(models.Model):
1508

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

    
1519
    objects = ExtendedManager()
1520

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

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

    
1531

    
1532
class ProjectManager(ForUpdateManager):
1533

    
1534
    def _q_terminated(self):
1535
        return Q(state=Project.TERMINATED)
1536
    def _q_suspended(self):
1537
        return Q(state=Project.SUSPENDED)
1538
    def _q_deactivated(self):
1539
        return self._q_terminated() | self._q_suspended()
1540

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

    
1545
    def not_terminated_projects(self):
1546
        q = ~self._q_terminated()
1547
        return self.filter(q)
1548

    
1549
    def terminating_projects(self):
1550
        q = self._q_terminated() & Q(is_active=True)
1551
        return self.filter(q)
1552

    
1553
    def deactivated_projects(self):
1554
        q = self._q_deactivated()
1555
        return self.filter(q)
1556

    
1557
    def deactivating_projects(self):
1558
        q = self._q_deactivated() & Q(is_active=True)
1559
        return self.filter(q)
1560

    
1561
    def modified_projects(self):
1562
        return self.filter(is_modified=True)
1563

    
1564
    def reactivating_projects(self):
1565
        return self.filter(state=Project.APPROVED, is_active=False)
1566

    
1567
class Project(models.Model):
1568

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

    
1574
    members                     =   models.ManyToManyField(
1575
                                            AstakosUser,
1576
                                            through='ProjectMembership')
1577

    
1578
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1579
    deactivation_date           =   models.DateTimeField(null=True)
1580

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

    
1587
    APPROVED    = 1
1588
    SUSPENDED   = 10
1589
    TERMINATED  = 100
1590

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

    
1598
    objects     =   ProjectManager()
1599

    
1600
    def __str__(self):
1601
        return _("<project %s '%s'>") % (self.id, self.application.name)
1602

    
1603
    __repr__ = __str__
1604

    
1605
    def is_deactivated(self, reason=None):
1606
        if reason is not None:
1607
            return self.state == reason
1608

    
1609
        return self.state != self.APPROVED
1610

    
1611
    def is_deactivating(self, reason=None):
1612
        if not self.is_active:
1613
            return False
1614

    
1615
        return self.is_deactivated(reason)
1616

    
1617
    def is_deactivated_strict(self, reason=None):
1618
        if self.is_active:
1619
            return False
1620

    
1621
        return self.is_deactivated(reason)
1622

    
1623
    ### Deactivation calls
1624

    
1625
    def deactivate(self):
1626
        self.deactivation_date = datetime.now()
1627
        self.is_active = False
1628

    
1629
    def reactivate(self):
1630
        self.deactivation_date = None
1631
        self.is_active = True
1632

    
1633
    def terminate(self):
1634
        self.deactivation_reason = 'TERMINATED'
1635
        self.state = self.TERMINATED
1636
        self.save()
1637

    
1638
    def suspend(self):
1639
        self.deactivation_reason = 'SUSPENDED'
1640
        self.state = self.SUSPENDED
1641
        self.save()
1642

    
1643
    def resume(self):
1644
        self.deactivation_reason = None
1645
        self.state = self.APPROVED
1646
        self.save()
1647

    
1648
    ### Logical checks
1649

    
1650
    def is_inconsistent(self):
1651
        now = datetime.now()
1652
        dates = [self.creation_date,
1653
                 self.last_approval_date,
1654
                 self.deactivation_date]
1655
        return any([date > now for date in dates])
1656

    
1657
    def is_active_strict(self):
1658
        return self.is_active and self.state == self.APPROVED
1659

    
1660
    def is_approved(self):
1661
        return self.state == self.APPROVED
1662

    
1663
    @property
1664
    def is_alive(self):
1665
        return self.is_active_strict()
1666

    
1667
    @property
1668
    def is_terminated(self):
1669
        return self.is_deactivated(self.TERMINATED)
1670

    
1671
    @property
1672
    def is_suspended(self):
1673
        return self.is_deactivated(self.SUSPENDED)
1674

    
1675
    def violates_resource_grants(self):
1676
        return False
1677

    
1678
    def violates_members_limit(self, adding=0):
1679
        application = self.application
1680
        limit = application.limit_on_members_number
1681
        if limit is None:
1682
            return False
1683
        return (len(self.approved_members) + adding > limit)
1684

    
1685

    
1686
    ### Other
1687

    
1688
    def count_pending_memberships(self):
1689
        memb_set = self.projectmembership_set
1690
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1691
        return memb_count
1692

    
1693
    @property
1694
    def approved_memberships(self):
1695
        query = ProjectMembership.query_approved()
1696
        return self.projectmembership_set.filter(query)
1697

    
1698
    @property
1699
    def approved_members(self):
1700
        return [m.person for m in self.approved_memberships]
1701

    
1702
    def add_member(self, user):
1703
        """
1704
        Raises:
1705
            django.exceptions.PermissionDenied
1706
            astakos.im.models.AstakosUser.DoesNotExist
1707
        """
1708
        if isinstance(user, int):
1709
            user = AstakosUser.objects.get(user=user)
1710

    
1711
        m, created = ProjectMembership.objects.get_or_create(
1712
            person=user, project=self
1713
        )
1714
        m.accept()
1715

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

    
1726
        m = ProjectMembership.objects.get(person=user, project=self)
1727
        m.remove()
1728

    
1729

    
1730
class PendingMembershipError(Exception):
1731
    pass
1732

    
1733

    
1734
class ProjectMembershipManager(ForUpdateManager):
1735
    pass
1736

    
1737
class ProjectMembership(models.Model):
1738

    
1739
    person              =   models.ForeignKey(AstakosUser)
1740
    request_date        =   models.DateField(auto_now_add=True)
1741
    project             =   models.ForeignKey(Project)
1742

    
1743
    REQUESTED           =   0
1744
    ACCEPTED            =   1
1745
    # User deactivation
1746
    USER_SUSPENDED      =   10
1747
    # Project deactivation
1748
    PROJECT_DEACTIVATED =   100
1749

    
1750
    REMOVED             =   200
1751

    
1752
    ASSOCIATED_STATES   =   set([REQUESTED,
1753
                                 ACCEPTED,
1754
                                 USER_SUSPENDED,
1755
                                 PROJECT_DEACTIVATED])
1756

    
1757
    ACCEPTED_STATES     =   set([ACCEPTED,
1758
                                 USER_SUSPENDED,
1759
                                 PROJECT_DEACTIVATED])
1760

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

    
1775
    acceptance_date     =   models.DateField(null=True, db_index=True)
1776
    leave_request_date  =   models.DateField(null=True)
1777

    
1778
    objects     =   ProjectMembershipManager()
1779

    
1780

    
1781
    def get_combined_state(self):
1782
        return self.state, self.is_active, self.is_pending
1783

    
1784
    @classmethod
1785
    def query_approved(cls):
1786
        return (~Q(state=cls.REQUESTED) &
1787
                ~Q(state=cls.REMOVED))
1788

    
1789
    class Meta:
1790
        unique_together = ("person", "project")
1791
        #index_together = [["project", "state"]]
1792

    
1793
    def __str__(self):
1794
        return _("<'%s' membership in '%s'>") % (
1795
                self.person.username, self.project)
1796

    
1797
    __repr__ = __str__
1798

    
1799
    def __init__(self, *args, **kwargs):
1800
        self.state = self.REQUESTED
1801
        super(ProjectMembership, self).__init__(*args, **kwargs)
1802

    
1803
    def _set_history_item(self, reason, date=None):
1804
        if isinstance(reason, basestring):
1805
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1806

    
1807
        history_item = ProjectMembershipHistory(
1808
                            serial=self.id,
1809
                            person=self.person_id,
1810
                            project=self.project_id,
1811
                            date=date or datetime.now(),
1812
                            reason=reason)
1813
        history_item.save()
1814
        serial = history_item.id
1815

    
1816
    def accept(self):
1817
        if self.is_pending:
1818
            m = _("%s: attempt to accept while is pending") % (self,)
1819
            raise AssertionError(m)
1820

    
1821
        state = self.state
1822
        if state != self.REQUESTED:
1823
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1824
            raise AssertionError(m)
1825

    
1826
        now = datetime.now()
1827
        self.acceptance_date = now
1828
        self._set_history_item(reason='ACCEPT', date=now)
1829
        if self.project.is_approved():
1830
            self.state = self.ACCEPTED
1831
            self.is_pending = True
1832
        else:
1833
            self.state = self.PROJECT_DEACTIVATED
1834

    
1835
        self.save()
1836

    
1837
    def remove(self):
1838
        if self.is_pending:
1839
            m = _("%s: attempt to remove while is pending") % (self,)
1840
            raise AssertionError(m)
1841

    
1842
        state = self.state
1843
        if state not in self.ACCEPTED_STATES:
1844
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1845
            raise AssertionError(m)
1846

    
1847
        self._set_history_item(reason='REMOVE')
1848
        self.state = self.REMOVED
1849
        self.is_pending = True
1850
        self.save()
1851

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

    
1857
        state = self.state
1858
        if state != self.REQUESTED:
1859
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1860
            raise AssertionError(m)
1861

    
1862
        # rejected requests don't need sync,
1863
        # because they were never effected
1864
        self._set_history_item(reason='REJECT')
1865
        self.delete()
1866

    
1867
    def get_diff_quotas(self, sub_list=None, add_list=None):
1868
        if sub_list is None:
1869
            sub_list = []
1870

    
1871
        if add_list is None:
1872
            add_list = []
1873

    
1874
        sub_append = sub_list.append
1875
        add_append = add_list.append
1876
        holder = self.person.uuid
1877

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

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

    
1900
        return (sub_list, add_list)
1901

    
1902
    def set_sync(self):
1903
        if not self.is_pending:
1904
            m = _("%s: attempt to sync a non pending membership") % (self,)
1905
            raise AssertionError(m)
1906

    
1907
        state = self.state
1908
        if state == self.ACCEPTED:
1909
            pending_application = self.pending_application
1910
            if pending_application is None:
1911
                m = _("%s: attempt to sync an empty pending application") % (
1912
                    self,)
1913
                raise AssertionError(m)
1914

    
1915
            self.application = pending_application
1916
            self.is_active = True
1917

    
1918
            self.pending_application = None
1919
            self.pending_serial = None
1920

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

    
1928
        elif state == self.PROJECT_DEACTIVATED:
1929
            if self.pending_application:
1930
                m = _("%s: attempt to sync in state '%s' "
1931
                      "with a pending application") % (self, state)
1932
                raise AssertionError(m)
1933

    
1934
            self.application = None
1935
            self.pending_serial = None
1936
            self.is_pending = False
1937
            self.save()
1938

    
1939
        elif state == self.REMOVED:
1940
            self.delete()
1941

    
1942
        else:
1943
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1944
            raise AssertionError(m)
1945

    
1946
    def reset_sync(self):
1947
        if not self.is_pending:
1948
            m = _("%s: attempt to reset a non pending membership") % (self,)
1949
            raise AssertionError(m)
1950

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

    
1960
class Serial(models.Model):
1961
    serial  =   models.AutoField(primary_key=True)
1962

    
1963
def new_serial():
1964
    s = Serial.objects.create()
1965
    serial = s.serial
1966
    s.delete()
1967
    return serial
1968

    
1969
def sync_finish_serials(serials_to_ack=None):
1970
    if serials_to_ack is None:
1971
        serials_to_ack = qh_query_serials([])
1972

    
1973
    serials_to_ack = set(serials_to_ack)
1974
    sfu = ProjectMembership.objects.select_for_update()
1975
    memberships = list(sfu.filter(pending_serial__isnull=False))
1976

    
1977
    if memberships:
1978
        for membership in memberships:
1979
            serial = membership.pending_serial
1980
            if serial in serials_to_ack:
1981
                membership.set_sync()
1982
            else:
1983
                membership.reset_sync()
1984

    
1985
        transaction.commit()
1986

    
1987
    qh_ack_serials(list(serials_to_ack))
1988
    return len(memberships)
1989

    
1990
def pre_sync():
1991
    ACCEPTED = ProjectMembership.ACCEPTED
1992
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
1993
    psfu = Project.objects.select_for_update()
1994

    
1995
    modified = psfu.modified_projects()
1996
    for project in modified:
1997
        objects = project.projectmembership_set.select_for_update()
1998

    
1999
        memberships = objects.filter(state=ACCEPTED)
2000
        for membership in memberships:
2001
            membership.is_pending = True
2002
            membership.save()
2003

    
2004
    reactivating = psfu.reactivating_projects()
2005
    for project in reactivating:
2006
        objects = project.projectmembership_set.select_for_update()
2007
        memberships = objects.filter(state=PROJECT_DEACTIVATED)
2008
        for membership in memberships:
2009
            membership.is_pending = True
2010
            membership.state = ACCEPTED
2011
            membership.save()
2012

    
2013
    deactivating = psfu.deactivating_projects()
2014
    for project in deactivating:
2015
        objects = project.projectmembership_set.select_for_update()
2016

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

    
2024
def do_sync():
2025

    
2026
    ACCEPTED = ProjectMembership.ACCEPTED
2027
    objects = ProjectMembership.objects.select_for_update()
2028

    
2029
    sub_quota, add_quota = [], []
2030

    
2031
    serial = new_serial()
2032

    
2033
    pending = objects.filter(is_pending=True)
2034
    for membership in pending:
2035

    
2036
        if membership.pending_application:
2037
            m = "%s: impossible: pending_application is not None (%s)" % (
2038
                membership, membership.pending_application)
2039
            raise AssertionError(m)
2040
        if membership.pending_serial:
2041
            m = "%s: impossible: pending_serial is not None (%s)" % (
2042
                membership, membership.pending_serial)
2043
            raise AssertionError(m)
2044

    
2045
        if membership.state == ACCEPTED:
2046
            membership.pending_application = membership.project.application
2047

    
2048
        membership.pending_serial = serial
2049
        membership.get_diff_quotas(sub_quota, add_quota)
2050
        membership.save()
2051

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

    
2058
    r = qh_add_quota(serial, sub_quota, add_quota)
2059
    if r:
2060
        m = "cannot sync serial: %d" % serial
2061
        raise RuntimeError(m)
2062

    
2063
    return serial
2064

    
2065
def post_sync():
2066
    ACCEPTED = ProjectMembership.ACCEPTED
2067
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2068
    psfu = Project.objects.select_for_update()
2069

    
2070
    modified = psfu.modified_projects()
2071
    for project in modified:
2072
        objects = project.projectmembership_set.select_for_update()
2073

    
2074
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2075
        if not memberships:
2076
            project.is_modified = False
2077
            project.save()
2078

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

    
2088
    deactivating = psfu.deactivating_projects()
2089
    for project in deactivating:
2090
        objects = project.projectmembership_set.select_for_update()
2091

    
2092
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2093
                                          Q(is_pending=True)))
2094
        if not memberships:
2095
            project.deactivate()
2096
            project.save()
2097

    
2098
    transaction.commit()
2099

    
2100
def sync_projects():
2101
    sync_finish_serials()
2102
    pre_sync()
2103
    serial = do_sync()
2104
    sync_finish_serials([serial])
2105
    post_sync()
2106

    
2107
def trigger_sync(retries=3, retry_wait=1.0):
2108
    transaction.commit()
2109

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

    
2123
            retries -= 1
2124
            if retries <= 0:
2125
                return False
2126
            sleep(retry_wait)
2127

    
2128
        sync_projects()
2129
        return True
2130

    
2131
    finally:
2132
        if locked:
2133
            cursor.execute("SELECT pg_advisory_unlock(1)")
2134
            cursor.fetchall()
2135

    
2136

    
2137
class ProjectMembershipHistory(models.Model):
2138
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2139
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2140

    
2141
    person  =   models.BigIntegerField()
2142
    project =   models.BigIntegerField()
2143
    date    =   models.DateField(auto_now_add=True)
2144
    reason  =   models.IntegerField()
2145
    serial  =   models.BigIntegerField()
2146

    
2147
### SIGNALS ###
2148
################
2149

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

    
2162

    
2163
def fix_superusers(sender, **kwargs):
2164
    # Associate superusers with AstakosUser
2165
    admins = User.objects.filter(is_superuser=True)
2166
    for u in admins:
2167
        create_astakos_user(u)
2168
post_syncdb.connect(fix_superusers)
2169

    
2170

    
2171
def user_post_save(sender, instance, created, **kwargs):
2172
    if not created:
2173
        return
2174
    create_astakos_user(instance)
2175
post_save.connect(user_post_save, sender=User)
2176

    
2177
def astakosuser_post_save(sender, instance, created, **kwargs):
2178
    pass
2179

    
2180
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2181

    
2182
def resource_post_save(sender, instance, created, **kwargs):
2183
    pass
2184

    
2185
post_save.connect(resource_post_save, sender=Resource)
2186

    
2187
def renew_token(sender, instance, **kwargs):
2188
    if not instance.auth_token:
2189
        instance.renew_token()
2190
pre_save.connect(renew_token, sender=AstakosUser)
2191
pre_save.connect(renew_token, sender=Service)
2192