Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 2ef1e2d7

History | View | Annotate | Download (94.8 kB)

1
# Copyright 2011, 2012, 2013 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
import copy
40

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

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

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

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

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

    
85
from synnefo.lib.quotaholder.api import QH_PRACTICALLY_INFINITE
86
from synnefo.lib.db.intdecimalfield import intDecimalField
87
from synnefo.util.text import uenc, udec
88

    
89
logger = logging.getLogger(__name__)
90

    
91
DEFAULT_CONTENT_TYPE = None
92
_content_type = None
93

    
94
def get_content_type():
95
    global _content_type
96
    if _content_type is not None:
97
        return _content_type
98

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

    
106
RESOURCE_SEPARATOR = '.'
107

    
108
inf = float('inf')
109

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

    
121
    class Meta:
122
        ordering = ('order', )
123

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

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

    
137
    def __str__(self):
138
        return self.name
139

    
140
    @property
141
    def resources(self):
142
        return self.resource_set.all()
143

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

    
149

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

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

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

    
173
    class Meta:
174
        unique_together = ("service", "name")
175

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

    
179
    def full_name(self):
180
        return str(self)
181

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

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

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

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

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

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

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

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

    
219
def load_service_resources():
220
    ss = []
221
    rs = []
222
    counter = 0
223
    for service_name, data in sorted(SERVICES.iteritems()):
224
        url = data.get('url')
225
        order = data.get('order', counter)
226
        counter = order + 1
227
        resources = data.get('resources') or ()
228
        service, created = Service.objects.get_or_create(
229
            name=service_name,
230
            defaults={'url': url, 'order': order}
231
        )
232
        if not created and url is not None:
233
            service.url = url
234
            service.save()
235

    
236
        ss.append(service)
237

    
238
        for resource in resources:
239
            try:
240
                resource_name = resource.pop('name', '')
241
                r, created = Resource.objects.get_or_create(
242
                        service=service, name=resource_name,
243
                        defaults=resource)
244
                if not created:
245
                    r.desc = resource['desc']
246
                    r.unit = resource.get('unit', None)
247
                    r.group = resource['group']
248
                    r.uplimit = resource['uplimit']
249
                    r.save()
250

    
251
                rs.append(r)
252

    
253
            except Exception, e:
254
                print "Cannot create resource ", resource_name
255
                import traceback; traceback.print_exc()
256
                continue
257

    
258
    register_services(ss)
259
    register_resources(rs)
260

    
261
def _quota_values(capacity):
262
    return QuotaValues(
263
        quantity = 0,
264
        capacity = capacity,
265
        import_limit = QH_PRACTICALLY_INFINITE,
266
        export_limit = QH_PRACTICALLY_INFINITE)
267

    
268
def get_default_quota():
269
    _DEFAULT_QUOTA = {}
270
    resources = Resource.objects.select_related('service').all()
271
    for resource in resources:
272
        capacity = resource.uplimit
273
        limits = _quota_values(capacity)
274
        _DEFAULT_QUOTA[resource.full_name()] = limits
275

    
276
    return _DEFAULT_QUOTA
277

    
278
def get_resource_names():
279
    _RESOURCE_NAMES = []
280
    resources = Resource.objects.select_related('service').all()
281
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
282
    return _RESOURCE_NAMES
283

    
284

    
285
class AstakosUserManager(UserManager):
286

    
287
    def get_auth_provider_user(self, provider, **kwargs):
288
        """
289
        Retrieve AstakosUser instance associated with the specified third party
290
        id.
291
        """
292
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
293
                          kwargs.iteritems()))
294
        return self.get(auth_providers__module=provider, **kwargs)
295

    
296
    def get_by_email(self, email):
297
        return self.get(email=email)
298

    
299
    def get_by_identifier(self, email_or_username, **kwargs):
300
        try:
301
            return self.get(email__iexact=email_or_username, **kwargs)
302
        except AstakosUser.DoesNotExist:
303
            return self.get(username__iexact=email_or_username, **kwargs)
304

    
305
    def user_exists(self, email_or_username, **kwargs):
306
        qemail = Q(email__iexact=email_or_username)
307
        qusername = Q(username__iexact=email_or_username)
308
        qextra = Q(**kwargs)
309
        return self.filter((qemail | qusername) & qextra).exists()
310

    
311
    def verified_user_exists(self, email_or_username):
312
        return self.user_exists(email_or_username, email_verified=True)
313

    
314
    def verified(self):
315
        return self.filter(email_verified=True)
316

    
317
    def uuid_catalog(self, l=None):
318
        """
319
        Returns a uuid to username mapping for the uuids appearing in l.
320
        If l is None returns the mapping for all existing users.
321
        """
322
        q = self.filter(uuid__in=l) if l != None else self
323
        return dict(q.values_list('uuid', 'username'))
324

    
325
    def displayname_catalog(self, l=None):
326
        """
327
        Returns a username to uuid mapping for the usernames appearing in l.
328
        If l is None returns the mapping for all existing users.
329
        """
330
        if l is not None:
331
            lmap = dict((x.lower(), x) for x in l)
332
            q = self.filter(username__in=lmap.keys())
333
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
334
        else:
335
            q = self
336
            values = self.values_list('username', 'uuid')
337
        return dict(values)
338

    
339

    
340

    
341
class AstakosUser(User):
342
    """
343
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
344
    """
345
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
346
                                   null=True)
347

    
348
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
349
    #                    AstakosUserProvider model.
350
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
351
                                null=True)
352
    # ex. screen_name for twitter, eppn for shibboleth
353
    third_party_identifier = models.CharField(_('Third-party identifier'),
354
                                              max_length=255, null=True,
355
                                              blank=True)
356

    
357

    
358
    #for invitations
359
    user_level = DEFAULT_USER_LEVEL
360
    level = models.IntegerField(_('Inviter level'), default=user_level)
361
    invitations = models.IntegerField(
362
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
363

    
364
    auth_token = models.CharField(_('Authentication Token'), 
365
                                  max_length=32,
366
                                  null=True, 
367
                                  blank=True, 
368
                                  help_text = _('Renew your authentication '
369
                                                'token. Make sure to set the new '
370
                                                'token in any client you may be '
371
                                                'using, to preserve its '
372
                                                'functionality.'))
373
    auth_token_created = models.DateTimeField(_('Token creation date'), 
374
                                              null=True)
375
    auth_token_expires = models.DateTimeField(
376
        _('Token expiration date'), null=True)
377

    
378
    updated = models.DateTimeField(_('Update date'))
379
    is_verified = models.BooleanField(_('Is verified?'), default=False)
380

    
381
    email_verified = models.BooleanField(_('Email verified?'), default=False)
382

    
383
    has_credits = models.BooleanField(_('Has credits?'), default=False)
384
    has_signed_terms = models.BooleanField(
385
        _('I agree with the terms'), default=False)
386
    date_signed_terms = models.DateTimeField(
387
        _('Signed terms date'), null=True, blank=True)
388

    
389
    activation_sent = models.DateTimeField(
390
        _('Activation sent data'), null=True, blank=True)
391

    
392
    policy = models.ManyToManyField(
393
        Resource, null=True, through='AstakosUserQuota')
394

    
395
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
396

    
397
    __has_signed_terms = False
398
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
399
                                           default=False, db_index=True)
400

    
401
    objects = AstakosUserManager()
402

    
403
    def __init__(self, *args, **kwargs):
404
        super(AstakosUser, self).__init__(*args, **kwargs)
405
        self.__has_signed_terms = self.has_signed_terms
406
        if not self.id:
407
            self.is_active = False
408

    
409
    @property
410
    def realname(self):
411
        return '%s %s' % (self.first_name, self.last_name)
412

    
413
    @property
414
    def log_display(self):
415
        """
416
        Should be used in all logger.* calls that refer to a user so that
417
        user display is consistent across log entries.
418
        """
419
        return '%s (%s)' % (self.uuid, self.email)
420

    
421
    @realname.setter
422
    def realname(self, value):
423
        parts = value.split(' ')
424
        if len(parts) == 2:
425
            self.first_name = parts[0]
426
            self.last_name = parts[1]
427
        else:
428
            self.last_name = parts[0]
429

    
430
    def add_permission(self, pname):
431
        if self.has_perm(pname):
432
            return
433
        p, created = Permission.objects.get_or_create(
434
                                    codename=pname,
435
                                    name=pname.capitalize(),
436
                                    content_type=get_content_type())
437
        self.user_permissions.add(p)
438

    
439
    def remove_permission(self, pname):
440
        if self.has_perm(pname):
441
            return
442
        p = Permission.objects.get(codename=pname,
443
                                   content_type=get_content_type())
444
        self.user_permissions.remove(p)
445

    
446
    def is_project_admin(self, application_id=None):
447
        return self.uuid in PROJECT_ADMINS
448

    
449
    @property
450
    def invitation(self):
451
        try:
452
            return Invitation.objects.get(username=self.email)
453
        except Invitation.DoesNotExist:
454
            return None
455

    
456
    @property
457
    def policies(self):
458
        return self.astakosuserquota_set.select_related().all()
459

    
460
    @policies.setter
461
    def policies(self, policies):
462
        for p in policies:
463
            p.setdefault('resource', '')
464
            p.setdefault('capacity', 0)
465
            p.setdefault('quantity', 0)
466
            p.setdefault('import_limit', 0)
467
            p.setdefault('export_limit', 0)
468
            p.setdefault('update', True)
469
            self.add_resource_policy(**p)
470

    
471
    def add_resource_policy(
472
            self, resource, capacity, quantity, import_limit,
473
            export_limit, update=True):
474
        """Raises ObjectDoesNotExist, IntegrityError"""
475
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
476
        resource = Resource.objects.get(service__name=s, name=r)
477
        if update:
478
            AstakosUserQuota.objects.update_or_create(
479
                user=self, resource=resource, defaults={
480
                    'capacity':capacity,
481
                    'quantity': quantity,
482
                    'import_limit':import_limit,
483
                    'export_limit':export_limit})
484
        else:
485
            q = self.astakosuserquota_set
486
            q.create(
487
                resource=resource, capacity=capacity, quanity=quantity,
488
                import_limit=import_limit, export_limit=export_limit)
489

    
490
    def get_resource_policy(self, resource):
491
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
492
        resource = Resource.objects.get(service__name=s, name=r)
493
        default_capacity = resource.uplimit
494
        try:
495
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
496
            return policy, default_capacity
497
        except AstakosUserQuota.DoesNotExist:
498
            return None, default_capacity
499

    
500
    def remove_resource_policy(self, service, resource):
501
        """Raises ObjectDoesNotExist, IntegrityError"""
502
        resource = Resource.objects.get(service__name=service, name=resource)
503
        q = self.policies.get(resource=resource).delete()
504

    
505
    def update_uuid(self):
506
        while not self.uuid:
