Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 531fca05

History | View | Annotate | Download (73.2 kB)

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

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

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

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

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

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

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

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

    
84
logger = logging.getLogger(__name__)
85

    
86
DEFAULT_CONTENT_TYPE = None
87
_content_type = None
88

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

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

    
101
RESOURCE_SEPARATOR = '.'
102

    
103
inf = float('inf')
104

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

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

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

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

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

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

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

    
144

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

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

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

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

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

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

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

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

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

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

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

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

    
201

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

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

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

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

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

    
244
    return _DEFAULT_QUOTA
245

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

    
252

    
253
class AstakosUserManager(UserManager):
254

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

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

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

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

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

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

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

    
288

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

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

    
305

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

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

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

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

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

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

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

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

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

    
341
    objects = AstakosUserManager()
342

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
466
        self.update_uuid()
467

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
574
        return True
575

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

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

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

    
587
        return True
588

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

    
592
    def has_required_auth_providers(self):
593
        required = auth_providers.REQUIRED_PROVIDERS
594
        for provider in required:
595
            if not self.has_auth_provider(provider):
596
                return False
597
        return True
598

    
599
    def has_auth_provider(self, provider, **kwargs):
600
        return bool(self.auth_providers.filter(module=provider,
601
                                               **kwargs).count())
602

    
603
    def add_auth_provider(self, provider, **kwargs):
604
        info_data = ''
605
        if 'provider_info' in kwargs:
606
            info_data = kwargs.pop('provider_info')
607
            if isinstance(info_data, dict):
608
                info_data = json.dumps(info_data)
609

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

    
619
    def add_pending_auth_provider(self, pending):
620
        """
621
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
622
        the current user.
623
        """
624
        if not isinstance(pending, PendingThirdPartyUser):
625
            pending = PendingThirdPartyUser.objects.get(token=pending)
626

    
627
        provider = self.add_auth_provider(pending.provider,
628
                               identifier=pending.third_party_identifier,
629
                                affiliation=pending.affiliation,
630
                                          provider_info=pending.info)
631

    
632
        if email_re.match(pending.email or '') and pending.email != self.email:
633
            self.additionalmail_set.get_or_create(email=pending.email)
634

    
635
        pending.delete()
636
        return provider
637

    
638
    def remove_auth_provider(self, provider, **kwargs):
639
        self.auth_providers.get(module=provider, **kwargs).delete()
640

    
641
    # user urls
642
    def get_resend_activation_url(self):
643
        return reverse('send_activation', kwargs={'user_id': self.pk})
644

    
645
    def get_provider_remove_url(self, module, **kwargs):
