Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (78.8 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

    
305
class AstakosUser(User):
306
    """
307
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
308
    """
309
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
310
                                   null=True)
311

    
312
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
313
    #                    AstakosUserProvider model.
314
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
315
                                null=True)
316
    # ex. screen_name for twitter, eppn for shibboleth
317
    third_party_identifier = models.CharField(_('Third-party identifier'),
318
                                              max_length=255, null=True,
319
                                              blank=True)
320

    
321

    
322
    #for invitations
323
    user_level = DEFAULT_USER_LEVEL
324
    level = models.IntegerField(_('Inviter level'), default=user_level)
325
    invitations = models.IntegerField(
326
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
327

    
328
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
329
                                  null=True, blank=True, help_text = _( 'test' ))
330
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
331
    auth_token_expires = models.DateTimeField(
332
        _('Token expiration date'), null=True)
333

    
334
    updated = models.DateTimeField(_('Update date'))
335
    is_verified = models.BooleanField(_('Is verified?'), default=False)
336

    
337
    email_verified = models.BooleanField(_('Email verified?'), default=False)
338

    
339
    has_credits = models.BooleanField(_('Has credits?'), default=False)
340
    has_signed_terms = models.BooleanField(
341
        _('I agree with the terms'), default=False)
342
    date_signed_terms = models.DateTimeField(
343
        _('Signed terms date'), null=True, blank=True)
344

    
345
    activation_sent = models.DateTimeField(
346
        _('Activation sent data'), null=True, blank=True)
347

    
348
    policy = models.ManyToManyField(
349
        Resource, null=True, through='AstakosUserQuota')
350

    
351
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
352

    
353
    __has_signed_terms = False
354
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
355
                                           default=False, db_index=True)
356

    
357
    objects = AstakosUserManager()
358

    
359
    def __init__(self, *args, **kwargs):
360
        super(AstakosUser, self).__init__(*args, **kwargs)
361
        self.__has_signed_terms = self.has_signed_terms
362
        if not self.id:
363
            self.is_active = False
364

    
365
    @property
366
    def realname(self):
367
        return '%s %s' % (self.first_name, self.last_name)
368

    
369
    @realname.setter
370
    def realname(self, value):
371
        parts = value.split(' ')
372
        if len(parts) == 2:
373
            self.first_name = parts[0]
374
            self.last_name = parts[1]
375
        else:
376
            self.last_name = parts[0]
377

    
378
    def add_permission(self, pname):
379
        if self.has_perm(pname):
380
            return
381
        p, created = Permission.objects.get_or_create(
382
                                    codename=pname,
383
                                    name=pname.capitalize(),
384
                                    content_type=get_content_type())
385
        self.user_permissions.add(p)
386

    
387
    def remove_permission(self, pname):
388
        if self.has_perm(pname):
389
            return
390
        p = Permission.objects.get(codename=pname,
391
                                   content_type=get_content_type())
392
        self.user_permissions.remove(p)
393

    
394
    @property
395
    def invitation(self):
396
        try:
397
            return Invitation.objects.get(username=self.email)
398
        except Invitation.DoesNotExist:
399
            return None
400

    
401
    def initial_quotas(self):
402
        quotas = dict(get_default_quota())
403
        for user_quota in self.policies:
404
            resource = user_quota.resource.full_name()
405
            quotas[resource] = user_quota.quota_values()
406
        return quotas
407

    
408
    def all_quotas(self):
409
        quotas = self.initial_quotas()
410

    
411
        objects = self.projectmembership_set.select_related()
412
        memberships = objects.filter(is_active=True)
413
        for membership in memberships:
414
            application = membership.application
415
            if application is None:
416
                m = _("missing application for active membership %s"
417
                      % (membership,))
418
                raise AssertionError(m)
419

    
420
            grants = application.projectresourcegrant_set.all()
421
            for grant in grants:
422
                resource = grant.resource.full_name()
423
                prev = quotas.get(resource, 0)
424
                new = add_quota_values(prev, grant.member_quota_values())
425
                quotas[resource] = new
426
        return quotas
427

    
428
    @property
429
    def policies(self):
430
        return self.astakosuserquota_set.select_related().all()
431

    
432
    @policies.setter
433
    def policies(self, policies):
434
        for p in policies:
435
            p.setdefault('resource', '')
436
            p.setdefault('capacity', 0)
437
            p.setdefault('quantity', 0)
438
            p.setdefault('import_limit', 0)
439
            p.setdefault('export_limit', 0)
440
            p.setdefault('update', True)
441
            self.add_resource_policy(**p)
442

    
443
    def add_resource_policy(
444
            self, resource, capacity, quantity, import_limit,
445
            export_limit, update=True):
446
        """Raises ObjectDoesNotExist, IntegrityError"""
447
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
448
        resource = Resource.objects.get(service__name=s, name=r)
449
        if update:
450
            AstakosUserQuota.objects.update_or_create(
451
                user=self, resource=resource, defaults={
452
                    'capacity':capacity,
453
                    'quantity': quantity,
454
                    'import_limit':import_limit,
455
                    'export_limit':export_limit})
456
        else:
457
            q = self.astakosuserquota_set
458
            q.create(
459
                resource=resource, capacity=capacity, quanity=quantity,
460
                import_limit=import_limit, export_limit=export_limit)
461

    
462
    def remove_resource_policy(self, service, resource):
463
        """Raises ObjectDoesNotExist, IntegrityError"""
464
        resource = Resource.objects.get(service__name=service, name=resource)
465
        q = self.policies.get(resource=resource).delete()
466

    
467
    def update_uuid(self):
468
        while not self.uuid:
469
            uuid_val =  str(uuid.uuid4())
470
            try:
471
                AstakosUser.objects.get(uuid=uuid_val)
472
            except AstakosUser.DoesNotExist, e:
473
                self.uuid = uuid_val
474
        return self.uuid
475

    
476
    def save(self, update_timestamps=True, **kwargs):
477
        if update_timestamps:
478
            if not self.id:
479
                self.date_joined = datetime.now()
480
            self.updated = datetime.now()
481

    
482
        # update date_signed_terms if necessary
483
        if self.__has_signed_terms != self.has_signed_terms:
484
            self.date_signed_terms = datetime.now()
485

    
486
        self.update_uuid()
487

    
488
        if self.username != self.email.lower():
489
            # set username
490
            self.username = self.email.lower()
491

    
492
        super(AstakosUser, self).save(**kwargs)
493

    
494
    def renew_token(self, flush_sessions=False, current_key=None):
495
        md5 = hashlib.md5()
496
        md5.update(settings.SECRET_KEY)
497
        md5.update(self.username)
498
        md5.update(self.realname.encode('ascii', 'ignore'))
499
        md5.update(asctime())
500

    
501
        self.auth_token = b64encode(md5.digest())
502
        self.auth_token_created = datetime.now()
503
        self.auth_token_expires = self.auth_token_created + \
504
                                  timedelta(hours=AUTH_TOKEN_DURATION)
505
        if flush_sessions:
506
            self.flush_sessions(current_key)
507
        msg = 'Token renewed for %s' % self.email
508
        logger.log(LOGGING_LEVEL, msg)
509

    
510
    def flush_sessions(self, current_key=None):
511
        q = self.sessions
512
        if current_key:
513
            q = q.exclude(session_key=current_key)
514

    
515
        keys = q.values_list('session_key', flat=True)
516
        if keys:
517
            msg = 'Flushing sessions: %s' % ','.join(keys)
518
            logger.log(LOGGING_LEVEL, msg, [])
519
        engine = import_module(settings.SESSION_ENGINE)
520
        for k in keys:
521
            s = engine.SessionStore(k)
522
            s.flush()
523

    
524
    def __unicode__(self):
525
        return '%s (%s)' % (self.realname, self.email)
526

    
527
    def conflicting_email(self):
528
        q = AstakosUser.objects.exclude(username=self.username)
529
        q = q.filter(email__iexact=self.email)
530
        if q.count() != 0:
531
            return True
532
        return False
533

    
534
    def email_change_is_pending(self):
535
        return self.emailchanges.count() > 0
536

    
537
    def email_change_is_pending(self):
538
        return self.emailchanges.count() > 0
539

    
540
    @property
541
    def signed_terms(self):
542
        term = get_latest_terms()
