Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 88f4d3b3

History | View | Annotate | Download (94.3 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
    @realname.setter
414
    def realname(self, value):
415
        parts = value.split(' ')
416
        if len(parts) == 2:
417
            self.first_name = parts[0]
418
            self.last_name = parts[1]
419
        else:
420
            self.last_name = parts[0]
421

    
422
    def add_permission(self, pname):
423
        if self.has_perm(pname):
424
            return
425
        p, created = Permission.objects.get_or_create(
426
                                    codename=pname,
427
                                    name=pname.capitalize(),
428
                                    content_type=get_content_type())
429
        self.user_permissions.add(p)
430

    
431
    def remove_permission(self, pname):
432
        if self.has_perm(pname):
433
            return
434
        p = Permission.objects.get(codename=pname,
435
                                   content_type=get_content_type())
436
        self.user_permissions.remove(p)
437

    
438
    def is_project_admin(self, application_id=None):
439
        return self.uuid in PROJECT_ADMINS
440

    
441
    @property
442
    def invitation(self):
443
        try:
444
            return Invitation.objects.get(username=self.email)
445
        except Invitation.DoesNotExist:
446
            return None
447

    
448
    @property
449
    def policies(self):
450
        return self.astakosuserquota_set.select_related().all()
451

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

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

    
482
    def get_resource_policy(self, resource):
483
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
484
        resource = Resource.objects.get(service__name=s, name=r)
485
        default_capacity = resource.uplimit
486
        try:
487
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
488
            return policy, default_capacity
489
        except AstakosUserQuota.DoesNotExist:
490
            return None, default_capacity
491

    
492
    def remove_resource_policy(self, service, resource):
493
        """Raises ObjectDoesNotExist, IntegrityError"""
494
        resource = Resource.objects.get(service__name=service, name=resource)
495
        q = self.policies.get(resource=resource).delete()
496

    
497
    def update_uuid(self):
498
        while not self.uuid:
499
            uuid_val =  str(uuid.uuid4())
500
            try:
501
                AstakosUser.objects.get(uuid=uuid_val)
502
            except AstakosUser.DoesNotExist, e:
503
                self.uuid = uuid_val
504
        return self.uuid
505

    
506
    def save(self, update_timestamps=True, **kwargs):
507
        if update_timestamps:
508
            if not self.id:
509
                self.date_joined = datetime.now()
510
            self.updated = datetime.now()
511

    
512
        # update date_signed_terms if necessary
513
        if self.__has_signed_terms != self.has_signed_terms:
514
            self.date_signed_terms = datetime.now()
515

    
516
        self.update_uuid()
517

    
518
        if self.username != self.email.lower():
519
            # set username
520
            self.username = self.email.lower()
521

    
522
        super(AstakosUser, self).save(**kwargs)
523

    
524
    def renew_token(self, flush_sessions=False, current_key=None):
525
        md5 = hashlib.md5()
526
        md5.update(settings.SECRET_KEY)
527
        md5.update(self.username)
528
        md5.update(self.realname.encode('ascii', 'ignore'))
529
        md5.update(asctime())
530

    
531
        self.auth_token = b64encode(md5.digest())
532
        self.auth_token_created = datetime.now()
533
        self.auth_token_expires = self.auth_token_created + \
534
                                  timedelta(hours=AUTH_TOKEN_DURATION)
535
        if flush_sessions:
536
            self.flush_sessions(current_key)
537
        msg = 'Token renewed for %s' % self.email
538
        logger.log(LOGGING_LEVEL, msg)
539

    
540
    def flush_sessions(self, current_key=None):
541
        q = self.sessions
542
        if current_key:
543
            q = q.exclude(session_key=current_key)
544

    
545
        keys = q.values_list('session_key', flat=True)
546
        if keys:
547
            msg = 'Flushing sessions: %s' % ','.join(keys)
548
            logger.log(LOGGING_LEVEL, msg, [])
549
        engine = import_module(settings.SESSION_ENGINE)
550
        for k in keys:
551
            s = engine.SessionStore(k)
552
            s.flush()
553

    
554
    def __unicode__(self):
555
        return '%s (%s)' % (self.realname, self.email)
556

    
557
    def conflicting_email(self):
558
        q = AstakosUser.objects.exclude(username=self.username)
559
        q = q.filter(email__iexact=self.email)
560
        if q.count() != 0:
561
            return True
562
        return False
563

    
564
    def email_change_is_pending(self):
565
        return self.emailchanges.count() > 0
566

    
567
    @property
568
    def signed_terms(self):
569
        term = get_latest_terms()
570
        if not term:
571
            return True
572
        if not self.has_signed_terms:
573
            return False
574
        if not self.date_signed_terms:
575
            return False
576
        if self.date_signed_terms < term.date:
577
            self.has_signed_terms = False
578
            self.date_signed_terms = None
579
            self.save()
580
            return False
581
        return True
582

    
583
    def set_invitations_level(self):
584
        """
585
        Update user invitation level
586
        """
587
        level = self.invitation.inviter.level + 1
588
        self.level = level
589
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
590

    
591
    def can_login_with_auth_provider(self, provider):
592
        if not self.has_auth_provider(provider):
593
            return False
594
        else:
595
            return auth_providers.get_provider(provider).is_available_for_login()
596

    
597
    def can_add_auth_provider(self, provider, include_unverified=False, **kwargs):
598
        provider_settings = auth_providers.get_provider(provider)
599

    
600
        if not provider_settings.is_available_for_add():
601
            return False
602

    
603
        if self.has_auth_provider(provider) and \
604
           provider_settings.one_per_user:
605
            return False
606

    
607
        if 'provider_info' in kwargs:
608
            kwargs.pop('provider_info')
609

    
610
        if 'identifier' in kwargs:
611
            try:
612
                # provider with specified params already exist
613
                if not include_unverified:
614
                    kwargs['user__email_verified'] = True
615
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
616
                                                                   **kwargs)
617
            except AstakosUser.DoesNotExist:
618
                return True
619
            else:
620
                return False
621

    
622
        return True
623

    
624
    def can_remove_auth_provider(self, module):
625
        provider = auth_providers.get_provider(module)
626
        existing = self.get_active_auth_providers()
627
        existing_for_provider = self.get_active_auth_providers(module=module)
628

    
629
        if len(existing) <= 1:
630
            return False
631

    
632
        if len(existing_for_provider) == 1 and provider.is_required():
633
            return False
634

    
635
        return provider.is_available_for_remove()
636

    
637
    def can_change_password(self):
638
        return self.has_auth_provider('local', auth_backend='astakos')
639

    
640
    def can_change_email(self):
641
        non_astakos_local = self.get_auth_providers().filter(module='local')
642
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
643
        return non_astakos_local.count() == 0
644

    
645
    def has_required_auth_providers(self):
646
        required = auth_providers.REQUIRED_PROVIDERS
647
        for provider in required:
648
            if not self.has_auth_provider(provider):
649
                return False
650
        return True
651

    
652
    def has_auth_provider(self, provider, **kwargs):
653
        return bool(self.get_auth_providers().filter(module=provider,
654
                                               **kwargs).count())
655

    
656
    def add_auth_provider(self, provider, **kwargs):
657
        info_data = ''
658
        if 'provider_info' in kwargs:
659
            info_data = kwargs.pop('provider_info')
660
            if isinstance(info_data, dict):
661
                info_data = json.dumps(info_data)
662

    
663
        if self.can_add_auth_provider(provider, **kwargs):
664
            if 'identifier' in kwargs:
665
                # clean up third party pending for activation users of the same
666
                # identifier
667
                AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
668
                                                                **kwargs)
669
            self.auth_providers.create(module=provider, active=True,
670
                                       info_data=info_data,
671
                                       **kwargs)
672
        else:
673
            raise Exception('Cannot add provider')
674

    
675
    def add_pending_auth_provider(self, pending):
676
        """
677
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
678
        the current user.
679
        """
680
        if not isinstance(pending, PendingThirdPartyUser):
681
            pending = PendingThirdPartyUser.objects.get(token=pending)
682

    
683
        provider = self.add_auth_provider(pending.provider,
684
                               identifier=pending.third_party_identifier,
685
                                affiliation=pending.affiliation,
686
                                          provider_info=pending.info)
687

    
688
        if email_re.match(pending.email or '') and pending.email != self.email:
689
            self.additionalmail_set.get_or_create(email=pending.email)
690

    
691
        pending.delete()
692
        return provider
693

    
694
    def remove_auth_provider(self, provider, **kwargs):
695
        self.get_auth_providers().get(module=provider, **kwargs).delete()
696

    
697
    # user urls
698
    def get_resend_activation_url(self):
699
        return reverse('send_activation', kwargs={'user_id': self.pk})
700

    
701
    def get_provider_remove_url(self, module, **kwargs):