507
            uuid_val =  str(uuid.uuid4())
508
            try:
509
                AstakosUser.objects.get(uuid=uuid_val)
510
            except AstakosUser.DoesNotExist, e:
511
                self.uuid = uuid_val
512
        return self.uuid
513

    
514
    def save(self, update_timestamps=True, **kwargs):
515
        if update_timestamps:
516
            if not self.id:
517
                self.date_joined = datetime.now()
518
            self.updated = datetime.now()
519

    
520
        # update date_signed_terms if necessary
521
        if self.__has_signed_terms != self.has_signed_terms:
522
            self.date_signed_terms = datetime.now()
523

    
524
        self.update_uuid()
525

    
526
        if self.username != self.email.lower():
527
            # set username
528
            self.username = self.email.lower()
529

    
530
        super(AstakosUser, self).save(**kwargs)
531

    
532
    def renew_token(self, flush_sessions=False, current_key=None):
533
        md5 = hashlib.md5()
534
        md5.update(settings.SECRET_KEY)
535
        md5.update(self.username)
536
        md5.update(self.realname.encode('ascii', 'ignore'))
537
        md5.update(asctime())
538

    
539
        self.auth_token = b64encode(md5.digest())
540
        self.auth_token_created = datetime.now()
541
        self.auth_token_expires = self.auth_token_created + \
542
                                  timedelta(hours=AUTH_TOKEN_DURATION)
543
        if flush_sessions:
544
            self.flush_sessions(current_key)
545
        msg = 'Token renewed for %s' % self.email
546
        logger.log(LOGGING_LEVEL, msg)
547

    
548
    def flush_sessions(self, current_key=None):
549
        q = self.sessions
550
        if current_key:
551
            q = q.exclude(session_key=current_key)
552

    
553
        keys = q.values_list('session_key', flat=True)
554
        if keys:
555
            msg = 'Flushing sessions: %s' % ','.join(keys)
556
            logger.log(LOGGING_LEVEL, msg, [])
557
        engine = import_module(settings.SESSION_ENGINE)
558
        for k in keys:
559
            s = engine.SessionStore(k)
560
            s.flush()
561

    
562
    def __unicode__(self):
563
        return '%s (%s)' % (self.realname, self.email)
564

    
565
    def conflicting_email(self):
566
        q = AstakosUser.objects.exclude(username=self.username)
567
        q = q.filter(email__iexact=self.email)
568
        if q.count() != 0:
569
            return True
570
        return False
571

    
572
    def email_change_is_pending(self):
573
        return self.emailchanges.count() > 0
574

    
575
    @property
576
    def signed_terms(self):
577
        term = get_latest_terms()
578
        if not term:
579
            return True
580
        if not self.has_signed_terms:
581
            return False
582
        if not self.date_signed_terms:
583
            return False
584
        if self.date_signed_terms < term.date:
585
            self.has_signed_terms = False
586
            self.date_signed_terms = None
587
            self.save()
588
            return False
589
        return True
590

    
591
    def set_invitations_level(self):
592
        """
593
        Update user invitation level
594
        """
595
        level = self.invitation.inviter.level + 1
596
        self.level = level
597
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
598

    
599
    def can_login_with_auth_provider(self, provider):
600
        if not self.has_auth_provider(provider):
601
            return False
602
        else:
603
            return auth_providers.get_provider(provider).is_available_for_login()
604

    
605
    def can_add_auth_provider(self, provider, include_unverified=False, **kwargs):
606
        provider_settings = auth_providers.get_provider(provider)
607

    
608
        if not provider_settings.is_available_for_add():
609
            return False
610

    
611
        if self.has_auth_provider(provider) and \
612
           provider_settings.one_per_user:
613
            return False
614

    
615
        if 'provider_info' in kwargs:
616
            kwargs.pop('provider_info')
617

    
618
        if 'identifier' in kwargs:
619
            try:
620
                # provider with specified params already exist
621
                if not include_unverified:
622
                    kwargs['user__email_verified'] = True
623
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
624
                                                                   **kwargs)
625
            except AstakosUser.DoesNotExist:
626
                return True
627
            else:
628
                return False
629

    
630
        return True
631

    
632
    def can_remove_auth_provider(self, module):
633
        provider = auth_providers.get_provider(module)
634
        existing = self.get_active_auth_providers()
635
        existing_for_provider = self.get_active_auth_providers(module=module)
636

    
637
        if len(existing) <= 1:
638
            return False
639

    
640
        if len(existing_for_provider) == 1 and provider.is_required():
641
            return False
642

    
643
        return provider.is_available_for_remove()
644

    
645
    def can_change_password(self):
646
        return self.has_auth_provider('local', auth_backend='astakos')
647

    
648
    def can_change_email(self):
649
        non_astakos_local = self.get_auth_providers().filter(module='local')
650
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
651
        return non_astakos_local.count() == 0
652

    
653
    def has_required_auth_providers(self):
654
        required = auth_providers.REQUIRED_PROVIDERS
655
        for provider in required:
656
            if not self.has_auth_provider(provider):
657
                return False
658
        return True
659

    
660
    def has_auth_provider(self, provider, **kwargs):
661
        return bool(self.get_auth_providers().filter(module=provider,
662
                                               **kwargs).count())
663

    
664
    def add_auth_provider(self, provider, **kwargs):
665
        info_data = ''
666
        if 'provider_info' in kwargs:
667
            info_data = kwargs.pop('provider_info')
668
            if isinstance(info_data, dict):
669
                info_data = json.dumps(info_data)
670

    
671
        if self.can_add_auth_provider(provider, **kwargs):
672
            if 'identifier' in kwargs:
673
                # clean up third party pending for activation users of the same
674
                # identifier
675
                AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
676
                                                                **kwargs)
677
            self.auth_providers.create(module=provider, active=True,
678
                                       info_data=info_data,
679
                                       **kwargs)
680
        else:
681
            raise Exception('Cannot add provider')
682

    
683
    def add_pending_auth_provider(self, pending):
684
        """
685
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
686
        the current user.
687
        """
688
        if not isinstance(pending, PendingThirdPartyUser):
689
            pending = PendingThirdPartyUser.objects.get(token=pending)
690

    
691
        provider = self.add_auth_provider(pending.provider,
692
                               identifier=pending.third_party_identifier,
693
                                affiliation=pending.affiliation,
694
                                          provider_info=pending.info)
695

    
696
        if email_re.match(pending.email or '') and pending.email != self.email:
697
            self.additionalmail_set.get_or_create(email=pending.email)
698

    
699
        pending.delete()
700
        return provider
701

    
702
    def remove_auth_provider(self, provider, **kwargs):
703
        self.get_auth_providers().get(module=provider, **kwargs).delete()
704

    
705
    # user urls
706
    def get_resend_activation_url(self):
707
        return reverse('send_activation', kwargs={'user_id': self.pk})
708

    
709
    def get_provider_remove_url(self, module, **kwargs):
710
        return reverse('remove_auth_provider', kwargs={
711
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
712

    
713
    def get_activation_url(self, nxt=False):
714
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
715
                                 quote(self.auth_token))
716
        if nxt:
717
            url += "&next=%s" % quote(nxt)
718
        return url
719

    
720
    def get_password_reset_url(self, token_generator=default_token_generator):
721
        return reverse('django.contrib.auth.views.password_reset_confirm',
722
                          kwargs={'uidb36':int_to_base36(self.id),
723
                                  'token':token_generator.make_token(self)})
724

    
725
    def get_primary_auth_provider(self):
726
        return self.get_auth_providers().filter()[0]
727

    
728
    def get_auth_providers(self):
729
        return self.auth_providers
730

    
731
    def get_available_auth_providers(self):
732
        """
733
        Returns a list of providers available for user to connect to.
734
        """
735
        providers = []
736
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
737
            if self.can_add_auth_provider(module):
738
                providers.append(provider_settings(self))
739

    
740
        modules = astakos_settings.IM_MODULES
741
        def key(p):
742
            if not p.module in modules:
743
                return 100
744
            return modules.index(p.module)
745
        providers = sorted(providers, key=key)
746
        return providers
747

    
748
    def get_active_auth_providers(self, **filters):
749
        providers = []
750
        for provider in self.get_auth_providers().active(**filters):
751
            if auth_providers.get_provider(provider.module).is_available_for_login():
752
                providers.append(provider)
753

    
754
        modules = astakos_settings.IM_MODULES
755
        def key(p):
756
            if not p.module in modules:
757
                return 100
758
            return modules.index(p.module)
759
        providers = sorted(providers, key=key)
760
        return providers
761

    
762
    @property
763
    def auth_providers_display(self):
764
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
765

    
766
    def get_inactive_message(self):
767
        msg_extra = ''
768
        message = ''
769
        if self.activation_sent:
770
            if self.email_verified:
771
                message = _(astakos_messages.ACCOUNT_INACTIVE)
772
            else:
773
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
774
                if astakos_settings.MODERATION_ENABLED:
775
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
776
                else:
777
                    url = self.get_resend_activation_url()
778
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
779
                                u' ' + \
780
                                _('<a href="%s">%s?</a>') % (url,
781
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
782
        else:
783
            if astakos_settings.MODERATION_ENABLED:
784
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
785
            else:
786
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
787
                url = self.get_resend_activation_url()
788
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
789
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
790

    
791
        return mark_safe(message + u' '+ msg_extra)
792

    
793
    def owns_application(self, application):
794
        return application.owner == self
795

    
796
    def owns_project(self, project):
797
        return project.application.owner == self
798

    
799
    def is_associated(self, project):
800
        try:
801
            m = ProjectMembership.objects.get(person=self, project=project)
802
            return m.state in ProjectMembership.ASSOCIATED_STATES
803
        except ProjectMembership.DoesNotExist:
804
            return False
805

    
806
    def get_membership(self, project):
807
        try:
808
            return ProjectMembership.objects.get(
809
                project=project,
810
                person=self)
811
        except ProjectMembership.DoesNotExist:
812
            return None
813

    
814
    def membership_display(self, project):
815
        m = self.get_membership(project)
816
        if m is None:
817
            return _('Not a member')
818
        else:
819
            return m.user_friendly_state_display()
820

    
821
    def non_owner_can_view(self, maybe_project):
822
        if self.is_project_admin():
823
            return True
824
        if maybe_project is None:
825
            return False
826
        project = maybe_project
827
        if self.is_associated(project):
828
            return True
829
        if project.is_deactivated():
830
            return False
831
        return True
832

    
833
    def settings(self):
834
        return UserSetting.objects.filter(user=self)
835

    
836
    def all_quotas(self):
837
        quotas = users_quotas([self])
838
        try:
839
            return quotas[self.uuid]
840
        except:
841
            raise ValueError("could not compute quotas")
842

    
843

    
844
def initial_quotas(users):
845
    initial = {}
846
    default_quotas = get_default_quota()
847

    
848
    for user in users:
849
        uuid = user.uuid
850
        initial[uuid] = dict(default_quotas)
851

    
852
    objs = AstakosUserQuota.objects.select_related()
853
    orig_quotas = objs.filter(user__in=users)
854
    for user_quota in orig_quotas:
855
        uuid = user_quota.user.uuid
856
        user_init = initial.get(uuid, {})
857
        resource = user_quota.resource.full_name()
858
        user_init[resource] = user_quota.quota_values()
859
        initial[uuid] = user_init
860

    
861
    return initial
862

    
863

    
864
def users_quotas(users, initial=None):
865
    if initial is None:
866
        quotas = initial_quotas(users)
867
    else:
868
        quotas = copy.deepcopy(initial)
869

    
870
    objs = ProjectMembership.objects.select_related('application', 'person')
871
    memberships = objs.filter(person__in=users, is_active=True)
872

    
873
    apps = set(m.application for m in memberships if m.application is not None)
874
    objs = ProjectResourceGrant.objects.select_related()
875
    grants = objs.filter(project_application__in=apps)
876

    
877
    for membership in memberships:
878
        uuid = membership.person.uuid
879
        userquotas = quotas.get(uuid, {})
880

    
881
        application = membership.application
882
        if application is None:
883
            m = _("missing application for active membership %s"
884
                  % (membership,))
885
            raise AssertionError(m)
886

    
887
        for grant in grants:
888
            if grant.project_application_id != application.id:
889
                continue
890
            resource = grant.resource.full_name()
891
            prev = userquotas.get(resource, 0)
892
            new = add_quota_values(prev, grant.member_quota_values())
893
            userquotas[resource] = new
894
        quotas[uuid] = userquotas
895

    
896
    return quotas
897

    
898

    
899
class AstakosUserAuthProviderManager(models.Manager):
900

    
901
    def active(self, **filters):
902
        return self.filter(active=True, **filters)
903

    
904
    def remove_unverified_providers(self, provider, **filters):
905
        try:
906
            existing = self.filter(module=provider, user__email_verified=False, **filters)
907
            for p in existing:
908
                p.user.delete()
909
        except:
910
            pass
911

    
912

    
913

    
914
class AstakosUserAuthProvider(models.Model):
915
    """
916
    Available user authentication methods.
917
    """
918
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
919
                                   null=True, default=None)
920
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
921
    module = models.CharField(_('Provider'), max_length=255, blank=False,
922
                                default='local')
923
    identifier = models.CharField(_('Third-party identifier'),
924
                                              max_length=255, null=True,
925
                                              blank=True)
926
    active = models.BooleanField(default=True)
927
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
928
                                   default='astakos')
