Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 890c2065

History | View | Annotate | Download (79.7 kB)

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

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

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

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

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

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

    
81
import astakos.im.messages as astakos_messages
82
from .managers import ForUpdateManager
83

    
84
from synnefo.lib.quotaholder.api import QH_PRACTICALLY_INFINITE
85
from synnefo.lib.db.intdecimalfield import intDecimalField
86

    
87
logger = logging.getLogger(__name__)
88

    
89
DEFAULT_CONTENT_TYPE = None
90
_content_type = None
91

    
92
def get_content_type():
93
    global _content_type
94
    if _content_type is not None:
95
        return _content_type
96

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

    
104
RESOURCE_SEPARATOR = '.'
105

    
106
inf = float('inf')
107

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

    
119
    class Meta:
120
        ordering = ('order', )
121

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

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

    
135
    def __str__(self):
136
        return self.name
137

    
138
    @property
139
    def resources(self):
140
        return self.resource_set.all()
141

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

    
147

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

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

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

    
171
    class Meta:
172
        unique_together = ("service", "name")
173

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

    
177
    def full_name(self):
178
        return str(self)
179

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

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

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

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

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

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

    
204
    @property
205
    def display_name(self):
206
        name = self.verbose_name
207
        if self.is_abbreviation:
208
            name = name.upper()
209
        return name
210

    
211
    @property
212
    def pluralized_display_name(self):
213
        if not self.unit:
214
            return '%ss' % self.display_name
215
        return self.display_name
216

    
217

    
218
def load_service_resources():
219
    ss = []
220
    rs = []
221
    for service_name, data in SERVICES.iteritems():
222
        url = data.get('url')
223
        resources = data.get('resources') or ()
224
        service, created = Service.objects.get_or_create(
225
            name=service_name,
226
            defaults={'url': url}
227
        )
228
        ss.append(service)
229

    
230
        for resource in resources:
231
            try:
232
                resource_name = resource.pop('name', '')
233
                r, created = Resource.objects.get_or_create(
234
                    service=service,
235
                    name=resource_name,
236
                    defaults=resource)
237
                rs.append(r)
238

    
239
            except Exception, e:
240
                print "Cannot create resource ", resource_name
241
                continue
242
    register_services(ss)
243
    register_resources(rs)
244

    
245
def _quota_values(capacity):
246
    return QuotaValues(
247
        quantity = 0,
248
        capacity = capacity,
249
        import_limit = QH_PRACTICALLY_INFINITE,
250
        export_limit = QH_PRACTICALLY_INFINITE)
251

    
252
def get_default_quota():
253
    _DEFAULT_QUOTA = {}
254
    resources = Resource.objects.all()
255
    for resource in resources:
256
        capacity = resource.uplimit
257
        limits = _quota_values(capacity)
258
        _DEFAULT_QUOTA[resource.full_name()] = limits
259

    
260
    return _DEFAULT_QUOTA
261

    
262
def get_resource_names():
263
    _RESOURCE_NAMES = []
264
    resources = Resource.objects.all()
265
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
266
    return _RESOURCE_NAMES
267

    
268

    
269
class AstakosUserManager(UserManager):
270

    
271
    def get_auth_provider_user(self, provider, **kwargs):
272
        """
273
        Retrieve AstakosUser instance associated with the specified third party
274
        id.
275
        """
276
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
277
                          kwargs.iteritems()))
278
        return self.get(auth_providers__module=provider, **kwargs)
279

    
280
    def get_by_email(self, email):
281
        return self.get(email=email)
282

    
283
    def get_by_identifier(self, email_or_username, **kwargs):
284
        try:
285
            return self.get(email__iexact=email_or_username, **kwargs)
286
        except AstakosUser.DoesNotExist:
287
            return self.get(username__iexact=email_or_username, **kwargs)
288

    
289
    def user_exists(self, email_or_username, **kwargs):
290
        qemail = Q(email__iexact=email_or_username)
291
        qusername = Q(username__iexact=email_or_username)
292
        qextra = Q(**kwargs)
293
        return self.filter((qemail | qusername) & qextra).exists()
294

    
295
    def verified_user_exists(self, email_or_username):
296
        return self.user_exists(email_or_username, email_verified=True)
297

    
298
    def verified(self):
299
        return self.filter(email_verified=True)
300

    
301
    def verified(self):
302
        return self.filter(email_verified=True)
303

    
304
    def uuid_catalog(self, l=None):
305
        """
306
        Returns a uuid to username mapping for the uuids appearing in l.
307
        If l is None returns the mapping for all existing users.
308
        """
309
        q = self.filter(uuid__in=l) if l != None else self
310
        return dict(q.values_list('uuid', 'username'))
311

    
312
    def displayname_catalog(self, l=None):
313
        """
314
        Returns a username to uuid mapping for the usernames appearing in l.
315
        If l is None returns the mapping for all existing users.
316
        """
317
        q = self.filter(uuid__in=l) if l != None else self
318
        q = self.filter(username__in=l) if l != None else self
319
        return dict(q.values_list('username', 'uuid'))
320

    
321

    
322

    
323
class AstakosUser(User):
324
    """
325
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
326
    """
327
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
328
                                   null=True)
329

    
330
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
331
    #                    AstakosUserProvider model.
332
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
333
                                null=True)
334
    # ex. screen_name for twitter, eppn for shibboleth
335
    third_party_identifier = models.CharField(_('Third-party identifier'),
336
                                              max_length=255, null=True,
337
                                              blank=True)
338

    
339

    
340
    #for invitations
341
    user_level = DEFAULT_USER_LEVEL
342
    level = models.IntegerField(_('Inviter level'), default=user_level)
343
    invitations = models.IntegerField(
344
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
345

    
346
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
347
                                  null=True, blank=True, help_text = _( 'test' ))
348
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
349
    auth_token_expires = models.DateTimeField(
350
        _('Token expiration date'), null=True)
351

    
352
    updated = models.DateTimeField(_('Update date'))
353
    is_verified = models.BooleanField(_('Is verified?'), default=False)
354

    
355
    email_verified = models.BooleanField(_('Email verified?'), default=False)
356

    
357
    has_credits = models.BooleanField(_('Has credits?'), default=False)
358
    has_signed_terms = models.BooleanField(
359
        _('I agree with the terms'), default=False)
360
    date_signed_terms = models.DateTimeField(
361
        _('Signed terms date'), null=True, blank=True)
362

    
363
    activation_sent = models.DateTimeField(
364
        _('Activation sent data'), null=True, blank=True)
365

    
366
    policy = models.ManyToManyField(
367
        Resource, null=True, through='AstakosUserQuota')
368

    
369
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
370

    
371
    __has_signed_terms = False
372
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
373
                                           default=False, db_index=True)
374

    
375
    objects = AstakosUserManager()
376

    
377
    def __init__(self, *args, **kwargs):
378
        super(AstakosUser, self).__init__(*args, **kwargs)
379
        self.__has_signed_terms = self.has_signed_terms
380
        if not self.id:
381
            self.is_active = False
382

    
383
    @property
384
    def realname(self):
385
        return '%s %s' % (self.first_name, self.last_name)
386

    
387
    @realname.setter
388
    def realname(self, value):
389
        parts = value.split(' ')
390
        if len(parts) == 2:
391
            self.first_name = parts[0]
392
            self.last_name = parts[1]
393
        else:
394
            self.last_name = parts[0]
395

    
396
    def add_permission(self, pname):
397
        if self.has_perm(pname):
398
            return
399
        p, created = Permission.objects.get_or_create(
400
                                    codename=pname,
401
                                    name=pname.capitalize(),
402
                                    content_type=get_content_type())
403
        self.user_permissions.add(p)
404

    
405
    def remove_permission(self, pname):
406
        if self.has_perm(pname):
407
            return
408
        p = Permission.objects.get(codename=pname,
409
                                   content_type=get_content_type())
410
        self.user_permissions.remove(p)
411

    
412
    @property
413
    def invitation(self):
414
        try:
415
            return Invitation.objects.get(username=self.email)
416
        except Invitation.DoesNotExist:
417
            return None
418

    
419
    def initial_quotas(self):
420
        quotas = dict(get_default_quota())
421
        for user_quota in self.policies:
422
            resource = user_quota.resource.full_name()
423
            quotas[resource] = user_quota.quota_values()
424
        return quotas
425

    
426
    def all_quotas(self):
427
        quotas = self.initial_quotas()
428

    
429
        objects = self.projectmembership_set.select_related()
430
        memberships = objects.filter(is_active=True)
431
        for membership in memberships:
432
            application = membership.application
433
            if application is None:
434
                m = _("missing application for active membership %s"
435
                      % (membership,))
436
                raise AssertionError(m)
437

    
438
            grants = application.projectresourcegrant_set.all()
439
            for grant in grants:
440
                resource = grant.resource.full_name()
441
                prev = quotas.get(resource, 0)
442
                new = add_quota_values(prev, grant.member_quota_values())
443
                quotas[resource] = new
444
        return quotas
445

    
446
    @property
447
    def policies(self):