702
        return reverse('remove_auth_provider', kwargs={
703
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
704

    
705
    def get_activation_url(self, nxt=False):
706
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
707
                                 quote(self.auth_token))
708
        if nxt:
709
            url += "&next=%s" % quote(nxt)
710
        return url
711

    
712
    def get_password_reset_url(self, token_generator=default_token_generator):
713
        return reverse('django.contrib.auth.views.password_reset_confirm',
714
                          kwargs={'uidb36':int_to_base36(self.id),
715
                                  'token':token_generator.make_token(self)})
716

    
717
    def get_primary_auth_provider(self):
718
        return self.get_auth_providers().filter()[0]
719

    
720
    def get_auth_providers(self):
721
        return self.auth_providers
722

    
723
    def get_available_auth_providers(self):
724
        """
725
        Returns a list of providers available for user to connect to.
726
        """
727
        providers = []
728
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
729
            if self.can_add_auth_provider(module):
730
                providers.append(provider_settings(self))
731

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

    
740
    def get_active_auth_providers(self, **filters):
741
        providers = []
742
        for provider in self.get_auth_providers().active(**filters):
743
            if auth_providers.get_provider(provider.module).is_available_for_login():
744
                providers.append(provider)
745

    
746
        modules = astakos_settings.IM_MODULES
747
        def key(p):
748
            if not p.module in modules:
749
                return 100
750
            return modules.index(p.module)
751
        providers = sorted(providers, key=key)
752
        return providers
753

    
754
    @property
755
    def auth_providers_display(self):
756
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
757

    
758
    def get_inactive_message(self):
759
        msg_extra = ''
760
        message = ''
761
        if self.activation_sent:
762
            if self.email_verified:
763
                message = _(astakos_messages.ACCOUNT_INACTIVE)
764
            else:
765
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
766
                if astakos_settings.MODERATION_ENABLED:
767
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
768
                else:
769
                    url = self.get_resend_activation_url()
770
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
771
                                u' ' + \
772
                                _('<a href="%s">%s?</a>') % (url,
773
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
774
        else:
775
            if astakos_settings.MODERATION_ENABLED:
776
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
777
            else:
778
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
779
                url = self.get_resend_activation_url()
780
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
781
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
782

    
783
        return mark_safe(message + u' '+ msg_extra)
784

    
785
    def owns_application(self, application):
786
        return application.owner == self
787

    
788
    def owns_project(self, project):
789
        return project.application.owner == self
790

    
791
    def is_associated(self, project):
792
        try:
793
            m = ProjectMembership.objects.get(person=self, project=project)
794
            return m.state in ProjectMembership.ASSOCIATED_STATES
795
        except ProjectMembership.DoesNotExist:
796
            return False
797

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

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

    
813
    def non_owner_can_view(self, maybe_project):
814
        if self.is_project_admin():
815
            return True
816
        if maybe_project is None:
817
            return False
818
        project = maybe_project
819
        if self.is_associated(project):
820
            return True
821
        if project.is_deactivated():
822
            return False
823
        return True
824

    
825
    def settings(self):
826
        return UserSetting.objects.filter(user=self)
827

    
828
    def all_quotas(self):
829
        quotas = users_quotas([self])
830
        try:
831
            return quotas[self.uuid]
832
        except:
833
            raise ValueError("could not compute quotas")
834

    
835

    
836
def initial_quotas(users):
837
    initial = {}
838
    default_quotas = get_default_quota()
839

    
840
    for user in users:
841
        uuid = user.uuid
842
        initial[uuid] = dict(default_quotas)
843

    
844
    objs = AstakosUserQuota.objects.select_related()
845
    orig_quotas = objs.filter(user__in=users)
846
    for user_quota in orig_quotas:
847
        uuid = user_quota.user.uuid
848
        user_init = initial.get(uuid, {})
849
        resource = user_quota.resource.full_name()
850
        user_init[resource] = user_quota.quota_values()
851
        initial[uuid] = user_init
852

    
853
    return initial
854

    
855

    
856
def users_quotas(users, initial=None):
857
    if initial is None:
858
        quotas = initial_quotas(users)
859
    else:
860
        quotas = copy.deepcopy(initial)
861

    
862
    objs = ProjectMembership.objects.select_related('application', 'person')
863
    memberships = objs.filter(person__in=users, is_active=True)
864

    
865
    apps = set(m.application for m in memberships if m.application is not None)
866
    objs = ProjectResourceGrant.objects.select_related()
867
    grants = objs.filter(project_application__in=apps)
868

    
869
    for membership in memberships:
870
        uuid = membership.person.uuid
871
        userquotas = quotas.get(uuid, {})
872

    
873
        application = membership.application
874
        if application is None:
875
            m = _("missing application for active membership %s"
876
                  % (membership,))
877
            raise AssertionError(m)
878

    
879
        for grant in grants:
880
            if grant.project_application_id != application.id:
881
                continue
882
            resource = grant.resource.full_name()
883
            prev = userquotas.get(resource, 0)
884
            new = add_quota_values(prev, grant.member_quota_values())
885
            userquotas[resource] = new
886
        quotas[uuid] = userquotas
887

    
888
    return quotas
889

    
890

    
891
class AstakosUserAuthProviderManager(models.Manager):
892

    
893
    def active(self, **filters):
894
        return self.filter(active=True, **filters)
895

    
896
    def remove_unverified_providers(self, provider, **filters):
897
        try:
898
            existing = self.filter(module=provider, user__email_verified=False, **filters)
899
            for p in existing:
900
                p.user.delete()
901
        except:
902
            pass
903

    
904

    
905

    
906
class AstakosUserAuthProvider(models.Model):
907
    """
908
    Available user authentication methods.
909
    """
910
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
911
                                   null=True, default=None)
912
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
913
    module = models.CharField(_('Provider'), max_length=255, blank=False,
914
                                default='local')
915
    identifier = models.CharField(_('Third-party identifier'),
916
                                              max_length=255, null=True,
917
                                              blank=True)
918
    active = models.BooleanField(default=True)
919
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
920
                                   default='astakos')
921
    info_data = models.TextField(default="", null=True, blank=True)
922
    created = models.DateTimeField('Creation date', auto_now_add=True)
923

    
924
    objects = AstakosUserAuthProviderManager()
925

    
926
    class Meta:
927
        unique_together = (('identifier', 'module', 'user'), )
928
        ordering = ('module', 'created')
929

    
930
    def __init__(self, *args, **kwargs):
931
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
932
        try:
933
            self.info = json.loads(self.info_data)
934
            if not self.info:
935
                self.info = {}
936
        except Exception, e:
937
            self.info = {}
938

    
939
        for key,value in self.info.iteritems():
940
            setattr(self, 'info_%s' % key, value)
941

    
942

    
943
    @property
944
    def settings(self):
945
        return auth_providers.get_provider(self.module)
946

    
947
    @property
948
    def details_display(self):
949
        try:
950
            params = self.user.__dict__
951
            params.update(self.__dict__)
952
            return self.settings.get_details_tpl_display % params
953
        except:
954
            return ''
955

    
956
    @property
957
    def title_display(self):
958
        title_tpl = self.settings.get_title_display
959
        try:
960
            if self.settings.get_user_title_display:
961
                title_tpl = self.settings.get_user_title_display
962
        except Exception, e:
963
            pass
964
        try:
965
          return title_tpl % self.__dict__
966
        except:
967
          return self.settings.get_title_display % self.__dict__
968

    
969
    def can_remove(self):
970
        return self.user.can_remove_auth_provider(self.module)
971

    
972
    def delete(self, *args, **kwargs):
973
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
974
        if self.module == 'local':
975
            self.user.set_unusable_password()
976
            self.user.save()
977
        return ret
978

    
979
    def __repr__(self):
980
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
981

    
982
    def __unicode__(self):
983
        if self.identifier:
984
            return "%s:%s" % (self.module, self.identifier)
985
        if self.auth_backend:
986
            return "%s:%s" % (self.module, self.auth_backend)
987
        return self.module
988

    
989
    def save(self, *args, **kwargs):
990
        self.info_data = json.dumps(self.info)
991
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
992

    
993

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

    
1021
    update_or_create = _update_or_create
1022

    
1023

    
1024
class AstakosUserQuota(models.Model):
1025
    objects = ExtendedManager()
1026
    capacity = intDecimalField()
1027
    quantity = intDecimalField(default=0)
1028
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1029
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1030
    resource = models.ForeignKey(Resource)
1031
    user = models.ForeignKey(AstakosUser)
1032

    
1033
    class Meta:
1034
        unique_together = ("resource", "user")
1035

    
1036
    def quota_values(self):
1037
        return QuotaValues(
1038
            quantity = self.quantity,
1039
            capacity = self.capacity,
1040
            import_limit = self.import_limit,
1041
            export_limit = self.export_limit)
1042

    
1043

    
1044
class ApprovalTerms(models.Model):
1045
    """
1046
    Model for approval terms
1047
    """
1048

    
1049
    date = models.DateTimeField(
1050
        _('Issue date'), db_index=True, auto_now_add=True)
1051
    location = models.CharField(_('Terms location'), max_length=255)
1052

    
1053

    
1054
class Invitation(models.Model):
1055
    """
1056
    Model for registring invitations
1057
    """
1058
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1059
                                null=True)
1060
    realname = models.CharField(_('Real name'), max_length=255)
1061
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1062
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1063
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1064
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1065
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1066

    
1067
    def __init__(self, *args, **kwargs):
1068
        super(Invitation, self).__init__(*args, **kwargs)
1069
        if not self.id:
1070
            self.code = _generate_invitation_code()
1071

    
1072
    def consume(self):
1073
        self.is_consumed = True
1074
        self.consumed = datetime.now()
