Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (80.8 kB)

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

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

    
40
from time import asctime
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
    @property
556
    def signed_terms(self):
557
        term = get_latest_terms()
558
        if not term:
559
            return True
560
        if not self.has_signed_terms:
561
            return False
562
        if not self.date_signed_terms:
563
            return False
564
        if self.date_signed_terms < term.date:
565
            self.has_signed_terms = False
566
            self.date_signed_terms = None
567
            self.save()
568
            return False
569
        return True
570

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

    
579
    def can_login_with_auth_provider(self, provider):
580
        if not self.has_auth_provider(provider):
581
            return False
582
        else:
583
            return auth_providers.get_provider(provider).is_available_for_login()
584

    
585
    def can_add_auth_provider(self, provider, include_unverified=False, **kwargs):
586
        provider_settings = auth_providers.get_provider(provider)
587

    
588
        if not provider_settings.is_available_for_add():
589
            return False
590

    
591
        if self.has_auth_provider(provider) and \
592
           provider_settings.one_per_user:
593
            return False
594

    
595
        if 'provider_info' in kwargs:
596
            kwargs.pop('provider_info')
597

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

    
610
        return True
611

    
612
    def can_remove_auth_provider(self, module):
613
        provider = auth_providers.get_provider(module)
614
        existing = self.get_active_auth_providers()
615
        existing_for_provider = self.get_active_auth_providers(module=module)
616

    
617
        if len(existing) <= 1:
618
            return False
619

    
620
        if len(existing_for_provider) == 1 and provider.is_required():
621
            return False
622

    
623
        return provider.is_available_for_remove()
624

    
625
    def can_change_password(self):
626
        return self.has_auth_provider('local', auth_backend='astakos')
627

    
628
    def can_change_email(self):
629
        non_astakos_local = self.get_auth_providers().filter(module='local')
630
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
631
        return non_astakos_local.count() == 0
632

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

    
640
    def has_auth_provider(self, provider, **kwargs):
641
        return bool(self.get_auth_providers().filter(module=provider,
642
                                               **kwargs).count())
643

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

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

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

    
671
        provider = self.add_auth_provider(pending.provider,
672
                               identifier=pending.third_party_identifier,
673
                                affiliation=pending.affiliation,
674
                                          provider_info=pending.info)
675

    
676
        if email_re.match(pending.email or '') and pending.email != self.email:
677
            self.additionalmail_set.get_or_create(email=pending.email)
678

    
679
        pending.delete()
680
        return provider
681

    
682
    def remove_auth_provider(self, provider, **kwargs):
683
        self.get_auth_providers().get(module=provider, **kwargs).delete()
684

    
685
    # user urls
686
    def get_resend_activation_url(self):
687
        return reverse('send_activation', kwargs={'user_id': self.pk})
688

    
689
    def get_provider_remove_url(self, module, **kwargs):
690
        return reverse('remove_auth_provider', kwargs={
691
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
692

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

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

    
705
    def get_auth_providers(self):
706
        return self.auth_providers
707

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

    
717
        modules = astakos_settings.IM_MODULES
718
        def key(p):
719
            if not p.module in modules:
720
                return 100
721
            return modules.index(p.module)
722
        providers = sorted(providers, key=key)
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

    
731
        modules = astakos_settings.IM_MODULES
732
        def key(p):
733
            if not p.module in modules:
734
                return 100
735
            return modules.index(p.module)
736
        providers = sorted(providers, key=key)
737
        return providers
738

    
739
    @property
740
    def auth_providers_display(self):
741
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
742

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

    
768
        return mark_safe(message + u' '+ msg_extra)
769

    
770
    def owns_application(self, application):
771
        return application.owner == self
772

    
773
    def owns_project(self, project):
774
        return project.application.owner == self
775

    
776
    def is_associated(self, project):
777
        try:
778
            m = ProjectMembership.objects.get(person=self, project=project)
779
            return m.state in ProjectMembership.ASSOCIATED_STATES
780
        except ProjectMembership.DoesNotExist:
781
            return False
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
    def membership_display(self, project):
792
        m = self.get_membership(project)
793
        if m is None:
794
            return _('Not a member')
795
        else:
796
            return m.user_friendly_state_display()
797

    
798

    
799
class AstakosUserAuthProviderManager(models.Manager):
800

    
801
    def active(self, **filters):
802
        return self.filter(active=True, **filters)
803

    
804
    def remove_unverified_providers(self, provider, **filters):
805
        try:
806
            existing = self.filter(module=provider, user__email_verified=False, **filters)
807
            for p in existing:
808
                p.user.delete()
809
        except:
810
            pass
811

    
812

    
813

    
814
class AstakosUserAuthProvider(models.Model):
815
    """
816
    Available user authentication methods.
817
    """
818
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
819
                                   null=True, default=None)
820
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
821
    module = models.CharField(_('Provider'), max_length=255, blank=False,
822
                                default='local')
823
    identifier = models.CharField(_('Third-party identifier'),
824
                                              max_length=255, null=True,
825
                                              blank=True)
826
    active = models.BooleanField(default=True)
827
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
828
                                   default='astakos')
