Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (82.1 kB)

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

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

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

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

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

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

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

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

    
87
logger = logging.getLogger(__name__)
88

    
89
DEFAULT_CONTENT_TYPE = None
90
_content_type = None
91

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

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

    
104
RESOURCE_SEPARATOR = '.'
105

    
106
inf = float('inf')
107

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

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

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

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

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

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

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

    
147

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
217

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

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

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

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

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

    
260
    return _DEFAULT_QUOTA
261

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

    
268

    
269
class AstakosUserManager(UserManager):
270

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

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

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

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

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

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

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

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

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

    
321

    
322

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

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

    
339

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

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

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

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

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

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

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

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

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

    
375
    objects = AstakosUserManager()
376

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
504
        self.update_uuid()
505

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
613
        return True
614

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

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

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

    
626
        return provider.is_available_for_remove()
627

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

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

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

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

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

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

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

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

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

    
682
        pending.delete()
683
        return provider
684

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

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

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

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

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

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

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

    
720
        modules = astakos_settings.IM_MODULES
721
        def key(p):
722
            if not p.module in modules:
723
                return 100
724
            return modules.index(p.module)
725
        providers = sorted(providers, key=key)
726
        return providers
727

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

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

    
742
    @property
743
    def auth_providers_display(self):
744
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
745

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

    
771
        return mark_safe(message + u' '+ msg_extra)
772

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

    
776
    def owns_project(self, project):
777
        return project.application.owner == self
778

    
779
    def is_associated(self, project):
780
        try:
781
            m = ProjectMembership.objects.get(person=self, project=project)
782
            return m.state in ProjectMembership.ASSOCIATED_STATES
783
        except ProjectMembership.DoesNotExist:
784
            return False
785

    
786
    def is_project_member(self, project_or_application):
787
        return self.get_status_in_project(project_or_application) in \
788
                                        ProjectMembership.ASSOCIATED_STATES
789

    
790
    def is_project_accepted_member(self, project_or_application):
791
        return self.get_status_in_project(project_or_application) in \
792
                                            ProjectMembership.ACCEPTED_STATES
793

    
794
    def get_status_in_project(self, project_or_application):
795
        application = project_or_application
796
        if isinstance(project_or_application, Project):
797
            application = project_or_application.project
798
        return application.user_status(self)
799

    
800
    def get_membership(self, project):
801
        try:
802
            return ProjectMembership.objects.get(
803
                project=project,
804
                person=self)
805
        except ProjectMembership.DoesNotExist:
806
            return None
807

    
808
    def membership_display(self, project):
809
        m = self.get_membership(project)
810
        if m is None:
811
            return _('Not a member')
812
        else:
813
            return m.user_friendly_state_display()
814

    
815

    
816
class AstakosUserAuthProviderManager(models.Manager):
817

    
818
    def active(self, **filters):
819
        return self.filter(active=True, **filters)
820

    
821
    def remove_unverified_providers(self, provider, **filters):
822
        try:
823
            existing = self.filter(module=provider, user__email_verified=False, **filters)
824
            for p in existing:
825
                p.user.delete()
826
        except:
827
            pass
828

    
829

    
830

    
831
class AstakosUserAuthProvider(models.Model):
832
    """
833
    Available user authentication methods.
834
    """
835
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
836
                                   null=True, default=None)
837
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
838
    module = models.CharField(_('Provider'), max_length=255, blank=False,
839
                                default='local')
840
    identifier = models.CharField(_('Third-party identifier'),
841
                                              max_length=255, null=True,
842
                                              blank=True)
843
    active = models.BooleanField(default=True)
844
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
845
                                   default='astakos')
846
    info_data = models.TextField(default="", null=True, blank=True)
847
    created = models.DateTimeField('Creation date', auto_now_add=True)
848

    
849
    objects = AstakosUserAuthProviderManager()
850

    
851
    class Meta:
852
        unique_together = (('identifier', 'module', 'user'), )
853
        ordering = ('module', 'created')
854

    
855
    def __init__(self, *args, **kwargs):
856
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
857
        try:
858
            self.info = json.loads(self.info_data)
859
            if not self.info:
860
                self.info = {}
861
        except Exception, e:
862
            self.info = {}
863

    
864
        for key,value in self.info.iteritems():
865
            setattr(self, 'info_%s' % key, value)
866

    
867

    
868
    @property
869
    def settings(self):
870
        return auth_providers.get_provider(self.module)
871

    
872
    @property
873
    def details_display(self):
874
        try:
875
            params = self.user.__dict__
876
            params.update(self.__dict__)
877
            return self.settings.get_details_tpl_display % params
878
        except:
879
            return ''
880

    
881
    @property
882
    def title_display(self):
883
        title_tpl = self.settings.get_title_display
884
        try:
885
            if self.settings.get_user_title_display:
886
                title_tpl = self.settings.get_user_title_display
887
        except Exception, e:
888
            pass
889
        try:
890
          return title_tpl % self.__dict__
891
        except:
892
          return self.settings.get_title_display % self.__dict__
893

    
894
    def can_remove(self):
895
        return self.user.can_remove_auth_provider(self.module)
896

    
897
    def delete(self, *args, **kwargs):
898
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
899
        if self.module == 'local':
900
            self.user.set_unusable_password()
901
            self.user.save()
902
        return ret
903

    
904
    def __repr__(self):
905
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
906

    
907
    def __unicode__(self):
908
        if self.identifier:
909
            return "%s:%s" % (self.module, self.identifier)
910
        if self.auth_backend:
911
            return "%s:%s" % (self.module, self.auth_backend)
912
        return self.module
913

    
914
    def save(self, *args, **kwargs):
915
        self.info_data = json.dumps(self.info)
916
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
917

    
918

    
919
class ExtendedManager(models.Manager):
920
    def _update_or_create(self, **kwargs):
921
        assert kwargs, \
922
            'update_or_create() must be passed at least one keyword argument'
923
        obj, created = self.get_or_create(**kwargs)
924
        defaults = kwargs.pop('defaults', {})
925
        if created:
926
            return obj, True, False
927
        else:
928
            try:
929
                params = dict(
930
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
931
                params.update(defaults)
932
                for attr, val in params.items():
933
                    if hasattr(obj, attr):
934
                        setattr(obj, attr, val)
935
                sid = transaction.savepoint()
936
                obj.save(force_update=True)
937
                transaction.savepoint_commit(sid)
938
                return obj, False, True
939
            except IntegrityError, e:
940
                transaction.savepoint_rollback(sid)
941
                try:
942
                    return self.get(**kwargs), False, False
943
                except self.model.DoesNotExist:
944
                    raise e
945

    
946
    update_or_create = _update_or_create
947

    
948

    
949
class AstakosUserQuota(models.Model):
950
    objects = ExtendedManager()
951
    capacity = intDecimalField()
952
    quantity = intDecimalField(default=0)
953
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
954
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
955
    resource = models.ForeignKey(Resource)
956
    user = models.ForeignKey(AstakosUser)
957

    
958
    class Meta:
959
        unique_together = ("resource", "user")
960

    
961
    def quota_values(self):
962
        return QuotaValues(
963
            quantity = self.quantity,
964
            capacity = self.capacity,
965
            import_limit = self.import_limit,
966
            export_limit = self.export_limit)
967

    
968

    
969
class ApprovalTerms(models.Model):
970
    """
971
    Model for approval terms
972
    """
973

    
974
    date = models.DateTimeField(
975
        _('Issue date'), db_index=True, auto_now_add=True)
976
    location = models.CharField(_('Terms location'), max_length=255)
977

    
978

    
979
class Invitation(models.Model):
980
    """
981
    Model for registring invitations
982
    """
983
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
984
                                null=True)