543
        if not term:
544
            return True
545
        if not self.has_signed_terms:
546
            return False
547
        if not self.date_signed_terms:
548
            return False
549
        if self.date_signed_terms < term.date:
550
            self.has_signed_terms = False
551
            self.date_signed_terms = None
552
            self.save()
553
            return False
554
        return True
555

    
556
    def set_invitations_level(self):
557
        """
558
        Update user invitation level
559
        """
560
        level = self.invitation.inviter.level + 1
561
        self.level = level
562
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
563

    
564
    def can_login_with_auth_provider(self, provider):
565
        if not self.has_auth_provider(provider):
566
            return False
567
        else:
568
            return auth_providers.get_provider(provider).is_available_for_login()
569

    
570
    def can_add_auth_provider(self, provider, include_unverified=False, **kwargs):
571
        provider_settings = auth_providers.get_provider(provider)
572

    
573
        if not provider_settings.is_available_for_add():
574
            return False
575

    
576
        if self.has_auth_provider(provider) and \
577
           provider_settings.one_per_user:
578
            return False
579

    
580
        if 'provider_info' in kwargs:
581
            kwargs.pop('provider_info')
582

    
583
        if 'identifier' in kwargs:
584
            try:
585
                # provider with specified params already exist
586
                if not include_unverified:
587
                    kwargs['user__email_verified'] = True
588
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
589
                                                                   **kwargs)
590
            except AstakosUser.DoesNotExist:
591
                return True
592
            else:
593
                return False
594

    
595
        return True
596

    
597
    def can_remove_auth_provider(self, module):
598
        provider = auth_providers.get_provider(module)
599
        existing = self.get_active_auth_providers()
600
        existing_for_provider = self.get_active_auth_providers(module=module)
601

    
602
        if len(existing) <= 1:
603
            return False
604

    
605
        if len(existing_for_provider) == 1 and provider.is_required():
606
            return False
607

    
608
        return True
609

    
610
    def can_change_password(self):
611
        return self.has_auth_provider('local', auth_backend='astakos')
612

    
613
    def can_change_email(self):
614
        non_astakos_local = self.get_auth_providers().filter(module='local')
615
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
616
        return non_astakos_local.count() == 0
617

    
618
    def has_required_auth_providers(self):
619
        required = auth_providers.REQUIRED_PROVIDERS
620
        for provider in required:
621
            if not self.has_auth_provider(provider):
622
                return False
623
        return True
624

    
625
    def has_auth_provider(self, provider, **kwargs):
626
        return bool(self.get_auth_providers().filter(module=provider,
627
                                               **kwargs).count())
628

    
629
    def add_auth_provider(self, provider, **kwargs):
630
        info_data = ''
631
        if 'provider_info' in kwargs:
632
            info_data = kwargs.pop('provider_info')
633
            if isinstance(info_data, dict):
634
                info_data = json.dumps(info_data)
635

    
636
        if self.can_add_auth_provider(provider, **kwargs):
637
            if 'identifier' in kwargs:
638
                # clean up third party pending for activation users of the same
639
                # identifier
640
                AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
641
                                                                **kwargs)
642
            self.auth_providers.create(module=provider, active=True,
643
                                       info_data=info_data,
644
                                       **kwargs)
645
        else:
646
            raise Exception('Cannot add provider')
647

    
648
    def add_pending_auth_provider(self, pending):
649
        """
650
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
651
        the current user.
652
        """
653
        if not isinstance(pending, PendingThirdPartyUser):
654
            pending = PendingThirdPartyUser.objects.get(token=pending)
655

    
656
        provider = self.add_auth_provider(pending.provider,
657
                               identifier=pending.third_party_identifier,
658
                                affiliation=pending.affiliation,
659
                                          provider_info=pending.info)
660

    
661
        if email_re.match(pending.email or '') and pending.email != self.email:
662
            self.additionalmail_set.get_or_create(email=pending.email)
663

    
664
        pending.delete()
665
        return provider
666

    
667
    def remove_auth_provider(self, provider, **kwargs):
668
        self.get_auth_providers().get(module=provider, **kwargs).delete()
669

    
670
    # user urls
671
    def get_resend_activation_url(self):
672
        return reverse('send_activation', kwargs={'user_id': self.pk})
673

    
674
    def get_provider_remove_url(self, module, **kwargs):
675
        return reverse('remove_auth_provider', kwargs={
676
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
677

    
678
    def get_activation_url(self, nxt=False):
679
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
680
                                 quote(self.auth_token))
681
        if nxt:
682
            url += "&next=%s" % quote(nxt)
683
        return url
684

    
685
    def get_password_reset_url(self, token_generator=default_token_generator):
686
        return reverse('django.contrib.auth.views.password_reset_confirm',
687
                          kwargs={'uidb36':int_to_base36(self.id),
688
                                  'token':token_generator.make_token(self)})
689

    
690
    def get_auth_providers(self):
691
        return self.auth_providers
692

    
693
    def get_available_auth_providers(self):
694
        """
695
        Returns a list of providers available for user to connect to.
696
        """
697
        providers = []
698
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
699
            if self.can_add_auth_provider(module):
700
                providers.append(provider_settings(self))
701

    
702
        return providers
703

    
704
    def get_active_auth_providers(self, **filters):
705
        providers = []
706
        for provider in self.get_auth_providers().active(**filters):
707
            if auth_providers.get_provider(provider.module).is_available_for_login():
708
                providers.append(provider)
709
        return providers
710

    
711
    @property
712
    def auth_providers_display(self):
713
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
714

    
715
    def get_inactive_message(self):
716
        msg_extra = ''
717
        message = ''
718
        if self.activation_sent:
719
            if self.email_verified:
720
                message = _(astakos_messages.ACCOUNT_INACTIVE)
721
            else:
722
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
723
                if astakos_settings.MODERATION_ENABLED:
724
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
725
                else:
726
                    url = self.get_resend_activation_url()
727
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
728
                                u' ' + \
729
                                _('<a href="%s">%s?</a>') % (url,
730
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
731
        else:
732
            if astakos_settings.MODERATION_ENABLED:
733
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
734
            else:
735
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
736
                url = self.get_resend_activation_url()
737
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
738
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
739

    
740
        return mark_safe(message + u' '+ msg_extra)
741

    
742
    def owns_project(self, project):
743
        return project.owner == self
744

    
745
    def is_project_member(self, project_or_application):
746
        return self.get_status_in_project(project_or_application) in \
747
                                        ProjectMembership.ASSOCIATED_STATES
748

    
749
    def is_project_accepted_member(self, project_or_application):
750
        return self.get_status_in_project(project_or_application) in \
751
                                            ProjectMembership.ACCEPTED_STATES
752

    
753
    def get_status_in_project(self, project_or_application):
754
        application = project_or_application
755
        if isinstance(project_or_application, Project):
756
            application = project_or_application.project
757
        return application.user_status(self)
758

    
759

    
760
class AstakosUserAuthProviderManager(models.Manager):
761

    
762
    def active(self, **filters):
763
        return self.filter(active=True, **filters)
764

    
765
    def remove_unverified_providers(self, provider, **filters):
766
        try:
767
            existing = self.filter(module=provider, user__email_verified=False, **filters)
768
            for p in existing:
769
                p.user.delete()
770
        except:
771
            pass
772

    
773

    
774

    
775
class AstakosUserAuthProvider(models.Model):
776
    """
777
    Available user authentication methods.
778
    """
779
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
780
                                   null=True, default=None)
781
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
782
    module = models.CharField(_('Provider'), max_length=255, blank=False,
783
                                default='local')
784
    identifier = models.CharField(_('Third-party identifier'),
785
                                              max_length=255, null=True,
786
                                              blank=True)
787
    active = models.BooleanField(default=True)
788
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
789
                                   default='astakos')
790
    info_data = models.TextField(default="", null=True, blank=True)
791
    created = models.DateTimeField('Creation date', auto_now_add=True)
792

    
793
    objects = AstakosUserAuthProviderManager()
794

    
795
    class Meta:
796
        unique_together = (('identifier', 'module', 'user'), )
797
        ordering = ('module', 'created')
798

    
799
    def __init__(self, *args, **kwargs):
800
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
801
        try:
802
            self.info = json.loads(self.info_data)