929
    info_data = models.TextField(default="", null=True, blank=True)
930
    created = models.DateTimeField('Creation date', auto_now_add=True)
931

    
932
    objects = AstakosUserAuthProviderManager()
933

    
934
    class Meta:
935
        unique_together = (('identifier', 'module', 'user'), )
936
        ordering = ('module', 'created')
937

    
938
    def __init__(self, *args, **kwargs):
939
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
940
        try:
941
            self.info = json.loads(self.info_data)
942
            if not self.info:
943
                self.info = {}
944
        except Exception, e:
945
            self.info = {}
946

    
947
        for key,value in self.info.iteritems():
948
            setattr(self, 'info_%s' % key, value)
949

    
950

    
951
    @property
952
    def settings(self):
953
        return auth_providers.get_provider(self.module)
954

    
955
    @property
956
    def details_display(self):
957
        try:
958
            params = self.user.__dict__
959
            params.update(self.__dict__)
960
            return self.settings.get_details_tpl_display % params
961
        except:
962
            return ''
963

    
964
    @property
965
    def title_display(self):
966
        title_tpl = self.settings.get_title_display
967
        try:
968
            if self.settings.get_user_title_display:
969
                title_tpl = self.settings.get_user_title_display
970
        except Exception, e:
971
            pass
972
        try:
973
          return title_tpl % self.__dict__
974
        except:
975
          return self.settings.get_title_display % self.__dict__
976

    
977
    def can_remove(self):
978
        return self.user.can_remove_auth_provider(self.module)
979

    
980
    def delete(self, *args, **kwargs):
981
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
982
        if self.module == 'local':
983
            self.user.set_unusable_password()
984
            self.user.save()
985
        return ret
986

    
987
    def __repr__(self):
988
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
989

    
990
    def __unicode__(self):
991
        if self.identifier:
992
            return "%s:%s" % (self.module, self.identifier)
993
        if self.auth_backend:
994
            return "%s:%s" % (self.module, self.auth_backend)
995
        return self.module
996

    
997
    def save(self, *args, **kwargs):
998
        self.info_data = json.dumps(self.info)
999
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1000

    
1001

    
1002
class ExtendedManager(models.Manager):
1003
    def _update_or_create(self, **kwargs):
1004
        assert kwargs, \
1005
            'update_or_create() must be passed at least one keyword argument'
1006
        obj, created = self.get_or_create(**kwargs)
1007
        defaults = kwargs.pop('defaults', {})
1008
        if created:
1009
            return obj, True, False
1010
        else:
1011
            try:
1012
                params = dict(
1013
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
1014
                params.update(defaults)
1015
                for attr, val in params.items():
1016
                    if hasattr(obj, attr):
1017
                        setattr(obj, attr, val)
1018
                sid = transaction.savepoint()
1019
                obj.save(force_update=True)
1020
                transaction.savepoint_commit(sid)
1021
                return obj, False, True
1022
            except IntegrityError, e:
1023
                transaction.savepoint_rollback(sid)
1024
                try:
1025
                    return self.get(**kwargs), False, False
1026
                except self.model.DoesNotExist:
1027
                    raise e
1028

    
1029
    update_or_create = _update_or_create
1030

    
1031

    
1032
class AstakosUserQuota(models.Model):
1033
    objects = ExtendedManager()
1034
    capacity = intDecimalField()
1035
    quantity = intDecimalField(default=0)
1036
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1037
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1038
    resource = models.ForeignKey(Resource)
1039
    user = models.ForeignKey(AstakosUser)
1040

    
1041
    class Meta:
1042
        unique_together = ("resource", "user")
1043

    
1044
    def quota_values(self):
1045
        return QuotaValues(
1046
            quantity = self.quantity,
1047
            capacity = self.capacity,
1048
            import_limit = self.import_limit,
1049
            export_limit = self.export_limit)
1050

    
1051

    
1052
class ApprovalTerms(models.Model):
1053
    """
1054
    Model for approval terms
1055
    """
1056

    
1057
    date = models.DateTimeField(
1058
        _('Issue date'), db_index=True, auto_now_add=True)
1059
    location = models.CharField(_('Terms location'), max_length=255)
1060

    
1061

    
1062
class Invitation(models.Model):
1063
    """
1064
    Model for registring invitations
1065
    """
1066
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1067
                                null=True)
1068
    realname = models.CharField(_('Real name'), max_length=255)
1069
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1070
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1071
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1072
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1073
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1074

    
1075
    def __init__(self, *args, **kwargs):
1076
        super(Invitation, self).__init__(*args, **kwargs)
1077
        if not self.id:
1078
            self.code = _generate_invitation_code()
1079

    
1080
    def consume(self):
1081
        self.is_consumed = True
1082
        self.consumed = datetime.now()
1083
        self.save()
1084

    
1085
    def __unicode__(self):
1086
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1087

    
1088

    
1089
class EmailChangeManager(models.Manager):
1090

    
1091
    @transaction.commit_on_success
1092
    def change_email(self, activation_key):
1093
        """
1094
        Validate an activation key and change the corresponding
1095
        ``User`` if valid.
1096

1097
        If the key is valid and has not expired, return the ``User``
1098
        after activating.
1099

1100
        If the key is not valid or has expired, return ``None``.
1101

1102
        If the key is valid but the ``User`` is already active,
1103
        return ``None``.
1104

1105
        After successful email change the activation record is deleted.
1106

1107
        Throws ValueError if there is already
1108
        """
1109
        try:
1110
            email_change = self.model.objects.get(
1111
                activation_key=activation_key)
1112
            if email_change.activation_key_expired():
1113
                email_change.delete()
1114
                raise EmailChange.DoesNotExist
1115
            # is there an active user with this address?
1116
            try:
1117
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1118
            except AstakosUser.DoesNotExist:
1119
                pass
1120
            else:
1121
                raise ValueError(_('The new email address is reserved.'))
1122
            # update user
1123
            user = AstakosUser.objects.get(pk=email_change.user_id)
1124
            old_email = user.email
1125
            user.email = email_change.new_email_address
1126
            user.save()
1127
            email_change.delete()
1128
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1129
                                                          user.email)
1130
            logger.log(LOGGING_LEVEL, msg)
1131
            return user
1132
        except EmailChange.DoesNotExist:
1133
            raise ValueError(_('Invalid activation key.'))
1134

    
1135

    
1136
class EmailChange(models.Model):
1137
    new_email_address = models.EmailField(
1138
        _(u'new e-mail address'),
1139
        help_text=_('Provide a new email address. Until you verify the new '
1140
                    'address by following the activation link that will be '
1141
                    'sent to it, your old email address will remain active.'))
1142
    user = models.ForeignKey(
1143
        AstakosUser, unique=True, related_name='emailchanges')
1144
    requested_at = models.DateTimeField(auto_now_add=True)
1145
    activation_key = models.CharField(
1146
        max_length=40, unique=True, db_index=True)
1147

    
1148
    objects = EmailChangeManager()
1149

    
1150
    def get_url(self):
1151
        return reverse('email_change_confirm',
1152
                      kwargs={'activation_key': self.activation_key})
1153

    
1154
    def activation_key_expired(self):
1155
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1156
        return self.requested_at + expiration_date < datetime.now()
1157

    
1158

    
1159
class AdditionalMail(models.Model):
1160
    """
1161
    Model for registring invitations
1162
    """
1163
    owner = models.ForeignKey(AstakosUser)
1164
    email = models.EmailField()
1165

    
1166

    
1167
def _generate_invitation_code():
1168
    while True:
1169
        code = randint(1, 2L ** 63 - 1)
1170
        try:
1171
            Invitation.objects.get(code=code)
1172
            # An invitation with this code already exists, try again
1173
        except Invitation.DoesNotExist:
1174
            return code
1175

    
1176

    
1177
def get_latest_terms():
1178
    try:
1179
        term = ApprovalTerms.objects.order_by('-id')[0]
1180
        return term
1181
    except IndexError:
1182
        pass
1183
    return None
1184

    
1185
class PendingThirdPartyUser(models.Model):
1186
    """
1187
    Model for registring successful third party user authentications
1188
    """
1189
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1190
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1191
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1192
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1193
                                  null=True)
1194
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1195
                                 null=True)
1196
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1197
                                   null=True)