985
    realname = models.CharField(_('Real name'), max_length=255)
986
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
987
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
988
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
989
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
990
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
991

    
992
    def __init__(self, *args, **kwargs):
993
        super(Invitation, self).__init__(*args, **kwargs)
994
        if not self.id:
995
            self.code = _generate_invitation_code()
996

    
997
    def consume(self):
998
        self.is_consumed = True
999
        self.consumed = datetime.now()
1000
        self.save()
1001

    
1002
    def __unicode__(self):
1003
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1004

    
1005

    
1006
class EmailChangeManager(models.Manager):
1007

    
1008
    @transaction.commit_on_success
1009
    def change_email(self, activation_key):
1010
        """
1011
        Validate an activation key and change the corresponding
1012
        ``User`` if valid.
1013

1014
        If the key is valid and has not expired, return the ``User``
1015
        after activating.
1016

1017
        If the key is not valid or has expired, return ``None``.
1018

1019
        If the key is valid but the ``User`` is already active,
1020
        return ``None``.
1021

1022
        After successful email change the activation record is deleted.
1023

1024
        Throws ValueError if there is already
1025
        """
1026
        try:
1027
            email_change = self.model.objects.get(
1028
                activation_key=activation_key)
1029
            if email_change.activation_key_expired():
1030
                email_change.delete()
1031
                raise EmailChange.DoesNotExist
1032
            # is there an active user with this address?
1033
            try:
1034
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1035
            except AstakosUser.DoesNotExist:
1036
                pass
1037
            else:
1038
                raise ValueError(_('The new email address is reserved.'))
1039
            # update user
1040
            user = AstakosUser.objects.get(pk=email_change.user_id)
1041
            old_email = user.email
1042
            user.email = email_change.new_email_address
1043
            user.save()
1044
            email_change.delete()
1045
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1046
                                                          user.email)
1047
            logger.log(LOGGING_LEVEL, msg)
1048
            return user
1049
        except EmailChange.DoesNotExist:
1050
            raise ValueError(_('Invalid activation key.'))
1051

    
1052

    
1053
class EmailChange(models.Model):
1054
    new_email_address = models.EmailField(
1055
        _(u'new e-mail address'),
1056
        help_text=_('Provide a new email address. Until you verify the new '
1057
                    'address by following the activation link that will be '
1058
                    'sent to it, your old email address will remain active.'))
1059
    user = models.ForeignKey(
1060
        AstakosUser, unique=True, related_name='emailchanges')
1061
    requested_at = models.DateTimeField(auto_now_add=True)
1062
    activation_key = models.CharField(
1063
        max_length=40, unique=True, db_index=True)
1064

    
1065
    objects = EmailChangeManager()
1066

    
1067
    def get_url(self):
1068
        return reverse('email_change_confirm',
1069
                      kwargs={'activation_key': self.activation_key})
1070

    
1071
    def activation_key_expired(self):
1072
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1073
        return self.requested_at + expiration_date < datetime.now()
1074

    
1075

    
1076
class AdditionalMail(models.Model):
1077
    """
1078
    Model for registring invitations
1079
    """
1080
    owner = models.ForeignKey(AstakosUser)
1081
    email = models.EmailField()
1082

    
1083

    
1084
def _generate_invitation_code():
1085
    while True:
1086
        code = randint(1, 2L ** 63 - 1)
1087
        try:
1088
            Invitation.objects.get(code=code)
1089
            # An invitation with this code already exists, try again
1090
        except Invitation.DoesNotExist:
1091
            return code
1092

    
1093

    
1094
def get_latest_terms():
1095
    try:
1096
        term = ApprovalTerms.objects.order_by('-id')[0]
1097
        return term
1098
    except IndexError:
1099
        pass
1100
    return None
1101

    
1102
class PendingThirdPartyUser(models.Model):
1103
    """
1104
    Model for registring successful third party user authentications
1105
    """
1106
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1107
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1108
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1109
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1110
                                  null=True)
1111
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1112
                                 null=True)
1113
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1114
                                   null=True)
1115
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1116
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1117
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1118
    info = models.TextField(default="", null=True, blank=True)
1119

    
1120
    class Meta:
1121
        unique_together = ("provider", "third_party_identifier")
1122

    
1123
    def get_user_instance(self):
1124
        d = self.__dict__
1125
        d.pop('_state', None)
1126
        d.pop('id', None)
1127
        d.pop('token', None)
1128
        d.pop('created', None)
1129
        d.pop('info', None)
1130
        user = AstakosUser(**d)
1131

    
1132
        return user
1133

    
1134
    @property
1135
    def realname(self):
1136
        return '%s %s' %(self.first_name, self.last_name)
1137

    
1138
    @realname.setter
1139
    def realname(self, value):
1140
        parts = value.split(' ')
1141
        if len(parts) == 2:
1142
            self.first_name = parts[0]
1143
            self.last_name = parts[1]
1144
        else:
1145
            self.last_name = parts[0]
1146

    
1147
    def save(self, **kwargs):
1148
        if not self.id:
1149
            # set username
1150
            while not self.username:
1151
                username =  uuid.uuid4().hex[:30]
1152
                try:
1153
                    AstakosUser.objects.get(username = username)
1154
                except AstakosUser.DoesNotExist, e:
1155
                    self.username = username
1156
        super(PendingThirdPartyUser, self).save(**kwargs)
1157

    
1158
    def generate_token(self):
1159
        self.password = self.third_party_identifier
1160
        self.last_login = datetime.now()
1161
        self.token = default_token_generator.make_token(self)
1162

    
1163
class SessionCatalog(models.Model):
1164
    session_key = models.CharField(_('session key'), max_length=40)
1165
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1166

    
1167

    
1168
### PROJECTS ###
1169
################
1170

    
1171
def synced_model_metaclass(class_name, class_parents, class_attributes):
1172

    
1173
    new_attributes = {}
1174
    sync_attributes = {}
1175

    
1176
    for name, value in class_attributes.iteritems():
1177
        sync, underscore, rest = name.partition('_')
1178
        if sync == 'sync' and underscore == '_':
1179
            sync_attributes[rest] = value
1180
        else:
1181
            new_attributes[name] = value
1182

    
1183
    if 'prefix' not in sync_attributes:
1184
        m = ("you did not specify a 'sync_prefix' attribute "
1185
             "in class '%s'" % (class_name,))
1186
        raise ValueError(m)
1187

    
1188
    prefix = sync_attributes.pop('prefix')
1189
    class_name = sync_attributes.pop('classname', prefix + '_model')
1190

    
1191
    for name, value in sync_attributes.iteritems():
1192
        newname = prefix + '_' + name
1193
        if newname in new_attributes:
1194
            m = ("class '%s' was specified with prefix '%s' "
1195
                 "but it already has an attribute named '%s'"
1196
                 % (class_name, prefix, newname))
1197
            raise ValueError(m)
1198

    
1199
        new_attributes[newname] = value
1200

    
1201
    newclass = type(class_name, class_parents, new_attributes)
1202
    return newclass
1203

    
1204

    
1205
def make_synced(prefix='sync', name='SyncedState'):
1206

    
1207
    the_name = name
1208
    the_prefix = prefix
1209

    
1210
    class SyncedState(models.Model):
1211

    
1212
        sync_classname      = the_name
1213
        sync_prefix         = the_prefix
1214
        __metaclass__       = synced_model_metaclass
1215

    
1216
        sync_new_state      = models.BigIntegerField(null=True)
1217
        sync_synced_state   = models.BigIntegerField(null=True)
1218
        STATUS_SYNCED       = 0
1219
        STATUS_PENDING      = 1
1220
        sync_status         = models.IntegerField(db_index=True)
1221

    
1222
        class Meta:
1223
            abstract = True
1224

    
1225
        class NotSynced(Exception):
1226
            pass
1227

    
1228
        def sync_init_state(self, state):
1229
            self.sync_synced_state = state
1230
            self.sync_new_state = state
1231
            self.sync_status = self.STATUS_SYNCED
1232

    
1233
        def sync_get_status(self):
1234
            return self.sync_status
1235

    
1236
        def sync_set_status(self):
1237
            if self.sync_new_state != self.sync_synced_state:
1238
                self.sync_status = self.STATUS_PENDING
1239
            else:
1240
                self.sync_status = self.STATUS_SYNCED
1241

    
1242
        def sync_set_synced(self):
1243
            self.sync_synced_state = self.sync_new_state
1244
            self.sync_status = self.STATUS_SYNCED
1245

    
1246
        def sync_get_synced_state(self):
1247
            return self.sync_synced_state
1248

    
1249
        def sync_set_new_state(self, new_state):
1250
            self.sync_new_state = new_state
1251
            self.sync_set_status()
1252

    
1253
        def sync_get_new_state(self):
1254
            return self.sync_new_state
1255

    
1256
        def sync_set_synced_state(self, synced_state):
1257
            self.sync_synced_state = synced_state
1258
            self.sync_set_status()
1259

    
1260
        def sync_get_pending_objects(self):
1261
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1262
            return self.objects.filter(**kw)
1263

    
1264
        def sync_get_synced_objects(self):
1265
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1266
            return self.objects.filter(**kw)
1267

    
1268
        def sync_verify_get_synced_state(self):
1269
            status = self.sync_get_status()
1270
            state = self.sync_get_synced_state()
1271
            verified = (status == self.STATUS_SYNCED)
1272
            return state, verified
1273

    
1274
        def sync_is_synced(self):
1275
            state, verified = self.sync_verify_get_synced_state()
1276
            return verified
1277

    
1278
    return SyncedState
1279

    
1280
SyncedState = make_synced(prefix='sync', name='SyncedState')
1281

    
1282

    
1283
class ProjectApplicationManager(ForUpdateManager):
1284

    
1285
    def user_visible_projects(self, *filters, **kw_filters):
1286
        model = self.model
1287
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1288

    
1289
    def user_visible_by_chain(self, flt):
1290
        model = self.model
1291
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1292
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1293
        by_chain = dict(pending.annotate(models.Max('id')))
1294
        by_chain.update(approved.annotate(models.Max('id')))
1295
        return self.filter(flt, id__in=by_chain.values())
1296

    
1297
    def user_accessible_projects(self, user):
1298
        """
1299
        Return projects accessed by specified user.
1300
        """
1301
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1302
                               Q(project__projectmembership__person=user)
1303

    
1304
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1305

    
1306
    def search_by_name(self, *search_strings):
1307
        q = Q()
1308
        for s in search_strings:
1309
            q = q | Q(name__icontains=s)
1310
        return self.filter(q)
1311

    
1312
    def latest_of_chain(self, chain_id):
1313
        try:
1314
            return self.filter(chain=chain_id).order_by('-id')[0]
1315
        except IndexError:
1316
            return None
1317

    
1318
class Chain(models.Model):
1319
    chain  =   models.AutoField(primary_key=True)
1320

    
1321
def new_chain():
1322
    c = Chain.objects.create()
1323
    chain = c.chain
1324
    c.delete()
1325
    return chain
1326

    
1327

    
1328

    
1329
USER_STATUS_DISPLAY = {}
1330

    
1331

    
1332
class ProjectApplication(models.Model):
1333
    applicant               =   models.ForeignKey(
1334
                                    AstakosUser,
1335
                                    related_name='projects_applied',
1336
                                    db_index=True)
1337

    
1338
    PENDING     =    0
1339
    APPROVED    =    1
1340
    REPLACED    =    2
1341
    DENIED      =    3
1342
    DISMISSED   =    4
1343
    CANCELLED   =    5
1344

    
1345
    state                   =   models.IntegerField(default=PENDING,
1346
                                                    db_index=True)
1347

    
1348
    owner                   =   models.ForeignKey(
1349
                                    AstakosUser,
1350
                                    related_name='projects_owned',
1351
                                    db_index=True)
1352

    
1353
    chain                   =   models.IntegerField()
1354
    precursor_application   =   models.ForeignKey('ProjectApplication',
1355
                                                  null=True,
1356
                                                  blank=True)
1357

    
1358
    name                    =   models.CharField(max_length=80)
1359
    homepage                =   models.URLField(max_length=255, null=True,
1360
                                                verify_exists=False)
1361
    description             =   models.TextField(null=True, blank=True)
1362
    start_date              =   models.DateTimeField(null=True, blank=True)
1363
    end_date                =   models.DateTimeField()
1364
    member_join_policy      =   models.IntegerField()
1365
    member_leave_policy     =   models.IntegerField()
1366
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1367
    resource_grants         =   models.ManyToManyField(
1368
                                    Resource,
1369
                                    null=True,
1370
                                    blank=True,
1371
                                    through='ProjectResourceGrant')
1372
    comments                =   models.TextField(null=True, blank=True)
1373
    issue_date              =   models.DateTimeField(auto_now_add=True)
1374
    response_date           =   models.DateTimeField(null=True, blank=True)
1375

    
1376
    objects                 =   ProjectApplicationManager()
1377

    
1378
    # Compiled queries
1379
    Q_PENDING  = Q(state=PENDING)
1380
    Q_APPROVED = Q(state=APPROVED)
