Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (93.1 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
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 remove_resource_policy(self, service, resource):
483
        """Raises ObjectDoesNotExist, IntegrityError"""
484
        resource = Resource.objects.get(service__name=service, name=resource)
485
        q = self.policies.get(resource=resource).delete()
486

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

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

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

    
506
        self.update_uuid()
507

    
508
        if self.username != self.email.lower():
509
            # set username
510
            self.username = self.email.lower()
511

    
512
        super(AstakosUser, self).save(**kwargs)
513

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

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

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

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

    
544
    def __unicode__(self):
545
        return '%s (%s)' % (self.realname, self.email)
546

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

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

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

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

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

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

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

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

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

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

    
612
        return True
613

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

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

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

    
625
        return provider.is_available_for_remove()
626

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

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

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

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

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

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

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

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

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

    
681
        pending.delete()
682
        return provider
683

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

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

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

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

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

    
707
    def get_primary_auth_provider(self):
708
        return self.get_auth_providers().filter()[0]
709

    
710
    def get_auth_providers(self):
711
        return self.auth_providers
712

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

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

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

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

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

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

    
773
        return mark_safe(message + u' '+ msg_extra)
774

    
775
    def owns_application(self, application):
776
        return application.owner == self
777

    
778
    def owns_project(self, project):
779
        return project.application.owner == self
780

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

    
788
    def get_membership(self, project):
789
        try:
790
            return ProjectMembership.objects.get(
791
                project=project,
792
                person=self)
793
        except ProjectMembership.DoesNotExist:
794
            return None
795

    
796
    def membership_display(self, project):
797
        m = self.get_membership(project)
798
        if m is None:
799
            return _('Not a member')
800
        else:
801
            return m.user_friendly_state_display()
802

    
803
    def non_owner_can_view(self, maybe_project):
804
        if self.is_project_admin():
805
            return True
806
        if maybe_project is None:
807
            return False
808
        project = maybe_project
809
        if self.is_associated(project):
810
            return True
811
        if project.is_deactivated():
812
            return False
813
        return True
814

    
815
    def settings(self):
816
        return UserSetting.objects.filter(user=self)
817

    
818

    
819
def initial_quotas(users):
820
    initial = {}
821
    default_quotas = get_default_quota()
822

    
823
    for user in users:
824
        uuid = user.uuid
825
        initial[uuid] = dict(default_quotas)
826

    
827
    objs = AstakosUserQuota.objects.select_related()
828
    orig_quotas = objs.filter(user__in=users)
829
    for user_quota in orig_quotas:
830
        uuid = user_quota.uuid
831
        user_init = initial.get(uuid, {})
832
        resource = user_quota.resource.full_name()
833
        user_init[resource] = user_quota.quota_values()
834
        initial[uuid] = user_init
835

    
836
    return initial
837

    
838

    
839
def users_quotas(users, initial=None):
840
    if initial is None:
841
        quotas = initial_quotas(users)
842
    else:
843
        quotas = copy.deepcopy(initial)
844

    
845
    objs = ProjectMembership.objects.select_related('application', 'person')
846
    memberships = objs.filter(person__in=users, is_active=True)
847

    
848
    apps = set(m.application for m in memberships if m.application is not None)
849
    objs = ProjectResourceGrant.objects.select_related()
850
    grants = objs.filter(project_application__in=apps)
851

    
852
    for membership in memberships:
853
        uuid = membership.person.uuid
854
        userquotas = quotas.get(uuid, {})
855

    
856
        application = membership.application
857
        if application is None:
858
            m = _("missing application for active membership %s"
859
                  % (membership,))
860
            raise AssertionError(m)
861

    
862
        for grant in grants:
863
            if grant.project_application_id != application.id:
864
                continue
865
            resource = grant.resource.full_name()
866
            prev = userquotas.get(resource, 0)
867
            new = add_quota_values(prev, grant.member_quota_values())
868
            userquotas[resource] = new
869
        quotas[uuid] = userquotas
870

    
871
    return quotas
872

    
873

    
874
class AstakosUserAuthProviderManager(models.Manager):
875

    
876
    def active(self, **filters):
877
        return self.filter(active=True, **filters)
878

    
879
    def remove_unverified_providers(self, provider, **filters):
880
        try:
881
            existing = self.filter(module=provider, user__email_verified=False, **filters)
882
            for p in existing:
883
                p.user.delete()
884
        except:
885
            pass
886

    
887

    
888

    
889
class AstakosUserAuthProvider(models.Model):
890
    """
891
    Available user authentication methods.
892
    """
893
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
894
                                   null=True, default=None)
895
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
896
    module = models.CharField(_('Provider'), max_length=255, blank=False,
897
                                default='local')
898
    identifier = models.CharField(_('Third-party identifier'),
899
                                              max_length=255, null=True,
900
                                              blank=True)
901
    active = models.BooleanField(default=True)
902
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
903
                                   default='astakos')
904
    info_data = models.TextField(default="", null=True, blank=True)
905
    created = models.DateTimeField('Creation date', auto_now_add=True)
906

    
907
    objects = AstakosUserAuthProviderManager()
908

    
909
    class Meta:
910
        unique_together = (('identifier', 'module', 'user'), )
911
        ordering = ('module', 'created')
912

    
913
    def __init__(self, *args, **kwargs):
914
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
915
        try:
916
            self.info = json.loads(self.info_data)
917
            if not self.info:
918
                self.info = {}
919
        except Exception, e:
920
            self.info = {}
921

    
922
        for key,value in self.info.iteritems():
923
            setattr(self, 'info_%s' % key, value)
924

    
925

    
926
    @property
927
    def settings(self):
928
        return auth_providers.get_provider(self.module)
929

    
930
    @property
931
    def details_display(self):
932
        try:
933
            params = self.user.__dict__
934
            params.update(self.__dict__)
935
            return self.settings.get_details_tpl_display % params
936
        except:
937
            return ''
938

    
939
    @property
940
    def title_display(self):
941
        title_tpl = self.settings.get_title_display
942
        try:
943
            if self.settings.get_user_title_display:
944
                title_tpl = self.settings.get_user_title_display
945
        except Exception, e:
946
            pass
947
        try:
948
          return title_tpl % self.__dict__
949
        except:
950
          return self.settings.get_title_display % self.__dict__
951

    
952
    def can_remove(self):
953
        return self.user.can_remove_auth_provider(self.module)
954

    
955
    def delete(self, *args, **kwargs):
956
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
957
        if self.module == 'local':
958
            self.user.set_unusable_password()
959
            self.user.save()
960
        return ret
961

    
962
    def __repr__(self):
963
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
964

    
965
    def __unicode__(self):
966
        if self.identifier:
967
            return "%s:%s" % (self.module, self.identifier)
968
        if self.auth_backend:
969
            return "%s:%s" % (self.module, self.auth_backend)
970
        return self.module
971

    
972
    def save(self, *args, **kwargs):
973
        self.info_data = json.dumps(self.info)
974
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
975

    
976

    
977
class ExtendedManager(models.Manager):
978
    def _update_or_create(self, **kwargs):
979
        assert kwargs, \
980
            'update_or_create() must be passed at least one keyword argument'
981
        obj, created = self.get_or_create(**kwargs)
982
        defaults = kwargs.pop('defaults', {})
983
        if created:
984
            return obj, True, False
985
        else:
986
            try:
987
                params = dict(
988
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
989
                params.update(defaults)
990
                for attr, val in params.items():
991
                    if hasattr(obj, attr):
992
                        setattr(obj, attr, val)
993
                sid = transaction.savepoint()
994
                obj.save(force_update=True)
995
                transaction.savepoint_commit(sid)
996
                return obj, False, True
997
            except IntegrityError, e:
998
                transaction.savepoint_rollback(sid)
999
                try:
1000
                    return self.get(**kwargs), False, False
1001
                except self.model.DoesNotExist:
1002
                    raise e
1003

    
1004
    update_or_create = _update_or_create
1005

    
1006

    
1007
class AstakosUserQuota(models.Model):
1008
    objects = ExtendedManager()
1009
    capacity = intDecimalField()
1010
    quantity = intDecimalField(default=0)
1011
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1012
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1013
    resource = models.ForeignKey(Resource)
1014
    user = models.ForeignKey(AstakosUser)
1015

    
1016
    class Meta:
1017
        unique_together = ("resource", "user")
1018

    
1019
    def quota_values(self):
1020
        return QuotaValues(
1021
            quantity = self.quantity,
1022
            capacity = self.capacity,
1023
            import_limit = self.import_limit,
1024
            export_limit = self.export_limit)
1025

    
1026

    
1027
class ApprovalTerms(models.Model):
1028
    """
1029
    Model for approval terms
1030
    """
1031

    
1032
    date = models.DateTimeField(
1033
        _('Issue date'), db_index=True, auto_now_add=True)
1034
    location = models.CharField(_('Terms location'), max_length=255)
1035

    
1036

    
1037
class Invitation(models.Model):
1038
    """
1039
    Model for registring invitations
1040
    """
1041
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1042
                                null=True)