448
        return self.astakosuserquota_set.select_related().all()
449

    
450
    @policies.setter
451
    def policies(self, policies):
452
        for p in policies:
453
            p.setdefault('resource', '')
454
            p.setdefault('capacity', 0)
455
            p.setdefault('quantity', 0)
456
            p.setdefault('import_limit', 0)
457
            p.setdefault('export_limit', 0)
458
            p.setdefault('update', True)
459
            self.add_resource_policy(**p)
460

    
461
    def add_resource_policy(
462
            self, resource, capacity, quantity, import_limit,
463
            export_limit, update=True):
464
        """Raises ObjectDoesNotExist, IntegrityError"""
465
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
466
        resource = Resource.objects.get(service__name=s, name=r)
467
        if update:
468
            AstakosUserQuota.objects.update_or_create(
469
                user=self, resource=resource, defaults={
470
                    'capacity':capacity,
471
                    'quantity': quantity,
472
                    'import_limit':import_limit,
473
                    'export_limit':export_limit})
474
        else:
475
            q = self.astakosuserquota_set
476
            q.create(
477
                resource=resource, capacity=capacity, quanity=quantity,
478
                import_limit=import_limit, export_limit=export_limit)
479

    
480
    def remove_resource_policy(self, service, resource):
481
        """Raises ObjectDoesNotExist, IntegrityError"""
482
        resource = Resource.objects.get(service__name=service, name=resource)
483
        q = self.policies.get(resource=resource).delete()
484

    
485
    def update_uuid(self):
486
        while not self.uuid:
487
            uuid_val =  str(uuid.uuid4())
488
            try:
489
                AstakosUser.objects.get(uuid=uuid_val)
490
            except AstakosUser.DoesNotExist, e:
491
                self.uuid = uuid_val
492
        return self.uuid
493

    
494
    def save(self, update_timestamps=True, **kwargs):
495
        if update_timestamps:
496
            if not self.id:
497
                self.date_joined = datetime.now()
498
            self.updated = datetime.now()
499

    
500
        # update date_signed_terms if necessary
501
        if self.__has_signed_terms != self.has_signed_terms:
502
            self.date_signed_terms = datetime.now()
503

    
504
        self.update_uuid()
505

    
506
        if self.username != self.email.lower():
507
            # set username
508
            self.username = self.email.lower()
509

    
510
        super(AstakosUser, self).save(**kwargs)
511

    
512
    def renew_token(self, flush_sessions=False, current_key=None):
513
        md5 = hashlib.md5()
514
        md5.update(settings.SECRET_KEY)
515
        md5.update(self.username)
516
        md5.update(self.realname.encode('ascii', 'ignore'))
517
        md5.update(asctime())
518

    
519
        self.auth_token = b64encode(md5.digest())
520
        self.auth_token_created = datetime.now()
521
        self.auth_token_expires = self.auth_token_created + \
522
                                  timedelta(hours=AUTH_TOKEN_DURATION)
523
        if flush_sessions:
524
            self.flush_sessions(current_key)
525
        msg = 'Token renewed for %s' % self.email
526
        logger.log(LOGGING_LEVEL, msg)
527

    
528
    def flush_sessions(self, current_key=None):
529
        q = self.sessions
530
        if current_key:
531
            q = q.exclude(session_key=current_key)
532

    
533
        keys = q.values_list('session_key', flat=True)
534
        if keys:
535
            msg = 'Flushing sessions: %s' % ','.join(keys)
536
            logger.log(LOGGING_LEVEL, msg, [])
537
        engine = import_module(settings.SESSION_ENGINE)
538
        for k in keys:
539
            s = engine.SessionStore(k)
540
            s.flush()
541

    
542
    def __unicode__(self):
543
        return '%s (%s)' % (self.realname, self.email)
544

    
545
    def conflicting_email(self):
546
        q = AstakosUser.objects.exclude(username=self.username)
547
        q = q.filter(email__iexact=self.email)
548
        if q.count() != 0:
549
            return True
550
        return False
551

    
552
    def email_change_is_pending(self):
553
        return self.emailchanges.count() > 0
554

    
555
    def email_change_is_pending(self):
556
        return self.emailchanges.count() > 0
557

    
558
    @property
559
    def signed_terms(self):
560
        term = get_latest_terms()
561
        if not term:
562
            return True
563
        if not self.has_signed_terms:
564
            return False
565
        if not self.date_signed_terms:
566
            return False
567
        if self.date_signed_terms < term.date:
568
            self.has_signed_terms = False
569
            self.date_signed_terms = None
570
            self.save()
571
            return False
572
        return True
573

    
574
    def set_invitations_level(self):
575
        """
576
        Update user invitation level
577
        """
578
        level = self.invitation.inviter.level + 1
579
        self.level = level
580
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
581

    
582
    def can_login_with_auth_provider(self, provider):
583
        if not self.has_auth_provider(provider):
584
            return False
585
        else:
586
            return auth_providers.get_provider(provider).is_available_for_login()
587

    
588
    def can_add_auth_provider(self, provider, include_unverified=False, **kwargs):
589
        provider_settings = auth_providers.get_provider(provider)
590

    
591
        if not provider_settings.is_available_for_add():
592
            return False
593

    
594
        if self.has_auth_provider(provider) and \
595
           provider_settings.one_per_user:
596
            return False
597

    
598
        if 'provider_info' in kwargs:
599
            kwargs.pop('provider_info')
600

    
601
        if 'identifier' in kwargs:
602
            try:
603
                # provider with specified params already exist
604
                if not include_unverified:
605
                    kwargs['user__email_verified'] = True
606
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
607
                                                                   **kwargs)
608
            except AstakosUser.DoesNotExist:
609
                return True
610
            else:
611
                return False
612

    
613
        return True
614

    
615
    def can_remove_auth_provider(self, module):
616
        provider = auth_providers.get_provider(module)
617
        existing = self.get_active_auth_providers()
618
        existing_for_provider = self.get_active_auth_providers(module=module)
619

    
620
        if len(existing) <= 1:
621
            return False
622

    
623
        if len(existing_for_provider) == 1 and provider.is_required():
624
            return False
625

    
626
        return True
627

    
628
    def can_change_password(self):
629
        return self.has_auth_provider('local', auth_backend='astakos')
630

    
631
    def can_change_email(self):
632
        non_astakos_local = self.get_auth_providers().filter(module='local')
633
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
634
        return non_astakos_local.count() == 0
635

    
636
    def has_required_auth_providers(self):
637
        required = auth_providers.REQUIRED_PROVIDERS
638
        for provider in required:
639
            if not self.has_auth_provider(provider):
640
                return False
641
        return True
642

    
643
    def has_auth_provider(self, provider, **kwargs):
644
        return bool(self.get_auth_providers().filter(module=provider,
645
                                               **kwargs).count())
646

    
647
    def add_auth_provider(self, provider, **kwargs):
648
        info_data = ''
649
        if 'provider_info' in kwargs:
650
            info_data = kwargs.pop('provider_info')
651
            if isinstance(info_data, dict):
652
                info_data = json.dumps(info_data)
653

    
654
        if self.can_add_auth_provider(provider, **kwargs):
655
            if 'identifier' in kwargs:
656
                # clean up third party pending for activation users of the same
657
                # identifier
658
                AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
659
                                                                **kwargs)
660
            self.auth_providers.create(module=provider, active=True,
661
                                       info_data=info_data,
662
                                       **kwargs)
663
        else:
664
            raise Exception('Cannot add provider')
665

    
666
    def add_pending_auth_provider(self, pending):
667
        """
668
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
669
        the current user.
670
        """
671
        if not isinstance(pending, PendingThirdPartyUser):
672
            pending = PendingThirdPartyUser.objects.get(token=pending)
673

    
674
        provider = self.add_auth_provider(pending.provider,
675
                               identifier=pending.third_party_identifier,
676
                                affiliation=pending.affiliation,
677
                                          provider_info=pending.info)
678

    
679
        if email_re.match(pending.email or '') and pending.email != self.email:
680
            self.additionalmail_set.get_or_create(email=pending.email)
681

    
682
        pending.delete()
683
        return provider
684

    
685
    def remove_auth_provider(self, provider, **kwargs):
686
        self.get_auth_providers().get(module=provider, **kwargs).delete()
687

    
688
    # user urls
689
    def get_resend_activation_url(self):
690
        return reverse('send_activation', kwargs={'user_id': self.pk})
691

    
692
    def get_provider_remove_url(self, module, **kwargs):