1381
    Q_DENIED   = Q(state=DENIED)
1382

    
1383
    class Meta:
1384
        unique_together = ("chain", "id")
1385

    
1386
    def __unicode__(self):
1387
        return "%s applied by %s" % (self.name, self.applicant)
1388

    
1389
    # TODO: Move to a more suitable place
1390
    APPLICATION_STATE_DISPLAY = {
1391
        PENDING  : _('Pending review'),
1392
        APPROVED : _('Approved'),
1393
        REPLACED : _('Replaced'),
1394
        DENIED   : _('Denied'),
1395
        DISMISSED: _('Dismissed'),
1396
        CANCELLED: _('Cancelled')
1397
    }
1398

    
1399
    def get_project(self):
1400
        try:
1401
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1402
            return Project
1403
        except Project.DoesNotExist, e:
1404
            return None
1405

    
1406
    def state_display(self):
1407
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1408

    
1409
    def project_state_display(self):
1410
        try:
1411
            project = self.project
1412
            return project.state_display()
1413
        except Project.DoesNotExist:
1414
            return self.state_display()
1415

    
1416
    def add_resource_policy(self, service, resource, uplimit):
1417
        """Raises ObjectDoesNotExist, IntegrityError"""
1418
        q = self.projectresourcegrant_set
1419
        resource = Resource.objects.get(service__name=service, name=resource)
1420
        q.create(resource=resource, member_capacity=uplimit)
1421

    
1422
    def user_status(self, user):
1423
        try:
1424
            project = self.get_project()
1425
            if not project:
1426
                return -1
1427
            membership = project.projectmembership_set
1428
            membership = membership.exclude(state=ProjectMembership.REMOVED)
1429
            membership = membership.get(person=user)
1430
            status = membership.state
1431
        except ProjectMembership.DoesNotExist:
1432
            return -1
1433

    
1434
        return status
1435

    
1436
    def user_status_display(self, user):
1437
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1438

    
1439
    def members_count(self):
1440
        return self.project.approved_memberships.count()
1441

    
1442
    @property
1443
    def grants(self):
1444
        return self.projectresourcegrant_set.values(
1445
            'member_capacity', 'resource__name', 'resource__service__name')
1446

    
1447
    @property
1448
    def resource_policies(self):
1449
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1450

    
1451
    @resource_policies.setter
1452
    def resource_policies(self, policies):
1453
        for p in policies:
1454
            service = p.get('service', None)
1455
            resource = p.get('resource', None)
1456
            uplimit = p.get('uplimit', 0)
1457
            self.add_resource_policy(service, resource, uplimit)
1458

    
1459
    def pending_modifications_incl_me(self):
1460
        q = self.chained_applications()
1461
        q = q.filter(Q(state=self.PENDING))
1462
        return q
1463

    
1464
    def last_pending_incl_me(self):
1465
        try:
1466
            return self.pending_modifications_incl_me().order_by('-id')[0]
1467
        except IndexError:
1468
            return None
1469

    
1470
    def pending_modifications(self):
1471
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1472

    
1473
    def last_pending(self):
1474
        try:
1475
            return self.pending_modifications().order_by('-id')[0]
1476
        except IndexError:
1477
            return None
1478

    
1479
    def is_modification(self):
1480
        # if self.state != self.PENDING:
1481
        #     return False
1482
        parents = self.chained_applications().filter(id__lt=self.id)
1483
        parents = parents.filter(state__in=[self.APPROVED])
1484
        return parents.count() > 0
1485

    
1486
    def chained_applications(self):
1487
        return ProjectApplication.objects.filter(chain=self.chain)
1488

    
1489
    def has_pending_modifications(self):
1490
        return bool(self.last_pending())
1491

    
1492
    def get_project(self):
1493
        try:
1494
            return Project.objects.get(id=self.chain)
1495
        except Project.DoesNotExist:
1496
            return None
1497

    
1498
    def _get_project_for_update(self):
1499
        try:
1500
            objects = Project.objects.select_for_update()
1501
            project = objects.get(id=self.chain)
1502
            return project
1503
        except Project.DoesNotExist:
1504
            return None
1505

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

    
1509
    def cancel(self):
1510
        if not self.can_cancel():
1511
            m = _("cannot cancel: application '%s' in state '%s'") % (
1512
                    self.id, self.state)
1513
            raise AssertionError(m)
1514

    
1515
        self.state = self.CANCELLED
1516
        self.save()
1517

    
1518
    def can_dismiss(self):
1519
        return self.state == self.DENIED
1520

    
1521
    def dismiss(self):
1522
        if not self.can_dismiss():
1523
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1524
                    self.id, self.state)
1525
            raise AssertionError(m)
1526

    
1527
        self.state = self.DISMISSED
1528
        self.save()
1529

    
1530
    def can_deny(self):
1531
        return self.state == self.PENDING
1532

    
1533
    def deny(self):
1534
        if not self.can_deny():
1535
            m = _("cannot deny: application '%s' in state '%s'") % (
1536
                    self.id, self.state)
1537
            raise AssertionError(m)
1538

    
1539
        self.state = self.DENIED
1540
        self.response_date = datetime.now()
1541
        self.save()
1542

    
1543
    def can_approve(self):
1544
        return self.state == self.PENDING
1545

    
1546
    def approve(self, approval_user=None):
1547
        """
1548
        If approval_user then during owner membership acceptance
1549
        it is checked whether the request_user is eligible.
1550

1551
        Raises:
1552
            PermissionDenied
1553
        """
1554

    
1555
        if not transaction.is_managed():
1556
            raise AssertionError("NOPE")
1557

    
1558
        new_project_name = self.name
1559
        if not self.can_approve():
1560
            m = _("cannot approve: project '%s' in state '%s'") % (
1561
                    new_project_name, self.state)
1562
            raise AssertionError(m) # invalid argument
1563

    
1564
        now = datetime.now()
1565
        project = self._get_project_for_update()
1566

    
1567
        try:
1568
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1569
            conflicting_project = Project.objects.get(q)
1570
            if (conflicting_project != project):
1571
                m = (_("cannot approve: project with name '%s' "
1572
                       "already exists (id: %s)") % (
1573
                        new_project_name, conflicting_project.id))
1574
                raise PermissionDenied(m) # invalid argument
1575
        except Project.DoesNotExist:
1576
            pass
1577

    
1578
        new_project = False
1579
        if project is None:
1580
            new_project = True
1581
            project = Project(id=self.chain)
1582

    
1583
        project.name = new_project_name
1584
        project.application = self
1585
        project.last_approval_date = now
1586
        if not new_project:
1587
            project.is_modified = True
1588

    
1589
        project.save()
1590

    
1591
        self.state = self.APPROVED
1592
        self.response_date = now
1593
        self.save()
1594

    
1595
    @property
1596
    def member_join_policy_display(self):
1597
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1598

    
1599
    @property
1600
    def member_leave_policy_display(self):
1601
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1602

    
1603
class ProjectResourceGrant(models.Model):
1604

    
1605
    resource                =   models.ForeignKey(Resource)