829
    info_data = models.TextField(default="", null=True, blank=True)
830
    created = models.DateTimeField('Creation date', auto_now_add=True)
831

    
832
    objects = AstakosUserAuthProviderManager()
833

    
834
    class Meta:
835
        unique_together = (('identifier', 'module', 'user'), )
836
        ordering = ('module', 'created')
837

    
838
    def __init__(self, *args, **kwargs):
839
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
840
        try:
841
            self.info = json.loads(self.info_data)
842
            if not self.info:
843
                self.info = {}
844
        except Exception, e:
845
            self.info = {}
846

    
847
        for key,value in self.info.iteritems():
848
            setattr(self, 'info_%s' % key, value)
849

    
850

    
851
    @property
852
    def settings(self):
853
        return auth_providers.get_provider(self.module)
854

    
855
    @property
856
    def details_display(self):
857
        try:
858
            params = self.user.__dict__
859
            params.update(self.__dict__)
860
            return self.settings.get_details_tpl_display % params
861
        except:
862
            return ''
863

    
864
    @property
865
    def title_display(self):
866
        title_tpl = self.settings.get_title_display
867
        try:
868
            if self.settings.get_user_title_display:
869
                title_tpl = self.settings.get_user_title_display
870
        except Exception, e:
871
            pass
872
        try:
873
          return title_tpl % self.__dict__
874
        except:
875
          return self.settings.get_title_display % self.__dict__
876

    
877
    def can_remove(self):
878
        return self.user.can_remove_auth_provider(self.module)
879

    
880
    def delete(self, *args, **kwargs):
881
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
882
        if self.module == 'local':
883
            self.user.set_unusable_password()
884
            self.user.save()
885
        return ret
886

    
887
    def __repr__(self):
888
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
889

    
890
    def __unicode__(self):
891
        if self.identifier:
892
            return "%s:%s" % (self.module, self.identifier)
893
        if self.auth_backend:
894
            return "%s:%s" % (self.module, self.auth_backend)
895
        return self.module
896

    
897
    def save(self, *args, **kwargs):
898
        self.info_data = json.dumps(self.info)
899
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
900

    
901

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

    
929
    update_or_create = _update_or_create
930

    
931

    
932
class AstakosUserQuota(models.Model):
933
    objects = ExtendedManager()
934
    capacity = intDecimalField()
935
    quantity = intDecimalField(default=0)
936
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
937
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
938
    resource = models.ForeignKey(Resource)
939
    user = models.ForeignKey(AstakosUser)
940

    
941
    class Meta:
942
        unique_together = ("resource", "user")
943

    
944
    def quota_values(self):
945
        return QuotaValues(
946
            quantity = self.quantity,
947
            capacity = self.capacity,
948
            import_limit = self.import_limit,
949
            export_limit = self.export_limit)
950

    
951

    
952
class ApprovalTerms(models.Model):
953
    """
954
    Model for approval terms
955
    """
956

    
957
    date = models.DateTimeField(
958
        _('Issue date'), db_index=True, auto_now_add=True)
959
    location = models.CharField(_('Terms location'), max_length=255)
960

    
961

    
962
class Invitation(models.Model):
963
    """
964
    Model for registring invitations
965
    """
966
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
967
                                null=True)
968
    realname = models.CharField(_('Real name'), max_length=255)
969
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
970
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
971
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
972
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
973
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
974

    
975
    def __init__(self, *args, **kwargs):
976
        super(Invitation, self).__init__(*args, **kwargs)
977
        if not self.id:
978
            self.code = _generate_invitation_code()
979

    
980
    def consume(self):
981
        self.is_consumed = True
982
        self.consumed = datetime.now()
983
        self.save()
984

    
985
    def __unicode__(self):
986
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
987

    
988

    
989
class EmailChangeManager(models.Manager):
990

    
991
    @transaction.commit_on_success
992
    def change_email(self, activation_key):
