Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 033f2822

History | View | Annotate | Download (81.6 kB)

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

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

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

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

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

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

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

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

    
87
logger = logging.getLogger(__name__)
88

    
89
DEFAULT_CONTENT_TYPE = None
90
_content_type = None
91

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

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

    
104
RESOURCE_SEPARATOR = '.'
105

    
106
inf = float('inf')
107

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

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

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

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

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

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

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

    
147

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
217

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

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

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

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

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

    
260
    return _DEFAULT_QUOTA
261

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

    
268

    
269
class AstakosUserManager(UserManager):
270

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

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

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

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

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

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

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

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

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

    
321

    
322

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

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

    
339

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

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

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

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

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

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

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

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

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

    
375
    objects = AstakosUserManager()
376

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
504
        self.update_uuid()
505

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
610
        return True
611

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

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

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

    
623
        return provider.is_available_for_remove()
624

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

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

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

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

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

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

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

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

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

    
679
        pending.delete()
680
        return provider
681

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
791
    def membership_display(self, project):
792
        m = self.get_membership(project)
793
        if m is None:
794
            return _('Not a member')
795
        else:
796
            return m.user_friendly_state_display()
797

    
798
    def non_owner_can_view(self, maybe_project):
799
        if maybe_project is None:
800
            return False
801
        project = maybe_project
802
        if self.is_associated(project):
803
            return True
804
        if project.is_deactivated():
805
            return False
806
        return True
807

    
808

    
809
class AstakosUserAuthProviderManager(models.Manager):
810

    
811
    def active(self, **filters):
812
        return self.filter(active=True, **filters)
813

    
814
    def remove_unverified_providers(self, provider, **filters):
815
        try:
816
            existing = self.filter(module=provider, user__email_verified=False, **filters)
817
            for p in existing:
818
                p.user.delete()
819
        except:
820
            pass
821

    
822

    
823

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

    
842
    objects = AstakosUserAuthProviderManager()
843

    
844
    class Meta:
845
        unique_together = (('identifier', 'module', 'user'), )
846
        ordering = ('module', 'created')
847

    
848
    def __init__(self, *args, **kwargs):
849
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
850
        try:
851
            self.info = json.loads(self.info_data)
852
            if not self.info:
853
                self.info = {}
854
        except Exception, e:
855
            self.info = {}
856

    
857
        for key,value in self.info.iteritems():
858
            setattr(self, 'info_%s' % key, value)
859

    
860

    
861
    @property
862
    def settings(self):
863
        return auth_providers.get_provider(self.module)
864

    
865
    @property
866
    def details_display(self):
867
        try:
868
            params = self.user.__dict__
869
            params.update(self.__dict__)
870
            return self.settings.get_details_tpl_display % params
871
        except:
872
            return ''
873

    
874
    @property
875
    def title_display(self):
876
        title_tpl = self.settings.get_title_display
877
        try:
878
            if self.settings.get_user_title_display:
879
                title_tpl = self.settings.get_user_title_display
880
        except Exception, e:
881
            pass
882
        try:
883
          return title_tpl % self.__dict__
884
        except:
885
          return self.settings.get_title_display % self.__dict__
886

    
887
    def can_remove(self):
888
        return self.user.can_remove_auth_provider(self.module)
889

    
890
    def delete(self, *args, **kwargs):
891
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
892
        if self.module == 'local':
893
            self.user.set_unusable_password()
894
            self.user.save()
895
        return ret
896

    
897
    def __repr__(self):
898
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
899

    
900
    def __unicode__(self):
901
        if self.identifier:
902
            return "%s:%s" % (self.module, self.identifier)
903
        if self.auth_backend:
904
            return "%s:%s" % (self.module, self.auth_backend)
905
        return self.module
906

    
907
    def save(self, *args, **kwargs):
908
        self.info_data = json.dumps(self.info)
909
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
910

    
911

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

    
939
    update_or_create = _update_or_create
940

    
941

    
942
class AstakosUserQuota(models.Model):
943
    objects = ExtendedManager()
944
    capacity = intDecimalField()
945
    quantity = intDecimalField(default=0)
946
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
947
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
948
    resource = models.ForeignKey(Resource)
949
    user = models.ForeignKey(AstakosUser)
950

    
951
    class Meta:
952
        unique_together = ("resource", "user")
953

    
954
    def quota_values(self):
955
        return QuotaValues(
956
            quantity = self.quantity,
957
            capacity = self.capacity,
958
            import_limit = self.import_limit,
959
            export_limit = self.export_limit)
960

    
961

    
962
class ApprovalTerms(models.Model):
963
    """
964
    Model for approval terms
965
    """
966

    
967
    date = models.DateTimeField(
968
        _('Issue date'), db_index=True, auto_now_add=True)
969
    location = models.CharField(_('Terms location'), max_length=255)
970

    
971

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

    
985
    def __init__(self, *args, **kwargs):
986
        super(Invitation, self).__init__(*args, **kwargs)
987
        if not self.id:
988
            self.code = _generate_invitation_code()
989

    
990
    def consume(self):
991
        self.is_consumed = True
992
        self.consumed = datetime.now()
993
        self.save()
994

    
995
    def __unicode__(self):
996
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
997

    
998

    
999
class EmailChangeManager(models.Manager):
1000

    
1001
    @transaction.commit_on_success
1002
    def change_email(self, activation_key):
1003
        """
1004
        Validate an activation key and change the corresponding
1005
        ``User`` if valid.
1006

1007
        If the key is valid and has not expired, return the ``User``
1008
        after activating.
1009

1010
        If the key is not valid or has expired, return ``None``.
1011

1012
        If the key is valid but the ``User`` is already active,
1013
        return ``None``.
1014

1015
        After successful email change the activation record is deleted.
1016

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

    
1045

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

    
1058
    objects = EmailChangeManager()
1059

    
1060
    def get_url(self):
1061
        return reverse('email_change_confirm',
1062
                      kwargs={'activation_key': self.activation_key})
1063

    
1064
    def activation_key_expired(self):
1065
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1066
        return self.requested_at + expiration_date < datetime.now()
1067

    
1068

    
1069
class AdditionalMail(models.Model):
1070
    """
1071
    Model for registring invitations