1606
    project_application     =   models.ForeignKey(ProjectApplication,
1607
                                                  null=True)
1608
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1609
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1610
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1611
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1612
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1613
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1614

    
1615
    objects = ExtendedManager()
1616

    
1617
    class Meta:
1618
        unique_together = ("resource", "project_application")
1619

    
1620
    def member_quota_values(self):
1621
        return QuotaValues(
1622
            quantity = 0,
1623
            capacity = self.member_capacity,
1624
            import_limit = self.member_import_limit,
1625
            export_limit = self.member_export_limit)
1626

    
1627
    def display_member_capacity(self):
1628
        if self.member_capacity:
1629
            if self.resource.unit:
1630
                return ProjectResourceGrant.display_filesize(
1631
                    self.member_capacity)
1632
            else:
1633
                if math.isinf(self.member_capacity):
1634
                    return 'Unlimited'
1635
                else:
1636
                    return self.member_capacity
1637
        else:
1638
            return 'Unlimited'
1639

    
1640
    def __str__(self):
1641
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1642
                                        self.display_member_capacity())
1643

    
1644
    @classmethod
1645
    def display_filesize(cls, value):
1646
        try:
1647
            value = float(value)
1648
        except:
1649
            return
1650
        else:
1651
            if math.isinf(value):
1652
                return 'Unlimited'
1653
            if value > 1:
1654
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1655
                                [0, 0, 0, 0, 0, 0])
1656
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1657
                quotient = float(value) / 1024**exponent
1658
                unit, value_decimals = unit_list[exponent]
1659
                format_string = '{0:.%sf} {1}' % (value_decimals)
1660
                return format_string.format(quotient, unit)
1661
            if value == 0:
1662
                return '0 bytes'
1663
            if value == 1:
1664
                return '1 byte'
1665
            else:
1666
               return '0'
1667

    
1668

    
1669
class ProjectManager(ForUpdateManager):
1670

    
1671
    def terminated_projects(self):
1672
        q = self.model.Q_TERMINATED
1673
        return self.filter(q)
1674

    
1675
    def not_terminated_projects(self):
1676
        q = ~self.model.Q_TERMINATED
1677
        return self.filter(q)
1678

    
1679
    def terminating_projects(self):
1680
        q = self.model.Q_TERMINATED & Q(is_active=True)
1681
        return self.filter(q)
1682

    
1683
    def deactivated_projects(self):
1684
        q = self.model.Q_DEACTIVATED
1685
        return self.filter(q)
1686

    
1687
    def deactivating_projects(self):
1688
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1689
        return self.filter(q)
1690

    
1691
    def modified_projects(self):
1692
        return self.filter(is_modified=True)
1693

    
1694
    def reactivating_projects(self):
1695
        return self.filter(state=Project.APPROVED, is_active=False)
1696

    
1697
    def expired_projects(self):
1698
        q = (~Q(state=Project.TERMINATED) &
1699
              Q(application__end_date__lt=datetime.now()))
1700
        return self.filter(q)
1701

    
1702
    def search_by_name(self, *search_strings):
1703
        q = Q()
1704
        for s in search_strings:
1705
            q = q | Q(name__icontains=s)
1706
        return self.filter(q)
1707

    
1708

    
1709
class Project(models.Model):
1710

    
1711
    application                 =   models.OneToOneField(
1712
                                            ProjectApplication,
1713
                                            related_name='project')
1714
    last_approval_date          =   models.DateTimeField(null=True)
1715

    
1716
    members                     =   models.ManyToManyField(
1717
                                            AstakosUser,
1718
                                            through='ProjectMembership')
1719

    
1720
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1721
    deactivation_date           =   models.DateTimeField(null=True)
1722

    
1723
    creation_date               =   models.DateTimeField(auto_now_add=True)
1724
    name                        =   models.CharField(
1725
                                            max_length=80,
1726
                                            null=True,
1727
                                            db_index=True,
1728
                                            unique=True)
1729

    
1730
    APPROVED    = 1
1731
    SUSPENDED   = 10
1732
    TERMINATED  = 100
1733

    
1734
    is_modified                 =   models.BooleanField(default=False,
1735
                                                        db_index=True)
1736
    is_active                   =   models.BooleanField(default=True,
1737
                                                        db_index=True)
1738
    state                       =   models.IntegerField(default=APPROVED,
1739
                                                        db_index=True)
1740

    
1741
    objects     =   ProjectManager()
1742

    
1743
    # Compiled queries
1744
    Q_TERMINATED  = Q(state=TERMINATED)
1745
    Q_SUSPENDED   = Q(state=SUSPENDED)
1746
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1747

    
1748
    def __str__(self):
1749
        return _("<project %s '%s'>") % (self.id, self.application.name)
1750

    
1751
    __repr__ = __str__
1752

    
1753
    STATE_DISPLAY = {
1754
        APPROVED   : 'Active',
1755
        SUSPENDED  : 'Suspended',
1756
        TERMINATED : 'Terminated'
1757
        }
1758

    
1759
    def state_display(self):
1760
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1761

    
1762
    def admin_state_display(self):
1763
        s = self.state_display()
1764
        if self.sync_pending():
1765
            s += ' (sync pending)'
1766
        return s
1767

    
1768
    def sync_pending(self):
1769
        if self.state != self.APPROVED:
1770
            return self.is_active
1771
        return not self.is_active or self.is_modified
1772

    
1773
    def expiration_info(self):
1774
        return (str(self.id), self.name, self.state_display(),
1775
                str(self.application.end_date))
1776

    
1777
    def is_deactivated(self, reason=None):
1778
        if reason is not None:
1779
            return self.state == reason
1780

    
1781
        return self.state != self.APPROVED
1782

    
1783
    def is_deactivating(self, reason=None):
1784
        if not self.is_active:
1785
            return False
1786

    
1787
        return self.is_deactivated(reason)
1788

    
1789
    def is_deactivated_strict(self, reason=None):
1790
        if self.is_active:
1791
            return False
1792

    
1793
        return self.is_deactivated(reason)
1794

    
1795
    ### Deactivation calls
1796

    
1797
    def deactivate(self):
1798
        self.deactivation_date = datetime.now()
1799
        self.is_active = False
1800

    
1801
    def reactivate(self):
1802
        self.deactivation_date = None
1803
        self.is_active = True
1804

    
1805
    def terminate(self):
1806
        self.deactivation_reason = 'TERMINATED'
1807
        self.state = self.TERMINATED
1808
        self.name = None
1809
        self.save()
1810

    
1811
    def suspend(self):
1812
        self.deactivation_reason = 'SUSPENDED'
1813
        self.state = self.SUSPENDED
1814
        self.save()
1815

    
1816
    def resume(self):
1817
        self.deactivation_reason = None
1818
        self.state = self.APPROVED
1819
        self.save()
1820

    
1821
    ### Logical checks
1822

    
1823
    def is_inconsistent(self):