803
            if not self.info:
804
                self.info = {}
805
        except Exception, e:
806
            self.info = {}
807

    
808
        for key,value in self.info.iteritems():
809
            setattr(self, 'info_%s' % key, value)
810

    
811

    
812
    @property
813
    def settings(self):
814
        return auth_providers.get_provider(self.module)
815

    
816
    @property
817
    def details_display(self):
818
        try:
819
          return self.settings.get_details_tpl_display % self.__dict__
820
        except:
821
          return ''
822

    
823
    @property
824
    def title_display(self):
825
        title_tpl = self.settings.get_title_display
826
        try:
827
            if self.settings.get_user_title_display:
828
                title_tpl = self.settings.get_user_title_display
829
        except Exception, e:
830
            pass
831
        try:
832
          return title_tpl % self.__dict__
833
        except:
834
          return self.settings.get_title_display % self.__dict__
835

    
836
    def can_remove(self):
837
        return self.user.can_remove_auth_provider(self.module)
838

    
839
    def delete(self, *args, **kwargs):
840
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
841
        if self.module == 'local':
842
            self.user.set_unusable_password()
843
            self.user.save()
844
        return ret
845

    
846
    def __repr__(self):
847
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
848

    
849
    def __unicode__(self):
850
        if self.identifier:
851
            return "%s:%s" % (self.module, self.identifier)
852
        if self.auth_backend:
853
            return "%s:%s" % (self.module, self.auth_backend)
854
        return self.module
855

    
856
    def save(self, *args, **kwargs):
857
        self.info_data = json.dumps(self.info)
858
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
859

    
860

    
861
class ExtendedManager(models.Manager):
862
    def _update_or_create(self, **kwargs):
863
        assert kwargs, \
864
            'update_or_create() must be passed at least one keyword argument'
865
        obj, created = self.get_or_create(**kwargs)
866
        defaults = kwargs.pop('defaults', {})
867
        if created:
868
            return obj, True, False
869
        else:
870
            try:
871
                params = dict(
872
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
873
                params.update(defaults)
874
                for attr, val in params.items():
875
                    if hasattr(obj, attr):
876
                        setattr(obj, attr, val)
877
                sid = transaction.savepoint()
878
                obj.save(force_update=True)
879
                transaction.savepoint_commit(sid)
880
                return obj, False, True
881
            except IntegrityError, e:
882
                transaction.savepoint_rollback(sid)
883
                try:
884
                    return self.get(**kwargs), False, False
885
                except self.model.DoesNotExist:
886
                    raise e
887

    
888
    update_or_create = _update_or_create
889

    
890

    
891
class AstakosUserQuota(models.Model):
892
    objects = ExtendedManager()
893
    capacity = intDecimalField()
894
    quantity = intDecimalField(default=0)
895
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
896
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
897
    resource = models.ForeignKey(Resource)
898
    user = models.ForeignKey(AstakosUser)
899

    
900
    class Meta:
901
        unique_together = ("resource", "user")
902

    
903
    def quota_values(self):
904
        return QuotaValues(
905
            quantity = self.quantity,
906
            capacity = self.capacity,
907
            import_limit = self.import_limit,
908
            export_limit = self.export_limit)
909

    
910

    
911
class ApprovalTerms(models.Model):
912
    """
913
    Model for approval terms
914
    """
915

    
916
    date = models.DateTimeField(
917
        _('Issue date'), db_index=True, auto_now_add=True)
918
    location = models.CharField(_('Terms location'), max_length=255)
919

    
920

    
921
class Invitation(models.Model):
922
    """
923
    Model for registring invitations
924
    """
925
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
926
                                null=True)
927
    realname = models.CharField(_('Real name'), max_length=255)
928
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
929
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
930
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
931
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
932
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
933

    
934
    def __init__(self, *args, **kwargs):
935
        super(Invitation, self).__init__(*args, **kwargs)
936
        if not self.id:
937
            self.code = _generate_invitation_code()
938

    
939
    def consume(self):
940
        self.is_consumed = True
941
        self.consumed = datetime.now()
942
        self.save()
943

    
944
    def __unicode__(self):
945
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
946

    
947

    
948
class EmailChangeManager(models.Manager):
949

    
950
    @transaction.commit_on_success
951
    def change_email(self, activation_key):
952
        """
953
        Validate an activation key and change the corresponding
954
        ``User`` if valid.
955

956
        If the key is valid and has not expired, return the ``User``
957
        after activating.
958

959
        If the key is not valid or has expired, return ``None``.
960

961
        If the key is valid but the ``User`` is already active,
962
        return ``None``.
963

964
        After successful email change the activation record is deleted.
965

966
        Throws ValueError if there is already
967
        """
968
        try:
969
            email_change = self.model.objects.get(
970
                activation_key=activation_key)
971
            if email_change.activation_key_expired():
972
                email_change.delete()
973
                raise EmailChange.DoesNotExist
974
            # is there an active user with this address?
975
            try:
976
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
977
            except AstakosUser.DoesNotExist:
978
                pass
979
            else:
980
                raise ValueError(_('The new email address is reserved.'))
981
            # update user
982
            user = AstakosUser.objects.get(pk=email_change.user_id)
983
            old_email = user.email
984
            user.email = email_change.new_email_address
985
            user.save()
986
            email_change.delete()
987
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
988
                                                          user.email)
989
            logger.log(LOGGING_LEVEL, msg)
990
            return user
991
        except EmailChange.DoesNotExist:
992
            raise ValueError(_('Invalid activation key.'))
993

    
994

    
995
class EmailChange(models.Model):
996
    new_email_address = models.EmailField(
997
        _(u'new e-mail address'),
998
        help_text=_('Your old email address will be used until you verify your new one.'))
999
    user = models.ForeignKey(
1000
        AstakosUser, unique=True, related_name='emailchanges')
1001
    requested_at = models.DateTimeField(auto_now_add=True)
1002
    activation_key = models.CharField(
1003
        max_length=40, unique=True, db_index=True)
1004

    
1005
    objects = EmailChangeManager()
1006

    
1007
    def get_url(self):
1008
        return reverse('email_change_confirm',
1009
                      kwargs={'activation_key': self.activation_key})
1010

    
1011
    def activation_key_expired(self):
1012
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1013
        return self.requested_at + expiration_date < datetime.now()
1014

    
1015

    
1016
class AdditionalMail(models.Model):
1017
    """
1018
    Model for registring invitations
1019
    """
1020
    owner = models.ForeignKey(AstakosUser)
1021
    email = models.EmailField()
1022

    
1023

    
1024
def _generate_invitation_code():
1025
    while True:
1026
        code = randint(1, 2L ** 63 - 1)
1027
        try:
1028
            Invitation.objects.get(code=code)
1029
            # An invitation with this code already exists, try again
1030
        except Invitation.DoesNotExist:
1031
            return code
1032

    
1033

    
1034
def get_latest_terms():
1035
    try:
1036
        term = ApprovalTerms.objects.order_by('-id')[0]
1037
        return term
1038
    except IndexError:
1039
        pass
1040
    return None
1041

    
1042
class PendingThirdPartyUser(models.Model):
1043
    """
1044
    Model for registring successful third party user authentications
1045
    """
1046
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1047
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1048
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1049
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1050
                                  null=True)
1051
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1052
                                 null=True)
1053
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1054
                                   null=True)
1055
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1056
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1057
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1058
    info = models.TextField(default="", null=True, blank=True)
1059

    
1060
    class Meta:
1061
        unique_together = ("provider", "third_party_identifier")
1062

    
1063
    def get_user_instance(self):
1064
        d = self.__dict__
1065
        d.pop('_state', None)
1066
        d.pop('id', None)
1067
        d.pop('token', None)
1068
        d.pop('created', None)
1069
        d.pop('info', None)
1070
        user = AstakosUser(**d)
1071

    
1072
        return user
1073

    
1074
    @property
1075
    def realname(self):
1076
        return '%s %s' %(self.first_name, self.last_name)
1077

    
1078
    @realname.setter
1079
    def realname(self, value):
1080
        parts = value.split(' ')
1081
        if len(parts) == 2:
1082
            self.first_name = parts[0]
1083
            self.last_name = parts[1]
1084
        else:
1085
            self.last_name = parts[0]