1072
    """
1073
    owner = models.ForeignKey(AstakosUser)
1074
    email = models.EmailField()
1075

    
1076

    
1077
def _generate_invitation_code():
1078
    while True:
1079
        code = randint(1, 2L ** 63 - 1)
1080
        try:
1081
            Invitation.objects.get(code=code)
1082
            # An invitation with this code already exists, try again
1083
        except Invitation.DoesNotExist:
1084
            return code
1085

    
1086

    
1087
def get_latest_terms():
1088
    try:
1089
        term = ApprovalTerms.objects.order_by('-id')[0]
1090
        return term
1091
    except IndexError:
1092
        pass
1093
    return None
1094

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

    
1113
    class Meta:
1114
        unique_together = ("provider", "third_party_identifier")
1115

    
1116
    def get_user_instance(self):
1117
        d = self.__dict__
1118
        d.pop('_state', None)
1119
        d.pop('id', None)
1120
        d.pop('token', None)
1121
        d.pop('created', None)
1122
        d.pop('info', None)
1123
        user = AstakosUser(**d)
1124

    
1125
        return user
1126

    
1127
    @property
1128
    def realname(self):
1129
        return '%s %s' %(self.first_name, self.last_name)
1130

    
1131
    @realname.setter
1132
    def realname(self, value):
1133
        parts = value.split(' ')
1134
        if len(parts) == 2:
1135
            self.first_name = parts[0]
1136
            self.last_name = parts[1]
1137
        else:
1138
            self.last_name = parts[0]
1139

    
1140
    def save(self, **kwargs):
1141
        if not self.id:
1142
            # set username
1143
            while not self.username:
1144
                username =  uuid.uuid4().hex[:30]
1145
                try:
1146
                    AstakosUser.objects.get(username = username)
1147
                except AstakosUser.DoesNotExist, e:
1148
                    self.username = username
1149
        super(PendingThirdPartyUser, self).save(**kwargs)
1150

    
1151
    def generate_token(self):
1152
        self.password = self.third_party_identifier
1153
        self.last_login = datetime.now()
1154
        self.token = default_token_generator.make_token(self)
1155

    
1156
class SessionCatalog(models.Model):
1157
    session_key = models.CharField(_('session key'), max_length=40)
1158
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1159

    
1160

    
1161
### PROJECTS ###
1162
################
1163

    
1164
def synced_model_metaclass(class_name, class_parents, class_attributes):
1165

    
1166
    new_attributes = {}
1167
    sync_attributes = {}
1168

    
1169
    for name, value in class_attributes.iteritems():
1170
        sync, underscore, rest = name.partition('_')
1171
        if sync == 'sync' and underscore == '_':
1172
            sync_attributes[rest] = value
1173
        else:
1174
            new_attributes[name] = value
1175

    
1176
    if 'prefix' not in sync_attributes:
1177
        m = ("you did not specify a 'sync_prefix' attribute "
1178
             "in class '%s'" % (class_name,))
1179
        raise ValueError(m)
1180

    
1181
    prefix = sync_attributes.pop('prefix')
1182
    class_name = sync_attributes.pop('classname', prefix + '_model')
1183

    
1184
    for name, value in sync_attributes.iteritems():
1185
        newname = prefix + '_' + name
1186
        if newname in new_attributes:
1187
            m = ("class '%s' was specified with prefix '%s' "
1188
                 "but it already has an attribute named '%s'"
1189
                 % (class_name, prefix, newname))
1190
            raise ValueError(m)
1191

    
1192
        new_attributes[newname] = value
1193

    
1194
    newclass = type(class_name, class_parents, new_attributes)
1195
    return newclass
1196

    
1197

    
1198
def make_synced(prefix='sync', name='SyncedState'):
1199

    
1200
    the_name = name
1201
    the_prefix = prefix
1202

    
1203
    class SyncedState(models.Model):
1204

    
1205
        sync_classname      = the_name
1206
        sync_prefix         = the_prefix
1207
        __metaclass__       = synced_model_metaclass
1208

    
1209
        sync_new_state      = models.BigIntegerField(null=True)
1210
        sync_synced_state   = models.BigIntegerField(null=True)
1211
        STATUS_SYNCED       = 0
1212
        STATUS_PENDING      = 1
1213
        sync_status         = models.IntegerField(db_index=True)
1214

    
1215
        class Meta:
1216
            abstract = True
1217

    
1218
        class NotSynced(Exception):
1219
            pass
1220

    
1221
        def sync_init_state(self, state):
1222
            self.sync_synced_state = state
1223
            self.sync_new_state = state
1224
            self.sync_status = self.STATUS_SYNCED
1225

    
1226
        def sync_get_status(self):
1227
            return self.sync_status
1228

    
1229
        def sync_set_status(self):
1230
            if self.sync_new_state != self.sync_synced_state:
1231
                self.sync_status = self.STATUS_PENDING
1232
            else:
1233
                self.sync_status = self.STATUS_SYNCED
1234

    
1235
        def sync_set_synced(self):
1236
            self.sync_synced_state = self.sync_new_state
1237
            self.sync_status = self.STATUS_SYNCED
1238

    
1239
        def sync_get_synced_state(self):
1240
            return self.sync_synced_state
1241

    
1242
        def sync_set_new_state(self, new_state):
1243
            self.sync_new_state = new_state
1244
            self.sync_set_status()
1245

    
1246
        def sync_get_new_state(self):
1247
            return self.sync_new_state
1248

    
1249
        def sync_set_synced_state(self, synced_state):
1250
            self.sync_synced_state = synced_state
1251
            self.sync_set_status()
1252

    
1253
        def sync_get_pending_objects(self):
1254
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1255
            return self.objects.filter(**kw)
1256

    
1257
        def sync_get_synced_objects(self):
1258
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1259
            return self.objects.filter(**kw)
1260

    
1261
        def sync_verify_get_synced_state(self):
1262
            status = self.sync_get_status()
1263
            state = self.sync_get_synced_state()
1264
            verified = (status == self.STATUS_SYNCED)
1265
            return state, verified
1266

    
1267
        def sync_is_synced(self):
1268
            state, verified = self.sync_verify_get_synced_state()
1269
            return verified
1270

    
1271
    return SyncedState
1272

    
1273
SyncedState = make_synced(prefix='sync', name='SyncedState')
1274

    
1275

    
1276
class Chain(models.Model):
1277
    chain  =   models.AutoField(primary_key=True)
1278

    
1279
    def __str__(self):
1280
        return "%s" % (self.chain,)
1281

    
1282
def new_chain():
1283
    c = Chain.objects.create()
1284
    return c
1285

    
1286

    
1287
class ProjectApplicationManager(ForUpdateManager):
1288

    
1289
    def user_visible_projects(self, *filters, **kw_filters):
1290
        model = self.model
1291
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1292

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

    
1301
    def user_accessible_projects(self, user):
1302
        """
