Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 3e3743f2

History | View | Annotate | Download (78.2 kB)

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

    
34
import hashlib
35
import uuid
36
import logging
37
import json
38
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_application(self, application):
743
        return application.owner == self
744

    
745
    def owns_project(self, project):
746
        return project.application.owner == self
747

    
748
    def is_project_member(self, project_or_application):
749
        return self.get_status_in_project(project_or_application) in \
750
                                        ProjectMembership.ASSOCIATED_STATES
751

    
752
    def is_project_accepted_member(self, project_or_application):
753
        return self.get_status_in_project(project_or_application) in \
754
                                            ProjectMembership.ACCEPTED_STATES
755

    
756
    def get_status_in_project(self, project_or_application):
757
        application = project_or_application
758
        if isinstance(project_or_application, Project):
759
            application = project_or_application.project
760
        return application.user_status(self)
761

    
762

    
763
class AstakosUserAuthProviderManager(models.Manager):
764

    
765
    def active(self, **filters):
766
        return self.filter(active=True, **filters)
767

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

    
776

    
777

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

    
796
    objects = AstakosUserAuthProviderManager()
797

    
798
    class Meta:
799
        unique_together = (('identifier', 'module', 'user'), )
800
        ordering = ('module', 'created')
801

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

    
811
        for key,value in self.info.iteritems():
812
            setattr(self, 'info_%s' % key, value)
813

    
814

    
815
    @property
816
    def settings(self):
817
        return auth_providers.get_provider(self.module)
818

    
819
    @property
820
    def details_display(self):
821
        try:
822
          return self.settings.get_details_tpl_display % self.__dict__
823
        except:
824
          return ''
825

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

    
839
    def can_remove(self):
840
        return self.user.can_remove_auth_provider(self.module)
841

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

    
849
    def __repr__(self):
850
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
851

    
852
    def __unicode__(self):
853
        if self.identifier:
854
            return "%s:%s" % (self.module, self.identifier)
855
        if self.auth_backend:
856
            return "%s:%s" % (self.module, self.auth_backend)
857
        return self.module
858

    
859
    def save(self, *args, **kwargs):
860
        self.info_data = json.dumps(self.info)
861
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
862

    
863

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

    
891
    update_or_create = _update_or_create
892

    
893

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

    
903
    class Meta:
904
        unique_together = ("resource", "user")
905

    
906
    def quota_values(self):
907
        return QuotaValues(
908
            quantity = self.quantity,
909
            capacity = self.capacity,
910
            import_limit = self.import_limit,
911
            export_limit = self.export_limit)
912

    
913

    
914
class ApprovalTerms(models.Model):
915
    """
916
    Model for approval terms
917
    """
918

    
919
    date = models.DateTimeField(
920
        _('Issue date'), db_index=True, auto_now_add=True)
921
    location = models.CharField(_('Terms location'), max_length=255)
922

    
923

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

    
937
    def __init__(self, *args, **kwargs):
938
        super(Invitation, self).__init__(*args, **kwargs)
939
        if not self.id:
940
            self.code = _generate_invitation_code()
941

    
942
    def consume(self):
943
        self.is_consumed = True
944
        self.consumed = datetime.now()
945
        self.save()
946

    
947
    def __unicode__(self):
948
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
949

    
950

    
951
class EmailChangeManager(models.Manager):
952

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

959
        If the key is valid and has not expired, return the ``User``
960
        after activating.
961

962
        If the key is not valid or has expired, return ``None``.
963

964
        If the key is valid but the ``User`` is already active,
965
        return ``None``.
966

967
        After successful email change the activation record is deleted.
968

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

    
997

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

    
1008
    objects = EmailChangeManager()
1009

    
1010
    def get_url(self):
1011
        return reverse('email_change_confirm',
1012
                      kwargs={'activation_key': self.activation_key})
1013

    
1014
    def activation_key_expired(self):
1015
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1016
        return self.requested_at + expiration_date < datetime.now()
1017

    
1018

    
1019
class AdditionalMail(models.Model):
1020
    """
1021
    Model for registring invitations
1022
    """
1023
    owner = models.ForeignKey(AstakosUser)
1024
    email = models.EmailField()
1025

    
1026

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

    
1036

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

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

    
1063
    class Meta:
1064
        unique_together = ("provider", "third_party_identifier")
1065

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

    
1075
        return user
1076

    
1077
    @property
1078
    def realname(self):
1079
        return '%s %s' %(self.first_name, self.last_name)
1080

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

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

    
1101
    def generate_token(self):
1102
        self.password = self.third_party_identifier
1103
        self.last_login = datetime.now()
1104
        self.token = default_token_generator.make_token(self)
1105

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

    
1110

    
1111
### PROJECTS ###
1112
################
1113

    
1114
def synced_model_metaclass(class_name, class_parents, class_attributes):
1115

    
1116
    new_attributes = {}
1117
    sync_attributes = {}