993
        """
994
        Validate an activation key and change the corresponding
995
        ``User`` if valid.
996

997
        If the key is valid and has not expired, return the ``User``
998
        after activating.
999

1000
        If the key is not valid or has expired, return ``None``.
1001

1002
        If the key is valid but the ``User`` is already active,
1003
        return ``None``.
1004

1005
        After successful email change the activation record is deleted.
1006

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

    
1035

    
1036
class EmailChange(models.Model):
1037
    new_email_address = models.EmailField(
1038
        _(u'new e-mail address'),
1039
        help_text=_('Provide a new email address. Until you verify the new '
1040
                    'address by following the activation link that will be '
1041
                    'sent to it, your old email address will remain active.'))
1042
    user = models.ForeignKey(
1043
        AstakosUser, unique=True, related_name='emailchanges')
1044
    requested_at = models.DateTimeField(auto_now_add=True)
1045
    activation_key = models.CharField(
1046
        max_length=40, unique=True, db_index=True)
1047

    
1048
    objects = EmailChangeManager()
1049

    
1050
    def get_url(self):
1051
        return reverse('email_change_confirm',
1052
                      kwargs={'activation_key': self.activation_key})
1053

    
1054
    def activation_key_expired(self):
1055
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1056
        return self.requested_at + expiration_date < datetime.now()
1057

    
1058

    
1059
class AdditionalMail(models.Model):
1060
    """
1061
    Model for registring invitations
1062
    """
1063
    owner = models.ForeignKey(AstakosUser)
1064
    email = models.EmailField()
1065

    
1066

    
1067
def _generate_invitation_code():
1068
    while True:
1069
        code = randint(1, 2L ** 63 - 1)
1070
        try:
1071
            Invitation.objects.get(code=code)
1072
            # An invitation with this code already exists, try again
1073
        except Invitation.DoesNotExist:
1074
            return code
1075

    
1076

    
1077
def get_latest_terms():
1078
    try:
1079
        term = ApprovalTerms.objects.order_by('-id')[0]
1080
        return term
1081
    except IndexError:
1082
        pass
1083
    return None
1084

    
1085
class PendingThirdPartyUser(models.Model):
1086
    """
1087
    Model for registring successful third party user authentications
