Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 140da2d1

History | View | Annotate | Download (75.5 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_users, register_quotas, qh_check_users, qh_get_quota_limits,
74
    register_services, register_resources, qh_add_quota, QuotaLimits,
75
    qh_query_serials, qh_ack_serials,
76
    QuotaValues, add_quota_values)
77
from astakos.im import auth_providers
78

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

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

    
85
logger = logging.getLogger(__name__)
86

    
87
DEFAULT_CONTENT_TYPE = None
88
_content_type = None
89

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

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

    
102
RESOURCE_SEPARATOR = '.'
103

    
104
inf = float('inf')
105

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

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

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

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

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

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

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

    
145

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

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

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

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

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

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

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

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

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

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

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

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

    
202

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

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

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

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

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

    
245
    return _DEFAULT_QUOTA
246

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

    
253

    
254
class AstakosUserManager(UserManager):
255

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

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

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

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

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

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

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

    
289

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

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

    
306

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

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

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

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

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

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

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

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

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

    
342
    objects = AstakosUserManager()
343

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

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

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

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

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

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

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

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

    
396
        objects = self.projectmembership_set.select_related()
397
        memberships = objects.filter(is_active=True)
398
        for membership in memberships:
399
            application = membership.application
400
            if application is None:
401
                m = _("missing application for active membership %s"
402
                      % (membership,))
403
                raise AssertionError(m)
404

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

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

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

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

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

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

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

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

    
471
        self.update_uuid()
472

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
580
        return True
581

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

    
587
        if len(existing) <= 1:
588
            return False
589

    
590
        if len(existing_for_provider) == 1 and provider.is_required():
591
            return False
592

    
593
        return True
594

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

    
598
    def can_change_email(self):
599
        non_astakos_local = self.get_auth_providers().filter(module='local')
600
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
601
        return non_astakos_local.count() == 0
602

    
603
    def has_required_auth_providers(self):
604
        required = auth_providers.REQUIRED_PROVIDERS
605
        for provider in required:
606
            if not self.has_auth_provider(provider):
607
                return False
608
        return True
609

    
610
    def has_auth_provider(self, provider, **kwargs):
611
        return bool(self.get_auth_providers().filter(module=provider,
612
                                               **kwargs).count())
613

    
614
    def add_auth_provider(self, provider, **kwargs):
615
        info_data = ''
616
        if 'provider_info' in kwargs:
617
            info_data = kwargs.pop('provider_info')
618
            if isinstance(info_data, dict):
619
                info_data = json.dumps(info_data)
620

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

    
633
    def add_pending_auth_provider(self, pending):
634
        """
635
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
636
        the current user.
637
        """
638
        if not isinstance(pending, PendingThirdPartyUser):
639
            pending = PendingThirdPartyUser.objects.get(token=pending)
640

    
641
        provider = self.add_auth_provider(pending.provider,
642
                               identifier=pending.third_party_identifier,
643
                                affiliation=pending.affiliation,
644
                                          provider_info=pending.info)
645

    
646
        if email_re.match(pending.email or '') and pending.email != self.email:
647
            self.additionalmail_set.get_or_create(email=pending.email)
648

    
649
        pending.delete()
650
        return provider
651

    
652
    def remove_auth_provider(self, provider, **kwargs):
653
        self.get_auth_providers().get(module=provider, **kwargs).delete()
654

    
655
    # user urls
656
    def get_resend_activation_url(self):
657
        return reverse('send_activation', kwargs={'user_id': self.pk})
658

    
659
    def get_provider_remove_url(self, module, **kwargs):