1824
        now = datetime.now()
1825
        dates = [self.creation_date,
1826
                 self.last_approval_date,
1827
                 self.deactivation_date]
1828
        return any([date > now for date in dates])
1829

    
1830
    def is_active_strict(self):
1831
        return self.is_active and self.state == self.APPROVED
1832

    
1833
    def is_approved(self):
1834
        return self.state == self.APPROVED
1835

    
1836
    @property
1837
    def is_alive(self):
1838
        return not self.is_terminated
1839

    
1840
    @property
1841
    def is_terminated(self):
1842
        return self.is_deactivated(self.TERMINATED)
1843

    
1844
    @property
1845
    def is_suspended(self):
1846
        return self.is_deactivated(self.SUSPENDED)
1847

    
1848
    def violates_resource_grants(self):
1849
        return False
1850

    
1851
    def violates_members_limit(self, adding=0):
1852
        application = self.application
1853
        limit = application.limit_on_members_number
1854
        if limit is None:
1855
            return False
1856
        return (len(self.approved_members) + adding > limit)
1857

    
1858

    
1859
    ### Other
1860

    
1861
    def count_pending_memberships(self):
1862
        memb_set = self.projectmembership_set
1863
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1864
        return memb_count
1865

    
1866
    def members_count(self):
1867
        return self.approved_memberships.count()
1868

    
1869
    @property
1870
    def approved_memberships(self):
1871
        query = ProjectMembership.Q_ACCEPTED_STATES
1872
        return self.projectmembership_set.filter(query)
1873

    
1874
    @property
1875
    def approved_members(self):
1876
        return [m.person for m in self.approved_memberships]
1877

    
1878
    def add_member(self, user):
1879
        """
1880
        Raises:
1881
            django.exceptions.PermissionDenied
1882
            astakos.im.models.AstakosUser.DoesNotExist
1883
        """
1884
        if isinstance(user, int):
1885
            user = AstakosUser.objects.get(user=user)
1886

    
1887
        m, created = ProjectMembership.objects.get_or_create(
1888
            person=user, project=self
1889
        )
1890
        m.accept()
1891

    
1892
    def remove_member(self, user):
1893
        """
1894
        Raises:
1895
            django.exceptions.PermissionDenied
1896
            astakos.im.models.AstakosUser.DoesNotExist
1897
            astakos.im.models.ProjectMembership.DoesNotExist
1898
        """
1899
        if isinstance(user, int):
1900
            user = AstakosUser.objects.get(user=user)
1901

    
1902
        m = ProjectMembership.objects.get(person=user, project=self)
1903
        m.remove()
1904

    
1905

    
1906
class PendingMembershipError(Exception):
1907
    pass
1908

    
1909

    
1910
class ProjectMembershipManager(ForUpdateManager):
1911

    
1912
    def any_accepted(self):