1088
    """
1089
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1090
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1091
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1092
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1093
                                  null=True)
1094
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1095
                                 null=True)
1096
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1097
                                   null=True)
1098
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1099
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1100
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1101
    info = models.TextField(default="", null=True, blank=True)
1102

    
1103
    class Meta:
1104
        unique_together = ("provider", "third_party_identifier")
1105

    
1106
    def get_user_instance(self):
1107
        d = self.__dict__
1108
        d.pop('_state', None)
1109
        d.pop('id', None)
1110
        d.pop('token', None)
1111
        d.pop('created', None)
1112
        d.pop('info', None)
1113
        user = AstakosUser(**d)
1114

    
1115
        return user
1116

    
1117
    @property
1118
    def realname(self):
1119
        return '%s %s' %(self.first_name, self.last_name)
1120

    
1121
    @realname.setter
1122
    def realname(self, value):
1123
        parts = value.split(' ')
1124
        if len(parts) == 2:
1125
            self.first_name = parts[0]
1126
            self.last_name = parts[1]
1127
        else:
1128
            self.last_name = parts[0]
1129

    
1130
    def save(self, **kwargs):
1131
        if not self.id:
1132
            # set username
1133
            while not self.username:
1134
                username =  uuid.uuid4().hex[:30]
1135
                try:
1136
                    AstakosUser.objects.get(username = username)
1137
                except AstakosUser.DoesNotExist, e:
1138
                    self.username = username
1139
        super(PendingThirdPartyUser, self).save(**kwargs)
1140

    
1141
    def generate_token(self):
1142
        self.password = self.third_party_identifier
1143
        self.last_login = datetime.now()
1144
        self.token = default_token_generator.make_token(self)
1145

    
1146
class SessionCatalog(models.Model):
1147
    session_key = models.CharField(_('session key'), max_length=40)
1148
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1149

    
1150

    
1151
### PROJECTS ###
1152
################
1153

    
1154
def synced_model_metaclass(class_name, class_parents, class_attributes):
1155

    
1156
    new_attributes = {}
1157
    sync_attributes = {}
1158

    
1159
    for name, value in class_attributes.iteritems():
1160
        sync, underscore, rest = name.partition('_')
1161
        if sync == 'sync' and underscore == '_':
1162
            sync_attributes[rest] = value
1163
        else:
1164
            new_attributes[name] = value
1165

    
1166
    if 'prefix' not in sync_attributes:
1167
        m = ("you did not specify a 'sync_prefix' attribute "
1168
             "in class '%s'" % (class_name,))
1169
        raise ValueError(m)
1170

    
1171
    prefix = sync_attributes.pop('prefix')
1172
    class_name = sync_attributes.pop('classname', prefix + '_model')
1173

    
1174
    for name, value in sync_attributes.iteritems():
1175
        newname = prefix + '_' + name
1176
        if newname in new_attributes:
1177
            m = ("class '%s' was specified with prefix '%s' "
1178
                 "but it already has an attribute named '%s'"
1179
                 % (class_name, prefix, newname))
1180
            raise ValueError(m)
1181

    
1182
        new_attributes[newname] = value
1183

    
1184
    newclass = type(class_name, class_parents, new_attributes)
1185
    return newclass
1186

    
1187

    
1188
def make_synced(prefix='sync', name='SyncedState'):
1189

    
1190
    the_name = name
1191
    the_prefix = prefix
1192

    
1193
    class SyncedState(models.Model):
1194

    
1195
        sync_classname      = the_name
1196
        sync_prefix         = the_prefix
1197
        __metaclass__       = synced_model_metaclass
1198

    
1199
        sync_new_state      = models.BigIntegerField(null=True)
1200
        sync_synced_state   = models.BigIntegerField(null=True)
1201
        STATUS_SYNCED       = 0
1202
        STATUS_PENDING      = 1
1203
        sync_status         = models.IntegerField(db_index=True)
1204

    
1205
        class Meta:
1206
            abstract = True
1207

    
1208
        class NotSynced(Exception):
1209
            pass
1210

    
1211
        def sync_init_state(self, state):
1212
            self.sync_synced_state = state
1213
            self.sync_new_state = state
1214
            self.sync_status = self.STATUS_SYNCED
1215

    
1216
        def sync_get_status(self):
1217
            return self.sync_status
1218

    
1219
        def sync_set_status(self):
1220
            if self.sync_new_state != self.sync_synced_state:
1221
                self.sync_status = self.STATUS_PENDING
1222
            else:
1223
                self.sync_status = self.STATUS_SYNCED
1224

    
1225
        def sync_set_synced(self):
1226
            self.sync_synced_state = self.sync_new_state
1227
            self.sync_status = self.STATUS_SYNCED
1228

    
1229
        def sync_get_synced_state(self):
1230
            return self.sync_synced_state
1231

    
1232
        def sync_set_new_state(self, new_state):
1233
            self.sync_new_state = new_state
1234
            self.sync_set_status()
1235

    
1236
        def sync_get_new_state(self):
1237
            return self.sync_new_state
1238

    
1239
        def sync_set_synced_state(self, synced_state):
1240
            self.sync_synced_state = synced_state
1241
            self.sync_set_status()
1242

    
1243
        def sync_get_pending_objects(self):
1244
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1245
            return self.objects.filter(**kw)
1246

    
1247
        def sync_get_synced_objects(self):
1248
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1249
            return self.objects.filter(**kw)
1250

    
1251
        def sync_verify_get_synced_state(self):
1252
            status = self.sync_get_status()
1253
            state = self.sync_get_synced_state()
1254
            verified = (status == self.STATUS_SYNCED)
1255
            return state, verified
1256

    
1257
        def sync_is_synced(self):
1258
            state, verified = self.sync_verify_get_synced_state()
1259
            return verified
1260

    
1261
    return SyncedState
1262

    
1263
SyncedState = make_synced(prefix='sync', name='SyncedState')
1264

    
1265

    
1266
class ProjectApplicationManager(ForUpdateManager):
1267

    
1268
    def user_visible_projects(self, *filters, **kw_filters):
1269
        model = self.model
1270
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1271

    
1272
    def user_visible_by_chain(self, flt):
1273
        model = self.model
1274
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1275
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1276
        by_chain = dict(pending.annotate(models.Max('id')))
1277
        by_chain.update(approved.annotate(models.Max('id')))
1278
        return self.filter(flt, id__in=by_chain.values())
1279

    
1280
    def user_accessible_projects(self, user):
1281
        """
1282
        Return projects accessed by specified user.