1086

    
1087
    def save(self, **kwargs):
1088
        if not self.id:
1089
            # set username
1090
            while not self.username:
1091
                username =  uuid.uuid4().hex[:30]
1092
                try:
1093
                    AstakosUser.objects.get(username = username)
1094
                except AstakosUser.DoesNotExist, e:
1095
                    self.username = username
1096
        super(PendingThirdPartyUser, self).save(**kwargs)
1097

    
1098
    def generate_token(self):
1099
        self.password = self.third_party_identifier
1100
        self.last_login = datetime.now()
1101
        self.token = default_token_generator.make_token(self)
1102

    
1103
class SessionCatalog(models.Model):
1104
    session_key = models.CharField(_('session key'), max_length=40)
1105
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1106

    
1107

    
1108
### PROJECTS ###
1109
################
1110

    
1111
def synced_model_metaclass(class_name, class_parents, class_attributes):
1112

    
1113
    new_attributes = {}
1114
    sync_attributes = {}
1115

    
1116
    for name, value in class_attributes.iteritems():
1117
        sync, underscore, rest = name.partition('_')
1118
        if sync == 'sync' and underscore == '_':
1119
            sync_attributes[rest] = value
1120
        else:
1121
            new_attributes[name] = value
1122

    
1123
    if 'prefix' not in sync_attributes:
1124
        m = ("you did not specify a 'sync_prefix' attribute "
1125
             "in class '%s'" % (class_name,))
1126
        raise ValueError(m)
1127

    
1128
    prefix = sync_attributes.pop('prefix')
1129
    class_name = sync_attributes.pop('classname', prefix + '_model')
1130

    
1131
    for name, value in sync_attributes.iteritems():
1132
        newname = prefix + '_' + name
1133
        if newname in new_attributes:
1134
            m = ("class '%s' was specified with prefix '%s' "
1135
                 "but it already has an attribute named '%s'"
1136
                 % (class_name, prefix, newname))
1137
            raise ValueError(m)
1138

    
1139
        new_attributes[newname] = value
1140

    
1141
    newclass = type(class_name, class_parents, new_attributes)
1142
    return newclass
1143

    
1144

    
1145
def make_synced(prefix='sync', name='SyncedState'):
1146

    
1147
    the_name = name
1148
    the_prefix = prefix
1149

    
1150
    class SyncedState(models.Model):
1151

    
1152
        sync_classname      = the_name
1153
        sync_prefix         = the_prefix
1154
        __metaclass__       = synced_model_metaclass
1155

    
1156
        sync_new_state      = models.BigIntegerField(null=True)
1157
        sync_synced_state   = models.BigIntegerField(null=True)
1158
        STATUS_SYNCED       = 0
1159
        STATUS_PENDING      = 1
1160
        sync_status         = models.IntegerField(db_index=True)
1161

    
1162
        class Meta:
1163
            abstract = True
1164

    
1165
        class NotSynced(Exception):
1166
            pass
1167

    
1168
        def sync_init_state(self, state):
1169
            self.sync_synced_state = state
1170
            self.sync_new_state = state
1171
            self.sync_status = self.STATUS_SYNCED
1172

    
1173
        def sync_get_status(self):
1174
            return self.sync_status
1175

    
1176
        def sync_set_status(self):
1177
            if self.sync_new_state != self.sync_synced_state:
1178
                self.sync_status = self.STATUS_PENDING
1179
            else:
1180
                self.sync_status = self.STATUS_SYNCED
1181

    
1182
        def sync_set_synced(self):
1183
            self.sync_synced_state = self.sync_new_state
1184
            self.sync_status = self.STATUS_SYNCED
1185

    
1186
        def sync_get_synced_state(self):
1187
            return self.sync_synced_state
1188

    
1189
        def sync_set_new_state(self, new_state):
1190
            self.sync_new_state = new_state
1191
            self.sync_set_status()
1192

    
1193
        def sync_get_new_state(self):
1194
            return self.sync_new_state
1195

    
1196
        def sync_set_synced_state(self, synced_state):
1197
            self.sync_synced_state = synced_state
1198
            self.sync_set_status()
1199

    
1200
        def sync_get_pending_objects(self):
1201
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1202
            return self.objects.filter(**kw)
1203

    
1204
        def sync_get_synced_objects(self):
1205
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1206
            return self.objects.filter(**kw)
1207

    
1208
        def sync_verify_get_synced_state(self):
1209
            status = self.sync_get_status()
1210
            state = self.sync_get_synced_state()
1211
            verified = (status == self.STATUS_SYNCED)
1212
            return state, verified
1213

    
1214
        def sync_is_synced(self):
1215
            state, verified = self.sync_verify_get_synced_state()
1216
            return verified
1217

    
1218
    return SyncedState
1219

    
1220
SyncedState = make_synced(prefix='sync', name='SyncedState')
1221

    
1222

    
1223
class ProjectApplicationManager(ForUpdateManager):
1224

    
1225
    def user_visible_projects(self, *filters, **kw_filters):
1226
        model = self.model
1227
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1228

    
1229
    def user_visible_by_chain(self, *filters, **kw_filters):
1230
        model = self.model
1231
        pending = self.filter(model.Q_PENDING).values_list('chain')
1232
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1233
        by_chain = dict(pending.annotate(models.Max('id')))
1234
        by_chain.update(approved.annotate(models.Max('id')))
1235
        return self.filter(id__in=by_chain.values())
1236

    
1237
    def user_accessible_projects(self, user):
1238
        """
1239
        Return projects accessed by specified user.
1240
        """
1241
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1242
                               Q(project__projectmembership__person=user)
1243

    
1244
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1245

    
1246
    def search_by_name(self, *search_strings):
1247
        q = Q()
1248
        for s in search_strings:
1249
            q = q | Q(name__icontains=s)
1250
        return self.filter(q)
1251

    
1252
    def latest_of_chain(self, chain_id):
1253
        try:
1254
            return self.filter(chain=chain_id).order_by('-id')[0]
1255
        except IndexError:
1256
            return None
1257

    
1258
USER_STATUS_DISPLAY = {
1259
      0: _('Join requested'),
1260
      1: _('Accepted member'),
1261
     10: _('Suspended'),
1262
    100: _('Terminated'),
1263
    200: _('Removed'),
1264
     -1: _('Not a member'),
1265
}
1266

    
1267

    
1268
class Chain(models.Model):
1269
    chain  =   models.AutoField(primary_key=True)
1270

    
1271
def new_chain():
1272
    c = Chain.objects.create()
1273
    chain = c.chain
1274
    c.delete()
1275
    return chain
1276

    
1277

    
1278
class ProjectApplication(models.Model):
1279
    applicant               =   models.ForeignKey(
1280
                                    AstakosUser,
1281
                                    related_name='projects_applied',
1282
                                    db_index=True)
1283

    
1284
    PENDING     =    0
1285
    APPROVED    =    1
1286
    REPLACED    =    2
1287
    DENIED      =    3
1288
    DISMISSED   =    4
1289
    CANCELLED   =    5
1290

    
1291
    state                   =   models.IntegerField(default=PENDING,
1292
                                                    db_index=True)
1293

    
1294
    owner                   =   models.ForeignKey(
1295
                                    AstakosUser,
1296
                                    related_name='projects_owned',
1297
                                    db_index=True)
1298

    
1299
    chain                   =   models.IntegerField()
1300
    precursor_application   =   models.ForeignKey('ProjectApplication',
1301
                                                  null=True,
1302
                                                  blank=True)
1303

    
1304
    name                    =   models.CharField(max_length=80)
1305
    homepage                =   models.URLField(max_length=255, null=True)
1306
    description             =   models.TextField(null=True, blank=True)
1307
    start_date              =   models.DateTimeField(null=True, blank=True)
1308
    end_date                =   models.DateTimeField()
1309
    member_join_policy      =   models.IntegerField()
1310
    member_leave_policy     =   models.IntegerField()
1311
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1312
    resource_grants         =   models.ManyToManyField(
1313
                                    Resource,
1314
                                    null=True,
1315
                                    blank=True,
1316
                                    through='ProjectResourceGrant')
1317
    comments                =   models.TextField(null=True, blank=True)
1318
    issue_date              =   models.DateTimeField(auto_now_add=True)