1913
        q = (Q(state=ProjectMembership.ACCEPTED) |
1914
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
1915
        return self.filter(q)
1916

    
1917
    def requested(self):
1918
        return self.filter(state=ProjectMembership.REQUESTED)
1919

    
1920
    def suspended(self):
1921
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1922

    
1923
class ProjectMembership(models.Model):
1924

    
1925
    person              =   models.ForeignKey(AstakosUser)
1926
    request_date        =   models.DateField(auto_now_add=True)
1927
    project             =   models.ForeignKey(Project)
1928

    
1929
    REQUESTED           =   0
1930
    ACCEPTED            =   1
1931
    # User deactivation
1932
    USER_SUSPENDED      =   10
1933
    # Project deactivation
1934
    PROJECT_DEACTIVATED =   100
1935

    
1936
    REMOVED             =   200
1937

    
1938
    ASSOCIATED_STATES   =   set([REQUESTED,
1939
                                 ACCEPTED,
1940
                                 USER_SUSPENDED,
1941
                                 PROJECT_DEACTIVATED])
1942

    
1943
    ACCEPTED_STATES     =   set([ACCEPTED,
1944
                                 USER_SUSPENDED,
1945
                                 PROJECT_DEACTIVATED])
1946

    
1947
    state               =   models.IntegerField(default=REQUESTED,
1948
                                                db_index=True)
1949
    is_pending          =   models.BooleanField(default=False, db_index=True)
1950
    is_active           =   models.BooleanField(default=False, db_index=True)
1951
    application         =   models.ForeignKey(
1952
                                ProjectApplication,
1953
                                null=True,
1954
                                related_name='memberships')
1955
    pending_application =   models.ForeignKey(
1956
                                ProjectApplication,
1957
                                null=True,
1958
                                related_name='pending_memberships')
1959
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1960

    
1961
    acceptance_date     =   models.DateField(null=True, db_index=True)
1962
    leave_request_date  =   models.DateField(null=True)
1963

    
1964
    objects     =   ProjectMembershipManager()
1965

    
1966
    # Compiled queries
1967
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1968

    
1969
    MEMBERSHIP_STATE_DISPLAY = {
1970
        REQUESTED           : _('Requested'),
1971
        ACCEPTED            : _('Accepted'),
1972
        USER_SUSPENDED      : _('Suspended'),
1973
        PROJECT_DEACTIVATED : _('Accepted'), # sic
1974
        REMOVED             : _('Pending removal'),
1975
        }
1976

    
1977
    USER_FRIENDLY_STATE_DISPLAY = {
1978
        REQUESTED           : _('Join requested'),
1979
        ACCEPTED            : _('Accepted member'),
1980
        USER_SUSPENDED      : _('Suspended member'),
1981
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
1982
        REMOVED             : _('Pending removal'),
1983
        }
1984

    
1985
    def state_display(self):
1986
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1987

    
1988
    def user_friendly_state_display(self):
1989
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1990

    
1991
    def get_combined_state(self):
1992
        return self.state, self.is_active, self.is_pending
1993

    
1994
    class Meta:
1995
        unique_together = ("person", "project")
1996
        #index_together = [["project", "state"]]
1997

    
1998
    def __str__(self):
1999
        return _("<'%s' membership in '%s'>") % (
2000
                self.person.username, self.project)
2001

    
2002
    __repr__ = __str__
2003

    
2004
    def __init__(self, *args, **kwargs):
2005
        self.state = self.REQUESTED
2006
        super(ProjectMembership, self).__init__(*args, **kwargs)
2007

    
2008
    def _set_history_item(self, reason, date=None):
2009
        if isinstance(reason, basestring):
2010
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2011

    
2012
        history_item = ProjectMembershipHistory(
2013
                            serial=self.id,
2014
                            person=self.person_id,
2015
                            project=self.project_id,
2016
                            date=date or datetime.now(),
2017
                            reason=reason)
2018
        history_item.save()
2019
        serial = history_item.id
2020

    
2021
    def can_accept(self):
2022
        return self.state == self.REQUESTED
2023

    
2024
    def accept(self):
2025
        if self.is_pending:
2026
            m = _("%s: attempt to accept while is pending") % (self,)
2027
            raise AssertionError(m)
2028

    
2029
        if not self.can_accept():
2030
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2031
            raise AssertionError(m)
2032

    
2033
        now = datetime.now()
2034
        self.acceptance_date = now
2035
        self._set_history_item(reason='ACCEPT', date=now)
2036
        if self.project.is_approved():
2037
            self.state = self.ACCEPTED
2038
            self.is_pending = True
2039
        else:
2040
            self.state = self.PROJECT_DEACTIVATED
2041

    
2042
        self.save()
2043

    
2044
    def can_leave(self):
2045
        return self.can_remove()
2046

    
2047
    def can_remove(self):
2048
        return self.state in self.ACCEPTED_STATES
2049

    
2050
    def remove(self):
2051
        if self.is_pending:
2052
            m = _("%s: attempt to remove while is pending") % (self,)
2053
            raise AssertionError(m)
2054

    
2055
        if not self.can_remove():
2056
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2057
            raise AssertionError(m)
2058

    
2059
        self._set_history_item(reason='REMOVE')
2060
        self.state = self.REMOVED
2061
        self.is_pending = True
2062
        self.save()
2063

    
2064
    def can_reject(self):
2065
        return self.state == self.REQUESTED
2066

    
2067
    def reject(self):
2068
        if self.is_pending:
2069
            m = _("%s: attempt to reject while is pending") % (self,)
2070
            raise AssertionError(m)
2071

    
2072
        if not self.can_reject():
2073
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2074
            raise AssertionError(m)
2075

    
2076
        # rejected requests don't need sync,
2077
        # because they were never effected
2078
        self._set_history_item(reason='REJECT')
2079
        self.delete()
2080

    
2081
    def can_cancel(self):
2082
        return self.state == self.REQUESTED
2083

    
2084
    def cancel(self):
2085
        if self.is_pending:
2086
            m = _("%s: attempt to cancel while is pending") % (self,)
2087
            raise AssertionError(m)
2088

    
2089
        if not self.can_cancel():
2090
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2091
            raise AssertionError(m)
2092

    
2093
        # rejected requests don't need sync,
2094
        # because they were never effected
2095
        self._set_history_item(reason='CANCEL')
2096
        self.delete()
2097

    
2098
    def get_diff_quotas(self, sub_list=None, add_list=None):
2099
        if sub_list is None:
2100
            sub_list = []
2101

    
2102
        if add_list is None:
2103
            add_list = []
2104

    
2105
        sub_append = sub_list.append
2106
        add_append = add_list.append
2107
        holder = self.person.uuid
2108

    
2109
        synced_application = self.application
2110
        if synced_application is not None:
2111
            cur_grants = synced_application.projectresourcegrant_set.all()
2112
            for grant in cur_grants:
2113
                sub_append(QuotaLimits(
2114
                               holder       = holder,
2115
                               resource     = str(grant.resource),
2116
                               capacity     = grant.member_capacity,
2117
                               import_limit = grant.member_import_limit,
2118
                               export_limit = grant.member_export_limit))
2119

    
2120
        pending_application = self.pending_application
2121
        if pending_application is not None:
2122
            new_grants = pending_application.projectresourcegrant_set.all()
2123
            for new_grant in new_grants:
2124
                add_append(QuotaLimits(
2125
                               holder       = holder,
2126
                               resource     = str(new_grant.resource),
2127
                               capacity     = new_grant.member_capacity,
2128
                               import_limit = new_grant.member_import_limit,
2129
                               export_limit = new_grant.member_export_limit))
2130

    
2131
        return (sub_list, add_list)
2132

    
2133
    def set_sync(self):
2134
        if not self.is_pending:
2135
            m = _("%s: attempt to sync a non pending membership") % (self,)
2136
            raise AssertionError(m)
2137

    
2138
        state = self.state
2139
        if state == self.ACCEPTED:
2140
            pending_application = self.pending_application
2141
            if pending_application is None:
2142
                m = _("%s: attempt to sync an empty pending application") % (
2143
                    self,)
2144
                raise AssertionError(m)
2145

    
2146
            self.application = pending_application
2147
            self.is_active = True
2148

    
2149
            self.pending_application = None
2150
            self.pending_serial = None
2151

    
2152
            # project.application may have changed in the meantime,
2153
            # in which case we stay PENDING;
2154
            # we are safe to check due to select_for_update
2155
            if self.application == self.project.application:
2156
                self.is_pending = False
2157
            self.save()
2158

    
2159
        elif state == self.PROJECT_DEACTIVATED:
2160
            if self.pending_application:
2161
                m = _("%s: attempt to sync in state '%s' "
2162
                      "with a pending application") % (self, state)
2163
                raise AssertionError(m)
2164

    
2165
            self.application = None
2166
            self.is_active = False
2167
            self.pending_serial = None
2168
            self.is_pending = False
2169
            self.save()
2170

    
2171
        elif state == self.REMOVED:
2172
            self.delete()
2173

    
2174
        else:
2175
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2176
            raise AssertionError(m)
2177

    
2178
    def reset_sync(self):
2179
        if not self.is_pending:
2180
            m = _("%s: attempt to reset a non pending membership") % (self,)
2181
            raise AssertionError(m)
2182

    
2183
        state = self.state
2184
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
2185
            self.pending_application = None
2186
            self.pending_serial = None
2187
            self.save()
2188
        else:
2189
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2190
            raise AssertionError(m)
2191

    
2192
class Serial(models.Model):
2193
    serial  =   models.AutoField(primary_key=True)
2194

    
2195
def new_serial():
2196
    s = Serial.objects.create()
2197
    serial = s.serial
2198
    s.delete()
2199
    return serial
2200

    
2201
def sync_finish_serials(serials_to_ack=None):
2202
    if serials_to_ack is None:
2203
        serials_to_ack = qh_query_serials([])
2204

    
2205
    serials_to_ack = set(serials_to_ack)
2206
    sfu = ProjectMembership.objects.select_for_update()
2207
    memberships = list(sfu.filter(pending_serial__isnull=False))
2208

    
2209
    if memberships:
2210
        for membership in memberships:
2211
            serial = membership.pending_serial
2212
            if serial in serials_to_ack:
2213
                membership.set_sync()
2214
            else:
2215
                membership.reset_sync()
2216

    
2217
        transaction.commit()
2218

    
2219
    qh_ack_serials(list(serials_to_ack))
2220
    return len(memberships)
2221

    
2222
def pre_sync_projects(sync=True):
2223
    ACCEPTED = ProjectMembership.ACCEPTED
2224
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2225
    psfu = Project.objects.select_for_update()
2226

    
2227
    modified = list(psfu.modified_projects())
2228
    if sync:
2229
        for project in modified:
2230
            objects = project.projectmembership_set.select_for_update()
2231

    
2232
            memberships = objects.filter(state=ACCEPTED)
2233
            for membership in memberships:
2234
                membership.is_pending = True
2235
                membership.save()
2236

    
2237
    reactivating = list(psfu.reactivating_projects())
2238
    if sync:
2239
        for project in reactivating:
2240
            objects = project.projectmembership_set.select_for_update()
2241

    
2242
            memberships = objects.filter(state=PROJECT_DEACTIVATED)
2243
            for membership in memberships:
2244
                membership.is_pending = True
2245
                membership.state = ACCEPTED
2246
                membership.save()
2247

    
2248
    deactivating = list(psfu.deactivating_projects())
2249
    if sync:
2250
        for project in deactivating:
2251
            objects = project.projectmembership_set.select_for_update()
2252

    
2253
            # Note: we keep a user-level deactivation
2254
            # (e.g. USER_SUSPENDED) intact
2255
            memberships = objects.filter(state=ACCEPTED)
2256
            for membership in memberships:
2257
                membership.is_pending = True
2258
                membership.state = PROJECT_DEACTIVATED
2259
                membership.save()
2260

    
2261
    return (modified, reactivating, deactivating)
2262

    
2263
def do_sync_projects():
2264

    
2265
    ACCEPTED = ProjectMembership.ACCEPTED
2266
    objects = ProjectMembership.objects.select_for_update()
2267

    
2268
    sub_quota, add_quota = [], []
2269

    
2270
    serial = new_serial()
2271

    
2272
    pending = objects.filter(is_pending=True)
2273
    for membership in pending:
2274

    
2275
        if membership.pending_application:
2276
            m = "%s: impossible: pending_application is not None (%s)" % (
2277
                membership, membership.pending_application)
2278
            raise AssertionError(m)
2279
        if membership.pending_serial:
2280
            m = "%s: impossible: pending_serial is not None (%s)" % (
2281
                membership, membership.pending_serial)
2282
            raise AssertionError(m)
2283

    
2284
        if membership.state == ACCEPTED:
2285
            membership.pending_application = membership.project.application
2286

    
2287
        membership.pending_serial = serial
2288
        membership.get_diff_quotas(sub_quota, add_quota)
2289
        membership.save()
2290

    
2291
    transaction.commit()
2292
    # ProjectApplication.approve() unblocks here
2293
    # and can set PENDING an already PENDING membership
2294
    # which has been scheduled to sync with the old project.application
2295
    # Need to check in ProjectMembership.set_sync()
2296

    
2297
    r = qh_add_quota(serial, sub_quota, add_quota)
2298
    if r:
2299
        m = "cannot sync serial: %d" % serial
2300
        raise RuntimeError(m)
2301

    
2302
    return serial
2303

    
2304
def post_sync_projects():
2305
    ACCEPTED = ProjectMembership.ACCEPTED
2306
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2307
    psfu = Project.objects.select_for_update()
2308

    
2309
    modified = psfu.modified_projects()
2310
    for project in modified:
2311
        objects = project.projectmembership_set.select_for_update()
2312

    
2313
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2314
        if not memberships:
2315
            project.is_modified = False
2316
            project.save()
2317

    
2318
    reactivating = psfu.reactivating_projects()
2319
    for project in reactivating:
2320
        objects = project.projectmembership_set.select_for_update()
2321
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2322
                                          Q(is_pending=True)))