1043
    realname = models.CharField(_('Real name'), max_length=255)
1044
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1045
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1046
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1047
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1048
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1049

    
1050
    def __init__(self, *args, **kwargs):
1051
        super(Invitation, self).__init__(*args, **kwargs)
1052
        if not self.id:
1053
            self.code = _generate_invitation_code()
1054

    
1055
    def consume(self):
1056
        self.is_consumed = True
1057
        self.consumed = datetime.now()
1058
        self.save()
1059

    
1060
    def __unicode__(self):
1061
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1062

    
1063

    
1064
class EmailChangeManager(models.Manager):
1065

    
1066
    @transaction.commit_on_success
1067
    def change_email(self, activation_key):
1068
        """
1069
        Validate an activation key and change the corresponding
1070
        ``User`` if valid.
1071

1072
        If the key is valid and has not expired, return the ``User``
1073
        after activating.
1074

1075
        If the key is not valid or has expired, return ``None``.
1076

1077
        If the key is valid but the ``User`` is already active,
1078
        return ``None``.
1079

1080
        After successful email change the activation record is deleted.
1081

1082
        Throws ValueError if there is already
1083
        """
1084
        try:
1085
            email_change = self.model.objects.get(
1086
                activation_key=activation_key)
1087
            if email_change.activation_key_expired():
1088
                email_change.delete()
1089
                raise EmailChange.DoesNotExist
1090
            # is there an active user with this address?
1091
            try:
1092
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1093
            except AstakosUser.DoesNotExist:
1094
                pass
1095
            else:
1096
                raise ValueError(_('The new email address is reserved.'))
1097
            # update user
1098
            user = AstakosUser.objects.get(pk=email_change.user_id)
1099
            old_email = user.email
1100
            user.email = email_change.new_email_address
1101
            user.save()
1102
            email_change.delete()
1103
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1104
                                                          user.email)
1105
            logger.log(LOGGING_LEVEL, msg)
1106
            return user
1107
        except EmailChange.DoesNotExist:
1108
            raise ValueError(_('Invalid activation key.'))
1109

    
1110

    
1111
class EmailChange(models.Model):
1112
    new_email_address = models.EmailField(
1113
        _(u'new e-mail address'),
1114
        help_text=_('Provide a new email address. Until you verify the new '
1115
                    'address by following the activation link that will be '
1116
                    'sent to it, your old email address will remain active.'))
1117
    user = models.ForeignKey(
1118
        AstakosUser, unique=True, related_name='emailchanges')
1119
    requested_at = models.DateTimeField(auto_now_add=True)
1120
    activation_key = models.CharField(
1121
        max_length=40, unique=True, db_index=True)
1122

    
1123
    objects = EmailChangeManager()
1124

    
1125
    def get_url(self):
1126
        return reverse('email_change_confirm',
1127
                      kwargs={'activation_key': self.activation_key})
1128

    
1129
    def activation_key_expired(self):
1130
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1131
        return self.requested_at + expiration_date < datetime.now()
1132

    
1133

    
1134
class AdditionalMail(models.Model):
1135
    """
1136
    Model for registring invitations
1137
    """
1138
    owner = models.ForeignKey(AstakosUser)
1139
    email = models.EmailField()
1140

    
1141

    
1142
def _generate_invitation_code():
1143
    while True:
1144
        code = randint(1, 2L ** 63 - 1)
1145
        try:
1146
            Invitation.objects.get(code=code)
1147
            # An invitation with this code already exists, try again
1148
        except Invitation.DoesNotExist:
1149
            return code
1150

    
1151

    
1152
def get_latest_terms():
1153
    try:
1154
        term = ApprovalTerms.objects.order_by('-id')[0]
1155
        return term
1156
    except IndexError:
1157
        pass
1158
    return None
1159

    
1160
class PendingThirdPartyUser(models.Model):
1161
    """
1162
    Model for registring successful third party user authentications
1163
    """
1164
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1165
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1166
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1167
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1168
                                  null=True)
1169
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1170
                                 null=True)
1171
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1172
                                   null=True)
1173
    username = models.CharField(_('username'), max_length=30, unique=True,  
1174
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1175
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1176
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1177
    info = models.TextField(default="", null=True, blank=True)
1178

    
1179
    class Meta:
1180
        unique_together = ("provider", "third_party_identifier")
1181

    
1182
    def get_user_instance(self):
1183
        d = self.__dict__
1184
        d.pop('_state', None)
1185
        d.pop('id', None)
1186
        d.pop('token', None)
1187
        d.pop('created', None)
1188
        d.pop('info', None)
1189
        user = AstakosUser(**d)
1190

    
1191
        return user
1192

    
1193
    @property
1194
    def realname(self):
1195
        return '%s %s' %(self.first_name, self.last_name)
1196

    
1197
    @realname.setter
1198
    def realname(self, value):
1199
        parts = value.split(' ')
1200
        if len(parts) == 2:
1201
            self.first_name = parts[0]
1202
            self.last_name = parts[1]
1203
        else:
1204
            self.last_name = parts[0]
1205

    
1206
    def save(self, **kwargs):
1207
        if not self.id:
1208
            # set username
1209
            while not self.username:
1210
                username =  uuid.uuid4().hex[:30]
1211
                try:
1212
                    AstakosUser.objects.get(username = username)
1213
                except AstakosUser.DoesNotExist, e:
1214
                    self.username = username
1215
        super(PendingThirdPartyUser, self).save(**kwargs)
1216

    
1217
    def generate_token(self):
1218
        self.password = self.third_party_identifier
1219
        self.last_login = datetime.now()
1220
        self.token = default_token_generator.make_token(self)
1221

    
1222
    def existing_user(self):
1223
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1224
                                         auth_providers__identifier=self.third_party_identifier)
1225

    
1226
class SessionCatalog(models.Model):
1227
    session_key = models.CharField(_('session key'), max_length=40)
1228
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1229

    
1230

    
1231
class UserSetting(models.Model):
1232
    user = models.ForeignKey(AstakosUser)
1233
    setting = models.CharField(max_length=255)
1234
    value = models.IntegerField()
1235

    
1236
    objects = ForUpdateManager()
1237

    
1238
    class Meta:
1239
        unique_together = ("user", "setting")
1240

    
1241

    
1242
### PROJECTS ###
1243
################
1244

    
1245
def synced_model_metaclass(class_name, class_parents, class_attributes):
1246

    
1247
    new_attributes = {}
1248
    sync_attributes = {}
1249

    
1250
    for name, value in class_attributes.iteritems():
1251
        sync, underscore, rest = name.partition('_')
1252
        if sync == 'sync' and underscore == '_':
1253
            sync_attributes[rest] = value
1254
        else:
1255
            new_attributes[name] = value
1256

    
1257
    if 'prefix' not in sync_attributes:
1258
        m = ("you did not specify a 'sync_prefix' attribute "
1259
             "in class '%s'" % (class_name,))
1260
        raise ValueError(m)
1261

    
1262
    prefix = sync_attributes.pop('prefix')
1263
    class_name = sync_attributes.pop('classname', prefix + '_model')
1264

    
1265
    for name, value in sync_attributes.iteritems():
1266
        newname = prefix + '_' + name
1267
        if newname in new_attributes:
1268
            m = ("class '%s' was specified with prefix '%s' "
1269
                 "but it already has an attribute named '%s'"
1270
                 % (class_name, prefix, newname))
1271
            raise ValueError(m)
1272

    
1273
        new_attributes[newname] = value