646
        return reverse('remove_auth_provider', kwargs={
647
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
648

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

    
656
    def get_password_reset_url(self, token_generator=default_token_generator):
657
        return reverse('django.contrib.auth.views.password_reset_confirm',
658
                          kwargs={'uidb36':int_to_base36(self.id),
659
                                  'token':token_generator.make_token(self)})
660

    
661
    def get_auth_providers(self):
662
        return self.auth_providers.all()
663

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

    
673
        return providers
674

    
675
    def get_active_auth_providers(self, **filters):
676
        providers = []
677
        for provider in self.auth_providers.active(**filters):
678
            if auth_providers.get_provider(provider.module).is_available_for_login():
679
                providers.append(auth_providers.get_provider(provider.module))
680
        return providers
681

    
682
    @property
683
    def auth_providers_display(self):
684
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
685

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

    
711
        return mark_safe(message + u' '+ msg_extra)
712

    
713
    def owns_project(self, project):
714
        return project.owner == self
715

    
716
    def is_project_member(self, project_or_application):
717
        return self.get_status_in_project(project_or_application) in \
718
                                        ProjectMembership.ASSOCIATED_STATES
719

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

    
724
    def get_status_in_project(self, project_or_application):
725
        application = project_or_application
726
        if isinstance(project_or_application, Project):
727
            application = project_or_application.project
728
        return application.user_status(self)
729

    
730

    
731
class AstakosUserAuthProviderManager(models.Manager):
732

    
733
    def active(self, **filters):
734
        return self.filter(active=True, **filters)
735

    
736
    def remove_unverified_providers(self, provider, **filters):
737
        try:
738
            existing = self.filter(module=provider, user__email_verified=False, **filters)
739
            for p in existing:
740
                p.user.delete()
741
        except:
742
            pass
743

    
744

    
745

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

    
764
    objects = AstakosUserAuthProviderManager()
765

    
766
    class Meta:
767
        unique_together = (('identifier', 'module', 'user'), )
768
        ordering = ('module', 'created')
769

    
770
    def __init__(self, *args, **kwargs):
771
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
772
        try:
773
            self.info = json.loads(self.info_data)
774
            if not self.info:
775
                self.info = {}
776
        except Exception, e:
777
            self.info = {}
778

    
779
        for key,value in self.info.iteritems():
780
            setattr(self, 'info_%s' % key, value)
781

    
782

    
783
    @property
784
    def settings(self):
785
        return auth_providers.get_provider(self.module)
786

    
787
    @property
788
    def details_display(self):
789
        try:
790
          return self.settings.get_details_tpl_display % self.__dict__
791
        except:
792
          return ''
793

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

    
807
    def can_remove(self):
808
        return self.user.can_remove_auth_provider(self.module)
809

    
810
    def delete(self, *args, **kwargs):
811
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
812
        if self.module == 'local':
813
            self.user.set_unusable_password()
814
            self.user.save()
815
        return ret
816

    
817
    def __repr__(self):
818
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
819

    
820
    def __unicode__(self):
821
        if self.identifier:
822
            return "%s:%s" % (self.module, self.identifier)
823
        if self.auth_backend:
824
            return "%s:%s" % (self.module, self.auth_backend)
825
        return self.module
826

    
827
    def save(self, *args, **kwargs):
828
        self.info_data = json.dumps(self.info)
829
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
830

    
831

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

    
859
    update_or_create = _update_or_create
860

    
861

    
862
class AstakosUserQuota(models.Model):
863
    objects = ExtendedManager()
864
    capacity = intDecimalField()
865
    quantity = intDecimalField(default=0)
866
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
867
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
868
    resource = models.ForeignKey(Resource)
869
    user = models.ForeignKey(AstakosUser)
870

    
871
    class Meta:
872
        unique_together = ("resource", "user")
873

    
874
    def quota_values(self):
875
        return QuotaValues(
876
            quantity = self.quantity,
877
            capacity = self.capacity,
878
            import_limit = self.import_limit,
879
            export_limit = self.export_limit)
880

    
881

    
882
class ApprovalTerms(models.Model):
883
    """
884
    Model for approval terms
885
    """
886

    
887
    date = models.DateTimeField(
888
        _('Issue date'), db_index=True, auto_now_add=True)
889
    location = models.CharField(_('Terms location'), max_length=255)
890

    
891

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

    
905
    def __init__(self, *args, **kwargs):
906
        super(Invitation, self).__init__(*args, **kwargs)
907
        if not self.id:
908
            self.code = _generate_invitation_code()
909

    
910
    def consume(self):
911
        self.is_consumed = True
912
        self.consumed = datetime.now()
913
        self.save()
914

    
915
    def __unicode__(self):
916
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
917

    
918

    
919
class EmailChangeManager(models.Manager):
920

    
921
    @transaction.commit_on_success
922
    def change_email(self, activation_key):
923
        """
924
        Validate an activation key and change the corresponding
925
        ``User`` if valid.
926

927
        If the key is valid and has not expired, return the ``User``
928
        after activating.
929

930
        If the key is not valid or has expired, return ``None``.
931

932
        If the key is valid but the ``User`` is already active,
933
        return ``None``.
934

935
        After successful email change the activation record is deleted.
936

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

    
965

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

    
976
    objects = EmailChangeManager()
977

    
978
    def get_url(self):
979
        return reverse('email_change_confirm',
980
                      kwargs={'activation_key': self.activation_key})
981

    
982
    def activation_key_expired(self):
983
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
984
        return self.requested_at + expiration_date < datetime.now()
985

    
986

    
987
class AdditionalMail(models.Model):
988
    """
989
    Model for registring invitations
990
    """
991
    owner = models.ForeignKey(AstakosUser)
992
    email = models.EmailField()
993

    
994

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

    
1004

    
1005
def get_latest_terms():
1006
    try:
1007
        term = ApprovalTerms.objects.order_by('-id')[0]
1008
        return term
1009
    except IndexError:
1010
        pass
1011
    return None
1012

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

    
1031
    class Meta:
1032
        unique_together = ("provider", "third_party_identifier")
1033

    
1034
    def get_user_instance(self):
1035
        d = self.__dict__
1036
        d.pop('_state', None)
1037
        d.pop('id', None)
1038
        d.pop('token', None)
1039
        d.pop('created', None)
1040
        d.pop('info', None)
1041
        user = AstakosUser(**d)
1042

    
1043
        return user
1044

    
1045
    @property
1046
    def realname(self):
1047
        return '%s %s' %(self.first_name, self.last_name)
1048

    
1049
    @realname.setter
1050
    def realname(self, value):
1051
        parts = value.split(' ')
1052
        if len(parts) == 2:
1053
            self.first_name = parts[0]
1054
            self.last_name = parts[1]
1055
        else:
1056
            self.last_name = parts[0]
1057

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

    
1069
    def generate_token(self):
1070
        self.password = self.third_party_identifier
1071
        self.last_login = datetime.now()
1072
        self.token = default_token_generator.make_token(self)
1073

    
1074
class SessionCatalog(models.Model):
1075
    session_key = models.CharField(_('session key'), max_length=40)
1076
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1077

    
1078

    
1079
### PROJECTS ###
1080
################
1081

    
1082
def synced_model_metaclass(class_name, class_parents, class_attributes):
1083

    
1084
    new_attributes = {}
1085
    sync_attributes = {}
1086

    
1087
    for name, value in class_attributes.iteritems():
1088
        sync, underscore, rest = name.partition('_')
1089
        if sync == 'sync' and underscore == '_':
1090
            sync_attributes[rest] = value
1091
        else:
1092
            new_attributes[name] = value
1093

    
1094
    if 'prefix' not in sync_attributes:
1095
        m = ("you did not specify a 'sync_prefix' attribute "
1096
             "in class '%s'" % (class_name,))
1097
        raise ValueError(m)
1098

    
1099
    prefix = sync_attributes.pop('prefix')
1100
    class_name = sync_attributes.pop('classname', prefix + '_model')
1101

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

    
1110
        new_attributes[newname] = value
1111

    
1112
    newclass = type(class_name, class_parents, new_attributes)
1113
    return newclass
1114

    
1115

    
1116
def make_synced(prefix='sync', name='SyncedState'):
1117

    
1118
    the_name = name
1119
    the_prefix = prefix
1120

    
1121
    class SyncedState(models.Model):
1122

    
1123
        sync_classname      = the_name
1124
        sync_prefix         = the_prefix
1125
        __metaclass__       = synced_model_metaclass
1126

    
1127
        sync_new_state      = models.BigIntegerField(null=True)
1128
        sync_synced_state   = models.BigIntegerField(null=True)
1129
        STATUS_SYNCED       = 0
1130
        STATUS_PENDING      = 1
1131
        sync_status         = models.IntegerField(db_index=True)
1132

    
1133
        class Meta:
1134
            abstract = True
1135

    
1136
        class NotSynced(Exception):
1137
            pass
1138

    
1139
        def sync_init_state(self, state):
1140
            self.sync_synced_state = state
1141
            self.sync_new_state = state
1142
            self.sync_status = self.STATUS_SYNCED
1143

    
1144
        def sync_get_status(self):
1145
            return self.sync_status
1146

    
1147
        def sync_set_status(self):
1148
            if self.sync_new_state != self.sync_synced_state:
1149
                self.sync_status = self.STATUS_PENDING
1150
            else:
1151
                self.sync_status = self.STATUS_SYNCED
1152

    
1153
        def sync_set_synced(self):
1154
            self.sync_synced_state = self.sync_new_state
1155
            self.sync_status = self.STATUS_SYNCED
1156

    
1157
        def sync_get_synced_state(self):
1158
            return self.sync_synced_state
1159

    
1160
        def sync_set_new_state(self, new_state):
1161
            self.sync_new_state = new_state
1162
            self.sync_set_status()
1163

    
1164
        def sync_get_new_state(self):
1165
            return self.sync_new_state
1166

    
1167
        def sync_set_synced_state(self, synced_state):
1168
            self.sync_synced_state = synced_state
1169
            self.sync_set_status()
1170

    
1171
        def sync_get_pending_objects(self):
1172
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1173
            return self.objects.filter(**kw)
1174

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

    
1179
        def sync_verify_get_synced_state(self):
1180
            status = self.sync_get_status()
1181
            state = self.sync_get_synced_state()
1182
            verified = (status == self.STATUS_SYNCED)
1183
            return state, verified
1184

    
1185
        def sync_is_synced(self):
1186
            state, verified = self.sync_verify_get_synced_state()
1187
            return verified
1188

    
1189
    return SyncedState
1190

    
1191
SyncedState = make_synced(prefix='sync', name='SyncedState')
1192

    
1193

    
1194
class ProjectApplicationManager(ForUpdateManager):
1195

    
1196
    def user_visible_projects(self, *filters, **kw_filters):
1197
        return self.filter(Q(state=ProjectApplication.PENDING)|\
1198
                           Q(state=ProjectApplication.APPROVED))
1199

    
1200
    def user_visible_by_chain(self, *filters, **kw_filters):
1201
        Q_PENDING = Q(state=ProjectApplication.PENDING)
1202
        Q_APPROVED = Q(state=ProjectApplication.APPROVED)
1203
        pending = self.filter(Q_PENDING).values_list('chain')
1204
        approved = self.filter(Q_APPROVED).values_list('chain')
1205
        by_chain = dict(pending.annotate(models.Max('id')))
1206
        by_chain.update(approved.annotate(models.Max('id')))
1207
        return self.filter(id__in=by_chain.values())
1208

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

    
1216
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1217

    
1218
    def search_by_name(self, *search_strings):
1219
        q = Q()
1220
        for s in search_strings:
1221
            q = q | Q(name__icontains=s)
1222
        return self.filter(q)
1223

    
1224

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

    
1234

    
1235
class Chain(models.Model):
1236
    chain  =   models.AutoField(primary_key=True)
1237

    
1238
def new_chain():
1239
    c = Chain.objects.create()
1240
    chain = c.chain
1241
    c.delete()
1242
    return chain
1243

    
1244

    
1245
class ProjectApplication(models.Model):
1246
    applicant               =   models.ForeignKey(
1247
                                    AstakosUser,
1248
                                    related_name='projects_applied',
1249
                                    db_index=True)
1250

    
1251
    PENDING     =    0
1252
    APPROVED    =    1
1253
    REPLACED    =    2
1254
    DENIED      =    3
1255
    DISMISSED   =    4
1256
    CANCELLED   =    5
1257

    
1258
    state                   =   models.IntegerField(default=PENDING)
1259

    
1260
    owner                   =   models.ForeignKey(
1261
                                    AstakosUser,
1262
                                    related_name='projects_owned',
1263
                                    db_index=True)
1264

    
1265
    chain                   =   models.IntegerField()
1266
    precursor_application   =   models.ForeignKey('ProjectApplication',
1267
                                                  null=True,
1268
                                                  blank=True)
1269

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

    
1287
    objects                 =   ProjectApplicationManager()
1288

    
1289
    class Meta:
1290
        unique_together = ("chain", "id")
1291

    
1292
    def __unicode__(self):
1293
        return "%s applied by %s" % (self.name, self.applicant)
1294

    
1295
    # TODO: Move to a more suitable place
1296
    PROJECT_STATE_DISPLAY = {
1297
        PENDING  : _('Pending review'),
1298
        APPROVED : _('Active'),
1299
        REPLACED : _('Replaced'),
1300
        DENIED   : _('Denied'),
1301
        DISMISSED: _('Dismissed'),
1302
        CANCELLED: _('Cancelled')
1303
    }
1304

    
1305
    def get_project(self):
1306
        try:
1307
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1308
            return Project
1309
        except Project.DoesNotExist, e:
1310
            return None
1311

    
1312
    def state_display(self):
1313
        return self.PROJECT_STATE_DISPLAY.get(self.state, _('Unknown'))
1314

    
1315
    def add_resource_policy(self, service, resource, uplimit):
1316
        """Raises ObjectDoesNotExist, IntegrityError"""
1317
        q = self.projectresourcegrant_set
1318
        resource = Resource.objects.get(service__name=service, name=resource)
1319
        q.create(resource=resource, member_capacity=uplimit)
1320

    
1321
    def user_status(self, user):
1322
        try:
1323
            project = self.get_project()
1324
            if not project:
1325
                return -1
1326
            membership = project.projectmembership_set
1327
            membership = membership.exclude(state=ProjectMembership.REMOVED)
1328
            membership = membership.get(person=user)
1329
            status = membership.state
1330
        except ProjectMembership.DoesNotExist:
1331
            return -1
1332

    
1333
        return status
1334

    
1335
    def user_status_display(self, user):
1336
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1337

    
1338
    def members_count(self):
1339
        return self.project.approved_memberships.count()
1340

    
1341
    @property
1342
    def grants(self):
1343
        return self.projectresourcegrant_set.values(
1344
            'member_capacity', 'resource__name', 'resource__service__name')
1345

    
1346
    @property
1347
    def resource_policies(self):
1348
        return self.projectresourcegrant_set.all()
1349

    
1350
    @resource_policies.setter
1351
    def resource_policies(self, policies):
1352
        for p in policies:
1353
            service = p.get('service', None)
1354
            resource = p.get('resource', None)
1355
            uplimit = p.get('uplimit', 0)
1356
            self.add_resource_policy(service, resource, uplimit)
1357

    
1358
    @property
1359
    def follower(self):
1360
        try:
1361
            return ProjectApplication.objects.get(precursor_application=self)
1362
        except ProjectApplication.DoesNotExist:
1363
            return
1364

    
1365
    def followers(self):
1366
        followers = self.chained_applications()
1367
        followers = followers.exclude(id=self.pk).filter(state=self.PENDING)
1368
        followers = followers.order_by('id')
1369
        return followers
1370

    
1371
    def last_follower(self):
1372
        try:
1373
            return self.followers().order_by('-id')[0]
1374
        except IndexError:
1375
            return None
1376

    
1377
    def is_modification(self):
1378
        parents = self.chained_applications().filter(id__lt=self.id)
1379
        parents = parents.filter(state__in=[self.APPROVED])
1380
        return parents.count() > 0
1381

    
1382
    def chained_applications(self):
1383
        return ProjectApplication.objects.filter(chain=self.chain)
1384

    
1385
    def has_pending_modifications(self):
1386
        return bool(self.last_follower())
1387

    
1388
    def get_project(self):
1389
        try:
1390
            return Project.objects.get(id=self.chain)
1391
        except Project.DoesNotExist:
1392
            return None
1393

    
1394
    def _get_project_for_update(self):
1395
        try:
1396
            objects = Project.objects.select_for_update()
1397
            project = objects.get(id=self.chain)
1398
            return project
1399
        except Project.DoesNotExist:
1400
            return None
1401

    
1402
    def cancel(self):
1403
        if self.state != self.PENDING:
1404
            m = _("cannot cancel: application '%s' in state '%s'") % (
1405
                    self.id, self.state)
1406
            raise AssertionError(m)
1407

    
1408
        self.state = self.CANCELLED
1409
        self.save()
1410

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

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

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

    
1426
        self.state = self.DENIED
1427
        self.response_date = datetime.now()
1428
        self.save()
1429

    
1430
    def approve(self, approval_user=None):
1431
        """
1432
        If approval_user then during owner membership acceptance
1433
        it is checked whether the request_user is eligible.
1434

1435
        Raises:
1436
            PermissionDenied
1437
        """
1438

    
1439
        if not transaction.is_managed():
1440
            raise AssertionError("NOPE")
1441

    
1442
        new_project_name = self.name
1443
        if self.state != self.PENDING:
1444
            m = _("cannot approve: project '%s' in state '%s'") % (
1445
                    new_project_name, self.state)
1446
            raise PermissionDenied(m) # invalid argument
1447

    
1448
        now = datetime.now()
1449
        project = self._get_project_for_update()
1450

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

    
1462
        new_project = False
1463
        if project is None:
1464
            new_project = True
1465
            project = Project(id=self.chain)
1466

    
1467
        project.name = new_project_name
1468
        project.application = self
1469
        project.last_approval_date = now
1470
        if not new_project:
1471
            project.is_modified = True
1472

    
1473
        project.save()
1474

    
1475
        self.state = self.APPROVED
1476
        self.response_date = now
1477
        self.save()
1478

    
1479
def submit_application(**kw):
1480

    
1481
    resource_policies = kw.pop('resource_policies', None)
1482
    application = ProjectApplication(**kw)
1483

    
1484
    precursor = kw['precursor_application']
1485

    
1486
    if precursor is None:
1487
        application.chain = new_chain()
1488
    else:
1489
        application.chain = precursor.chain
1490
        if precursor.state == ProjectApplication.PENDING:
1491
            precursor.state = ProjectApplication.REPLACED
1492
            precursor.save()
1493

    
1494
    application.save()
1495
    application.resource_policies = resource_policies
1496
    return application
1497

    
1498
class ProjectResourceGrant(models.Model):
1499

    
1500
    resource                =   models.ForeignKey(Resource)
1501
    project_application     =   models.ForeignKey(ProjectApplication,
1502
                                                  null=True)
1503
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1504
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1505
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1506
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1507
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1508
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1509

    
1510
    objects = ExtendedManager()
1511

    
1512
    class Meta:
1513
        unique_together = ("resource", "project_application")
1514

    
1515
    def member_quota_values(self):
1516
        return QuotaValues(
1517
            quantity = 0,
1518
            capacity = self.member_capacity,
1519
            import_limit = self.member_import_limit,
1520
            export_limit = self.member_export_limit)
1521

    
1522

    
1523
class ProjectManager(ForUpdateManager):
1524

    
1525
    def _q_terminated(self):
1526
        return Q(state=Project.TERMINATED)
1527
    def _q_suspended(self):
1528
        return Q(state=Project.SUSPENDED)
1529
    def _q_deactivated(self):
1530
        return self._q_terminated() | self._q_suspended()
1531

    
1532
    def terminated_projects(self):
1533
        q = self._q_terminated()
1534
        return self.filter(q)
1535

    
1536
    def not_terminated_projects(self):
1537
        q = ~self._q_terminated()
1538
        return self.filter(q)
1539

    
1540
    def terminating_projects(self):
1541
        q = self._q_terminated() & Q(is_active=True)
1542
        return self.filter(q)
1543

    
1544
    def deactivated_projects(self):
1545
        q = self._q_deactivated()
1546
        return self.filter(q)
1547

    
1548
    def deactivating_projects(self):
1549
        q = self._q_deactivated() & Q(is_active=True)
1550
        return self.filter(q)
1551

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

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

    
1558
class Project(models.Model):
1559

    
1560
    application                 =   models.OneToOneField(
1561
                                            ProjectApplication,
1562
                                            related_name='project')
1563
    last_approval_date          =   models.DateTimeField(null=True)
1564

    
1565
    members                     =   models.ManyToManyField(
1566
                                            AstakosUser,
1567
                                            through='ProjectMembership')
1568

    
1569
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1570
    deactivation_date           =   models.DateTimeField(null=True)
1571

    
1572
    creation_date               =   models.DateTimeField(auto_now_add=True)
1573
    name                        =   models.CharField(
1574
                                            max_length=80,
1575
                                            db_index=True,
1576
                                            unique=True)
1577

    
1578
    APPROVED    = 1
1579
    SUSPENDED   = 10
1580
    TERMINATED  = 100
1581

    
1582
    is_modified                 =   models.BooleanField(default=False,
1583
                                                        db_index=True)
1584
    is_active                   =   models.BooleanField(default=True,
1585
                                                        db_index=True)
1586
    state                       =   models.IntegerField(default=APPROVED,
1587
                                                        db_index=True)
1588

    
1589
    objects     =   ProjectManager()
1590

    
1591
    def __str__(self):
1592
        return _("<project %s '%s'>") % (self.id, self.application.name)
1593

    
1594
    __repr__ = __str__
1595

    
1596
    def is_deactivated(self, reason=None):
1597
        if reason is not None:
1598
            return self.state == reason
1599

    
1600
        return self.state != self.APPROVED
1601

    
1602
    def is_deactivating(self, reason=None):
1603
        if not self.is_active:
1604
            return False
1605

    
1606
        return self.is_deactivated(reason)
1607

    
1608
    def is_deactivated_strict(self, reason=None):
1609
        if self.is_active:
1610
            return False
1611

    
1612
        return self.is_deactivated(reason)
1613

    
1614
    ### Deactivation calls
1615

    
1616
    def deactivate(self):
1617
        self.deactivation_date = datetime.now()
1618
        self.is_active = False
1619

    
1620
    def reactivate(self):
1621
        self.deactivation_date = None
1622
        self.is_active = True
1623

    
1624
    def terminate(self):
1625
        self.deactivation_reason = 'TERMINATED'
1626
        self.state = self.TERMINATED
1627
        self.save()
1628

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

    
1634
    def resume(self):
1635
        self.deactivation_reason = None
1636
        self.state = self.APPROVED
1637
        self.save()
1638

    
1639
    ### Logical checks
1640

    
1641
    def is_inconsistent(self):
1642
        now = datetime.now()
1643
        dates = [self.creation_date,
1644
                 self.last_approval_date,
1645
                 self.deactivation_date]
1646
        return any([date > now for date in dates])
1647

    
1648
    def is_active_strict(self):
1649
        return self.is_active and self.state == self.APPROVED
1650

    
1651
    def is_approved(self):
1652
        return self.state == self.APPROVED
1653

    
1654
    @property
1655
    def is_alive(self):
1656
        return self.is_active_strict()
1657

    
1658
    @property
1659
    def is_terminated(self):
1660
        return self.is_deactivated(self.TERMINATED)
1661

    
1662
    @property
1663
    def is_suspended(self):
1664
        return self.is_deactivated(self.SUSPENDED)
1665

    
1666
    def violates_resource_grants(self):
1667
        return False
1668

    
1669
    def violates_members_limit(self, adding=0):
1670
        application = self.application
1671
        limit = application.limit_on_members_number
1672
        if limit is None:
1673
            return False
1674
        return (len(self.approved_members) + adding > limit)
1675

    
1676

    
1677
    ### Other
1678

    
1679
    def count_pending_memberships(self):
1680
        memb_set = self.projectmembership_set
1681
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1682
        return memb_count
1683

    
1684
    @property
1685
    def approved_memberships(self):
1686
        query = ProjectMembership.query_approved()
1687
        return self.projectmembership_set.filter(query)
1688

    
1689
    @property
1690
    def approved_members(self):
1691
        return [m.person for m in self.approved_memberships]
1692

    
1693
    def add_member(self, user):
1694
        """
1695
        Raises:
1696
            django.exceptions.PermissionDenied
1697
            astakos.im.models.AstakosUser.DoesNotExist
1698
        """
1699
        if isinstance(user, int):
1700
            user = AstakosUser.objects.get(user=user)
1701

    
1702
        m, created = ProjectMembership.objects.get_or_create(
1703
            person=user, project=self
1704
        )
1705
        m.accept()
1706

    
1707
    def remove_member(self, user):
1708
        """
1709
        Raises:
1710
            django.exceptions.PermissionDenied
1711
            astakos.im.models.AstakosUser.DoesNotExist
1712
            astakos.im.models.ProjectMembership.DoesNotExist
1713
        """
1714
        if isinstance(user, int):
1715
            user = AstakosUser.objects.get(user=user)
1716

    
1717
        m = ProjectMembership.objects.get(person=user, project=self)
1718
        m.remove()
1719

    
1720

    
1721
class PendingMembershipError(Exception):
1722
    pass
1723

    
1724

    
1725
class ProjectMembershipManager(ForUpdateManager):
1726
    pass
1727

    
1728
class ProjectMembership(models.Model):
1729

    
1730
    person              =   models.ForeignKey(AstakosUser)
1731
    request_date        =   models.DateField(auto_now_add=True)
1732
    project             =   models.ForeignKey(Project)
1733

    
1734
    REQUESTED           =   0
1735
    ACCEPTED            =   1
1736
    # User deactivation
1737
    USER_SUSPENDED      =   10
1738
    # Project deactivation
1739
    PROJECT_DEACTIVATED =   100
1740

    
1741
    REMOVED             =   200
1742

    
1743
    ASSOCIATED_STATES   =   set([REQUESTED,
1744
                                 ACCEPTED,
1745
                                 USER_SUSPENDED,
1746
                                 PROJECT_DEACTIVATED])
1747

    
1748
    ACCEPTED_STATES     =   set([ACCEPTED,
1749
                                 USER_SUSPENDED,
1750
                                 PROJECT_DEACTIVATED])
1751

    
1752
    state               =   models.IntegerField(default=REQUESTED,
1753
                                                db_index=True)
1754
    is_pending          =   models.BooleanField(default=False, db_index=True)
1755
    is_active           =   models.BooleanField(default=False, db_index=True)
1756
    application         =   models.ForeignKey(
1757
                                ProjectApplication,
1758
                                null=True,
1759
                                related_name='memberships')
1760
    pending_application =   models.ForeignKey(
1761
                                ProjectApplication,
1762
                                null=True,
1763
                                related_name='pending_memebrships')
1764
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1765

    
1766
    acceptance_date     =   models.DateField(null=True, db_index=True)
1767
    leave_request_date  =   models.DateField(null=True)
1768

    
1769
    objects     =   ProjectMembershipManager()
1770

    
1771

    
1772
    def get_combined_state(self):
1773
        return self.state, self.is_active, self.is_pending
1774

    
1775
    @classmethod
1776
    def query_approved(cls):
1777
        return (~Q(state=cls.REQUESTED) &
1778
                ~Q(state=cls.REMOVED))
1779

    
1780
    class Meta:
1781
        unique_together = ("person", "project")
1782
        #index_together = [["project", "state"]]
1783

    
1784
    def __str__(self):
1785
        return _("<'%s' membership in '%s'>") % (
1786
                self.person.username, self.project)
1787

    
1788
    __repr__ = __str__
1789

    
1790
    def __init__(self, *args, **kwargs):
1791
        self.state = self.REQUESTED
1792
        super(ProjectMembership, self).__init__(*args, **kwargs)
1793

    
1794
    def _set_history_item(self, reason, date=None):
1795
        if isinstance(reason, basestring):
1796
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1797

    
1798
        history_item = ProjectMembershipHistory(
1799
                            serial=self.id,
1800
                            person=self.person_id,
1801
                            project=self.project_id,
1802
                            date=date or datetime.now(),
1803
                            reason=reason)
1804
        history_item.save()
1805
        serial = history_item.id
1806

    
1807
    def accept(self):
1808
        if self.is_pending:
1809
            m = _("%s: attempt to accept while is pending") % (self,)
1810
            raise AssertionError(m)
1811

    
1812
        state = self.state
1813
        if state != self.REQUESTED:
1814
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1815
            raise AssertionError(m)
1816

    
1817
        now = datetime.now()
1818
        self.acceptance_date = now
1819
        self._set_history_item(reason='ACCEPT', date=now)
1820
        if self.project.is_approved():
1821
            self.state = self.ACCEPTED
1822
            self.is_pending = True
1823
        else:
1824
            self.state = self.PROJECT_DEACTIVATED
1825

    
1826
        self.save()
1827

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

    
1833
        state = self.state
1834
        if state not in self.ACCEPTED_STATES:
1835
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1836
            raise AssertionError(m)
1837

    
1838
        self._set_history_item(reason='REMOVE')
1839
        self.state = self.REMOVED
1840
        self.is_pending = True
1841
        self.save()
1842

    
1843
    def reject(self):
1844
        if self.is_pending:
1845
            m = _("%s: attempt to reject while is pending") % (self,)
1846
            raise AssertionError(m)
1847

    
1848
        state = self.state
1849
        if state != self.REQUESTED:
1850
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1851
            raise AssertionError(m)
1852

    
1853
        # rejected requests don't need sync,
1854
        # because they were never effected
1855
        self._set_history_item(reason='REJECT')
1856
        self.delete()
1857

    
1858
    def get_diff_quotas(self, sub_list=None, add_list=None):
1859
        if sub_list is None:
1860
            sub_list = []
1861

    
1862
        if add_list is None:
1863
            add_list = []
1864

    
1865
        sub_append = sub_list.append
1866
        add_append = add_list.append
1867
        holder = self.person.uuid
1868

    
1869
        synced_application = self.application
1870
        if synced_application is not None:
1871
            cur_grants = synced_application.projectresourcegrant_set.all()
1872
            for grant in cur_grants:
1873
                sub_append(QuotaLimits(
1874
                               holder       = holder,
1875
                               resource     = str(grant.resource),
1876
                               capacity     = grant.member_capacity,
1877
                               import_limit = grant.member_import_limit,
1878
                               export_limit = grant.member_export_limit))
1879

    
1880
        pending_application = self.pending_application
1881
        if pending_application is not None:
1882
            new_grants = pending_application.projectresourcegrant_set.all()
1883
            for new_grant in new_grants:
1884
                add_append(QuotaLimits(
1885
                               holder       = holder,
1886
                               resource     = str(new_grant.resource),
1887
                               capacity     = new_grant.member_capacity,
1888
                               import_limit = new_grant.member_import_limit,
1889
                               export_limit = new_grant.member_export_limit))
1890

    
1891
        return (sub_list, add_list)
1892

    
1893
    def set_sync(self):
1894
        if not self.is_pending:
1895
            m = _("%s: attempt to sync a non pending membership") % (self,)
1896
            raise AssertionError(m)
1897

    
1898
        state = self.state
1899
        if state == self.ACCEPTED:
1900
            pending_application = self.pending_application
1901
            if pending_application is None:
1902
                m = _("%s: attempt to sync an empty pending application") % (
1903
                    self,)
1904
                raise AssertionError(m)
1905

    
1906
            self.application = pending_application
1907
            self.is_active = True
1908

    
1909
            self.pending_application = None
1910
            self.pending_serial = None
1911

    
1912
            # project.application may have changed in the meantime,
1913
            # in which case we stay PENDING;
1914
            # we are safe to check due to select_for_update
1915
            if self.application == self.project.application:
1916
                self.is_pending = False
1917
            self.save()
1918

    
1919
        elif state == self.PROJECT_DEACTIVATED:
1920
            if self.pending_application:
1921
                m = _("%s: attempt to sync in state '%s' "
1922
                      "with a pending application") % (self, state)
1923
                raise AssertionError(m)
1924

    
1925
            self.application = None
1926
            self.pending_serial = None
1927
            self.is_pending = False
1928
            self.save()
1929

    
1930
        elif state == self.REMOVED:
1931
            self.delete()
1932

    
1933
        else:
1934
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1935
            raise AssertionError(m)
1936

    
1937
    def reset_sync(self):
1938
        if not self.is_pending:
1939
            m = _("%s: attempt to reset a non pending membership") % (self,)
1940
            raise AssertionError(m)
1941

    
1942
        state = self.state
1943
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
1944
            self.pending_application = None
1945
            self.pending_serial = None
1946
            self.save()
1947
        else:
1948
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1949
            raise AssertionError(m)
1950

    
1951
class Serial(models.Model):
1952
    serial  =   models.AutoField(primary_key=True)
1953

    
1954
def new_serial():
1955
    s = Serial.objects.create()
1956
    serial = s.serial
1957
    s.delete()
1958
    return serial
1959

    
1960
def sync_finish_serials(serials_to_ack=None):
1961
    if serials_to_ack is None:
1962
        serials_to_ack = qh_query_serials([])
1963

    
1964
    serials_to_ack = set(serials_to_ack)
1965
    sfu = ProjectMembership.objects.select_for_update()
1966
    memberships = list(sfu.filter(pending_serial__isnull=False))
1967

    
1968
    if memberships:
1969
        for membership in memberships:
1970
            serial = membership.pending_serial
1971
            if serial in serials_to_ack:
1972
                membership.set_sync()
1973
            else:
1974
                membership.reset_sync()
1975

    
1976
        transaction.commit()
1977

    
1978
    qh_ack_serials(list(serials_to_ack))
1979
    return len(memberships)
1980

    
1981
def pre_sync():
1982
    ACCEPTED = ProjectMembership.ACCEPTED
1983
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
1984
    psfu = Project.objects.select_for_update()
1985

    
1986
    modified = psfu.modified_projects()
1987
    for project in modified:
1988
        objects = project.projectmembership_set.select_for_update()
1989

    
1990
        memberships = objects.filter(state=ACCEPTED)
1991
        for membership in memberships:
1992
            membership.is_pending = True
1993
            membership.save()
1994

    
1995
    reactivating = psfu.reactivating_projects()
1996
    for project in reactivating:
1997
        objects = project.projectmembership_set.select_for_update()
1998
        memberships = objects.filter(state=PROJECT_DEACTIVATED)
1999
        for membership in memberships:
2000
            membership.is_pending = True
2001
            membership.state = ACCEPTED
2002
            membership.save()
2003

    
2004
    deactivating = psfu.deactivating_projects()
2005
    for project in deactivating:
2006
        objects = project.projectmembership_set.select_for_update()
2007

    
2008
        # Note: we keep a user-level deactivation (e.g. USER_SUSPENDED) intact
2009
        memberships = objects.filter(state=ACCEPTED)
2010
        for membership in memberships:
2011
            membership.is_pending = True
2012
            membership.state = PROJECT_DEACTIVATED
2013
            membership.save()
2014

    
2015
def do_sync():
2016

    
2017
    ACCEPTED = ProjectMembership.ACCEPTED
2018
    objects = ProjectMembership.objects.select_for_update()
2019

    
2020
    sub_quota, add_quota = [], []
2021

    
2022
    serial = new_serial()
2023

    
2024
    pending = objects.filter(is_pending=True)
2025
    for membership in pending:
2026

    
2027
        if membership.pending_application:
2028
            m = "%s: impossible: pending_application is not None (%s)" % (
2029
                membership, membership.pending_application)
2030
            raise AssertionError(m)
2031
        if membership.pending_serial:
2032
            m = "%s: impossible: pending_serial is not None (%s)" % (
2033
                membership, membership.pending_serial)
2034
            raise AssertionError(m)
2035

    
2036
        if membership.state == ACCEPTED:
2037
            membership.pending_application = membership.project.application
2038

    
2039
        membership.pending_serial = serial
2040
        membership.get_diff_quotas(sub_quota, add_quota)
2041
        membership.save()
2042

    
2043
    transaction.commit()
2044
    # ProjectApplication.approve() unblocks here
2045
    # and can set PENDING an already PENDING membership
2046
    # which has been scheduled to sync with the old project.application
2047
    # Need to check in ProjectMembership.set_sync()
2048

    
2049
    r = qh_add_quota(serial, sub_quota, add_quota)
2050
    if r:
2051
        m = "cannot sync serial: %d" % serial
2052
        raise RuntimeError(m)
2053

    
2054
    return serial
2055

    
2056
def post_sync():
2057
    ACCEPTED = ProjectMembership.ACCEPTED
2058
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2059
    psfu = Project.objects.select_for_update()
2060

    
2061
    modified = psfu.modified_projects()
2062
    for project in modified:
2063
        objects = project.projectmembership_set.select_for_update()
2064

    
2065
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2066
        if not memberships:
2067
            project.is_modified = False
2068
            project.save()
2069

    
2070
    reactivating = psfu.reactivating_projects()
2071
    for project in reactivating:
2072
        objects = project.projectmembership_set.select_for_update()
2073
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2074
                                          Q(is_pending=True)))
2075
        if not memberships:
2076
            project.reactivate()
2077
            project.save()
2078

    
2079
    deactivating = psfu.deactivating_projects()
2080
    for project in deactivating:
2081
        objects = project.projectmembership_set.select_for_update()
2082

    
2083
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2084
                                          Q(is_pending=True)))
2085
        if not memberships:
2086
            project.deactivate()
2087
            project.save()
2088

    
2089
    transaction.commit()
2090

    
2091
def sync_projects():
2092
    sync_finish_serials()
2093
    pre_sync()
2094
    serial = do_sync()
2095
    sync_finish_serials([serial])
2096
    post_sync()
2097

    
2098
def trigger_sync(retries=3, retry_wait=1.0):
2099
    transaction.commit()
2100

    
2101
    cursor = connection.cursor()
2102
    locked = True
2103
    try:
2104
        while 1:
2105
            cursor.execute("SELECT pg_try_advisory_lock(1)")
2106
            r = cursor.fetchone()
2107
            if r is None:
2108
                m = "Impossible"
2109
                raise AssertionError(m)
2110
            locked = r[0]
2111
            if locked:
2112
                break
2113

    
2114
            retries -= 1
2115
            if retries <= 0:
2116
                return False
2117
            sleep(retry_wait)
2118

    
2119
        sync_projects()
2120
        return True
2121

    
2122
    finally:
2123
        if locked:
2124
            cursor.execute("SELECT pg_advisory_unlock(1)")
2125
            cursor.fetchall()
2126

    
2127

    
2128
class ProjectMembershipHistory(models.Model):
2129
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2130
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2131

    
2132
    person  =   models.BigIntegerField()
2133
    project =   models.BigIntegerField()
2134
    date    =   models.DateField(auto_now_add=True)
2135
    reason  =   models.IntegerField()
2136
    serial  =   models.BigIntegerField()
2137

    
2138
### SIGNALS ###
2139
################
2140

    
2141
def create_astakos_user(u):
2142
    try:
2143
        AstakosUser.objects.get(user_ptr=u.pk)
2144
    except AstakosUser.DoesNotExist:
2145
        extended_user = AstakosUser(user_ptr_id=u.pk)
2146
        extended_user.__dict__.update(u.__dict__)
2147
        extended_user.save()
2148
        if not extended_user.has_auth_provider('local'):
2149
            extended_user.add_auth_provider('local')
2150
    except BaseException, e:
2151
        logger.exception(e)
2152

    
2153

    
2154
def fix_superusers(sender, **kwargs):
2155
    # Associate superusers with AstakosUser
2156
    admins = User.objects.filter(is_superuser=True)
2157
    for u in admins:
2158
        create_astakos_user(u)
2159
post_syncdb.connect(fix_superusers)
2160

    
2161

    
2162
def user_post_save(sender, instance, created, **kwargs):
2163
    if not created:
2164
        return
2165
    create_astakos_user(instance)
2166
post_save.connect(user_post_save, sender=User)
2167

    
2168
def astakosuser_post_save(sender, instance, created, **kwargs):
2169
    pass
2170

    
2171
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2172

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

    
2176
post_save.connect(resource_post_save, sender=Resource)
2177

    
2178
def renew_token(sender, instance, **kwargs):
2179
    if not instance.auth_token:
2180
        instance.renew_token()
2181
pre_save.connect(renew_token, sender=AstakosUser)
2182
pre_save.connect(renew_token, sender=Service)
2183