1118

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

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

    
1131
    prefix = sync_attributes.pop('prefix')
1132
    class_name = sync_attributes.pop('classname', prefix + '_model')
1133

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

    
1142
        new_attributes[newname] = value
1143

    
1144
    newclass = type(class_name, class_parents, new_attributes)
1145
    return newclass
1146

    
1147

    
1148
def make_synced(prefix='sync', name='SyncedState'):
1149

    
1150
    the_name = name
1151
    the_prefix = prefix
1152

    
1153
    class SyncedState(models.Model):
1154

    
1155
        sync_classname      = the_name
1156
        sync_prefix         = the_prefix
1157
        __metaclass__       = synced_model_metaclass
1158

    
1159
        sync_new_state      = models.BigIntegerField(null=True)
1160
        sync_synced_state   = models.BigIntegerField(null=True)
1161
        STATUS_SYNCED       = 0
1162
        STATUS_PENDING      = 1
1163
        sync_status         = models.IntegerField(db_index=True)
1164

    
1165
        class Meta:
1166
            abstract = True
1167

    
1168
        class NotSynced(Exception):
1169
            pass
1170

    
1171
        def sync_init_state(self, state):
1172
            self.sync_synced_state = state
1173
            self.sync_new_state = state
1174
            self.sync_status = self.STATUS_SYNCED
1175

    
1176
        def sync_get_status(self):
1177
            return self.sync_status
1178

    
1179
        def sync_set_status(self):
1180
            if self.sync_new_state != self.sync_synced_state:
1181
                self.sync_status = self.STATUS_PENDING
1182
            else:
1183
                self.sync_status = self.STATUS_SYNCED
1184

    
1185
        def sync_set_synced(self):
1186
            self.sync_synced_state = self.sync_new_state
1187
            self.sync_status = self.STATUS_SYNCED
1188

    
1189
        def sync_get_synced_state(self):
1190
            return self.sync_synced_state
1191

    
1192
        def sync_set_new_state(self, new_state):
1193
            self.sync_new_state = new_state
1194
            self.sync_set_status()
1195

    
1196
        def sync_get_new_state(self):
1197
            return self.sync_new_state
1198

    
1199
        def sync_set_synced_state(self, synced_state):
1200
            self.sync_synced_state = synced_state
1201
            self.sync_set_status()
1202

    
1203
        def sync_get_pending_objects(self):
1204
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1205
            return self.objects.filter(**kw)
1206

    
1207
        def sync_get_synced_objects(self):
1208
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1209
            return self.objects.filter(**kw)
1210

    
1211
        def sync_verify_get_synced_state(self):
1212
            status = self.sync_get_status()
1213
            state = self.sync_get_synced_state()
1214
            verified = (status == self.STATUS_SYNCED)
1215
            return state, verified
1216

    
1217
        def sync_is_synced(self):
1218
            state, verified = self.sync_verify_get_synced_state()
1219
            return verified
1220

    
1221
    return SyncedState
1222

    
1223
SyncedState = make_synced(prefix='sync', name='SyncedState')
1224

    
1225

    
1226
class ProjectApplicationManager(ForUpdateManager):
1227

    
1228
    def user_visible_projects(self, *filters, **kw_filters):
1229
        model = self.model
1230
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1231

    
1232
    def user_visible_by_chain(self, flt):
1233
        model = self.model
1234
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1235
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1236
        by_chain = dict(pending.annotate(models.Max('id')))
1237
        by_chain.update(approved.annotate(models.Max('id')))
1238
        return self.filter(flt, id__in=by_chain.values())
1239

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

    
1247
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1248

    
1249
    def search_by_name(self, *search_strings):
1250
        q = Q()
1251
        for s in search_strings:
1252
            q = q | Q(name__icontains=s)
1253
        return self.filter(q)
1254

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

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

    
1270

    
1271
class Chain(models.Model):
1272
    chain  =   models.AutoField(primary_key=True)
1273

    
1274
def new_chain():
1275
    c = Chain.objects.create()
1276
    chain = c.chain
1277
    c.delete()
1278
    return chain
1279

    
1280

    
1281
class ProjectApplication(models.Model):
1282
    applicant               =   models.ForeignKey(
1283
                                    AstakosUser,
1284
                                    related_name='projects_applied',
1285
                                    db_index=True)
1286

    
1287
    PENDING     =    0
1288
    APPROVED    =    1
1289
    REPLACED    =    2
1290
    DENIED      =    3
1291
    DISMISSED   =    4
1292
    CANCELLED   =    5
1293

    
1294
    state                   =   models.IntegerField(default=PENDING,
1295
                                                    db_index=True)
1296

    
1297
    owner                   =   models.ForeignKey(
1298
                                    AstakosUser,
1299
                                    related_name='projects_owned',
1300
                                    db_index=True)
1301

    
1302
    chain                   =   models.IntegerField()
1303
    precursor_application   =   models.ForeignKey('ProjectApplication',
1304
                                                  null=True,
1305
                                                  blank=True)