693
        return reverse('remove_auth_provider', kwargs={
694
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
695

    
696
    def get_activation_url(self, nxt=False):
697
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
698
                                 quote(self.auth_token))
699
        if nxt:
700
            url += "&next=%s" % quote(nxt)
701
        return url
702

    
703
    def get_password_reset_url(self, token_generator=default_token_generator):
704
        return reverse('django.contrib.auth.views.password_reset_confirm',
705
                          kwargs={'uidb36':int_to_base36(self.id),
706
                                  'token':token_generator.make_token(self)})
707

    
708
    def get_auth_providers(self):
709
        return self.auth_providers
710

    
711
    def get_available_auth_providers(self):
712
        """
713
        Returns a list of providers available for user to connect to.
714
        """
715
        providers = []
716
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
717
            if self.can_add_auth_provider(module):
718
                providers.append(provider_settings(self))
719

    
720
        return providers
721

    
722
    def get_active_auth_providers(self, **filters):
723
        providers = []
724
        for provider in self.get_auth_providers().active(**filters):
725
            if auth_providers.get_provider(provider.module).is_available_for_login():
726
                providers.append(provider)
727
        return providers
728

    
729
    @property
730
    def auth_providers_display(self):
731
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
732

    
733
    def get_inactive_message(self):
734
        msg_extra = ''
735
        message = ''
736
        if self.activation_sent:
737
            if self.email_verified:
738
                message = _(astakos_messages.ACCOUNT_INACTIVE)
739
            else:
740
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
741
                if astakos_settings.MODERATION_ENABLED:
742
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
743
                else:
744
                    url = self.get_resend_activation_url()
745
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
746
                                u' ' + \
747
                                _('<a href="%s">%s?</a>') % (url,
748
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
749
        else:
750
            if astakos_settings.MODERATION_ENABLED:
751
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
752
            else:
753
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
754
                url = self.get_resend_activation_url()
755
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
756
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
757

    
758
        return mark_safe(message + u' '+ msg_extra)
759

    
760
    def owns_application(self, application):
761
        return application.owner == self
762

    
763
    def owns_project(self, project):
764
        return project.application.owner == self
765

    
766
    def is_project_member(self, project_or_application):
767
        return self.get_status_in_project(project_or_application) in \
768
                                        ProjectMembership.ASSOCIATED_STATES
769

    
770
    def is_project_accepted_member(self, project_or_application):
771
        return self.get_status_in_project(project_or_application) in \
772
                                            ProjectMembership.ACCEPTED_STATES
773

    
774
    def get_status_in_project(self, project_or_application):
775
        application = project_or_application
776
        if isinstance(project_or_application, Project):
777
            application = project_or_application.project
778
        return application.user_status(self)
779

    
780
    def get_membership(self, project):
781
        try:
782
            return ProjectMembership.objects.get(
783
                project=project,
784
                person=self)
785
        except ProjectMembership.DoesNotExist:
786
            return None
787

    
788
class AstakosUserAuthProviderManager(models.Manager):
789

    
790
    def active(self, **filters):
791
        return self.filter(active=True, **filters)
792

    
793
    def remove_unverified_providers(self, provider, **filters):
794
        try:
795
            existing = self.filter(module=provider, user__email_verified=False, **filters)
796
            for p in existing:
797
                p.user.delete()
798
        except:
799
            pass
800

    
801

    
802

    
803
class AstakosUserAuthProvider(models.Model):
804
    """
805
    Available user authentication methods.
806
    """
807
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
808
                                   null=True, default=None)
809
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
810
    module = models.CharField(_('Provider'), max_length=255, blank=False,
811
                                default='local')
812
    identifier = models.CharField(_('Third-party identifier'),
813
                                              max_length=255, null=True,
814
                                              blank=True)
815
    active = models.BooleanField(default=True)
816
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
817
                                   default='astakos')
818
    info_data = models.TextField(default="", null=True, blank=True)
819
    created = models.DateTimeField('Creation date', auto_now_add=True)
820

    
821
    objects = AstakosUserAuthProviderManager()
822

    
823
    class Meta:
824
        unique_together = (('identifier', 'module', 'user'), )
825
        ordering = ('module', 'created')
826

    
827
    def __init__(self, *args, **kwargs):
828
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
829
        try:
830
            self.info = json.loads(self.info_data)
831
            if not self.info:
832
                self.info = {}
833
        except Exception, e:
834
            self.info = {}
835

    
836
        for key,value in self.info.iteritems():
837
            setattr(self, 'info_%s' % key, value)
838

    
839

    
840
    @property
841
    def settings(self):
842
        return auth_providers.get_provider(self.module)
843

    
844
    @property
845
    def details_display(self):
846
        try:
847
          return self.settings.get_details_tpl_display % self.__dict__
848
        except:
849
          return ''
850

    
851
    @property
852
    def title_display(self):
853
        title_tpl = self.settings.get_title_display
854
        try:
855
            if self.settings.get_user_title_display:
856
                title_tpl = self.settings.get_user_title_display
857
        except Exception, e:
858
            pass
859
        try:
860
          return title_tpl % self.__dict__
861
        except:
862
          return self.settings.get_title_display % self.__dict__
863

    
864
    def can_remove(self):
865
        return self.user.can_remove_auth_provider(self.module)
866

    
867
    def delete(self, *args, **kwargs):
868
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
869
        if self.module == 'local':
870
            self.user.set_unusable_password()
871
            self.user.save()
872
        return ret
873

    
874
    def __repr__(self):
875
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
876

    
877
    def __unicode__(self):
878
        if self.identifier:
879
            return "%s:%s" % (self.module, self.identifier)
880
        if self.auth_backend:
881
            return "%s:%s" % (self.module, self.auth_backend)
882
        return self.module
883

    
884
    def save(self, *args, **kwargs):
885
        self.info_data = json.dumps(self.info)
886
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
887

    
888

    
889
class ExtendedManager(models.Manager):
890
    def _update_or_create(self, **kwargs):
891
        assert kwargs, \
892
            'update_or_create() must be passed at least one keyword argument'
893
        obj, created = self.get_or_create(**kwargs)
894
        defaults = kwargs.pop('defaults', {})
895
        if created:
896
            return obj, True, False
897
        else:
898
            try:
899
                params = dict(
900
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
901
                params.update(defaults)
902
                for attr, val in params.items():
903
                    if hasattr(obj, attr):
904
                        setattr(obj, attr, val)
905
                sid = transaction.savepoint()
906
                obj.save(force_update=True)
907
                transaction.savepoint_commit(sid)
908
                return obj, False, True
909
            except IntegrityError, e:
910
                transaction.savepoint_rollback(sid)
911
                try:
912
                    return self.get(**kwargs), False, False
913
                except self.model.DoesNotExist:
914
                    raise e
915

    
916
    update_or_create = _update_or_create
917

    
918

    
919
class AstakosUserQuota(models.Model):
920
    objects = ExtendedManager()
921
    capacity = intDecimalField()
922
    quantity = intDecimalField(default=0)
923
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
924
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
925
    resource = models.ForeignKey(Resource)
926
    user = models.ForeignKey(AstakosUser)
927

    
928
    class Meta:
929
        unique_together = ("resource", "user")
930

    
931
    def quota_values(self):
932
        return QuotaValues(
933
            quantity = self.quantity,
934
            capacity = self.capacity,
935
            import_limit = self.import_limit,
936
            export_limit = self.export_limit)
937

    
938

    
939
class ApprovalTerms(models.Model):
940
    """
941
    Model for approval terms
942
    """
943

    
944
    date = models.DateTimeField(
945
        _('Issue date'), db_index=True, auto_now_add=True)
946
    location = models.CharField(_('Terms location'), max_length=255)
947

    
948

    
949
class Invitation(models.Model):
950
    """
951
    Model for registring invitations
952
    """
953
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
954
                                null=True)
955
    realname = models.CharField(_('Real name'), max_length=255)
956
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
957
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
958
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
959
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
960
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
961

    
962
    def __init__(self, *args, **kwargs):
963
        super(Invitation, self).__init__(*args, **kwargs)
964
        if not self.id:
965
            self.code = _generate_invitation_code()
966

    
967
    def consume(self):
968
        self.is_consumed = True
969
        self.consumed = datetime.now()
970
        self.save()
971

    
972
    def __unicode__(self):
973
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
974

    
975

    
976
class EmailChangeManager(models.Manager):
977

    
978
    @transaction.commit_on_success
979
    def change_email(self, activation_key):
980
        """
981
        Validate an activation key and change the corresponding
982
        ``User`` if valid.
983

984
        If the key is valid and has not expired, return the ``User``
985
        after activating.
986

987
        If the key is not valid or has expired, return ``None``.
988

989
        If the key is valid but the ``User`` is already active,
990
        return ``None``.
991

992
        After successful email change the activation record is deleted.
993

994
        Throws ValueError if there is already
995
        """
996
        try:
997
            email_change = self.model.objects.get(
998
                activation_key=activation_key)
999
            if email_change.activation_key_expired():
1000
                email_change.delete()
1001
                raise EmailChange.DoesNotExist
1002
            # is there an active user with this address?
1003
            try:
1004
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1005
            except AstakosUser.DoesNotExist:
1006
                pass
1007
            else:
1008
                raise ValueError(_('The new email address is reserved.'))
1009
            # update user
1010
            user = AstakosUser.objects.get(pk=email_change.user_id)
1011
            old_email = user.email
1012
            user.email = email_change.new_email_address
1013
            user.save()
1014
            email_change.delete()
1015
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1016
                                                          user.email)