1274

    
1275
    newclass = type(class_name, class_parents, new_attributes)
1276
    return newclass
1277

    
1278

    
1279
def make_synced(prefix='sync', name='SyncedState'):
1280

    
1281
    the_name = name
1282
    the_prefix = prefix
1283

    
1284
    class SyncedState(models.Model):
1285

    
1286
        sync_classname      = the_name
1287
        sync_prefix         = the_prefix
1288
        __metaclass__       = synced_model_metaclass
1289

    
1290
        sync_new_state      = models.BigIntegerField(null=True)
1291
        sync_synced_state   = models.BigIntegerField(null=True)
1292
        STATUS_SYNCED       = 0
1293
        STATUS_PENDING      = 1
1294
        sync_status         = models.IntegerField(db_index=True)
1295

    
1296
        class Meta:
1297
            abstract = True
1298

    
1299
        class NotSynced(Exception):
1300
            pass
1301

    
1302
        def sync_init_state(self, state):
1303
            self.sync_synced_state = state
1304
            self.sync_new_state = state
1305
            self.sync_status = self.STATUS_SYNCED
1306

    
1307
        def sync_get_status(self):
1308
            return self.sync_status
1309

    
1310
        def sync_set_status(self):
1311
            if self.sync_new_state != self.sync_synced_state:
1312
                self.sync_status = self.STATUS_PENDING
1313
            else:
1314
                self.sync_status = self.STATUS_SYNCED
1315

    
1316
        def sync_set_synced(self):
1317
            self.sync_synced_state = self.sync_new_state
1318
            self.sync_status = self.STATUS_SYNCED
1319

    
1320
        def sync_get_synced_state(self):
1321
            return self.sync_synced_state
1322

    
1323
        def sync_set_new_state(self, new_state):
1324
            self.sync_new_state = new_state
1325
            self.sync_set_status()
1326

    
1327
        def sync_get_new_state(self):
1328
            return self.sync_new_state
1329

    
1330
        def sync_set_synced_state(self, synced_state):
1331
            self.sync_synced_state = synced_state
1332
            self.sync_set_status()
1333

    
1334
        def sync_get_pending_objects(self):
1335
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1336
            return self.objects.filter(**kw)
1337

    
1338
        def sync_get_synced_objects(self):
1339
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1340
            return self.objects.filter(**kw)
1341

    
1342
        def sync_verify_get_synced_state(self):
1343
            status = self.sync_get_status()
1344
            state = self.sync_get_synced_state()
1345
            verified = (status == self.STATUS_SYNCED)
1346
            return state, verified
1347

    
1348
        def sync_is_synced(self):
1349
            state, verified = self.sync_verify_get_synced_state()
1350
            return verified
1351

    
1352
    return SyncedState
1353

    
1354
SyncedState = make_synced(prefix='sync', name='SyncedState')
1355

    
1356

    
1357
class ChainManager(ForUpdateManager):
1358

    
1359
    def search_by_name(self, *search_strings):
1360
        projects = Project.objects.search_by_name(*search_strings)
1361
        chains = [p.id for p in projects]
1362
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1363
        apps = (app for app in apps if app.is_latest())
1364
        app_chains = [app.chain for app in apps if app.chain not in chains]
1365
        return chains + app_chains
1366

    
1367
    def all_full_state(self):
1368
        d = {}
1369
        chains = self.all()
1370
        for chain in chains:
1371
            d[chain.pk] = chain.full_state()
1372
        return d
1373

    
1374
    def of_project(self, project):
1375
        if project is None:
1376
            return None
1377
        try:
1378
            return self.get(chain=project.id)
1379
        except Chain.DoesNotExist:
1380
            raise AssertionError('project with no chain')
1381

    
1382

    
1383
class Chain(models.Model):
1384
    chain  =   models.AutoField(primary_key=True)
1385

    
1386
    def __str__(self):
1387
        return "%s" % (self.chain,)
1388

    
1389
    objects = ChainManager()
1390

    
1391
    PENDING            = 0
1392
    DENIED             = 3
1393
    DISMISSED          = 4
1394
    CANCELLED          = 5
1395

    
1396
    APPROVED           = 10
1397
    APPROVED_PENDING   = 11
1398
    SUSPENDED          = 12
1399
    SUSPENDED_PENDING  = 13
1400
    TERMINATED         = 14
1401
    TERMINATED_PENDING = 15
1402

    
1403
    PENDING_STATES = [PENDING,
1404
                      APPROVED_PENDING,
1405
                      SUSPENDED_PENDING,
1406
                      TERMINATED_PENDING,
1407
                      ]
1408

    
1409
    MODIFICATION_STATES = [APPROVED_PENDING,
1410
                           SUSPENDED_PENDING,
1411
                           TERMINATED_PENDING,
1412
                           ]
1413

    
1414
    RELEVANT_STATES = [PENDING,
1415
                       DENIED,
1416
                       APPROVED,
1417
                       APPROVED_PENDING,
1418
                       SUSPENDED,
1419
                       SUSPENDED_PENDING,
1420
                       TERMINATED_PENDING,
1421
                       ]
1422

    
1423
    SKIP_STATES = [DISMISSED,
1424
                   CANCELLED,
1425
                   TERMINATED]
1426

    
1427
    STATE_DISPLAY = {
1428
        PENDING            : _("Pending"),
1429
        DENIED             : _("Denied"),
1430
        DISMISSED          : _("Dismissed"),
1431
        CANCELLED          : _("Cancelled"),
1432
        APPROVED           : _("Active"),
1433
        APPROVED_PENDING   : _("Active - Pending"),
1434
        SUSPENDED          : _("Suspended"),
1435
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1436
        TERMINATED         : _("Terminated"),
1437
        TERMINATED_PENDING : _("Terminated - Pending"),
1438
        }
1439

    
1440

    
1441
    @classmethod
1442
    def _chain_state(cls, project_state, app_state):
1443
        s = CHAIN_STATE.get((project_state, app_state), None)
1444
        if s is None:
1445
            raise AssertionError('inconsistent chain state')
1446
        return s
1447

    
1448
    @classmethod
1449
    def chain_state(cls, project, app):
1450
        p_state = project.state if project else None
1451
        return cls._chain_state(p_state, app.state)
1452

    
1453
    @classmethod
1454
    def state_display(cls, s):
1455
        if s is None:
1456
            return _("Unknown")
1457
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1458

    
1459
    def last_application(self):
1460
        return self.chained_apps.order_by('-id')[0]
1461

    
1462
    def get_project(self):
1463
        try:
1464
            return self.chained_project
1465
        except Project.DoesNotExist:
1466
            return None
1467

    
1468
    def get_elements(self):
1469
        project = self.get_project()
1470
        app = self.last_application()
1471
        return project, app
1472

    
1473
    def full_state(self):
1474
        project, app = self.get_elements()
1475
        s = self.chain_state(project, app)
1476
        return s, project, app
1477

    
1478
def new_chain():
1479
    c = Chain.objects.create()
1480
    return c
1481

    
1482

    
1483
class ProjectApplicationManager(ForUpdateManager):
1484

    
1485
    def user_visible_projects(self, *filters, **kw_filters):
1486
        model = self.model
1487
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1488

    
1489
    def user_visible_by_chain(self, flt):
1490
        model = self.model
1491
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1492
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1493
        by_chain = dict(pending.annotate(models.Max('id')))
1494
        by_chain.update(approved.annotate(models.Max('id')))
1495
        return self.filter(flt, id__in=by_chain.values())
1496

    
1497
    def user_accessible_projects(self, user):
1498
        """
1499
        Return projects accessed by specified user.
1500
        """
1501
        if user.is_project_admin():
1502
            participates_filters = Q()
1503
        else:
1504
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1505
                                   Q(project__projectmembership__person=user)
1506

    
1507
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1508

    
1509
    def search_by_name(self, *search_strings):
1510
        q = Q()
1511
        for s in search_strings:
1512
            q = q | Q(name__icontains=s)
1513
        return self.filter(q)
1514

    
1515
    def latest_of_chain(self, chain_id):
1516
        try:
1517
            return self.filter(chain=chain_id).order_by('-id')[0]
1518
        except IndexError:
1519
            return None