1306

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

    
1324
    objects                 =   ProjectApplicationManager()
1325

    
1326
    # Compiled queries
1327
    Q_PENDING  = Q(state=PENDING)
1328
    Q_APPROVED = Q(state=APPROVED)
1329
    Q_DENIED   = Q(state=DENIED)
1330

    
1331
    class Meta:
1332
        unique_together = ("chain", "id")
1333

    
1334
    def __unicode__(self):
1335
        return "%s applied by %s" % (self.name, self.applicant)
1336

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

    
1347
    def get_project(self):
1348
        try:
1349
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1350
            return Project
1351
        except Project.DoesNotExist, e:
1352
            return None
1353

    
1354
    def state_display(self):
1355
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1356

    
1357
    def add_resource_policy(self, service, resource, uplimit):
1358
        """Raises ObjectDoesNotExist, IntegrityError"""
1359
        q = self.projectresourcegrant_set
1360
        resource = Resource.objects.get(service__name=service, name=resource)
1361
        q.create(resource=resource, member_capacity=uplimit)
1362

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

    
1375
        return status
1376

    
1377
    def user_status_display(self, user):
1378
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1379

    
1380
    def members_count(self):
1381
        return self.project.approved_memberships.count()
1382

    
1383
    @property
1384
    def grants(self):
1385
        return self.projectresourcegrant_set.values(
1386
            'member_capacity', 'resource__name', 'resource__service__name')
1387

    
1388
    @property
1389
    def resource_policies(self):
1390
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1391

    
1392
    @resource_policies.setter
1393
    def resource_policies(self, policies):
1394
        for p in policies:
1395
            service = p.get('service', None)
1396
            resource = p.get('resource', None)
1397
            uplimit = p.get('uplimit', 0)
1398
            self.add_resource_policy(service, resource, uplimit)
1399

    
1400
    def pending_modifications(self):
1401
        q = self.chained_applications()
1402
        q = q.filter(~Q(id=self.id) & Q(state=self.PENDING))
1403
        q = q.order_by('id')
1404
        return q
1405

    
1406
    def last_pending(self):
1407
        try:
1408
            return self.pending_modifications().order_by('-id')[0]
1409
        except IndexError:
1410
            return None
1411

    
1412
    def is_modification(self):
1413
        if self.state != self.PENDING:
1414
            return False
1415
        parents = self.chained_applications().filter(id__lt=self.id)
1416
        parents = parents.filter(state__in=[self.APPROVED])
1417
        return parents.count() > 0
1418

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

    
1422
    def has_pending_modifications(self):
1423
        return bool(self.last_pending())
1424

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

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

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

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

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

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

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

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

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

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

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

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

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

1484
        Raises:
1485
            PermissionDenied