1075
        self.save()
1076

    
1077
    def __unicode__(self):
1078
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1079

    
1080

    
1081
class EmailChangeManager(models.Manager):
1082

    
1083
    @transaction.commit_on_success
1084
    def change_email(self, activation_key):
1085
        """
1086
        Validate an activation key and change the corresponding
1087
        ``User`` if valid.
1088

1089
        If the key is valid and has not expired, return the ``User``
1090
        after activating.
1091

1092
        If the key is not valid or has expired, return ``None``.
1093

1094
        If the key is valid but the ``User`` is already active,
1095
        return ``None``.
1096

1097
        After successful email change the activation record is deleted.
1098

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

    
1127

    
1128
class EmailChange(models.Model):
1129
    new_email_address = models.EmailField(
1130
        _(u'new e-mail address'),
1131
        help_text=_('Provide a new email address. Until you verify the new '
1132
                    'address by following the activation link that will be '
1133
                    'sent to it, your old email address will remain active.'))
1134
    user = models.ForeignKey(
1135
        AstakosUser, unique=True, related_name='emailchanges')
1136
    requested_at = models.DateTimeField(auto_now_add=True)
1137
    activation_key = models.CharField(
1138
        max_length=40, unique=True, db_index=True)
1139

    
1140
    objects = EmailChangeManager()
1141

    
1142
    def get_url(self):
1143
        return reverse('email_change_confirm',
1144
                      kwargs={'activation_key': self.activation_key})
1145

    
1146
    def activation_key_expired(self):
1147
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1148
        return self.requested_at + expiration_date < datetime.now()
1149

    
1150

    
1151
class AdditionalMail(models.Model):
1152
    """
1153
    Model for registring invitations
1154
    """
1155
    owner = models.ForeignKey(AstakosUser)
1156
    email = models.EmailField()
1157

    
1158

    
1159
def _generate_invitation_code():
1160
    while True:
1161
        code = randint(1, 2L ** 63 - 1)
1162
        try:
1163
            Invitation.objects.get(code=code)
1164
            # An invitation with this code already exists, try again
1165
        except Invitation.DoesNotExist:
1166
            return code
1167

    
1168

    
1169
def get_latest_terms():
1170
    try:
1171
        term = ApprovalTerms.objects.order_by('-id')[0]
1172
        return term
1173
    except IndexError:
1174
        pass
1175
    return None
1176

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

    
1196
    class Meta:
1197
        unique_together = ("provider", "third_party_identifier")
1198

    
1199
    def get_user_instance(self):
1200
        d = self.__dict__
1201
        d.pop('_state', None)
1202
        d.pop('id', None)
1203
        d.pop('token', None)
1204
        d.pop('created', None)
1205
        d.pop('info', None)
1206
        user = AstakosUser(**d)
1207

    
1208
        return user
1209

    
1210
    @property
1211
    def realname(self):
1212
        return '%s %s' %(self.first_name, self.last_name)
1213

    
1214
    @realname.setter
1215
    def realname(self, value):
1216
        parts = value.split(' ')
1217
        if len(parts) == 2:
1218
            self.first_name = parts[0]
1219
            self.last_name = parts[1]
1220
        else:
1221
            self.last_name = parts[0]
1222

    
1223
    def save(self, **kwargs):
1224
        if not self.id:
1225
            # set username
1226
            while not self.username:
1227
                username =  uuid.uuid4().hex[:30]
1228
                try:
1229
                    AstakosUser.objects.get(username = username)
1230
                except AstakosUser.DoesNotExist, e:
1231
                    self.username = username
1232
        super(PendingThirdPartyUser, self).save(**kwargs)
1233

    
1234
    def generate_token(self):
1235
        self.password = self.third_party_identifier
1236
        self.last_login = datetime.now()
1237
        self.token = default_token_generator.make_token(self)
1238

    
1239
    def existing_user(self):
1240
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1241
                                         auth_providers__identifier=self.third_party_identifier)
1242

    
1243
class SessionCatalog(models.Model):
1244
    session_key = models.CharField(_('session key'), max_length=40)
1245
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1246

    
1247

    
1248
class UserSetting(models.Model):
1249
    user = models.ForeignKey(AstakosUser)
1250
    setting = models.CharField(max_length=255)
1251
    value = models.IntegerField()
1252

    
1253
    objects = ForUpdateManager()
1254

    
1255
    class Meta:
1256
        unique_together = ("user", "setting")
1257

    
1258

    
1259
### PROJECTS ###
1260
################
1261

    
1262
def synced_model_metaclass(class_name, class_parents, class_attributes):
1263

    
1264
    new_attributes = {}
1265
    sync_attributes = {}
1266

    
1267
    for name, value in class_attributes.iteritems():
1268
        sync, underscore, rest = name.partition('_')
1269
        if sync == 'sync' and underscore == '_':
1270
            sync_attributes[rest] = value
1271
        else:
1272
            new_attributes[name] = value
1273

    
1274
    if 'prefix' not in sync_attributes:
1275
        m = ("you did not specify a 'sync_prefix' attribute "
1276
             "in class '%s'" % (class_name,))
1277
        raise ValueError(m)
1278

    
1279
    prefix = sync_attributes.pop('prefix')
1280
    class_name = sync_attributes.pop('classname', prefix + '_model')
1281

    
1282
    for name, value in sync_attributes.iteritems():
1283
        newname = prefix + '_' + name
1284
        if newname in new_attributes:
1285
            m = ("class '%s' was specified with prefix '%s' "
1286
                 "but it already has an attribute named '%s'"
1287
                 % (class_name, prefix, newname))
1288
            raise ValueError(m)
1289

    
1290
        new_attributes[newname] = value
1291

    
1292
    newclass = type(class_name, class_parents, new_attributes)
1293
    return newclass
1294

    
1295

    
1296
def make_synced(prefix='sync', name='SyncedState'):
1297

    
1298
    the_name = name
1299
    the_prefix = prefix
1300

    
1301
    class SyncedState(models.Model):
1302

    
1303
        sync_classname      = the_name
1304
        sync_prefix         = the_prefix
1305
        __metaclass__       = synced_model_metaclass
1306

    
1307
        sync_new_state      = models.BigIntegerField(null=True)
1308
        sync_synced_state   = models.BigIntegerField(null=True)
1309
        STATUS_SYNCED       = 0
1310
        STATUS_PENDING      = 1
1311
        sync_status         = models.IntegerField(db_index=True)
1312

    
1313
        class Meta:
1314
            abstract = True
1315

    
1316
        class NotSynced(Exception):
1317
            pass
1318

    
1319
        def sync_init_state(self, state):
1320
            self.sync_synced_state = state
1321
            self.sync_new_state = state
1322
            self.sync_status = self.STATUS_SYNCED
1323

    
1324
        def sync_get_status(self):
1325
            return self.sync_status
1326

    
1327
        def sync_set_status(self):
1328
            if self.sync_new_state != self.sync_synced_state:
1329
                self.sync_status = self.STATUS_PENDING
1330
            else:
1331
                self.sync_status = self.STATUS_SYNCED
1332

    
1333
        def sync_set_synced(self):
1334
            self.sync_synced_state = self.sync_new_state
1335
            self.sync_status = self.STATUS_SYNCED
1336

    
1337
        def sync_get_synced_state(self):
1338
            return self.sync_synced_state
1339

    
1340
        def sync_set_new_state(self, new_state):
1341
            self.sync_new_state = new_state
1342
            self.sync_set_status()
1343

    
1344
        def sync_get_new_state(self):
1345
            return self.sync_new_state
1346

    
1347
        def sync_set_synced_state(self, synced_state):
1348
            self.sync_synced_state = synced_state
1349
            self.sync_set_status()
1350

    
1351
        def sync_get_pending_objects(self):
1352
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1353
            return self.objects.filter(**kw)
1354

    
1355
        def sync_get_synced_objects(self):
1356
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1357
            return self.objects.filter(**kw)
1358

    
1359
        def sync_verify_get_synced_state(self):
1360
            status = self.sync_get_status()
1361
            state = self.sync_get_synced_state()
1362
            verified = (status == self.STATUS_SYNCED)
1363
            return state, verified
1364

    
1365
        def sync_is_synced(self):
1366
            state, verified = self.sync_verify_get_synced_state()
1367
            return verified
1368

    
1369
    return SyncedState
1370

    
1371
SyncedState = make_synced(prefix='sync', name='SyncedState')
1372

    
1373

    
1374
class ChainManager(ForUpdateManager):
1375

    
1376
    def search_by_name(self, *search_strings):
1377
        projects = Project.objects.search_by_name(*search_strings)
1378
        chains = [p.id for p in projects]
1379
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1380
        apps = (app for app in apps if app.is_latest())
1381
        app_chains = [app.chain for app in apps if app.chain not in chains]
1382
        return chains + app_chains
1383

    
1384
    def all_full_state(self):
1385
        chains = self.all()
1386
        cids = [c.chain for c in chains]
1387
        projects = Project.objects.select_related('application').in_bulk(cids)
1388

    
1389
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1390
        chain_latest = dict(objs.values_list('chain', 'latest'))
1391

    
1392
        objs = ProjectApplication.objects.select_related('applicant')
1393
        apps = objs.in_bulk(chain_latest.values())
1394

    
1395
        d = {}
1396
        for chain in chains:
1397
            pk = chain.pk
1398
            project = projects.get(pk, None)
1399
            app = apps[chain_latest[pk]]
1400
            d[chain.pk] = chain.get_state(project, app)
1401

    
1402
        return d
1403

    
1404
    def of_project(self, project):
1405
        if project is None:
1406
            return None
1407
        try:
1408
            return self.get(chain=project.id)
1409
        except Chain.DoesNotExist:
1410
            raise AssertionError('project with no chain')
1411

    
1412

    
1413
class Chain(models.Model):
1414
    chain  =   models.AutoField(primary_key=True)
1415

    
1416
    def __str__(self):
1417
        return "%s" % (self.chain,)
1418

    
1419
    objects = ChainManager()
1420

    
1421
    PENDING            = 0
1422
    DENIED             = 3
1423
    DISMISSED          = 4
1424
    CANCELLED          = 5
1425

    
1426
    APPROVED           = 10
1427
    APPROVED_PENDING   = 11
1428
    SUSPENDED          = 12
1429
    SUSPENDED_PENDING  = 13
1430
    TERMINATED         = 14
1431
    TERMINATED_PENDING = 15
1432

    
1433
    PENDING_STATES = [PENDING,
1434
                      APPROVED_PENDING,
1435
                      SUSPENDED_PENDING,
1436
                      TERMINATED_PENDING,
1437
                      ]
1438

    
1439
    MODIFICATION_STATES = [APPROVED_PENDING,
1440
                           SUSPENDED_PENDING,
1441
                           TERMINATED_PENDING,
1442
                           ]
1443

    
1444
    RELEVANT_STATES = [PENDING,
1445
                       DENIED,
1446
                       APPROVED,
1447
                       APPROVED_PENDING,
1448
                       SUSPENDED,
1449
                       SUSPENDED_PENDING,
1450
                       TERMINATED_PENDING,
1451
                       ]
1452

    
1453
    SKIP_STATES = [DISMISSED,
1454
                   CANCELLED,
1455
                   TERMINATED]
1456

    
1457
    STATE_DISPLAY = {
1458
        PENDING            : _("Pending"),
1459
        DENIED             : _("Denied"),
1460
        DISMISSED          : _("Dismissed"),
1461
        CANCELLED          : _("Cancelled"),
1462
        APPROVED           : _("Active"),
1463
        APPROVED_PENDING   : _("Active - Pending"),
1464
        SUSPENDED          : _("Suspended"),
1465
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1466
        TERMINATED         : _("Terminated"),
1467
        TERMINATED_PENDING : _("Terminated - Pending"),
1468
        }
1469

    
1470

    
1471
    @classmethod
1472
    def _chain_state(cls, project_state, app_state):
1473
        s = CHAIN_STATE.get((project_state, app_state), None)
1474
        if s is None:
1475
            raise AssertionError('inconsistent chain state')
1476
        return s
1477

    
1478
    @classmethod
1479
    def chain_state(cls, project, app):
1480
        p_state = project.state if project else None
1481
        return cls._chain_state(p_state, app.state)
1482

    
1483
    @classmethod
1484
    def state_display(cls, s):
1485
        if s is None:
1486
            return _("Unknown")
1487
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1488

    
1489
    def last_application(self):
1490
        return self.chained_apps.order_by('-id')[0]
1491

    
1492
    def get_project(self):
1493
        try:
1494
            return self.chained_project
1495
        except Project.DoesNotExist:
1496
            return None
1497

    
1498
    def get_elements(self):
1499
        project = self.get_project()
1500
        app = self.last_application()
1501
        return project, app
1502

    
1503
    def get_state(self, project, app):
1504
        s = self.chain_state(project, app)
1505
        return s, project, app
1506

    
1507
    def full_state(self):
1508
        project, app = self.get_elements()
1509
        return self.get_state(project, app)
1510

    
1511

    
1512
def new_chain():
1513
    c = Chain.objects.create()
1514
    return c
1515

    
1516

    
1517
class ProjectApplicationManager(ForUpdateManager):
1518

    
1519
    def user_visible_projects(self, *filters, **kw_filters):
1520
        model = self.model
1521
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1522

    
1523
    def user_visible_by_chain(self, flt):
1524
        model = self.model
1525
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1526
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1527
        by_chain = dict(pending.annotate(models.Max('id')))
1528
        by_chain.update(approved.annotate(models.Max('id')))
1529
        return self.filter(flt, id__in=by_chain.values())
1530

    
1531
    def user_accessible_projects(self, user):
1532
        """