1283
        """
1284
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1285
                               Q(project__projectmembership__person=user)
1286

    
1287
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1288

    
1289
    def search_by_name(self, *search_strings):
1290
        q = Q()
1291
        for s in search_strings:
1292
            q = q | Q(name__icontains=s)
1293
        return self.filter(q)
1294

    
1295
    def latest_of_chain(self, chain_id):
1296
        try:
1297
            return self.filter(chain=chain_id).order_by('-id')[0]
1298
        except IndexError:
1299
            return None
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
                                                verify_exists=False)
1340
    description             =   models.TextField(null=True, blank=True)
1341
    start_date              =   models.DateTimeField(null=True, blank=True)
1342
    end_date                =   models.DateTimeField()
1343
    member_join_policy      =   models.IntegerField()
1344
    member_leave_policy     =   models.IntegerField()
1345
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1346
    resource_grants         =   models.ManyToManyField(
1347
                                    Resource,
1348
                                    null=True,
1349
                                    blank=True,
1350
                                    through='ProjectResourceGrant')
1351
    comments                =   models.TextField(null=True, blank=True)
1352
    issue_date              =   models.DateTimeField(auto_now_add=True)
1353
    response_date           =   models.DateTimeField(null=True, blank=True)
1354

    
1355
    objects                 =   ProjectApplicationManager()
1356

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

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

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

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

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

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

    
1388
    def project_state_display(self):
1389
        try:
1390
            project = self.project
1391
            return project.state_display()
1392
        except Project.DoesNotExist:
1393
            return self.state_display()
1394

    
1395
    def add_resource_policy(self, service, resource, uplimit):
1396
        """Raises ObjectDoesNotExist, IntegrityError"""
1397
        q = self.projectresourcegrant_set
1398
        resource = Resource.objects.get(service__name=service, name=resource)
1399
        q.create(resource=resource, member_capacity=uplimit)
1400

    
1401
    def members_count(self):
1402
        return self.project.approved_memberships.count()
1403

    
1404
    @property
1405
    def grants(self):
1406
        return self.projectresourcegrant_set.values(
1407
            'member_capacity', 'resource__name', 'resource__service__name')
1408

    
1409
    @property
1410
    def resource_policies(self):
1411
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1412

    
1413
    @resource_policies.setter
1414
    def resource_policies(self, policies):
1415
        for p in policies:
1416
            service = p.get('service', None)
1417
            resource = p.get('resource', None)
1418
            uplimit = p.get('uplimit', 0)
1419
            self.add_resource_policy(service, resource, uplimit)
1420

    
1421
    def pending_modifications_incl_me(self):
1422
        q = self.chained_applications()
1423
        q = q.filter(Q(state=self.PENDING))
1424
        return q
1425

    
1426
    def last_pending_incl_me(self):
1427
        try:
1428
            return self.pending_modifications_incl_me().order_by('-id')[0]
1429
        except IndexError:
1430
            return None
1431

    
1432
    def pending_modifications(self):
1433
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1434

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1513
        Raises:
1514
            PermissionDenied
1515
        """
1516

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

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

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

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

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

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

    
1551
        project.save()
1552

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

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

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

    
1565
class ProjectResourceGrant(models.Model):
1566

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

    
1577
    objects = ExtendedManager()
1578

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

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

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

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

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

    
1630

    
1631
class ProjectManager(ForUpdateManager):
1632

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

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

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

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

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

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

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

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

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

    
1670

    
1671
class Project(models.Model):
1672

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

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

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

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

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

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

    
1703
    objects     =   ProjectManager()
1704

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

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

    
1713
    __repr__ = __str__
1714

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

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

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

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

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

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

    
1743
        return self.state != self.APPROVED
1744

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

    
1749
        return self.is_deactivated(reason)
1750

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

    
1755
        return self.is_deactivated(reason)
1756

    
1757
    ### Deactivation calls
1758

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

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

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

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

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

    
1783
    ### Logical checks
1784

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

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

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

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

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

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

    
1810
    def violates_resource_grants(self):
1811
        return False
1812

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

    
1820

    
1821
    ### Other
1822

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

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

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

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

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

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

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

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

    
1867

    
1868
class PendingMembershipError(Exception):
1869
    pass
1870

    
1871

    
1872
class ProjectMembershipManager(ForUpdateManager):
1873

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

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

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

    
1885
class ProjectMembership(models.Model):
1886

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

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

    
1898
    REMOVED             =   200
1899

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

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

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

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

    
1926
    objects     =   ProjectMembershipManager()
1927

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

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

    
1939
    USER_FRIENDLY_STATE_DISPLAY = {
1940
        REQUESTED           : _('Join requested'),
1941
        ACCEPTED            : _('Accepted member'),
1942
        USER_SUSPENDED      : _('Suspended member'),
1943
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
1944
        REMOVED             : _('Pending removal'),
1945
        }
1946

    
1947
    def state_display(self):
1948
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1949

    
1950
    def user_friendly_state_display(self):
1951
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1952

    
1953
    def get_combined_state(self):
1954
        return self.state, self.is_active, self.is_pending
1955

    
1956
    class Meta:
1957
        unique_together = ("person", "project")
1958
        #index_together = [["project", "state"]]
1959

    
1960
    def __str__(self):
1961
        return _("<'%s' membership in '%s'>") % (
1962
                self.person.username, self.project)
1963

    
1964
    __repr__ = __str__
1965

    
1966
    def __init__(self, *args, **kwargs):
1967
        self.state = self.REQUESTED
1968
        super(ProjectMembership, self).__init__(*args, **kwargs)
1969

    
1970
    def _set_history_item(self, reason, date=None):
1971
        if isinstance(reason, basestring):