1303
        Return projects accessed by specified user.
1304
        """
1305
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1306
                               Q(project__projectmembership__person=user)
1307

    
1308
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1309

    
1310
    def search_by_name(self, *search_strings):
1311
        q = Q()
1312
        for s in search_strings:
1313
            q = q | Q(name__icontains=s)
1314
        return self.filter(q)
1315

    
1316
    def latest_of_chain(self, chain_id):
1317
        try:
1318
            return self.filter(chain=chain_id).order_by('-id')[0]
1319
        except IndexError:
1320
            return None
1321

    
1322

    
1323
class ProjectApplication(models.Model):
1324
    applicant               =   models.ForeignKey(
1325
                                    AstakosUser,
1326
                                    related_name='projects_applied',
1327
                                    db_index=True)
1328

    
1329
    PENDING     =    0
1330
    APPROVED    =    1
1331
    REPLACED    =    2
1332
    DENIED      =    3
1333
    DISMISSED   =    4
1334
    CANCELLED   =    5
1335

    
1336
    state                   =   models.IntegerField(default=PENDING,
1337
                                                    db_index=True)
1338

    
1339
    owner                   =   models.ForeignKey(
1340
                                    AstakosUser,
1341
                                    related_name='projects_owned',
1342
                                    db_index=True)
1343

    
1344
    chain                   =   models.ForeignKey(Chain,
1345
                                                  related_name='chained_apps',
1346
                                                  db_column='chain')
1347
    precursor_application   =   models.ForeignKey('ProjectApplication',
1348
                                                  null=True,
1349
                                                  blank=True)
1350

    
1351
    name                    =   models.CharField(max_length=80)
1352
    homepage                =   models.URLField(max_length=255, null=True,
1353
                                                verify_exists=False)
1354
    description             =   models.TextField(null=True, blank=True)
1355
    start_date              =   models.DateTimeField(null=True, blank=True)
1356
    end_date                =   models.DateTimeField()
1357
    member_join_policy      =   models.IntegerField()
1358
    member_leave_policy     =   models.IntegerField()
1359
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1360
    resource_grants         =   models.ManyToManyField(
1361
                                    Resource,
1362
                                    null=True,
1363
                                    blank=True,
1364
                                    through='ProjectResourceGrant')
1365
    comments                =   models.TextField(null=True, blank=True)
1366
    issue_date              =   models.DateTimeField(auto_now_add=True)
1367
    response_date           =   models.DateTimeField(null=True, blank=True)
1368

    
1369
    objects                 =   ProjectApplicationManager()
1370

    
1371
    # Compiled queries
1372
    Q_PENDING  = Q(state=PENDING)
1373
    Q_APPROVED = Q(state=APPROVED)
1374
    Q_DENIED   = Q(state=DENIED)
1375

    
1376
    class Meta:
1377
        unique_together = ("chain", "id")
1378

    
1379
    def __unicode__(self):
1380
        return "%s applied by %s" % (self.name, self.applicant)
1381

    
1382
    # TODO: Move to a more suitable place
1383
    APPLICATION_STATE_DISPLAY = {
1384
        PENDING  : _('Pending review'),
1385
        APPROVED : _('Approved'),
1386
        REPLACED : _('Replaced'),
1387
        DENIED   : _('Denied'),
1388
        DISMISSED: _('Dismissed'),
1389
        CANCELLED: _('Cancelled')
1390
    }
1391

    
1392
    def get_project(self):
1393
        try:
1394
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1395
            return Project
1396
        except Project.DoesNotExist, e:
1397
            return None
1398

    
1399
    def state_display(self):
1400
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1401

    
1402
    def project_state_display(self):
1403
        try:
1404
            project = self.project
1405
            return project.state_display()
1406
        except Project.DoesNotExist:
1407
            return self.state_display()
1408

    
1409
    def add_resource_policy(self, service, resource, uplimit):
1410
        """Raises ObjectDoesNotExist, IntegrityError"""
1411
        q = self.projectresourcegrant_set
1412
        resource = Resource.objects.get(service__name=service, name=resource)
1413
        q.create(resource=resource, member_capacity=uplimit)
1414

    
1415
    def members_count(self):
1416
        return self.project.approved_memberships.count()
1417

    
1418
    @property
1419
    def grants(self):
1420
        return self.projectresourcegrant_set.values(
1421
            'member_capacity', 'resource__name', 'resource__service__name')
1422

    
1423
    @property
1424
    def resource_policies(self):
1425
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1426

    
1427
    @resource_policies.setter
1428
    def resource_policies(self, policies):
1429
        for p in policies:
1430
            service = p.get('service', None)
1431
            resource = p.get('resource', None)
1432
            uplimit = p.get('uplimit', 0)
1433
            self.add_resource_policy(service, resource, uplimit)
1434

    
1435
    def pending_modifications_incl_me(self):
1436
        q = self.chained_applications()
1437
        q = q.filter(Q(state=self.PENDING))
1438
        return q
1439

    
1440
    def last_pending_incl_me(self):
1441
        try:
1442
            return self.pending_modifications_incl_me().order_by('-id')[0]
1443
        except IndexError:
1444
            return None
1445

    
1446
    def pending_modifications(self):
1447
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1448

    
1449
    def last_pending(self):
1450
        try:
1451
            return self.pending_modifications().order_by('-id')[0]
1452
        except IndexError:
1453
            return None
1454

    
1455
    def is_modification(self):
1456
        # if self.state != self.PENDING:
1457
        #     return False
1458
        parents = self.chained_applications().filter(id__lt=self.id)
1459
        parents = parents.filter(state__in=[self.APPROVED])
1460
        return parents.count() > 0
1461

    
1462
    def chained_applications(self):
1463
        return ProjectApplication.objects.filter(chain=self.chain)
1464

    
1465
    def has_pending_modifications(self):
1466
        return bool(self.last_pending())
1467

    
1468
    def get_project(self):
1469
        try:
1470
            return Project.objects.get(id=self.chain)
1471
        except Project.DoesNotExist:
1472
            return None
1473

    
1474
    def project_exists(self):
1475
        return self.get_project() is not None
1476

    
1477
    def _get_project_for_update(self):
1478
        try:
1479
            objects = Project.objects.select_for_update()
1480
            project = objects.get(id=self.chain)
1481
            return project
1482
        except Project.DoesNotExist:
1483
            return None
1484

    
1485
    def can_cancel(self):
1486
        return self.state == self.PENDING
1487

    
1488
    def cancel(self):
1489
        if not self.can_cancel():
1490
            m = _("cannot cancel: application '%s' in state '%s'") % (
1491
                    self.id, self.state)
1492
            raise AssertionError(m)
1493

    
1494
        self.state = self.CANCELLED
1495
        self.save()
1496

    
1497
    def can_dismiss(self):
1498
        return self.state == self.DENIED
1499

    
1500
    def dismiss(self):
1501
        if not self.can_dismiss():
1502
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1503
                    self.id, self.state)
1504
            raise AssertionError(m)
1505

    
1506
        self.state = self.DISMISSED
1507
        self.save()
1508

    
1509
    def can_deny(self):
1510
        return self.state == self.PENDING
1511

    
1512
    def deny(self):
1513
        if not self.can_deny():
1514
            m = _("cannot deny: application '%s' in state '%s'") % (
1515
                    self.id, self.state)
1516
            raise AssertionError(m)
1517

    
1518
        self.state = self.DENIED
1519
        self.response_date = datetime.now()
1520
        self.save()
1521

    
1522
    def can_approve(self):
1523
        return self.state == self.PENDING
1524

    
1525
    def approve(self, approval_user=None):
1526
        """