1520

    
1521

    
1522
class ProjectApplication(models.Model):
1523
    applicant               =   models.ForeignKey(
1524
                                    AstakosUser,
1525
                                    related_name='projects_applied',
1526
                                    db_index=True)
1527

    
1528
    PENDING     =    0
1529
    APPROVED    =    1
1530
    REPLACED    =    2
1531
    DENIED      =    3
1532
    DISMISSED   =    4
1533
    CANCELLED   =    5
1534

    
1535
    state                   =   models.IntegerField(default=PENDING,
1536
                                                    db_index=True)
1537

    
1538
    owner                   =   models.ForeignKey(
1539
                                    AstakosUser,
1540
                                    related_name='projects_owned',
1541
                                    db_index=True)
1542

    
1543
    chain                   =   models.ForeignKey(Chain,
1544
                                                  related_name='chained_apps',
1545
                                                  db_column='chain')
1546
    precursor_application   =   models.ForeignKey('ProjectApplication',
1547
                                                  null=True,
1548
                                                  blank=True)
1549

    
1550
    name                    =   models.CharField(max_length=80)
1551
    homepage                =   models.URLField(max_length=255, null=True,
1552
                                                verify_exists=False)
1553
    description             =   models.TextField(null=True, blank=True)
1554
    start_date              =   models.DateTimeField(null=True, blank=True)
1555
    end_date                =   models.DateTimeField()
1556
    member_join_policy      =   models.IntegerField()
1557
    member_leave_policy     =   models.IntegerField()
1558
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1559
    resource_grants         =   models.ManyToManyField(
1560
                                    Resource,
1561
                                    null=True,
1562
                                    blank=True,
1563
                                    through='ProjectResourceGrant')
1564
    comments                =   models.TextField(null=True, blank=True)
1565
    issue_date              =   models.DateTimeField(auto_now_add=True)
1566
    response_date           =   models.DateTimeField(null=True, blank=True)
1567

    
1568
    objects                 =   ProjectApplicationManager()
1569

    
1570
    # Compiled queries
1571
    Q_PENDING  = Q(state=PENDING)
1572
    Q_APPROVED = Q(state=APPROVED)
1573
    Q_DENIED   = Q(state=DENIED)
1574

    
1575
    class Meta:
1576
        unique_together = ("chain", "id")
1577

    
1578
    def __unicode__(self):
1579
        return "%s applied by %s" % (self.name, self.applicant)
1580

    
1581
    # TODO: Move to a more suitable place
1582
    APPLICATION_STATE_DISPLAY = {
1583
        PENDING  : _('Pending review'),
1584
        APPROVED : _('Approved'),
1585
        REPLACED : _('Replaced'),
1586
        DENIED   : _('Denied'),
1587
        DISMISSED: _('Dismissed'),
1588
        CANCELLED: _('Cancelled')
1589
    }
1590

    
1591
    def get_project(self):
1592
        try:
1593
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1594
            return Project
1595
        except Project.DoesNotExist, e:
1596
            return None
1597

    
1598
    def state_display(self):
1599
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1600

    
1601
    def project_state_display(self):
1602
        try:
1603
            project = self.project
1604
            return project.state_display()
1605
        except Project.DoesNotExist:
1606
            return self.state_display()
1607

    
1608
    def add_resource_policy(self, service, resource, uplimit):
1609
        """Raises ObjectDoesNotExist, IntegrityError"""
1610
        q = self.projectresourcegrant_set
1611
        resource = Resource.objects.get(service__name=service, name=resource)
1612
        q.create(resource=resource, member_capacity=uplimit)
1613

    
1614
    def members_count(self):
1615
        return self.project.approved_memberships.count()
1616

    
1617
    @property
1618
    def grants(self):
1619
        return self.projectresourcegrant_set.values(
1620
            'member_capacity', 'resource__name', 'resource__service__name')
1621

    
1622
    @property
1623
    def resource_policies(self):
1624
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1625

    
1626
    @resource_policies.setter
1627
    def resource_policies(self, policies):
1628
        for p in policies:
1629
            service = p.get('service', None)
1630
            resource = p.get('resource', None)
1631
            uplimit = p.get('uplimit', 0)
1632
            self.add_resource_policy(service, resource, uplimit)
1633

    
1634
    def pending_modifications_incl_me(self):
1635
        q = self.chained_applications()
1636
        q = q.filter(Q(state=self.PENDING))
1637
        return q
1638

    
1639
    def last_pending_incl_me(self):
1640
        try:
1641
            return self.pending_modifications_incl_me().order_by('-id')[0]
1642
        except IndexError:
1643
            return None
1644

    
1645
    def pending_modifications(self):
1646
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1647

    
1648
    def last_pending(self):
1649
        try:
1650
            return self.pending_modifications().order_by('-id')[0]
1651
        except IndexError:
1652
            return None
1653

    
1654
    def is_modification(self):
1655
        # if self.state != self.PENDING:
1656
        #     return False
1657
        parents = self.chained_applications().filter(id__lt=self.id)
1658
        parents = parents.filter(state__in=[self.APPROVED])
1659
        return parents.count() > 0
1660

    
1661
    def chained_applications(self):
1662
        return ProjectApplication.objects.filter(chain=self.chain)
1663

    
1664
    def is_latest(self):
1665
        return self.chained_applications().order_by('-id')[0] == self
1666

    
1667
    def has_pending_modifications(self):
1668
        return bool(self.last_pending())
1669

    
1670
    def denied_modifications(self):
1671
        q = self.chained_applications()
1672
        q = q.filter(Q(state=self.DENIED))
1673
        q = q.filter(~Q(id=self.id))
1674
        return q
1675

    
1676
    def last_denied(self):
1677
        try:
1678
            return self.denied_modifications().order_by('-id')[0]
1679
        except IndexError:
1680
            return None
1681

    
1682
    def has_denied_modifications(self):
1683
        return bool(self.last_denied())
1684

    
1685
    def is_applied(self):
1686
        try:
1687
            self.project
1688
            return True
1689
        except Project.DoesNotExist:
1690
            return False
1691

    
1692
    def get_project(self):
1693
        try:
1694
            return Project.objects.get(id=self.chain)
1695
        except Project.DoesNotExist:
1696
            return None
1697

    
1698
    def project_exists(self):
1699
        return self.get_project() is not None
1700

    
1701
    def _get_project_for_update(self):
1702
        try:
1703
            objects = Project.objects
1704
            project = objects.get_for_update(id=self.chain)
1705
            return project
1706
        except Project.DoesNotExist:
1707
            return None
1708

    
1709
    def can_cancel(self):
1710
        return self.state == self.PENDING
1711

    
1712
    def cancel(self):
1713
        if not self.can_cancel():
1714
            m = _("cannot cancel: application '%s' in state '%s'") % (
1715
                    self.id, self.state)
1716
            raise AssertionError(m)
1717

    
1718
        self.state = self.CANCELLED
1719
        self.save()
1720

    
1721
    def can_dismiss(self):
1722
        return self.state == self.DENIED
1723

    
1724
    def dismiss(self):
1725
        if not self.can_dismiss():
1726
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1727
                    self.id, self.state)
1728
            raise AssertionError(m)
1729

    
1730
        self.state = self.DISMISSED
1731
        self.save()
1732

    
1733
    def can_deny(self):
1734
        return self.state == self.PENDING
1735

    
1736
    def deny(self):
1737
        if not self.can_deny():
1738
            m = _("cannot deny: application '%s' in state '%s'") % (
1739
                    self.id, self.state)
1740
            raise AssertionError(m)
1741

    
1742
        self.state = self.DENIED
1743
        self.response_date = datetime.now()
1744
        self.save()
1745

    
1746
    def can_approve(self):
1747
        return self.state == self.PENDING
1748

    
1749
    def approve(self, approval_user=None):
1750
        """
1751
        If approval_user then during owner membership acceptance
1752
        it is checked whether the request_user is eligible.
1753

1754
        Raises:
1755
            PermissionDenied
1756
        """
1757

    
1758
        if not transaction.is_managed():
1759
            raise AssertionError("NOPE")
1760

    
1761
        new_project_name = self.name
1762
        if not self.can_approve():
1763
            m = _("cannot approve: project '%s' in state '%s'") % (
1764
                    new_project_name, self.state)
1765
            raise AssertionError(m) # invalid argument
