Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (80.3 kB)

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

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

    
40
from time import asctime
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
49
from django.contrib.auth.models import User, UserManager, Group, Permission
50
from django.utils.translation import ugettext as _
51
from django.core.exceptions import ValidationError
52
from django.db.models.signals import (
53
    pre_save, post_save, post_syncdb, post_delete)
54
from django.contrib.contenttypes.models import ContentType
55

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

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

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

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

    
87
logger = logging.getLogger(__name__)
88

    
89
DEFAULT_CONTENT_TYPE = None
90
_content_type = None
91

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

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

    
104
RESOURCE_SEPARATOR = '.'
105

    
106
inf = float('inf')
107

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

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

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

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

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

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

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

    
147

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
217

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

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

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

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

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

    
260
    return _DEFAULT_QUOTA
261

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

    
268

    
269
class AstakosUserManager(UserManager):
270

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

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

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

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

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

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

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

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

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

    
321

    
322

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

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

    
339

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

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

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

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

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

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

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

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

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

    
375
    objects = AstakosUserManager()
376

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
504
        self.update_uuid()
505

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
613
        return True
614

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

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

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

    
626
        if not provider.is_available_for_remove():
627
            return False
628

    
629
        return True
630

    
631
    def can_change_password(self):
632
        return self.has_auth_provider('local', auth_backend='astakos')
633

    
634
    def can_change_email(self):
635
        non_astakos_local = self.get_auth_providers().filter(module='local')
636
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
637
        return non_astakos_local.count() == 0
638

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

    
646
    def has_auth_provider(self, provider, **kwargs):
647
        return bool(self.get_auth_providers().filter(module=provider,
648
                                               **kwargs).count())
649

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

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

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

    
677
        provider = self.add_auth_provider(pending.provider,
678
                               identifier=pending.third_party_identifier,
679
                                affiliation=pending.affiliation,
680
                                          provider_info=pending.info)
681

    
682
        if email_re.match(pending.email or '') and pending.email != self.email:
683
            self.additionalmail_set.get_or_create(email=pending.email)
684

    
685
        pending.delete()
686
        return provider
687

    
688
    def remove_auth_provider(self, provider, **kwargs):
689
        self.get_auth_providers().get(module=provider, **kwargs).delete()
690

    
691
    # user urls
692
    def get_resend_activation_url(self):
693
        return reverse('send_activation', kwargs={'user_id': self.pk})
694

    
695
    def get_provider_remove_url(self, module, **kwargs):