1198
    username = models.CharField(_('username'), max_length=30, unique=True,  
1199
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1200
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1201
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1202
    info = models.TextField(default="", null=True, blank=True)
1203

    
1204
    class Meta:
1205
        unique_together = ("provider", "third_party_identifier")
1206

    
1207
    def get_user_instance(self):
1208
        d = self.__dict__
1209
        d.pop('_state', None)
1210
        d.pop('id', None)
1211
        d.pop('token', None)
1212
        d.pop('created', None)
1213
        d.pop('info', None)
1214
        user = AstakosUser(**d)
1215

    
1216
        return user
1217

    
1218
    @property
1219
    def realname(self):
1220
        return '%s %s' %(self.first_name, self.last_name)
1221

    
1222
    @realname.setter
1223
    def realname(self, value):
1224
        parts = value.split(' ')
1225
        if len(parts) == 2:
1226
            self.first_name = parts[0]
1227
            self.last_name = parts[1]
1228
        else:
1229
            self.last_name = parts[0]
1230

    
1231
    def save(self, **kwargs):
1232
        if not self.id:
1233
            # set username
1234
            while not self.username:
1235
                username =  uuid.uuid4().hex[:30]
1236
                try:
1237
                    AstakosUser.objects.get(username = username)
1238
                except AstakosUser.DoesNotExist, e:
1239
                    self.username = username
1240
        super(PendingThirdPartyUser, self).save(**kwargs)
1241

    
1242
    def generate_token(self):
1243
        self.password = self.third_party_identifier
1244
        self.last_login = datetime.now()
1245
        self.token = default_token_generator.make_token(self)
1246

    
1247
    def existing_user(self):
1248
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1249
                                         auth_providers__identifier=self.third_party_identifier)
1250

    
1251
class SessionCatalog(models.Model):
1252
    session_key = models.CharField(_('session key'), max_length=40)
1253
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1254

    
1255

    
1256
class UserSetting(models.Model):
1257
    user = models.ForeignKey(AstakosUser)
1258
    setting = models.CharField(max_length=255)
1259
    value = models.IntegerField()
1260

    
1261
    objects = ForUpdateManager()
1262

    
1263
    class Meta:
1264
        unique_together = ("user", "setting")
1265

    
1266

    
1267
### PROJECTS ###
1268
################
1269

    
1270
def synced_model_metaclass(class_name, class_parents, class_attributes):
1271

    
1272
    new_attributes = {}
1273
    sync_attributes = {}
1274

    
1275
    for name, value in class_attributes.iteritems():
1276
        sync, underscore, rest = name.partition('_')
1277
        if sync == 'sync' and underscore == '_':
1278
            sync_attributes[rest] = value
1279
        else:
1280
            new_attributes[name] = value
1281

    
1282
    if 'prefix' not in sync_attributes:
1283
        m = ("you did not specify a 'sync_prefix' attribute "
1284
             "in class '%s'" % (class_name,))
1285
        raise ValueError(m)
1286

    
1287
    prefix = sync_attributes.pop('prefix')
1288
    class_name = sync_attributes.pop('classname', prefix + '_model')
1289

    
1290
    for name, value in sync_attributes.iteritems():
1291
        newname = prefix + '_' + name
1292
        if newname in new_attributes:
1293
            m = ("class '%s' was specified with prefix '%s' "
1294
                 "but it already has an attribute named '%s'"
1295
                 % (class_name, prefix, newname))
1296
            raise ValueError(m)
1297

    
1298
        new_attributes[newname] = value
1299

    
1300
    newclass = type(class_name, class_parents, new_attributes)
1301
    return newclass
1302

    
1303

    
1304
def make_synced(prefix='sync', name='SyncedState'):
1305

    
1306
    the_name = name
1307
    the_prefix = prefix
1308

    
1309
    class SyncedState(models.Model):
1310

    
1311
        sync_classname      = the_name
1312
        sync_prefix         = the_prefix
1313
        __metaclass__       = synced_model_metaclass
1314

    
1315
        sync_new_state      = models.BigIntegerField(null=True)
1316
        sync_synced_state   = models.BigIntegerField(null=True)
1317
        STATUS_SYNCED       = 0
1318
        STATUS_PENDING      = 1
1319
        sync_status         = models.IntegerField(db_index=True)
1320

    
1321
        class Meta:
1322
            abstract = True
1323

    
1324
        class NotSynced(Exception):
1325
            pass
1326

    
1327
        def sync_init_state(self, state):
1328
            self.sync_synced_state = state
1329
            self.sync_new_state = state
1330
            self.sync_status = self.STATUS_SYNCED
1331

    
1332
        def sync_get_status(self):
1333
            return self.sync_status
1334

    
1335
        def sync_set_status(self):
1336
            if self.sync_new_state != self.sync_synced_state:
1337
                self.sync_status = self.STATUS_PENDING
1338
            else:
1339
                self.sync_status = self.STATUS_SYNCED
1340

    
1341
        def sync_set_synced(self):
1342
            self.sync_synced_state = self.sync_new_state
1343
            self.sync_status = self.STATUS_SYNCED
1344

    
1345
        def sync_get_synced_state(self):
1346
            return self.sync_synced_state
1347

    
1348
        def sync_set_new_state(self, new_state):
1349
            self.sync_new_state = new_state
1350
            self.sync_set_status()
1351

    
1352
        def sync_get_new_state(self):
1353
            return self.sync_new_state
1354

    
1355
        def sync_set_synced_state(self, synced_state):
1356
            self.sync_synced_state = synced_state
1357
            self.sync_set_status()
1358

    
1359
        def sync_get_pending_objects(self):
1360
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1361
            return self.objects.filter(**kw)
1362

    
1363
        def sync_get_synced_objects(self):
1364
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1365
            return self.objects.filter(**kw)
1366

    
1367
        def sync_verify_get_synced_state(self):
1368
            status = self.sync_get_status()
1369
            state = self.sync_get_synced_state()
1370
            verified = (status == self.STATUS_SYNCED)
1371
            return state, verified
1372

    
1373
        def sync_is_synced(self):
1374
            state, verified = self.sync_verify_get_synced_state()
1375
            return verified
1376

    
1377
    return SyncedState
1378

    
1379
SyncedState = make_synced(prefix='sync', name='SyncedState')
1380

    
1381

    
1382
class ChainManager(ForUpdateManager):
1383

    
1384
    def search_by_name(self, *search_strings):
1385
        projects = Project.objects.search_by_name(*search_strings)
1386
        chains = [p.id for p in projects]
1387
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1388
        apps = (app for app in apps if app.is_latest())
1389
        app_chains = [app.chain for app in apps if app.chain not in chains]
1390
        return chains + app_chains
1391

    
1392
    def all_full_state(self):
1393
        chains = self.all()
1394
        cids = [c.chain for c in chains]
1395
        projects = Project.objects.select_related('application').in_bulk(cids)
1396

    
1397
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1398
        chain_latest = dict(objs.values_list('chain', 'latest'))
1399

    
1400
        objs = ProjectApplication.objects.select_related('applicant')
1401
        apps = objs.in_bulk(chain_latest.values())
1402

    
1403
        d = {}
1404
        for chain in chains:
1405
            pk = chain.pk
1406
            project = projects.get(pk, None)
1407
            app = apps[chain_latest[pk]]
1408
            d[chain.pk] = chain.get_state(project, app)
1409

    
1410
        return d
1411

    
1412
    def of_project(self, project):
1413
        if project is None:
1414
            return None
1415
        try:
1416
            return self.get(chain=project.id)
1417
        except Chain.DoesNotExist:
1418
            raise AssertionError('project with no chain')
1419

    
1420

    
1421
class Chain(models.Model):
1422
    chain  =   models.AutoField(primary_key=True)
1423

    
1424
    def __str__(self):
1425
        return "%s" % (self.chain,)
1426

    
1427
    objects = ChainManager()
1428

    
1429
    PENDING            = 0
1430
    DENIED             = 3
1431
    DISMISSED          = 4
1432
    CANCELLED          = 5
1433

    
1434
    APPROVED           = 10
1435
    APPROVED_PENDING   = 11
1436
    SUSPENDED          = 12
1437
    SUSPENDED_PENDING  = 13
1438
    TERMINATED         = 14
1439
    TERMINATED_PENDING = 15
1440

    
1441
    PENDING_STATES = [PENDING,
1442
                      APPROVED_PENDING,
1443
                      SUSPENDED_PENDING,
1444
                      TERMINATED_PENDING,
1445
                      ]
1446

    
1447
    MODIFICATION_STATES = [APPROVED_PENDING,
1448
                           SUSPENDED_PENDING,
1449
                           TERMINATED_PENDING,
1450
                           ]
1451

    
1452
    RELEVANT_STATES = [PENDING,
1453
                       DENIED,
1454
                       APPROVED,
1455
                       APPROVED_PENDING,
1456
                       SUSPENDED,
1457
                       SUSPENDED_PENDING,
1458
                       TERMINATED_PENDING,
1459
                       ]
1460

    
1461
    SKIP_STATES = [DISMISSED,
1462
                   CANCELLED,
1463
                   TERMINATED]
1464

    
1465
    STATE_DISPLAY = {
1466
        PENDING            : _("Pending"),
1467
        DENIED             : _("Denied"),
1468
        DISMISSED          : _("Dismissed"),
1469
        CANCELLED          : _("Cancelled"),
1470
        APPROVED           : _("Active"),
1471
        APPROVED_PENDING   : _("Active - Pending"),
1472
        SUSPENDED          : _("Suspended"),
1473
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1474
        TERMINATED         : _("Terminated"),
1475
        TERMINATED_PENDING : _("Terminated - Pending"),
1476
        }
1477

    
1478

    
1479
    @classmethod
1480
    def _chain_state(cls, project_state, app_state):
1481
        s = CHAIN_STATE.get((project_state, app_state), None)
1482
        if s is None:
1483
            raise AssertionError('inconsistent chain state')
1484
        return s
1485

    
1486
    @classmethod
1487
    def chain_state(cls, project, app):
1488
        p_state = project.state if project else None
1489
        return cls._chain_state(p_state, app.state)
1490

    
1491
    @classmethod
1492
    def state_display(cls, s):
1493
        if s is None:
1494
            return _("Unknown")
1495
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1496

    
1497
    def last_application(self):
1498
        return self.chained_apps.order_by('-id')[0]
1499

    
1500
    def get_project(self):
1501
        try:
1502
            return self.chained_project
1503
        except Project.DoesNotExist:
1504
            return None
1505

    
1506
    def get_elements(self):
1507
        project = self.get_project()
1508
        app = self.last_application()
1509
        return project, app
1510

    
1511
    def get_state(self, project, app):
1512
        s = self.chain_state(project, app)
1513
        return s, project, app
1514

    
1515
    def full_state(self):
1516
        project, app = self.get_elements()