1766

    
1767
        now = datetime.now()
1768
        project = self._get_project_for_update()
1769

    
1770
        try:
1771
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1772
            conflicting_project = Project.objects.get(q)
1773
            if (conflicting_project != project):
1774
                m = (_("cannot approve: project with name '%s' "
1775
                       "already exists (id: %s)") % (
1776
                        new_project_name, conflicting_project.id))
1777
                raise PermissionDenied(m) # invalid argument
1778
        except Project.DoesNotExist:
1779
            pass
1780

    
1781
        new_project = False
1782
        if project is None:
1783
            new_project = True
1784
            project = Project(id=self.chain)
1785

    
1786
        project.name = new_project_name
1787
        project.application = self
1788
        project.last_approval_date = now
1789
        if not new_project:
1790
            project.is_modified = True
1791

    
1792
        project.save()
1793

    
1794
        self.state = self.APPROVED
1795
        self.response_date = now
1796
        self.save()
1797

    
1798
    @property
1799
    def member_join_policy_display(self):
1800
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1801

    
1802
    @property
1803
    def member_leave_policy_display(self):
1804
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1805

    
1806
class ProjectResourceGrant(models.Model):
1807

    
1808
    resource                =   models.ForeignKey(Resource)
1809
    project_application     =   models.ForeignKey(ProjectApplication,
1810
                                                  null=True)
1811
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1812
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1813
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1814
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1815
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1816
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1817

    
1818
    objects = ExtendedManager()
1819

    
1820
    class Meta:
1821
        unique_together = ("resource", "project_application")
1822

    
1823
    def member_quota_values(self):
1824
        return QuotaValues(
1825
            quantity = 0,
1826
            capacity = self.member_capacity,
1827
            import_limit = self.member_import_limit,
1828
            export_limit = self.member_export_limit)
1829

    
1830
    def display_member_capacity(self):
1831
        if self.member_capacity:
1832
            if self.resource.unit:
1833
                return ProjectResourceGrant.display_filesize(
1834
                    self.member_capacity)
1835
            else:
1836
                if math.isinf(self.member_capacity):
1837
                    return 'Unlimited'
1838
                else:
1839
                    return self.member_capacity
1840
        else:
1841
            return 'Unlimited'
1842

    
1843
    def __str__(self):
1844
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1845
                                        self.display_member_capacity())
1846

    
1847
    @classmethod
1848
    def display_filesize(cls, value):
1849
        try:
1850
            value = float(value)
1851
        except:
1852
            return
1853
        else:
1854
            if math.isinf(value):
1855
                return 'Unlimited'
1856
            if value > 1:
1857
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1858
                                [0, 0, 0, 0, 0, 0])
1859
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1860
                quotient = float(value) / 1024**exponent
1861
                unit, value_decimals = unit_list[exponent]
1862
                format_string = '{0:.%sf} {1}' % (value_decimals)
1863
                return format_string.format(quotient, unit)
1864
            if value == 0:
1865
                return '0 bytes'
1866
            if value == 1:
1867
                return '1 byte'
1868
            else:
1869
               return '0'
1870

    
1871

    
1872
class ProjectManager(ForUpdateManager):
1873

    
1874
    def terminated_projects(self):
1875
        q = self.model.Q_TERMINATED
1876
        return self.filter(q)
1877

    
1878
    def not_terminated_projects(self):
1879
        q = ~self.model.Q_TERMINATED
1880
        return self.filter(q)
1881

    
1882
    def terminating_projects(self):
1883
        q = self.model.Q_TERMINATED & Q(is_active=True)
1884
        return self.filter(q)
1885

    
1886
    def deactivated_projects(self):
1887
        q = self.model.Q_DEACTIVATED
1888
        return self.filter(q)
1889

    
1890
    def deactivating_projects(self):
1891
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1892
        return self.filter(q)
1893

    
1894
    def modified_projects(self):
1895
        return self.filter(is_modified=True)
1896

    
1897
    def reactivating_projects(self):
1898
        return self.filter(state=Project.APPROVED, is_active=False)
1899

    
1900
    def expired_projects(self):
1901
        q = (~Q(state=Project.TERMINATED) &
1902
              Q(application__end_date__lt=datetime.now()))
1903
        return self.filter(q)
1904

    
1905
    def search_by_name(self, *search_strings):
1906
        q = Q()
1907
        for s in search_strings:
1908
            q = q | Q(name__icontains=s)
1909
        return self.filter(q)
1910

    
1911

    
1912
class Project(models.Model):
1913

    
1914
    id                          =   models.OneToOneField(Chain,
1915
                                                      related_name='chained_project',
1916
                                                      db_column='id',
1917
                                                      primary_key=True)
1918

    
1919
    application                 =   models.OneToOneField(
1920
                                            ProjectApplication,
1921
                                            related_name='project')
1922
    last_approval_date          =   models.DateTimeField(null=True)
1923

    
1924
    members                     =   models.ManyToManyField(
1925
                                            AstakosUser,
1926
                                            through='ProjectMembership')
1927

    
1928
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1929
    deactivation_date           =   models.DateTimeField(null=True)
1930

    
1931
    creation_date               =   models.DateTimeField(auto_now_add=True)
1932
    name                        =   models.CharField(
1933
                                            max_length=80,
1934
                                            null=True,
1935
                                            db_index=True,
1936
                                            unique=True)
1937

    
1938
    APPROVED    = 1
1939
    SUSPENDED   = 10
1940
    TERMINATED  = 100
1941

    
1942
    is_modified                 =   models.BooleanField(default=False,
1943
                                                        db_index=True)
1944
    is_active                   =   models.BooleanField(default=True,
1945
                                                        db_index=True)
1946
    state                       =   models.IntegerField(default=APPROVED,
1947
                                                        db_index=True)
1948

    
1949
    objects     =   ProjectManager()
1950

    
1951
    # Compiled queries
1952
    Q_TERMINATED  = Q(state=TERMINATED)
1953
    Q_SUSPENDED   = Q(state=SUSPENDED)
1954
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1955

    
1956
    def __str__(self):
1957
        return uenc(_("<project %s '%s'>") %
1958
                    (self.id, udec(self.application.name)))
1959

    
1960
    __repr__ = __str__
1961

    
1962
    def __unicode__(self):
1963
        return _("<project %s '%s'>") % (self.id, self.application.name)
1964

    
1965
    STATE_DISPLAY = {
1966
        APPROVED   : 'Active',
1967
        SUSPENDED  : 'Suspended',
1968
        TERMINATED : 'Terminated'
1969
        }
1970

    
1971
    def state_display(self):
1972
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1973

    
1974
    def admin_state_display(self):
1975
        s = self.state_display()
1976
        if self.sync_pending():
1977
            s += ' (sync pending)'
1978
        return s
1979

    
1980
    def sync_pending(self):
1981
        if self.state != self.APPROVED:
1982
            return self.is_active
1983
        return not self.is_active or self.is_modified
1984

    
1985
    def expiration_info(self):
1986
        return (str(self.id), self.name, self.state_display(),
1987
                str(self.application.end_date))
1988

    
1989
    def is_deactivated(self, reason=None):
1990
        if reason is not None:
1991
            return self.state == reason
1992

    
1993
        return self.state != self.APPROVED
1994

    
1995
    def is_deactivating(self, reason=None):
1996
        if not self.is_active:
1997
            return False
1998

    
1999
        return self.is_deactivated(reason)
2000

    
2001
    def is_deactivated_strict(self, reason=None):
2002
        if self.is_active:
2003
            return False
2004

    
2005
        return self.is_deactivated(reason)
2006

    
2007
    ### Deactivation calls
2008

    
2009
    def deactivate(self):
2010
        self.deactivation_date = datetime.now()
2011
        self.is_active = False
2012

    
2013
    def reactivate(self):
2014
        self.deactivation_date = None
2015
        self.is_active = True
2016

    
2017
    def terminate(self):
2018
        self.deactivation_reason = 'TERMINATED'
2019
        self.state = self.TERMINATED
2020
        self.name = None
2021
        self.save()
2022

    
2023
    def suspend(self):
2024
        self.deactivation_reason = 'SUSPENDED'
2025
        self.state = self.SUSPENDED
2026
        self.save()
2027

    
2028
    def resume(self):