1533
        Return projects accessed by specified user.
1534
        """
1535
        if user.is_project_admin():
1536
            participates_filters = Q()
1537
        else:
1538
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1539
                                   Q(project__projectmembership__person=user)
1540

    
1541
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1542

    
1543
    def search_by_name(self, *search_strings):
1544
        q = Q()
1545
        for s in search_strings:
1546
            q = q | Q(name__icontains=s)
1547
        return self.filter(q)
1548

    
1549
    def latest_of_chain(self, chain_id):
1550
        try:
1551
            return self.filter(chain=chain_id).order_by('-id')[0]
1552
        except IndexError:
1553
            return None
1554

    
1555

    
1556
class ProjectApplication(models.Model):
1557
    applicant               =   models.ForeignKey(
1558
                                    AstakosUser,
1559
                                    related_name='projects_applied',
1560
                                    db_index=True)
1561

    
1562
    PENDING     =    0
1563
    APPROVED    =    1
1564
    REPLACED    =    2
1565
    DENIED      =    3
1566
    DISMISSED   =    4
1567
    CANCELLED   =    5
1568

    
1569
    state                   =   models.IntegerField(default=PENDING,
1570
                                                    db_index=True)
1571

    
1572
    owner                   =   models.ForeignKey(
1573
                                    AstakosUser,
1574
                                    related_name='projects_owned',
1575
                                    db_index=True)
1576

    
1577
    chain                   =   models.ForeignKey(Chain,
1578
                                                  related_name='chained_apps',
1579
                                                  db_column='chain')
1580
    precursor_application   =   models.ForeignKey('ProjectApplication',
1581
                                                  null=True,
1582
                                                  blank=True)
1583

    
1584
    name                    =   models.CharField(max_length=80)
1585
    homepage                =   models.URLField(max_length=255, null=True,
1586
                                                verify_exists=False)
1587
    description             =   models.TextField(null=True, blank=True)
1588
    start_date              =   models.DateTimeField(null=True, blank=True)
1589
    end_date                =   models.DateTimeField()
1590
    member_join_policy      =   models.IntegerField()
1591
    member_leave_policy     =   models.IntegerField()
1592
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1593
    resource_grants         =   models.ManyToManyField(
1594
                                    Resource,
1595
                                    null=True,
1596
                                    blank=True,
1597
                                    through='ProjectResourceGrant')
1598
    comments                =   models.TextField(null=True, blank=True)
1599
    issue_date              =   models.DateTimeField(auto_now_add=True)
1600
    response_date           =   models.DateTimeField(null=True, blank=True)
1601

    
1602
    objects                 =   ProjectApplicationManager()
1603

    
1604
    # Compiled queries
1605
    Q_PENDING  = Q(state=PENDING)
1606
    Q_APPROVED = Q(state=APPROVED)
1607
    Q_DENIED   = Q(state=DENIED)
1608

    
1609
    class Meta:
1610
        unique_together = ("chain", "id")
1611

    
1612
    def __unicode__(self):
1613
        return "%s applied by %s" % (self.name, self.applicant)
1614

    
1615
    # TODO: Move to a more suitable place
1616
    APPLICATION_STATE_DISPLAY = {
1617
        PENDING  : _('Pending review'),
1618
        APPROVED : _('Approved'),
1619
        REPLACED : _('Replaced'),
1620
        DENIED   : _('Denied'),
1621
        DISMISSED: _('Dismissed'),
1622
        CANCELLED: _('Cancelled')
1623
    }
1624

    
1625
    def get_project(self):
1626
        try:
1627
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1628
            return Project
1629
        except Project.DoesNotExist, e:
1630
            return None
1631

    
1632
    def state_display(self):
1633
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1634

    
1635
    def project_state_display(self):
1636
        try:
1637
            project = self.project
1638
            return project.state_display()
1639
        except Project.DoesNotExist:
1640
            return self.state_display()
1641

    
1642
    def add_resource_policy(self, service, resource, uplimit):
1643
        """Raises ObjectDoesNotExist, IntegrityError"""
1644
        q = self.projectresourcegrant_set
1645
        resource = Resource.objects.get(service__name=service, name=resource)
1646
        q.create(resource=resource, member_capacity=uplimit)
1647

    
1648
    def members_count(self):
1649
        return self.project.approved_memberships.count()
1650

    
1651
    @property
1652
    def grants(self):
1653
        return self.projectresourcegrant_set.values(
1654
            'member_capacity', 'resource__name', 'resource__service__name')
1655

    
1656
    @property
1657
    def resource_policies(self):
1658
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1659

    
1660
    @resource_policies.setter
1661
    def resource_policies(self, policies):
1662
        for p in policies:
1663
            service = p.get('service', None)
1664
            resource = p.get('resource', None)
1665
            uplimit = p.get('uplimit', 0)
1666
            self.add_resource_policy(service, resource, uplimit)
1667

    
1668
    def pending_modifications_incl_me(self):
1669
        q = self.chained_applications()
1670
        q = q.filter(Q(state=self.PENDING))
1671
        return q
1672

    
1673
    def last_pending_incl_me(self):
1674
        try:
1675
            return self.pending_modifications_incl_me().order_by('-id')[0]
1676
        except IndexError:
1677
            return None
1678

    
1679
    def pending_modifications(self):
1680
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1681

    
1682
    def last_pending(self):
1683
        try:
1684
            return self.pending_modifications().order_by('-id')[0]
1685
        except IndexError:
1686
            return None
1687

    
1688
    def is_modification(self):
1689
        # if self.state != self.PENDING:
1690
        #     return False
1691
        parents = self.chained_applications().filter(id__lt=self.id)
1692
        parents = parents.filter(state__in=[self.APPROVED])
1693
        return parents.count() > 0
1694

    
1695
    def chained_applications(self):
1696
        return ProjectApplication.objects.filter(chain=self.chain)
1697

    
1698
    def is_latest(self):
1699
        return self.chained_applications().order_by('-id')[0] == self
1700

    
1701
    def has_pending_modifications(self):
1702
        return bool(self.last_pending())
1703

    
1704
    def denied_modifications(self):
1705
        q = self.chained_applications()
1706
        q = q.filter(Q(state=self.DENIED))
1707
        q = q.filter(~Q(id=self.id))
1708
        return q
1709

    
1710
    def last_denied(self):
1711
        try:
1712
            return self.denied_modifications().order_by('-id')[0]
1713
        except IndexError:
1714
            return None
1715

    
1716
    def has_denied_modifications(self):
1717
        return bool(self.last_denied())
1718

    
1719
    def is_applied(self):
1720
        try:
1721
            self.project
1722
            return True
1723
        except Project.DoesNotExist:
1724
            return False
1725

    
1726
    def get_project(self):
1727
        try:
1728
            return Project.objects.get(id=self.chain)
1729
        except Project.DoesNotExist:
1730
            return None
1731

    
1732
    def project_exists(self):
1733
        return self.get_project() is not None
1734

    
1735
    def _get_project_for_update(self):
1736
        try:
1737
            objects = Project.objects
1738
            project = objects.get_for_update(id=self.chain)
1739
            return project
1740
        except Project.DoesNotExist:
1741
            return None
1742

    
1743
    def can_cancel(self):
1744
        return self.state == self.PENDING
1745

    
1746
    def cancel(self):
1747
        if not self.can_cancel():
1748
            m = _("cannot cancel: application '%s' in state '%s'") % (
1749
                    self.id, self.state)
1750
            raise AssertionError(m)
1751

    
1752
        self.state = self.CANCELLED
1753
        self.save()
1754

    
1755
    def can_dismiss(self):
1756
        return self.state == self.DENIED
1757

    
1758
    def dismiss(self):
1759
        if not self.can_dismiss():
1760
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1761
                    self.id, self.state)
1762
            raise AssertionError(m)
1763

    
1764
        self.state = self.DISMISSED
1765
        self.save()
1766

    
1767
    def can_deny(self):
1768
        return self.state == self.PENDING
1769

    
1770
    def deny(self):
1771
        if not self.can_deny():
1772
            m = _("cannot deny: application '%s' in state '%s'") % (
1773
                    self.id, self.state)
1774
            raise AssertionError(m)
1775

    
1776
        self.state = self.DENIED
1777
        self.response_date = datetime.now()
1778
        self.save()
1779

    
1780
    def can_approve(self):
1781
        return self.state == self.PENDING
1782

    
1783
    def approve(self, approval_user=None):
1784
        """