1486
        """
1487

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

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

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

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

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

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

    
1522
        project.save()
1523

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

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

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

    
1536
class ProjectResourceGrant(models.Model):
1537

    
1538
    resource                =   models.ForeignKey(Resource)
1539
    project_application     =   models.ForeignKey(ProjectApplication,
1540
                                                  null=True)
1541
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1542
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1543
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1544
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1545
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1546
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1547

    
1548
    objects = ExtendedManager()
1549

    
1550
    class Meta:
1551
        unique_together = ("resource", "project_application")
1552

    
1553
    def member_quota_values(self):
1554
        return QuotaValues(
1555
            quantity = 0,
1556
            capacity = self.member_capacity,
1557
            import_limit = self.member_import_limit,
1558
            export_limit = self.member_export_limit)
1559

    
1560
    def display_member_capacity(self):
1561
        if self.member_capacity:
1562
            if self.resource.unit:
1563
                return ProjectResourceGrant.display_filesize(
1564
                    self.member_capacity)
1565
            else:
1566
                if math.isinf(self.member_capacity):
1567
                    return 'Unlimited'
1568
                else:
1569
                    return self.member_capacity
1570
        else:
1571
            return 'Unlimited'
1572

    
1573
    def __str__(self):
1574
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1575
                                        self.display_member_capacity())
1576

    
1577
    @classmethod
1578
    def display_filesize(cls, value):
1579
        try:
1580
            value = float(value)
1581
        except:
1582
            return
1583
        else:
1584
            if math.isinf(value):
1585
                return 'Unlimited'
1586
            if value > 1:
1587
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1588
                                [0, 0, 0, 0, 0, 0])
1589
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1590
                quotient = float(value) / 1024**exponent
1591
                unit, value_decimals = unit_list[exponent]
1592
                format_string = '{0:.%sf} {1}' % (value_decimals)
1593
                return format_string.format(quotient, unit)
1594
            if value == 0:
1595
                return '0 bytes'
1596
            if value == 1:
1597
                return '1 byte'
1598
            else:
1599
               return '0'
1600

    
1601

    
1602
class ProjectManager(ForUpdateManager):
1603

    
1604
    def terminated_projects(self):
1605
        q = self.model.Q_TERMINATED
1606
        return self.filter(q)
1607

    
1608
    def not_terminated_projects(self):
1609
        q = ~self.model.Q_TERMINATED
1610
        return self.filter(q)
1611

    
1612
    def terminating_projects(self):
1613
        q = self.model.Q_TERMINATED & Q(is_active=True)
1614
        return self.filter(q)
1615

    
1616
    def deactivated_projects(self):
1617
        q = self.model.Q_DEACTIVATED
1618
        return self.filter(q)
1619

    
1620
    def deactivating_projects(self):
1621
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1622
        return self.filter(q)
1623

    
1624
    def modified_projects(self):
1625
        return self.filter(is_modified=True)
1626

    
1627
    def reactivating_projects(self):
1628
        return self.filter(state=Project.APPROVED, is_active=False)
1629

    
1630
    def expired_projects(self):
1631
        q = (~Q(state=Project.TERMINATED) &
1632
              Q(application__end_date__lt=datetime.now()))
1633
        return self.filter(q)
1634

    
1635

    
1636
class Project(models.Model):
1637

    
1638
    application                 =   models.OneToOneField(
1639
                                            ProjectApplication,
1640
                                            related_name='project')
1641
    last_approval_date          =   models.DateTimeField(null=True)
1642

    
1643
    members                     =   models.ManyToManyField(
1644
                                            AstakosUser,
1645
                                            through='ProjectMembership')
1646

    
1647
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1648
    deactivation_date           =   models.DateTimeField(null=True)
1649

    
1650
    creation_date               =   models.DateTimeField(auto_now_add=True)
1651
    name                        =   models.CharField(
1652
                                            max_length=80,
1653
                                            db_index=True,
1654
                                            unique=True)
1655

    
1656
    APPROVED    = 1
1657
    SUSPENDED   = 10
1658
    TERMINATED  = 100
1659

    
1660
    is_modified                 =   models.BooleanField(default=False,
1661
                                                        db_index=True)
1662
    is_active                   =   models.BooleanField(default=True,
1663
                                                        db_index=True)
1664
    state                       =   models.IntegerField(default=APPROVED,
1665
                                                        db_index=True)
1666

    
1667
    objects     =   ProjectManager()
1668

    
1669
    # Compiled queries
1670
    Q_TERMINATED  = Q(state=TERMINATED)
1671
    Q_SUSPENDED   = Q(state=SUSPENDED)
1672
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1673

    
1674
    def __str__(self):
1675
        return _("<project %s '%s'>") % (self.id, self.application.name)
1676

    
1677
    __repr__ = __str__
1678

    
1679
    STATE_DISPLAY = {
1680
        APPROVED   : 'APPROVED',
1681
        SUSPENDED  : 'SUSPENDED',
1682
        TERMINATED : 'TERMINATED'
1683
        }
1684

    
1685
    def state_display(self):
1686
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1687

    
1688
    def expiration_info(self):
1689
        return (str(self.id), self.name, self.state_display(),
1690
                str(self.application.end_date))
1691

    
1692
    def is_deactivated(self, reason=None):
1693
        if reason is not None:
1694
            return self.state == reason
1695

    
1696
        return self.state != self.APPROVED
1697

    
1698
    def is_deactivating(self, reason=None):
1699
        if not self.is_active:
1700
            return False
1701

    
1702
        return self.is_deactivated(reason)
1703

    
1704
    def is_deactivated_strict(self, reason=None):
1705
        if self.is_active:
1706
            return False
1707

    
1708
        return self.is_deactivated(reason)
1709

    
1710
    ### Deactivation calls
1711

    
1712
    def deactivate(self):
1713
        self.deactivation_date = datetime.now()
1714
        self.is_active = False
1715

    
1716
    def reactivate(self):
1717
        self.deactivation_date = None
1718
        self.is_active = True
1719

    
1720
    def terminate(self):
1721
        self.deactivation_reason = 'TERMINATED'
1722
        self.state = self.TERMINATED
1723
        self.save()
1724

    
1725
    def suspend(self):
1726
        self.deactivation_reason = 'SUSPENDED'
1727
        self.state = self.SUSPENDED
1728
        self.save()
1729

    
1730
    def resume(self):
1731
        self.deactivation_reason = None
1732
        self.state = self.APPROVED
1733
        self.save()
1734

    
1735
    ### Logical checks
1736

    
1737
    def is_inconsistent(self):
1738
        now = datetime.now()
1739
        dates = [self.creation_date,
1740
                 self.last_approval_date,
1741
                 self.deactivation_date]
1742
        return any([date > now for date in dates])
1743

    
1744
    def is_active_strict(self):
1745
        return self.is_active and self.state == self.APPROVED
1746

    
1747
    def is_approved(self):
1748
        return self.state == self.APPROVED
1749

    
1750
    @property
1751
    def is_alive(self):
1752
        return not self.is_terminated
1753

    
1754
    @property
1755
    def is_terminated(self):
1756
        return self.is_deactivated(self.TERMINATED)
1757

    
1758
    @property
1759
    def is_suspended(self):
1760
        return self.is_deactivated(self.SUSPENDED)
1761

    
1762
    def violates_resource_grants(self):
1763
        return False
1764

    
1765
    def violates_members_limit(self, adding=0):
1766
        application = self.application
1767
        limit = application.limit_on_members_number
1768
        if limit is None:
1769
            return False
1770
        return (len(self.approved_members) + adding > limit)
1771

    
1772

    
1773
    ### Other
1774

    
1775
    def count_pending_memberships(self):
1776
        memb_set = self.projectmembership_set
1777
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1778
        return memb_count
1779

    
1780
    @property
1781
    def approved_memberships(self):
1782
        query = ProjectMembership.Q_ACCEPTED_STATES
1783
        return self.projectmembership_set.filter(query)
1784

    
1785
    @property
1786
    def approved_members(self):
1787
        return [m.person for m in self.approved_memberships]
1788

    
1789
    def add_member(self, user):
1790
        """