2029
        self.deactivation_reason = None
2030
        self.state = self.APPROVED
2031
        self.save()
2032

    
2033
    ### Logical checks
2034

    
2035
    def is_inconsistent(self):
2036
        now = datetime.now()
2037
        dates = [self.creation_date,
2038
                 self.last_approval_date,
2039
                 self.deactivation_date]
2040
        return any([date > now for date in dates])
2041

    
2042
    def is_active_strict(self):
2043
        return self.is_active and self.state == self.APPROVED
2044

    
2045
    def is_approved(self):
2046
        return self.state == self.APPROVED
2047

    
2048
    @property
2049
    def is_alive(self):
2050
        return not self.is_terminated
2051

    
2052
    @property
2053
    def is_terminated(self):
2054
        return self.is_deactivated(self.TERMINATED)
2055

    
2056
    @property
2057
    def is_suspended(self):
2058
        return self.is_deactivated(self.SUSPENDED)
2059

    
2060
    def violates_resource_grants(self):
2061
        return False
2062

    
2063
    def violates_members_limit(self, adding=0):
2064
        application = self.application
2065
        limit = application.limit_on_members_number
2066
        if limit is None:
2067
            return False
2068
        return (len(self.approved_members) + adding > limit)
2069

    
2070

    
2071
    ### Other
2072

    
2073
    def count_pending_memberships(self):
2074
        memb_set = self.projectmembership_set
2075
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2076
        return memb_count
2077

    
2078
    def members_count(self):
2079
        return self.approved_memberships.count()
2080

    
2081
    @property
2082
    def approved_memberships(self):
2083
        query = ProjectMembership.Q_ACCEPTED_STATES
2084
        return self.projectmembership_set.filter(query)
2085

    
2086
    @property
2087
    def approved_members(self):
2088
        return [m.person for m in self.approved_memberships]
2089

    
2090
    def add_member(self, user):
2091
        """
2092
        Raises:
2093
            django.exceptions.PermissionDenied
2094
            astakos.im.models.AstakosUser.DoesNotExist
2095
        """
2096
        if isinstance(user, (int, long)):
2097
            user = AstakosUser.objects.get(user=user)
2098

    
2099
        m, created = ProjectMembership.objects.get_or_create(
2100
            person=user, project=self
2101
        )
2102
        m.accept()
2103

    
2104
    def remove_member(self, user):
2105
        """
2106
        Raises:
2107
            django.exceptions.PermissionDenied
2108
            astakos.im.models.AstakosUser.DoesNotExist
2109
            astakos.im.models.ProjectMembership.DoesNotExist
2110
        """
2111
        if isinstance(user, (int, long)):
2112
            user = AstakosUser.objects.get(user=user)
2113

    
2114
        m = ProjectMembership.objects.get(person=user, project=self)
2115
        m.remove()
2116

    
2117

    
2118
CHAIN_STATE = {
2119
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2120
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2121
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2122
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2123
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2124

    
2125
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2126
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2127
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2128
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2129
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2130

    
2131
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2132
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2133
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2134
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2135
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2136

    
2137
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2138
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2139
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2140
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2141
    }
2142

    
2143

    
2144
class PendingMembershipError(Exception):
2145
    pass
2146

    
2147

    
2148
class ProjectMembershipManager(ForUpdateManager):
2149

    
2150
    def any_accepted(self):
2151
        q = (Q(state=ProjectMembership.ACCEPTED) |
2152
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
2153
        return self.filter(q)
2154

    
2155
    def actually_accepted(self):
2156
        q = self.model.Q_ACTUALLY_ACCEPTED
2157
        return self.filter(q)
2158

    
2159
    def requested(self):
2160
        return self.filter(state=ProjectMembership.REQUESTED)
2161

    
2162
    def suspended(self):
2163
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2164

    
2165
class ProjectMembership(models.Model):
2166

    
2167
    person              =   models.ForeignKey(AstakosUser)
2168
    request_date        =   models.DateField(auto_now_add=True)
2169
    project             =   models.ForeignKey(Project)
2170

    
2171
    REQUESTED           =   0
2172
    ACCEPTED            =   1
2173
    LEAVE_REQUESTED     =   5
2174
    # User deactivation
2175
    USER_SUSPENDED      =   10
2176
    # Project deactivation
2177
    PROJECT_DEACTIVATED =   100
2178

    
2179
    REMOVED             =   200
2180

    
2181
    ASSOCIATED_STATES   =   set([REQUESTED,
2182
                                 ACCEPTED,
2183
                                 LEAVE_REQUESTED,
2184
                                 USER_SUSPENDED,
2185
                                 PROJECT_DEACTIVATED])
2186

    
2187
    ACCEPTED_STATES     =   set([ACCEPTED,
2188
                                 LEAVE_REQUESTED,
2189
                                 USER_SUSPENDED,
2190
                                 PROJECT_DEACTIVATED])
2191

    
2192
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2193

    
2194
    state               =   models.IntegerField(default=REQUESTED,
2195
                                                db_index=True)
2196
    is_pending          =   models.BooleanField(default=False, db_index=True)
2197
    is_active           =   models.BooleanField(default=False, db_index=True)
2198
    application         =   models.ForeignKey(
2199
                                ProjectApplication,
2200
                                null=True,
2201
                                related_name='memberships')
2202
    pending_application =   models.ForeignKey(
2203
                                ProjectApplication,
2204
                                null=True,
2205
                                related_name='pending_memberships')
2206
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2207

    
2208
    acceptance_date     =   models.DateField(null=True, db_index=True)
2209
    leave_request_date  =   models.DateField(null=True)
2210

    
2211
    objects     =   ProjectMembershipManager()
2212

    
2213
    # Compiled queries
2214
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2215
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2216

    
2217
    MEMBERSHIP_STATE_DISPLAY = {
2218
        REQUESTED           : _('Requested'),
2219
        ACCEPTED            : _('Accepted'),
2220
        LEAVE_REQUESTED     : _('Leave Requested'),
2221
        USER_SUSPENDED      : _('Suspended'),
2222
        PROJECT_DEACTIVATED : _('Accepted'), # sic
2223
        REMOVED             : _('Pending removal'),
2224
        }
2225

    
2226
    USER_FRIENDLY_STATE_DISPLAY = {
2227
        REQUESTED           : _('Join requested'),
2228
        ACCEPTED            : _('Accepted member'),
2229
        LEAVE_REQUESTED     : _('Requested to leave'),
2230
        USER_SUSPENDED      : _('Suspended member'),
2231
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
2232
        REMOVED             : _('Pending removal'),
2233
        }
2234

    
2235
    def state_display(self):
2236
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2237

    
2238
    def user_friendly_state_display(self):
2239
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2240

    
2241
    def get_combined_state(self):
2242
        return self.state, self.is_active, self.is_pending
2243

    
2244
    class Meta:
2245
        unique_together = ("person", "project")
2246
        #index_together = [["project", "state"]]
2247

    
2248
    def __str__(self):
2249
        return uenc(_("<'%s' membership in '%s'>") % (
2250
                self.person.username, self.project))
2251

    
2252
    __repr__ = __str__
2253

    
2254
    def __init__(self, *args, **kwargs):
2255
        self.state = self.REQUESTED
2256
        super(ProjectMembership, self).__init__(*args, **kwargs)
2257

    
2258
    def _set_history_item(self, reason, date=None):
2259
        if isinstance(reason, basestring):
2260
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2261

    
2262
        history_item = ProjectMembershipHistory(
2263
                            serial=self.id,
2264
                            person=self.person_id,
2265
                            project=self.project_id,
2266
                            date=date or datetime.now(),
2267
                            reason=reason)
2268
        history_item.save()
2269
        serial = history_item.id
2270

    
2271
    def can_accept(self):
2272
        return self.state == self.REQUESTED
2273

    
2274
    def accept(self):
2275
        if self.is_pending:
2276
            m = _("%s: attempt to accept while is pending") % (self,)
2277
            raise AssertionError(m)
2278

    
2279
        if not self.can_accept():
2280
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2281
            raise AssertionError(m)
2282

    
2283
        now = datetime.now()
2284
        self.acceptance_date = now
2285
        self._set_history_item(reason='ACCEPT', date=now)
2286
        if self.project.is_approved():
2287
            self.state = self.ACCEPTED
2288
            self.is_pending = True
2289
        else:
2290
            self.state = self.PROJECT_DEACTIVATED
2291

    
2292
        self.save()
2293

    
2294
    def can_leave(self):
2295
        return self.state in self.ACCEPTED_STATES
2296

    
2297
    def leave_request(self):
2298
        if self.is_pending:
2299
            m = _("%s: attempt to request to leave while is pending") % (self,)
2300
            raise AssertionError(m)
2301

    
2302
        if not self.can_leave():
2303
            m = _("%s: attempt to request to leave in state '%s'") % (
2304
                self, self.state)
2305
            raise AssertionError(m)
2306

    
2307
        self.leave_request_date = datetime.now()
2308
        self.state = self.LEAVE_REQUESTED
2309
        self.save()
2310

    
2311
    def can_deny_leave(self):
2312
        return self.state == self.LEAVE_REQUESTED
2313

    
2314
    def leave_request_deny(self):
2315
        if self.is_pending:
2316
            m = _("%s: attempt to deny leave request while is pending") % (
2317
                self,)
2318
            raise AssertionError(m)
2319

    
2320
        if not self.can_deny_leave():
2321
            m = _("%s: attempt to deny leave request in state '%s'") % (
2322
                self, self.state)
2323
            raise AssertionError(m)
2324

    
2325
        self.leave_request_date = None
2326
        self.state = self.ACCEPTED
2327
        self.save()
2328

    
2329
    def can_cancel_leave(self):
2330
        return self.state == self.LEAVE_REQUESTED
2331

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

    
2338
        if not self.can_cancel_leave():
2339
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2340
                self, self.state)
