Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 69ab4df9

History | View | Annotate | Download (73.3 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(provider)
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
                                                    db_index=True)
1260

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

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

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

    
1288
    objects                 =   ProjectApplicationManager()
1289

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

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

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

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

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

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

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

    
1334
        return status
1335

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1436
        Raises:
1437
            PermissionDenied
1438
        """
1439

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

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

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

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

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

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

    
1474
        project.save()
1475

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

    
1480
def submit_application(**kw):
1481

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

    
1485
    precursor = kw['precursor_application']
1486

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

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

    
1499
class ProjectResourceGrant(models.Model):
1500

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

    
1511
    objects = ExtendedManager()
1512

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

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

    
1523

    
1524
class ProjectManager(ForUpdateManager):
1525

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

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

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

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

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

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

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

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

    
1559
class Project(models.Model):
1560

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

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

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

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

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

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

    
1590
    objects     =   ProjectManager()
1591

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

    
1595
    __repr__ = __str__
1596

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

    
1601
        return self.state != self.APPROVED
1602

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

    
1607
        return self.is_deactivated(reason)
1608

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

    
1613
        return self.is_deactivated(reason)
1614

    
1615
    ### Deactivation calls
1616

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

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

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

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

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

    
1640
    ### Logical checks
1641

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

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

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

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

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

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

    
1667
    def violates_resource_grants(self):
1668
        return False
1669

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

    
1677

    
1678
    ### Other
1679

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

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

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

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

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

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

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

    
1721

    
1722
class PendingMembershipError(Exception):
1723
    pass
1724

    
1725

    
1726
class ProjectMembershipManager(ForUpdateManager):
1727
    pass
1728

    
1729
class ProjectMembership(models.Model):
1730

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

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

    
1742
    REMOVED             =   200
1743

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

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

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

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

    
1770
    objects     =   ProjectMembershipManager()
1771

    
1772

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

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

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

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

    
1789
    __repr__ = __str__
1790

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

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

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

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

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

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

    
1827
        self.save()
1828

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

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

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

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

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

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

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

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

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

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

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

    
1892
        return (sub_list, add_list)
1893

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1977
        transaction.commit()
1978

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

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

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

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

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

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

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

    
2016
def do_sync():
2017

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

    
2021
    sub_quota, add_quota = [], []
2022

    
2023
    serial = new_serial()
2024

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

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

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

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

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

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

    
2055
    return serial
2056

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

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

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

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

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

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

    
2090
    transaction.commit()
2091

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

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

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

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

    
2120
        sync_projects()
2121
        return True
2122

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

    
2128

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

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

    
2139
### SIGNALS ###
2140
################
2141

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

    
2154

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

    
2162

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

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

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

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

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

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