1517
        return self.get_state(project, app)
1518

    
1519

    
1520
def new_chain():
1521
    c = Chain.objects.create()
1522
    return c
1523

    
1524

    
1525
class ProjectApplicationManager(ForUpdateManager):
1526

    
1527
    def user_visible_projects(self, *filters, **kw_filters):
1528
        model = self.model
1529
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1530

    
1531
    def user_visible_by_chain(self, flt):
1532
        model = self.model
1533
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1534
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1535
        by_chain = dict(pending.annotate(models.Max('id')))
1536
        by_chain.update(approved.annotate(models.Max('id')))
1537
        return self.filter(flt, id__in=by_chain.values())
1538

    
1539
    def user_accessible_projects(self, user):
1540
        """
1541
        Return projects accessed by specified user.
1542
        """
1543
        if user.is_project_admin():
1544
            participates_filters = Q()
1545
        else:
1546
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1547
                                   Q(project__projectmembership__person=user)
1548

    
1549
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1550

    
1551
    def search_by_name(self, *search_strings):
1552
        q = Q()
1553
        for s in search_strings:
1554
            q = q | Q(name__icontains=s)
1555
        return self.filter(q)
1556

    
1557
    def latest_of_chain(self, chain_id):
1558
        try:
1559
            return self.filter(chain=chain_id).order_by('-id')[0]
1560
        except IndexError:
1561
            return None
1562

    
1563

    
1564
class ProjectApplication(models.Model):
1565
    applicant               =   models.ForeignKey(
1566
                                    AstakosUser,
1567
                                    related_name='projects_applied',
1568
                                    db_index=True)
1569

    
1570
    PENDING     =    0
1571
    APPROVED    =    1
1572
    REPLACED    =    2
1573
    DENIED      =    3
1574
    DISMISSED   =    4
1575
    CANCELLED   =    5
1576

    
1577
    state                   =   models.IntegerField(default=PENDING,
1578
                                                    db_index=True)
1579

    
1580
    owner                   =   models.ForeignKey(
1581
                                    AstakosUser,
1582
                                    related_name='projects_owned',
1583
                                    db_index=True)
1584

    
1585
    chain                   =   models.ForeignKey(Chain,
1586
                                                  related_name='chained_apps',
1587
                                                  db_column='chain')
1588
    precursor_application   =   models.ForeignKey('ProjectApplication',
1589
                                                  null=True,
1590
                                                  blank=True)
1591

    
1592
    name                    =   models.CharField(max_length=80)
1593
    homepage                =   models.URLField(max_length=255, null=True,
1594
                                                verify_exists=False)
1595
    description             =   models.TextField(null=True, blank=True)
1596
    start_date              =   models.DateTimeField(null=True, blank=True)
1597
    end_date                =   models.DateTimeField()
1598
    member_join_policy      =   models.IntegerField()
1599
    member_leave_policy     =   models.IntegerField()
1600
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1601
    resource_grants         =   models.ManyToManyField(
1602
                                    Resource,
1603
                                    null=True,
1604
                                    blank=True,
1605
                                    through='ProjectResourceGrant')
1606
    comments                =   models.TextField(null=True, blank=True)
1607
    issue_date              =   models.DateTimeField(auto_now_add=True)
1608
    response_date           =   models.DateTimeField(null=True, blank=True)
1609

    
1610
    objects                 =   ProjectApplicationManager()
1611

    
1612
    # Compiled queries
1613
    Q_PENDING  = Q(state=PENDING)
1614
    Q_APPROVED = Q(state=APPROVED)
1615
    Q_DENIED   = Q(state=DENIED)
1616

    
1617
    class Meta:
1618
        unique_together = ("chain", "id")
1619

    
1620
    def __unicode__(self):
1621
        return "%s applied by %s" % (self.name, self.applicant)
1622

    
1623
    # TODO: Move to a more suitable place
1624
    APPLICATION_STATE_DISPLAY = {
1625
        PENDING  : _('Pending review'),
1626
        APPROVED : _('Approved'),
1627
        REPLACED : _('Replaced'),
1628
        DENIED   : _('Denied'),
1629
        DISMISSED: _('Dismissed'),
1630
        CANCELLED: _('Cancelled')
1631
    }
1632

    
1633
    def get_project(self):
1634
        try:
1635
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1636
            return Project
1637
        except Project.DoesNotExist, e:
1638
            return None
1639

    
1640
    def state_display(self):
1641
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1642

    
1643
    def project_state_display(self):
1644
        try:
1645
            project = self.project
1646
            return project.state_display()
1647
        except Project.DoesNotExist:
1648
            return self.state_display()
1649

    
1650
    def add_resource_policy(self, service, resource, uplimit):
1651
        """Raises ObjectDoesNotExist, IntegrityError"""
1652
        q = self.projectresourcegrant_set
1653
        resource = Resource.objects.get(service__name=service, name=resource)
1654
        q.create(resource=resource, member_capacity=uplimit)
1655

    
1656
    def members_count(self):
1657
        return self.project.approved_memberships.count()
1658

    
1659
    @property
1660
    def grants(self):
1661
        return self.projectresourcegrant_set.values(
1662
            'member_capacity', 'resource__name', 'resource__service__name')
1663

    
1664
    @property
1665
    def resource_policies(self):
1666
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1667

    
1668
    @resource_policies.setter
1669
    def resource_policies(self, policies):
1670
        for p in policies:
1671
            service = p.get('service', None)
1672
            resource = p.get('resource', None)
1673
            uplimit = p.get('uplimit', 0)
1674
            self.add_resource_policy(service, resource, uplimit)
1675

    
1676
    def pending_modifications_incl_me(self):
1677
        q = self.chained_applications()
1678
        q = q.filter(Q(state=self.PENDING))
1679
        return q
1680

    
1681
    def last_pending_incl_me(self):
1682
        try:
1683
            return self.pending_modifications_incl_me().order_by('-id')[0]
1684
        except IndexError:
1685
            return None
1686

    
1687
    def pending_modifications(self):
1688
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1689

    
1690
    def last_pending(self):
1691
        try:
1692
            return self.pending_modifications().order_by('-id')[0]
1693
        except IndexError:
1694
            return None
1695

    
1696
    def is_modification(self):
1697
        # if self.state != self.PENDING:
1698
        #     return False
1699
        parents = self.chained_applications().filter(id__lt=self.id)
1700
        parents = parents.filter(state__in=[self.APPROVED])
1701
        return parents.count() > 0
1702

    
1703
    def chained_applications(self):
1704
        return ProjectApplication.objects.filter(chain=self.chain)
1705

    
1706
    def is_latest(self):
1707
        return self.chained_applications().order_by('-id')[0] == self
1708

    
1709
    def has_pending_modifications(self):
1710
        return bool(self.last_pending())
1711

    
1712
    def denied_modifications(self):
1713
        q = self.chained_applications()
1714
        q = q.filter(Q(state=self.DENIED))
1715
        q = q.filter(~Q(id=self.id))
1716
        return q
1717

    
1718
    def last_denied(self):
1719
        try:
1720
            return self.denied_modifications().order_by('-id')[0]
1721
        except IndexError:
1722
            return None
1723

    
1724
    def has_denied_modifications(self):
1725
        return bool(self.last_denied())
1726

    
1727
    def is_applied(self):
1728
        try:
1729
            self.project
1730
            return True
1731
        except Project.DoesNotExist:
1732
            return False
1733

    
1734
    def get_project(self):
1735
        try:
1736
            return Project.objects.get(id=self.chain)
1737
        except Project.DoesNotExist:
1738
            return None
1739

    
1740
    def project_exists(self):
1741
        return self.get_project() is not None
1742

    
1743
    def _get_project_for_update(self):
1744
        try:
1745
            objects = Project.objects
1746
            project = objects.get_for_update(id=self.chain)
1747
            return project
1748
        except Project.DoesNotExist:
1749
            return None
1750

    
1751
    def can_cancel(self):
1752
        return self.state == self.PENDING
1753

    
1754
    def cancel(self):
1755
        if not self.can_cancel():
1756
            m = _("cannot cancel: application '%s' in state '%s'") % (
1757
                    self.id, self.state)
1758
            raise AssertionError(m)
1759

    
1760
        self.state = self.CANCELLED
1761
        self.save()
1762

    
1763
    def can_dismiss(self):
1764
        return self.state == self.DENIED
1765

    
1766
    def dismiss(self):
1767
        if not self.can_dismiss():
1768
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1769
                    self.id, self.state)
1770
            raise AssertionError(m)
1771

    
1772
        self.state = self.DISMISSED
1773
        self.save()
1774

    
1775
    def can_deny(self):
1776
        return self.state == self.PENDING
1777

    
1778
    def deny(self):
1779
        if not self.can_deny():
1780
            m = _("cannot deny: application '%s' in state '%s'") % (
1781
                    self.id, self.state)
1782
            raise AssertionError(m)
1783

    
1784
        self.state = self.DENIED
1785
        self.response_date = datetime.now()
1786
        self.save()
1787

    
1788
    def can_approve(self):
1789
        return self.state == self.PENDING
1790

    
1791
    def approve(self, approval_user=None):
1792
        """
1793
        If approval_user then during owner membership acceptance
1794
        it is checked whether the request_user is eligible.
1795

1796
        Raises:
1797
            PermissionDenied
1798
        """
1799

    
1800
        if not transaction.is_managed():
1801
            raise AssertionError("NOPE")
1802

    
1803
        new_project_name = self.name
1804
        if not self.can_approve():
1805
            m = _("cannot approve: project '%s' in state '%s'") % (
1806
                    new_project_name, self.state)
1807
            raise AssertionError(m) # invalid argument
1808

    
1809
        now = datetime.now()
1810
        project = self._get_project_for_update()
1811

    
1812
        try:
1813
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1814
            conflicting_project = Project.objects.get(q)
1815
            if (conflicting_project != project):
1816
                m = (_("cannot approve: project with name '%s' "
1817
                       "already exists (id: %s)") % (
1818
                        new_project_name, conflicting_project.id))
1819
                raise PermissionDenied(m) # invalid argument
1820
        except Project.DoesNotExist:
1821
            pass
1822

    
1823
        new_project = False
1824
        if project is None:
1825
            new_project = True
1826
            project = Project(id=self.chain)
1827

    
1828
        project.name = new_project_name
1829
        project.application = self
1830
        project.last_approval_date = now
1831
        if not new_project:
1832
            project.is_modified = True
1833

    
1834
        project.save()
1835

    
1836
        self.state = self.APPROVED
1837
        self.response_date = now
1838
        self.save()
1839

    
1840
    @property
1841
    def member_join_policy_display(self):
1842
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1843

    
1844
    @property
1845
    def member_leave_policy_display(self):
1846
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1847

    
1848
class ProjectResourceGrant(models.Model):
1849

    
1850
    resource                =   models.ForeignKey(Resource)