1785
        If approval_user then during owner membership acceptance
1786
        it is checked whether the request_user is eligible.
1787

1788
        Raises:
1789
            PermissionDenied
1790
        """
1791

    
1792
        if not transaction.is_managed():
1793
            raise AssertionError("NOPE")
1794

    
1795
        new_project_name = self.name
1796
        if not self.can_approve():
1797
            m = _("cannot approve: project '%s' in state '%s'") % (
1798
                    new_project_name, self.state)
1799
            raise AssertionError(m) # invalid argument
1800

    
1801
        now = datetime.now()
1802
        project = self._get_project_for_update()
1803

    
1804
        try:
1805
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1806
            conflicting_project = Project.objects.get(q)
1807
            if (conflicting_project != project):
1808
                m = (_("cannot approve: project with name '%s' "
1809
                       "already exists (id: %s)") % (
1810
                        new_project_name, conflicting_project.id))
1811
                raise PermissionDenied(m) # invalid argument
1812
        except Project.DoesNotExist:
1813
            pass
1814

    
1815
        new_project = False
1816
        if project is None:
1817
            new_project = True
1818
            project = Project(id=self.chain)
1819

    
1820
        project.name = new_project_name
1821
        project.application = self
1822
        project.last_approval_date = now
1823
        if not new_project:
1824
            project.is_modified = True
1825

    
1826
        project.save()
1827

    
1828
        self.state = self.APPROVED
1829
        self.response_date = now
1830
        self.save()
1831

    
1832
    @property
1833
    def member_join_policy_display(self):
1834
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1835

    
1836
    @property
1837
    def member_leave_policy_display(self):
1838
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1839

    
1840
class ProjectResourceGrant(models.Model):
1841

    
1842
    resource                =   models.ForeignKey(Resource)
1843
    project_application     =   models.ForeignKey(ProjectApplication,
1844
                                                  null=True)
1845
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1846
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1847
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1848
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1849
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1850
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1851

    
1852
    objects = ExtendedManager()
1853

    
1854
    class Meta:
1855
        unique_together = ("resource", "project_application")
1856

    
1857
    def member_quota_values(self):
1858
        return QuotaValues(
1859
            quantity = 0,
1860
            capacity = self.member_capacity,
1861
            import_limit = self.member_import_limit,
1862
            export_limit = self.member_export_limit)
1863

    
1864
    def display_member_capacity(self):
1865
        if self.member_capacity:
1866
            if self.resource.unit:
1867
                return ProjectResourceGrant.display_filesize(
1868
                    self.member_capacity)
1869
            else:
1870
                if math.isinf(self.member_capacity):
1871
                    return 'Unlimited'
1872
                else:
1873
                    return self.member_capacity
1874
        else:
1875
            return 'Unlimited'
1876

    
1877
    def __str__(self):
1878
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1879
                                        self.display_member_capacity())
1880

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

    
1905

    
1906
class ProjectManager(ForUpdateManager):
1907

    
1908
    def terminated_projects(self):
1909
        q = self.model.Q_TERMINATED
1910
        return self.filter(q)
1911

    
1912
    def not_terminated_projects(self):
1913
        q = ~self.model.Q_TERMINATED
1914
        return self.filter(q)
1915

    
1916
    def terminating_projects(self):
1917
        q = self.model.Q_TERMINATED & Q(is_active=True)
1918
        return self.filter(q)
1919

    
1920
    def deactivated_projects(self):
1921
        q = self.model.Q_DEACTIVATED
1922
        return self.filter(q)
1923

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

    
1928
    def modified_projects(self):
1929
        return self.filter(is_modified=True)
1930

    
1931
    def reactivating_projects(self):
1932
        return self.filter(state=Project.APPROVED, is_active=False)
1933

    
1934
    def expired_projects(self):
1935
        q = (~Q(state=Project.TERMINATED) &
1936
              Q(application__end_date__lt=datetime.now()))
1937
        return self.filter(q)
1938

    
1939
    def search_by_name(self, *search_strings):
1940
        q = Q()
1941
        for s in search_strings:
1942
            q = q | Q(name__icontains=s)
1943
        return self.filter(q)
1944

    
1945

    
1946
class Project(models.Model):
1947

    
1948
    id                          =   models.OneToOneField(Chain,
1949
                                                      related_name='chained_project',
1950
                                                      db_column='id',
1951
                                                      primary_key=True)
1952

    
1953
    application                 =   models.OneToOneField(
1954
                                            ProjectApplication,
1955
                                            related_name='project')
1956
    last_approval_date          =   models.DateTimeField(null=True)
1957

    
1958
    members                     =   models.ManyToManyField(
1959
                                            AstakosUser,
1960
                                            through='ProjectMembership')
1961

    
1962
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1963
    deactivation_date           =   models.DateTimeField(null=True)
1964

    
1965
    creation_date               =   models.DateTimeField(auto_now_add=True)
1966
    name                        =   models.CharField(
1967
                                            max_length=80,
1968
                                            null=True,
1969
                                            db_index=True,
1970
                                            unique=True)
1971

    
1972
    APPROVED    = 1
1973
    SUSPENDED   = 10
1974
    TERMINATED  = 100
1975

    
1976
    is_modified                 =   models.BooleanField(default=False,
1977
                                                        db_index=True)
1978
    is_active                   =   models.BooleanField(default=True,
1979
                                                        db_index=True)
1980
    state                       =   models.IntegerField(default=APPROVED,
1981
                                                        db_index=True)
1982

    
1983
    objects     =   ProjectManager()
1984

    
1985
    # Compiled queries
1986
    Q_TERMINATED  = Q(state=TERMINATED)
1987
    Q_SUSPENDED   = Q(state=SUSPENDED)
1988
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1989

    
1990
    def __str__(self):
1991
        return uenc(_("<project %s '%s'>") %
1992
                    (self.id, udec(self.application.name)))
1993

    
1994
    __repr__ = __str__
1995

    
1996
    def __unicode__(self):
1997
        return _("<project %s '%s'>") % (self.id, self.application.name)
1998

    
1999
    STATE_DISPLAY = {
2000
        APPROVED   : 'Active',
2001
        SUSPENDED  : 'Suspended',
2002
        TERMINATED : 'Terminated'
2003
        }
2004

    
2005
    def state_display(self):
2006
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
2007

    
2008
    def admin_state_display(self):
2009
        s = self.state_display()
2010
        if self.sync_pending():
2011
            s += ' (sync pending)'
2012
        return s
2013

    
2014
    def sync_pending(self):
2015
        if self.state != self.APPROVED:
2016
            return self.is_active
2017
        return not self.is_active or self.is_modified
2018

    
2019
    def expiration_info(self):
2020
        return (str(self.id), self.name, self.state_display(),
2021
                str(self.application.end_date))
2022

    
2023
    def is_deactivated(self, reason=None):
2024
        if reason is not None:
2025
            return self.state == reason
2026

    
2027
        return self.state != self.APPROVED
2028

    
2029
    def is_deactivating(self, reason=None):
2030
        if not self.is_active:
2031
            return False
2032

    
2033
        return self.is_deactivated(reason)
2034

    
2035
    def is_deactivated_strict(self, reason=None):
2036
        if self.is_active:
2037
            return False
2038

    
2039
        return self.is_deactivated(reason)
2040

    
2041
    ### Deactivation calls
2042

    
2043
    def deactivate(self):
2044
        self.deactivation_date = datetime.now()
2045
        self.is_active = False
2046

    
2047
    def reactivate(self):
2048
        self.deactivation_date = None
2049
        self.is_active = True
2050

    
2051
    def terminate(self):
2052
        self.deactivation_reason = 'TERMINATED'
2053
        self.state = self.TERMINATED
2054
        self.name = None
2055
        self.save()
2056

    
2057
    def suspend(self):
2058
        self.deactivation_reason = 'SUSPENDED'
2059
        self.state = self.SUSPENDED
2060
        self.save()
2061

    
2062
    def resume(self):
2063
        self.deactivation_reason = None
2064
        self.state = self.APPROVED
2065
        self.save()
2066

    
2067
    ### Logical checks
2068

    
2069
    def is_inconsistent(self):
2070
        now = datetime.now()
2071
        dates = [self.creation_date,
2072
                 self.last_approval_date,
2073
                 self.deactivation_date]
2074
        return any([date > now for date in dates])
2075

    
2076
    def is_active_strict(self):
2077
        return self.is_active and self.state == self.APPROVED
2078

    
2079
    def is_approved(self):
2080
        return self.state == self.APPROVED
2081

    
2082
    @property
2083
    def is_alive(self):
2084
        return not self.is_terminated
2085

    
2086
    @property
2087
    def is_terminated(self):
2088
        return self.is_deactivated(self.TERMINATED)
2089

    
2090
    @property
2091
    def is_suspended(self):
2092
        return self.is_deactivated(self.SUSPENDED)
2093

    
2094
    def violates_resource_grants(self):
2095
        return False
2096

    
2097
    def violates_members_limit(self, adding=0):
2098
        application = self.application
2099
        limit = application.limit_on_members_number
2100
        if limit is None:
2101
            return False
2102
        return (len(self.approved_members) + adding > limit)
2103

    
2104

    
2105
    ### Other
2106

    
2107
    def count_pending_memberships(self):
2108
        memb_set = self.projectmembership_set
2109
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2110
        return memb_count
2111

    
2112
    def members_count(self):
2113
        return self.approved_memberships.count()
2114

    
2115
    @property
2116
    def approved_memberships(self):
2117
        query = ProjectMembership.Q_ACCEPTED_STATES
2118
        return self.projectmembership_set.filter(query)
2119

    
2120
    @property
2121
    def approved_members(self):
2122
        return [m.person for m in self.approved_memberships]
2123

    
2124
    def add_member(self, user):
2125
        """