1017
            logger.log(LOGGING_LEVEL, msg)
1018
            return user
1019
        except EmailChange.DoesNotExist:
1020
            raise ValueError(_('Invalid activation key.'))
1021

    
1022

    
1023
class EmailChange(models.Model):
1024
    new_email_address = models.EmailField(
1025
        _(u'new e-mail address'),
1026
        help_text=_('Your old email address will be used until you verify your new one.'))
1027
    user = models.ForeignKey(
1028
        AstakosUser, unique=True, related_name='emailchanges')
1029
    requested_at = models.DateTimeField(auto_now_add=True)
1030
    activation_key = models.CharField(
1031
        max_length=40, unique=True, db_index=True)
1032

    
1033
    objects = EmailChangeManager()
1034

    
1035
    def get_url(self):
1036
        return reverse('email_change_confirm',
1037
                      kwargs={'activation_key': self.activation_key})
1038

    
1039
    def activation_key_expired(self):
1040
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1041
        return self.requested_at + expiration_date < datetime.now()
1042

    
1043

    
1044
class AdditionalMail(models.Model):
1045
    """
1046
    Model for registring invitations
1047
    """
1048
    owner = models.ForeignKey(AstakosUser)
1049
    email = models.EmailField()
1050

    
1051

    
1052
def _generate_invitation_code():
1053
    while True:
1054
        code = randint(1, 2L ** 63 - 1)
1055
        try:
1056
            Invitation.objects.get(code=code)
1057
            # An invitation with this code already exists, try again
1058
        except Invitation.DoesNotExist:
1059
            return code
1060

    
1061

    
1062
def get_latest_terms():
1063
    try:
1064
        term = ApprovalTerms.objects.order_by('-id')[0]
1065
        return term
1066
    except IndexError:
1067
        pass
1068
    return None
1069

    
1070
class PendingThirdPartyUser(models.Model):
1071
    """
1072
    Model for registring successful third party user authentications
1073
    """
1074
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1075
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1076
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1077
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1078
                                  null=True)
1079
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1080
                                 null=True)
1081
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1082
                                   null=True)
1083
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1084
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1085
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1086
    info = models.TextField(default="", null=True, blank=True)
1087

    
1088
    class Meta:
1089
        unique_together = ("provider", "third_party_identifier")
1090

    
1091
    def get_user_instance(self):
1092
        d = self.__dict__
1093
        d.pop('_state', None)
1094
        d.pop('id', None)
1095
        d.pop('token', None)
1096
        d.pop('created', None)
1097
        d.pop('info', None)
1098
        user = AstakosUser(**d)
1099

    
1100
        return user
1101

    
1102
    @property
1103
    def realname(self):
1104
        return '%s %s' %(self.first_name, self.last_name)
1105

    
1106
    @realname.setter
1107
    def realname(self, value):
1108
        parts = value.split(' ')
1109
        if len(parts) == 2:
1110
            self.first_name = parts[0]
1111
            self.last_name = parts[1]
1112
        else:
1113
            self.last_name = parts[0]
1114

    
1115
    def save(self, **kwargs):
1116
        if not self.id:
1117
            # set username
1118
            while not self.username:
1119
                username =  uuid.uuid4().hex[:30]
1120
                try:
1121
                    AstakosUser.objects.get(username = username)
1122
                except AstakosUser.DoesNotExist, e:
1123
                    self.username = username
1124
        super(PendingThirdPartyUser, self).save(**kwargs)
1125

    
1126
    def generate_token(self):
1127
        self.password = self.third_party_identifier
1128
        self.last_login = datetime.now()
1129
        self.token = default_token_generator.make_token(self)
1130

    
1131
class SessionCatalog(models.Model):
1132
    session_key = models.CharField(_('session key'), max_length=40)
1133
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1134

    
1135

    
1136
### PROJECTS ###
1137
################
1138

    
1139
def synced_model_metaclass(class_name, class_parents, class_attributes):
1140

    
1141
    new_attributes = {}
1142
    sync_attributes = {}
1143

    
1144
    for name, value in class_attributes.iteritems():
1145
        sync, underscore, rest = name.partition('_')
1146
        if sync == 'sync' and underscore == '_':
1147
            sync_attributes[rest] = value
1148
        else:
1149
            new_attributes[name] = value
1150

    
1151
    if 'prefix' not in sync_attributes:
1152
        m = ("you did not specify a 'sync_prefix' attribute "
1153
             "in class '%s'" % (class_name,))
1154
        raise ValueError(m)
1155

    
1156
    prefix = sync_attributes.pop('prefix')
1157
    class_name = sync_attributes.pop('classname', prefix + '_model')
1158

    
1159
    for name, value in sync_attributes.iteritems():
1160
        newname = prefix + '_' + name
1161
        if newname in new_attributes:
1162
            m = ("class '%s' was specified with prefix '%s' "
1163
                 "but it already has an attribute named '%s'"
1164
                 % (class_name, prefix, newname))
1165
            raise ValueError(m)
1166

    
1167
        new_attributes[newname] = value
1168

    
1169
    newclass = type(class_name, class_parents, new_attributes)
1170
    return newclass
1171

    
1172

    
1173
def make_synced(prefix='sync', name='SyncedState'):
1174

    
1175
    the_name = name
1176
    the_prefix = prefix
1177

    
1178
    class SyncedState(models.Model):
1179

    
1180
        sync_classname      = the_name
1181
        sync_prefix         = the_prefix
1182
        __metaclass__       = synced_model_metaclass
1183

    
1184
        sync_new_state      = models.BigIntegerField(null=True)
1185
        sync_synced_state   = models.BigIntegerField(null=True)
1186
        STATUS_SYNCED       = 0
1187
        STATUS_PENDING      = 1
1188
        sync_status         = models.IntegerField(db_index=True)
1189

    
1190
        class Meta:
1191
            abstract = True
1192

    
1193
        class NotSynced(Exception):
1194
            pass
1195

    
1196
        def sync_init_state(self, state):
1197
            self.sync_synced_state = state
1198
            self.sync_new_state = state
1199
            self.sync_status = self.STATUS_SYNCED
1200

    
1201
        def sync_get_status(self):
1202
            return self.sync_status
1203

    
1204
        def sync_set_status(self):
1205
            if self.sync_new_state != self.sync_synced_state:
1206
                self.sync_status = self.STATUS_PENDING
1207
            else:
1208
                self.sync_status = self.STATUS_SYNCED
1209

    
1210
        def sync_set_synced(self):
1211
            self.sync_synced_state = self.sync_new_state
1212
            self.sync_status = self.STATUS_SYNCED
1213

    
1214
        def sync_get_synced_state(self):
1215
            return self.sync_synced_state
1216

    
1217
        def sync_set_new_state(self, new_state):
1218
            self.sync_new_state = new_state
1219
            self.sync_set_status()
1220

    
1221
        def sync_get_new_state(self):
1222
            return self.sync_new_state
1223

    
1224
        def sync_set_synced_state(self, synced_state):
1225
            self.sync_synced_state = synced_state
1226
            self.sync_set_status()
1227

    
1228
        def sync_get_pending_objects(self):
1229
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1230
            return self.objects.filter(**kw)
1231

    
1232
        def sync_get_synced_objects(self):
1233
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1234
            return self.objects.filter(**kw)
1235

    
1236
        def sync_verify_get_synced_state(self):
1237
            status = self.sync_get_status()
1238
            state = self.sync_get_synced_state()
1239
            verified = (status == self.STATUS_SYNCED)
1240
            return state, verified
1241

    
1242
        def sync_is_synced(self):
1243
            state, verified = self.sync_verify_get_synced_state()
1244
            return verified
1245

    
1246
    return SyncedState
1247

    
1248
SyncedState = make_synced(prefix='sync', name='SyncedState')
1249

    
1250

    
1251
class ProjectApplicationManager(ForUpdateManager):
1252

    
1253
    def user_visible_projects(self, *filters, **kw_filters):
1254
        model = self.model
1255
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1256

    
1257
    def user_visible_by_chain(self, flt):
1258
        model = self.model
1259
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1260
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1261
        by_chain = dict(pending.annotate(models.Max('id')))
1262
        by_chain.update(approved.annotate(models.Max('id')))
1263
        return self.filter(flt, id__in=by_chain.values())
1264

    
1265
    def user_accessible_projects(self, user):
1266
        """
1267
        Return projects accessed by specified user.
1268
        """
1269
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1270
                               Q(project__projectmembership__person=user)
1271

    
1272
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1273

    
1274
    def search_by_name(self, *search_strings):
1275
        q = Q()
1276
        for s in search_strings:
1277
            q = q | Q(name__icontains=s)
1278
        return self.filter(q)
1279

    
1280
    def latest_of_chain(self, chain_id):
1281
        try:
1282
            return self.filter(chain=chain_id).order_by('-id')[0]
1283
        except IndexError:
1284
            return None