2341
            raise AssertionError(m)
2342

    
2343
        self.leave_request_date = None
2344
        self.state = self.ACCEPTED
2345
        self.save()
2346

    
2347
    def can_remove(self):
2348
        return self.state in self.ACCEPTED_STATES
2349

    
2350
    def remove(self):
2351
        if self.is_pending:
2352
            m = _("%s: attempt to remove while is pending") % (self,)
2353
            raise AssertionError(m)
2354

    
2355
        if not self.can_remove():
2356
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2357
            raise AssertionError(m)
2358

    
2359
        self._set_history_item(reason='REMOVE')
2360
        self.state = self.REMOVED
2361
        self.is_pending = True
2362
        self.save()
2363

    
2364
    def can_reject(self):
2365
        return self.state == self.REQUESTED
2366

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

    
2372
        if not self.can_reject():
2373
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2374
            raise AssertionError(m)
2375

    
2376
        # rejected requests don't need sync,
2377
        # because they were never effected
2378
        self._set_history_item(reason='REJECT')
2379
        self.delete()
2380

    
2381
    def can_cancel(self):
2382
        return self.state == self.REQUESTED
2383

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

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

    
2393
        # rejected requests don't need sync,
2394
        # because they were never effected
2395
        self._set_history_item(reason='CANCEL')
2396
        self.delete()
2397

    
2398
    def get_diff_quotas(self, sub_list=None, add_list=None):
2399
        if sub_list is None:
2400
            sub_list = []
2401

    
2402
        if add_list is None:
2403
            add_list = []
2404

    
2405
        sub_append = sub_list.append
2406
        add_append = add_list.append
2407
        holder = self.person.uuid
2408

    
2409
        synced_application = self.application
2410
        if synced_application is not None:
2411
            cur_grants = synced_application.projectresourcegrant_set.all()
2412
            for grant in cur_grants:
2413
                sub_append(QuotaLimits(
2414
                               holder       = holder,
2415
                               resource     = str(grant.resource),
2416
                               capacity     = grant.member_capacity,
2417
                               import_limit = grant.member_import_limit,
2418
                               export_limit = grant.member_export_limit))
2419

    
2420
        pending_application = self.pending_application
2421
        if pending_application is not None:
2422
            new_grants = pending_application.projectresourcegrant_set.all()
2423
            for new_grant in new_grants:
2424
                add_append(QuotaLimits(
2425
                               holder       = holder,
2426
                               resource     = str(new_grant.resource),
2427
                               capacity     = new_grant.member_capacity,
2428
                               import_limit = new_grant.member_import_limit,
2429
                               export_limit = new_grant.member_export_limit))
2430

    
2431
        return (sub_list, add_list)
2432

    
2433
    def set_sync(self):
2434
        if not self.is_pending:
2435
            m = _("%s: attempt to sync a non pending membership") % (self,)
2436
            raise AssertionError(m)
2437

    
2438
        state = self.state
2439
        if state in self.ACTUALLY_ACCEPTED:
2440
            pending_application = self.pending_application
2441
            if pending_application is None:
2442
                m = _("%s: attempt to sync an empty pending application") % (
2443
                    self,)
2444
                raise AssertionError(m)
2445

    
2446
            self.application = pending_application
2447
            self.is_active = True
2448

    
2449
            self.pending_application = None
2450
            self.pending_serial = None
2451

    
2452
            # project.application may have changed in the meantime,
2453
            # in which case we stay PENDING;
2454
            # we are safe to check due to select_for_update
2455
            if self.application == self.project.application:
2456
                self.is_pending = False
2457
            self.save()
2458

    
2459
        elif state == self.PROJECT_DEACTIVATED:
2460
            if self.pending_application:
2461
                m = _("%s: attempt to sync in state '%s' "
2462
                      "with a pending application") % (self, state)
2463
                raise AssertionError(m)
2464

    
2465
            self.application = None
2466
            self.is_active = False
2467
            self.pending_serial = None
2468
            self.is_pending = False
2469
            self.save()
2470

    
2471
        elif state == self.REMOVED:
2472
            self.delete()
2473

    
2474
        else:
2475
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2476
            raise AssertionError(m)
2477

    
2478
    def reset_sync(self):
2479
        if not self.is_pending:
2480
            m = _("%s: attempt to reset a non pending membership") % (self,)
2481
            raise AssertionError(m)
2482

    
2483
        state = self.state
2484
        if state in [self.ACCEPTED, self.LEAVE_REQUESTED,
2485
                     self.PROJECT_DEACTIVATED, self.REMOVED]:
2486
            self.pending_application = None
2487
            self.pending_serial = None
2488
            self.save()
2489
        else:
2490
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2491
            raise AssertionError(m)
2492

    
2493
class Serial(models.Model):
2494
    serial  =   models.AutoField(primary_key=True)
2495

    
2496
def new_serial():
2497
    s = Serial.objects.create()
2498
    serial = s.serial
2499
    s.delete()
2500
    return serial
2501

    
2502
class SyncError(Exception):
2503
    pass
2504

    
2505
def reset_serials(serials):
2506
    objs = ProjectMembership.objects
2507
    q = objs.filter(pending_serial__in=serials).select_for_update()
2508
    memberships = list(q)
2509

    
2510
    if memberships:
2511
        for membership in memberships:
2512
            membership.reset_sync()
2513

    
2514
        transaction.commit()
2515

    
2516
def sync_finish_serials(serials_to_ack=None):
2517
    if serials_to_ack is None:
2518
        serials_to_ack = qh_query_serials([])
2519

    
2520
    serials_to_ack = set(serials_to_ack)
2521
    objs = ProjectMembership.objects
2522
    q = objs.filter(pending_serial__isnull=False).select_for_update()
2523
    memberships = list(q)
2524

    
2525
    if memberships:
2526
        for membership in memberships:
2527
            serial = membership.pending_serial
2528
            if serial in serials_to_ack:
2529
                membership.set_sync()
2530
            else:
2531
                membership.reset_sync()
2532

    
2533
        transaction.commit()
2534

    
2535
    qh_ack_serials(list(serials_to_ack))
2536
    return len(memberships)
2537

    
2538
def pre_sync_projects(sync=True):
2539
    ACCEPTED = ProjectMembership.ACCEPTED
2540
    LEAVE_REQUESTED = ProjectMembership.LEAVE_REQUESTED
2541
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2542
    objs = Project.objects
2543

    
2544
    modified = list(objs.modified_projects().select_for_update())
2545
    if sync:
2546
        for project in modified:
2547
            objects = project.projectmembership_set
2548

    
2549
            memberships = objects.actually_accepted().select_for_update()
2550
            for membership in memberships:
2551
                membership.is_pending = True
2552
                membership.save()
2553

    
2554
    reactivating = list(objs.reactivating_projects().select_for_update())
2555
    if sync:
2556
        for project in reactivating:
2557
            objects = project.projectmembership_set
2558

    
2559
            q = objects.filter(state=PROJECT_DEACTIVATED)
2560
            memberships = q.select_for_update()
2561
            for membership in memberships:
2562
                membership.is_pending = True
2563
                if membership.leave_request_date is None:
2564
                    membership.state = ACCEPTED
2565
                else:
2566
                    membership.state = LEAVE_REQUESTED
2567
                membership.save()
2568

    
2569
    deactivating = list(objs.deactivating_projects().select_for_update())
2570
    if sync:
2571
        for project in deactivating:
2572
            objects = project.projectmembership_set
2573

    
2574
            # Note: we keep a user-level deactivation
2575
            # (e.g. USER_SUSPENDED) intact
2576
            memberships = objects.actually_accepted().select_for_update()
2577
            for membership in memberships:
2578
                membership.is_pending = True
2579
                membership.state = PROJECT_DEACTIVATED
2580
                membership.save()
2581

    
2582
#    transaction.commit()
2583
    return (modified, reactivating, deactivating)
2584

    
2585
def set_sync_projects(exclude=None):
2586

    
2587
    ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED
2588
    objects = ProjectMembership.objects
2589

    
2590
    sub_quota, add_quota = [], []
2591

    
2592
    serial = new_serial()
2593

    
2594
    pending = objects.filter(is_pending=True).select_for_update()
2595
    for membership in pending:
2596

    
2597
        if membership.pending_application:
2598
            m = "%s: impossible: pending_application is not None (%s)" % (
2599
                membership, membership.pending_application)
2600
            raise AssertionError(m)
2601
        if membership.pending_serial:
2602
            m = "%s: impossible: pending_serial is not None (%s)" % (
2603
                membership, membership.pending_serial)
2604
            raise AssertionError(m)
2605

    
2606
        if exclude is not None:
2607
            uuid = membership.person.uuid
2608
            if uuid in exclude:
2609
                logger.warning("Excluded from sync: %s" % uuid)
2610
                continue
2611

    
2612
        if membership.state in ACTUALLY_ACCEPTED:
2613
            membership.pending_application = membership.project.application
2614

    
2615
        membership.pending_serial = serial
2616
        membership.get_diff_quotas(sub_quota, add_quota)
2617
        membership.save()
2618

    
2619
    transaction.commit()
2620
    return serial, sub_quota, add_quota
2621

    
2622
def do_sync_projects():
2623
    serial, sub_quota, add_quota = set_sync_projects()
2624
    r = qh_add_quota(serial, sub_quota, add_quota)
2625
    if not r:
2626
        return serial
2627

    
2628
    m = "cannot sync serial: %d" % serial
2629
    logger.error(m)
2630
    logger.error("Failed: %s" % r)
2631

    
2632
    reset_serials([serial])
2633
    uuids = set(uuid for (uuid, resource) in r)
2634
    serial, sub_quota, add_quota = set_sync_projects(exclude=uuids)
2635
    r = qh_add_quota(serial, sub_quota, add_quota)
2636
    if not r:
2637
        return serial
2638

    
2639
    m = "cannot sync serial: %d" % serial
2640
    logger.error(m)
2641
    logger.error("Failed: %s" % r)
2642
    raise SyncError(m)
2643

    
2644
def post_sync_projects():
2645
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2646
    Q_ACTUALLY_ACCEPTED = ProjectMembership.Q_ACTUALLY_ACCEPTED
2647
    objs = Project.objects
2648

    
2649
    modified = objs.modified_projects().select_for_update()
2650
    for project in modified:
2651
        objects = project.projectmembership_set
2652
        q = objects.filter(Q_ACTUALLY_ACCEPTED & Q(is_pending=True))
2653
        memberships = list(q.select_for_update())
2654
        if not memberships:
2655
            project.is_modified = False
2656
            project.save()
2657

    
2658
    reactivating = objs.reactivating_projects().select_for_update()
2659
    for project in reactivating:
2660
        objects = project.projectmembership_set
2661
        q = objects.filter(Q(state=PROJECT_DEACTIVATED) | Q(is_pending=True))
2662
        memberships = list(q.select_for_update())
2663
        if not memberships:
2664
            project.reactivate()
2665
            project.save()
2666

    
2667
    deactivating = objs.deactivating_projects().select_for_update()
2668
    for project in deactivating:
2669
        objects = project.projectmembership_set
2670
        q = objects.filter(Q_ACTUALLY_ACCEPTED | Q(is_pending=True))
2671
        memberships = list(q.select_for_update())
2672
        if not memberships:
2673
            project.deactivate()
2674
            project.save()
2675

    
2676
    transaction.commit()
2677

    
2678
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2679
    @with_lock(retries, retry_wait)
2680
    def _sync_projects(sync):
2681
        sync_finish_serials()
2682
        # Informative only -- no select_for_update()
2683
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2684

    
2685
        projects_log = pre_sync_projects(sync)
2686
        if sync:
2687
            serial = do_sync_projects()
2688
            sync_finish_serials([serial])
2689
            post_sync_projects()
2690

    
2691
        return (pending, projects_log)
2692
    return _sync_projects(sync)
2693

    
2694

    
2695

    
2696
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2697
    @with_lock(retries, retry_wait)
2698
    def _sync_users(users, sync):
2699
        sync_finish_serials()
2700

    
2701
        info = {}
2702
        for user in users:
2703
            info[user.uuid] = user.email
2704

    
2705
        existing, nonexisting = qh_check_users(users)
2706
        resources = get_resource_names()
2707
        qh_limits, qh_counters = qh_get_quotas(existing, resources)
2708
        astakos_initial = initial_quotas(users)
2709
        astakos_quotas = users_quotas(users, astakos_initial)
2710

    
2711
        if sync:
2712
            r = register_users(nonexisting)
2713
            r = send_quotas(astakos_quotas)
2714

    
2715
        return (existing, nonexisting,
2716
                qh_limits, qh_counters,
2717
                astakos_initial, astakos_quotas, info)
2718
    return _sync_users(users, sync)
2719

    
2720

    
2721
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2722
    users = AstakosUser.objects.verified()
2723
    return sync_users(users, sync, retries, retry_wait)
2724

    
2725
class ProjectMembershipHistory(models.Model):
2726
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2727
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2728

    
2729
    person  =   models.BigIntegerField()
2730
    project =   models.BigIntegerField()
2731
    date    =   models.DateField(auto_now_add=True)
2732
    reason  =   models.IntegerField()
2733
    serial  =   models.BigIntegerField()
2734

    
2735
### SIGNALS ###
2736
################
2737

    
2738
def create_astakos_user(u):
2739
    try:
2740
        AstakosUser.objects.get(user_ptr=u.pk)
2741
    except AstakosUser.DoesNotExist:
2742
        extended_user = AstakosUser(user_ptr_id=u.pk)
2743
        extended_user.__dict__.update(u.__dict__)
2744
        extended_user.save()
2745
        if not extended_user.has_auth_provider('local'):
2746
            extended_user.add_auth_provider('local')
2747
    except BaseException, e:
2748
        logger.exception(e)
2749

    
2750
def fix_superusers():
2751
    # Associate superusers with AstakosUser
2752
    admins = User.objects.filter(is_superuser=True)
2753
    for u in admins:
2754
        create_astakos_user(u)
2755

    
2756
def user_post_save(sender, instance, created, **kwargs):
2757
    if not created:
2758
        return
2759
    create_astakos_user(instance)
2760
post_save.connect(user_post_save, sender=User)
2761

    
2762
def astakosuser_post_save(sender, instance, created, **kwargs):
2763
    pass
2764

    
2765
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2766

    
2767
def resource_post_save(sender, instance, created, **kwargs):
2768
    pass
2769

    
2770
post_save.connect(resource_post_save, sender=Resource)
2771

    
2772
def renew_token(sender, instance, **kwargs):
2773
    if not instance.auth_token:
2774
        instance.renew_token()
2775
pre_save.connect(renew_token, sender=AstakosUser)
2776
pre_save.connect(renew_token, sender=Service)