660
        return reverse('remove_auth_provider', kwargs={
661
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
662

    
663
    def get_activation_url(self, nxt=False):
664
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
665
                                 quote(self.auth_token))
666
        if nxt:
667
            url += "&next=%s" % quote(nxt)
668
        return url
669

    
670
    def get_password_reset_url(self, token_generator=default_token_generator):
671
        return reverse('django.contrib.auth.views.password_reset_confirm',
672
                          kwargs={'uidb36':int_to_base36(self.id),
673
                                  'token':token_generator.make_token(self)})
674

    
675
    def get_auth_providers(self):
676
        return self.auth_providers
677

    
678
    def get_available_auth_providers(self):
679
        """
680
        Returns a list of providers available for user to connect to.
681
        """
682
        providers = []
683
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
684
            if self.can_add_auth_provider(module):
685
                providers.append(provider_settings(self))
686

    
687
        return providers
688

    
689
    def get_active_auth_providers(self, **filters):
690
        providers = []
691
        for provider in self.get_auth_providers().active(**filters):
692
            if auth_providers.get_provider(provider.module).is_available_for_login():
693
                providers.append(provider)
694
        return providers
695

    
696
    @property
697
    def auth_providers_display(self):
698
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
699

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

    
725
        return mark_safe(message + u' '+ msg_extra)
726

    
727
    def owns_project(self, project):
728
        return project.owner == self
729

    
730
    def is_project_member(self, project_or_application):
731
        return self.get_status_in_project(project_or_application) in \
732
                                        ProjectMembership.ASSOCIATED_STATES
733

    
734
    def is_project_accepted_member(self, project_or_application):
735
        return self.get_status_in_project(project_or_application) in \
736
                                            ProjectMembership.ACCEPTED_STATES
737

    
738
    def get_status_in_project(self, project_or_application):
739
        application = project_or_application
740
        if isinstance(project_or_application, Project):
741
            application = project_or_application.project
742
        return application.user_status(self)
743

    
744

    
745
class AstakosUserAuthProviderManager(models.Manager):
746

    
747
    def active(self, **filters):
748
        return self.filter(active=True, **filters)
749

    
750
    def remove_unverified_providers(self, provider, **filters):
751
        try:
752
            existing = self.filter(module=provider, user__email_verified=False, **filters)
753
            for p in existing:
754
                p.user.delete()
755
        except:
756
            pass
757

    
758

    
759

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

    
778
    objects = AstakosUserAuthProviderManager()
779

    
780
    class Meta:
781
        unique_together = (('identifier', 'module', 'user'), )
782
        ordering = ('module', 'created')
783

    
784
    def __init__(self, *args, **kwargs):
785
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
786
        try:
787
            self.info = json.loads(self.info_data)
788
            if not self.info:
789
                self.info = {}
790
        except Exception, e:
791
            self.info = {}
792

    
793
        for key,value in self.info.iteritems():
794
            setattr(self, 'info_%s' % key, value)
795

    
796

    
797
    @property
798
    def settings(self):
799
        return auth_providers.get_provider(self.module)
800

    
801
    @property
802
    def details_display(self):
803
        try:
804
          return self.settings.get_details_tpl_display % self.__dict__
805
        except:
806
          return ''
807

    
808
    @property
809
    def title_display(self):
810
        title_tpl = self.settings.get_title_display
811
        try:
812
            if self.settings.get_user_title_display:
813
                title_tpl = self.settings.get_user_title_display
814
        except Exception, e:
815
            pass
816
        try:
817
          return title_tpl % self.__dict__
818
        except:
819
          return self.settings.get_title_display % self.__dict__
820

    
821
    def can_remove(self):
822
        return self.user.can_remove_auth_provider(self.module)
823

    
824
    def delete(self, *args, **kwargs):
825
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
826
        if self.module == 'local':
827
            self.user.set_unusable_password()
828
            self.user.save()
829
        return ret
830

    
831
    def __repr__(self):
832
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
833

    
834
    def __unicode__(self):
835
        if self.identifier:
836
            return "%s:%s" % (self.module, self.identifier)
837
        if self.auth_backend:
838
            return "%s:%s" % (self.module, self.auth_backend)
839
        return self.module
840

    
841
    def save(self, *args, **kwargs):
842
        self.info_data = json.dumps(self.info)
843
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
844

    
845

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

    
873
    update_or_create = _update_or_create
874

    
875

    
876
class AstakosUserQuota(models.Model):
877
    objects = ExtendedManager()
878
    capacity = intDecimalField()
879
    quantity = intDecimalField(default=0)
880
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
881
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
882
    resource = models.ForeignKey(Resource)
883
    user = models.ForeignKey(AstakosUser)
884

    
885
    class Meta:
886
        unique_together = ("resource", "user")
887

    
888
    def quota_values(self):
889
        return QuotaValues(
890
            quantity = self.quantity,
891
            capacity = self.capacity,
892
            import_limit = self.import_limit,
893
            export_limit = self.export_limit)
894

    
895

    
896
class ApprovalTerms(models.Model):
897
    """
898
    Model for approval terms
899
    """
900

    
901
    date = models.DateTimeField(
902
        _('Issue date'), db_index=True, auto_now_add=True)
903
    location = models.CharField(_('Terms location'), max_length=255)
904

    
905

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

    
919
    def __init__(self, *args, **kwargs):
920
        super(Invitation, self).__init__(*args, **kwargs)
921
        if not self.id:
922
            self.code = _generate_invitation_code()
923

    
924
    def consume(self):
925
        self.is_consumed = True
926
        self.consumed = datetime.now()
927
        self.save()
928

    
929
    def __unicode__(self):
930
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
931

    
932

    
933
class EmailChangeManager(models.Manager):
934

    
935
    @transaction.commit_on_success
936
    def change_email(self, activation_key):
937
        """
938
        Validate an activation key and change the corresponding
939
        ``User`` if valid.
940

941
        If the key is valid and has not expired, return the ``User``
942
        after activating.
943

944
        If the key is not valid or has expired, return ``None``.
945

946
        If the key is valid but the ``User`` is already active,
947
        return ``None``.
948

949
        After successful email change the activation record is deleted.
950

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

    
979

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

    
990
    objects = EmailChangeManager()
991

    
992
    def get_url(self):
993
        return reverse('email_change_confirm',
994
                      kwargs={'activation_key': self.activation_key})
995

    
996
    def activation_key_expired(self):
997
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
998
        return self.requested_at + expiration_date < datetime.now()
999

    
1000

    
1001
class AdditionalMail(models.Model):
1002
    """
1003
    Model for registring invitations
1004
    """
1005
    owner = models.ForeignKey(AstakosUser)
1006
    email = models.EmailField()
1007

    
1008

    
1009
def _generate_invitation_code():
1010
    while True:
1011
        code = randint(1, 2L ** 63 - 1)
1012
        try:
1013
            Invitation.objects.get(code=code)
1014
            # An invitation with this code already exists, try again
1015
        except Invitation.DoesNotExist:
1016
            return code
1017

    
1018

    
1019
def get_latest_terms():
1020
    try:
1021
        term = ApprovalTerms.objects.order_by('-id')[0]
1022
        return term
1023
    except IndexError:
1024
        pass
1025
    return None
1026

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

    
1045
    class Meta:
1046
        unique_together = ("provider", "third_party_identifier")
1047

    
1048
    def get_user_instance(self):
1049
        d = self.__dict__
1050
        d.pop('_state', None)
1051
        d.pop('id', None)
1052
        d.pop('token', None)
1053
        d.pop('created', None)
1054
        d.pop('info', None)
1055
        user = AstakosUser(**d)
1056

    
1057
        return user
1058

    
1059
    @property
1060
    def realname(self):
1061
        return '%s %s' %(self.first_name, self.last_name)
1062

    
1063
    @realname.setter
1064
    def realname(self, value):
1065
        parts = value.split(' ')
1066
        if len(parts) == 2:
1067
            self.first_name = parts[0]
1068
            self.last_name = parts[1]
1069
        else:
1070
            self.last_name = parts[0]
1071

    
1072
    def save(self, **kwargs):
1073
        if not self.id:
1074
            # set username
1075
            while not self.username:
1076
                username =  uuid.uuid4().hex[:30]
1077
                try:
1078
                    AstakosUser.objects.get(username = username)
1079
                except AstakosUser.DoesNotExist, e:
1080
                    self.username = username
1081
        super(PendingThirdPartyUser, self).save(**kwargs)
1082

    
1083
    def generate_token(self):
1084
        self.password = self.third_party_identifier
1085
        self.last_login = datetime.now()
1086
        self.token = default_token_generator.make_token(self)
1087

    
1088
class SessionCatalog(models.Model):
1089
    session_key = models.CharField(_('session key'), max_length=40)
1090
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1091

    
1092

    
1093
### PROJECTS ###
1094
################
1095

    
1096
def synced_model_metaclass(class_name, class_parents, class_attributes):
1097

    
1098
    new_attributes = {}
1099
    sync_attributes = {}
1100

    
1101
    for name, value in class_attributes.iteritems():
1102
        sync, underscore, rest = name.partition('_')
1103
        if sync == 'sync' and underscore == '_':
1104
            sync_attributes[rest] = value
1105
        else:
1106
            new_attributes[name] = value
1107

    
1108
    if 'prefix' not in sync_attributes:
1109
        m = ("you did not specify a 'sync_prefix' attribute "
1110
             "in class '%s'" % (class_name,))
1111
        raise ValueError(m)
1112

    
1113
    prefix = sync_attributes.pop('prefix')
1114
    class_name = sync_attributes.pop('classname', prefix + '_model')
1115

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

    
1124
        new_attributes[newname] = value
1125

    
1126
    newclass = type(class_name, class_parents, new_attributes)
1127
    return newclass
1128

    
1129

    
1130
def make_synced(prefix='sync', name='SyncedState'):
1131

    
1132
    the_name = name
1133
    the_prefix = prefix
1134

    
1135
    class SyncedState(models.Model):
1136

    
1137
        sync_classname      = the_name
1138
        sync_prefix         = the_prefix
1139
        __metaclass__       = synced_model_metaclass
1140

    
1141
        sync_new_state      = models.BigIntegerField(null=True)
1142
        sync_synced_state   = models.BigIntegerField(null=True)
1143
        STATUS_SYNCED       = 0
1144
        STATUS_PENDING      = 1
1145
        sync_status         = models.IntegerField(db_index=True)
1146

    
1147
        class Meta:
1148
            abstract = True
1149

    
1150
        class NotSynced(Exception):
1151
            pass
1152

    
1153
        def sync_init_state(self, state):
1154
            self.sync_synced_state = state
1155
            self.sync_new_state = state
1156
            self.sync_status = self.STATUS_SYNCED
1157

    
1158
        def sync_get_status(self):
1159
            return self.sync_status
1160

    
1161
        def sync_set_status(self):
1162
            if self.sync_new_state != self.sync_synced_state:
1163
                self.sync_status = self.STATUS_PENDING
1164
            else:
1165
                self.sync_status = self.STATUS_SYNCED
1166

    
1167
        def sync_set_synced(self):
1168
            self.sync_synced_state = self.sync_new_state
1169
            self.sync_status = self.STATUS_SYNCED
1170

    
1171
        def sync_get_synced_state(self):
1172
            return self.sync_synced_state
1173

    
1174
        def sync_set_new_state(self, new_state):
1175
            self.sync_new_state = new_state
1176
            self.sync_set_status()
1177

    
1178
        def sync_get_new_state(self):
1179
            return self.sync_new_state
1180

    
1181
        def sync_set_synced_state(self, synced_state):
1182
            self.sync_synced_state = synced_state
1183
            self.sync_set_status()
1184

    
1185
        def sync_get_pending_objects(self):
1186
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1187
            return self.objects.filter(**kw)
1188

    
1189
        def sync_get_synced_objects(self):
1190
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1191
            return self.objects.filter(**kw)
1192

    
1193
        def sync_verify_get_synced_state(self):
1194
            status = self.sync_get_status()
1195
            state = self.sync_get_synced_state()
1196
            verified = (status == self.STATUS_SYNCED)
1197
            return state, verified
1198

    
1199
        def sync_is_synced(self):
1200
            state, verified = self.sync_verify_get_synced_state()
1201
            return verified
1202

    
1203
    return SyncedState
1204

    
1205
SyncedState = make_synced(prefix='sync', name='SyncedState')
1206

    
1207

    
1208
class ProjectApplicationManager(ForUpdateManager):
1209

    
1210
    def user_visible_projects(self, *filters, **kw_filters):
1211
        model = self.model
1212
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1213

    
1214
    def user_visible_by_chain(self, *filters, **kw_filters):
1215
        model = self.model
1216
        pending = self.filter(model.Q_PENDING).values_list('chain')
1217
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1218
        by_chain = dict(pending.annotate(models.Max('id')))
1219
        by_chain.update(approved.annotate(models.Max('id')))
1220
        return self.filter(id__in=by_chain.values())
1221

    
1222
    def user_accessible_projects(self, user):
1223
        """
1224
        Return projects accessed by specified user.
1225
        """
1226
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1227
                               Q(project__projectmembership__person=user)
1228

    
1229
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1230

    
1231
    def search_by_name(self, *search_strings):
1232
        q = Q()
1233
        for s in search_strings:
1234
            q = q | Q(name__icontains=s)
1235
        return self.filter(q)
1236

    
1237

    
1238
USER_STATUS_DISPLAY = {
1239
      0: _('Join requested'),
1240
      1: _('Accepted member'),
1241
     10: _('Suspended'),
1242
    100: _('Terminated'),
1243
    200: _('Removed'),
1244
     -1: _('Not a member'),
1245
}
1246

    
1247

    
1248
class Chain(models.Model):
1249
    chain  =   models.AutoField(primary_key=True)
1250

    
1251
def new_chain():
1252
    c = Chain.objects.create()
1253
    chain = c.chain
1254
    c.delete()
1255
    return chain
1256

    
1257

    
1258
class ProjectApplication(models.Model):
1259
    applicant               =   models.ForeignKey(
1260
                                    AstakosUser,
1261
                                    related_name='projects_applied',
1262
                                    db_index=True)
1263

    
1264
    PENDING     =    0
1265
    APPROVED    =    1
1266
    REPLACED    =    2
1267
    DENIED      =    3
1268
    DISMISSED   =    4
1269
    CANCELLED   =    5
1270

    
1271
    state                   =   models.IntegerField(default=PENDING,
1272
                                                    db_index=True)
1273

    
1274
    owner                   =   models.ForeignKey(
1275
                                    AstakosUser,
1276
                                    related_name='projects_owned',
1277
                                    db_index=True)
1278

    
1279
    chain                   =   models.IntegerField()
1280
    precursor_application   =   models.ForeignKey('ProjectApplication',
1281
                                                  null=True,
1282
                                                  blank=True)
1283

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

    
1301
    objects                 =   ProjectApplicationManager()
1302

    
1303
    # Compiled queries
1304
    Q_PENDING  = Q(state=PENDING)
1305
    Q_APPROVED = Q(state=APPROVED)
1306

    
1307
    class Meta:
1308
        unique_together = ("chain", "id")
1309

    
1310
    def __unicode__(self):
1311
        return "%s applied by %s" % (self.name, self.applicant)
1312

    
1313
    # TODO: Move to a more suitable place
1314
    APPLICATION_STATE_DISPLAY = {
1315
        PENDING  : _('Pending review'),
1316
        APPROVED : _('Active'),
1317
        REPLACED : _('Replaced'),
1318
        DENIED   : _('Denied'),
1319
        DISMISSED: _('Dismissed'),
1320
        CANCELLED: _('Cancelled')
1321
    }
1322

    
1323
    def get_project(self):
1324
        try:
1325
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1326
            return Project
1327
        except Project.DoesNotExist, e:
1328
            return None
1329

    
1330
    def state_display(self):
1331
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1332

    
1333
    def add_resource_policy(self, service, resource, uplimit):
1334
        """Raises ObjectDoesNotExist, IntegrityError"""
1335
        q = self.projectresourcegrant_set
1336
        resource = Resource.objects.get(service__name=service, name=resource)
1337
        q.create(resource=resource, member_capacity=uplimit)
1338

    
1339
    def user_status(self, user):
1340
        try:
1341
            project = self.get_project()
1342
            if not project:
1343
                return -1
1344
            membership = project.projectmembership_set
1345
            membership = membership.exclude(state=ProjectMembership.REMOVED)
1346
            membership = membership.get(person=user)
1347
            status = membership.state
1348
        except ProjectMembership.DoesNotExist:
1349
            return -1
1350

    
1351
        return status
1352

    
1353
    def user_status_display(self, user):
1354
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1355

    
1356
    def members_count(self):
1357
        return self.project.approved_memberships.count()
1358

    
1359
    @property
1360
    def grants(self):
1361
        return self.projectresourcegrant_set.values(
1362
            'member_capacity', 'resource__name', 'resource__service__name')
1363

    
1364
    @property
1365
    def resource_policies(self):
1366
        return self.projectresourcegrant_set.all()
1367

    
1368
    @resource_policies.setter
1369
    def resource_policies(self, policies):
1370
        for p in policies:
1371
            service = p.get('service', None)
1372
            resource = p.get('resource', None)
1373
            uplimit = p.get('uplimit', 0)
1374
            self.add_resource_policy(service, resource, uplimit)
1375

    
1376
    @property
1377
    def follower(self):
1378
        try:
1379
            return ProjectApplication.objects.get(precursor_application=self)
1380
        except ProjectApplication.DoesNotExist:
1381
            return
1382

    
1383
    def followers(self):
1384
        followers = self.chained_applications()
1385
        followers = followers.exclude(id=self.pk).filter(state=self.PENDING)
1386
        followers = followers.order_by('id')
1387
        return followers
1388

    
1389
    def last_follower(self):
1390
        try:
1391
            return self.followers().order_by('-id')[0]
1392
        except IndexError:
1393
            return None
1394

    
1395
    def is_modification(self):
1396
        parents = self.chained_applications().filter(id__lt=self.id)
1397
        parents = parents.filter(state__in=[self.APPROVED])
1398
        return parents.count() > 0
1399

    
1400
    def chained_applications(self):
1401
        return ProjectApplication.objects.filter(chain=self.chain)
1402

    
1403
    def has_pending_modifications(self):
1404
        return bool(self.last_follower())
1405

    
1406
    def get_project(self):
1407
        try:
1408
            return Project.objects.get(id=self.chain)
1409
        except Project.DoesNotExist:
1410
            return None
1411

    
1412
    def _get_project_for_update(self):
1413
        try:
1414
            objects = Project.objects.select_for_update()
1415
            project = objects.get(id=self.chain)
1416
            return project
1417
        except Project.DoesNotExist:
1418
            return None
1419

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

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

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

    
1435
        self.state = self.DISMISSED
1436
        self.save()
1437

    
1438
    def deny(self):
1439
        if self.state != self.PENDING:
1440
            m = _("cannot deny: application '%s' in state '%s'") % (
1441
                    self.id, self.state)
1442
            raise AssertionError(m)
1443

    
1444
        self.state = self.DENIED
1445
        self.response_date = datetime.now()
1446
        self.save()
1447

    
1448
    def approve(self, approval_user=None):
1449
        """
1450
        If approval_user then during owner membership acceptance
1451
        it is checked whether the request_user is eligible.
1452

1453
        Raises:
1454
            PermissionDenied
1455
        """
1456

    
1457
        if not transaction.is_managed():
1458
            raise AssertionError("NOPE")
1459

    
1460
        new_project_name = self.name
1461
        if self.state != self.PENDING:
1462
            m = _("cannot approve: project '%s' in state '%s'") % (
1463
                    new_project_name, self.state)
1464
            raise PermissionDenied(m) # invalid argument
1465

    
1466
        now = datetime.now()
1467
        project = self._get_project_for_update()
1468

    
1469
        try:
1470
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1471
            conflicting_project = Project.objects.get(q)
1472
            if (conflicting_project != project):
1473
                m = (_("cannot approve: project with name '%s' "
1474
                       "already exists (serial: %s)") % (
1475
                        new_project_name, conflicting_project.id))
1476
                raise PermissionDenied(m) # invalid argument
1477
        except Project.DoesNotExist:
1478
            pass
1479

    
1480
        new_project = False
1481
        if project is None:
1482
            new_project = True
1483
            project = Project(id=self.chain)
1484

    
1485
        project.name = new_project_name
1486
        project.application = self
1487
        project.last_approval_date = now
1488
        if not new_project:
1489
            project.is_modified = True
1490

    
1491
        project.save()
1492

    
1493
        self.state = self.APPROVED
1494
        self.response_date = now
1495
        self.save()
1496

    
1497
def submit_application(**kw):
1498

    
1499
    resource_policies = kw.pop('resource_policies', None)
1500
    application = ProjectApplication(**kw)
1501

    
1502
    precursor = kw['precursor_application']
1503

    
1504
    if precursor is None:
1505
        application.chain = new_chain()
1506
    else:
1507
        application.chain = precursor.chain
1508
        if precursor.state == ProjectApplication.PENDING:
1509
            precursor.state = ProjectApplication.REPLACED
1510
            precursor.save()
1511

    
1512
    application.save()
1513
    application.resource_policies = resource_policies
1514
    return application
1515

    
1516
class ProjectResourceGrant(models.Model):
1517

    
1518
    resource                =   models.ForeignKey(Resource)
1519
    project_application     =   models.ForeignKey(ProjectApplication,
1520
                                                  null=True)
1521
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1522
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1523
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1524
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1525
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1526
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1527

    
1528
    objects = ExtendedManager()
1529

    
1530
    class Meta:
1531
        unique_together = ("resource", "project_application")
1532

    
1533
    def member_quota_values(self):
1534
        return QuotaValues(
1535
            quantity = 0,
1536
            capacity = self.member_capacity,
1537
            import_limit = self.member_import_limit,
1538
            export_limit = self.member_export_limit)
1539

    
1540

    
1541
class ProjectManager(ForUpdateManager):
1542

    
1543
    def terminated_projects(self):
1544
        q = self.model.Q_TERMINATED
1545
        return self.filter(q)
1546

    
1547
    def not_terminated_projects(self):
1548
        q = ~self.model.Q_TERMINATED
1549
        return self.filter(q)
1550

    
1551
    def terminating_projects(self):
1552
        q = self.model.Q_TERMINATED & Q(is_active=True)
1553
        return self.filter(q)
1554

    
1555
    def deactivated_projects(self):
1556
        q = self.model.Q_DEACTIVATED
1557
        return self.filter(q)
1558

    
1559
    def deactivating_projects(self):
1560
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1561
        return self.filter(q)
1562

    
1563
    def modified_projects(self):
1564
        return self.filter(is_modified=True)
1565

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

    
1569
    def expired_projects(self):
1570
        q = (~Q(state=Project.TERMINATED) &
1571
              Q(application__end_date__lt=datetime.now()))
1572
        return self.filter(q)
1573

    
1574

    
1575
class Project(models.Model):
1576

    
1577
    application                 =   models.OneToOneField(
1578
                                            ProjectApplication,
1579
                                            related_name='project')
1580
    last_approval_date          =   models.DateTimeField(null=True)
1581

    
1582
    members                     =   models.ManyToManyField(
1583
                                            AstakosUser,
1584
                                            through='ProjectMembership')
1585

    
1586
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1587
    deactivation_date           =   models.DateTimeField(null=True)
1588

    
1589
    creation_date               =   models.DateTimeField(auto_now_add=True)
1590
    name                        =   models.CharField(
1591
                                            max_length=80,
1592
                                            db_index=True,
1593
                                            unique=True)
1594

    
1595
    APPROVED    = 1
1596
    SUSPENDED   = 10
1597
    TERMINATED  = 100
1598

    
1599
    is_modified                 =   models.BooleanField(default=False,
1600
                                                        db_index=True)
1601
    is_active                   =   models.BooleanField(default=True,
1602
                                                        db_index=True)
1603
    state                       =   models.IntegerField(default=APPROVED,
1604
                                                        db_index=True)
1605

    
1606
    objects     =   ProjectManager()
1607

    
1608
    # Compiled queries
1609
    Q_TERMINATED  = Q(state=TERMINATED)
1610
    Q_SUSPENDED   = Q(state=SUSPENDED)
1611
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1612

    
1613
    def __str__(self):
1614
        return _("<project %s '%s'>") % (self.id, self.application.name)
1615

    
1616
    __repr__ = __str__
1617

    
1618
    STATE_DISPLAY = {
1619
        APPROVED   : 'APPROVED',
1620
        SUSPENDED  : 'SUSPENDED',
1621
        TERMINATED : 'TERMINATED'
1622
        }
1623

    
1624
    def state_display(self):
1625
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1626

    
1627
    def expiration_info(self):
1628
        return (str(self.id), self.name, self.state_display(),
1629
                str(self.application.end_date))
1630

    
1631
    def is_deactivated(self, reason=None):
1632
        if reason is not None:
1633
            return self.state == reason
1634

    
1635
        return self.state != self.APPROVED
1636

    
1637
    def is_deactivating(self, reason=None):
1638
        if not self.is_active:
1639
            return False
1640

    
1641
        return self.is_deactivated(reason)
1642

    
1643
    def is_deactivated_strict(self, reason=None):
1644
        if self.is_active:
1645
            return False
1646

    
1647
        return self.is_deactivated(reason)
1648

    
1649
    ### Deactivation calls
1650

    
1651
    def deactivate(self):
1652
        self.deactivation_date = datetime.now()
1653
        self.is_active = False
1654

    
1655
    def reactivate(self):
1656
        self.deactivation_date = None
1657
        self.is_active = True
1658

    
1659
    def terminate(self):
1660
        self.deactivation_reason = 'TERMINATED'
1661
        self.state = self.TERMINATED
1662
        self.save()
1663

    
1664
    def suspend(self):
1665
        self.deactivation_reason = 'SUSPENDED'
1666
        self.state = self.SUSPENDED
1667
        self.save()
1668

    
1669
    def resume(self):
1670
        self.deactivation_reason = None
1671
        self.state = self.APPROVED
1672
        self.save()
1673

    
1674
    ### Logical checks
1675

    
1676
    def is_inconsistent(self):
1677
        now = datetime.now()
1678
        dates = [self.creation_date,
1679
                 self.last_approval_date,
1680
                 self.deactivation_date]
1681
        return any([date > now for date in dates])
1682

    
1683
    def is_active_strict(self):
1684
        return self.is_active and self.state == self.APPROVED
1685

    
1686
    def is_approved(self):
1687
        return self.state == self.APPROVED
1688

    
1689
    @property
1690
    def is_alive(self):
1691
        return not self.is_terminated
1692

    
1693
    @property
1694
    def is_terminated(self):
1695
        return self.is_deactivated(self.TERMINATED)
1696

    
1697
    @property
1698
    def is_suspended(self):
1699
        return self.is_deactivated(self.SUSPENDED)
1700

    
1701
    def violates_resource_grants(self):
1702
        return False
1703

    
1704
    def violates_members_limit(self, adding=0):
1705
        application = self.application
1706
        limit = application.limit_on_members_number
1707
        if limit is None:
1708
            return False
1709
        return (len(self.approved_members) + adding > limit)
1710

    
1711

    
1712
    ### Other
1713

    
1714
    def count_pending_memberships(self):
1715
        memb_set = self.projectmembership_set
1716
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1717
        return memb_count
1718

    
1719
    @property
1720
    def approved_memberships(self):
1721
        query = ProjectMembership.Q_ACCEPTED_STATES
1722
        return self.projectmembership_set.filter(query)
1723

    
1724
    @property
1725
    def approved_members(self):
1726
        return [m.person for m in self.approved_memberships]
1727

    
1728
    def add_member(self, user):
1729
        """
1730
        Raises:
1731
            django.exceptions.PermissionDenied
1732
            astakos.im.models.AstakosUser.DoesNotExist
1733
        """
1734
        if isinstance(user, int):
1735
            user = AstakosUser.objects.get(user=user)
1736

    
1737
        m, created = ProjectMembership.objects.get_or_create(
1738
            person=user, project=self
1739
        )
1740
        m.accept()
1741

    
1742
    def remove_member(self, user):
1743
        """
1744
        Raises:
1745
            django.exceptions.PermissionDenied
1746
            astakos.im.models.AstakosUser.DoesNotExist
1747
            astakos.im.models.ProjectMembership.DoesNotExist
1748
        """
1749
        if isinstance(user, int):
1750
            user = AstakosUser.objects.get(user=user)
1751

    
1752
        m = ProjectMembership.objects.get(person=user, project=self)
1753
        m.remove()
1754

    
1755

    
1756
class PendingMembershipError(Exception):
1757
    pass
1758

    
1759

    
1760
class ProjectMembershipManager(ForUpdateManager):
1761
    pass
1762

    
1763
class ProjectMembership(models.Model):
1764

    
1765
    person              =   models.ForeignKey(AstakosUser)
1766
    request_date        =   models.DateField(auto_now_add=True)
1767
    project             =   models.ForeignKey(Project)
1768

    
1769
    REQUESTED           =   0
1770
    ACCEPTED            =   1
1771
    # User deactivation
1772
    USER_SUSPENDED      =   10
1773
    # Project deactivation
1774
    PROJECT_DEACTIVATED =   100
1775

    
1776
    REMOVED             =   200
1777

    
1778
    ASSOCIATED_STATES   =   set([REQUESTED,
1779
                                 ACCEPTED,
1780
                                 USER_SUSPENDED,
1781
                                 PROJECT_DEACTIVATED])
1782

    
1783
    ACCEPTED_STATES     =   set([ACCEPTED,
1784
                                 USER_SUSPENDED,
1785
                                 PROJECT_DEACTIVATED])
1786

    
1787
    state               =   models.IntegerField(default=REQUESTED,
1788
                                                db_index=True)
1789
    is_pending          =   models.BooleanField(default=False, db_index=True)
1790
    is_active           =   models.BooleanField(default=False, db_index=True)
1791
    application         =   models.ForeignKey(
1792
                                ProjectApplication,
1793
                                null=True,
1794
                                related_name='memberships')
1795
    pending_application =   models.ForeignKey(
1796
                                ProjectApplication,
1797
                                null=True,
1798
                                related_name='pending_memberships')
1799
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1800

    
1801
    acceptance_date     =   models.DateField(null=True, db_index=True)
1802
    leave_request_date  =   models.DateField(null=True)
1803

    
1804
    objects     =   ProjectMembershipManager()
1805

    
1806
    # Compiled queries
1807
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1808

    
1809
    def get_combined_state(self):
1810
        return self.state, self.is_active, self.is_pending
1811

    
1812
    class Meta:
1813
        unique_together = ("person", "project")
1814
        #index_together = [["project", "state"]]
1815

    
1816
    def __str__(self):
1817
        return _("<'%s' membership in '%s'>") % (
1818
                self.person.username, self.project)
1819

    
1820
    __repr__ = __str__
1821

    
1822
    def __init__(self, *args, **kwargs):
1823
        self.state = self.REQUESTED
1824
        super(ProjectMembership, self).__init__(*args, **kwargs)
1825

    
1826
    def _set_history_item(self, reason, date=None):
1827
        if isinstance(reason, basestring):
1828
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1829

    
1830
        history_item = ProjectMembershipHistory(
1831
                            serial=self.id,
1832
                            person=self.person_id,
1833
                            project=self.project_id,
1834
                            date=date or datetime.now(),
1835
                            reason=reason)
1836
        history_item.save()
1837
        serial = history_item.id
1838

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

    
1844
        state = self.state
1845
        if state != self.REQUESTED:
1846
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1847
            raise AssertionError(m)
1848

    
1849
        now = datetime.now()
1850
        self.acceptance_date = now
1851
        self._set_history_item(reason='ACCEPT', date=now)
1852
        if self.project.is_approved():
1853
            self.state = self.ACCEPTED
1854
            self.is_pending = True
1855
        else:
1856
            self.state = self.PROJECT_DEACTIVATED
1857

    
1858
        self.save()
1859

    
1860
    def remove(self):
1861
        if self.is_pending:
1862
            m = _("%s: attempt to remove while is pending") % (self,)
1863
            raise AssertionError(m)
1864

    
1865
        state = self.state
1866
        if state not in self.ACCEPTED_STATES:
1867
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1868
            raise AssertionError(m)
1869

    
1870
        self._set_history_item(reason='REMOVE')
1871
        self.state = self.REMOVED
1872
        self.is_pending = True
1873
        self.save()
1874

    
1875
    def reject(self):
1876
        if self.is_pending:
1877
            m = _("%s: attempt to reject while is pending") % (self,)
1878
            raise AssertionError(m)
1879

    
1880
        state = self.state
1881
        if state != self.REQUESTED:
1882
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1883
            raise AssertionError(m)
1884

    
1885
        # rejected requests don't need sync,
1886
        # because they were never effected
1887
        self._set_history_item(reason='REJECT')
1888
        self.delete()
1889

    
1890
    def get_diff_quotas(self, sub_list=None, add_list=None):
1891
        if sub_list is None:
1892
            sub_list = []
1893

    
1894
        if add_list is None:
1895
            add_list = []
1896

    
1897
        sub_append = sub_list.append
1898
        add_append = add_list.append
1899
        holder = self.person.uuid
1900

    
1901
        synced_application = self.application
1902
        if synced_application is not None:
1903
            cur_grants = synced_application.projectresourcegrant_set.all()
1904
            for grant in cur_grants:
1905
                sub_append(QuotaLimits(
1906
                               holder       = holder,
1907
                               resource     = str(grant.resource),
1908
                               capacity     = grant.member_capacity,
1909
                               import_limit = grant.member_import_limit,
1910
                               export_limit = grant.member_export_limit))
1911

    
1912
        pending_application = self.pending_application
1913
        if pending_application is not None:
1914
            new_grants = pending_application.projectresourcegrant_set.all()
1915
            for new_grant in new_grants:
1916
                add_append(QuotaLimits(
1917
                               holder       = holder,
1918
                               resource     = str(new_grant.resource),
1919
                               capacity     = new_grant.member_capacity,
1920
                               import_limit = new_grant.member_import_limit,
1921
                               export_limit = new_grant.member_export_limit))
1922

    
1923
        return (sub_list, add_list)
1924

    
1925
    def set_sync(self):
1926
        if not self.is_pending:
1927
            m = _("%s: attempt to sync a non pending membership") % (self,)
1928
            raise AssertionError(m)
1929

    
1930
        state = self.state
1931
        if state == self.ACCEPTED:
1932
            pending_application = self.pending_application
1933
            if pending_application is None:
1934
                m = _("%s: attempt to sync an empty pending application") % (
1935
                    self,)
1936
                raise AssertionError(m)
1937

    
1938
            self.application = pending_application
1939
            self.is_active = True
1940

    
1941
            self.pending_application = None
1942
            self.pending_serial = None
1943

    
1944
            # project.application may have changed in the meantime,
1945
            # in which case we stay PENDING;
1946
            # we are safe to check due to select_for_update
1947
            if self.application == self.project.application:
1948
                self.is_pending = False
1949
            self.save()
1950

    
1951
        elif state == self.PROJECT_DEACTIVATED:
1952
            if self.pending_application:
1953
                m = _("%s: attempt to sync in state '%s' "
1954
                      "with a pending application") % (self, state)
1955
                raise AssertionError(m)
1956

    
1957
            self.application = None
1958
            self.is_active = False
1959
            self.pending_serial = None
1960
            self.is_pending = False
1961
            self.save()
1962

    
1963
        elif state == self.REMOVED:
1964
            self.delete()
1965

    
1966
        else:
1967
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1968
            raise AssertionError(m)
1969

    
1970
    def reset_sync(self):
1971
        if not self.is_pending:
1972
            m = _("%s: attempt to reset a non pending membership") % (self,)
1973
            raise AssertionError(m)
1974

    
1975
        state = self.state
1976
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
1977
            self.pending_application = None
1978
            self.pending_serial = None
1979
            self.save()
1980
        else:
1981
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1982
            raise AssertionError(m)
1983

    
1984
class Serial(models.Model):
1985
    serial  =   models.AutoField(primary_key=True)
1986

    
1987
def new_serial():
1988
    s = Serial.objects.create()
1989
    serial = s.serial
1990
    s.delete()
1991
    return serial
1992

    
1993
def sync_finish_serials(serials_to_ack=None):
1994
    if serials_to_ack is None:
1995
        serials_to_ack = qh_query_serials([])
1996

    
1997
    serials_to_ack = set(serials_to_ack)
1998
    sfu = ProjectMembership.objects.select_for_update()
1999
    memberships = list(sfu.filter(pending_serial__isnull=False))
2000

    
2001
    if memberships:
2002
        for membership in memberships:
2003
            serial = membership.pending_serial
2004
            if serial in serials_to_ack:
2005
                membership.set_sync()
2006
            else:
2007
                membership.reset_sync()
2008

    
2009
        transaction.commit()
2010

    
2011
    qh_ack_serials(list(serials_to_ack))
2012
    return len(memberships)
2013

    
2014
def pre_sync_projects():
2015
    ACCEPTED = ProjectMembership.ACCEPTED
2016
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2017
    psfu = Project.objects.select_for_update()
2018

    
2019
    modified = psfu.modified_projects()
2020
    for project in modified:
2021
        objects = project.projectmembership_set.select_for_update()
2022

    
2023
        memberships = objects.filter(state=ACCEPTED)
2024
        for membership in memberships:
2025
            membership.is_pending = True
2026
            membership.save()
2027

    
2028
    reactivating = psfu.reactivating_projects()
2029
    for project in reactivating:
2030
        objects = project.projectmembership_set.select_for_update()
2031
        memberships = objects.filter(state=PROJECT_DEACTIVATED)
2032
        for membership in memberships:
2033
            membership.is_pending = True
2034
            membership.state = ACCEPTED
2035
            membership.save()
2036

    
2037
    deactivating = psfu.deactivating_projects()
2038
    for project in deactivating:
2039
        objects = project.projectmembership_set.select_for_update()
2040

    
2041
        # Note: we keep a user-level deactivation (e.g. USER_SUSPENDED) intact
2042
        memberships = objects.filter(state=ACCEPTED)
2043
        for membership in memberships:
2044
            membership.is_pending = True
2045
            membership.state = PROJECT_DEACTIVATED
2046
            membership.save()
2047

    
2048
def do_sync_projects():
2049

    
2050
    ACCEPTED = ProjectMembership.ACCEPTED
2051
    objects = ProjectMembership.objects.select_for_update()
2052

    
2053
    sub_quota, add_quota = [], []
2054

    
2055
    serial = new_serial()
2056

    
2057
    pending = objects.filter(is_pending=True)
2058
    for membership in pending:
2059

    
2060
        if membership.pending_application:
2061
            m = "%s: impossible: pending_application is not None (%s)" % (
2062
                membership, membership.pending_application)
2063
            raise AssertionError(m)
2064
        if membership.pending_serial:
2065
            m = "%s: impossible: pending_serial is not None (%s)" % (
2066
                membership, membership.pending_serial)
2067
            raise AssertionError(m)
2068

    
2069
        if membership.state == ACCEPTED:
2070
            membership.pending_application = membership.project.application
2071

    
2072
        membership.pending_serial = serial
2073
        membership.get_diff_quotas(sub_quota, add_quota)
2074
        membership.save()
2075

    
2076
    transaction.commit()
2077
    # ProjectApplication.approve() unblocks here
2078
    # and can set PENDING an already PENDING membership
2079
    # which has been scheduled to sync with the old project.application
2080
    # Need to check in ProjectMembership.set_sync()
2081

    
2082
    r = qh_add_quota(serial, sub_quota, add_quota)
2083
    if r:
2084
        m = "cannot sync serial: %d" % serial
2085
        raise RuntimeError(m)
2086

    
2087
    return serial
2088

    
2089
def post_sync_projects():
2090
    ACCEPTED = ProjectMembership.ACCEPTED
2091
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2092
    psfu = Project.objects.select_for_update()
2093

    
2094
    modified = psfu.modified_projects()
2095
    for project in modified:
2096
        objects = project.projectmembership_set.select_for_update()
2097

    
2098
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2099
        if not memberships:
2100
            project.is_modified = False
2101
            project.save()
2102

    
2103
    reactivating = psfu.reactivating_projects()
2104
    for project in reactivating:
2105
        objects = project.projectmembership_set.select_for_update()
2106
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2107
                                          Q(is_pending=True)))
2108
        if not memberships:
2109
            project.reactivate()
2110
            project.save()
2111

    
2112
    deactivating = psfu.deactivating_projects()
2113
    for project in deactivating:
2114
        objects = project.projectmembership_set.select_for_update()
2115

    
2116
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2117
                                          Q(is_pending=True)))
2118
        if not memberships:
2119
            project.deactivate()
2120
            project.save()
2121

    
2122
    transaction.commit()
2123

    
2124
def _sync_projects(sync):
2125
    sync_finish_serials()
2126
    if not sync:
2127
        # Do some reporting and
2128
        return
2129

    
2130
    pre_sync_projects()
2131
    serial = do_sync_projects()
2132
    sync_finish_serials([serial])
2133
    post_sync_projects()
2134

    
2135
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2136
    return lock_sync(_sync_projects,
2137
                     args=[sync],
2138
                     retries=retries,
2139
                     retry_wait=retry_wait)
2140

    
2141
def _sync_users(users, sync):
2142
    sync_finish_serials()
2143

    
2144
    existing, nonexisting = qh_check_users(users)
2145
    resources = get_resource_names()
2146
    quotas = qh_get_quota_limits(existing, resources)
2147

    
2148
    if sync:
2149
        r = register_users(nonexisting)
2150
        r = register_quotas(users)
2151

    
2152
    # TODO: some proper reporting
2153
    return (existing, nonexisting, quotas)
2154

    
2155
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2156
    return lock_sync(_sync_users,
2157
                     args=[users, sync],
2158
                     retries=retries,
2159
                     retry_wait=retry_wait)
2160

    
2161
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2162
    users = AstakosUser.objects.all()
2163
    return sync_users(users, sync, retries=retries, retry_wait=retry_wait)
2164

    
2165
def lock_sync(func, args=[], kwargs={}, retries=3, retry_wait=1.0):
2166
    transaction.commit()
2167

    
2168
    cursor = connection.cursor()
2169
    locked = True
2170
    try:
2171
        while 1:
2172
            cursor.execute("SELECT pg_try_advisory_lock(1)")
2173
            r = cursor.fetchone()
2174
            if r is None:
2175
                m = "Impossible"
2176
                raise AssertionError(m)
2177
            locked = r[0]
2178
            if locked:
2179
                break
2180

    
2181
            retries -= 1
2182
            if retries <= 0:
2183
                return False
2184
            sleep(retry_wait)
2185

    
2186
        return func(*args, **kwargs)
2187

    
2188
    finally:
2189
        if locked:
2190
            cursor.execute("SELECT pg_advisory_unlock(1)")
2191
            cursor.fetchall()
2192

    
2193

    
2194
class ProjectMembershipHistory(models.Model):
2195
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2196
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2197

    
2198
    person  =   models.BigIntegerField()
2199
    project =   models.BigIntegerField()
2200
    date    =   models.DateField(auto_now_add=True)
2201
    reason  =   models.IntegerField()
2202
    serial  =   models.BigIntegerField()
2203

    
2204
### SIGNALS ###
2205
################
2206

    
2207
def create_astakos_user(u):
2208
    try:
2209
        AstakosUser.objects.get(user_ptr=u.pk)
2210
    except AstakosUser.DoesNotExist:
2211
        extended_user = AstakosUser(user_ptr_id=u.pk)
2212
        extended_user.__dict__.update(u.__dict__)
2213
        extended_user.save()
2214
        if not extended_user.has_auth_provider('local'):
2215
            extended_user.add_auth_provider('local')
2216
    except BaseException, e:
2217
        logger.exception(e)
2218

    
2219

    
2220
def fix_superusers(sender, **kwargs):
2221
    # Associate superusers with AstakosUser
2222
    admins = User.objects.filter(is_superuser=True)
2223
    for u in admins:
2224
        create_astakos_user(u)
2225
post_syncdb.connect(fix_superusers)
2226

    
2227

    
2228
def user_post_save(sender, instance, created, **kwargs):
2229
    if not created:
2230
        return
2231
    create_astakos_user(instance)
2232
post_save.connect(user_post_save, sender=User)
2233

    
2234
def astakosuser_post_save(sender, instance, created, **kwargs):
2235
    pass
2236

    
2237
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2238

    
2239
def resource_post_save(sender, instance, created, **kwargs):
2240
    pass
2241

    
2242
post_save.connect(resource_post_save, sender=Resource)
2243

    
2244
def renew_token(sender, instance, **kwargs):
2245
    if not instance.auth_token:
2246
        instance.renew_token()
2247
pre_save.connect(renew_token, sender=AstakosUser)
2248
pre_save.connect(renew_token, sender=Service)
2249