1285

    
1286
USER_STATUS_DISPLAY = {
1287
      0: _('Join requested'),
1288
      1: _('Accepted member'),
1289
     10: _('Suspended'),
1290
    100: _('Terminated'),
1291
    200: _('Removed'),
1292
     -1: _('Not a member'),
1293
}
1294

    
1295

    
1296
class Chain(models.Model):
1297
    chain  =   models.AutoField(primary_key=True)
1298

    
1299
def new_chain():
1300
    c = Chain.objects.create()
1301
    chain = c.chain
1302
    c.delete()
1303
    return chain
1304

    
1305

    
1306
class ProjectApplication(models.Model):
1307
    applicant               =   models.ForeignKey(
1308
                                    AstakosUser,
1309
                                    related_name='projects_applied',
1310
                                    db_index=True)
1311

    
1312
    PENDING     =    0
1313
    APPROVED    =    1
1314
    REPLACED    =    2
1315
    DENIED      =    3
1316
    DISMISSED   =    4
1317
    CANCELLED   =    5
1318

    
1319
    state                   =   models.IntegerField(default=PENDING,
1320
                                                    db_index=True)
1321

    
1322
    owner                   =   models.ForeignKey(
1323
                                    AstakosUser,
1324
                                    related_name='projects_owned',
1325
                                    db_index=True)
1326

    
1327
    chain                   =   models.IntegerField()
1328
    precursor_application   =   models.ForeignKey('ProjectApplication',
1329
                                                  null=True,
1330
                                                  blank=True)
1331

    
1332
    name                    =   models.CharField(max_length=80)
1333
    homepage                =   models.URLField(max_length=255, null=True)
1334
    description             =   models.TextField(null=True, blank=True)
1335
    start_date              =   models.DateTimeField(null=True, blank=True)
1336
    end_date                =   models.DateTimeField()
1337
    member_join_policy      =   models.IntegerField()
1338
    member_leave_policy     =   models.IntegerField()
1339
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1340
    resource_grants         =   models.ManyToManyField(
1341
                                    Resource,
1342
                                    null=True,
1343
                                    blank=True,
1344
                                    through='ProjectResourceGrant')
1345
    comments                =   models.TextField(null=True, blank=True)
1346
    issue_date              =   models.DateTimeField(auto_now_add=True)
1347
    response_date           =   models.DateTimeField(null=True, blank=True)
1348

    
1349
    objects                 =   ProjectApplicationManager()
1350

    
1351
    # Compiled queries
1352
    Q_PENDING  = Q(state=PENDING)
1353
    Q_APPROVED = Q(state=APPROVED)
1354
    Q_DENIED   = Q(state=DENIED)
1355

    
1356
    class Meta:
1357
        unique_together = ("chain", "id")
1358

    
1359
    def __unicode__(self):
1360
        return "%s applied by %s" % (self.name, self.applicant)
1361

    
1362
    # TODO: Move to a more suitable place
1363
    APPLICATION_STATE_DISPLAY = {
1364
        PENDING  : _('Pending review'),
1365
        APPROVED : _('Active'),
1366
        REPLACED : _('Replaced'),
1367
        DENIED   : _('Denied'),
1368
        DISMISSED: _('Dismissed'),
1369
        CANCELLED: _('Cancelled')
1370
    }
1371

    
1372
    def get_project(self):
1373
        try:
1374
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1375
            return Project
1376
        except Project.DoesNotExist, e:
1377
            return None
1378

    
1379
    def state_display(self):
1380
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1381

    
1382
    def add_resource_policy(self, service, resource, uplimit):
1383
        """Raises ObjectDoesNotExist, IntegrityError"""
1384
        q = self.projectresourcegrant_set
1385
        resource = Resource.objects.get(service__name=service, name=resource)
1386
        q.create(resource=resource, member_capacity=uplimit)
1387

    
1388
    def user_status(self, user):
1389
        try:
1390
            project = self.get_project()
1391
            if not project:
1392
                return -1
1393
            membership = project.projectmembership_set
1394
            membership = membership.exclude(state=ProjectMembership.REMOVED)
1395
            membership = membership.get(person=user)
1396
            status = membership.state
1397
        except ProjectMembership.DoesNotExist:
1398
            return -1
1399

    
1400
        return status
1401

    
1402
    def user_status_display(self, user):
1403
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1404

    
1405
    def members_count(self):
1406
        return self.project.approved_memberships.count()
1407

    
1408
    @property
1409
    def grants(self):
1410
        return self.projectresourcegrant_set.values(
1411
            'member_capacity', 'resource__name', 'resource__service__name')
1412

    
1413
    @property
1414
    def resource_policies(self):
1415
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1416

    
1417
    @resource_policies.setter
1418
    def resource_policies(self, policies):
1419
        for p in policies:
1420
            service = p.get('service', None)
1421
            resource = p.get('resource', None)
1422
            uplimit = p.get('uplimit', 0)
1423
            self.add_resource_policy(service, resource, uplimit)
1424

    
1425
    def pending_modifications(self):
1426
        q = self.chained_applications()
1427
        q = q.filter(~Q(id=self.id) & Q(state=self.PENDING))
1428
        q = q.order_by('id')
1429
        return q
1430

    
1431
    def last_pending(self):
1432
        try:
1433
            return self.pending_modifications().order_by('-id')[0]
1434
        except IndexError:
1435
            return None
1436

    
1437
    def is_modification(self):
1438
        if self.state != self.PENDING:
1439
            return False
1440
        parents = self.chained_applications().filter(id__lt=self.id)
1441
        parents = parents.filter(state__in=[self.APPROVED])
1442
        return parents.count() > 0
1443

    
1444
    def chained_applications(self):
1445
        return ProjectApplication.objects.filter(chain=self.chain)
1446

    
1447
    def has_pending_modifications(self):
1448
        return bool(self.last_pending())
1449

    
1450
    def get_project(self):
1451
        try:
1452
            return Project.objects.get(id=self.chain)
1453
        except Project.DoesNotExist:
1454
            return None
1455

    
1456
    def _get_project_for_update(self):
1457
        try:
1458
            objects = Project.objects.select_for_update()
1459
            project = objects.get(id=self.chain)
1460
            return project
1461
        except Project.DoesNotExist:
1462
            return None
1463

    
1464
    def can_cancel(self):
1465
        return self.state == self.PENDING
1466

    
1467
    def cancel(self):
1468
        if not self.can_cancel():
1469
            m = _("cannot cancel: application '%s' in state '%s'") % (
1470
                    self.id, self.state)
1471
            raise AssertionError(m)
1472

    
1473
        self.state = self.CANCELLED
1474
        self.save()
1475

    
1476
    def can_dismiss(self):
1477
        return self.state == self.DENIED
1478

    
1479
    def dismiss(self):
1480
        if not self.can_dismiss():
1481
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1482
                    self.id, self.state)
1483
            raise AssertionError(m)
1484

    
1485
        self.state = self.DISMISSED
1486
        self.save()
1487

    
1488
    def can_deny(self):
1489
        return self.state == self.PENDING
1490

    
1491
    def deny(self):
1492
        if not self.can_deny():
1493
            m = _("cannot deny: application '%s' in state '%s'") % (
1494
                    self.id, self.state)
1495
            raise AssertionError(m)
1496

    
1497
        self.state = self.DENIED
1498
        self.response_date = datetime.now()
1499
        self.save()
1500

    
1501
    def can_approve(self):
1502
        return self.state == self.PENDING
1503

    
1504
    def approve(self, approval_user=None):
1505
        """
1506
        If approval_user then during owner membership acceptance
1507
        it is checked whether the request_user is eligible.
1508

1509
        Raises:
1510
            PermissionDenied
1511
        """
1512

    
1513
        if not transaction.is_managed():
1514
            raise AssertionError("NOPE")
1515

    
1516
        new_project_name = self.name
1517
        if not self.can_approve():
1518
            m = _("cannot approve: project '%s' in state '%s'") % (
1519
                    new_project_name, self.state)
1520
            raise AssertionError(m) # invalid argument
1521

    
1522
        now = datetime.now()
1523
        project = self._get_project_for_update()
1524

    
1525
        try:
1526
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1527
            conflicting_project = Project.objects.get(q)
1528
            if (conflicting_project != project):
1529
                m = (_("cannot approve: project with name '%s' "
1530
                       "already exists (serial: %s)") % (
1531
                        new_project_name, conflicting_project.id))
1532
                raise PermissionDenied(m) # invalid argument
1533
        except Project.DoesNotExist:
1534
            pass
1535

    
1536
        new_project = False
1537
        if project is None:
1538
            new_project = True
1539
            project = Project(id=self.chain)
1540

    
1541
        project.name = new_project_name
1542
        project.application = self
1543
        project.last_approval_date = now
1544
        if not new_project:
1545
            project.is_modified = True
1546

    
1547
        project.save()
1548

    
1549
        self.state = self.APPROVED
1550
        self.response_date = now
1551
        self.save()
1552

    
1553
    @property
1554
    def member_join_policy_display(self):
1555
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1556

    
1557
    @property
1558
    def member_leave_policy_display(self):
1559
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1560

    
1561
class ProjectResourceGrant(models.Model):
1562

    
1563
    resource                =   models.ForeignKey(Resource)