1851
    project_application     =   models.ForeignKey(ProjectApplication,
1852
                                                  null=True)
1853
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1854
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1855
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1856
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1857
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1858
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1859

    
1860
    objects = ExtendedManager()
1861

    
1862
    class Meta:
1863
        unique_together = ("resource", "project_application")
1864

    
1865
    def member_quota_values(self):
1866
        return QuotaValues(
1867
            quantity = 0,
1868
            capacity = self.member_capacity,
1869
            import_limit = self.member_import_limit,
1870
            export_limit = self.member_export_limit)
1871

    
1872
    def display_member_capacity(self):
1873
        if self.member_capacity:
1874
            if self.resource.unit:
1875
                return ProjectResourceGrant.display_filesize(
1876
                    self.member_capacity)
1877
            else:
1878
                if math.isinf(self.member_capacity):
1879
                    return 'Unlimited'
1880
                else:
1881
                    return self.member_capacity
1882
        else:
1883
            return 'Unlimited'
1884

    
1885
    def __str__(self):
1886
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1887
                                        self.display_member_capacity())
1888

    
1889
    @classmethod
1890
    def display_filesize(cls, value):
1891
        try:
1892
            value = float(value)
1893
        except:
1894
            return
1895
        else:
1896
            if math.isinf(value):
1897
                return 'Unlimited'
1898
            if value > 1:
1899
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1900
                                [0, 0, 0, 0, 0, 0])
1901
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1902
                quotient = float(value) / 1024**exponent
1903
                unit, value_decimals = unit_list[exponent]
1904
                format_string = '{0:.%sf} {1}' % (value_decimals)
1905
                return format_string.format(quotient, unit)
1906
            if value == 0:
1907
                return '0 bytes'
1908
            if value == 1:
1909
                return '1 byte'
1910
            else:
1911
               return '0'
1912

    
1913

    
1914
class ProjectManager(ForUpdateManager):
1915

    
1916
    def terminated_projects(self):
1917
        q = self.model.Q_TERMINATED
1918
        return self.filter(q)
1919

    
1920
    def not_terminated_projects(self):
1921
        q = ~self.model.Q_TERMINATED
1922
        return self.filter(q)
1923

    
1924
    def terminating_projects(self):
1925
        q = self.model.Q_TERMINATED & Q(is_active=True)
1926
        return self.filter(q)
1927

    
1928
    def deactivated_projects(self):
1929
        q = self.model.Q_DEACTIVATED
1930
        return self.filter(q)
1931

    
1932
    def deactivating_projects(self):
1933
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1934
        return self.filter(q)
1935

    
1936
    def modified_projects(self):
1937
        return self.filter(is_modified=True)
1938

    
1939
    def reactivating_projects(self):
1940
        return self.filter(state=Project.APPROVED, is_active=False)
1941

    
1942
    def expired_projects(self):
1943
        q = (~Q(state=Project.TERMINATED) &
1944
              Q(application__end_date__lt=datetime.now()))
1945
        return self.filter(q)
1946

    
1947
    def search_by_name(self, *search_strings):
1948
        q = Q()
1949
        for s in search_strings:
1950
            q = q | Q(name__icontains=s)
1951
        return self.filter(q)
1952

    
1953

    
1954
class Project(models.Model):
1955

    
1956
    id                          =   models.OneToOneField(Chain,
1957
                                                      related_name='chained_project',
1958
                                                      db_column='id',
1959
                                                      primary_key=True)
1960

    
1961
    application                 =   models.OneToOneField(
1962
                                            ProjectApplication,
1963
                                            related_name='project')
1964
    last_approval_date          =   models.DateTimeField(null=True)
1965

    
1966
    members                     =   models.ManyToManyField(
1967
                                            AstakosUser,
1968
                                            through='ProjectMembership')
1969

    
1970
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1971
    deactivation_date           =   models.DateTimeField(null=True)
1972

    
1973
    creation_date               =   models.DateTimeField(auto_now_add=True)
1974
    name                        =   models.CharField(
1975
                                            max_length=80,
1976
                                            null=True,
1977
                                            db_index=True,
1978
                                            unique=True)
1979

    
1980
    APPROVED    = 1
1981
    SUSPENDED   = 10
1982
    TERMINATED  = 100
1983

    
1984
    is_modified                 =   models.BooleanField(default=False,
1985
                                                        db_index=True)
1986
    is_active                   =   models.BooleanField(default=True,
1987
                                                        db_index=True)
1988
    state                       =   models.IntegerField(default=APPROVED,
1989
                                                        db_index=True)
1990

    
1991
    objects     =   ProjectManager()
1992

    
1993
    # Compiled queries
1994
    Q_TERMINATED  = Q(state=TERMINATED)
1995
    Q_SUSPENDED   = Q(state=SUSPENDED)
1996
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1997

    
1998
    def __str__(self):
1999
        return uenc(_("<project %s '%s'>") %
2000
                    (self.id, udec(self.application.name)))
2001

    
2002
    __repr__ = __str__
2003

    
2004
    def __unicode__(self):
2005
        return _("<project %s '%s'>") % (self.id, self.application.name)
2006

    
2007
    STATE_DISPLAY = {
2008
        APPROVED   : 'Active',
2009
        SUSPENDED  : 'Suspended',
2010
        TERMINATED : 'Terminated'
2011
        }
2012

    
2013
    def state_display(self):
2014
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
2015

    
2016
    def admin_state_display(self):
2017
        s = self.state_display()
2018
        if self.sync_pending():
2019
            s += ' (sync pending)'
2020
        return s
2021

    
2022
    def sync_pending(self):
2023
        if self.state != self.APPROVED:
2024
            return self.is_active
2025
        return not self.is_active or self.is_modified
2026

    
2027
    def expiration_info(self):
2028
        return (str(self.id), self.name, self.state_display(),
2029
                str(self.application.end_date))
2030

    
2031
    def is_deactivated(self, reason=None):
2032
        if reason is not None:
2033
            return self.state == reason
2034

    
2035
        return self.state != self.APPROVED
2036

    
2037
    def is_deactivating(self, reason=None):
2038
        if not self.is_active:
2039
            return False
2040

    
2041
        return self.is_deactivated(reason)
2042

    
2043
    def is_deactivated_strict(self, reason=None):
2044
        if self.is_active:
2045
            return False
2046

    
2047
        return self.is_deactivated(reason)
2048

    
2049
    ### Deactivation calls
2050

    
2051
    def deactivate(self):
2052
        self.deactivation_date = datetime.now()
2053
        self.is_active = False
2054

    
2055
    def reactivate(self):
2056
        self.deactivation_date = None
2057
        self.is_active = True
2058

    
2059
    def terminate(self):
2060
        self.deactivation_reason = 'TERMINATED'
2061
        self.state = self.TERMINATED
2062
        self.name = None
2063
        self.save()
2064

    
2065
    def suspend(self):
2066
        self.deactivation_reason = 'SUSPENDED'
2067
        self.state = self.SUSPENDED
2068
        self.save()
2069

    
2070
    def resume(self):
2071
        self.deactivation_reason = None
2072
        self.state = self.APPROVED
2073
        self.save()
2074

    
2075
    ### Logical checks
2076

    
2077
    def is_inconsistent(self):
2078
        now = datetime.now()
2079
        dates = [self.creation_date,
2080
                 self.last_approval_date,
2081
                 self.deactivation_date]
2082
        return any([date > now for date in dates])
2083

    
2084
    def is_active_strict(self):
2085
        return self.is_active and self.state == self.APPROVED
2086

    
2087
    def is_approved(self):
2088
        return self.state == self.APPROVED
2089

    
2090
    @property
2091
    def is_alive(self):
2092
        return not self.is_terminated
2093

    
2094
    @property
2095
    def is_terminated(self):
2096
        return self.is_deactivated(self.TERMINATED)
2097

    
2098
    @property
2099
    def is_suspended(self):
2100
        return self.is_deactivated(self.SUSPENDED)
2101

    
2102
    def violates_resource_grants(self):
2103
        return False
2104

    
2105
    def violates_members_limit(self, adding=0):
2106
        application = self.application
2107
        limit = application.limit_on_members_number
2108
        if limit is None:
2109
            return False
2110
        return (len(self.approved_members) + adding > limit)
2111

    
2112

    
2113
    ### Other
2114

    
2115
    def count_pending_memberships(self):
2116
        memb_set = self.projectmembership_set
2117
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2118
        return memb_count
2119

    
2120
    def members_count(self):
2121
        return self.approved_memberships.count()
2122

    
2123
    @property
2124
    def approved_memberships(self):
2125
        query = ProjectMembership.Q_ACCEPTED_STATES
2126
        return self.projectmembership_set.filter(query)
2127

    
2128
    @property
2129
    def approved_members(self):
2130
        return [m.person for m in self.approved_memberships]
2131

    
2132
    def add_member(self, user):
2133
        """
2134
        Raises:
2135
            django.exceptions.PermissionDenied
2136
            astakos.im.models.AstakosUser.DoesNotExist
2137
        """
2138
        if isinstance(user, (int, long)):
2139
            user = AstakosUser.objects.get(user=user)
2140

    
2141
        m, created = ProjectMembership.objects.get_or_create(
2142
            person=user, project=self
2143
        )
2144
        m.accept()
2145

    
2146
    def remove_member(self, user):
2147
        """
2148
        Raises:
2149
            django.exceptions.PermissionDenied
2150
            astakos.im.models.AstakosUser.DoesNotExist
2151
            astakos.im.models.ProjectMembership.DoesNotExist
2152
        """
2153
        if isinstance(user, (int, long)):
2154
            user = AstakosUser.objects.get(user=user)
2155

    
2156
        m = ProjectMembership.objects.get(person=user, project=self)
2157
        m.remove()
2158

    
2159

    
2160
CHAIN_STATE = {
2161
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2162
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2163
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2164
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2165
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2166

    
2167
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2168
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2169
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2170
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2171
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2172

    
2173
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2174
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2175
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2176
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2177
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2178

    
2179
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2180
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2181
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2182
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2183
    }
2184

    
2185

    
2186
class PendingMembershipError(Exception):
2187
    pass
2188

    
2189

    
2190
class ProjectMembershipManager(ForUpdateManager):
2191

    
2192
    def any_accepted(self):