1319
    response_date           =   models.DateTimeField(null=True, blank=True)
1320

    
1321
    objects                 =   ProjectApplicationManager()
1322

    
1323
    # Compiled queries
1324
    Q_PENDING  = Q(state=PENDING)
1325
    Q_APPROVED = Q(state=APPROVED)
1326

    
1327
    class Meta:
1328
        unique_together = ("chain", "id")
1329

    
1330
    def __unicode__(self):
1331
        return "%s applied by %s" % (self.name, self.applicant)
1332

    
1333
    # TODO: Move to a more suitable place
1334
    APPLICATION_STATE_DISPLAY = {
1335
        PENDING  : _('Pending review'),
1336
        APPROVED : _('Active'),
1337
        REPLACED : _('Replaced'),
1338
        DENIED   : _('Denied'),
1339
        DISMISSED: _('Dismissed'),
1340
        CANCELLED: _('Cancelled')
1341
    }
1342

    
1343
    def get_project(self):
1344
        try:
1345
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1346
            return Project
1347
        except Project.DoesNotExist, e:
1348
            return None
1349

    
1350
    def state_display(self):
1351
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1352

    
1353
    def add_resource_policy(self, service, resource, uplimit):
1354
        """Raises ObjectDoesNotExist, IntegrityError"""
1355
        q = self.projectresourcegrant_set
1356
        resource = Resource.objects.get(service__name=service, name=resource)
1357
        q.create(resource=resource, member_capacity=uplimit)
1358

    
1359
    def user_status(self, user):
1360
        try:
1361
            project = self.get_project()
1362
            if not project:
1363
                return -1
1364
            membership = project.projectmembership_set
1365
            membership = membership.exclude(state=ProjectMembership.REMOVED)
1366
            membership = membership.get(person=user)
1367
            status = membership.state
1368
        except ProjectMembership.DoesNotExist:
1369
            return -1
1370

    
1371
        return status
1372

    
1373
    def user_status_display(self, user):
1374
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1375

    
1376
    def members_count(self):
1377
        return self.project.approved_memberships.count()
1378

    
1379
    @property
1380
    def grants(self):
1381
        return self.projectresourcegrant_set.values(
1382
            'member_capacity', 'resource__name', 'resource__service__name')
1383

    
1384
    @property
1385
    def resource_policies(self):
1386
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1387

    
1388
    @resource_policies.setter
1389
    def resource_policies(self, policies):
1390
        for p in policies:
1391
            service = p.get('service', None)
1392
            resource = p.get('resource', None)
1393
            uplimit = p.get('uplimit', 0)
1394
            self.add_resource_policy(service, resource, uplimit)
1395

    
1396
    @property
1397
    def follower(self):
1398
        try:
1399
            return ProjectApplication.objects.get(precursor_application=self)
1400
        except ProjectApplication.DoesNotExist:
1401
            return
1402

    
1403
    def followers(self):
1404
        followers = self.chained_applications()
1405
        followers = followers.exclude(id=self.pk).filter(state=self.PENDING)
1406
        followers = followers.order_by('id')
1407
        return followers
1408

    
1409
    def last_follower(self):
1410
        try:
1411
            return self.followers().order_by('-id')[0]
1412
        except IndexError:
1413
            return None
1414

    
1415
    def is_modification(self):
1416
        parents = self.chained_applications().filter(id__lt=self.id)
1417
        parents = parents.filter(state__in=[self.APPROVED])
1418
        return parents.count() > 0
1419

    
1420
    def chained_applications(self):
1421
        return ProjectApplication.objects.filter(chain=self.chain)
1422

    
1423
    def has_pending_modifications(self):
1424
        return bool(self.last_follower())
1425

    
1426
    def get_project(self):
1427
        try:
1428
            return Project.objects.get(id=self.chain)
1429
        except Project.DoesNotExist:
1430
            return None
1431

    
1432
    def _get_project_for_update(self):
1433
        try:
1434
            objects = Project.objects.select_for_update()
1435
            project = objects.get(id=self.chain)
1436
            return project
1437
        except Project.DoesNotExist:
1438
            return None
1439

    
1440
    def can_cancel(self):
1441
        return self.state == self.PENDING
1442

    
1443
    def cancel(self):
1444
        if not self.can_cancel():
1445
            m = _("cannot cancel: application '%s' in state '%s'") % (
1446
                    self.id, self.state)
1447
            raise AssertionError(m)
1448

    
1449
        self.state = self.CANCELLED
1450
        self.save()
1451

    
1452
    def can_dismiss(self):
1453
        return self.state == self.DENIED
1454

    
1455
    def dismiss(self):
1456
        if not self.can_dismiss():
1457
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1458
                    self.id, self.state)
1459
            raise AssertionError(m)
1460

    
1461
        self.state = self.DISMISSED
1462
        self.save()
1463

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

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

    
1473
        self.state = self.DENIED
1474
        self.response_date = datetime.now()
1475
        self.save()
1476

    
1477
    def can_approve(self):
1478
        return self.state == self.PENDING
1479

    
1480
    def approve(self, approval_user=None):
1481
        """
1482
        If approval_user then during owner membership acceptance
1483
        it is checked whether the request_user is eligible.
1484

1485
        Raises:
1486
            PermissionDenied
1487
        """
1488

    
1489
        if not transaction.is_managed():
1490
            raise AssertionError("NOPE")
1491

    
1492
        new_project_name = self.name
1493
        if not self.can_approve():
1494
            m = _("cannot approve: project '%s' in state '%s'") % (
1495
                    new_project_name, self.state)
1496
            raise AssertionError(m) # invalid argument
1497

    
1498
        now = datetime.now()
1499
        project = self._get_project_for_update()
1500

    
1501
        try:
1502
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1503
            conflicting_project = Project.objects.get(q)
1504
            if (conflicting_project != project):
1505
                m = (_("cannot approve: project with name '%s' "
1506
                       "already exists (serial: %s)") % (
1507
                        new_project_name, conflicting_project.id))
1508
                raise PermissionDenied(m) # invalid argument
1509
        except Project.DoesNotExist:
1510
            pass
1511

    
1512
        new_project = False
1513
        if project is None:
1514
            new_project = True
1515
            project = Project(id=self.chain)
1516

    
1517
        project.name = new_project_name
1518
        project.application = self
1519
        project.last_approval_date = now
1520
        if not new_project:
1521
            project.is_modified = True
1522

    
1523
        project.save()
1524

    
1525
        self.state = self.APPROVED
1526
        self.response_date = now
1527
        self.save()
1528

    
1529
    @property
1530
    def member_join_policy_display(self):
1531
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1532

    
1533
    @property
1534
    def member_leave_policy_display(self):
1535
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1536

    
1537
def submit_application(**kw):
1538

    
1539
    resource_policies = kw.pop('resource_policies', None)
1540
    application = ProjectApplication(**kw)
1541

    
1542
    precursor = kw['precursor_application']
1543

    
1544
    if precursor is None:
1545
        application.chain = new_chain()
1546
    else:
1547
        application.chain = precursor.chain
1548
        if precursor.state == ProjectApplication.PENDING:
1549
            precursor.state = ProjectApplication.REPLACED
1550
            precursor.save()
1551

    
1552
    application.save()
1553
    application.resource_policies = resource_policies
1554
    return application
1555

    
1556
class ProjectResourceGrant(models.Model):
1557

    
1558
    resource                =   models.ForeignKey(Resource)
1559
    project_application     =   models.ForeignKey(ProjectApplication,
1560
                                                  null=True)
1561
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1562
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1563
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1564
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1565
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1566
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1567

    
1568
    objects = ExtendedManager()
1569

    
1570
    class Meta:
1571
        unique_together = ("resource", "project_application")
1572

    
1573
    def member_quota_values(self):
1574
        return QuotaValues(
1575
            quantity = 0,
1576
            capacity = self.member_capacity,
1577
            import_limit = self.member_import_limit,
1578
            export_limit = self.member_export_limit)
1579

    
1580
    def display_member_capacity(self):
1581
        if self.member_capacity:
1582
            if self.resource.unit:
1583
                return ProjectResourceGrant.display_filesize(
1584
                    self.member_capacity)
1585
            else:
1586
                if math.isinf(self.member_capacity):
1587
                    return 'Unlimited'
1588
                else:
1589
                    return self.member_capacity
1590
        else:
1591
            return 'Unlimited'
1592

    
1593
    def __str__(self):
1594
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1595
                                        self.display_member_capacity())
1596

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

    
1621

    
1622
class ProjectManager(ForUpdateManager):
1623

    
1624
    def terminated_projects(self):
1625
        q = self.model.Q_TERMINATED
1626
        return self.filter(q)
1627

    
1628
    def not_terminated_projects(self):
1629
        q = ~self.model.Q_TERMINATED
1630
        return self.filter(q)
1631

    
1632
    def terminating_projects(self):
1633
        q = self.model.Q_TERMINATED & Q(is_active=True)
1634
        return self.filter(q)
1635

    
1636
    def deactivated_projects(self):
1637
        q = self.model.Q_DEACTIVATED
1638
        return self.filter(q)
1639

    
1640
    def deactivating_projects(self):
1641
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1642
        return self.filter(q)
1643

    
1644
    def modified_projects(self):
1645
        return self.filter(is_modified=True)
1646

    
1647
    def reactivating_projects(self):
1648
        return self.filter(state=Project.APPROVED, is_active=False)
1649

    
1650
    def expired_projects(self):
1651
        q = (~Q(state=Project.TERMINATED) &
1652
              Q(application__end_date__lt=datetime.now()))
1653
        return self.filter(q)
1654

    
1655

    
1656
class Project(models.Model):
1657

    
1658
    application                 =   models.OneToOneField(
1659
                                            ProjectApplication,
1660
                                            related_name='project')
1661
    last_approval_date          =   models.DateTimeField(null=True)
1662

    
1663
    members                     =   models.ManyToManyField(
1664
                                            AstakosUser,
1665
                                            through='ProjectMembership')
1666

    
1667
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1668
    deactivation_date           =   models.DateTimeField(null=True)
1669

    
1670
    creation_date               =   models.DateTimeField(auto_now_add=True)
1671
    name                        =   models.CharField(
1672
                                            max_length=80,
1673
                                            db_index=True,
1674
                                            unique=True)
1675

    
1676
    APPROVED    = 1
1677
    SUSPENDED   = 10
1678
    TERMINATED  = 100
1679

    
1680
    is_modified                 =   models.BooleanField(default=False,
1681
                                                        db_index=True)
1682
    is_active                   =   models.BooleanField(default=True,
1683
                                                        db_index=True)
1684
    state                       =   models.IntegerField(default=APPROVED,
1685
                                                        db_index=True)
1686

    
1687
    objects     =   ProjectManager()
1688

    
1689
    # Compiled queries
1690
    Q_TERMINATED  = Q(state=TERMINATED)
1691
    Q_SUSPENDED   = Q(state=SUSPENDED)
1692
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1693

    
1694
    def __str__(self):
1695
        return _("<project %s '%s'>") % (self.id, self.application.name)
1696

    
1697
    __repr__ = __str__
1698

    
1699
    STATE_DISPLAY = {
1700
        APPROVED   : 'APPROVED',
1701
        SUSPENDED  : 'SUSPENDED',
1702
        TERMINATED : 'TERMINATED'
1703
        }
1704

    
1705
    def state_display(self):
1706
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1707

    
1708
    def expiration_info(self):
1709
        return (str(self.id), self.name, self.state_display(),
1710
                str(self.application.end_date))
1711

    
1712
    def is_deactivated(self, reason=None):
1713
        if reason is not None:
1714
            return self.state == reason
1715

    
1716
        return self.state != self.APPROVED
1717

    
1718
    def is_deactivating(self, reason=None):
1719
        if not self.is_active:
1720
            return False
1721

    
1722
        return self.is_deactivated(reason)
1723

    
1724
    def is_deactivated_strict(self, reason=None):
1725
        if self.is_active:
1726
            return False
1727

    
1728
        return self.is_deactivated(reason)
1729

    
1730
    ### Deactivation calls
1731

    
1732
    def deactivate(self):
1733
        self.deactivation_date = datetime.now()
1734
        self.is_active = False
1735

    
1736
    def reactivate(self):
1737
        self.deactivation_date = None
1738
        self.is_active = True
1739

    
1740
    def terminate(self):
1741
        self.deactivation_reason = 'TERMINATED'
1742
        self.state = self.TERMINATED
1743
        self.save()
1744

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

    
1750
    def resume(self):
1751
        self.deactivation_reason = None
1752
        self.state = self.APPROVED
1753
        self.save()
1754

    
1755
    ### Logical checks
1756

    
1757
    def is_inconsistent(self):
1758
        now = datetime.now()
1759
        dates = [self.creation_date,
1760
                 self.last_approval_date,
1761
                 self.deactivation_date]
1762
        return any([date > now for date in dates])
1763

    
1764
    def is_active_strict(self):
1765
        return self.is_active and self.state == self.APPROVED
1766

    
1767
    def is_approved(self):
1768
        return self.state == self.APPROVED
1769

    
1770
    @property
1771
    def is_alive(self):
1772
        return not self.is_terminated
1773

    
1774
    @property
1775
    def is_terminated(self):
1776
        return self.is_deactivated(self.TERMINATED)
1777

    
1778
    @property
1779
    def is_suspended(self):
1780
        return self.is_deactivated(self.SUSPENDED)
1781

    
1782
    def violates_resource_grants(self):
1783
        return False
1784

    
1785
    def violates_members_limit(self, adding=0):
1786
        application = self.application
1787
        limit = application.limit_on_members_number
1788
        if limit is None:
1789
            return False
1790
        return (len(self.approved_members) + adding > limit)
1791

    
1792

    
1793
    ### Other
1794

    
1795
    def count_pending_memberships(self):
1796
        memb_set = self.projectmembership_set
1797
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1798
        return memb_count
1799

    
1800
    @property
1801
    def approved_memberships(self):
1802
        query = ProjectMembership.Q_ACCEPTED_STATES
1803
        return self.projectmembership_set.filter(query)
1804

    
1805
    @property
1806
    def approved_members(self):
1807
        return [m.person for m in self.approved_memberships]
1808

    
1809
    def add_member(self, user):
1810
        """
1811
        Raises:
1812
            django.exceptions.PermissionDenied
1813
            astakos.im.models.AstakosUser.DoesNotExist
1814
        """
1815
        if isinstance(user, int):
1816
            user = AstakosUser.objects.get(user=user)
1817

    
1818
        m, created = ProjectMembership.objects.get_or_create(
1819
            person=user, project=self
1820
        )
1821
        m.accept()
1822

    
1823
    def remove_member(self, user):
1824
        """
1825
        Raises:
1826
            django.exceptions.PermissionDenied
1827
            astakos.im.models.AstakosUser.DoesNotExist
1828
            astakos.im.models.ProjectMembership.DoesNotExist
1829
        """
1830
        if isinstance(user, int):
1831
            user = AstakosUser.objects.get(user=user)
1832

    
1833
        m = ProjectMembership.objects.get(person=user, project=self)
1834
        m.remove()
1835

    
1836

    
1837
class PendingMembershipError(Exception):
1838
    pass
1839

    
1840

    
1841
class ProjectMembershipManager(ForUpdateManager):
1842
    pass
1843

    
1844
class ProjectMembership(models.Model):
1845

    
1846
    person              =   models.ForeignKey(AstakosUser)
1847
    request_date        =   models.DateField(auto_now_add=True)
1848
    project             =   models.ForeignKey(Project)
1849

    
1850
    REQUESTED           =   0
1851
    ACCEPTED            =   1
1852
    # User deactivation
1853
    USER_SUSPENDED      =   10
1854
    # Project deactivation
1855
    PROJECT_DEACTIVATED =   100
1856

    
1857
    REMOVED             =   200
1858

    
1859
    ASSOCIATED_STATES   =   set([REQUESTED,
1860
                                 ACCEPTED,
1861
                                 USER_SUSPENDED,
1862
                                 PROJECT_DEACTIVATED])
1863

    
1864
    ACCEPTED_STATES     =   set([ACCEPTED,
1865
                                 USER_SUSPENDED,
1866
                                 PROJECT_DEACTIVATED])
1867

    
1868
    state               =   models.IntegerField(default=REQUESTED,
1869
                                                db_index=True)