1527
        If approval_user then during owner membership acceptance
1528
        it is checked whether the request_user is eligible.
1529

1530
        Raises:
1531
            PermissionDenied
1532
        """
1533

    
1534
        if not transaction.is_managed():
1535
            raise AssertionError("NOPE")
1536

    
1537
        new_project_name = self.name
1538
        if not self.can_approve():
1539
            m = _("cannot approve: project '%s' in state '%s'") % (
1540
                    new_project_name, self.state)
1541
            raise AssertionError(m) # invalid argument
1542

    
1543
        now = datetime.now()
1544
        project = self._get_project_for_update()
1545

    
1546
        try:
1547
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1548
            conflicting_project = Project.objects.get(q)
1549
            if (conflicting_project != project):
1550
                m = (_("cannot approve: project with name '%s' "
1551
                       "already exists (id: %s)") % (
1552
                        new_project_name, conflicting_project.id))
1553
                raise PermissionDenied(m) # invalid argument
1554
        except Project.DoesNotExist:
1555
            pass
1556

    
1557
        new_project = False
1558
        if project is None:
1559
            new_project = True
1560
            project = Project(id=self.chain)
1561

    
1562
        project.name = new_project_name
1563
        project.application = self
1564
        project.last_approval_date = now
1565
        if not new_project:
1566
            project.is_modified = True
1567

    
1568
        project.save()
1569

    
1570
        self.state = self.APPROVED
1571
        self.response_date = now
1572
        self.save()
1573

    
1574
    @property
1575
    def member_join_policy_display(self):
1576
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1577

    
1578
    @property
1579
    def member_leave_policy_display(self):
1580
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1581

    
1582
class ProjectResourceGrant(models.Model):
1583

    
1584
    resource                =   models.ForeignKey(Resource)
1585
    project_application     =   models.ForeignKey(ProjectApplication,
1586
                                                  null=True)
1587
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1588
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1589
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1590
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1591
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1592
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1593

    
1594
    objects = ExtendedManager()
1595

    
1596
    class Meta:
1597
        unique_together = ("resource", "project_application")
1598

    
1599
    def member_quota_values(self):
1600
        return QuotaValues(
1601
            quantity = 0,
1602
            capacity = self.member_capacity,
1603
            import_limit = self.member_import_limit,
1604
            export_limit = self.member_export_limit)
1605

    
1606
    def display_member_capacity(self):
1607
        if self.member_capacity:
1608
            if self.resource.unit:
1609
                return ProjectResourceGrant.display_filesize(
1610
                    self.member_capacity)
1611
            else:
1612
                if math.isinf(self.member_capacity):
1613
                    return 'Unlimited'
1614
                else:
1615
                    return self.member_capacity
1616
        else:
1617
            return 'Unlimited'
1618

    
1619
    def __str__(self):
1620
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1621
                                        self.display_member_capacity())
1622

    
1623
    @classmethod
1624
    def display_filesize(cls, value):
1625
        try:
1626
            value = float(value)
1627
        except:
1628
            return
1629
        else:
1630
            if math.isinf(value):
1631
                return 'Unlimited'
1632
            if value > 1:
1633
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1634
                                [0, 0, 0, 0, 0, 0])
1635
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1636
                quotient = float(value) / 1024**exponent
1637
                unit, value_decimals = unit_list[exponent]
1638
                format_string = '{0:.%sf} {1}' % (value_decimals)
1639
                return format_string.format(quotient, unit)
1640
            if value == 0:
1641
                return '0 bytes'
1642
            if value == 1:
1643
                return '1 byte'
1644
            else:
1645
               return '0'
1646

    
1647

    
1648
class ProjectManager(ForUpdateManager):
1649

    
1650
    def terminated_projects(self):
1651
        q = self.model.Q_TERMINATED
1652
        return self.filter(q)
1653

    
1654
    def not_terminated_projects(self):
1655
        q = ~self.model.Q_TERMINATED
1656
        return self.filter(q)
1657

    
1658
    def terminating_projects(self):
1659
        q = self.model.Q_TERMINATED & Q(is_active=True)
1660
        return self.filter(q)
1661

    
1662
    def deactivated_projects(self):
1663
        q = self.model.Q_DEACTIVATED
1664
        return self.filter(q)
1665

    
1666
    def deactivating_projects(self):
1667
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1668
        return self.filter(q)
1669

    
1670
    def modified_projects(self):
1671
        return self.filter(is_modified=True)
1672

    
1673
    def reactivating_projects(self):
1674
        return self.filter(state=Project.APPROVED, is_active=False)
1675

    
1676
    def expired_projects(self):
1677
        q = (~Q(state=Project.TERMINATED) &
1678
              Q(application__end_date__lt=datetime.now()))
1679
        return self.filter(q)
1680

    
1681
    def search_by_name(self, *search_strings):
1682
        q = Q()
1683
        for s in search_strings:
1684
            q = q | Q(name__icontains=s)
1685
        return self.filter(q)
1686

    
1687

    
1688
class Project(models.Model):
1689

    
1690
    id                          =   models.OneToOneField(Chain,
1691
                                                      related_name='chained_project',
1692
                                                      db_column='id',
1693
                                                      primary_key=True)
1694

    
1695
    application                 =   models.OneToOneField(
1696
                                            ProjectApplication,
1697
                                            related_name='project')
1698
    last_approval_date          =   models.DateTimeField(null=True)
1699

    
1700
    members                     =   models.ManyToManyField(
1701
                                            AstakosUser,
1702
                                            through='ProjectMembership')
1703

    
1704
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1705
    deactivation_date           =   models.DateTimeField(null=True)
1706

    
1707
    creation_date               =   models.DateTimeField(auto_now_add=True)
1708
    name                        =   models.CharField(
1709
                                            max_length=80,
1710
                                            null=True,
1711
                                            db_index=True,
1712
                                            unique=True)
1713

    
1714
    APPROVED    = 1
1715
    SUSPENDED   = 10
1716
    TERMINATED  = 100
1717

    
1718
    is_modified                 =   models.BooleanField(default=False,
1719
                                                        db_index=True)
1720
    is_active                   =   models.BooleanField(default=True,
1721
                                                        db_index=True)
1722
    state                       =   models.IntegerField(default=APPROVED,
1723
                                                        db_index=True)
1724

    
1725
    objects     =   ProjectManager()
1726

    
1727
    # Compiled queries
1728
    Q_TERMINATED  = Q(state=TERMINATED)
1729
    Q_SUSPENDED   = Q(state=SUSPENDED)
1730
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1731

    
1732
    def __str__(self):
1733
        return _("<project %s '%s'>") % (self.id, self.application.name)
1734

    
1735
    __repr__ = __str__
1736

    
1737
    STATE_DISPLAY = {
1738
        APPROVED   : 'Active',
1739
        SUSPENDED  : 'Suspended',
1740
        TERMINATED : 'Terminated'
1741
        }
1742

    
1743
    def state_display(self):
1744
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1745

    
1746
    def admin_state_display(self):
1747
        s = self.state_display()
1748
        if self.sync_pending():
1749
            s += ' (sync pending)'
1750
        return s
1751

    
1752
    def sync_pending(self):
1753
        if self.state != self.APPROVED:
1754
            return self.is_active
1755
        return not self.is_active or self.is_modified
1756

    
1757
    def expiration_info(self):
1758
        return (str(self.id), self.name, self.state_display(),
1759
                str(self.application.end_date))
1760

    
1761
    def is_deactivated(self, reason=None):
1762
        if reason is not None:
1763
            return self.state == reason
1764

    
1765
        return self.state != self.APPROVED
1766

    
1767
    def is_deactivating(self, reason=None):
1768
        if not self.is_active:
1769
            return False
1770

    
1771
        return self.is_deactivated(reason)
1772

    
1773
    def is_deactivated_strict(self, reason=None):
1774
        if self.is_active:
1775
            return False
1776

    
1777
        return self.is_deactivated(reason)
1778

    
1779
    ### Deactivation calls
1780

    
1781
    def deactivate(self):
1782
        self.deactivation_date = datetime.now()
1783
        self.is_active = False
1784

    
1785
    def reactivate(self):
1786
        self.deactivation_date = None
1787
        self.is_active = True
1788

    
1789
    def terminate(self):
1790
        self.deactivation_reason = 'TERMINATED'
1791
        self.state = self.TERMINATED
1792
        self.name = None
1793
        self.save()
1794

    
1795
    def suspend(self):
1796
        self.deactivation_reason = 'SUSPENDED'
1797
        self.state = self.SUSPENDED
1798
        self.save()
1799

    
1800
    def resume(self):
1801
        self.deactivation_reason = None
1802
        self.state = self.APPROVED
1803
        self.save()
1804

    
1805
    ### Logical checks
1806

    
1807
    def is_inconsistent(self):
1808
        now = datetime.now()
1809
        dates = [self.creation_date,
1810
                 self.last_approval_date,
1811
                 self.deactivation_date]
1812
        return any([date > now for date in dates])
1813

    
1814
    def is_active_strict(self):
1815
        return self.is_active and self.state == self.APPROVED
1816

    
1817
    def is_approved(self):
1818
        return self.state == self.APPROVED
1819

    
1820
    @property
1821
    def is_alive(self):
1822
        return not self.is_terminated
1823

    
1824
    @property
1825
    def is_terminated(self):
1826
        return self.is_deactivated(self.TERMINATED)
1827

    
1828
    @property
1829
    def is_suspended(self):
1830
        return self.is_deactivated(self.SUSPENDED)
1831

    
1832
    def violates_resource_grants(self):
1833
        return False
1834

    
1835
    def violates_members_limit(self, adding=0):
1836
        application = self.application
1837
        limit = application.limit_on_members_number
1838
        if limit is None:
1839
            return False
1840
        return (len(self.approved_members) + adding > limit)
1841

    
1842

    
1843
    ### Other
1844

    
1845
    def count_pending_memberships(self):
1846
        memb_set = self.projectmembership_set
1847
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1848
        return memb_count
1849

    
1850
    def members_count(self):
1851
        return self.approved_memberships.count()
1852

    
1853
    @property
1854
    def approved_memberships(self):
1855
        query = ProjectMembership.Q_ACCEPTED_STATES
1856
        return self.projectmembership_set.filter(query)
1857

    
1858
    @property
1859
    def approved_members(self):
1860
        return [m.person for m in self.approved_memberships]
1861

    
1862
    def add_member(self, user):
1863
        """