1791
        Raises:
1792
            django.exceptions.PermissionDenied
1793
            astakos.im.models.AstakosUser.DoesNotExist
1794
        """
1795
        if isinstance(user, int):
1796
            user = AstakosUser.objects.get(user=user)
1797

    
1798
        m, created = ProjectMembership.objects.get_or_create(
1799
            person=user, project=self
1800
        )
1801
        m.accept()
1802

    
1803
    def remove_member(self, user):
1804
        """
1805
        Raises:
1806
            django.exceptions.PermissionDenied
1807
            astakos.im.models.AstakosUser.DoesNotExist
1808
            astakos.im.models.ProjectMembership.DoesNotExist
1809
        """
1810
        if isinstance(user, int):
1811
            user = AstakosUser.objects.get(user=user)
1812

    
1813
        m = ProjectMembership.objects.get(person=user, project=self)
1814
        m.remove()
1815

    
1816

    
1817
class PendingMembershipError(Exception):
1818
    pass
1819

    
1820

    
1821
class ProjectMembershipManager(ForUpdateManager):
1822
    pass
1823

    
1824
class ProjectMembership(models.Model):
1825

    
1826
    person              =   models.ForeignKey(AstakosUser)
1827
    request_date        =   models.DateField(auto_now_add=True)
1828
    project             =   models.ForeignKey(Project)
1829

    
1830
    REQUESTED           =   0
1831
    ACCEPTED            =   1
1832
    # User deactivation
1833
    USER_SUSPENDED      =   10
1834
    # Project deactivation
1835
    PROJECT_DEACTIVATED =   100
1836

    
1837
    REMOVED             =   200
1838

    
1839
    ASSOCIATED_STATES   =   set([REQUESTED,
1840
                                 ACCEPTED,
1841
                                 USER_SUSPENDED,
1842
                                 PROJECT_DEACTIVATED])
1843

    
1844
    ACCEPTED_STATES     =   set([ACCEPTED,
1845
                                 USER_SUSPENDED,
1846
                                 PROJECT_DEACTIVATED])
1847

    
1848
    state               =   models.IntegerField(default=REQUESTED,
1849
                                                db_index=True)
1850
    is_pending          =   models.BooleanField(default=False, db_index=True)
1851
    is_active           =   models.BooleanField(default=False, db_index=True)
1852
    application         =   models.ForeignKey(
1853
                                ProjectApplication,
1854
                                null=True,
1855
                                related_name='memberships')
1856
    pending_application =   models.ForeignKey(
1857
                                ProjectApplication,
1858
                                null=True,
1859
                                related_name='pending_memberships')
1860
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1861

    
1862
    acceptance_date     =   models.DateField(null=True, db_index=True)
1863
    leave_request_date  =   models.DateField(null=True)
1864

    
1865
    objects     =   ProjectMembershipManager()
1866

    
1867
    # Compiled queries
1868
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1869

    
1870
    def get_combined_state(self):
1871
        return self.state, self.is_active, self.is_pending
1872

    
1873
    class Meta:
1874
        unique_together = ("person", "project")
1875
        #index_together = [["project", "state"]]
1876

    
1877
    def __str__(self):
1878
        return _("<'%s' membership in '%s'>") % (
1879
                self.person.username, self.project)
1880

    
1881
    __repr__ = __str__
1882

    
1883
    def __init__(self, *args, **kwargs):
1884
        self.state = self.REQUESTED
1885
        super(ProjectMembership, self).__init__(*args, **kwargs)
1886

    
1887
    def _set_history_item(self, reason, date=None):
1888
        if isinstance(reason, basestring):
1889
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1890

    
1891
        history_item = ProjectMembershipHistory(
1892
                            serial=self.id,
1893
                            person=self.person_id,
1894
                            project=self.project_id,
1895
                            date=date or datetime.now(),
1896
                            reason=reason)
1897
        history_item.save()
1898
        serial = history_item.id
1899

    
1900
    def can_accept(self):
1901
        return self.state == self.REQUESTED
1902

    
1903
    def accept(self):
1904
        if self.is_pending:
1905
            m = _("%s: attempt to accept while is pending") % (self,)
1906
            raise AssertionError(m)
1907

    
1908
        if not self.can_accept():
1909
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
1910
            raise AssertionError(m)
1911

    
1912
        now = datetime.now()
1913
        self.acceptance_date = now
1914
        self._set_history_item(reason='ACCEPT', date=now)
1915
        if self.project.is_approved():
1916
            self.state = self.ACCEPTED
1917
            self.is_pending = True
1918
        else:
1919
            self.state = self.PROJECT_DEACTIVATED
1920

    
1921
        self.save()
1922

    
1923
    def can_leave(self):
1924
        return self.can_remove()
1925

    
1926
    def can_remove(self):
1927
        return self.state in self.ACCEPTED_STATES
1928

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

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

    
1938
        self._set_history_item(reason='REMOVE')
1939
        self.state = self.REMOVED
1940
        self.is_pending = True
1941
        self.save()
1942

    
1943
    def can_reject(self):
1944
        return self.state == self.REQUESTED
1945

    
1946
    def reject(self):
1947
        if self.is_pending:
1948
            m = _("%s: attempt to reject while is pending") % (self,)
1949
            raise AssertionError(m)
1950

    
1951
        if not self.can_reject():
1952
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
1953
            raise AssertionError(m)
1954

    
1955
        # rejected requests don't need sync,
1956
        # because they were never effected
1957
        self._set_history_item(reason='REJECT')
1958
        self.delete()
1959

    
1960
    def get_diff_quotas(self, sub_list=None, add_list=None):
1961
        if sub_list is None:
1962
            sub_list = []
1963

    
1964
        if add_list is None:
1965
            add_list = []
1966

    
1967
        sub_append = sub_list.append
1968
        add_append = add_list.append
1969
        holder = self.person.uuid
1970

    
1971
        synced_application = self.application
1972
        if synced_application is not None:
1973
            cur_grants = synced_application.projectresourcegrant_set.all()
1974
            for grant in cur_grants:
1975
                sub_append(QuotaLimits(
1976
                               holder       = holder,
1977
                               resource     = str(grant.resource),
1978
                               capacity     = grant.member_capacity,
1979
                               import_limit = grant.member_import_limit,
1980
                               export_limit = grant.member_export_limit))
1981

    
1982
        pending_application = self.pending_application
1983
        if pending_application is not None:
1984
            new_grants = pending_application.projectresourcegrant_set.all()
1985
            for new_grant in new_grants:
1986
                add_append(QuotaLimits(
1987
                               holder       = holder,
1988
                               resource     = str(new_grant.resource),
1989
                               capacity     = new_grant.member_capacity,
1990
                               import_limit = new_grant.member_import_limit,
1991
                               export_limit = new_grant.member_export_limit))
1992

    
1993
        return (sub_list, add_list)
1994

    
1995
    def set_sync(self):
1996
        if not self.is_pending:
1997
            m = _("%s: attempt to sync a non pending membership") % (self,)
1998
            raise AssertionError(m)
1999

    
2000
        state = self.state
2001
        if state == self.ACCEPTED:
2002
            pending_application = self.pending_application
2003
            if pending_application is None:
2004
                m = _("%s: attempt to sync an empty pending application") % (
2005
                    self,)
2006
                raise AssertionError(m)
2007

    
2008
            self.application = pending_application
2009
            self.is_active = True
2010

    
2011
            self.pending_application = None
2012
            self.pending_serial = None
2013

    
2014
            # project.application may have changed in the meantime,
2015
            # in which case we stay PENDING;
2016
            # we are safe to check due to select_for_update
2017
            if self.application == self.project.application:
2018
                self.is_pending = False
2019
            self.save()
2020

    
2021
        elif state == self.PROJECT_DEACTIVATED:
2022
            if self.pending_application:
2023
                m = _("%s: attempt to sync in state '%s' "
2024
                      "with a pending application") % (self, state)
2025
                raise AssertionError(m)
2026

    
2027
            self.application = None
2028
            self.is_active = False
2029
            self.pending_serial = None
2030
            self.is_pending = False
2031
            self.save()
2032

    
2033
        elif state == self.REMOVED:
2034
            self.delete()
2035

    
2036
        else:
2037
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2038
            raise AssertionError(m)
2039

    
2040
    def reset_sync(self):
2041
        if not self.is_pending:
2042
            m = _("%s: attempt to reset a non pending membership") % (self,)
2043
            raise AssertionError(m)
2044

    
2045
        state = self.state
2046
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
2047
            self.pending_application = None
2048
            self.pending_serial = None
2049
            self.save()
2050
        else:
2051
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2052
            raise AssertionError(m)
2053

    
2054
class Serial(models.Model):
2055
    serial  =   models.AutoField(primary_key=True)
2056

    
2057
def new_serial():
2058
    s = Serial.objects.create()
2059
    serial = s.serial
2060
    s.delete()
2061
    return serial
2062

    
2063
def sync_finish_serials(serials_to_ack=None):
2064
    if serials_to_ack is None:
2065
        serials_to_ack = qh_query_serials([])
2066

    
2067
    serials_to_ack = set(serials_to_ack)
2068
    sfu = ProjectMembership.objects.select_for_update()
2069
    memberships = list(sfu.filter(pending_serial__isnull=False))
2070

    
2071
    if memberships:
2072
        for membership in memberships:
2073
            serial = membership.pending_serial
2074
            if serial in serials_to_ack:
2075
                membership.set_sync()
2076
            else:
2077
                membership.reset_sync()
2078

    
2079
        transaction.commit()
2080

    
2081
    qh_ack_serials(list(serials_to_ack))
2082
    return len(memberships)
2083

    
2084
def pre_sync_projects(sync=True):
2085
    ACCEPTED = ProjectMembership.ACCEPTED
2086
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2087
    psfu = Project.objects.select_for_update()
2088

    
2089
    modified = list(psfu.modified_projects())
2090
    if sync:
2091
        for project in modified:
2092
            objects = project.projectmembership_set.select_for_update()
2093

    
2094
            memberships = objects.filter(state=ACCEPTED)
2095
            for membership in memberships:
2096
                membership.is_pending = True
2097
                membership.save()
2098

    
2099
    reactivating = list(psfu.reactivating_projects())
2100
    if sync:
2101
        for project in reactivating:
2102
            objects = project.projectmembership_set.select_for_update()
2103

    
2104
            memberships = objects.filter(state=PROJECT_DEACTIVATED)
2105
            for membership in memberships:
2106
                membership.is_pending = True
2107
                membership.state = ACCEPTED
2108
                membership.save()
2109

    
2110
    deactivating = list(psfu.deactivating_projects())
2111
    if sync:
2112
        for project in deactivating:
2113
            objects = project.projectmembership_set.select_for_update()
2114

    
2115
            # Note: we keep a user-level deactivation
2116
            # (e.g. USER_SUSPENDED) intact
2117
            memberships = objects.filter(state=ACCEPTED)
2118
            for membership in memberships:
2119
                membership.is_pending = True
2120
                membership.state = PROJECT_DEACTIVATED
2121
                membership.save()
2122

    
2123
    return (modified, reactivating, deactivating)
2124

    
2125
def do_sync_projects():
2126

    
2127
    ACCEPTED = ProjectMembership.ACCEPTED
2128
    objects = ProjectMembership.objects.select_for_update()
2129

    
2130
    sub_quota, add_quota = [], []
2131

    
2132
    serial = new_serial()
2133

    
2134
    pending = objects.filter(is_pending=True)
2135
    for membership in pending:
2136

    
2137
        if membership.pending_application:
2138
            m = "%s: impossible: pending_application is not None (%s)" % (
2139
                membership, membership.pending_application)
2140
            raise AssertionError(m)
2141
        if membership.pending_serial:
2142
            m = "%s: impossible: pending_serial is not None (%s)" % (
2143
                membership, membership.pending_serial)
2144
            raise AssertionError(m)
2145

    
2146
        if membership.state == ACCEPTED:
2147
            membership.pending_application = membership.project.application
2148

    
2149
        membership.pending_serial = serial
2150
        membership.get_diff_quotas(sub_quota, add_quota)
2151
        membership.save()
2152

    
2153
    transaction.commit()
2154
    # ProjectApplication.approve() unblocks here
2155
    # and can set PENDING an already PENDING membership
2156
    # which has been scheduled to sync with the old project.application
2157
    # Need to check in ProjectMembership.set_sync()
2158

    
2159
    r = qh_add_quota(serial, sub_quota, add_quota)
2160
    if r:
2161
        m = "cannot sync serial: %d" % serial
2162
        raise RuntimeError(m)
2163

    
2164
    return serial
2165

    
2166
def post_sync_projects():
2167
    ACCEPTED = ProjectMembership.ACCEPTED
2168
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2169
    psfu = Project.objects.select_for_update()
2170

    
2171
    modified = psfu.modified_projects()
2172
    for project in modified:
2173
        objects = project.projectmembership_set.select_for_update()
2174

    
2175
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2176
        if not memberships:
2177
            project.is_modified = False
2178
            project.save()
2179

    
2180
    reactivating = psfu.reactivating_projects()
2181
    for project in reactivating:
2182
        objects = project.projectmembership_set.select_for_update()
2183
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2184
                                          Q(is_pending=True)))
2185
        if not memberships:
2186
            project.reactivate()
2187
            project.save()
2188

    
2189
    deactivating = psfu.deactivating_projects()
2190
    for project in deactivating:
2191
        objects = project.projectmembership_set.select_for_update()
2192

    
2193
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2194
                                          Q(is_pending=True)))
2195
        if not memberships:
2196
            project.deactivate()
2197
            project.save()
2198

    
2199
    transaction.commit()
2200

    
2201
def _sync_projects(sync):
2202
    sync_finish_serials()
2203
    # Informative only -- no select_for_update()
2204
    pending = list(ProjectMembership.objects.filter(is_pending=True))
2205

    
2206
    projects_log = pre_sync_projects(sync)
2207
    if sync:
2208
        serial = do_sync_projects()
2209
        sync_finish_serials([serial])
2210
        post_sync_projects()
2211

    
2212
    return (pending, projects_log)
2213

    
2214
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2215
    return lock_sync(_sync_projects,
2216
                     args=[sync],
2217
                     retries=retries,
2218
                     retry_wait=retry_wait)
2219

    
2220

    
2221
def all_users_quotas(users):
2222
    quotas = {}
2223
    for user in users:
2224
        quotas[user.uuid] = user.all_quotas()
2225
    return quotas
2226

    
2227
def _sync_users(users, sync):
2228
    sync_finish_serials()
2229

    
2230
    existing, nonexisting = qh_check_users(users)
2231
    resources = get_resource_names()
2232
    registered_quotas = qh_get_quota_limits(existing, resources)
2233
    astakos_quotas = all_users_quotas(users)
2234

    
2235
    if sync:
2236
        r = register_users(nonexisting)
2237
        r = send_quotas(astakos_quotas)
2238

    
2239
    return (existing, nonexisting, registered_quotas, astakos_quotas)
2240

    
2241
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2242
    return lock_sync(_sync_users,
2243
                     args=[users, sync],
2244
                     retries=retries,
2245
                     retry_wait=retry_wait)
2246

    
2247
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2248
    users = AstakosUser.objects.filter(is_active=True)
2249
    return sync_users(users, sync, retries=retries, retry_wait=retry_wait)
2250

    
2251
def lock_sync(func, args=[], kwargs={}, retries=3, retry_wait=1.0):
2252
    transaction.commit()
2253

    
2254
    cursor = connection.cursor()
2255
    locked = True
2256
    try:
2257
        while 1:
2258
            cursor.execute("SELECT pg_try_advisory_lock(1)")
2259
            r = cursor.fetchone()
2260
            if r is None:
2261
                m = "Impossible"
2262
                raise AssertionError(m)
2263
            locked = r[0]
2264
            if locked:
2265
                break
2266

    
2267
            retries -= 1
2268
            if retries <= 0:
2269
                return False
2270
            sleep(retry_wait)
2271

    
2272
        return func(*args, **kwargs)
2273

    
2274
    finally:
2275
        if locked:
2276
            cursor.execute("SELECT pg_advisory_unlock(1)")
2277
            cursor.fetchall()
2278

    
2279

    
2280
class ProjectMembershipHistory(models.Model):
2281
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2282
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2283

    
2284
    person  =   models.BigIntegerField()
2285
    project =   models.BigIntegerField()
2286
    date    =   models.DateField(auto_now_add=True)
2287
    reason  =   models.IntegerField()
2288
    serial  =   models.BigIntegerField()
2289

    
2290
### SIGNALS ###
2291
################
2292

    
2293
def create_astakos_user(u):
2294
    try:
2295
        AstakosUser.objects.get(user_ptr=u.pk)
2296
    except AstakosUser.DoesNotExist:
2297
        extended_user = AstakosUser(user_ptr_id=u.pk)
2298
        extended_user.__dict__.update(u.__dict__)
2299
        extended_user.save()
2300
        if not extended_user.has_auth_provider('local'):
2301
            extended_user.add_auth_provider('local')
2302
    except BaseException, e:
2303
        logger.exception(e)
2304

    
2305

    
2306
def fix_superusers(sender, **kwargs):
2307
    # Associate superusers with AstakosUser
2308
    admins = User.objects.filter(is_superuser=True)
2309
    for u in admins:
2310
        create_astakos_user(u)
2311
post_syncdb.connect(fix_superusers)
2312

    
2313

    
2314
def user_post_save(sender, instance, created, **kwargs):
2315
    if not created:
2316
        return
2317
    create_astakos_user(instance)
2318
post_save.connect(user_post_save, sender=User)
2319

    
2320
def astakosuser_post_save(sender, instance, created, **kwargs):
2321
    pass
2322

    
2323
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2324

    
2325
def resource_post_save(sender, instance, created, **kwargs):
2326
    pass
2327

    
2328
post_save.connect(resource_post_save, sender=Resource)
2329

    
2330
def renew_token(sender, instance, **kwargs):
2331
    if not instance.auth_token:
2332
        instance.renew_token()
2333
pre_save.connect(renew_token, sender=AstakosUser)
2334
pre_save.connect(renew_token, sender=Service)
2335