2126
        Raises:
2127
            django.exceptions.PermissionDenied
2128
            astakos.im.models.AstakosUser.DoesNotExist
2129
        """
2130
        if isinstance(user, (int, long)):
2131
            user = AstakosUser.objects.get(user=user)
2132

    
2133
        m, created = ProjectMembership.objects.get_or_create(
2134
            person=user, project=self
2135
        )
2136
        m.accept()
2137

    
2138
    def remove_member(self, user):
2139
        """
2140
        Raises:
2141
            django.exceptions.PermissionDenied
2142
            astakos.im.models.AstakosUser.DoesNotExist
2143
            astakos.im.models.ProjectMembership.DoesNotExist
2144
        """
2145
        if isinstance(user, (int, long)):
2146
            user = AstakosUser.objects.get(user=user)
2147

    
2148
        m = ProjectMembership.objects.get(person=user, project=self)
2149
        m.remove()
2150

    
2151

    
2152
CHAIN_STATE = {
2153
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2154
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2155
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2156
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2157
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2158

    
2159
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2160
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2161
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2162
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2163
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2164

    
2165
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2166
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2167
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2168
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2169
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2170

    
2171
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2172
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2173
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2174
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2175
    }
2176

    
2177

    
2178
class PendingMembershipError(Exception):
2179
    pass
2180

    
2181

    
2182
class ProjectMembershipManager(ForUpdateManager):
2183

    
2184
    def any_accepted(self):
2185
        q = (Q(state=ProjectMembership.ACCEPTED) |
2186
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
2187
        return self.filter(q)
2188

    
2189
    def actually_accepted(self):
2190
        q = self.model.Q_ACTUALLY_ACCEPTED
2191
        return self.filter(q)
2192

    
2193
    def requested(self):
2194
        return self.filter(state=ProjectMembership.REQUESTED)
2195

    
2196
    def suspended(self):
2197
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2198

    
2199
class ProjectMembership(models.Model):
2200

    
2201
    person              =   models.ForeignKey(AstakosUser)
2202
    request_date        =   models.DateField(auto_now_add=True)
2203
    project             =   models.ForeignKey(Project)
2204

    
2205
    REQUESTED           =   0
2206
    ACCEPTED            =   1
2207
    LEAVE_REQUESTED     =   5
2208
    # User deactivation
2209
    USER_SUSPENDED      =   10
2210
    # Project deactivation
2211
    PROJECT_DEACTIVATED =   100
2212

    
2213
    REMOVED             =   200
2214

    
2215
    ASSOCIATED_STATES   =   set([REQUESTED,
2216
                                 ACCEPTED,
2217
                                 LEAVE_REQUESTED,
2218
                                 USER_SUSPENDED,
2219
                                 PROJECT_DEACTIVATED])
2220

    
2221
    ACCEPTED_STATES     =   set([ACCEPTED,
2222
                                 LEAVE_REQUESTED,
2223
                                 USER_SUSPENDED,
2224
                                 PROJECT_DEACTIVATED])
2225

    
2226
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2227

    
2228
    state               =   models.IntegerField(default=REQUESTED,
2229
                                                db_index=True)
2230
    is_pending          =   models.BooleanField(default=False, db_index=True)
2231
    is_active           =   models.BooleanField(default=False, db_index=True)
2232
    application         =   models.ForeignKey(
2233
                                ProjectApplication,
2234
                                null=True,
2235
                                related_name='memberships')
2236
    pending_application =   models.ForeignKey(
2237
                                ProjectApplication,
2238
                                null=True,
2239
                                related_name='pending_memberships')
2240
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2241

    
2242
    acceptance_date     =   models.DateField(null=True, db_index=True)
2243
    leave_request_date  =   models.DateField(null=True)
2244

    
2245
    objects     =   ProjectMembershipManager()
2246

    
2247
    # Compiled queries
2248
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2249
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2250

    
2251
    MEMBERSHIP_STATE_DISPLAY = {
2252
        REQUESTED           : _('Requested'),
2253
        ACCEPTED            : _('Accepted'),
2254
        LEAVE_REQUESTED     : _('Leave Requested'),
2255
        USER_SUSPENDED      : _('Suspended'),
2256
        PROJECT_DEACTIVATED : _('Accepted'), # sic
2257
        REMOVED             : _('Pending removal'),
2258
        }
2259

    
2260
    USER_FRIENDLY_STATE_DISPLAY = {
2261
        REQUESTED           : _('Join requested'),
2262
        ACCEPTED            : _('Accepted member'),
2263
        LEAVE_REQUESTED     : _('Requested to leave'),
2264
        USER_SUSPENDED      : _('Suspended member'),
2265
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
2266
        REMOVED             : _('Pending removal'),
2267
        }
2268

    
2269
    def state_display(self):
2270
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2271

    
2272
    def user_friendly_state_display(self):
2273
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2274

    
2275
    def get_combined_state(self):
2276
        return self.state, self.is_active, self.is_pending
2277

    
2278
    class Meta:
2279
        unique_together = ("person", "project")
2280
        #index_together = [["project", "state"]]
2281

    
2282
    def __str__(self):
2283
        return uenc(_("<'%s' membership in '%s'>") % (
2284
                self.person.username, self.project))
2285

    
2286
    __repr__ = __str__
2287

    
2288
    def __init__(self, *args, **kwargs):
2289
        self.state = self.REQUESTED
2290
        super(ProjectMembership, self).__init__(*args, **kwargs)
2291

    
2292
    def _set_history_item(self, reason, date=None):
2293
        if isinstance(reason, basestring):
2294
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2295

    
2296
        history_item = ProjectMembershipHistory(
2297
                            serial=self.id,
2298
                            person=self.person_id,
2299
                            project=self.project_id,
2300
                            date=date or datetime.now(),
2301
                            reason=reason)
2302
        history_item.save()
2303
        serial = history_item.id
2304

    
2305
    def can_accept(self):
2306
        return self.state == self.REQUESTED
2307

    
2308
    def accept(self):
2309
        if self.is_pending:
2310
            m = _("%s: attempt to accept while is pending") % (self,)
2311
            raise AssertionError(m)
2312

    
2313
        if not self.can_accept():
2314
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2315
            raise AssertionError(m)
2316

    
2317
        now = datetime.now()
2318
        self.acceptance_date = now
2319
        self._set_history_item(reason='ACCEPT', date=now)
2320
        if self.project.is_approved():
2321
            self.state = self.ACCEPTED
2322
            self.is_pending = True
2323
        else:
2324
            self.state = self.PROJECT_DEACTIVATED
2325

    
2326
        self.save()
2327

    
2328
    def can_leave(self):
2329
        return self.state in self.ACCEPTED_STATES
2330

    
2331
    def leave_request(self):
2332
        if self.is_pending:
2333
            m = _("%s: attempt to request to leave while is pending") % (self,)
2334
            raise AssertionError(m)
2335

    
2336
        if not self.can_leave():
2337
            m = _("%s: attempt to request to leave in state '%s'") % (
2338
                self, self.state)
2339
            raise AssertionError(m)
2340

    
2341
        self.leave_request_date = datetime.now()
2342
        self.state = self.LEAVE_REQUESTED
2343
        self.save()
2344

    
2345
    def can_deny_leave(self):
2346
        return self.state == self.LEAVE_REQUESTED
2347

    
2348
    def leave_request_deny(self):
2349
        if self.is_pending:
2350
            m = _("%s: attempt to deny leave request while is pending") % (
2351
                self,)
2352
            raise AssertionError(m)
2353

    
2354
        if not self.can_deny_leave():
2355
            m = _("%s: attempt to deny leave request in state '%s'") % (
2356
                self, self.state)
2357
            raise AssertionError(m)
2358

    
2359
        self.leave_request_date = None
2360
        self.state = self.ACCEPTED
2361
        self.save()
2362

    
2363
    def can_cancel_leave(self):
2364
        return self.state == self.LEAVE_REQUESTED
2365

    
2366
    def leave_request_cancel(self):
2367
        if self.is_pending:
2368
            m = _("%s: attempt to cancel leave request while is pending") % (
2369
                self,)
2370
            raise AssertionError(m)
2371

    
2372
        if not self.can_cancel_leave():
2373
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2374
                self, self.state)
2375
            raise AssertionError(m)
2376

    
2377
        self.leave_request_date = None
2378
        self.state = self.ACCEPTED
2379
        self.save()
2380

    
2381
    def can_remove(self):
2382
        return self.state in self.ACCEPTED_STATES
2383

    
2384
    def remove(self):
2385
        if self.is_pending:
2386
            m = _("%s: attempt to remove while is pending") % (self,)
2387
            raise AssertionError(m)
2388

    
2389
        if not self.can_remove():
2390
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2391
            raise AssertionError(m)
2392

    
2393
        self._set_history_item(reason='REMOVE')
2394
        self.state = self.REMOVED
2395
        self.is_pending = True
2396
        self.save()
2397

    
2398
    def can_reject(self):
2399
        return self.state == self.REQUESTED
2400

    
2401
    def reject(self):
2402
        if self.is_pending:
2403
            m = _("%s: attempt to reject while is pending") % (self,)
2404
            raise AssertionError(m)
2405

    
2406
        if not self.can_reject():
2407
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2408
            raise AssertionError(m)
2409

    
2410
        # rejected requests don't need sync,
2411
        # because they were never effected
2412
        self._set_history_item(reason='REJECT')
2413
        self.delete()
2414

    
2415
    def can_cancel(self):
2416
        return self.state == self.REQUESTED
2417

    
2418
    def cancel(self):
2419
        if self.is_pending:
2420
            m = _("%s: attempt to cancel while is pending") % (self,)
2421
            raise AssertionError(m)
2422

    
2423
        if not self.can_cancel():
2424
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2425
            raise AssertionError(m)
2426

    
2427
        # rejected requests don't need sync,
2428
        # because they were never effected
2429
        self._set_history_item(reason='CANCEL')
2430
        self.delete()
2431

    
2432
    def get_diff_quotas(self, sub_list=None, add_list=None):
2433
        if sub_list is None:
2434
            sub_list = []
2435

    
2436
        if add_list is None:
2437
            add_list = []
2438

    
2439
        sub_append = sub_list.append
2440
        add_append = add_list.append
2441
        holder = self.person.uuid
2442

    
2443
        synced_application = self.application
2444
        if synced_application is not None:
2445
            cur_grants = synced_application.projectresourcegrant_set.all()
2446
            for grant in cur_grants:
2447
                sub_append(QuotaLimits(
2448
                               holder       = holder,
2449
                               resource     = str(grant.resource),
2450
                               capacity     = grant.member_capacity,
2451
                               import_limit = grant.member_import_limit,
2452
                               export_limit = grant.member_export_limit))
2453

    
2454
        pending_application = self.pending_application
2455
        if pending_application is not None:
2456
            new_grants = pending_application.projectresourcegrant_set.all()
2457
            for new_grant in new_grants:
2458
                add_append(QuotaLimits(
2459
                               holder       = holder,
2460
                               resource     = str(new_grant.resource),
2461
                               capacity     = new_grant.member_capacity,
2462
                               import_limit = new_grant.member_import_limit,
2463
                               export_limit = new_grant.member_export_limit))
2464

    
2465
        return (sub_list, add_list)
2466

    
2467
    def set_sync(self):
2468
        if not self.is_pending:
2469
            m = _("%s: attempt to sync a non pending membership") % (self,)
2470
            raise AssertionError(m)
2471

    
2472
        state = self.state
2473
        if state in self.ACTUALLY_ACCEPTED:
2474
            pending_application = self.pending_application
2475
            if pending_application is None:
2476
                m = _("%s: attempt to sync an empty pending application") % (
2477
                    self,)
2478
                raise AssertionError(m)
2479

    
2480
            self.application = pending_application
2481
            self.is_active = True
2482

    
2483
            self.pending_application = None
2484
            self.pending_serial = None
2485

    
2486
            # project.application may have changed in the meantime,
2487
            # in which case we stay PENDING;
2488
            # we are safe to check due to select_for_update
2489
            if self.application == self.project.application:
2490
                self.is_pending = False
2491
            self.save()
2492

    
2493
        elif state == self.PROJECT_DEACTIVATED:
2494
            if self.pending_application:
2495
                m = _("%s: attempt to sync in state '%s' "
2496
                      "with a pending application") % (self, state)
2497
                raise AssertionError(m)
2498

    
2499
            self.application = None
2500
            self.is_active = False
2501
            self.pending_serial = None
2502
            self.is_pending = False
2503
            self.save()
2504

    
2505
        elif state == self.REMOVED:
2506
            self.delete()
2507

    
2508
        else:
2509
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2510
            raise AssertionError(m)
2511

    
2512
    def reset_sync(self):
2513
        if not self.is_pending:
2514
            m = _("%s: attempt to reset a non pending membership") % (self,)
2515
            raise AssertionError(m)
2516

    
2517
        state = self.state
2518
        if state in [self.ACCEPTED, self.LEAVE_REQUESTED,
2519
                     self.PROJECT_DEACTIVATED, self.REMOVED]:
2520
            self.pending_application = None
2521
            self.pending_serial = None
2522
            self.save()
2523
        else:
2524
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2525
            raise AssertionError(m)
2526

    
2527
class Serial(models.Model):
2528
    serial  =   models.AutoField(primary_key=True)
2529

    
2530
def new_serial():
2531
    s = Serial.objects.create()
2532
    serial = s.serial
2533
    s.delete()
2534
    return serial
2535

    
2536
class SyncError(Exception):
2537
    pass
2538

    
2539
def reset_serials(serials):
2540
    objs = ProjectMembership.objects
2541
    q = objs.filter(pending_serial__in=serials).select_for_update()
2542
    memberships = list(q)
2543

    
2544
    if memberships:
2545
        for membership in memberships:
2546
            membership.reset_sync()
2547

    
2548
        transaction.commit()
2549

    
2550
def sync_finish_serials(serials_to_ack=None):
2551
    if serials_to_ack is None:
2552
        serials_to_ack = qh_query_serials([])
2553

    
2554
    serials_to_ack = set(serials_to_ack)
2555
    objs = ProjectMembership.objects
2556
    q = objs.filter(pending_serial__isnull=False).select_for_update()
2557
    memberships = list(q)
2558

    
2559
    if memberships:
2560
        for membership in memberships:
2561
            serial = membership.pending_serial
2562
            if serial in serials_to_ack:
2563
                membership.set_sync()
2564
            else:
2565
                membership.reset_sync()
2566

    
2567
        transaction.commit()
2568

    
2569
    qh_ack_serials(list(serials_to_ack))
2570
    return len(memberships)
2571

    
2572
def pre_sync_projects(sync=True):
2573
    ACCEPTED = ProjectMembership.ACCEPTED
2574
    LEAVE_REQUESTED = ProjectMembership.LEAVE_REQUESTED
2575
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2576
    objs = Project.objects
2577

    
2578
    modified = list(objs.modified_projects().select_for_update())
2579
    if sync:
2580
        for project in modified:
2581
            objects = project.projectmembership_set
2582

    
2583
            memberships = objects.actually_accepted().select_for_update()
2584
            for membership in memberships:
2585
                membership.is_pending = True
2586
                membership.save()
2587

    
2588
    reactivating = list(objs.reactivating_projects().select_for_update())
2589
    if sync:
2590
        for project in reactivating:
2591
            objects = project.projectmembership_set
2592

    
2593
            q = objects.filter(state=PROJECT_DEACTIVATED)
2594
            memberships = q.select_for_update()
2595
            for membership in memberships:
2596
                membership.is_pending = True
2597
                if membership.leave_request_date is None:
2598
                    membership.state = ACCEPTED
2599
                else:
2600
                    membership.state = LEAVE_REQUESTED
2601
                membership.save()
2602

    
2603
    deactivating = list(objs.deactivating_projects().select_for_update())
2604
    if sync:
2605
        for project in deactivating:
2606
            objects = project.projectmembership_set
2607

    
2608
            # Note: we keep a user-level deactivation
2609
            # (e.g. USER_SUSPENDED) intact
2610
            memberships = objects.actually_accepted().select_for_update()
2611
            for membership in memberships:
2612
                membership.is_pending = True
2613
                membership.state = PROJECT_DEACTIVATED
2614
                membership.save()
2615

    
2616
#    transaction.commit()
2617
    return (modified, reactivating, deactivating)
2618

    
2619
def set_sync_projects(exclude=None):
2620

    
2621
    ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED
2622
    objects = ProjectMembership.objects
2623

    
2624
    sub_quota, add_quota = [], []
2625

    
2626
    serial = new_serial()
2627

    
2628
    pending = objects.filter(is_pending=True).select_for_update()
2629
    for membership in pending:
2630

    
2631
        if membership.pending_application:
2632
            m = "%s: impossible: pending_application is not None (%s)" % (
2633
                membership, membership.pending_application)
2634
            raise AssertionError(m)
2635
        if membership.pending_serial:
2636
            m = "%s: impossible: pending_serial is not None (%s)" % (
2637
                membership, membership.pending_serial)
2638
            raise AssertionError(m)
2639

    
2640
        if exclude is not None:
2641
            uuid = membership.person.uuid
2642
            if uuid in exclude:
2643
                logger.warning("Excluded from sync: %s" % uuid)
2644
                continue
2645

    
2646
        if membership.state in ACTUALLY_ACCEPTED:
2647
            membership.pending_application = membership.project.application
2648

    
2649
        membership.pending_serial = serial
2650
        membership.get_diff_quotas(sub_quota, add_quota)
2651
        membership.save()
2652

    
2653
    transaction.commit()
2654
    return serial, sub_quota, add_quota
2655

    
2656
def do_sync_projects():
2657
    serial, sub_quota, add_quota = set_sync_projects()
2658
    r = qh_add_quota(serial, sub_quota, add_quota)
2659
    if not r:
2660
        return serial
2661

    
2662
    m = "cannot sync serial: %d" % serial
2663
    logger.error(m)
2664
    logger.error("Failed: %s" % r)
2665

    
2666
    reset_serials([serial])
2667
    uuids = set(uuid for (uuid, resource) in r)
2668
    serial, sub_quota, add_quota = set_sync_projects(exclude=uuids)
2669
    r = qh_add_quota(serial, sub_quota, add_quota)
2670
    if not r:
2671
        return serial
2672

    
2673
    m = "cannot sync serial: %d" % serial
2674
    logger.error(m)
2675
    logger.error("Failed: %s" % r)
2676
    raise SyncError(m)
2677

    
2678
def post_sync_projects():
2679
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2680
    Q_ACTUALLY_ACCEPTED = ProjectMembership.Q_ACTUALLY_ACCEPTED
2681
    objs = Project.objects
2682

    
2683
    modified = objs.modified_projects().select_for_update()
2684
    for project in modified:
2685
        objects = project.projectmembership_set
2686
        q = objects.filter(Q_ACTUALLY_ACCEPTED & Q(is_pending=True))
2687
        memberships = list(q.select_for_update())
2688
        if not memberships:
2689
            project.is_modified = False
2690
            project.save()
2691

    
2692
    reactivating = objs.reactivating_projects().select_for_update()
2693
    for project in reactivating:
2694
        objects = project.projectmembership_set
2695
        q = objects.filter(Q(state=PROJECT_DEACTIVATED) | Q(is_pending=True))
2696
        memberships = list(q.select_for_update())
2697
        if not memberships:
2698
            project.reactivate()
2699
            project.save()
2700

    
2701
    deactivating = objs.deactivating_projects().select_for_update()
2702
    for project in deactivating:
2703
        objects = project.projectmembership_set
2704
        q = objects.filter(Q_ACTUALLY_ACCEPTED | Q(is_pending=True))
2705
        memberships = list(q.select_for_update())
2706
        if not memberships:
2707
            project.deactivate()
2708
            project.save()
2709

    
2710
    transaction.commit()
2711

    
2712
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2713
    @with_lock(retries, retry_wait)
2714
    def _sync_projects(sync):
2715
        sync_finish_serials()
2716
        # Informative only -- no select_for_update()
2717
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2718

    
2719
        projects_log = pre_sync_projects(sync)
2720
        if sync:
2721
            serial = do_sync_projects()
2722
            sync_finish_serials([serial])
2723
            post_sync_projects()
2724

    
2725
        return (pending, projects_log)
2726
    return _sync_projects(sync)
2727

    
2728

    
2729

    
2730
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2731
    @with_lock(retries, retry_wait)
2732
    def _sync_users(users, sync):
2733
        sync_finish_serials()
2734

    
2735
        info = {}
2736
        for user in users:
2737
            info[user.uuid] = user.email
2738

    
2739
        existing, nonexisting = qh_check_users(users)
2740
        resources = get_resource_names()
2741
        qh_limits, qh_counters = qh_get_quotas(existing, resources)
2742
        astakos_initial = initial_quotas(users)
2743
        astakos_quotas = users_quotas(users, astakos_initial)
2744

    
2745
        if sync:
2746
            r = register_users(nonexisting)
2747
            r = send_quotas(astakos_quotas)
2748

    
2749
        return (existing, nonexisting,
2750
                qh_limits, qh_counters,
2751
                astakos_initial, astakos_quotas, info)
2752
    return _sync_users(users, sync)
2753

    
2754

    
2755
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2756
    users = AstakosUser.objects.verified()
2757
    return sync_users(users, sync, retries, retry_wait)
2758

    
2759
class ProjectMembershipHistory(models.Model):
2760
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2761
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2762

    
2763
    person  =   models.BigIntegerField()
2764
    project =   models.BigIntegerField()
2765
    date    =   models.DateField(auto_now_add=True)
2766
    reason  =   models.IntegerField()
2767
    serial  =   models.BigIntegerField()
2768

    
2769
### SIGNALS ###
2770
################
2771

    
2772
def create_astakos_user(u):
2773
    try:
2774
        AstakosUser.objects.get(user_ptr=u.pk)
2775
    except AstakosUser.DoesNotExist:
2776
        extended_user = AstakosUser(user_ptr_id=u.pk)
2777
        extended_user.__dict__.update(u.__dict__)
2778
        extended_user.save()
2779
        if not extended_user.has_auth_provider('local'):
2780
            extended_user.add_auth_provider('local')
2781
    except BaseException, e:
2782
        logger.exception(e)
2783

    
2784
def fix_superusers():
2785
    # Associate superusers with AstakosUser
2786
    admins = User.objects.filter(is_superuser=True)
2787
    for u in admins:
2788
        create_astakos_user(u)
2789

    
2790
def user_post_save(sender, instance, created, **kwargs):
2791
    if not created:
2792
        return
2793
    create_astakos_user(instance)
2794
post_save.connect(user_post_save, sender=User)
2795

    
2796
def astakosuser_post_save(sender, instance, created, **kwargs):
2797
    pass
2798

    
2799
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2800

    
2801
def resource_post_save(sender, instance, created, **kwargs):
2802
    pass
2803

    
2804
post_save.connect(resource_post_save, sender=Resource)
2805

    
2806
def renew_token(sender, instance, **kwargs):
2807
    if not instance.auth_token:
2808
        instance.renew_token()
2809
pre_save.connect(renew_token, sender=AstakosUser)
2810
pre_save.connect(renew_token, sender=Service)