2193
        q = (Q(state=ProjectMembership.ACCEPTED) |
2194
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
2195
        return self.filter(q)
2196

    
2197
    def actually_accepted(self):
2198
        q = self.model.Q_ACTUALLY_ACCEPTED
2199
        return self.filter(q)
2200

    
2201
    def requested(self):
2202
        return self.filter(state=ProjectMembership.REQUESTED)
2203

    
2204
    def suspended(self):
2205
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2206

    
2207
class ProjectMembership(models.Model):
2208

    
2209
    person              =   models.ForeignKey(AstakosUser)
2210
    request_date        =   models.DateField(auto_now_add=True)
2211
    project             =   models.ForeignKey(Project)
2212

    
2213
    REQUESTED           =   0
2214
    ACCEPTED            =   1
2215
    LEAVE_REQUESTED     =   5
2216
    # User deactivation
2217
    USER_SUSPENDED      =   10
2218
    # Project deactivation
2219
    PROJECT_DEACTIVATED =   100
2220

    
2221
    REMOVED             =   200
2222

    
2223
    ASSOCIATED_STATES   =   set([REQUESTED,
2224
                                 ACCEPTED,
2225
                                 LEAVE_REQUESTED,
2226
                                 USER_SUSPENDED,
2227
                                 PROJECT_DEACTIVATED])
2228

    
2229
    ACCEPTED_STATES     =   set([ACCEPTED,
2230
                                 LEAVE_REQUESTED,
2231
                                 USER_SUSPENDED,
2232
                                 PROJECT_DEACTIVATED])
2233

    
2234
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2235

    
2236
    state               =   models.IntegerField(default=REQUESTED,
2237
                                                db_index=True)
2238
    is_pending          =   models.BooleanField(default=False, db_index=True)
2239
    is_active           =   models.BooleanField(default=False, db_index=True)
2240
    application         =   models.ForeignKey(
2241
                                ProjectApplication,
2242
                                null=True,
2243
                                related_name='memberships')
2244
    pending_application =   models.ForeignKey(
2245
                                ProjectApplication,
2246
                                null=True,
2247
                                related_name='pending_memberships')
2248
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2249

    
2250
    acceptance_date     =   models.DateField(null=True, db_index=True)
2251
    leave_request_date  =   models.DateField(null=True)
2252

    
2253
    objects     =   ProjectMembershipManager()
2254

    
2255
    # Compiled queries
2256
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2257
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2258

    
2259
    MEMBERSHIP_STATE_DISPLAY = {
2260
        REQUESTED           : _('Requested'),
2261
        ACCEPTED            : _('Accepted'),
2262
        LEAVE_REQUESTED     : _('Leave Requested'),
2263
        USER_SUSPENDED      : _('Suspended'),
2264
        PROJECT_DEACTIVATED : _('Accepted'), # sic
2265
        REMOVED             : _('Pending removal'),
2266
        }
2267

    
2268
    USER_FRIENDLY_STATE_DISPLAY = {
2269
        REQUESTED           : _('Join requested'),
2270
        ACCEPTED            : _('Accepted member'),
2271
        LEAVE_REQUESTED     : _('Requested to leave'),
2272
        USER_SUSPENDED      : _('Suspended member'),
2273
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
2274
        REMOVED             : _('Pending removal'),
2275
        }
2276

    
2277
    def state_display(self):
2278
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2279

    
2280
    def user_friendly_state_display(self):
2281
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2282

    
2283
    def get_combined_state(self):
2284
        return self.state, self.is_active, self.is_pending
2285

    
2286
    class Meta:
2287
        unique_together = ("person", "project")
2288
        #index_together = [["project", "state"]]
2289

    
2290
    def __str__(self):
2291
        return uenc(_("<'%s' membership in '%s'>") % (
2292
                self.person.username, self.project))
2293

    
2294
    __repr__ = __str__
2295

    
2296
    def __init__(self, *args, **kwargs):
2297
        self.state = self.REQUESTED
2298
        super(ProjectMembership, self).__init__(*args, **kwargs)
2299

    
2300
    def _set_history_item(self, reason, date=None):
2301
        if isinstance(reason, basestring):
2302
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2303

    
2304
        history_item = ProjectMembershipHistory(
2305
                            serial=self.id,
2306
                            person=self.person_id,
2307
                            project=self.project_id,
2308
                            date=date or datetime.now(),
2309
                            reason=reason)
2310
        history_item.save()
2311
        serial = history_item.id
2312

    
2313
    def can_accept(self):
2314
        return self.state == self.REQUESTED
2315

    
2316
    def accept(self):
2317
        if self.is_pending:
2318
            m = _("%s: attempt to accept while is pending") % (self,)
2319
            raise AssertionError(m)
2320

    
2321
        if not self.can_accept():
2322
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2323
            raise AssertionError(m)
2324

    
2325
        now = datetime.now()
2326
        self.acceptance_date = now
2327
        self._set_history_item(reason='ACCEPT', date=now)
2328
        if self.project.is_approved():
2329
            self.state = self.ACCEPTED
2330
            self.is_pending = True
2331
        else:
2332
            self.state = self.PROJECT_DEACTIVATED
2333

    
2334
        self.save()
2335

    
2336
    def can_leave(self):
2337
        return self.state in self.ACCEPTED_STATES
2338

    
2339
    def leave_request(self):
2340
        if self.is_pending:
2341
            m = _("%s: attempt to request to leave while is pending") % (self,)
2342
            raise AssertionError(m)
2343

    
2344
        if not self.can_leave():
2345
            m = _("%s: attempt to request to leave in state '%s'") % (
2346
                self, self.state)
2347
            raise AssertionError(m)
2348

    
2349
        self.leave_request_date = datetime.now()
2350
        self.state = self.LEAVE_REQUESTED
2351
        self.save()
2352

    
2353
    def can_deny_leave(self):
2354
        return self.state == self.LEAVE_REQUESTED
2355

    
2356
    def leave_request_deny(self):
2357
        if self.is_pending:
2358
            m = _("%s: attempt to deny leave request while is pending") % (
2359
                self,)
2360
            raise AssertionError(m)
2361

    
2362
        if not self.can_deny_leave():
2363
            m = _("%s: attempt to deny leave request in state '%s'") % (
2364
                self, self.state)
2365
            raise AssertionError(m)
2366

    
2367
        self.leave_request_date = None
2368
        self.state = self.ACCEPTED
2369
        self.save()
2370

    
2371
    def can_cancel_leave(self):
2372
        return self.state == self.LEAVE_REQUESTED
2373

    
2374
    def leave_request_cancel(self):
2375
        if self.is_pending:
2376
            m = _("%s: attempt to cancel leave request while is pending") % (
2377
                self,)
2378
            raise AssertionError(m)
2379

    
2380
        if not self.can_cancel_leave():
2381
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2382
                self, self.state)
2383
            raise AssertionError(m)
2384

    
2385
        self.leave_request_date = None
2386
        self.state = self.ACCEPTED
2387
        self.save()
2388

    
2389
    def can_remove(self):
2390
        return self.state in self.ACCEPTED_STATES
2391

    
2392
    def remove(self):
2393
        if self.is_pending:
2394
            m = _("%s: attempt to remove while is pending") % (self,)
2395
            raise AssertionError(m)
2396

    
2397
        if not self.can_remove():
2398
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2399
            raise AssertionError(m)
2400

    
2401
        self._set_history_item(reason='REMOVE')
2402
        self.state = self.REMOVED
2403
        self.is_pending = True
2404
        self.save()
2405

    
2406
    def can_reject(self):
2407
        return self.state == self.REQUESTED
2408

    
2409
    def reject(self):
2410
        if self.is_pending:
2411
            m = _("%s: attempt to reject while is pending") % (self,)
2412
            raise AssertionError(m)
2413

    
2414
        if not self.can_reject():
2415
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2416
            raise AssertionError(m)
2417

    
2418
        # rejected requests don't need sync,
2419
        # because they were never effected
2420
        self._set_history_item(reason='REJECT')
2421
        self.delete()
2422

    
2423
    def can_cancel(self):
2424
        return self.state == self.REQUESTED
2425

    
2426
    def cancel(self):
2427
        if self.is_pending:
2428
            m = _("%s: attempt to cancel while is pending") % (self,)
2429
            raise AssertionError(m)
2430

    
2431
        if not self.can_cancel():
2432
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2433
            raise AssertionError(m)
2434

    
2435
        # rejected requests don't need sync,
2436
        # because they were never effected
2437
        self._set_history_item(reason='CANCEL')
2438
        self.delete()
2439

    
2440
    def get_diff_quotas(self, sub_list=None, add_list=None):
2441
        if sub_list is None:
2442
            sub_list = []
2443

    
2444
        if add_list is None:
2445
            add_list = []
2446

    
2447
        sub_append = sub_list.append
2448
        add_append = add_list.append
2449
        holder = self.person.uuid
2450

    
2451
        synced_application = self.application
2452
        if synced_application is not None:
2453
            cur_grants = synced_application.projectresourcegrant_set.all()
2454
            for grant in cur_grants:
2455
                sub_append(QuotaLimits(
2456
                               holder       = holder,
2457
                               resource     = str(grant.resource),
2458
                               capacity     = grant.member_capacity,
2459
                               import_limit = grant.member_import_limit,
2460
                               export_limit = grant.member_export_limit))
2461

    
2462
        pending_application = self.pending_application
2463
        if pending_application is not None:
2464
            new_grants = pending_application.projectresourcegrant_set.all()
2465
            for new_grant in new_grants:
2466
                add_append(QuotaLimits(
2467
                               holder       = holder,
2468
                               resource     = str(new_grant.resource),
2469
                               capacity     = new_grant.member_capacity,
2470
                               import_limit = new_grant.member_import_limit,
2471
                               export_limit = new_grant.member_export_limit))
2472

    
2473
        return (sub_list, add_list)
2474

    
2475
    def set_sync(self):
2476
        if not self.is_pending:
2477
            m = _("%s: attempt to sync a non pending membership") % (self,)
2478
            raise AssertionError(m)
2479

    
2480
        state = self.state
2481
        if state in self.ACTUALLY_ACCEPTED:
2482
            pending_application = self.pending_application
2483
            if pending_application is None:
2484
                m = _("%s: attempt to sync an empty pending application") % (
2485
                    self,)
2486
                raise AssertionError(m)
2487

    
2488
            self.application = pending_application
2489
            self.is_active = True
2490

    
2491
            self.pending_application = None
2492
            self.pending_serial = None
2493

    
2494
            # project.application may have changed in the meantime,
2495
            # in which case we stay PENDING;
2496
            # we are safe to check due to select_for_update
2497
            if self.application == self.project.application:
2498
                self.is_pending = False
2499
            self.save()
2500

    
2501
        elif state == self.PROJECT_DEACTIVATED:
2502
            if self.pending_application:
2503
                m = _("%s: attempt to sync in state '%s' "
2504
                      "with a pending application") % (self, state)
2505
                raise AssertionError(m)
2506

    
2507
            self.application = None
2508
            self.is_active = False
2509
            self.pending_serial = None
2510
            self.is_pending = False
2511
            self.save()
2512

    
2513
        elif state == self.REMOVED:
2514
            self.delete()
2515

    
2516
        else:
2517
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2518
            raise AssertionError(m)
2519

    
2520
    def reset_sync(self):
2521
        if not self.is_pending:
2522
            m = _("%s: attempt to reset a non pending membership") % (self,)
2523
            raise AssertionError(m)
2524

    
2525
        state = self.state
2526
        if state in [self.ACCEPTED, self.LEAVE_REQUESTED,
2527
                     self.PROJECT_DEACTIVATED, self.REMOVED]:
2528
            self.pending_application = None
2529
            self.pending_serial = None
2530
            self.save()
2531
        else:
2532
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2533
            raise AssertionError(m)
2534

    
2535
class Serial(models.Model):
2536
    serial  =   models.AutoField(primary_key=True)
2537

    
2538
def new_serial():
2539
    s = Serial.objects.create()
2540
    serial = s.serial
2541
    s.delete()
2542
    return serial
2543

    
2544
class SyncError(Exception):
2545
    pass
2546

    
2547
def reset_serials(serials):
2548
    objs = ProjectMembership.objects
2549
    q = objs.filter(pending_serial__in=serials).select_for_update()
2550
    memberships = list(q)
2551

    
2552
    if memberships:
2553
        for membership in memberships:
2554
            membership.reset_sync()
2555

    
2556
        transaction.commit()
2557

    
2558
def sync_finish_serials(serials_to_ack=None):
2559
    if serials_to_ack is None:
2560
        serials_to_ack = qh_query_serials([])
2561

    
2562
    serials_to_ack = set(serials_to_ack)
2563
    objs = ProjectMembership.objects
2564
    q = objs.filter(pending_serial__isnull=False).select_for_update()
2565
    memberships = list(q)
2566

    
2567
    if memberships:
2568
        for membership in memberships:
2569
            serial = membership.pending_serial
2570
            if serial in serials_to_ack:
2571
                membership.set_sync()
2572
            else:
2573
                membership.reset_sync()
2574

    
2575
        transaction.commit()
2576

    
2577
    qh_ack_serials(list(serials_to_ack))
2578
    return len(memberships)
2579

    
2580
def pre_sync_projects(sync=True):
2581
    ACCEPTED = ProjectMembership.ACCEPTED
2582
    LEAVE_REQUESTED = ProjectMembership.LEAVE_REQUESTED
2583
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2584
    objs = Project.objects
2585

    
2586
    modified = list(objs.modified_projects().select_for_update())
2587
    if sync:
2588
        for project in modified:
2589
            objects = project.projectmembership_set
2590

    
2591
            memberships = objects.actually_accepted().select_for_update()
2592
            for membership in memberships:
2593
                membership.is_pending = True
2594
                membership.save()
2595

    
2596
    reactivating = list(objs.reactivating_projects().select_for_update())
2597
    if sync:
2598
        for project in reactivating:
2599
            objects = project.projectmembership_set
2600

    
2601
            q = objects.filter(state=PROJECT_DEACTIVATED)
2602
            memberships = q.select_for_update()
2603
            for membership in memberships:
2604
                membership.is_pending = True
2605
                if membership.leave_request_date is None:
2606
                    membership.state = ACCEPTED
2607
                else:
2608
                    membership.state = LEAVE_REQUESTED
2609
                membership.save()
2610

    
2611
    deactivating = list(objs.deactivating_projects().select_for_update())
2612
    if sync:
2613
        for project in deactivating:
2614
            objects = project.projectmembership_set
2615

    
2616
            # Note: we keep a user-level deactivation
2617
            # (e.g. USER_SUSPENDED) intact
2618
            memberships = objects.actually_accepted().select_for_update()
2619
            for membership in memberships:
2620
                membership.is_pending = True
2621
                membership.state = PROJECT_DEACTIVATED
2622
                membership.save()
2623

    
2624
#    transaction.commit()
2625
    return (modified, reactivating, deactivating)
2626

    
2627
def set_sync_projects(exclude=None):
2628

    
2629
    ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED
2630
    objects = ProjectMembership.objects
2631

    
2632
    sub_quota, add_quota = [], []
2633

    
2634
    serial = new_serial()
2635

    
2636
    pending = objects.filter(is_pending=True).select_for_update()
2637
    for membership in pending:
2638

    
2639
        if membership.pending_application:
2640
            m = "%s: impossible: pending_application is not None (%s)" % (
2641
                membership, membership.pending_application)
2642
            raise AssertionError(m)
2643
        if membership.pending_serial:
2644
            m = "%s: impossible: pending_serial is not None (%s)" % (
2645
                membership, membership.pending_serial)
2646
            raise AssertionError(m)
2647

    
2648
        if exclude is not None:
2649
            uuid = membership.person.uuid
2650
            if uuid in exclude:
2651
                logger.warning("Excluded from sync: %s" % uuid)
2652
                continue
2653

    
2654
        if membership.state in ACTUALLY_ACCEPTED:
2655
            membership.pending_application = membership.project.application
2656

    
2657
        membership.pending_serial = serial
2658
        membership.get_diff_quotas(sub_quota, add_quota)
2659
        membership.save()
2660

    
2661
    transaction.commit()
2662
    return serial, sub_quota, add_quota
2663

    
2664
def do_sync_projects():
2665
    serial, sub_quota, add_quota = set_sync_projects()
2666
    r = qh_add_quota(serial, sub_quota, add_quota)
2667
    if not r:
2668
        return serial
2669

    
2670
    m = "cannot sync serial: %d" % serial
2671
    logger.error(m)
2672
    logger.error("Failed: %s" % r)
2673

    
2674
    reset_serials([serial])
2675
    uuids = set(uuid for (uuid, resource) in r)
2676
    serial, sub_quota, add_quota = set_sync_projects(exclude=uuids)
2677
    r = qh_add_quota(serial, sub_quota, add_quota)
2678
    if not r:
2679
        return serial
2680

    
2681
    m = "cannot sync serial: %d" % serial
2682
    logger.error(m)
2683
    logger.error("Failed: %s" % r)
2684
    raise SyncError(m)
2685

    
2686
def post_sync_projects():
2687
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2688
    Q_ACTUALLY_ACCEPTED = ProjectMembership.Q_ACTUALLY_ACCEPTED
2689
    objs = Project.objects
2690

    
2691
    modified = objs.modified_projects().select_for_update()
2692
    for project in modified:
2693
        objects = project.projectmembership_set
2694
        q = objects.filter(Q_ACTUALLY_ACCEPTED & Q(is_pending=True))
2695
        memberships = list(q.select_for_update())
2696
        if not memberships:
2697
            project.is_modified = False
2698
            project.save()
2699

    
2700
    reactivating = objs.reactivating_projects().select_for_update()
2701
    for project in reactivating:
2702
        objects = project.projectmembership_set
2703
        q = objects.filter(Q(state=PROJECT_DEACTIVATED) | Q(is_pending=True))
2704
        memberships = list(q.select_for_update())
2705
        if not memberships:
2706
            project.reactivate()
2707
            project.save()
2708

    
2709
    deactivating = objs.deactivating_projects().select_for_update()
2710
    for project in deactivating:
2711
        objects = project.projectmembership_set
2712
        q = objects.filter(Q_ACTUALLY_ACCEPTED | Q(is_pending=True))
2713
        memberships = list(q.select_for_update())
2714
        if not memberships:
2715
            project.deactivate()
2716
            project.save()
2717

    
2718
    transaction.commit()
2719

    
2720
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2721
    @with_lock(retries, retry_wait)
2722
    def _sync_projects(sync):
2723
        sync_finish_serials()
2724
        # Informative only -- no select_for_update()
2725
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2726

    
2727
        projects_log = pre_sync_projects(sync)
2728
        if sync:
2729
            serial = do_sync_projects()
2730
            sync_finish_serials([serial])
2731
            post_sync_projects()
2732

    
2733
        return (pending, projects_log)
2734
    return _sync_projects(sync)
2735

    
2736

    
2737

    
2738
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2739
    @with_lock(retries, retry_wait)
2740
    def _sync_users(users, sync):
2741
        sync_finish_serials()
2742

    
2743
        info = {}
2744
        for user in users:
2745
            info[user.uuid] = user.email
2746

    
2747
        existing, nonexisting = qh_check_users(users)
2748
        resources = get_resource_names()
2749
        qh_limits, qh_counters = qh_get_quotas(existing, resources)
2750
        astakos_initial = initial_quotas(users)
2751
        astakos_quotas = users_quotas(users, astakos_initial)
2752

    
2753
        diff_quotas = {}
2754
        for holder, local in astakos_quotas.iteritems():
2755
            registered = qh_limits.get(holder, None)
2756
            if local != registered:
2757
                diff_quotas[holder] = dict(local)
2758

    
2759
        if sync:
2760
            r = register_users(nonexisting)
2761
            r = send_quotas(diff_quotas)
2762

    
2763
        return (existing, nonexisting,
2764
                qh_limits, qh_counters,
2765
                astakos_initial, diff_quotas, info)
2766
    return _sync_users(users, sync)
2767

    
2768

    
2769
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2770
    users = AstakosUser.objects.verified()
2771
    return sync_users(users, sync, retries, retry_wait)
2772

    
2773
class ProjectMembershipHistory(models.Model):
2774
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2775
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2776

    
2777
    person  =   models.BigIntegerField()
2778
    project =   models.BigIntegerField()
2779
    date    =   models.DateField(auto_now_add=True)
2780
    reason  =   models.IntegerField()
2781
    serial  =   models.BigIntegerField()
2782

    
2783
### SIGNALS ###
2784
################
2785

    
2786
def create_astakos_user(u):
2787
    try:
2788
        AstakosUser.objects.get(user_ptr=u.pk)
2789
    except AstakosUser.DoesNotExist:
2790
        extended_user = AstakosUser(user_ptr_id=u.pk)
2791
        extended_user.__dict__.update(u.__dict__)
2792
        extended_user.save()
2793
        if not extended_user.has_auth_provider('local'):
2794
            extended_user.add_auth_provider('local')
2795
    except BaseException, e:
2796
        logger.exception(e)
2797

    
2798
def fix_superusers():
2799
    # Associate superusers with AstakosUser
2800
    admins = User.objects.filter(is_superuser=True)
2801
    for u in admins:
2802
        create_astakos_user(u)
2803

    
2804
def user_post_save(sender, instance, created, **kwargs):
2805
    if not created:
2806
        return
2807
    create_astakos_user(instance)
2808
post_save.connect(user_post_save, sender=User)
2809

    
2810
def astakosuser_post_save(sender, instance, created, **kwargs):
2811
    pass
2812

    
2813
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2814

    
2815
def resource_post_save(sender, instance, created, **kwargs):
2816
    pass
2817

    
2818
post_save.connect(resource_post_save, sender=Resource)
2819

    
2820
def renew_token(sender, instance, **kwargs):
2821
    if not instance.auth_token:
2822
        instance.renew_token()
2823
pre_save.connect(renew_token, sender=AstakosUser)
2824
pre_save.connect(renew_token, sender=Service)