1870
    is_pending          =   models.BooleanField(default=False, db_index=True)
1871
    is_active           =   models.BooleanField(default=False, db_index=True)
1872
    application         =   models.ForeignKey(
1873
                                ProjectApplication,
1874
                                null=True,
1875
                                related_name='memberships')
1876
    pending_application =   models.ForeignKey(
1877
                                ProjectApplication,
1878
                                null=True,
1879
                                related_name='pending_memberships')
1880
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1881

    
1882
    acceptance_date     =   models.DateField(null=True, db_index=True)
1883
    leave_request_date  =   models.DateField(null=True)
1884

    
1885
    objects     =   ProjectMembershipManager()
1886

    
1887
    # Compiled queries
1888
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1889

    
1890
    def get_combined_state(self):
1891
        return self.state, self.is_active, self.is_pending
1892

    
1893
    class Meta:
1894
        unique_together = ("person", "project")
1895
        #index_together = [["project", "state"]]
1896

    
1897
    def __str__(self):
1898
        return _("<'%s' membership in '%s'>") % (
1899
                self.person.username, self.project)
1900

    
1901
    __repr__ = __str__
1902

    
1903
    def __init__(self, *args, **kwargs):
1904
        self.state = self.REQUESTED
1905
        super(ProjectMembership, self).__init__(*args, **kwargs)
1906

    
1907
    def _set_history_item(self, reason, date=None):
1908
        if isinstance(reason, basestring):
1909
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1910

    
1911
        history_item = ProjectMembershipHistory(
1912
                            serial=self.id,
1913
                            person=self.person_id,
1914
                            project=self.project_id,
1915
                            date=date or datetime.now(),
1916
                            reason=reason)
1917
        history_item.save()
1918
        serial = history_item.id
1919

    
1920
    def can_accept(self):
1921
        return self.state == self.REQUESTED
1922

    
1923
    def accept(self):
1924
        if self.is_pending:
1925
            m = _("%s: attempt to accept while is pending") % (self,)
1926
            raise AssertionError(m)
1927

    
1928
        if not self.can_accept():
1929
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
1930
            raise AssertionError(m)
1931

    
1932
        now = datetime.now()
1933
        self.acceptance_date = now
1934
        self._set_history_item(reason='ACCEPT', date=now)
1935
        if self.project.is_approved():
1936
            self.state = self.ACCEPTED
1937
            self.is_pending = True
1938
        else:
1939
            self.state = self.PROJECT_DEACTIVATED
1940

    
1941
        self.save()
1942

    
1943
    def can_leave(self):
1944
        return self.can_remove()
1945

    
1946
    def can_remove(self):
1947
        return self.state in self.ACCEPTED_STATES
1948

    
1949
    def remove(self):
1950
        if self.is_pending:
1951
            m = _("%s: attempt to remove while is pending") % (self,)
1952
            raise AssertionError(m)
1953

    
1954
        if not self.can_remove():
1955
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
1956
            raise AssertionError(m)
1957

    
1958
        self._set_history_item(reason='REMOVE')
1959
        self.state = self.REMOVED
1960
        self.is_pending = True
1961
        self.save()
1962

    
1963
    def can_reject(self):
1964
        return self.state == self.REQUESTED
1965

    
1966
    def reject(self):
1967
        if self.is_pending:
1968
            m = _("%s: attempt to reject while is pending") % (self,)
1969
            raise AssertionError(m)
1970

    
1971
        if not self.can_reject():
1972
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
1973
            raise AssertionError(m)
1974

    
1975
        # rejected requests don't need sync,
1976
        # because they were never effected
1977
        self._set_history_item(reason='REJECT')
1978
        self.delete()
1979

    
1980
    def get_diff_quotas(self, sub_list=None, add_list=None):
1981
        if sub_list is None:
1982
            sub_list = []
1983

    
1984
        if add_list is None:
1985
            add_list = []
1986

    
1987
        sub_append = sub_list.append
1988
        add_append = add_list.append
1989
        holder = self.person.uuid
1990

    
1991
        synced_application = self.application
1992
        if synced_application is not None:
1993
            cur_grants = synced_application.projectresourcegrant_set.all()
1994
            for grant in cur_grants:
1995
                sub_append(QuotaLimits(
1996
                               holder       = holder,
1997
                               resource     = str(grant.resource),
1998
                               capacity     = grant.member_capacity,
1999
                               import_limit = grant.member_import_limit,
2000
                               export_limit = grant.member_export_limit))
2001

    
2002
        pending_application = self.pending_application
2003
        if pending_application is not None:
2004
            new_grants = pending_application.projectresourcegrant_set.all()
2005
            for new_grant in new_grants:
2006
                add_append(QuotaLimits(
2007
                               holder       = holder,
2008
                               resource     = str(new_grant.resource),
2009
                               capacity     = new_grant.member_capacity,
2010
                               import_limit = new_grant.member_import_limit,
2011
                               export_limit = new_grant.member_export_limit))
2012

    
2013
        return (sub_list, add_list)
2014

    
2015
    def set_sync(self):
2016
        if not self.is_pending:
2017
            m = _("%s: attempt to sync a non pending membership") % (self,)
2018
            raise AssertionError(m)
2019

    
2020
        state = self.state
2021
        if state == self.ACCEPTED:
2022
            pending_application = self.pending_application
2023
            if pending_application is None:
2024
                m = _("%s: attempt to sync an empty pending application") % (
2025
                    self,)
2026
                raise AssertionError(m)
2027

    
2028
            self.application = pending_application
2029
            self.is_active = True
2030

    
2031
            self.pending_application = None
2032
            self.pending_serial = None
2033

    
2034
            # project.application may have changed in the meantime,
2035
            # in which case we stay PENDING;
2036
            # we are safe to check due to select_for_update
2037
            if self.application == self.project.application:
2038
                self.is_pending = False
2039
            self.save()
2040

    
2041
        elif state == self.PROJECT_DEACTIVATED:
2042
            if self.pending_application:
2043
                m = _("%s: attempt to sync in state '%s' "
2044
                      "with a pending application") % (self, state)
2045
                raise AssertionError(m)
2046

    
2047
            self.application = None
2048
            self.is_active = False
2049
            self.pending_serial = None
2050
            self.is_pending = False
2051
            self.save()
2052

    
2053
        elif state == self.REMOVED:
2054
            self.delete()
2055

    
2056
        else:
2057
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2058
            raise AssertionError(m)
2059

    
2060
    def reset_sync(self):
2061
        if not self.is_pending:
2062
            m = _("%s: attempt to reset a non pending membership") % (self,)
2063
            raise AssertionError(m)
2064

    
2065
        state = self.state
2066
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
2067
            self.pending_application = None
2068
            self.pending_serial = None
2069
            self.save()
2070
        else:
2071
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2072
            raise AssertionError(m)
2073

    
2074
class Serial(models.Model):
2075
    serial  =   models.AutoField(primary_key=True)
2076

    
2077
def new_serial():
2078
    s = Serial.objects.create()
2079
    serial = s.serial
2080
    s.delete()
2081
    return serial
2082

    
2083
def sync_finish_serials(serials_to_ack=None):
2084
    if serials_to_ack is None:
2085
        serials_to_ack = qh_query_serials([])
2086

    
2087
    serials_to_ack = set(serials_to_ack)
2088
    sfu = ProjectMembership.objects.select_for_update()
2089
    memberships = list(sfu.filter(pending_serial__isnull=False))
2090

    
2091
    if memberships:
2092
        for membership in memberships:
2093
            serial = membership.pending_serial
2094
            if serial in serials_to_ack:
2095
                membership.set_sync()
2096
            else:
2097
                membership.reset_sync()
2098

    
2099
        transaction.commit()
2100

    
2101
    qh_ack_serials(list(serials_to_ack))
2102
    return len(memberships)
2103

    
2104
def pre_sync_projects(sync=True):
2105
    ACCEPTED = ProjectMembership.ACCEPTED
2106
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2107
    psfu = Project.objects.select_for_update()
2108

    
2109
    modified = list(psfu.modified_projects())
2110
    if sync:
2111
        for project in modified:
2112
            objects = project.projectmembership_set.select_for_update()
2113

    
2114
            memberships = objects.filter(state=ACCEPTED)
2115
            for membership in memberships:
2116
                membership.is_pending = True
2117
                membership.save()
2118

    
2119
    reactivating = list(psfu.reactivating_projects())
2120
    if sync:
2121
        for project in reactivating:
2122
            objects = project.projectmembership_set.select_for_update()
2123

    
2124
            memberships = objects.filter(state=PROJECT_DEACTIVATED)
2125
            for membership in memberships:
2126
                membership.is_pending = True
2127
                membership.state = ACCEPTED
2128
                membership.save()
2129

    
2130
    deactivating = list(psfu.deactivating_projects())
2131
    if sync:
2132
        for project in deactivating:
2133
            objects = project.projectmembership_set.select_for_update()
2134

    
2135
            # Note: we keep a user-level deactivation
2136
            # (e.g. USER_SUSPENDED) intact
2137
            memberships = objects.filter(state=ACCEPTED)
2138
            for membership in memberships:
2139
                membership.is_pending = True
2140
                membership.state = PROJECT_DEACTIVATED
2141
                membership.save()
2142

    
2143
    return (modified, reactivating, deactivating)
2144

    
2145
def do_sync_projects():
2146

    
2147
    ACCEPTED = ProjectMembership.ACCEPTED
2148
    objects = ProjectMembership.objects.select_for_update()
2149

    
2150
    sub_quota, add_quota = [], []
2151

    
2152
    serial = new_serial()
2153

    
2154
    pending = objects.filter(is_pending=True)
2155
    for membership in pending:
2156

    
2157
        if membership.pending_application:
2158
            m = "%s: impossible: pending_application is not None (%s)" % (
2159
                membership, membership.pending_application)
2160
            raise AssertionError(m)
2161
        if membership.pending_serial:
2162
            m = "%s: impossible: pending_serial is not None (%s)" % (
2163
                membership, membership.pending_serial)
2164
            raise AssertionError(m)
2165

    
2166
        if membership.state == ACCEPTED:
2167
            membership.pending_application = membership.project.application
2168

    
2169
        membership.pending_serial = serial
2170
        membership.get_diff_quotas(sub_quota, add_quota)
2171
        membership.save()
2172

    
2173
    transaction.commit()
2174
    # ProjectApplication.approve() unblocks here
2175
    # and can set PENDING an already PENDING membership
2176
    # which has been scheduled to sync with the old project.application
2177
    # Need to check in ProjectMembership.set_sync()
2178

    
2179
    r = qh_add_quota(serial, sub_quota, add_quota)
2180
    if r:
2181
        m = "cannot sync serial: %d" % serial
2182
        raise RuntimeError(m)
2183

    
2184
    return serial
2185

    
2186
def post_sync_projects():
2187
    ACCEPTED = ProjectMembership.ACCEPTED
2188
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2189
    psfu = Project.objects.select_for_update()
2190

    
2191
    modified = psfu.modified_projects()
2192
    for project in modified:
2193
        objects = project.projectmembership_set.select_for_update()
2194

    
2195
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2196
        if not memberships:
2197
            project.is_modified = False
2198
            project.save()
2199

    
2200
    reactivating = psfu.reactivating_projects()
2201
    for project in reactivating:
2202
        objects = project.projectmembership_set.select_for_update()
2203
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2204
                                          Q(is_pending=True)))
2205
        if not memberships:
2206
            project.reactivate()
2207
            project.save()
2208

    
2209
    deactivating = psfu.deactivating_projects()
2210
    for project in deactivating:
2211
        objects = project.projectmembership_set.select_for_update()
2212

    
2213
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2214
                                          Q(is_pending=True)))
2215
        if not memberships:
2216
            project.deactivate()
2217
            project.save()
2218

    
2219
    transaction.commit()
2220

    
2221
def _sync_projects(sync):
2222
    sync_finish_serials()
2223
    # Informative only -- no select_for_update()
2224
    pending = list(ProjectMembership.objects.filter(is_pending=True))
2225

    
2226
    projects_log = pre_sync_projects(sync)
2227
    if sync:
2228
        serial = do_sync_projects()
2229
        sync_finish_serials([serial])
2230
        post_sync_projects()
2231

    
2232
    return (pending, projects_log)
2233

    
2234
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2235
    return lock_sync(_sync_projects,
2236
                     args=[sync],
2237
                     retries=retries,
2238
                     retry_wait=retry_wait)
2239

    
2240

    
2241
def all_users_quotas(users):
2242
    quotas = {}
2243
    for user in users:
2244
        quotas[user.uuid] = user.all_quotas()
2245
    return quotas
2246

    
2247
def _sync_users(users, sync):
2248
    sync_finish_serials()
2249

    
2250
    existing, nonexisting = qh_check_users(users)
2251
    resources = get_resource_names()
2252
    registered_quotas = qh_get_quota_limits(existing, resources)
2253
    astakos_quotas = all_users_quotas(users)
2254

    
2255
    if sync:
2256
        r = register_users(nonexisting)
2257
        r = send_quotas(astakos_quotas)
2258

    
2259
    return (existing, nonexisting, registered_quotas, astakos_quotas)
2260

    
2261
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2262
    return lock_sync(_sync_users,
2263
                     args=[users, sync],
2264
                     retries=retries,
2265
                     retry_wait=retry_wait)
2266

    
2267
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2268
    users = AstakosUser.objects.filter(is_active=True)
2269
    return sync_users(users, sync, retries=retries, retry_wait=retry_wait)
2270

    
2271
def lock_sync(func, args=[], kwargs={}, retries=3, retry_wait=1.0):
2272
    transaction.commit()
2273

    
2274
    cursor = connection.cursor()
2275
    locked = True
2276
    try:
2277
        while 1:
2278
            cursor.execute("SELECT pg_try_advisory_lock(1)")
2279
            r = cursor.fetchone()
2280
            if r is None:
2281
                m = "Impossible"
2282
                raise AssertionError(m)
2283
            locked = r[0]
2284
            if locked:
2285
                break
2286

    
2287
            retries -= 1
2288
            if retries <= 0:
2289
                return False
2290
            sleep(retry_wait)
2291

    
2292
        return func(*args, **kwargs)
2293

    
2294
    finally:
2295
        if locked:
2296
            cursor.execute("SELECT pg_advisory_unlock(1)")
2297
            cursor.fetchall()
2298

    
2299

    
2300
class ProjectMembershipHistory(models.Model):
2301
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2302
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2303

    
2304
    person  =   models.BigIntegerField()
2305
    project =   models.BigIntegerField()
2306
    date    =   models.DateField(auto_now_add=True)
2307
    reason  =   models.IntegerField()
2308
    serial  =   models.BigIntegerField()
2309

    
2310
### SIGNALS ###
2311
################
2312

    
2313
def create_astakos_user(u):
2314
    try:
2315
        AstakosUser.objects.get(user_ptr=u.pk)
2316
    except AstakosUser.DoesNotExist:
2317
        extended_user = AstakosUser(user_ptr_id=u.pk)
2318
        extended_user.__dict__.update(u.__dict__)
2319
        extended_user.save()
2320
        if not extended_user.has_auth_provider('local'):
2321
            extended_user.add_auth_provider('local')
2322
    except BaseException, e:
2323
        logger.exception(e)
2324

    
2325

    
2326
def fix_superusers(sender, **kwargs):
2327
    # Associate superusers with AstakosUser
2328
    admins = User.objects.filter(is_superuser=True)
2329
    for u in admins:
2330
        create_astakos_user(u)
2331
post_syncdb.connect(fix_superusers)
2332

    
2333

    
2334
def user_post_save(sender, instance, created, **kwargs):
2335
    if not created:
2336
        return
2337
    create_astakos_user(instance)
2338
post_save.connect(user_post_save, sender=User)
2339

    
2340
def astakosuser_post_save(sender, instance, created, **kwargs):
2341
    pass
2342

    
2343
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2344

    
2345
def resource_post_save(sender, instance, created, **kwargs):
2346
    pass
2347

    
2348
post_save.connect(resource_post_save, sender=Resource)
2349

    
2350
def renew_token(sender, instance, **kwargs):
2351
    if not instance.auth_token:
2352
        instance.renew_token()
2353
pre_save.connect(renew_token, sender=AstakosUser)
2354
pre_save.connect(renew_token, sender=Service)
2355