696
        return reverse('remove_auth_provider', kwargs={
697
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
698

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

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

    
711
    def get_auth_providers(self):
712
        return self.auth_providers
713

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

    
723
        return providers
724

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

    
732
    @property
733
    def auth_providers_display(self):
734
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
735

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

    
761
        return mark_safe(message + u' '+ msg_extra)
762

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

    
766
    def owns_project(self, project):
767
        return project.application.owner == self
768

    
769
    def is_project_member(self, project_or_application):
770
        return self.get_status_in_project(project_or_application) in \
771
                                        ProjectMembership.ASSOCIATED_STATES
772

    
773
    def is_project_accepted_member(self, project_or_application):
774
        return self.get_status_in_project(project_or_application) in \
775
                                            ProjectMembership.ACCEPTED_STATES
776

    
777
    def get_status_in_project(self, project_or_application):
778
        application = project_or_application
779
        if isinstance(project_or_application, Project):
780
            application = project_or_application.project
781
        return application.user_status(self)
782

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

    
791
class AstakosUserAuthProviderManager(models.Manager):
792

    
793
    def active(self, **filters):
794
        return self.filter(active=True, **filters)
795

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

    
804

    
805

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

    
824
    objects = AstakosUserAuthProviderManager()
825

    
826
    class Meta:
827
        unique_together = (('identifier', 'module', 'user'), )
828
        ordering = ('module', 'created')
829

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

    
839
        for key,value in self.info.iteritems():
840
            setattr(self, 'info_%s' % key, value)
841

    
842

    
843
    @property
844
    def settings(self):
845
        return auth_providers.get_provider(self.module)
846

    
847
    @property
848
    def details_display(self):
849
        try:
850
            params = self.user.__dict__
851
            params.update(self.__dict__)
852
            return self.settings.get_details_tpl_display % params
853
        except:
854
            return ''
855

    
856
    @property
857
    def title_display(self):
858
        title_tpl = self.settings.get_title_display
859
        try:
860
            if self.settings.get_user_title_display:
861
                title_tpl = self.settings.get_user_title_display
862
        except Exception, e:
863
            pass
864
        try:
865
          return title_tpl % self.__dict__
866
        except:
867
          return self.settings.get_title_display % self.__dict__
868

    
869
    def can_remove(self):
870
        return self.user.can_remove_auth_provider(self.module)
871

    
872
    def delete(self, *args, **kwargs):
873
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
874
        if self.module == 'local':
875
            self.user.set_unusable_password()
876
            self.user.save()
877
        return ret
878

    
879
    def __repr__(self):
880
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
881

    
882
    def __unicode__(self):
883
        if self.identifier:
884
            return "%s:%s" % (self.module, self.identifier)
885
        if self.auth_backend:
886
            return "%s:%s" % (self.module, self.auth_backend)
887
        return self.module
888

    
889
    def save(self, *args, **kwargs):
890
        self.info_data = json.dumps(self.info)
891
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
892

    
893

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

    
921
    update_or_create = _update_or_create
922

    
923

    
924
class AstakosUserQuota(models.Model):
925
    objects = ExtendedManager()
926
    capacity = intDecimalField()
927
    quantity = intDecimalField(default=0)
928
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
929
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
930
    resource = models.ForeignKey(Resource)
931
    user = models.ForeignKey(AstakosUser)
932

    
933
    class Meta:
934
        unique_together = ("resource", "user")
935

    
936
    def quota_values(self):
937
        return QuotaValues(
938
            quantity = self.quantity,
939
            capacity = self.capacity,
940
            import_limit = self.import_limit,
941
            export_limit = self.export_limit)
942

    
943

    
944
class ApprovalTerms(models.Model):
945
    """
946
    Model for approval terms
947
    """
948

    
949
    date = models.DateTimeField(
950
        _('Issue date'), db_index=True, auto_now_add=True)
951
    location = models.CharField(_('Terms location'), max_length=255)
952

    
953

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

    
967
    def __init__(self, *args, **kwargs):
968
        super(Invitation, self).__init__(*args, **kwargs)
969
        if not self.id:
970
            self.code = _generate_invitation_code()
971

    
972
    def consume(self):
973
        self.is_consumed = True
974
        self.consumed = datetime.now()
975
        self.save()
976

    
977
    def __unicode__(self):
978
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
979

    
980

    
981
class EmailChangeManager(models.Manager):
982

    
983
    @transaction.commit_on_success
984
    def change_email(self, activation_key):
985
        """
986
        Validate an activation key and change the corresponding
987
        ``User`` if valid.
988

989
        If the key is valid and has not expired, return the ``User``
990
        after activating.
991

992
        If the key is not valid or has expired, return ``None``.
993

994
        If the key is valid but the ``User`` is already active,
995
        return ``None``.
996

997
        After successful email change the activation record is deleted.
998

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

    
1027

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

    
1038
    objects = EmailChangeManager()
1039

    
1040
    def get_url(self):
1041
        return reverse('email_change_confirm',
1042
                      kwargs={'activation_key': self.activation_key})
1043

    
1044
    def activation_key_expired(self):
1045
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1046
        return self.requested_at + expiration_date < datetime.now()
1047

    
1048

    
1049
class AdditionalMail(models.Model):
1050
    """
1051
    Model for registring invitations
1052
    """
1053
    owner = models.ForeignKey(AstakosUser)
1054
    email = models.EmailField()
1055

    
1056

    
1057
def _generate_invitation_code():
1058
    while True:
1059
        code = randint(1, 2L ** 63 - 1)
1060
        try:
1061
            Invitation.objects.get(code=code)
1062
            # An invitation with this code already exists, try again
1063
        except Invitation.DoesNotExist:
1064
            return code
1065

    
1066

    
1067
def get_latest_terms():
1068
    try:
1069
        term = ApprovalTerms.objects.order_by('-id')[0]
1070
        return term
1071
    except IndexError:
1072
        pass
1073
    return None
1074

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

    
1093
    class Meta:
1094
        unique_together = ("provider", "third_party_identifier")
1095

    
1096
    def get_user_instance(self):
1097
        d = self.__dict__
1098
        d.pop('_state', None)
1099
        d.pop('id', None)
1100
        d.pop('token', None)
1101
        d.pop('created', None)
1102
        d.pop('info', None)
1103
        user = AstakosUser(**d)
1104

    
1105
        return user
1106

    
1107
    @property
1108
    def realname(self):
1109
        return '%s %s' %(self.first_name, self.last_name)
1110

    
1111
    @realname.setter
1112
    def realname(self, value):
1113
        parts = value.split(' ')
1114
        if len(parts) == 2:
1115
            self.first_name = parts[0]
1116
            self.last_name = parts[1]
1117
        else:
1118
            self.last_name = parts[0]
1119

    
1120
    def save(self, **kwargs):
1121
        if not self.id:
1122
            # set username
1123
            while not self.username:
1124
                username =  uuid.uuid4().hex[:30]
1125
                try:
1126
                    AstakosUser.objects.get(username = username)
1127
                except AstakosUser.DoesNotExist, e:
1128
                    self.username = username
1129
        super(PendingThirdPartyUser, self).save(**kwargs)
1130

    
1131
    def generate_token(self):
1132
        self.password = self.third_party_identifier
1133
        self.last_login = datetime.now()
1134
        self.token = default_token_generator.make_token(self)
1135

    
1136
class SessionCatalog(models.Model):
1137
    session_key = models.CharField(_('session key'), max_length=40)
1138
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1139

    
1140

    
1141
### PROJECTS ###
1142
################
1143

    
1144
def synced_model_metaclass(class_name, class_parents, class_attributes):
1145

    
1146
    new_attributes = {}
1147
    sync_attributes = {}
1148

    
1149
    for name, value in class_attributes.iteritems():
1150
        sync, underscore, rest = name.partition('_')
1151
        if sync == 'sync' and underscore == '_':
1152
            sync_attributes[rest] = value
1153
        else:
1154
            new_attributes[name] = value
1155

    
1156
    if 'prefix' not in sync_attributes:
1157
        m = ("you did not specify a 'sync_prefix' attribute "
1158
             "in class '%s'" % (class_name,))
1159
        raise ValueError(m)
1160

    
1161
    prefix = sync_attributes.pop('prefix')
1162
    class_name = sync_attributes.pop('classname', prefix + '_model')
1163

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

    
1172
        new_attributes[newname] = value
1173

    
1174
    newclass = type(class_name, class_parents, new_attributes)
1175
    return newclass
1176

    
1177

    
1178
def make_synced(prefix='sync', name='SyncedState'):
1179

    
1180
    the_name = name
1181
    the_prefix = prefix
1182

    
1183
    class SyncedState(models.Model):
1184

    
1185
        sync_classname      = the_name
1186
        sync_prefix         = the_prefix
1187
        __metaclass__       = synced_model_metaclass
1188

    
1189
        sync_new_state      = models.BigIntegerField(null=True)
1190
        sync_synced_state   = models.BigIntegerField(null=True)
1191
        STATUS_SYNCED       = 0
1192
        STATUS_PENDING      = 1
1193
        sync_status         = models.IntegerField(db_index=True)
1194

    
1195
        class Meta:
1196
            abstract = True
1197

    
1198
        class NotSynced(Exception):
1199
            pass
1200

    
1201
        def sync_init_state(self, state):
1202
            self.sync_synced_state = state
1203
            self.sync_new_state = state
1204
            self.sync_status = self.STATUS_SYNCED
1205

    
1206
        def sync_get_status(self):
1207
            return self.sync_status
1208

    
1209
        def sync_set_status(self):
1210
            if self.sync_new_state != self.sync_synced_state:
1211
                self.sync_status = self.STATUS_PENDING
1212
            else:
1213
                self.sync_status = self.STATUS_SYNCED
1214

    
1215
        def sync_set_synced(self):
1216
            self.sync_synced_state = self.sync_new_state
1217
            self.sync_status = self.STATUS_SYNCED
1218

    
1219
        def sync_get_synced_state(self):
1220
            return self.sync_synced_state
1221

    
1222
        def sync_set_new_state(self, new_state):
1223
            self.sync_new_state = new_state
1224
            self.sync_set_status()
1225

    
1226
        def sync_get_new_state(self):
1227
            return self.sync_new_state
1228

    
1229
        def sync_set_synced_state(self, synced_state):
1230
            self.sync_synced_state = synced_state
1231
            self.sync_set_status()
1232

    
1233
        def sync_get_pending_objects(self):
1234
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1235
            return self.objects.filter(**kw)
1236

    
1237
        def sync_get_synced_objects(self):
1238
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1239
            return self.objects.filter(**kw)
1240

    
1241
        def sync_verify_get_synced_state(self):
1242
            status = self.sync_get_status()
1243
            state = self.sync_get_synced_state()
1244
            verified = (status == self.STATUS_SYNCED)
1245
            return state, verified
1246

    
1247
        def sync_is_synced(self):
1248
            state, verified = self.sync_verify_get_synced_state()
1249
            return verified
1250

    
1251
    return SyncedState
1252

    
1253
SyncedState = make_synced(prefix='sync', name='SyncedState')
1254

    
1255

    
1256
class ProjectApplicationManager(ForUpdateManager):
1257

    
1258
    def user_visible_projects(self, *filters, **kw_filters):
1259
        model = self.model
1260
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1261

    
1262
    def user_visible_by_chain(self, flt):
1263
        model = self.model
1264
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1265
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1266
        by_chain = dict(pending.annotate(models.Max('id')))
1267
        by_chain.update(approved.annotate(models.Max('id')))
1268
        return self.filter(flt, id__in=by_chain.values())
1269

    
1270
    def user_accessible_projects(self, user):
1271
        """
1272
        Return projects accessed by specified user.
1273
        """
1274
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1275
                               Q(project__projectmembership__person=user)
1276

    
1277
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1278

    
1279
    def search_by_name(self, *search_strings):
1280
        q = Q()
1281
        for s in search_strings:
1282
            q = q | Q(name__icontains=s)
1283
        return self.filter(q)
1284

    
1285
    def latest_of_chain(self, chain_id):
1286
        try:
1287
            return self.filter(chain=chain_id).order_by('-id')[0]
1288
        except IndexError:
1289
            return None
1290

    
1291
USER_STATUS_DISPLAY = {
1292
      0: _('Join requested'),
1293
      1: _('Accepted member'),
1294
     10: _('Suspended'),
1295
    100: _('Terminated'),
1296
    200: _('Removed'),
1297
     -1: _('Not a member'),
1298
}
1299

    
1300

    
1301
class Chain(models.Model):
1302
    chain  =   models.AutoField(primary_key=True)
1303

    
1304
def new_chain():
1305
    c = Chain.objects.create()
1306
    chain = c.chain
1307
    c.delete()
1308
    return chain
1309

    
1310

    
1311
class ProjectApplication(models.Model):
1312
    applicant               =   models.ForeignKey(
1313
                                    AstakosUser,
1314
                                    related_name='projects_applied',
1315
                                    db_index=True)
1316

    
1317
    PENDING     =    0
1318
    APPROVED    =    1
1319
    REPLACED    =    2
1320
    DENIED      =    3
1321
    DISMISSED   =    4
1322
    CANCELLED   =    5
1323

    
1324
    state                   =   models.IntegerField(default=PENDING,
1325
                                                    db_index=True)
1326

    
1327
    owner                   =   models.ForeignKey(
1328
                                    AstakosUser,
1329
                                    related_name='projects_owned',
1330
                                    db_index=True)
1331

    
1332
    chain                   =   models.IntegerField()
1333
    precursor_application   =   models.ForeignKey('ProjectApplication',
1334
                                                  null=True,
1335
                                                  blank=True)
1336

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

    
1354
    objects                 =   ProjectApplicationManager()
1355

    
1356
    # Compiled queries
1357
    Q_PENDING  = Q(state=PENDING)
1358
    Q_APPROVED = Q(state=APPROVED)
1359
    Q_DENIED   = Q(state=DENIED)
1360

    
1361
    class Meta:
1362
        unique_together = ("chain", "id")
1363

    
1364
    def __unicode__(self):
1365
        return "%s applied by %s" % (self.name, self.applicant)
1366

    
1367
    # TODO: Move to a more suitable place
1368
    APPLICATION_STATE_DISPLAY = {
1369
        PENDING  : _('Pending review'),
1370
        APPROVED : _('Approved'),
1371
        REPLACED : _('Replaced'),
1372
        DENIED   : _('Denied'),
1373
        DISMISSED: _('Dismissed'),
1374
        CANCELLED: _('Cancelled')
1375
    }
1376

    
1377
    def get_project(self):
1378
        try:
1379
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1380
            return Project
1381
        except Project.DoesNotExist, e:
1382
            return None
1383

    
1384
    def state_display(self):
1385
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1386

    
1387
    def add_resource_policy(self, service, resource, uplimit):
1388
        """Raises ObjectDoesNotExist, IntegrityError"""
1389
        q = self.projectresourcegrant_set
1390
        resource = Resource.objects.get(service__name=service, name=resource)
1391
        q.create(resource=resource, member_capacity=uplimit)
1392

    
1393
    def user_status(self, user):
1394
        try:
1395
            project = self.get_project()
1396
            if not project:
1397
                return -1
1398
            membership = project.projectmembership_set
1399
            membership = membership.exclude(state=ProjectMembership.REMOVED)
1400
            membership = membership.get(person=user)
1401
            status = membership.state
1402
        except ProjectMembership.DoesNotExist:
1403
            return -1
1404

    
1405
        return status
1406

    
1407
    def user_status_display(self, user):
1408
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1409

    
1410
    def members_count(self):
1411
        return self.project.approved_memberships.count()
1412

    
1413
    @property
1414
    def grants(self):
1415
        return self.projectresourcegrant_set.values(
1416
            'member_capacity', 'resource__name', 'resource__service__name')
1417

    
1418
    @property
1419
    def resource_policies(self):
1420
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1421

    
1422
    @resource_policies.setter
1423
    def resource_policies(self, policies):
1424
        for p in policies:
1425
            service = p.get('service', None)
1426
            resource = p.get('resource', None)
1427
            uplimit = p.get('uplimit', 0)
1428
            self.add_resource_policy(service, resource, uplimit)
1429

    
1430
    def pending_modifications(self):
1431
        q = self.chained_applications()
1432
        q = q.filter(~Q(id=self.id) & Q(state=self.PENDING))
1433
        q = q.order_by('id')
1434
        return q
1435

    
1436
    def last_pending(self):
1437
        try:
1438
            return self.pending_modifications().order_by('-id')[0]
1439
        except IndexError:
1440
            return None
1441

    
1442
    def is_modification(self):
1443
        if self.state != self.PENDING:
1444
            return False
1445
        parents = self.chained_applications().filter(id__lt=self.id)
1446
        parents = parents.filter(state__in=[self.APPROVED])
1447
        return parents.count() > 0
1448

    
1449
    def chained_applications(self):
1450
        return ProjectApplication.objects.filter(chain=self.chain)
1451

    
1452
    def has_pending_modifications(self):
1453
        return bool(self.last_pending())
1454

    
1455
    def get_project(self):
1456
        try:
1457
            return Project.objects.get(id=self.chain)
1458
        except Project.DoesNotExist:
1459
            return None
1460

    
1461
    def _get_project_for_update(self):
1462
        try:
1463
            objects = Project.objects.select_for_update()
1464
            project = objects.get(id=self.chain)
1465
            return project
1466
        except Project.DoesNotExist:
1467
            return None
1468

    
1469
    def can_cancel(self):
1470
        return self.state == self.PENDING
1471

    
1472
    def cancel(self):
1473
        if not self.can_cancel():
1474
            m = _("cannot cancel: application '%s' in state '%s'") % (
1475
                    self.id, self.state)
1476
            raise AssertionError(m)
1477

    
1478
        self.state = self.CANCELLED
1479
        self.save()
1480

    
1481
    def can_dismiss(self):
1482
        return self.state == self.DENIED
1483

    
1484
    def dismiss(self):
1485
        if not self.can_dismiss():
1486
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1487
                    self.id, self.state)
1488
            raise AssertionError(m)
1489

    
1490
        self.state = self.DISMISSED
1491
        self.save()
1492

    
1493
    def can_deny(self):
1494
        return self.state == self.PENDING
1495

    
1496
    def deny(self):
1497
        if not self.can_deny():
1498
            m = _("cannot deny: application '%s' in state '%s'") % (
1499
                    self.id, self.state)
1500
            raise AssertionError(m)
1501

    
1502
        self.state = self.DENIED
1503
        self.response_date = datetime.now()
1504
        self.save()
1505

    
1506
    def can_approve(self):
1507
        return self.state == self.PENDING
1508

    
1509
    def approve(self, approval_user=None):
1510
        """
1511
        If approval_user then during owner membership acceptance
1512
        it is checked whether the request_user is eligible.
1513

1514
        Raises:
1515
            PermissionDenied
1516
        """
1517

    
1518
        if not transaction.is_managed():
1519
            raise AssertionError("NOPE")
1520

    
1521
        new_project_name = self.name
1522
        if not self.can_approve():
1523
            m = _("cannot approve: project '%s' in state '%s'") % (
1524
                    new_project_name, self.state)
1525
            raise AssertionError(m) # invalid argument
1526

    
1527
        now = datetime.now()
1528
        project = self._get_project_for_update()
1529

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

    
1541
        new_project = False
1542
        if project is None:
1543
            new_project = True
1544
            project = Project(id=self.chain)
1545

    
1546
        project.name = new_project_name
1547
        project.application = self
1548
        project.last_approval_date = now
1549
        if not new_project:
1550
            project.is_modified = True
1551

    
1552
        project.save()
1553

    
1554
        self.state = self.APPROVED
1555
        self.response_date = now
1556
        self.save()
1557

    
1558
    @property
1559
    def member_join_policy_display(self):
1560
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1561

    
1562
    @property
1563
    def member_leave_policy_display(self):
1564
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1565

    
1566
class ProjectResourceGrant(models.Model):
1567

    
1568
    resource                =   models.ForeignKey(Resource)
1569
    project_application     =   models.ForeignKey(ProjectApplication,
1570
                                                  null=True)
1571
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1572
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1573
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1574
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1575
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1576
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1577

    
1578
    objects = ExtendedManager()
1579

    
1580
    class Meta:
1581
        unique_together = ("resource", "project_application")
1582

    
1583
    def member_quota_values(self):
1584
        return QuotaValues(
1585
            quantity = 0,
1586
            capacity = self.member_capacity,
1587
            import_limit = self.member_import_limit,
1588
            export_limit = self.member_export_limit)
1589

    
1590
    def display_member_capacity(self):
1591
        if self.member_capacity:
1592
            if self.resource.unit:
1593
                return ProjectResourceGrant.display_filesize(
1594
                    self.member_capacity)
1595
            else:
1596
                if math.isinf(self.member_capacity):
1597
                    return 'Unlimited'
1598
                else:
1599
                    return self.member_capacity
1600
        else:
1601
            return 'Unlimited'
1602

    
1603
    def __str__(self):
1604
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1605
                                        self.display_member_capacity())
1606

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

    
1631

    
1632
class ProjectManager(ForUpdateManager):
1633

    
1634
    def terminated_projects(self):
1635
        q = self.model.Q_TERMINATED
1636
        return self.filter(q)
1637

    
1638
    def not_terminated_projects(self):
1639
        q = ~self.model.Q_TERMINATED
1640
        return self.filter(q)
1641

    
1642
    def terminating_projects(self):
1643
        q = self.model.Q_TERMINATED & Q(is_active=True)
1644
        return self.filter(q)
1645

    
1646
    def deactivated_projects(self):
1647
        q = self.model.Q_DEACTIVATED
1648
        return self.filter(q)
1649

    
1650
    def deactivating_projects(self):
1651
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1652
        return self.filter(q)
1653

    
1654
    def modified_projects(self):
1655
        return self.filter(is_modified=True)
1656

    
1657
    def reactivating_projects(self):
1658
        return self.filter(state=Project.APPROVED, is_active=False)
1659

    
1660
    def expired_projects(self):
1661
        q = (~Q(state=Project.TERMINATED) &
1662
              Q(application__end_date__lt=datetime.now()))
1663
        return self.filter(q)
1664

    
1665
    def search_by_name(self, *search_strings):
1666
        q = Q()
1667
        for s in search_strings:
1668
            q = q | Q(name__icontains=s)
1669
        return self.filter(q)
1670

    
1671

    
1672
class Project(models.Model):
1673

    
1674
    application                 =   models.OneToOneField(
1675
                                            ProjectApplication,
1676
                                            related_name='project')
1677
    last_approval_date          =   models.DateTimeField(null=True)
1678

    
1679
    members                     =   models.ManyToManyField(
1680
                                            AstakosUser,
1681
                                            through='ProjectMembership')
1682

    
1683
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1684
    deactivation_date           =   models.DateTimeField(null=True)
1685

    
1686
    creation_date               =   models.DateTimeField(auto_now_add=True)
1687
    name                        =   models.CharField(
1688
                                            max_length=80,
1689
                                            null=True,
1690
                                            db_index=True,
1691
                                            unique=True)
1692

    
1693
    APPROVED    = 1
1694
    SUSPENDED   = 10
1695
    TERMINATED  = 100
1696

    
1697
    is_modified                 =   models.BooleanField(default=False,
1698
                                                        db_index=True)
1699
    is_active                   =   models.BooleanField(default=True,
1700
                                                        db_index=True)
1701
    state                       =   models.IntegerField(default=APPROVED,
1702
                                                        db_index=True)
1703

    
1704
    objects     =   ProjectManager()
1705

    
1706
    # Compiled queries
1707
    Q_TERMINATED  = Q(state=TERMINATED)
1708
    Q_SUSPENDED   = Q(state=SUSPENDED)
1709
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1710

    
1711
    def __str__(self):
1712
        return _("<project %s '%s'>") % (self.id, self.application.name)
1713

    
1714
    __repr__ = __str__
1715

    
1716
    STATE_DISPLAY = {
1717
        APPROVED   : 'Active',
1718
        SUSPENDED  : 'Suspended',
1719
        TERMINATED : 'Terminated'
1720
        }
1721

    
1722
    def state_display(self):
1723
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1724

    
1725
    def admin_state_display(self):
1726
        s = self.state_display()
1727
        if self.sync_pending():
1728
            s += ' (sync pending)'
1729
        return s
1730

    
1731
    def sync_pending(self):
1732
        if self.state != self.APPROVED:
1733
            return self.is_active
1734
        return not self.is_active or self.is_modified
1735

    
1736
    def expiration_info(self):
1737
        return (str(self.id), self.name, self.state_display(),
1738
                str(self.application.end_date))
1739

    
1740
    def is_deactivated(self, reason=None):
1741
        if reason is not None:
1742
            return self.state == reason
1743

    
1744
        return self.state != self.APPROVED
1745

    
1746
    def is_deactivating(self, reason=None):
1747
        if not self.is_active:
1748
            return False
1749

    
1750
        return self.is_deactivated(reason)
1751

    
1752
    def is_deactivated_strict(self, reason=None):
1753
        if self.is_active:
1754
            return False
1755

    
1756
        return self.is_deactivated(reason)
1757

    
1758
    ### Deactivation calls
1759

    
1760
    def deactivate(self):
1761
        self.deactivation_date = datetime.now()
1762
        self.is_active = False
1763

    
1764
    def reactivate(self):
1765
        self.deactivation_date = None
1766
        self.is_active = True
1767

    
1768
    def terminate(self):
1769
        self.deactivation_reason = 'TERMINATED'
1770
        self.state = self.TERMINATED
1771
        self.name = None
1772
        self.save()
1773

    
1774
    def suspend(self):
1775
        self.deactivation_reason = 'SUSPENDED'
1776
        self.state = self.SUSPENDED
1777
        self.save()
1778

    
1779
    def resume(self):
1780
        self.deactivation_reason = None
1781
        self.state = self.APPROVED
1782
        self.save()
1783

    
1784
    ### Logical checks
1785

    
1786
    def is_inconsistent(self):
1787
        now = datetime.now()
1788
        dates = [self.creation_date,
1789
                 self.last_approval_date,
1790
                 self.deactivation_date]
1791
        return any([date > now for date in dates])
1792

    
1793
    def is_active_strict(self):
1794
        return self.is_active and self.state == self.APPROVED
1795

    
1796
    def is_approved(self):
1797
        return self.state == self.APPROVED
1798

    
1799
    @property
1800
    def is_alive(self):
1801
        return not self.is_terminated
1802

    
1803
    @property
1804
    def is_terminated(self):
1805
        return self.is_deactivated(self.TERMINATED)
1806

    
1807
    @property
1808
    def is_suspended(self):
1809
        return self.is_deactivated(self.SUSPENDED)
1810

    
1811
    def violates_resource_grants(self):
1812
        return False
1813

    
1814
    def violates_members_limit(self, adding=0):
1815
        application = self.application
1816
        limit = application.limit_on_members_number
1817
        if limit is None:
1818
            return False
1819
        return (len(self.approved_members) + adding > limit)
1820

    
1821

    
1822
    ### Other
1823

    
1824
    def count_pending_memberships(self):
1825
        memb_set = self.projectmembership_set
1826
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1827
        return memb_count
1828

    
1829
    def members_count(self):
1830
        return self.approved_memberships.count()
1831

    
1832
    @property
1833
    def approved_memberships(self):
1834
        query = ProjectMembership.Q_ACCEPTED_STATES
1835
        return self.projectmembership_set.filter(query)
1836

    
1837
    @property
1838
    def approved_members(self):
1839
        return [m.person for m in self.approved_memberships]
1840

    
1841
    def add_member(self, user):
1842
        """
1843
        Raises:
1844
            django.exceptions.PermissionDenied
1845
            astakos.im.models.AstakosUser.DoesNotExist
1846
        """
1847
        if isinstance(user, int):
1848
            user = AstakosUser.objects.get(user=user)
1849

    
1850
        m, created = ProjectMembership.objects.get_or_create(
1851
            person=user, project=self
1852
        )
1853
        m.accept()
1854

    
1855
    def remove_member(self, user):
1856
        """
1857
        Raises:
1858
            django.exceptions.PermissionDenied
1859
            astakos.im.models.AstakosUser.DoesNotExist
1860
            astakos.im.models.ProjectMembership.DoesNotExist
1861
        """
1862
        if isinstance(user, int):
1863
            user = AstakosUser.objects.get(user=user)
1864

    
1865
        m = ProjectMembership.objects.get(person=user, project=self)
1866
        m.remove()
1867

    
1868

    
1869
class PendingMembershipError(Exception):
1870
    pass
1871

    
1872

    
1873
class ProjectMembershipManager(ForUpdateManager):
1874

    
1875
    def any_accepted(self):
1876
        q = (Q(state=ProjectMembership.ACCEPTED) |
1877
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
1878
        return self.filter(q)
1879

    
1880
    def requested(self):
1881
        return self.filter(state=ProjectMembership.REQUESTED)
1882

    
1883
    def suspended(self):
1884
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1885

    
1886
class ProjectMembership(models.Model):
1887

    
1888
    person              =   models.ForeignKey(AstakosUser)
1889
    request_date        =   models.DateField(auto_now_add=True)
1890
    project             =   models.ForeignKey(Project)
1891

    
1892
    REQUESTED           =   0
1893
    ACCEPTED            =   1
1894
    # User deactivation
1895
    USER_SUSPENDED      =   10
1896
    # Project deactivation
1897
    PROJECT_DEACTIVATED =   100
1898

    
1899
    REMOVED             =   200
1900

    
1901
    ASSOCIATED_STATES   =   set([REQUESTED,
1902
                                 ACCEPTED,
1903
                                 USER_SUSPENDED,
1904
                                 PROJECT_DEACTIVATED])
1905

    
1906
    ACCEPTED_STATES     =   set([ACCEPTED,
1907
                                 USER_SUSPENDED,
1908
                                 PROJECT_DEACTIVATED])
1909

    
1910
    state               =   models.IntegerField(default=REQUESTED,
1911
                                                db_index=True)
1912
    is_pending          =   models.BooleanField(default=False, db_index=True)
1913
    is_active           =   models.BooleanField(default=False, db_index=True)
1914
    application         =   models.ForeignKey(
1915
                                ProjectApplication,
1916
                                null=True,
1917
                                related_name='memberships')
1918
    pending_application =   models.ForeignKey(
1919
                                ProjectApplication,
1920
                                null=True,
1921
                                related_name='pending_memberships')
1922
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1923

    
1924
    acceptance_date     =   models.DateField(null=True, db_index=True)
1925
    leave_request_date  =   models.DateField(null=True)
1926

    
1927
    objects     =   ProjectMembershipManager()
1928

    
1929
    # Compiled queries
1930
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1931

    
1932
    MEMBERSHIP_STATE_DISPLAY = {
1933
        REQUESTED           : 'Requested',
1934
        ACCEPTED            : 'Accepted',
1935
        USER_SUSPENDED      : 'Suspended',
1936
        PROJECT_DEACTIVATED : 'Accepted', # sic
1937
        REMOVED             : 'Pending removal'
1938
        }
1939

    
1940
    def state_display(self):
1941
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1942

    
1943
    def get_combined_state(self):
1944
        return self.state, self.is_active, self.is_pending
1945

    
1946
    class Meta:
1947
        unique_together = ("person", "project")
1948
        #index_together = [["project", "state"]]
1949

    
1950
    def __str__(self):
1951
        return _("<'%s' membership in '%s'>") % (
1952
                self.person.username, self.project)
1953

    
1954
    __repr__ = __str__
1955

    
1956
    def __init__(self, *args, **kwargs):
1957
        self.state = self.REQUESTED
1958
        super(ProjectMembership, self).__init__(*args, **kwargs)
1959

    
1960
    def _set_history_item(self, reason, date=None):
1961
        if isinstance(reason, basestring):
1962
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1963

    
1964
        history_item = ProjectMembershipHistory(
1965
                            serial=self.id,
1966
                            person=self.person_id,
1967
                            project=self.project_id,
1968
                            date=date or datetime.now(),
1969
                            reason=reason)
1970
        history_item.save()
1971
        serial = history_item.id
1972

    
1973
    def can_accept(self):
1974
        return self.state == self.REQUESTED
1975

    
1976
    def accept(self):
1977
        if self.is_pending:
1978
            m = _("%s: attempt to accept while is pending") % (self,)
1979
            raise AssertionError(m)
1980

    
1981
        if not self.can_accept():
1982
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
1983
            raise AssertionError(m)
1984

    
1985
        now = datetime.now()
1986
        self.acceptance_date = now
1987
        self._set_history_item(reason='ACCEPT', date=now)
1988
        if self.project.is_approved():
1989
            self.state = self.ACCEPTED
1990
            self.is_pending = True
1991
        else:
1992
            self.state = self.PROJECT_DEACTIVATED
1993

    
1994
        self.save()
1995

    
1996
    def can_leave(self):
1997
        return self.can_remove()
1998

    
1999
    def can_remove(self):
2000
        return self.state in self.ACCEPTED_STATES
2001

    
2002
    def remove(self):
2003
        if self.is_pending:
2004
            m = _("%s: attempt to remove while is pending") % (self,)
2005
            raise AssertionError(m)
2006

    
2007
        if not self.can_remove():
2008
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2009
            raise AssertionError(m)
2010

    
2011
        self._set_history_item(reason='REMOVE')
2012
        self.state = self.REMOVED
2013
        self.is_pending = True
2014
        self.save()
2015

    
2016
    def can_reject(self):
2017
        return self.state == self.REQUESTED
2018

    
2019
    def reject(self):
2020
        if self.is_pending:
2021
            m = _("%s: attempt to reject while is pending") % (self,)
2022
            raise AssertionError(m)
2023

    
2024
        if not self.can_reject():
2025
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2026
            raise AssertionError(m)
2027

    
2028
        # rejected requests don't need sync,
2029
        # because they were never effected
2030
        self._set_history_item(reason='REJECT')
2031
        self.delete()
2032

    
2033
    def can_cancel(self):
2034
        return self.state == self.REQUESTED
2035

    
2036
    def cancel(self):
2037
        if self.is_pending:
2038
            m = _("%s: attempt to cancel while is pending") % (self,)
2039
            raise AssertionError(m)
2040

    
2041
        if not self.can_cancel():
2042
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2043
            raise AssertionError(m)
2044

    
2045
        # rejected requests don't need sync,
2046
        # because they were never effected
2047
        self._set_history_item(reason='CANCEL')
2048
        self.delete()
2049

    
2050
    def get_diff_quotas(self, sub_list=None, add_list=None):
2051
        if sub_list is None:
2052
            sub_list = []
2053

    
2054
        if add_list is None:
2055
            add_list = []
2056

    
2057
        sub_append = sub_list.append
2058
        add_append = add_list.append
2059
        holder = self.person.uuid
2060

    
2061
        synced_application = self.application
2062
        if synced_application is not None:
2063
            cur_grants = synced_application.projectresourcegrant_set.all()
2064
            for grant in cur_grants:
2065
                sub_append(QuotaLimits(
2066
                               holder       = holder,
2067
                               resource     = str(grant.resource),
2068
                               capacity     = grant.member_capacity,
2069
                               import_limit = grant.member_import_limit,
2070
                               export_limit = grant.member_export_limit))
2071

    
2072
        pending_application = self.pending_application
2073
        if pending_application is not None:
2074
            new_grants = pending_application.projectresourcegrant_set.all()
2075
            for new_grant in new_grants:
2076
                add_append(QuotaLimits(
2077
                               holder       = holder,
2078
                               resource     = str(new_grant.resource),
2079
                               capacity     = new_grant.member_capacity,
2080
                               import_limit = new_grant.member_import_limit,
2081
                               export_limit = new_grant.member_export_limit))
2082

    
2083
        return (sub_list, add_list)
2084

    
2085
    def set_sync(self):
2086
        if not self.is_pending:
2087
            m = _("%s: attempt to sync a non pending membership") % (self,)
2088
            raise AssertionError(m)
2089

    
2090
        state = self.state
2091
        if state == self.ACCEPTED:
2092
            pending_application = self.pending_application
2093
            if pending_application is None:
2094
                m = _("%s: attempt to sync an empty pending application") % (
2095
                    self,)
2096
                raise AssertionError(m)
2097

    
2098
            self.application = pending_application
2099
            self.is_active = True
2100

    
2101
            self.pending_application = None
2102
            self.pending_serial = None
2103

    
2104
            # project.application may have changed in the meantime,
2105
            # in which case we stay PENDING;
2106
            # we are safe to check due to select_for_update
2107
            if self.application == self.project.application:
2108
                self.is_pending = False
2109
            self.save()
2110

    
2111
        elif state == self.PROJECT_DEACTIVATED:
2112
            if self.pending_application:
2113
                m = _("%s: attempt to sync in state '%s' "
2114
                      "with a pending application") % (self, state)
2115
                raise AssertionError(m)
2116

    
2117
            self.application = None
2118
            self.is_active = False
2119
            self.pending_serial = None
2120
            self.is_pending = False
2121
            self.save()
2122

    
2123
        elif state == self.REMOVED:
2124
            self.delete()
2125

    
2126
        else:
2127
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2128
            raise AssertionError(m)
2129

    
2130
    def reset_sync(self):
2131
        if not self.is_pending:
2132
            m = _("%s: attempt to reset a non pending membership") % (self,)
2133
            raise AssertionError(m)
2134

    
2135
        state = self.state
2136
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
2137
            self.pending_application = None
2138
            self.pending_serial = None
2139
            self.save()
2140
        else:
2141
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2142
            raise AssertionError(m)
2143

    
2144
class Serial(models.Model):
2145
    serial  =   models.AutoField(primary_key=True)
2146

    
2147
def new_serial():
2148
    s = Serial.objects.create()
2149
    serial = s.serial
2150
    s.delete()
2151
    return serial
2152

    
2153
def sync_finish_serials(serials_to_ack=None):
2154
    if serials_to_ack is None:
2155
        serials_to_ack = qh_query_serials([])
2156

    
2157
    serials_to_ack = set(serials_to_ack)
2158
    sfu = ProjectMembership.objects.select_for_update()
2159
    memberships = list(sfu.filter(pending_serial__isnull=False))
2160

    
2161
    if memberships:
2162
        for membership in memberships:
2163
            serial = membership.pending_serial
2164
            if serial in serials_to_ack:
2165
                membership.set_sync()
2166
            else:
2167
                membership.reset_sync()
2168

    
2169
        transaction.commit()
2170

    
2171
    qh_ack_serials(list(serials_to_ack))
2172
    return len(memberships)
2173

    
2174
def pre_sync_projects(sync=True):
2175
    ACCEPTED = ProjectMembership.ACCEPTED
2176
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2177
    psfu = Project.objects.select_for_update()
2178

    
2179
    modified = list(psfu.modified_projects())
2180
    if sync:
2181
        for project in modified:
2182
            objects = project.projectmembership_set.select_for_update()
2183

    
2184
            memberships = objects.filter(state=ACCEPTED)
2185
            for membership in memberships:
2186
                membership.is_pending = True
2187
                membership.save()
2188

    
2189
    reactivating = list(psfu.reactivating_projects())
2190
    if sync:
2191
        for project in reactivating:
2192
            objects = project.projectmembership_set.select_for_update()
2193

    
2194
            memberships = objects.filter(state=PROJECT_DEACTIVATED)
2195
            for membership in memberships:
2196
                membership.is_pending = True
2197
                membership.state = ACCEPTED
2198
                membership.save()
2199

    
2200
    deactivating = list(psfu.deactivating_projects())
2201
    if sync:
2202
        for project in deactivating:
2203
            objects = project.projectmembership_set.select_for_update()
2204

    
2205
            # Note: we keep a user-level deactivation
2206
            # (e.g. USER_SUSPENDED) intact
2207
            memberships = objects.filter(state=ACCEPTED)
2208
            for membership in memberships:
2209
                membership.is_pending = True
2210
                membership.state = PROJECT_DEACTIVATED
2211
                membership.save()
2212

    
2213
    return (modified, reactivating, deactivating)
2214

    
2215
def do_sync_projects():
2216

    
2217
    ACCEPTED = ProjectMembership.ACCEPTED
2218
    objects = ProjectMembership.objects.select_for_update()
2219

    
2220
    sub_quota, add_quota = [], []
2221

    
2222
    serial = new_serial()
2223

    
2224
    pending = objects.filter(is_pending=True)
2225
    for membership in pending:
2226

    
2227
        if membership.pending_application:
2228
            m = "%s: impossible: pending_application is not None (%s)" % (
2229
                membership, membership.pending_application)
2230
            raise AssertionError(m)
2231
        if membership.pending_serial:
2232
            m = "%s: impossible: pending_serial is not None (%s)" % (
2233
                membership, membership.pending_serial)
2234
            raise AssertionError(m)
2235

    
2236
        if membership.state == ACCEPTED:
2237
            membership.pending_application = membership.project.application
2238

    
2239
        membership.pending_serial = serial
2240
        membership.get_diff_quotas(sub_quota, add_quota)
2241
        membership.save()
2242

    
2243
    transaction.commit()
2244
    # ProjectApplication.approve() unblocks here
2245
    # and can set PENDING an already PENDING membership
2246
    # which has been scheduled to sync with the old project.application
2247
    # Need to check in ProjectMembership.set_sync()
2248

    
2249
    r = qh_add_quota(serial, sub_quota, add_quota)
2250
    if r:
2251
        m = "cannot sync serial: %d" % serial
2252
        raise RuntimeError(m)
2253

    
2254
    return serial
2255

    
2256
def post_sync_projects():
2257
    ACCEPTED = ProjectMembership.ACCEPTED
2258
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2259
    psfu = Project.objects.select_for_update()
2260

    
2261
    modified = psfu.modified_projects()
2262
    for project in modified:
2263
        objects = project.projectmembership_set.select_for_update()
2264

    
2265
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2266
        if not memberships:
2267
            project.is_modified = False
2268
            project.save()
2269

    
2270
    reactivating = psfu.reactivating_projects()
2271
    for project in reactivating:
2272
        objects = project.projectmembership_set.select_for_update()
2273
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2274
                                          Q(is_pending=True)))
2275
        if not memberships:
2276
            project.reactivate()
2277
            project.save()
2278

    
2279
    deactivating = psfu.deactivating_projects()
2280
    for project in deactivating:
2281
        objects = project.projectmembership_set.select_for_update()
2282

    
2283
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2284
                                          Q(is_pending=True)))
2285
        if not memberships:
2286
            project.deactivate()
2287
            project.save()
2288

    
2289
    transaction.commit()
2290

    
2291
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2292
    @with_lock(retries, retry_wait)
2293
    def _sync_projects(sync):
2294
        sync_finish_serials()
2295
        # Informative only -- no select_for_update()
2296
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2297

    
2298
        projects_log = pre_sync_projects(sync)
2299
        if sync:
2300
            serial = do_sync_projects()
2301
            sync_finish_serials([serial])
2302
            post_sync_projects()
2303

    
2304
        return (pending, projects_log)
2305
    return _sync_projects(sync)
2306

    
2307
def all_users_quotas(users):
2308
    quotas = {}
2309
    for user in users:
2310
        quotas[user.uuid] = user.all_quotas()
2311
    return quotas
2312

    
2313
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2314
    @with_lock(retries, retry_wait)
2315
    def _sync_users(users, sync):
2316
        sync_finish_serials()
2317

    
2318
        existing, nonexisting = qh_check_users(users)
2319
        resources = get_resource_names()
2320
        registered_quotas = qh_get_quota_limits(existing, resources)
2321
        astakos_quotas = all_users_quotas(users)
2322

    
2323
        if sync:
2324
            r = register_users(nonexisting)
2325
            r = send_quotas(astakos_quotas)
2326

    
2327
        return (existing, nonexisting, registered_quotas, astakos_quotas)
2328
    return _sync_users(users, sync)
2329

    
2330
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2331
    users = AstakosUser.objects.filter(is_active=True)
2332
    return sync_users(users, sync, retries, retry_wait)
2333

    
2334
class ProjectMembershipHistory(models.Model):
2335
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2336
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2337

    
2338
    person  =   models.BigIntegerField()
2339
    project =   models.BigIntegerField()
2340
    date    =   models.DateField(auto_now_add=True)
2341
    reason  =   models.IntegerField()
2342
    serial  =   models.BigIntegerField()
2343

    
2344
### SIGNALS ###
2345
################
2346

    
2347
def create_astakos_user(u):
2348
    try:
2349
        AstakosUser.objects.get(user_ptr=u.pk)
2350
    except AstakosUser.DoesNotExist:
2351
        extended_user = AstakosUser(user_ptr_id=u.pk)
2352
        extended_user.__dict__.update(u.__dict__)
2353
        extended_user.save()
2354
        if not extended_user.has_auth_provider('local'):
2355
            extended_user.add_auth_provider('local')
2356
    except BaseException, e:
2357
        logger.exception(e)
2358

    
2359

    
2360
def fix_superusers(sender, **kwargs):
2361
    # Associate superusers with AstakosUser
2362
    admins = User.objects.filter(is_superuser=True)
2363
    for u in admins:
2364
        create_astakos_user(u)
2365
post_syncdb.connect(fix_superusers)
2366

    
2367

    
2368
def user_post_save(sender, instance, created, **kwargs):
2369
    if not created:
2370
        return
2371
    create_astakos_user(instance)
2372
post_save.connect(user_post_save, sender=User)
2373

    
2374
def astakosuser_post_save(sender, instance, created, **kwargs):
2375
    pass
2376

    
2377
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2378

    
2379
def resource_post_save(sender, instance, created, **kwargs):
2380
    pass
2381

    
2382
post_save.connect(resource_post_save, sender=Resource)
2383

    
2384
def renew_token(sender, instance, **kwargs):
2385
    if not instance.auth_token:
2386
        instance.renew_token()
2387
pre_save.connect(renew_token, sender=AstakosUser)
2388
pre_save.connect(renew_token, sender=Service)
2389