1564
    project_application     =   models.ForeignKey(ProjectApplication,
1565
                                                  null=True)
1566
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1567
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1568
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1569
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1570
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1571
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1572

    
1573
    objects = ExtendedManager()
1574

    
1575
    class Meta:
1576
        unique_together = ("resource", "project_application")
1577

    
1578
    def member_quota_values(self):
1579
        return QuotaValues(
1580
            quantity = 0,
1581
            capacity = self.member_capacity,
1582
            import_limit = self.member_import_limit,
1583
            export_limit = self.member_export_limit)
1584

    
1585
    def display_member_capacity(self):
1586
        if self.member_capacity:
1587
            if self.resource.unit:
1588
                return ProjectResourceGrant.display_filesize(
1589
                    self.member_capacity)
1590
            else:
1591
                if math.isinf(self.member_capacity):
1592
                    return 'Unlimited'
1593
                else:
1594
                    return self.member_capacity
1595
        else:
1596
            return 'Unlimited'
1597

    
1598
    def __str__(self):
1599
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1600
                                        self.display_member_capacity())
1601

    
1602
    @classmethod
1603
    def display_filesize(cls, value):
1604
        try:
1605
            value = float(value)
1606
        except:
1607
            return
1608
        else:
1609
            if math.isinf(value):
1610
                return 'Unlimited'
1611
            if value > 1:
1612
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1613
                                [0, 0, 0, 0, 0, 0])
1614
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1615
                quotient = float(value) / 1024**exponent
1616
                unit, value_decimals = unit_list[exponent]
1617
                format_string = '{0:.%sf} {1}' % (value_decimals)
1618
                return format_string.format(quotient, unit)
1619
            if value == 0:
1620
                return '0 bytes'
1621
            if value == 1:
1622
                return '1 byte'
1623
            else:
1624
               return '0'
1625

    
1626

    
1627
class ProjectManager(ForUpdateManager):
1628

    
1629
    def terminated_projects(self):
1630
        q = self.model.Q_TERMINATED
1631
        return self.filter(q)
1632

    
1633
    def not_terminated_projects(self):
1634
        q = ~self.model.Q_TERMINATED
1635
        return self.filter(q)
1636

    
1637
    def terminating_projects(self):
1638
        q = self.model.Q_TERMINATED & Q(is_active=True)
1639
        return self.filter(q)
1640

    
1641
    def deactivated_projects(self):
1642
        q = self.model.Q_DEACTIVATED
1643
        return self.filter(q)
1644

    
1645
    def deactivating_projects(self):
1646
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1647
        return self.filter(q)
1648

    
1649
    def modified_projects(self):
1650
        return self.filter(is_modified=True)
1651

    
1652
    def reactivating_projects(self):
1653
        return self.filter(state=Project.APPROVED, is_active=False)
1654

    
1655
    def expired_projects(self):
1656
        q = (~Q(state=Project.TERMINATED) &
1657
              Q(application__end_date__lt=datetime.now()))
1658
        return self.filter(q)
1659

    
1660

    
1661
class Project(models.Model):
1662

    
1663
    application                 =   models.OneToOneField(
1664
                                            ProjectApplication,
1665
                                            related_name='project')
1666
    last_approval_date          =   models.DateTimeField(null=True)
1667

    
1668
    members                     =   models.ManyToManyField(
1669
                                            AstakosUser,
1670
                                            through='ProjectMembership')
1671

    
1672
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1673
    deactivation_date           =   models.DateTimeField(null=True)
1674

    
1675
    creation_date               =   models.DateTimeField(auto_now_add=True)
1676
    name                        =   models.CharField(
1677
                                            max_length=80,
1678
                                            db_index=True,
1679
                                            unique=True)
1680

    
1681
    APPROVED    = 1
1682
    SUSPENDED   = 10
1683
    TERMINATED  = 100
1684

    
1685
    is_modified                 =   models.BooleanField(default=False,
1686
                                                        db_index=True)
1687
    is_active                   =   models.BooleanField(default=True,
1688
                                                        db_index=True)
1689
    state                       =   models.IntegerField(default=APPROVED,
1690
                                                        db_index=True)
1691

    
1692
    objects     =   ProjectManager()
1693

    
1694
    # Compiled queries
1695
    Q_TERMINATED  = Q(state=TERMINATED)
1696
    Q_SUSPENDED   = Q(state=SUSPENDED)
1697
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1698

    
1699
    def __str__(self):
1700
        return _("<project %s '%s'>") % (self.id, self.application.name)
1701

    
1702
    __repr__ = __str__
1703

    
1704
    STATE_DISPLAY = {
1705
        APPROVED   : 'APPROVED',
1706
        SUSPENDED  : 'SUSPENDED',
1707
        TERMINATED : 'TERMINATED'
1708
        }
1709

    
1710
    def state_display(self):
1711
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1712

    
1713
    def expiration_info(self):
1714
        return (str(self.id), self.name, self.state_display(),
1715
                str(self.application.end_date))
1716

    
1717
    def is_deactivated(self, reason=None):
1718
        if reason is not None:
1719
            return self.state == reason
1720

    
1721
        return self.state != self.APPROVED
1722

    
1723
    def is_deactivating(self, reason=None):
1724
        if not self.is_active:
1725
            return False
1726

    
1727
        return self.is_deactivated(reason)
1728

    
1729
    def is_deactivated_strict(self, reason=None):
1730
        if self.is_active:
1731
            return False
1732

    
1733
        return self.is_deactivated(reason)
1734

    
1735
    ### Deactivation calls
1736

    
1737
    def deactivate(self):
1738
        self.deactivation_date = datetime.now()
1739
        self.is_active = False
1740

    
1741
    def reactivate(self):
1742
        self.deactivation_date = None
1743
        self.is_active = True
1744

    
1745
    def terminate(self):
1746
        self.deactivation_reason = 'TERMINATED'
1747
        self.state = self.TERMINATED
1748
        self.save()
1749

    
1750
    def suspend(self):
1751
        self.deactivation_reason = 'SUSPENDED'
1752
        self.state = self.SUSPENDED
1753
        self.save()
1754

    
1755
    def resume(self):
1756
        self.deactivation_reason = None
1757
        self.state = self.APPROVED
1758
        self.save()
1759

    
1760
    ### Logical checks
1761

    
1762
    def is_inconsistent(self):
1763
        now = datetime.now()
1764
        dates = [self.creation_date,
1765
                 self.last_approval_date,
1766
                 self.deactivation_date]
1767
        return any([date > now for date in dates])
1768

    
1769
    def is_active_strict(self):
1770
        return self.is_active and self.state == self.APPROVED
1771

    
1772
    def is_approved(self):
1773
        return self.state == self.APPROVED
1774

    
1775
    @property
1776
    def is_alive(self):
1777
        return not self.is_terminated
1778

    
1779
    @property
1780
    def is_terminated(self):
1781
        return self.is_deactivated(self.TERMINATED)
1782

    
1783
    @property
1784
    def is_suspended(self):
1785
        return self.is_deactivated(self.SUSPENDED)
1786

    
1787
    def violates_resource_grants(self):
1788
        return False
1789

    
1790
    def violates_members_limit(self, adding=0):
1791
        application = self.application
1792
        limit = application.limit_on_members_number
1793
        if limit is None:
1794
            return False
1795
        return (len(self.approved_members) + adding > limit)
1796

    
1797

    
1798
    ### Other
1799

    
1800
    def count_pending_memberships(self):
1801
        memb_set = self.projectmembership_set
1802
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1803
        return memb_count
1804

    
1805
    @property
1806
    def approved_memberships(self):
1807
        query = ProjectMembership.Q_ACCEPTED_STATES
1808
        return self.projectmembership_set.filter(query)
1809

    
1810
    @property
1811
    def approved_members(self):
1812
        return [m.person for m in self.approved_memberships]
1813

    
1814
    def add_member(self, user):
1815
        """
1816
        Raises:
1817
            django.exceptions.PermissionDenied
1818
            astakos.im.models.AstakosUser.DoesNotExist
1819
        """
1820
        if isinstance(user, int):
1821
            user = AstakosUser.objects.get(user=user)
1822

    
1823
        m, created = ProjectMembership.objects.get_or_create(
1824
            person=user, project=self
1825
        )
1826
        m.accept()
1827

    
1828
    def remove_member(self, user):
1829
        """
1830
        Raises:
1831
            django.exceptions.PermissionDenied
1832
            astakos.im.models.AstakosUser.DoesNotExist
1833
            astakos.im.models.ProjectMembership.DoesNotExist
1834
        """
1835
        if isinstance(user, int):
1836
            user = AstakosUser.objects.get(user=user)
1837

    
1838
        m = ProjectMembership.objects.get(person=user, project=self)
1839
        m.remove()
1840

    
1841

    
1842
class PendingMembershipError(Exception):
1843
    pass
1844

    
1845

    
1846
class ProjectMembershipManager(ForUpdateManager):
1847
    pass