1972
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1973

    
1974
        history_item = ProjectMembershipHistory(
1975
                            serial=self.id,
1976
                            person=self.person_id,
1977
                            project=self.project_id,
1978
                            date=date or datetime.now(),
1979
                            reason=reason)
1980
        history_item.save()
1981
        serial = history_item.id
1982

    
1983
    def can_accept(self):
1984
        return self.state == self.REQUESTED
1985

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

    
1991
        if not self.can_accept():
1992
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
1993
            raise AssertionError(m)
1994

    
1995
        now = datetime.now()
1996
        self.acceptance_date = now
1997
        self._set_history_item(reason='ACCEPT', date=now)
1998
        if self.project.is_approved():
1999
            self.state = self.ACCEPTED
2000
            self.is_pending = True
2001
        else:
2002
            self.state = self.PROJECT_DEACTIVATED
2003

    
2004
        self.save()
2005

    
2006
    def can_leave(self):
2007
        return self.can_remove()
2008

    
2009
    def can_remove(self):
2010
        return self.state in self.ACCEPTED_STATES
2011

    
2012
    def remove(self):
2013
        if self.is_pending:
2014
            m = _("%s: attempt to remove while is pending") % (self,)
2015
            raise AssertionError(m)
2016

    
2017
        if not self.can_remove():
2018
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2019
            raise AssertionError(m)
2020

    
2021
        self._set_history_item(reason='REMOVE')
2022
        self.state = self.REMOVED
2023
        self.is_pending = True
2024
        self.save()
2025

    
2026
    def can_reject(self):
2027
        return self.state == self.REQUESTED
2028

    
2029
    def reject(self):
2030
        if self.is_pending:
2031
            m = _("%s: attempt to reject while is pending") % (self,)
2032
            raise AssertionError(m)
2033

    
2034
        if not self.can_reject():
2035
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2036
            raise AssertionError(m)
2037

    
2038
        # rejected requests don't need sync,
2039
        # because they were never effected
2040
        self._set_history_item(reason='REJECT')
2041
        self.delete()
2042

    
2043
    def can_cancel(self):
2044
        return self.state == self.REQUESTED
2045

    
2046
    def cancel(self):
2047
        if self.is_pending:
2048
            m = _("%s: attempt to cancel while is pending") % (self,)
2049
            raise AssertionError(m)
2050

    
2051
        if not self.can_cancel():
2052
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2053
            raise AssertionError(m)
2054

    
2055
        # rejected requests don't need sync,
2056
        # because they were never effected
2057
        self._set_history_item(reason='CANCEL')
2058
        self.delete()
2059

    
2060
    def get_diff_quotas(self, sub_list=None, add_list=None):
2061
        if sub_list is None:
2062
            sub_list = []
2063

    
2064
        if add_list is None:
2065
            add_list = []
2066

    
2067
        sub_append = sub_list.append
2068
        add_append = add_list.append
2069
        holder = self.person.uuid
2070

    
2071
        synced_application = self.application
2072
        if synced_application is not None:
2073
            cur_grants = synced_application.projectresourcegrant_set.all()
2074
            for grant in cur_grants:
2075
                sub_append(QuotaLimits(
2076
                               holder       = holder,
2077
                               resource     = str(grant.resource),
2078
                               capacity     = grant.member_capacity,
2079
                               import_limit = grant.member_import_limit,
2080
                               export_limit = grant.member_export_limit))
2081

    
2082
        pending_application = self.pending_application
2083
        if pending_application is not None:
2084
            new_grants = pending_application.projectresourcegrant_set.all()
2085
            for new_grant in new_grants:
2086
                add_append(QuotaLimits(
2087
                               holder       = holder,
2088
                               resource     = str(new_grant.resource),
2089
                               capacity     = new_grant.member_capacity,
2090
                               import_limit = new_grant.member_import_limit,
2091
                               export_limit = new_grant.member_export_limit))
2092

    
2093
        return (sub_list, add_list)
2094

    
2095
    def set_sync(self):
2096
        if not self.is_pending:
2097
            m = _("%s: attempt to sync a non pending membership") % (self,)
2098
            raise AssertionError(m)
2099

    
2100
        state = self.state
2101
        if state == self.ACCEPTED:
2102
            pending_application = self.pending_application
2103
            if pending_application is None:
2104
                m = _("%s: attempt to sync an empty pending application") % (
2105
                    self,)
2106
                raise AssertionError(m)
2107

    
2108
            self.application = pending_application
2109
            self.is_active = True
2110

    
2111
            self.pending_application = None
2112
            self.pending_serial = None
2113

    
2114
            # project.application may have changed in the meantime,
2115
            # in which case we stay PENDING;
2116
            # we are safe to check due to select_for_update
2117
            if self.application == self.project.application:
2118
                self.is_pending = False
2119
            self.save()
2120

    
2121
        elif state == self.PROJECT_DEACTIVATED:
2122
            if self.pending_application:
2123
                m = _("%s: attempt to sync in state '%s' "
2124
                      "with a pending application") % (self, state)
2125
                raise AssertionError(m)
2126

    
2127
            self.application = None
2128
            self.is_active = False
2129
            self.pending_serial = None
2130
            self.is_pending = False
2131
            self.save()
2132

    
2133
        elif state == self.REMOVED:
2134
            self.delete()
2135

    
2136
        else:
2137
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2138
            raise AssertionError(m)
2139

    
2140
    def reset_sync(self):
2141
        if not self.is_pending:
2142
            m = _("%s: attempt to reset a non pending membership") % (self,)
2143
            raise AssertionError(m)
2144

    
2145
        state = self.state
2146
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
2147
            self.pending_application = None
2148
            self.pending_serial = None
2149
            self.save()
2150
        else:
2151
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2152
            raise AssertionError(m)
2153

    
2154
class Serial(models.Model):
2155
    serial  =   models.AutoField(primary_key=True)
2156

    
2157
def new_serial():
2158
    s = Serial.objects.create()
2159
    serial = s.serial
2160
    s.delete()
2161
    return serial
2162

    
2163
def sync_finish_serials(serials_to_ack=None):
2164
    if serials_to_ack is None:
2165
        serials_to_ack = qh_query_serials([])
2166

    
2167
    serials_to_ack = set(serials_to_ack)
2168
    sfu = ProjectMembership.objects.select_for_update()
2169
    memberships = list(sfu.filter(pending_serial__isnull=False))
2170

    
2171
    if memberships:
2172
        for membership in memberships:
2173
            serial = membership.pending_serial
2174
            if serial in serials_to_ack:
2175
                membership.set_sync()
2176
            else:
2177
                membership.reset_sync()
2178

    
2179
        transaction.commit()
2180

    
2181
    qh_ack_serials(list(serials_to_ack))
2182
    return len(memberships)
2183

    
2184
def pre_sync_projects(sync=True):
2185
    ACCEPTED = ProjectMembership.ACCEPTED
2186
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2187
    psfu = Project.objects.select_for_update()
2188

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

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

    
2199
    reactivating = list(psfu.reactivating_projects())
2200
    if sync:
2201
        for project in reactivating:
2202
            objects = project.projectmembership_set.select_for_update()
2203

    
2204
            memberships = objects.filter(state=PROJECT_DEACTIVATED)
2205
            for membership in memberships:
2206
                membership.is_pending = True
2207
                membership.state = ACCEPTED
2208
                membership.save()
2209

    
2210
    deactivating = list(psfu.deactivating_projects())
2211
    if sync:
2212
        for project in deactivating:
2213
            objects = project.projectmembership_set.select_for_update()
2214

    
2215
            # Note: we keep a user-level deactivation
2216
            # (e.g. USER_SUSPENDED) intact
2217
            memberships = objects.filter(state=ACCEPTED)
2218
            for membership in memberships:
2219
                membership.is_pending = True
2220
                membership.state = PROJECT_DEACTIVATED
2221
                membership.save()
2222

    
2223
    return (modified, reactivating, deactivating)
2224

    
2225
def do_sync_projects():
2226

    
2227
    ACCEPTED = ProjectMembership.ACCEPTED
2228
    objects = ProjectMembership.objects.select_for_update()
2229

    
2230
    sub_quota, add_quota = [], []
2231

    
2232
    serial = new_serial()
2233

    
2234
    pending = objects.filter(is_pending=True)
2235
    for membership in pending:
2236

    
2237
        if membership.pending_application:
2238
            m = "%s: impossible: pending_application is not None (%s)" % (
2239
                membership, membership.pending_application)
2240
            raise AssertionError(m)
2241
        if membership.pending_serial:
2242
            m = "%s: impossible: pending_serial is not None (%s)" % (
2243
                membership, membership.pending_serial)
2244
            raise AssertionError(m)
2245

    
2246
        if membership.state == ACCEPTED:
2247
            membership.pending_application = membership.project.application
2248

    
2249
        membership.pending_serial = serial
2250
        membership.get_diff_quotas(sub_quota, add_quota)
2251
        membership.save()
2252

    
2253
    transaction.commit()
2254
    # ProjectApplication.approve() unblocks here
2255
    # and can set PENDING an already PENDING membership
2256
    # which has been scheduled to sync with the old project.application
2257
    # Need to check in ProjectMembership.set_sync()
2258

    
2259
    r = qh_add_quota(serial, sub_quota, add_quota)
2260
    if r:
2261
        m = "cannot sync serial: %d" % serial
2262
        raise RuntimeError(m)
2263

    
2264
    return serial
2265

    
2266
def post_sync_projects():
2267
    ACCEPTED = ProjectMembership.ACCEPTED
2268
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2269
    psfu = Project.objects.select_for_update()
2270

    
2271
    modified = psfu.modified_projects()
2272
    for project in modified:
2273
        objects = project.projectmembership_set.select_for_update()
2274

    
2275
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2276
        if not memberships:
2277
            project.is_modified = False
2278
            project.save()
2279

    
2280
    reactivating = psfu.reactivating_projects()
2281
    for project in reactivating:
2282
        objects = project.projectmembership_set.select_for_update()
2283
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2284
                                          Q(is_pending=True)))
2285
        if not memberships:
2286
            project.reactivate()
2287
            project.save()
2288

    
2289
    deactivating = psfu.deactivating_projects()
2290
    for project in deactivating:
2291
        objects = project.projectmembership_set.select_for_update()
2292

    
2293
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2294
                                          Q(is_pending=True)))
2295
        if not memberships:
2296
            project.deactivate()
2297
            project.save()
2298

    
2299
    transaction.commit()
2300

    
2301
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2302
    @with_lock(retries, retry_wait)
2303
    def _sync_projects(sync):
2304
        sync_finish_serials()
2305
        # Informative only -- no select_for_update()
2306
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2307

    
2308
        projects_log = pre_sync_projects(sync)
2309
        if sync:
2310
            serial = do_sync_projects()
2311
            sync_finish_serials([serial])
2312
            post_sync_projects()
2313

    
2314
        return (pending, projects_log)
2315
    return _sync_projects(sync)
2316

    
2317
def all_users_quotas(users):
2318
    quotas = {}
2319
    for user in users:
2320
        quotas[user.uuid] = user.all_quotas()
2321
    return quotas
2322

    
2323
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2324
    @with_lock(retries, retry_wait)
2325
    def _sync_users(users, sync):
2326
        sync_finish_serials()
2327

    
2328
        existing, nonexisting = qh_check_users(users)
2329
        resources = get_resource_names()
2330
        registered_quotas = qh_get_quota_limits(existing, resources)
2331
        astakos_quotas = all_users_quotas(users)
2332

    
2333
        if sync:
2334
            r = register_users(nonexisting)
2335
            r = send_quotas(astakos_quotas)
2336

    
2337
        return (existing, nonexisting, registered_quotas, astakos_quotas)
2338
    return _sync_users(users, sync)
2339

    
2340
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2341
    users = AstakosUser.objects.filter(is_active=True)
2342
    return sync_users(users, sync, retries, retry_wait)
2343

    
2344
class ProjectMembershipHistory(models.Model):
2345
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2346
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2347

    
2348
    person  =   models.BigIntegerField()
2349
    project =   models.BigIntegerField()
2350
    date    =   models.DateField(auto_now_add=True)
2351
    reason  =   models.IntegerField()
2352
    serial  =   models.BigIntegerField()
2353

    
2354
### SIGNALS ###
2355
################
2356

    
2357
def create_astakos_user(u):
2358
    try:
2359
        AstakosUser.objects.get(user_ptr=u.pk)
2360
    except AstakosUser.DoesNotExist:
2361
        extended_user = AstakosUser(user_ptr_id=u.pk)
2362
        extended_user.__dict__.update(u.__dict__)
2363
        extended_user.save()
2364
        if not extended_user.has_auth_provider('local'):
2365
            extended_user.add_auth_provider('local')
2366
    except BaseException, e:
2367
        logger.exception(e)
2368

    
2369

    
2370
def fix_superusers(sender, **kwargs):
2371
    # Associate superusers with AstakosUser
2372
    admins = User.objects.filter(is_superuser=True)
2373
    for u in admins:
2374
        create_astakos_user(u)
2375
post_syncdb.connect(fix_superusers)
2376

    
2377

    
2378
def user_post_save(sender, instance, created, **kwargs):
2379
    if not created:
2380
        return
2381
    create_astakos_user(instance)
2382
post_save.connect(user_post_save, sender=User)
2383

    
2384
def astakosuser_post_save(sender, instance, created, **kwargs):
2385
    pass
2386

    
2387
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2388

    
2389
def resource_post_save(sender, instance, created, **kwargs):
2390
    pass
2391

    
2392
post_save.connect(resource_post_save, sender=Resource)
2393

    
2394
def renew_token(sender, instance, **kwargs):
2395
    if not instance.auth_token:
2396
        instance.renew_token()
2397
pre_save.connect(renew_token, sender=AstakosUser)
2398
pre_save.connect(renew_token, sender=Service)
2399