1864
        Raises:
1865
            django.exceptions.PermissionDenied
1866
            astakos.im.models.AstakosUser.DoesNotExist
1867
        """
1868
        if isinstance(user, int):
1869
            user = AstakosUser.objects.get(user=user)
1870

    
1871
        m, created = ProjectMembership.objects.get_or_create(
1872
            person=user, project=self
1873
        )
1874
        m.accept()
1875

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

    
1886
        m = ProjectMembership.objects.get(person=user, project=self)
1887
        m.remove()
1888

    
1889

    
1890
class PendingMembershipError(Exception):
1891
    pass
1892

    
1893

    
1894
class ProjectMembershipManager(ForUpdateManager):
1895

    
1896
    def any_accepted(self):
1897
        q = (Q(state=ProjectMembership.ACCEPTED) |
1898
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
1899
        return self.filter(q)
1900

    
1901
    def requested(self):
1902
        return self.filter(state=ProjectMembership.REQUESTED)
1903

    
1904
    def suspended(self):
1905
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1906

    
1907
class ProjectMembership(models.Model):
1908

    
1909
    person              =   models.ForeignKey(AstakosUser)
1910
    request_date        =   models.DateField(auto_now_add=True)
1911
    project             =   models.ForeignKey(Project)
1912

    
1913
    REQUESTED           =   0
1914
    ACCEPTED            =   1
1915
    # User deactivation
1916
    USER_SUSPENDED      =   10
1917
    # Project deactivation
1918
    PROJECT_DEACTIVATED =   100
1919

    
1920
    REMOVED             =   200
1921

    
1922
    ASSOCIATED_STATES   =   set([REQUESTED,
1923
                                 ACCEPTED,
1924
                                 USER_SUSPENDED,
1925
                                 PROJECT_DEACTIVATED])
1926

    
1927
    ACCEPTED_STATES     =   set([ACCEPTED,
1928
                                 USER_SUSPENDED,
1929
                                 PROJECT_DEACTIVATED])
1930

    
1931
    state               =   models.IntegerField(default=REQUESTED,
1932
                                                db_index=True)
1933
    is_pending          =   models.BooleanField(default=False, db_index=True)
1934
    is_active           =   models.BooleanField(default=False, db_index=True)
1935
    application         =   models.ForeignKey(
1936
                                ProjectApplication,
1937
                                null=True,
1938
                                related_name='memberships')
1939
    pending_application =   models.ForeignKey(
1940
                                ProjectApplication,
1941
                                null=True,
1942
                                related_name='pending_memberships')
1943
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1944

    
1945
    acceptance_date     =   models.DateField(null=True, db_index=True)
1946
    leave_request_date  =   models.DateField(null=True)
1947

    
1948
    objects     =   ProjectMembershipManager()
1949

    
1950
    # Compiled queries
1951
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1952

    
1953
    MEMBERSHIP_STATE_DISPLAY = {
1954
        REQUESTED           : _('Requested'),
1955
        ACCEPTED            : _('Accepted'),
1956
        USER_SUSPENDED      : _('Suspended'),
1957
        PROJECT_DEACTIVATED : _('Accepted'), # sic
1958
        REMOVED             : _('Pending removal'),
1959
        }
1960

    
1961
    USER_FRIENDLY_STATE_DISPLAY = {
1962
        REQUESTED           : _('Join requested'),
1963
        ACCEPTED            : _('Accepted member'),
1964
        USER_SUSPENDED      : _('Suspended member'),
1965
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
1966
        REMOVED             : _('Pending removal'),
1967
        }
1968

    
1969
    def state_display(self):
1970
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1971

    
1972
    def user_friendly_state_display(self):
1973
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1974

    
1975
    def get_combined_state(self):
1976
        return self.state, self.is_active, self.is_pending
1977

    
1978
    class Meta:
1979
        unique_together = ("person", "project")
1980
        #index_together = [["project", "state"]]
1981

    
1982
    def __str__(self):
1983
        return _("<'%s' membership in '%s'>") % (
1984
                self.person.username, self.project)
1985

    
1986
    __repr__ = __str__
1987

    
1988
    def __init__(self, *args, **kwargs):
1989
        self.state = self.REQUESTED
1990
        super(ProjectMembership, self).__init__(*args, **kwargs)
1991

    
1992
    def _set_history_item(self, reason, date=None):
1993
        if isinstance(reason, basestring):
1994
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1995

    
1996
        history_item = ProjectMembershipHistory(
1997
                            serial=self.id,
1998
                            person=self.person_id,
1999
                            project=self.project_id,
2000
                            date=date or datetime.now(),
2001
                            reason=reason)
2002
        history_item.save()
2003
        serial = history_item.id
2004

    
2005
    def can_accept(self):
2006
        return self.state == self.REQUESTED
2007

    
2008
    def accept(self):
2009
        if self.is_pending:
2010
            m = _("%s: attempt to accept while is pending") % (self,)
2011
            raise AssertionError(m)
2012

    
2013
        if not self.can_accept():
2014
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2015
            raise AssertionError(m)
2016

    
2017
        now = datetime.now()
2018
        self.acceptance_date = now
2019
        self._set_history_item(reason='ACCEPT', date=now)
2020
        if self.project.is_approved():
2021
            self.state = self.ACCEPTED
2022
            self.is_pending = True
2023
        else:
2024
            self.state = self.PROJECT_DEACTIVATED
2025

    
2026
        self.save()
2027

    
2028
    def can_leave(self):
2029
        return self.can_remove()
2030

    
2031
    def can_remove(self):
2032
        return self.state in self.ACCEPTED_STATES
2033

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

    
2039
        if not self.can_remove():
2040
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2041
            raise AssertionError(m)
2042

    
2043
        self._set_history_item(reason='REMOVE')
2044
        self.state = self.REMOVED
2045
        self.is_pending = True
2046
        self.save()
2047

    
2048
    def can_reject(self):
2049
        return self.state == self.REQUESTED
2050

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

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

    
2060
        # rejected requests don't need sync,
2061
        # because they were never effected
2062
        self._set_history_item(reason='REJECT')
2063
        self.delete()
2064

    
2065
    def can_cancel(self):
2066
        return self.state == self.REQUESTED
2067

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

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

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

    
2082
    def get_diff_quotas(self, sub_list=None, add_list=None):
2083
        if sub_list is None:
2084
            sub_list = []
2085

    
2086
        if add_list is None:
2087
            add_list = []
2088

    
2089
        sub_append = sub_list.append
2090
        add_append = add_list.append
2091
        holder = self.person.uuid
2092

    
2093
        synced_application = self.application
2094
        if synced_application is not None:
2095
            cur_grants = synced_application.projectresourcegrant_set.all()
2096
            for grant in cur_grants:
2097
                sub_append(QuotaLimits(
2098
                               holder       = holder,
2099
                               resource     = str(grant.resource),
2100
                               capacity     = grant.member_capacity,
2101
                               import_limit = grant.member_import_limit,
2102
                               export_limit = grant.member_export_limit))
2103

    
2104
        pending_application = self.pending_application
2105
        if pending_application is not None:
2106
            new_grants = pending_application.projectresourcegrant_set.all()
2107
            for new_grant in new_grants:
2108
                add_append(QuotaLimits(
2109
                               holder       = holder,
2110
                               resource     = str(new_grant.resource),
2111
                               capacity     = new_grant.member_capacity,
2112
                               import_limit = new_grant.member_import_limit,
2113
                               export_limit = new_grant.member_export_limit))
2114

    
2115
        return (sub_list, add_list)
2116

    
2117
    def set_sync(self):
2118
        if not self.is_pending:
2119
            m = _("%s: attempt to sync a non pending membership") % (self,)
2120
            raise AssertionError(m)
2121

    
2122
        state = self.state
2123
        if state == self.ACCEPTED:
2124
            pending_application = self.pending_application
2125
            if pending_application is None:
2126
                m = _("%s: attempt to sync an empty pending application") % (
2127
                    self,)
2128
                raise AssertionError(m)
2129

    
2130
            self.application = pending_application
2131
            self.is_active = True
2132

    
2133
            self.pending_application = None
2134
            self.pending_serial = None
2135

    
2136
            # project.application may have changed in the meantime,
2137
            # in which case we stay PENDING;
2138
            # we are safe to check due to select_for_update
2139
            if self.application == self.project.application:
2140
                self.is_pending = False
2141
            self.save()
2142

    
2143
        elif state == self.PROJECT_DEACTIVATED:
2144
            if self.pending_application:
2145
                m = _("%s: attempt to sync in state '%s' "
2146
                      "with a pending application") % (self, state)
2147
                raise AssertionError(m)
2148

    
2149
            self.application = None
2150
            self.is_active = False
2151
            self.pending_serial = None
2152
            self.is_pending = False
2153
            self.save()
2154

    
2155
        elif state == self.REMOVED:
2156
            self.delete()
2157

    
2158
        else:
2159
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2160
            raise AssertionError(m)
2161

    
2162
    def reset_sync(self):
2163
        if not self.is_pending:
2164
            m = _("%s: attempt to reset a non pending membership") % (self,)
2165
            raise AssertionError(m)
2166

    
2167
        state = self.state
2168
        if state in [self.ACCEPTED, self.PROJECT_DEACTIVATED, self.REMOVED]:
2169
            self.pending_application = None
2170
            self.pending_serial = None
2171
            self.save()
2172
        else:
2173
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2174
            raise AssertionError(m)
2175

    
2176
class Serial(models.Model):
2177
    serial  =   models.AutoField(primary_key=True)
2178

    
2179
def new_serial():
2180
    s = Serial.objects.create()
2181
    serial = s.serial
2182
    s.delete()
2183
    return serial
2184

    
2185
def sync_finish_serials(serials_to_ack=None):
2186
    if serials_to_ack is None:
2187
        serials_to_ack = qh_query_serials([])
2188

    
2189
    serials_to_ack = set(serials_to_ack)
2190
    sfu = ProjectMembership.objects.select_for_update()
2191
    memberships = list(sfu.filter(pending_serial__isnull=False))
2192

    
2193
    if memberships:
2194
        for membership in memberships:
2195
            serial = membership.pending_serial
2196
            if serial in serials_to_ack:
2197
                membership.set_sync()
2198
            else:
2199
                membership.reset_sync()
2200

    
2201
        transaction.commit()
2202

    
2203
    qh_ack_serials(list(serials_to_ack))
2204
    return len(memberships)
2205

    
2206
def pre_sync_projects(sync=True):
2207
    ACCEPTED = ProjectMembership.ACCEPTED
2208
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2209
    psfu = Project.objects.select_for_update()
2210

    
2211
    modified = list(psfu.modified_projects())
2212
    if sync:
2213
        for project in modified:
2214
            objects = project.projectmembership_set.select_for_update()
2215

    
2216
            memberships = objects.filter(state=ACCEPTED)
2217
            for membership in memberships:
2218
                membership.is_pending = True
2219
                membership.save()
2220

    
2221
    reactivating = list(psfu.reactivating_projects())
2222
    if sync:
2223
        for project in reactivating:
2224
            objects = project.projectmembership_set.select_for_update()
2225

    
2226
            memberships = objects.filter(state=PROJECT_DEACTIVATED)
2227
            for membership in memberships:
2228
                membership.is_pending = True
2229
                membership.state = ACCEPTED
2230
                membership.save()
2231

    
2232
    deactivating = list(psfu.deactivating_projects())
2233
    if sync:
2234
        for project in deactivating:
2235
            objects = project.projectmembership_set.select_for_update()
2236

    
2237
            # Note: we keep a user-level deactivation
2238
            # (e.g. USER_SUSPENDED) intact
2239
            memberships = objects.filter(state=ACCEPTED)
2240
            for membership in memberships:
2241
                membership.is_pending = True
2242
                membership.state = PROJECT_DEACTIVATED
2243
                membership.save()
2244

    
2245
    return (modified, reactivating, deactivating)
2246

    
2247
def do_sync_projects():
2248

    
2249
    ACCEPTED = ProjectMembership.ACCEPTED
2250
    objects = ProjectMembership.objects.select_for_update()
2251

    
2252
    sub_quota, add_quota = [], []
2253

    
2254
    serial = new_serial()
2255

    
2256
    pending = objects.filter(is_pending=True)
2257
    for membership in pending:
2258

    
2259
        if membership.pending_application:
2260
            m = "%s: impossible: pending_application is not None (%s)" % (
2261
                membership, membership.pending_application)
2262
            raise AssertionError(m)
2263
        if membership.pending_serial:
2264
            m = "%s: impossible: pending_serial is not None (%s)" % (
2265
                membership, membership.pending_serial)
2266
            raise AssertionError(m)
2267

    
2268
        if membership.state == ACCEPTED:
2269
            membership.pending_application = membership.project.application
2270

    
2271
        membership.pending_serial = serial
2272
        membership.get_diff_quotas(sub_quota, add_quota)
2273
        membership.save()
2274

    
2275
    transaction.commit()
2276
    # ProjectApplication.approve() unblocks here
2277
    # and can set PENDING an already PENDING membership
2278
    # which has been scheduled to sync with the old project.application
2279
    # Need to check in ProjectMembership.set_sync()
2280

    
2281
    r = qh_add_quota(serial, sub_quota, add_quota)
2282
    if r:
2283
        m = "cannot sync serial: %d" % serial
2284
        raise RuntimeError(m)
2285

    
2286
    return serial
2287

    
2288
def post_sync_projects():
2289
    ACCEPTED = ProjectMembership.ACCEPTED
2290
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2291
    psfu = Project.objects.select_for_update()
2292

    
2293
    modified = psfu.modified_projects()
2294
    for project in modified:
2295
        objects = project.projectmembership_set.select_for_update()
2296

    
2297
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
2298
        if not memberships:
2299
            project.is_modified = False
2300
            project.save()
2301

    
2302
    reactivating = psfu.reactivating_projects()
2303
    for project in reactivating:
2304
        objects = project.projectmembership_set.select_for_update()
2305
        memberships = list(objects.filter(Q(state=PROJECT_DEACTIVATED) |
2306
                                          Q(is_pending=True)))
2307
        if not memberships:
2308
            project.reactivate()
2309
            project.save()
2310

    
2311
    deactivating = psfu.deactivating_projects()
2312
    for project in deactivating:
2313
        objects = project.projectmembership_set.select_for_update()
2314

    
2315
        memberships = list(objects.filter(Q(state=ACCEPTED) |
2316
                                          Q(is_pending=True)))
2317
        if not memberships:
2318
            project.deactivate()
2319
            project.save()
2320

    
2321
    transaction.commit()
2322

    
2323
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2324
    @with_lock(retries, retry_wait)
2325
    def _sync_projects(sync):
2326
        sync_finish_serials()
2327
        # Informative only -- no select_for_update()
2328
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2329

    
2330
        projects_log = pre_sync_projects(sync)
2331
        if sync:
2332
            serial = do_sync_projects()
2333
            sync_finish_serials([serial])
2334
            post_sync_projects()
2335

    
2336
        return (pending, projects_log)
2337
    return _sync_projects(sync)
2338

    
2339
def all_users_quotas(users):
2340
    quotas = {}
2341
    for user in users:
2342
        quotas[user.uuid] = user.all_quotas()
2343
    return quotas
2344

    
2345
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2346
    @with_lock(retries, retry_wait)
2347
    def _sync_users(users, sync):
2348
        sync_finish_serials()
2349

    
2350
        existing, nonexisting = qh_check_users(users)
2351
        resources = get_resource_names()
2352
        registered_quotas = qh_get_quota_limits(existing, resources)
2353
        astakos_quotas = all_users_quotas(users)
2354

    
2355
        if sync:
2356
            r = register_users(nonexisting)
2357
            r = send_quotas(astakos_quotas)
2358

    
2359
        return (existing, nonexisting, registered_quotas, astakos_quotas)
2360
    return _sync_users(users, sync)
2361

    
2362
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2363
    users = AstakosUser.objects.filter(is_active=True)
2364
    return sync_users(users, sync, retries, retry_wait)
2365

    
2366
class ProjectMembershipHistory(models.Model):
2367
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2368
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2369

    
2370
    person  =   models.BigIntegerField()
2371
    project =   models.BigIntegerField()
2372
    date    =   models.DateField(auto_now_add=True)
2373
    reason  =   models.IntegerField()
2374
    serial  =   models.BigIntegerField()
2375

    
2376
### SIGNALS ###
2377
################
2378

    
2379
def create_astakos_user(u):
2380
    try:
2381
        AstakosUser.objects.get(user_ptr=u.pk)
2382
    except AstakosUser.DoesNotExist:
2383
        extended_user = AstakosUser(user_ptr_id=u.pk)
2384
        extended_user.__dict__.update(u.__dict__)
2385
        extended_user.save()
2386
        if not extended_user.has_auth_provider('local'):
2387
            extended_user.add_auth_provider('local')
2388
    except BaseException, e:
2389
        logger.exception(e)
2390

    
2391

    
2392
def fix_superusers(sender, **kwargs):
2393
    # Associate superusers with AstakosUser
2394
    admins = User.objects.filter(is_superuser=True)
2395
    for u in admins:
2396
        create_astakos_user(u)
2397
post_syncdb.connect(fix_superusers)
2398

    
2399

    
2400
def user_post_save(sender, instance, created, **kwargs):
2401
    if not created:
2402
        return
2403
    create_astakos_user(instance)
2404
post_save.connect(user_post_save, sender=User)
2405

    
2406
def astakosuser_post_save(sender, instance, created, **kwargs):
2407
    pass
2408

    
2409
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2410

    
2411
def resource_post_save(sender, instance, created, **kwargs):
2412
    pass
2413

    
2414
post_save.connect(resource_post_save, sender=Resource)
2415

    
2416
def renew_token(sender, instance, **kwargs):
2417
    if not instance.auth_token:
2418
        instance.renew_token()
2419
pre_save.connect(renew_token, sender=AstakosUser)
2420
pre_save.connect(renew_token, sender=Service)
2421