1848

    
1849
class ProjectMembership(models.Model):
1850

    
1851
    person              =   models.ForeignKey(AstakosUser)
1852
    request_date        =   models.DateField(auto_now_add=True)
1853
    project             =   models.ForeignKey(Project)
1854

    
1855
    REQUESTED           =   0
1856
    ACCEPTED            =   1
1857
    # User deactivation
1858
    USER_SUSPENDED      =   10
1859
    # Project deactivation
1860
    PROJECT_DEACTIVATED =   100
1861

    
1862
    REMOVED             =   200
1863

    
1864
    ASSOCIATED_STATES   =   set([REQUESTED,
1865
                                 ACCEPTED,
1866
                                 USER_SUSPENDED,
1867
                                 PROJECT_DEACTIVATED])
1868

    
1869
    ACCEPTED_STATES     =   set([ACCEPTED,
1870
                                 USER_SUSPENDED,
1871
                                 PROJECT_DEACTIVATED])
1872

    
1873
    state               =   models.IntegerField(default=REQUESTED,
1874
                                                db_index=True)
1875
    is_pending          =   models.BooleanField(default=False, db_index=True)
1876
    is_active           =   models.BooleanField(default=False, db_index=True)
1877
    application         =   models.ForeignKey(
1878
                                ProjectApplication,
1879
                                null=True,
1880
                                related_name='memberships')
1881
    pending_application =   models.ForeignKey(
1882
                                ProjectApplication,
1883
                                null=True,
1884
                                related_name='pending_memberships')
1885
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1886

    
1887
    acceptance_date     =   models.DateField(null=True, db_index=True)
1888
    leave_request_date  =   models.DateField(null=True)
1889

    
1890
    objects     =   ProjectMembershipManager()
1891

    
1892
    # Compiled queries
1893
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1894

    
1895
    def get_combined_state(self):
1896
        return self.state, self.is_active, self.is_pending
1897

    
1898
    class Meta:
1899
        unique_together = ("person", "project")
1900
        #index_together = [["project", "state"]]
1901

    
1902
    def __str__(self):
1903
        return _("<'%s' membership in '%s'>") % (
1904
                self.person.username, self.project)
1905

    
1906
    __repr__ = __str__
1907

    
1908
    def __init__(self, *args, **kwargs):
1909
        self.state = self.REQUESTED
1910
        super(ProjectMembership, self).__init__(*args, **kwargs)
1911

    
1912
    def _set_history_item(self, reason, date=None):
1913
        if isinstance(reason, basestring):
1914
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1915

    
1916
        history_item = ProjectMembershipHistory(
1917
                            serial=self.id,
1918
                            person=self.person_id,
1919
                            project=self.project_id,
1920
                            date=date or datetime.now(),
1921
                            reason=reason)
1922
        history_item.save()
1923
        serial = history_item.id
1924

    
1925
    def can_accept(self):
1926
        return self.state == self.REQUESTED
1927

    
1928
    def accept(self):
1929
        if self.is_pending:
1930
            m = _("%s: attempt to accept while is pending") % (self,)
1931
            raise AssertionError(m)
1932

    
1933
        if not self.can_accept():
1934
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
1935
            raise AssertionError(m)
1936

    
1937
        now = datetime.now()
1938
        self.acceptance_date = now
1939
        self._set_history_item(reason='ACCEPT', date=now)
1940
        if self.project.is_approved():
1941
            self.state = self.ACCEPTED
1942
            self.is_pending = True
1943
        else:
1944
            self.state = self.PROJECT_DEACTIVATED
1945

    
1946
        self.save()
1947

    
1948
    def can_leave(self):
1949
        return self.can_remove()
1950

    
1951
    def can_remove(self):
1952
        return self.state in self.ACCEPTED_STATES
1953

    
1954
    def remove(self):
1955
        if self.is_pending:
1956
            m = _("%s: attempt to remove while is pending") % (self,)
1957
            raise AssertionError(m)
1958

    
1959
        if not self.can_remove():
1960
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
1961
            raise AssertionError(m)
1962

    
1963
        self._set_history_item(reason='REMOVE')
1964
        self.state = self.REMOVED
1965
        self.is_pending = True
1966
        self.save()
1967

    
1968
    def can_reject(self):
1969
        return self.state == self.REQUESTED
1970

    
1971
    def reject(self):
1972
        if self.is_pending:
1973
            m = _("%s: attempt to reject while is pending") % (self,)
1974
            raise AssertionError(m)
1975

    
1976
        if not self.can_reject():
1977
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
1978
            raise AssertionError(m)
1979

    
1980
        # rejected requests don't need sync,
1981
        # because they were never effected
1982
        self._set_history_item(reason='REJECT')
1983
        self.delete()
1984

    
1985
    def can_cancel(self):
1986
        return self.state == self.REQUESTED
1987

    
1988
    def cancel(self):
1989
        if self.is_pending:
1990
            m = _("%s: attempt to cancel while is pending") % (self,)
1991
            raise AssertionError(m)
1992

    
1993
        if not self.can_cancel():
1994
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
1995
            raise AssertionError(m)
1996

    
1997
        # rejected requests don't need sync,
1998
        # because they were never effected
1999
        self._set_history_item(reason='CANCEL')
2000
        self.delete()
2001

    
2002
    def get_diff_quotas(self, sub_list=None, add_list=None):
2003
        if sub_list is None:
2004
            sub_list = []
2005

    
2006
        if add_list is None:
2007
            add_list = []
2008

    
2009
        sub_append = sub_list.append
2010
        add_append = add_list.append
2011
        holder = self.person.uuid
2012

    
2013
        synced_application = self.application
2014
        if synced_application is not None:
2015
            cur_grants = synced_application.projectresourcegrant_set.all()
2016
            for grant in cur_grants:
2017
                sub_append(QuotaLimits(
2018
                               holder       = holder,
2019
                               resource     = str(grant.resource),
2020
                               capacity     = grant.member_capacity,
2021
                               import_limit = grant.member_import_limit,
2022
                               export_limit = grant.member_export_limit))
2023

    
2024
        pending_application = self.pending_application
2025
        if pending_application is not None:
2026
            new_grants = pending_application.projectresourcegrant_set.all()
2027
            for new_grant in new_grants:
2028
                add_append(QuotaLimits(
2029
                               holder       = holder,
2030
                               resource     = str(new_grant.resource),
2031
                               capacity     = new_grant.member_capacity,
2032
                               import_limit = new_grant.member_import_limit,
2033
                               export_limit = new_grant.member_export_limit))
2034

    
2035
        return (sub_list, add_list)
2036

    
2037
    def set_sync(self):
2038
        if not self.is_pending:
2039
            m = _("%s: attempt to sync a non pending membership") % (self,)
2040
            raise AssertionError(m)
2041

    
2042
        state = self.state
2043
        if state == self.ACCEPTED:
2044
            pending_application = self.pending_application
2045
            if pending_application is None:
2046
                m = _("%s: attempt to sync an empty pending application") % (
2047
                    self,)
2048
                raise AssertionError(m)
2049

    
2050
            self.application = pending_application
2051
            self.is_active = True
2052

    
2053
            self.pending_application = None
2054
            self.pending_serial = None
2055

    
2056
            # project.application may have changed in the meantime,
2057
            # in which case we stay PENDING;
2058
            # we are safe to check due to select_for_update
2059
            if self.application == self.project.application:
2060
                self.is_pending = False
2061
            self.save()
2062

    
2063
        elif state == self.PROJECT_DEACTIVATED:
2064
            if self.pending_application:
2065
                m = _("%s: attempt to sync in state '%s' "
2066
                      "with a pending application") % (self, state)
2067
                raise AssertionError(m)
2068

    
2069
            self.application = None
2070
            self.is_active = False
2071
            self.pending_serial = None
2072
            self.is_pending = False
2073
            self.save()
2074

    
2075
        elif state == self.REMOVED:
2076
            self.delete()
2077

    
2078
        else:
2079
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2080
            raise AssertionError(m)
2081

    
2082
    def reset_sync(self):
2083
        if not self.is_pending:
2084
            m = _("%s: attempt to reset a non pending membership") % (self,)
2085
            raise AssertionError(m)
2086

    
2087
        state = self.state
2088
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
2089
            self.pending_application = None
2090
            self.pending_serial = None
2091
            self.save()
2092
        else:
2093
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2094
            raise AssertionError(m)
2095

    
2096
class Serial(models.Model):
2097
    serial  =   models.AutoField(primary_key=True)
2098

    
2099
def new_serial():
2100
    s = Serial.objects.create()
2101
    serial = s.serial
2102
    s.delete()
2103
    return serial
2104

    
2105
def sync_finish_serials(serials_to_ack=None):
2106
    if serials_to_ack is None:
2107
        serials_to_ack = qh_query_serials([])
2108

    
2109
    serials_to_ack = set(serials_to_ack)
2110
    sfu = ProjectMembership.objects.select_for_update()
2111
    memberships = list(sfu.filter(pending_serial__isnull=False))
2112

    
2113
    if memberships:
2114
        for membership in memberships:
2115
            serial = membership.pending_serial
2116
            if serial in serials_to_ack:
2117
                membership.set_sync()
2118
            else:
2119
                membership.reset_sync()
2120

    
2121
        transaction.commit()
2122

    
2123
    qh_ack_serials(list(serials_to_ack))
2124
    return len(memberships)
2125

    
2126
def pre_sync_projects(sync=True):
2127
    ACCEPTED = ProjectMembership.ACCEPTED
2128
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2129
    psfu = Project.objects.select_for_update()
2130

    
2131
    modified = list(psfu.modified_projects())
2132
    if sync:
2133
        for project in modified:
2134
            objects = project.projectmembership_set.select_for_update()
2135

    
2136
            memberships = objects.filter(state=ACCEPTED)
2137
            for membership in memberships:
2138
                membership.is_pending = True
2139
                membership.save()
2140

    
2141
    reactivating = list(psfu.reactivating_projects())
2142
    if sync:
2143
        for project in reactivating:
2144
            objects = project.projectmembership_set.select_for_update()
2145

    
2146
            memberships = objects.filter(state=PROJECT_DEACTIVATED)
2147
            for membership in memberships:
2148
                membership.is_pending = True
2149
                membership.state = ACCEPTED
2150
                membership.save()
2151

    
2152
    deactivating = list(psfu.deactivating_projects())
2153
    if sync:
2154
        for project in deactivating:
2155
            objects = project.projectmembership_set.select_for_update()
2156

    
2157
            # Note: we keep a user-level deactivation
2158
            # (e.g. USER_SUSPENDED) intact
2159
            memberships = objects.filter(state=ACCEPTED)
2160
            for membership in memberships:
2161
                membership.is_pending = True
2162
                membership.state = PROJECT_DEACTIVATED
2163
                membership.save()
2164

    
2165
    return (modified, reactivating, deactivating)
2166

    
2167
def do_sync_projects():
2168

    
2169
    ACCEPTED = ProjectMembership.ACCEPTED
2170
    objects = ProjectMembership.objects.select_for_update()
2171

    
2172
    sub_quota, add_quota = [], []
2173

    
2174
    serial = new_serial()
2175

    
2176
    pending = objects.filter(is_pending=True)
2177
    for membership in pending:
2178

    
2179
        if membership.pending_application:
2180
            m = "%s: impossible: pending_application is not None (%s)" % (
2181
                membership, membership.pending_application)
2182
            raise AssertionError(m)
2183
        if membership.pending_serial:
2184
            m = "%s: impossible: pending_serial is not None (%s)" % (
2185
                membership, membership.pending_serial)
2186
            raise AssertionError(m)
2187

    
2188
        if membership.state == ACCEPTED:
2189
            membership.pending_application = membership.project.application
2190

    
2191
        membership.pending_serial = serial
2192
        membership.get_diff_quotas(sub_quota, add_quota)
2193
        membership.save()
2194

    
2195
    transaction.commit()
2196
    # ProjectApplication.approve() unblocks here
2197
    # and can set PENDING an already PENDING membership
2198
    # which has been scheduled to sync with the old project.application
2199
    # Need to check in ProjectMembership.set_sync()
2200

    
2201
    r = qh_add_quota(serial, sub_quota, add_quota)
2202
    if r:
2203
        m = "cannot sync serial: %d" % serial
2204
        raise RuntimeError(m)
2205

    
2206
    return serial
2207

    
2208
def post_sync_projects():
2209
    ACCEPTED = ProjectMembership.ACCEPTED
2210
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2211
    psfu = Project.objects.select_for_update()
2212

    
2213
    modified = psfu.modified_projects()
2214
    for project in modified:
2215
        objects = project.projectmembership_set.select_for_update()
2216

    
2217
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2218
        if not memberships:
2219
            project.is_modified = False
2220
            project.save()
2221

    
2222
    reactivating = psfu.reactivating_projects()
2223
    for project in reactivating:
2224
        objects = project.projectmembership_set.select_for_update()
2225
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2226
                                          Q(is_pending=True)))
2227
        if not memberships:
2228
            project.reactivate()
2229
            project.save()
2230

    
2231
    deactivating = psfu.deactivating_projects()
2232
    for project in deactivating:
2233
        objects = project.projectmembership_set.select_for_update()
2234

    
2235
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2236
                                          Q(is_pending=True)))
2237
        if not memberships:
2238
            project.deactivate()
2239
            project.save()
2240

    
2241
    transaction.commit()
2242

    
2243
def _sync_projects(sync):
2244
    sync_finish_serials()
2245
    # Informative only -- no select_for_update()
2246
    pending = list(ProjectMembership.objects.filter(is_pending=True))
2247

    
2248
    projects_log = pre_sync_projects(sync)
2249
    if sync:
2250
        serial = do_sync_projects()
2251
        sync_finish_serials([serial])
2252
        post_sync_projects()
2253

    
2254
    return (pending, projects_log)
2255

    
2256
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2257
    return lock_sync(_sync_projects,
2258
                     args=[sync],
2259
                     retries=retries,
2260
                     retry_wait=retry_wait)
2261

    
2262

    
2263
def all_users_quotas(users):
2264
    quotas = {}
2265
    for user in users:
2266
        quotas[user.uuid] = user.all_quotas()
2267
    return quotas
2268

    
2269
def _sync_users(users, sync):
2270
    sync_finish_serials()
2271

    
2272
    existing, nonexisting = qh_check_users(users)
2273
    resources = get_resource_names()
2274
    registered_quotas = qh_get_quota_limits(existing, resources)
2275
    astakos_quotas = all_users_quotas(users)
2276

    
2277
    if sync:
2278
        r = register_users(nonexisting)
2279
        r = send_quotas(astakos_quotas)
2280

    
2281
    return (existing, nonexisting, registered_quotas, astakos_quotas)
2282

    
2283
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2284
    return lock_sync(_sync_users,
2285
                     args=[users, sync],
2286
                     retries=retries,
2287
                     retry_wait=retry_wait)
2288

    
2289
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2290
    users = AstakosUser.objects.filter(is_active=True)
2291
    return sync_users(users, sync, retries=retries, retry_wait=retry_wait)
2292

    
2293
def lock_sync(func, args=[], kwargs={}, retries=3, retry_wait=1.0):
2294
    transaction.commit()
2295

    
2296
    cursor = connection.cursor()
2297
    locked = True
2298
    try:
2299
        while 1:
2300
            cursor.execute("SELECT pg_try_advisory_lock(1)")
2301
            r = cursor.fetchone()
2302
            if r is None:
2303
                m = "Impossible"
2304
                raise AssertionError(m)
2305
            locked = r[0]
2306
            if locked:
2307
                break
2308

    
2309
            retries -= 1
2310
            if retries <= 0:
2311
                return False
2312
            sleep(retry_wait)
2313

    
2314
        return func(*args, **kwargs)
2315

    
2316
    finally:
2317
        if locked:
2318
            cursor.execute("SELECT pg_advisory_unlock(1)")
2319
            cursor.fetchall()
2320

    
2321

    
2322
class ProjectMembershipHistory(models.Model):
2323
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2324
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2325

    
2326
    person  =   models.BigIntegerField()
2327
    project =   models.BigIntegerField()
2328
    date    =   models.DateField(auto_now_add=True)
2329
    reason  =   models.IntegerField()
2330
    serial  =   models.BigIntegerField()
2331

    
2332
### SIGNALS ###
2333
################
2334

    
2335
def create_astakos_user(u):
2336
    try:
2337
        AstakosUser.objects.get(user_ptr=u.pk)
2338
    except AstakosUser.DoesNotExist:
2339
        extended_user = AstakosUser(user_ptr_id=u.pk)
2340
        extended_user.__dict__.update(u.__dict__)
2341
        extended_user.save()
2342
        if not extended_user.has_auth_provider('local'):
2343
            extended_user.add_auth_provider('local')
2344
    except BaseException, e:
2345
        logger.exception(e)
2346

    
2347

    
2348
def fix_superusers(sender, **kwargs):
2349
    # Associate superusers with AstakosUser
2350
    admins = User.objects.filter(is_superuser=True)
2351
    for u in admins:
2352
        create_astakos_user(u)
2353
post_syncdb.connect(fix_superusers)
2354

    
2355

    
2356
def user_post_save(sender, instance, created, **kwargs):
2357
    if not created:
2358
        return
2359
    create_astakos_user(instance)
2360
post_save.connect(user_post_save, sender=User)
2361

    
2362
def astakosuser_post_save(sender, instance, created, **kwargs):
2363
    pass
2364

    
2365
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2366

    
2367
def resource_post_save(sender, instance, created, **kwargs):
2368
    pass
2369

    
2370
post_save.connect(resource_post_save, sender=Resource)
2371

    
2372
def renew_token(sender, instance, **kwargs):
2373
    if not instance.auth_token:
2374
        instance.renew_token()
2375
pre_save.connect(renew_token, sender=AstakosUser)
2376
pre_save.connect(renew_token, sender=Service)
2377