2323
        if not memberships:
2324
            project.reactivate()
2325
            project.save()
2326

    
2327
    deactivating = psfu.deactivating_projects()
2328
    for project in deactivating:
2329
        objects = project.projectmembership_set.select_for_update()
2330

    
2331
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2332
                                          Q(is_pending=True)))
2333
        if not memberships:
2334
            project.deactivate()
2335
            project.save()
2336

    
2337
    transaction.commit()
2338

    
2339
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2340
    @with_lock(retries, retry_wait)
2341
    def _sync_projects(sync):
2342
        sync_finish_serials()
2343
        # Informative only -- no select_for_update()
2344
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2345

    
2346
        projects_log = pre_sync_projects(sync)
2347
        if sync:
2348
            serial = do_sync_projects()
2349
            sync_finish_serials([serial])
2350
            post_sync_projects()
2351

    
2352
        return (pending, projects_log)
2353
    return _sync_projects(sync)
2354

    
2355
def all_users_quotas(users):
2356
    quotas = {}
2357
    for user in users:
2358
        quotas[user.uuid] = user.all_quotas()
2359
    return quotas
2360

    
2361
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2362
    @with_lock(retries, retry_wait)
2363
    def _sync_users(users, sync):
2364
        sync_finish_serials()
2365

    
2366
        existing, nonexisting = qh_check_users(users)
2367
        resources = get_resource_names()
2368
        registered_quotas = qh_get_quota_limits(existing, resources)
2369
        astakos_quotas = all_users_quotas(users)
2370

    
2371
        if sync:
2372
            r = register_users(nonexisting)
2373
            r = send_quotas(astakos_quotas)
2374

    
2375
        return (existing, nonexisting, registered_quotas, astakos_quotas)
2376
    return _sync_users(users, sync)
2377

    
2378
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2379
    users = AstakosUser.objects.filter(is_active=True)
2380
    return sync_users(users, sync, retries, retry_wait)
2381

    
2382
class ProjectMembershipHistory(models.Model):
2383
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2384
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2385

    
2386
    person  =   models.BigIntegerField()
2387
    project =   models.BigIntegerField()
2388
    date    =   models.DateField(auto_now_add=True)
2389
    reason  =   models.IntegerField()
2390
    serial  =   models.BigIntegerField()
2391

    
2392
### SIGNALS ###
2393
################
2394

    
2395
def create_astakos_user(u):
2396
    try:
2397
        AstakosUser.objects.get(user_ptr=u.pk)
2398
    except AstakosUser.DoesNotExist:
2399
        extended_user = AstakosUser(user_ptr_id=u.pk)
2400
        extended_user.__dict__.update(u.__dict__)
2401
        extended_user.save()
2402
        if not extended_user.has_auth_provider('local'):
2403
            extended_user.add_auth_provider('local')
2404
    except BaseException, e:
2405
        logger.exception(e)
2406

    
2407

    
2408
def fix_superusers(sender, **kwargs):
2409
    # Associate superusers with AstakosUser
2410
    admins = User.objects.filter(is_superuser=True)
2411
    for u in admins:
2412
        create_astakos_user(u)
2413
post_syncdb.connect(fix_superusers)
2414

    
2415

    
2416
def user_post_save(sender, instance, created, **kwargs):
2417
    if not created:
2418
        return
2419
    create_astakos_user(instance)
2420
post_save.connect(user_post_save, sender=User)
2421

    
2422
def astakosuser_post_save(sender, instance, created, **kwargs):
2423
    pass
2424

    
2425
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2426

    
2427
def resource_post_save(sender, instance, created, **kwargs):
2428
    pass
2429

    
2430
post_save.connect(resource_post_save, sender=Resource)
2431

    
2432
def renew_token(sender, instance, **kwargs):
2433
    if not instance.auth_token:
2434
        instance.renew_token()
2435
pre_save.connect(renew_token, sender=AstakosUser)
2436
pre_save.connect(renew_token, sender=Service)
2437