Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 8d1636b5

History | View | Annotate | Download (93 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

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

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

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

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

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

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

    
88
logger = logging.getLogger(__name__)
89

    
90
DEFAULT_CONTENT_TYPE = None
91
_content_type = None
92

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

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

    
105
RESOURCE_SEPARATOR = '.'
106

    
107
inf = float('inf')
108

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

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

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

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

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

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

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

    
148

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
235
        ss.append(service)
236

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

    
250
                rs.append(r)
251

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

    
257
    register_services(ss)
258
    register_resources(rs)
259

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

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

    
275
    return _DEFAULT_QUOTA
276

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

    
283

    
284
class AstakosUserManager(UserManager):
285

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

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

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

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

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

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

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

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

    
338

    
339

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

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

    
356

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

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

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

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

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

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

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

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

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

    
400
    objects = AstakosUserManager()
401

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

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

    
412
    @realname.setter
413
    def realname(self, value):
414
        parts = value.split(' ')
415
        if len(parts) == 2:
416
            self.first_name = parts[0]
417
            self.last_name = parts[1]
418
        else:
419
            self.last_name = parts[0]
420

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

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

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

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

    
447
    def initial_quotas(self):
448
        quotas = dict(get_default_quota())
449
        for user_quota in self.policies:
450
            resource = user_quota.resource.full_name()
451
            quotas[resource] = user_quota.quota_values()
452
        return quotas
453

    
454
    def all_quotas(self, initial=None):
455
        if initial is None:
456
            quotas = self.initial_quotas()
457
        else:
458
            quotas = dict(initial)
459

    
460
        objects = self.projectmembership_set.select_related()
461
        memberships = objects.filter(is_active=True)
462
        for membership in memberships:
463
            application = membership.application
464
            if application is None:
465
                m = _("missing application for active membership %s"
466
                      % (membership,))
467
                raise AssertionError(m)
468

    
469
            grants = application.projectresourcegrant_set.all()
470
            for grant in grants:
471
                resource = grant.resource.full_name()
472
                prev = quotas.get(resource, 0)
473
                new = add_quota_values(prev, grant.member_quota_values())
474
                quotas[resource] = new
475
        return quotas
476

    
477
    @property
478
    def policies(self):
479
        return self.astakosuserquota_set.select_related().all()
480

    
481
    @policies.setter
482
    def policies(self, policies):
483
        for p in policies:
484
            p.setdefault('resource', '')
485
            p.setdefault('capacity', 0)
486
            p.setdefault('quantity', 0)
487
            p.setdefault('import_limit', 0)
488
            p.setdefault('export_limit', 0)
489
            p.setdefault('update', True)
490
            self.add_resource_policy(**p)
491

    
492
    def add_resource_policy(
493
            self, resource, capacity, quantity, import_limit,
494
            export_limit, update=True):
495
        """Raises ObjectDoesNotExist, IntegrityError"""
496
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
497
        resource = Resource.objects.get(service__name=s, name=r)
498
        if update:
499
            AstakosUserQuota.objects.update_or_create(
500
                user=self, resource=resource, defaults={
501
                    'capacity':capacity,
502
                    'quantity': quantity,
503
                    'import_limit':import_limit,
504
                    'export_limit':export_limit})
505
        else:
506
            q = self.astakosuserquota_set
507
            q.create(
508
                resource=resource, capacity=capacity, quanity=quantity,
509
                import_limit=import_limit, export_limit=export_limit)
510

    
511
    def get_resource_policy(self, resource):
512
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
513
        resource = Resource.objects.get(service__name=s, name=r)
514
        try:
515
            return AstakosUserQuota.objects.get(user=self, resource=resource)
516
        except AstakosUserQuota.DoesNotExist:
517
            return None
518

    
519
    def remove_resource_policy(self, service, resource):
520
        """Raises ObjectDoesNotExist, IntegrityError"""
521
        resource = Resource.objects.get(service__name=service, name=resource)
522
        q = self.policies.get(resource=resource).delete()
523

    
524
    def update_uuid(self):
525
        while not self.uuid:
526
            uuid_val =  str(uuid.uuid4())
527
            try:
528
                AstakosUser.objects.get(uuid=uuid_val)
529
            except AstakosUser.DoesNotExist, e:
530
                self.uuid = uuid_val
531
        return self.uuid
532

    
533
    def save(self, update_timestamps=True, **kwargs):
534
        if update_timestamps:
535
            if not self.id:
536
                self.date_joined = datetime.now()
537
            self.updated = datetime.now()
538

    
539
        # update date_signed_terms if necessary
540
        if self.__has_signed_terms != self.has_signed_terms:
541
            self.date_signed_terms = datetime.now()
542

    
543
        self.update_uuid()
544

    
545
        if self.username != self.email.lower():
546
            # set username
547
            self.username = self.email.lower()
548

    
549
        super(AstakosUser, self).save(**kwargs)
550

    
551
    def renew_token(self, flush_sessions=False, current_key=None):
552
        md5 = hashlib.md5()
553
        md5.update(settings.SECRET_KEY)
554
        md5.update(self.username)
555
        md5.update(self.realname.encode('ascii', 'ignore'))
556
        md5.update(asctime())
557

    
558
        self.auth_token = b64encode(md5.digest())
559
        self.auth_token_created = datetime.now()
560
        self.auth_token_expires = self.auth_token_created + \
561
                                  timedelta(hours=AUTH_TOKEN_DURATION)
562
        if flush_sessions:
563
            self.flush_sessions(current_key)
564
        msg = 'Token renewed for %s' % self.email
565
        logger.log(LOGGING_LEVEL, msg)
566

    
567
    def flush_sessions(self, current_key=None):
568
        q = self.sessions
569
        if current_key:
570
            q = q.exclude(session_key=current_key)
571

    
572
        keys = q.values_list('session_key', flat=True)
573
        if keys:
574
            msg = 'Flushing sessions: %s' % ','.join(keys)
575
            logger.log(LOGGING_LEVEL, msg, [])
576
        engine = import_module(settings.SESSION_ENGINE)
577
        for k in keys:
578
            s = engine.SessionStore(k)
579
            s.flush()
580

    
581
    def __unicode__(self):
582
        return '%s (%s)' % (self.realname, self.email)
583

    
584
    def conflicting_email(self):
585
        q = AstakosUser.objects.exclude(username=self.username)
586
        q = q.filter(email__iexact=self.email)
587
        if q.count() != 0:
588
            return True
589
        return False
590

    
591
    def email_change_is_pending(self):
592
        return self.emailchanges.count() > 0
593

    
594
    @property
595
    def signed_terms(self):
596
        term = get_latest_terms()
597
        if not term:
598
            return True
599
        if not self.has_signed_terms:
600
            return False
601
        if not self.date_signed_terms:
602
            return False
603
        if self.date_signed_terms < term.date:
604
            self.has_signed_terms = False
605
            self.date_signed_terms = None
606
            self.save()
607
            return False
608
        return True
609

    
610
    def set_invitations_level(self):
611
        """
612
        Update user invitation level
613
        """
614
        level = self.invitation.inviter.level + 1
615
        self.level = level
616
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
617

    
618
    def can_login_with_auth_provider(self, provider):
619
        if not self.has_auth_provider(provider):
620
            return False
621
        else:
622
            return auth_providers.get_provider(provider).is_available_for_login()
623

    
624
    def can_add_auth_provider(self, provider, include_unverified=False, **kwargs):
625
        provider_settings = auth_providers.get_provider(provider)
626

    
627
        if not provider_settings.is_available_for_add():
628
            return False
629

    
630
        if self.has_auth_provider(provider) and \
631
           provider_settings.one_per_user:
632
            return False
633

    
634
        if 'provider_info' in kwargs:
635
            kwargs.pop('provider_info')
636

    
637
        if 'identifier' in kwargs:
638
            try:
639
                # provider with specified params already exist
640
                if not include_unverified:
641
                    kwargs['user__email_verified'] = True
642
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
643
                                                                   **kwargs)
644
            except AstakosUser.DoesNotExist:
645
                return True
646
            else:
647
                return False
648

    
649
        return True
650

    
651
    def can_remove_auth_provider(self, module):
652
        provider = auth_providers.get_provider(module)
653
        existing = self.get_active_auth_providers()
654
        existing_for_provider = self.get_active_auth_providers(module=module)
655

    
656
        if len(existing) <= 1:
657
            return False
658

    
659
        if len(existing_for_provider) == 1 and provider.is_required():
660
            return False
661

    
662
        return provider.is_available_for_remove()
663

    
664
    def can_change_password(self):
665
        return self.has_auth_provider('local', auth_backend='astakos')
666

    
667
    def can_change_email(self):
668
        non_astakos_local = self.get_auth_providers().filter(module='local')
669
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
670
        return non_astakos_local.count() == 0
671

    
672
    def has_required_auth_providers(self):
673
        required = auth_providers.REQUIRED_PROVIDERS
674
        for provider in required:
675
            if not self.has_auth_provider(provider):
676
                return False
677
        return True
678

    
679
    def has_auth_provider(self, provider, **kwargs):
680
        return bool(self.get_auth_providers().filter(module=provider,
681
                                               **kwargs).count())
682

    
683
    def add_auth_provider(self, provider, **kwargs):
684
        info_data = ''
685
        if 'provider_info' in kwargs:
686
            info_data = kwargs.pop('provider_info')
687
            if isinstance(info_data, dict):
688
                info_data = json.dumps(info_data)
689

    
690
        if self.can_add_auth_provider(provider, **kwargs):
691
            if 'identifier' in kwargs:
692
                # clean up third party pending for activation users of the same
693
                # identifier
694
                AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
695
                                                                **kwargs)
696
            self.auth_providers.create(module=provider, active=True,
697
                                       info_data=info_data,
698
                                       **kwargs)
699
        else:
700
            raise Exception('Cannot add provider')
701

    
702
    def add_pending_auth_provider(self, pending):
703
        """
704
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
705
        the current user.
706
        """
707
        if not isinstance(pending, PendingThirdPartyUser):
708
            pending = PendingThirdPartyUser.objects.get(token=pending)
709

    
710
        provider = self.add_auth_provider(pending.provider,
711
                               identifier=pending.third_party_identifier,
712
                                affiliation=pending.affiliation,
713
                                          provider_info=pending.info)
714

    
715
        if email_re.match(pending.email or '') and pending.email != self.email:
716
            self.additionalmail_set.get_or_create(email=pending.email)
717

    
718
        pending.delete()
719
        return provider
720

    
721
    def remove_auth_provider(self, provider, **kwargs):
722
        self.get_auth_providers().get(module=provider, **kwargs).delete()
723

    
724
    # user urls
725
    def get_resend_activation_url(self):
726
        return reverse('send_activation', kwargs={'user_id': self.pk})
727

    
728
    def get_provider_remove_url(self, module, **kwargs):
729
        return reverse('remove_auth_provider', kwargs={
730
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
731

    
732
    def get_activation_url(self, nxt=False):
733
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
734
                                 quote(self.auth_token))
735
        if nxt:
736
            url += "&next=%s" % quote(nxt)
737
        return url
738

    
739
    def get_password_reset_url(self, token_generator=default_token_generator):
740
        return reverse('django.contrib.auth.views.password_reset_confirm',
741
                          kwargs={'uidb36':int_to_base36(self.id),
742
                                  'token':token_generator.make_token(self)})
743

    
744
    def get_primary_auth_provider(self):
745
        return self.get_auth_providers().filter()[0]
746

    
747
    def get_auth_providers(self):
748
        return self.auth_providers
749

    
750
    def get_available_auth_providers(self):
751
        """
752
        Returns a list of providers available for user to connect to.
753
        """
754
        providers = []
755
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
756
            if self.can_add_auth_provider(module):
757
                providers.append(provider_settings(self))
758

    
759
        modules = astakos_settings.IM_MODULES
760
        def key(p):
761
            if not p.module in modules:
762
                return 100
763
            return modules.index(p.module)
764
        providers = sorted(providers, key=key)
765
        return providers
766

    
767
    def get_active_auth_providers(self, **filters):
768
        providers = []
769
        for provider in self.get_auth_providers().active(**filters):
770
            if auth_providers.get_provider(provider.module).is_available_for_login():
771
                providers.append(provider)
772

    
773
        modules = astakos_settings.IM_MODULES
774
        def key(p):
775
            if not p.module in modules:
776
                return 100
777
            return modules.index(p.module)
778
        providers = sorted(providers, key=key)
779
        return providers
780

    
781
    @property
782
    def auth_providers_display(self):
783
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
784

    
785
    def get_inactive_message(self):
786
        msg_extra = ''
787
        message = ''
788
        if self.activation_sent:
789
            if self.email_verified:
790
                message = _(astakos_messages.ACCOUNT_INACTIVE)
791
            else:
792
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
793
                if astakos_settings.MODERATION_ENABLED:
794
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
795
                else:
796
                    url = self.get_resend_activation_url()
797
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
798
                                u' ' + \
799
                                _('<a href="%s">%s?</a>') % (url,
800
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
801
        else:
802
            if astakos_settings.MODERATION_ENABLED:
803
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
804
            else:
805
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
806
                url = self.get_resend_activation_url()
807
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
808
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
809

    
810
        return mark_safe(message + u' '+ msg_extra)
811

    
812
    def owns_application(self, application):
813
        return application.owner == self
814

    
815
    def owns_project(self, project):
816
        return project.application.owner == self
817

    
818
    def is_associated(self, project):
819
        try:
820
            m = ProjectMembership.objects.get(person=self, project=project)
821
            return m.state in ProjectMembership.ASSOCIATED_STATES
822
        except ProjectMembership.DoesNotExist:
823
            return False
824

    
825
    def get_membership(self, project):
826
        try:
827
            return ProjectMembership.objects.get(
828
                project=project,
829
                person=self)
830
        except ProjectMembership.DoesNotExist:
831
            return None
832

    
833
    def membership_display(self, project):
834
        m = self.get_membership(project)
835
        if m is None:
836
            return _('Not a member')
837
        else:
838
            return m.user_friendly_state_display()
839

    
840
    def non_owner_can_view(self, maybe_project):
841
        if self.is_project_admin():
842
            return True
843
        if maybe_project is None:
844
            return False
845
        project = maybe_project
846
        if self.is_associated(project):
847
            return True
848
        if project.is_deactivated():
849
            return False
850
        return True
851

    
852
    def settings(self):
853
        return UserSetting.objects.filter(user=self)
854

    
855

    
856
class AstakosUserAuthProviderManager(models.Manager):
857

    
858
    def active(self, **filters):
859
        return self.filter(active=True, **filters)
860

    
861
    def remove_unverified_providers(self, provider, **filters):
862
        try:
863
            existing = self.filter(module=provider, user__email_verified=False, **filters)
864
            for p in existing:
865
                p.user.delete()
866
        except:
867
            pass
868

    
869

    
870

    
871
class AstakosUserAuthProvider(models.Model):
872
    """
873
    Available user authentication methods.
874
    """
875
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
876
                                   null=True, default=None)
877
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
878
    module = models.CharField(_('Provider'), max_length=255, blank=False,
879
                                default='local')
880
    identifier = models.CharField(_('Third-party identifier'),
881
                                              max_length=255, null=True,
882
                                              blank=True)
883
    active = models.BooleanField(default=True)
884
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
885
                                   default='astakos')
886
    info_data = models.TextField(default="", null=True, blank=True)
887
    created = models.DateTimeField('Creation date', auto_now_add=True)
888

    
889
    objects = AstakosUserAuthProviderManager()
890

    
891
    class Meta:
892
        unique_together = (('identifier', 'module', 'user'), )
893
        ordering = ('module', 'created')
894

    
895
    def __init__(self, *args, **kwargs):
896
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
897
        try:
898
            self.info = json.loads(self.info_data)
899
            if not self.info:
900
                self.info = {}
901
        except Exception, e:
902
            self.info = {}
903

    
904
        for key,value in self.info.iteritems():
905
            setattr(self, 'info_%s' % key, value)
906

    
907

    
908
    @property
909
    def settings(self):
910
        return auth_providers.get_provider(self.module)
911

    
912
    @property
913
    def details_display(self):
914
        try:
915
            params = self.user.__dict__
916
            params.update(self.__dict__)
917
            return self.settings.get_details_tpl_display % params
918
        except:
919
            return ''
920

    
921
    @property
922
    def title_display(self):
923
        title_tpl = self.settings.get_title_display
924
        try:
925
            if self.settings.get_user_title_display:
926
                title_tpl = self.settings.get_user_title_display
927
        except Exception, e:
928
            pass
929
        try:
930
          return title_tpl % self.__dict__
931
        except:
932
          return self.settings.get_title_display % self.__dict__
933

    
934
    def can_remove(self):
935
        return self.user.can_remove_auth_provider(self.module)
936

    
937
    def delete(self, *args, **kwargs):
938
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
939
        if self.module == 'local':
940
            self.user.set_unusable_password()
941
            self.user.save()
942
        return ret
943

    
944
    def __repr__(self):
945
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
946

    
947
    def __unicode__(self):
948
        if self.identifier:
949
            return "%s:%s" % (self.module, self.identifier)
950
        if self.auth_backend:
951
            return "%s:%s" % (self.module, self.auth_backend)
952
        return self.module
953

    
954
    def save(self, *args, **kwargs):
955
        self.info_data = json.dumps(self.info)
956
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
957

    
958

    
959
class ExtendedManager(models.Manager):
960
    def _update_or_create(self, **kwargs):
961
        assert kwargs, \
962
            'update_or_create() must be passed at least one keyword argument'
963
        obj, created = self.get_or_create(**kwargs)
964
        defaults = kwargs.pop('defaults', {})
965
        if created:
966
            return obj, True, False
967
        else:
968
            try:
969
                params = dict(
970
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
971
                params.update(defaults)
972
                for attr, val in params.items():
973
                    if hasattr(obj, attr):
974
                        setattr(obj, attr, val)
975
                sid = transaction.savepoint()
976
                obj.save(force_update=True)
977
                transaction.savepoint_commit(sid)
978
                return obj, False, True
979
            except IntegrityError, e:
980
                transaction.savepoint_rollback(sid)
981
                try:
982
                    return self.get(**kwargs), False, False
983
                except self.model.DoesNotExist:
984
                    raise e
985

    
986
    update_or_create = _update_or_create
987

    
988

    
989
class AstakosUserQuota(models.Model):
990
    objects = ExtendedManager()
991
    capacity = intDecimalField()
992
    quantity = intDecimalField(default=0)
993
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
994
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
995
    resource = models.ForeignKey(Resource)
996
    user = models.ForeignKey(AstakosUser)
997

    
998
    class Meta:
999
        unique_together = ("resource", "user")
1000

    
1001
    def quota_values(self):
1002
        return QuotaValues(
1003
            quantity = self.quantity,
1004
            capacity = self.capacity,
1005
            import_limit = self.import_limit,
1006
            export_limit = self.export_limit)
1007

    
1008

    
1009
class ApprovalTerms(models.Model):
1010
    """
1011
    Model for approval terms
1012
    """
1013

    
1014
    date = models.DateTimeField(
1015
        _('Issue date'), db_index=True, auto_now_add=True)
1016
    location = models.CharField(_('Terms location'), max_length=255)
1017

    
1018

    
1019
class Invitation(models.Model):
1020
    """
1021
    Model for registring invitations
1022
    """
1023
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1024
                                null=True)
1025
    realname = models.CharField(_('Real name'), max_length=255)
1026
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1027
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1028
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1029
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1030
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1031

    
1032
    def __init__(self, *args, **kwargs):
1033
        super(Invitation, self).__init__(*args, **kwargs)
1034
        if not self.id:
1035
            self.code = _generate_invitation_code()
1036

    
1037
    def consume(self):
1038
        self.is_consumed = True
1039
        self.consumed = datetime.now()
1040
        self.save()
1041

    
1042
    def __unicode__(self):
1043
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1044

    
1045

    
1046
class EmailChangeManager(models.Manager):
1047

    
1048
    @transaction.commit_on_success
1049
    def change_email(self, activation_key):
1050
        """
1051
        Validate an activation key and change the corresponding
1052
        ``User`` if valid.
1053

1054
        If the key is valid and has not expired, return the ``User``
1055
        after activating.
1056

1057
        If the key is not valid or has expired, return ``None``.
1058

1059
        If the key is valid but the ``User`` is already active,
1060
        return ``None``.
1061

1062
        After successful email change the activation record is deleted.
1063

1064
        Throws ValueError if there is already
1065
        """
1066
        try:
1067
            email_change = self.model.objects.get(
1068
                activation_key=activation_key)
1069
            if email_change.activation_key_expired():
1070
                email_change.delete()
1071
                raise EmailChange.DoesNotExist
1072
            # is there an active user with this address?
1073
            try:
1074
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1075
            except AstakosUser.DoesNotExist:
1076
                pass
1077
            else:
1078
                raise ValueError(_('The new email address is reserved.'))
1079
            # update user
1080
            user = AstakosUser.objects.get(pk=email_change.user_id)
1081
            old_email = user.email
1082
            user.email = email_change.new_email_address
1083
            user.save()
1084
            email_change.delete()
1085
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1086
                                                          user.email)
1087
            logger.log(LOGGING_LEVEL, msg)
1088
            return user
1089
        except EmailChange.DoesNotExist:
1090
            raise ValueError(_('Invalid activation key.'))
1091

    
1092

    
1093
class EmailChange(models.Model):
1094
    new_email_address = models.EmailField(
1095
        _(u'new e-mail address'),
1096
        help_text=_('Provide a new email address. Until you verify the new '
1097
                    'address by following the activation link that will be '
1098
                    'sent to it, your old email address will remain active.'))
1099
    user = models.ForeignKey(
1100
        AstakosUser, unique=True, related_name='emailchanges')
1101
    requested_at = models.DateTimeField(auto_now_add=True)
1102
    activation_key = models.CharField(
1103
        max_length=40, unique=True, db_index=True)
1104

    
1105
    objects = EmailChangeManager()
1106

    
1107
    def get_url(self):
1108
        return reverse('email_change_confirm',
1109
                      kwargs={'activation_key': self.activation_key})
1110

    
1111
    def activation_key_expired(self):
1112
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1113
        return self.requested_at + expiration_date < datetime.now()
1114

    
1115

    
1116
class AdditionalMail(models.Model):
1117
    """
1118
    Model for registring invitations
1119
    """
1120
    owner = models.ForeignKey(AstakosUser)
1121
    email = models.EmailField()
1122

    
1123

    
1124
def _generate_invitation_code():
1125
    while True:
1126
        code = randint(1, 2L ** 63 - 1)
1127
        try:
1128
            Invitation.objects.get(code=code)
1129
            # An invitation with this code already exists, try again
1130
        except Invitation.DoesNotExist:
1131
            return code
1132

    
1133

    
1134
def get_latest_terms():
1135
    try:
1136
        term = ApprovalTerms.objects.order_by('-id')[0]
1137
        return term
1138
    except IndexError:
1139
        pass
1140
    return None
1141

    
1142
class PendingThirdPartyUser(models.Model):
1143
    """
1144
    Model for registring successful third party user authentications
1145
    """
1146
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1147
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1148
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1149
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1150
                                  null=True)
1151
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1152
                                 null=True)
1153
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1154
                                   null=True)
1155
    username = models.CharField(_('username'), max_length=30, unique=True,  
1156
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1157
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1158
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1159
    info = models.TextField(default="", null=True, blank=True)
1160

    
1161
    class Meta:
1162
        unique_together = ("provider", "third_party_identifier")
1163

    
1164
    def get_user_instance(self):
1165
        d = self.__dict__
1166
        d.pop('_state', None)
1167
        d.pop('id', None)
1168
        d.pop('token', None)
1169
        d.pop('created', None)
1170
        d.pop('info', None)
1171
        user = AstakosUser(**d)
1172

    
1173
        return user
1174

    
1175
    @property
1176
    def realname(self):
1177
        return '%s %s' %(self.first_name, self.last_name)
1178

    
1179
    @realname.setter
1180
    def realname(self, value):
1181
        parts = value.split(' ')
1182
        if len(parts) == 2:
1183
            self.first_name = parts[0]
1184
            self.last_name = parts[1]
1185
        else:
1186
            self.last_name = parts[0]
1187

    
1188
    def save(self, **kwargs):
1189
        if not self.id:
1190
            # set username
1191
            while not self.username:
1192
                username =  uuid.uuid4().hex[:30]
1193
                try:
1194
                    AstakosUser.objects.get(username = username)
1195
                except AstakosUser.DoesNotExist, e:
1196
                    self.username = username
1197
        super(PendingThirdPartyUser, self).save(**kwargs)
1198

    
1199
    def generate_token(self):
1200
        self.password = self.third_party_identifier
1201
        self.last_login = datetime.now()
1202
        self.token = default_token_generator.make_token(self)
1203

    
1204
    def existing_user(self):
1205
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1206
                                         auth_providers__identifier=self.third_party_identifier)
1207

    
1208
class SessionCatalog(models.Model):
1209
    session_key = models.CharField(_('session key'), max_length=40)
1210
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1211

    
1212

    
1213
class UserSetting(models.Model):
1214
    user = models.ForeignKey(AstakosUser)
1215
    setting = models.CharField(max_length=255)
1216
    value = models.IntegerField()
1217

    
1218
    objects = ForUpdateManager()
1219

    
1220
    class Meta:
1221
        unique_together = ("user", "setting")
1222

    
1223

    
1224
### PROJECTS ###
1225
################
1226

    
1227
def synced_model_metaclass(class_name, class_parents, class_attributes):
1228

    
1229
    new_attributes = {}
1230
    sync_attributes = {}
1231

    
1232
    for name, value in class_attributes.iteritems():
1233
        sync, underscore, rest = name.partition('_')
1234
        if sync == 'sync' and underscore == '_':
1235
            sync_attributes[rest] = value
1236
        else:
1237
            new_attributes[name] = value
1238

    
1239
    if 'prefix' not in sync_attributes:
1240
        m = ("you did not specify a 'sync_prefix' attribute "
1241
             "in class '%s'" % (class_name,))
1242
        raise ValueError(m)
1243

    
1244
    prefix = sync_attributes.pop('prefix')
1245
    class_name = sync_attributes.pop('classname', prefix + '_model')
1246

    
1247
    for name, value in sync_attributes.iteritems():
1248
        newname = prefix + '_' + name
1249
        if newname in new_attributes:
1250
            m = ("class '%s' was specified with prefix '%s' "
1251
                 "but it already has an attribute named '%s'"
1252
                 % (class_name, prefix, newname))
1253
            raise ValueError(m)
1254

    
1255
        new_attributes[newname] = value
1256

    
1257
    newclass = type(class_name, class_parents, new_attributes)
1258
    return newclass
1259

    
1260

    
1261
def make_synced(prefix='sync', name='SyncedState'):
1262

    
1263
    the_name = name
1264
    the_prefix = prefix
1265

    
1266
    class SyncedState(models.Model):
1267

    
1268
        sync_classname      = the_name
1269
        sync_prefix         = the_prefix
1270
        __metaclass__       = synced_model_metaclass
1271

    
1272
        sync_new_state      = models.BigIntegerField(null=True)
1273
        sync_synced_state   = models.BigIntegerField(null=True)
1274
        STATUS_SYNCED       = 0
1275
        STATUS_PENDING      = 1
1276
        sync_status         = models.IntegerField(db_index=True)
1277

    
1278
        class Meta:
1279
            abstract = True
1280

    
1281
        class NotSynced(Exception):
1282
            pass
1283

    
1284
        def sync_init_state(self, state):
1285
            self.sync_synced_state = state
1286
            self.sync_new_state = state
1287
            self.sync_status = self.STATUS_SYNCED
1288

    
1289
        def sync_get_status(self):
1290
            return self.sync_status
1291

    
1292
        def sync_set_status(self):
1293
            if self.sync_new_state != self.sync_synced_state:
1294
                self.sync_status = self.STATUS_PENDING
1295
            else:
1296
                self.sync_status = self.STATUS_SYNCED
1297

    
1298
        def sync_set_synced(self):
1299
            self.sync_synced_state = self.sync_new_state
1300
            self.sync_status = self.STATUS_SYNCED
1301

    
1302
        def sync_get_synced_state(self):
1303
            return self.sync_synced_state
1304

    
1305
        def sync_set_new_state(self, new_state):
1306
            self.sync_new_state = new_state
1307
            self.sync_set_status()
1308

    
1309
        def sync_get_new_state(self):
1310
            return self.sync_new_state
1311

    
1312
        def sync_set_synced_state(self, synced_state):
1313
            self.sync_synced_state = synced_state
1314
            self.sync_set_status()
1315

    
1316
        def sync_get_pending_objects(self):
1317
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1318
            return self.objects.filter(**kw)
1319

    
1320
        def sync_get_synced_objects(self):
1321
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1322
            return self.objects.filter(**kw)
1323

    
1324
        def sync_verify_get_synced_state(self):
1325
            status = self.sync_get_status()
1326
            state = self.sync_get_synced_state()
1327
            verified = (status == self.STATUS_SYNCED)
1328
            return state, verified
1329

    
1330
        def sync_is_synced(self):
1331
            state, verified = self.sync_verify_get_synced_state()
1332
            return verified
1333

    
1334
    return SyncedState
1335

    
1336
SyncedState = make_synced(prefix='sync', name='SyncedState')
1337

    
1338

    
1339
class ChainManager(ForUpdateManager):
1340

    
1341
    def search_by_name(self, *search_strings):
1342
        projects = Project.objects.search_by_name(*search_strings)
1343
        chains = [p.id for p in projects]
1344
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1345
        apps = (app for app in apps if app.is_latest())
1346
        app_chains = [app.chain for app in apps if app.chain not in chains]
1347
        return chains + app_chains
1348

    
1349
    def all_full_state(self):
1350
        d = {}
1351
        chains = self.all()
1352
        for chain in chains:
1353
            d[chain.pk] = chain.full_state()
1354
        return d
1355

    
1356
    def of_project(self, project):
1357
        if project is None:
1358
            return None
1359
        try:
1360
            return self.get(chain=project.id)
1361
        except Chain.DoesNotExist:
1362
            raise AssertionError('project with no chain')
1363

    
1364

    
1365
class Chain(models.Model):
1366
    chain  =   models.AutoField(primary_key=True)
1367

    
1368
    def __str__(self):
1369
        return "%s" % (self.chain,)
1370

    
1371
    objects = ChainManager()
1372

    
1373
    PENDING            = 0
1374
    DENIED             = 3
1375
    DISMISSED          = 4
1376
    CANCELLED          = 5
1377

    
1378
    APPROVED           = 10
1379
    APPROVED_PENDING   = 11
1380
    SUSPENDED          = 12
1381
    SUSPENDED_PENDING  = 13
1382
    TERMINATED         = 14
1383
    TERMINATED_PENDING = 15
1384

    
1385
    PENDING_STATES = [PENDING,
1386
                      APPROVED_PENDING,
1387
                      SUSPENDED_PENDING,
1388
                      TERMINATED_PENDING,
1389
                      ]
1390

    
1391
    MODIFICATION_STATES = [APPROVED_PENDING,
1392
                           SUSPENDED_PENDING,
1393
                           TERMINATED_PENDING,
1394
                           ]
1395

    
1396
    RELEVANT_STATES = [PENDING,
1397
                       DENIED,
1398
                       APPROVED,
1399
                       APPROVED_PENDING,
1400
                       SUSPENDED,
1401
                       SUSPENDED_PENDING,
1402
                       TERMINATED_PENDING,
1403
                       ]
1404

    
1405
    SKIP_STATES = [DISMISSED,
1406
                   CANCELLED,
1407
                   TERMINATED]
1408

    
1409
    STATE_DISPLAY = {
1410
        PENDING            : _("Pending"),
1411
        DENIED             : _("Denied"),
1412
        DISMISSED          : _("Dismissed"),
1413
        CANCELLED          : _("Cancelled"),
1414
        APPROVED           : _("Active"),
1415
        APPROVED_PENDING   : _("Active - Pending"),
1416
        SUSPENDED          : _("Suspended"),
1417
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1418
        TERMINATED         : _("Terminated"),
1419
        TERMINATED_PENDING : _("Terminated - Pending"),
1420
        }
1421

    
1422

    
1423
    @classmethod
1424
    def _chain_state(cls, project_state, app_state):
1425
        s = CHAIN_STATE.get((project_state, app_state), None)
1426
        if s is None:
1427
            raise AssertionError('inconsistent chain state')
1428
        return s
1429

    
1430
    @classmethod
1431
    def chain_state(cls, project, app):
1432
        p_state = project.state if project else None
1433
        return cls._chain_state(p_state, app.state)
1434

    
1435
    @classmethod
1436
    def state_display(cls, s):
1437
        if s is None:
1438
            return _("Unknown")
1439
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1440

    
1441
    def last_application(self):
1442
        return self.chained_apps.order_by('-id')[0]
1443

    
1444
    def get_project(self):
1445
        try:
1446
            return self.chained_project
1447
        except Project.DoesNotExist:
1448
            return None
1449

    
1450
    def get_elements(self):
1451
        project = self.get_project()
1452
        app = self.last_application()
1453
        return project, app
1454

    
1455
    def full_state(self):
1456
        project, app = self.get_elements()
1457
        s = self.chain_state(project, app)
1458
        return s, project, app
1459

    
1460
def new_chain():
1461
    c = Chain.objects.create()
1462
    return c
1463

    
1464

    
1465
class ProjectApplicationManager(ForUpdateManager):
1466

    
1467
    def user_visible_projects(self, *filters, **kw_filters):
1468
        model = self.model
1469
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1470

    
1471
    def user_visible_by_chain(self, flt):
1472
        model = self.model
1473
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1474
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1475
        by_chain = dict(pending.annotate(models.Max('id')))
1476
        by_chain.update(approved.annotate(models.Max('id')))
1477
        return self.filter(flt, id__in=by_chain.values())
1478

    
1479
    def user_accessible_projects(self, user):
1480
        """
1481
        Return projects accessed by specified user.
1482
        """
1483
        if user.is_project_admin():
1484
            participates_filters = Q()
1485
        else:
1486
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1487
                                   Q(project__projectmembership__person=user)
1488

    
1489
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1490

    
1491
    def search_by_name(self, *search_strings):
1492
        q = Q()
1493
        for s in search_strings:
1494
            q = q | Q(name__icontains=s)
1495
        return self.filter(q)
1496

    
1497
    def latest_of_chain(self, chain_id):
1498
        try:
1499
            return self.filter(chain=chain_id).order_by('-id')[0]
1500
        except IndexError:
1501
            return None
1502

    
1503

    
1504
class ProjectApplication(models.Model):
1505
    applicant               =   models.ForeignKey(
1506
                                    AstakosUser,
1507
                                    related_name='projects_applied',
1508
                                    db_index=True)
1509

    
1510
    PENDING     =    0
1511
    APPROVED    =    1
1512
    REPLACED    =    2
1513
    DENIED      =    3
1514
    DISMISSED   =    4
1515
    CANCELLED   =    5
1516

    
1517
    state                   =   models.IntegerField(default=PENDING,
1518
                                                    db_index=True)
1519

    
1520
    owner                   =   models.ForeignKey(
1521
                                    AstakosUser,
1522
                                    related_name='projects_owned',
1523
                                    db_index=True)
1524

    
1525
    chain                   =   models.ForeignKey(Chain,
1526
                                                  related_name='chained_apps',
1527
                                                  db_column='chain')
1528
    precursor_application   =   models.ForeignKey('ProjectApplication',
1529
                                                  null=True,
1530
                                                  blank=True)
1531

    
1532
    name                    =   models.CharField(max_length=80)
1533
    homepage                =   models.URLField(max_length=255, null=True,
1534
                                                verify_exists=False)
1535
    description             =   models.TextField(null=True, blank=True)
1536
    start_date              =   models.DateTimeField(null=True, blank=True)
1537
    end_date                =   models.DateTimeField()
1538
    member_join_policy      =   models.IntegerField()
1539
    member_leave_policy     =   models.IntegerField()
1540
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1541
    resource_grants         =   models.ManyToManyField(
1542
                                    Resource,
1543
                                    null=True,
1544
                                    blank=True,
1545
                                    through='ProjectResourceGrant')
1546
    comments                =   models.TextField(null=True, blank=True)
1547
    issue_date              =   models.DateTimeField(auto_now_add=True)
1548
    response_date           =   models.DateTimeField(null=True, blank=True)
1549

    
1550
    objects                 =   ProjectApplicationManager()
1551

    
1552
    # Compiled queries
1553
    Q_PENDING  = Q(state=PENDING)
1554
    Q_APPROVED = Q(state=APPROVED)
1555
    Q_DENIED   = Q(state=DENIED)
1556

    
1557
    class Meta:
1558
        unique_together = ("chain", "id")
1559

    
1560
    def __unicode__(self):
1561
        return "%s applied by %s" % (self.name, self.applicant)
1562

    
1563
    # TODO: Move to a more suitable place
1564
    APPLICATION_STATE_DISPLAY = {
1565
        PENDING  : _('Pending review'),
1566
        APPROVED : _('Approved'),
1567
        REPLACED : _('Replaced'),
1568
        DENIED   : _('Denied'),
1569
        DISMISSED: _('Dismissed'),
1570
        CANCELLED: _('Cancelled')
1571
    }
1572

    
1573
    def get_project(self):
1574
        try:
1575
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1576
            return Project
1577
        except Project.DoesNotExist, e:
1578
            return None
1579

    
1580
    def state_display(self):
1581
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1582

    
1583
    def project_state_display(self):
1584
        try:
1585
            project = self.project
1586
            return project.state_display()
1587
        except Project.DoesNotExist:
1588
            return self.state_display()
1589

    
1590
    def add_resource_policy(self, service, resource, uplimit):
1591
        """Raises ObjectDoesNotExist, IntegrityError"""
1592
        q = self.projectresourcegrant_set
1593
        resource = Resource.objects.get(service__name=service, name=resource)
1594
        q.create(resource=resource, member_capacity=uplimit)
1595

    
1596
    def members_count(self):
1597
        return self.project.approved_memberships.count()
1598

    
1599
    @property
1600
    def grants(self):
1601
        return self.projectresourcegrant_set.values(
1602
            'member_capacity', 'resource__name', 'resource__service__name')
1603

    
1604
    @property
1605
    def resource_policies(self):
1606
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1607

    
1608
    @resource_policies.setter
1609
    def resource_policies(self, policies):
1610
        for p in policies:
1611
            service = p.get('service', None)
1612
            resource = p.get('resource', None)
1613
            uplimit = p.get('uplimit', 0)
1614
            self.add_resource_policy(service, resource, uplimit)
1615

    
1616
    def pending_modifications_incl_me(self):
1617
        q = self.chained_applications()
1618
        q = q.filter(Q(state=self.PENDING))
1619
        return q
1620

    
1621
    def last_pending_incl_me(self):
1622
        try:
1623
            return self.pending_modifications_incl_me().order_by('-id')[0]
1624
        except IndexError:
1625
            return None
1626

    
1627
    def pending_modifications(self):
1628
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1629

    
1630
    def last_pending(self):
1631
        try:
1632
            return self.pending_modifications().order_by('-id')[0]
1633
        except IndexError:
1634
            return None
1635

    
1636
    def is_modification(self):
1637
        # if self.state != self.PENDING:
1638
        #     return False
1639
        parents = self.chained_applications().filter(id__lt=self.id)
1640
        parents = parents.filter(state__in=[self.APPROVED])
1641
        return parents.count() > 0
1642

    
1643
    def chained_applications(self):
1644
        return ProjectApplication.objects.filter(chain=self.chain)
1645

    
1646
    def is_latest(self):
1647
        return self.chained_applications().order_by('-id')[0] == self
1648

    
1649
    def has_pending_modifications(self):
1650
        return bool(self.last_pending())
1651

    
1652
    def denied_modifications(self):
1653
        q = self.chained_applications()
1654
        q = q.filter(Q(state=self.DENIED))
1655
        q = q.filter(~Q(id=self.id))
1656
        return q
1657

    
1658
    def last_denied(self):
1659
        try:
1660
            return self.denied_modifications().order_by('-id')[0]
1661
        except IndexError:
1662
            return None
1663

    
1664
    def has_denied_modifications(self):
1665
        return bool(self.last_denied())
1666

    
1667
    def is_applied(self):
1668
        try:
1669
            self.project
1670
            return True
1671
        except Project.DoesNotExist:
1672
            return False
1673

    
1674
    def get_project(self):
1675
        try:
1676
            return Project.objects.get(id=self.chain)
1677
        except Project.DoesNotExist:
1678
            return None
1679

    
1680
    def project_exists(self):
1681
        return self.get_project() is not None
1682

    
1683
    def _get_project_for_update(self):
1684
        try:
1685
            objects = Project.objects
1686
            project = objects.get_for_update(id=self.chain)
1687
            return project
1688
        except Project.DoesNotExist:
1689
            return None
1690

    
1691
    def can_cancel(self):
1692
        return self.state == self.PENDING
1693

    
1694
    def cancel(self):
1695
        if not self.can_cancel():
1696
            m = _("cannot cancel: application '%s' in state '%s'") % (
1697
                    self.id, self.state)
1698
            raise AssertionError(m)
1699

    
1700
        self.state = self.CANCELLED
1701
        self.save()
1702

    
1703
    def can_dismiss(self):
1704
        return self.state == self.DENIED
1705

    
1706
    def dismiss(self):
1707
        if not self.can_dismiss():
1708
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1709
                    self.id, self.state)
1710
            raise AssertionError(m)
1711

    
1712
        self.state = self.DISMISSED
1713
        self.save()
1714

    
1715
    def can_deny(self):
1716
        return self.state == self.PENDING
1717

    
1718
    def deny(self):
1719
        if not self.can_deny():
1720
            m = _("cannot deny: application '%s' in state '%s'") % (
1721
                    self.id, self.state)
1722
            raise AssertionError(m)
1723

    
1724
        self.state = self.DENIED
1725
        self.response_date = datetime.now()
1726
        self.save()
1727

    
1728
    def can_approve(self):
1729
        return self.state == self.PENDING
1730

    
1731
    def approve(self, approval_user=None):
1732
        """
1733
        If approval_user then during owner membership acceptance
1734
        it is checked whether the request_user is eligible.
1735

1736
        Raises:
1737
            PermissionDenied
1738
        """
1739

    
1740
        if not transaction.is_managed():
1741
            raise AssertionError("NOPE")
1742

    
1743
        new_project_name = self.name
1744
        if not self.can_approve():
1745
            m = _("cannot approve: project '%s' in state '%s'") % (
1746
                    new_project_name, self.state)
1747
            raise AssertionError(m) # invalid argument
1748

    
1749
        now = datetime.now()
1750
        project = self._get_project_for_update()
1751

    
1752
        try:
1753
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1754
            conflicting_project = Project.objects.get(q)
1755
            if (conflicting_project != project):
1756
                m = (_("cannot approve: project with name '%s' "
1757
                       "already exists (id: %s)") % (
1758
                        new_project_name, conflicting_project.id))
1759
                raise PermissionDenied(m) # invalid argument
1760
        except Project.DoesNotExist:
1761
            pass
1762

    
1763
        new_project = False
1764
        if project is None:
1765
            new_project = True
1766
            project = Project(id=self.chain)
1767

    
1768
        project.name = new_project_name
1769
        project.application = self
1770
        project.last_approval_date = now
1771
        if not new_project:
1772
            project.is_modified = True
1773

    
1774
        project.save()
1775

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

    
1780
    @property
1781
    def member_join_policy_display(self):
1782
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1783

    
1784
    @property
1785
    def member_leave_policy_display(self):
1786
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1787

    
1788
class ProjectResourceGrant(models.Model):
1789

    
1790
    resource                =   models.ForeignKey(Resource)
1791
    project_application     =   models.ForeignKey(ProjectApplication,
1792
                                                  null=True)
1793
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1794
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1795
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1796
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1797
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1798
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1799

    
1800
    objects = ExtendedManager()
1801

    
1802
    class Meta:
1803
        unique_together = ("resource", "project_application")
1804

    
1805
    def member_quota_values(self):
1806
        return QuotaValues(
1807
            quantity = 0,
1808
            capacity = self.member_capacity,
1809
            import_limit = self.member_import_limit,
1810
            export_limit = self.member_export_limit)
1811

    
1812
    def display_member_capacity(self):
1813
        if self.member_capacity:
1814
            if self.resource.unit:
1815
                return ProjectResourceGrant.display_filesize(
1816
                    self.member_capacity)
1817
            else:
1818
                if math.isinf(self.member_capacity):
1819
                    return 'Unlimited'
1820
                else:
1821
                    return self.member_capacity
1822
        else:
1823
            return 'Unlimited'
1824

    
1825
    def __str__(self):
1826
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1827
                                        self.display_member_capacity())
1828

    
1829
    @classmethod
1830
    def display_filesize(cls, value):
1831
        try:
1832
            value = float(value)
1833
        except:
1834
            return
1835
        else:
1836
            if math.isinf(value):
1837
                return 'Unlimited'
1838
            if value > 1:
1839
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1840
                                [0, 0, 0, 0, 0, 0])
1841
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1842
                quotient = float(value) / 1024**exponent
1843
                unit, value_decimals = unit_list[exponent]
1844
                format_string = '{0:.%sf} {1}' % (value_decimals)
1845
                return format_string.format(quotient, unit)
1846
            if value == 0:
1847
                return '0 bytes'
1848
            if value == 1:
1849
                return '1 byte'
1850
            else:
1851
               return '0'
1852

    
1853

    
1854
class ProjectManager(ForUpdateManager):
1855

    
1856
    def terminated_projects(self):
1857
        q = self.model.Q_TERMINATED
1858
        return self.filter(q)
1859

    
1860
    def not_terminated_projects(self):
1861
        q = ~self.model.Q_TERMINATED
1862
        return self.filter(q)
1863

    
1864
    def terminating_projects(self):
1865
        q = self.model.Q_TERMINATED & Q(is_active=True)
1866
        return self.filter(q)
1867

    
1868
    def deactivated_projects(self):
1869
        q = self.model.Q_DEACTIVATED
1870
        return self.filter(q)
1871

    
1872
    def deactivating_projects(self):
1873
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1874
        return self.filter(q)
1875

    
1876
    def modified_projects(self):
1877
        return self.filter(is_modified=True)
1878

    
1879
    def reactivating_projects(self):
1880
        return self.filter(state=Project.APPROVED, is_active=False)
1881

    
1882
    def expired_projects(self):
1883
        q = (~Q(state=Project.TERMINATED) &
1884
              Q(application__end_date__lt=datetime.now()))
1885
        return self.filter(q)
1886

    
1887
    def search_by_name(self, *search_strings):
1888
        q = Q()
1889
        for s in search_strings:
1890
            q = q | Q(name__icontains=s)
1891
        return self.filter(q)
1892

    
1893

    
1894
class Project(models.Model):
1895

    
1896
    id                          =   models.OneToOneField(Chain,
1897
                                                      related_name='chained_project',
1898
                                                      db_column='id',
1899
                                                      primary_key=True)
1900

    
1901
    application                 =   models.OneToOneField(
1902
                                            ProjectApplication,
1903
                                            related_name='project')
1904
    last_approval_date          =   models.DateTimeField(null=True)
1905

    
1906
    members                     =   models.ManyToManyField(
1907
                                            AstakosUser,
1908
                                            through='ProjectMembership')
1909

    
1910
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1911
    deactivation_date           =   models.DateTimeField(null=True)
1912

    
1913
    creation_date               =   models.DateTimeField(auto_now_add=True)
1914
    name                        =   models.CharField(
1915
                                            max_length=80,
1916
                                            null=True,
1917
                                            db_index=True,
1918
                                            unique=True)
1919

    
1920
    APPROVED    = 1
1921
    SUSPENDED   = 10
1922
    TERMINATED  = 100
1923

    
1924
    is_modified                 =   models.BooleanField(default=False,
1925
                                                        db_index=True)
1926
    is_active                   =   models.BooleanField(default=True,
1927
                                                        db_index=True)
1928
    state                       =   models.IntegerField(default=APPROVED,
1929
                                                        db_index=True)
1930

    
1931
    objects     =   ProjectManager()
1932

    
1933
    # Compiled queries
1934
    Q_TERMINATED  = Q(state=TERMINATED)
1935
    Q_SUSPENDED   = Q(state=SUSPENDED)
1936
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1937

    
1938
    def __str__(self):
1939
        return uenc(_("<project %s '%s'>") %
1940
                    (self.id, udec(self.application.name)))
1941

    
1942
    __repr__ = __str__
1943

    
1944
    def __unicode__(self):
1945
        return _("<project %s '%s'>") % (self.id, self.application.name)
1946

    
1947
    STATE_DISPLAY = {
1948
        APPROVED   : 'Active',
1949
        SUSPENDED  : 'Suspended',
1950
        TERMINATED : 'Terminated'
1951
        }
1952

    
1953
    def state_display(self):
1954
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1955

    
1956
    def admin_state_display(self):
1957
        s = self.state_display()
1958
        if self.sync_pending():
1959
            s += ' (sync pending)'
1960
        return s
1961

    
1962
    def sync_pending(self):
1963
        if self.state != self.APPROVED:
1964
            return self.is_active
1965
        return not self.is_active or self.is_modified
1966

    
1967
    def expiration_info(self):
1968
        return (str(self.id), self.name, self.state_display(),
1969
                str(self.application.end_date))
1970

    
1971
    def is_deactivated(self, reason=None):
1972
        if reason is not None:
1973
            return self.state == reason
1974

    
1975
        return self.state != self.APPROVED
1976

    
1977
    def is_deactivating(self, reason=None):
1978
        if not self.is_active:
1979
            return False
1980

    
1981
        return self.is_deactivated(reason)
1982

    
1983
    def is_deactivated_strict(self, reason=None):
1984
        if self.is_active:
1985
            return False
1986

    
1987
        return self.is_deactivated(reason)
1988

    
1989
    ### Deactivation calls
1990

    
1991
    def deactivate(self):
1992
        self.deactivation_date = datetime.now()
1993
        self.is_active = False
1994

    
1995
    def reactivate(self):
1996
        self.deactivation_date = None
1997
        self.is_active = True
1998

    
1999
    def terminate(self):
2000
        self.deactivation_reason = 'TERMINATED'
2001
        self.state = self.TERMINATED
2002
        self.name = None
2003
        self.save()
2004

    
2005
    def suspend(self):
2006
        self.deactivation_reason = 'SUSPENDED'
2007
        self.state = self.SUSPENDED
2008
        self.save()
2009

    
2010
    def resume(self):
2011
        self.deactivation_reason = None
2012
        self.state = self.APPROVED
2013
        self.save()
2014

    
2015
    ### Logical checks
2016

    
2017
    def is_inconsistent(self):
2018
        now = datetime.now()
2019
        dates = [self.creation_date,
2020
                 self.last_approval_date,
2021
                 self.deactivation_date]
2022
        return any([date > now for date in dates])
2023

    
2024
    def is_active_strict(self):
2025
        return self.is_active and self.state == self.APPROVED
2026

    
2027
    def is_approved(self):
2028
        return self.state == self.APPROVED
2029

    
2030
    @property
2031
    def is_alive(self):
2032
        return not self.is_terminated
2033

    
2034
    @property
2035
    def is_terminated(self):
2036
        return self.is_deactivated(self.TERMINATED)
2037

    
2038
    @property
2039
    def is_suspended(self):
2040
        return self.is_deactivated(self.SUSPENDED)
2041

    
2042
    def violates_resource_grants(self):
2043
        return False
2044

    
2045
    def violates_members_limit(self, adding=0):
2046
        application = self.application
2047
        limit = application.limit_on_members_number
2048
        if limit is None:
2049
            return False
2050
        return (len(self.approved_members) + adding > limit)
2051

    
2052

    
2053
    ### Other
2054

    
2055
    def count_pending_memberships(self):
2056
        memb_set = self.projectmembership_set
2057
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2058
        return memb_count
2059

    
2060
    def members_count(self):
2061
        return self.approved_memberships.count()
2062

    
2063
    @property
2064
    def approved_memberships(self):
2065
        query = ProjectMembership.Q_ACCEPTED_STATES
2066
        return self.projectmembership_set.filter(query)
2067

    
2068
    @property
2069
    def approved_members(self):
2070
        return [m.person for m in self.approved_memberships]
2071

    
2072
    def add_member(self, user):
2073
        """
2074
        Raises:
2075
            django.exceptions.PermissionDenied
2076
            astakos.im.models.AstakosUser.DoesNotExist
2077
        """
2078
        if isinstance(user, (int, long)):
2079
            user = AstakosUser.objects.get(user=user)
2080

    
2081
        m, created = ProjectMembership.objects.get_or_create(
2082
            person=user, project=self
2083
        )
2084
        m.accept()
2085

    
2086
    def remove_member(self, user):
2087
        """
2088
        Raises:
2089
            django.exceptions.PermissionDenied
2090
            astakos.im.models.AstakosUser.DoesNotExist
2091
            astakos.im.models.ProjectMembership.DoesNotExist
2092
        """
2093
        if isinstance(user, (int, long)):
2094
            user = AstakosUser.objects.get(user=user)
2095

    
2096
        m = ProjectMembership.objects.get(person=user, project=self)
2097
        m.remove()
2098

    
2099

    
2100
CHAIN_STATE = {
2101
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2102
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2103
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2104
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2105
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2106

    
2107
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2108
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2109
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2110
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2111
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2112

    
2113
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2114
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2115
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2116
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2117
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2118

    
2119
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2120
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2121
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2122
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2123
    }
2124

    
2125

    
2126
class PendingMembershipError(Exception):
2127
    pass
2128

    
2129

    
2130
class ProjectMembershipManager(ForUpdateManager):
2131

    
2132
    def any_accepted(self):
2133
        q = (Q(state=ProjectMembership.ACCEPTED) |
2134
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
2135
        return self.filter(q)
2136

    
2137
    def actually_accepted(self):
2138
        q = self.model.Q_ACTUALLY_ACCEPTED
2139
        return self.filter(q)
2140

    
2141
    def requested(self):
2142
        return self.filter(state=ProjectMembership.REQUESTED)
2143

    
2144
    def suspended(self):
2145
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2146

    
2147
class ProjectMembership(models.Model):
2148

    
2149
    person              =   models.ForeignKey(AstakosUser)
2150
    request_date        =   models.DateField(auto_now_add=True)
2151
    project             =   models.ForeignKey(Project)
2152

    
2153
    REQUESTED           =   0
2154
    ACCEPTED            =   1
2155
    LEAVE_REQUESTED     =   5
2156
    # User deactivation
2157
    USER_SUSPENDED      =   10
2158
    # Project deactivation
2159
    PROJECT_DEACTIVATED =   100
2160

    
2161
    REMOVED             =   200
2162

    
2163
    ASSOCIATED_STATES   =   set([REQUESTED,
2164
                                 ACCEPTED,
2165
                                 LEAVE_REQUESTED,
2166
                                 USER_SUSPENDED,
2167
                                 PROJECT_DEACTIVATED])
2168

    
2169
    ACCEPTED_STATES     =   set([ACCEPTED,
2170
                                 LEAVE_REQUESTED,
2171
                                 USER_SUSPENDED,
2172
                                 PROJECT_DEACTIVATED])
2173

    
2174
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2175

    
2176
    state               =   models.IntegerField(default=REQUESTED,
2177
                                                db_index=True)
2178
    is_pending          =   models.BooleanField(default=False, db_index=True)
2179
    is_active           =   models.BooleanField(default=False, db_index=True)
2180
    application         =   models.ForeignKey(
2181
                                ProjectApplication,
2182
                                null=True,
2183
                                related_name='memberships')
2184
    pending_application =   models.ForeignKey(
2185
                                ProjectApplication,
2186
                                null=True,
2187
                                related_name='pending_memberships')
2188
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2189

    
2190
    acceptance_date     =   models.DateField(null=True, db_index=True)
2191
    leave_request_date  =   models.DateField(null=True)
2192

    
2193
    objects     =   ProjectMembershipManager()
2194

    
2195
    # Compiled queries
2196
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2197
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2198

    
2199
    MEMBERSHIP_STATE_DISPLAY = {
2200
        REQUESTED           : _('Requested'),
2201
        ACCEPTED            : _('Accepted'),
2202
        LEAVE_REQUESTED     : _('Leave Requested'),
2203
        USER_SUSPENDED      : _('Suspended'),
2204
        PROJECT_DEACTIVATED : _('Accepted'), # sic
2205
        REMOVED             : _('Pending removal'),
2206
        }
2207

    
2208
    USER_FRIENDLY_STATE_DISPLAY = {
2209
        REQUESTED           : _('Join requested'),
2210
        ACCEPTED            : _('Accepted member'),
2211
        LEAVE_REQUESTED     : _('Requested to leave'),
2212
        USER_SUSPENDED      : _('Suspended member'),
2213
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
2214
        REMOVED             : _('Pending removal'),
2215
        }
2216

    
2217
    def state_display(self):
2218
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2219

    
2220
    def user_friendly_state_display(self):
2221
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2222

    
2223
    def get_combined_state(self):
2224
        return self.state, self.is_active, self.is_pending
2225

    
2226
    class Meta:
2227
        unique_together = ("person", "project")
2228
        #index_together = [["project", "state"]]
2229

    
2230
    def __str__(self):
2231
        return uenc(_("<'%s' membership in '%s'>") % (
2232
                self.person.username, self.project))
2233

    
2234
    __repr__ = __str__
2235

    
2236
    def __init__(self, *args, **kwargs):
2237
        self.state = self.REQUESTED
2238
        super(ProjectMembership, self).__init__(*args, **kwargs)
2239

    
2240
    def _set_history_item(self, reason, date=None):
2241
        if isinstance(reason, basestring):
2242
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2243

    
2244
        history_item = ProjectMembershipHistory(
2245
                            serial=self.id,
2246
                            person=self.person_id,
2247
                            project=self.project_id,
2248
                            date=date or datetime.now(),
2249
                            reason=reason)
2250
        history_item.save()
2251
        serial = history_item.id
2252

    
2253
    def can_accept(self):
2254
        return self.state == self.REQUESTED
2255

    
2256
    def accept(self):
2257
        if self.is_pending:
2258
            m = _("%s: attempt to accept while is pending") % (self,)
2259
            raise AssertionError(m)
2260

    
2261
        if not self.can_accept():
2262
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2263
            raise AssertionError(m)
2264

    
2265
        now = datetime.now()
2266
        self.acceptance_date = now
2267
        self._set_history_item(reason='ACCEPT', date=now)
2268
        if self.project.is_approved():
2269
            self.state = self.ACCEPTED
2270
            self.is_pending = True
2271
        else:
2272
            self.state = self.PROJECT_DEACTIVATED
2273

    
2274
        self.save()
2275

    
2276
    def can_leave(self):
2277
        return self.state in self.ACCEPTED_STATES
2278

    
2279
    def leave_request(self):
2280
        if self.is_pending:
2281
            m = _("%s: attempt to request to leave while is pending") % (self,)
2282
            raise AssertionError(m)
2283

    
2284
        if not self.can_leave():
2285
            m = _("%s: attempt to request to leave in state '%s'") % (
2286
                self, self.state)
2287
            raise AssertionError(m)
2288

    
2289
        self.leave_request_date = datetime.now()
2290
        self.state = self.LEAVE_REQUESTED
2291
        self.save()
2292

    
2293
    def can_deny_leave(self):
2294
        return self.state == self.LEAVE_REQUESTED
2295

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

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

    
2307
        self.leave_request_date = None
2308
        self.state = self.ACCEPTED
2309
        self.save()
2310

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

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

    
2320
        if not self.can_cancel_leave():
2321
            m = _("%s: attempt to cancel 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_remove(self):
2330
        return self.state in self.ACCEPTED_STATES
2331

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

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

    
2341
        self._set_history_item(reason='REMOVE')
2342
        self.state = self.REMOVED
2343
        self.is_pending = True
2344
        self.save()
2345

    
2346
    def can_reject(self):
2347
        return self.state == self.REQUESTED
2348

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

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

    
2358
        # rejected requests don't need sync,
2359
        # because they were never effected
2360
        self._set_history_item(reason='REJECT')
2361
        self.delete()
2362

    
2363
    def can_cancel(self):
2364
        return self.state == self.REQUESTED
2365

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

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

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

    
2380
    def get_diff_quotas(self, sub_list=None, add_list=None):
2381
        if sub_list is None:
2382
            sub_list = []
2383

    
2384
        if add_list is None:
2385
            add_list = []
2386

    
2387
        sub_append = sub_list.append
2388
        add_append = add_list.append
2389
        holder = self.person.uuid
2390

    
2391
        synced_application = self.application
2392
        if synced_application is not None:
2393
            cur_grants = synced_application.projectresourcegrant_set.all()
2394
            for grant in cur_grants:
2395
                sub_append(QuotaLimits(
2396
                               holder       = holder,
2397
                               resource     = str(grant.resource),
2398
                               capacity     = grant.member_capacity,
2399
                               import_limit = grant.member_import_limit,
2400
                               export_limit = grant.member_export_limit))
2401

    
2402
        pending_application = self.pending_application
2403
        if pending_application is not None:
2404
            new_grants = pending_application.projectresourcegrant_set.all()
2405
            for new_grant in new_grants:
2406
                add_append(QuotaLimits(
2407
                               holder       = holder,
2408
                               resource     = str(new_grant.resource),
2409
                               capacity     = new_grant.member_capacity,
2410
                               import_limit = new_grant.member_import_limit,
2411
                               export_limit = new_grant.member_export_limit))
2412

    
2413
        return (sub_list, add_list)
2414

    
2415
    def set_sync(self):
2416
        if not self.is_pending:
2417
            m = _("%s: attempt to sync a non pending membership") % (self,)
2418
            raise AssertionError(m)
2419

    
2420
        state = self.state
2421
        if state in self.ACTUALLY_ACCEPTED:
2422
            pending_application = self.pending_application
2423
            if pending_application is None:
2424
                m = _("%s: attempt to sync an empty pending application") % (
2425
                    self,)
2426
                raise AssertionError(m)
2427

    
2428
            self.application = pending_application
2429
            self.is_active = True
2430

    
2431
            self.pending_application = None
2432
            self.pending_serial = None
2433

    
2434
            # project.application may have changed in the meantime,
2435
            # in which case we stay PENDING;
2436
            # we are safe to check due to select_for_update
2437
            if self.application == self.project.application:
2438
                self.is_pending = False
2439
            self.save()
2440

    
2441
        elif state == self.PROJECT_DEACTIVATED:
2442
            if self.pending_application:
2443
                m = _("%s: attempt to sync in state '%s' "
2444
                      "with a pending application") % (self, state)
2445
                raise AssertionError(m)
2446

    
2447
            self.application = None
2448
            self.is_active = False
2449
            self.pending_serial = None
2450
            self.is_pending = False
2451
            self.save()
2452

    
2453
        elif state == self.REMOVED:
2454
            self.delete()
2455

    
2456
        else:
2457
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2458
            raise AssertionError(m)
2459

    
2460
    def reset_sync(self):
2461
        if not self.is_pending:
2462
            m = _("%s: attempt to reset a non pending membership") % (self,)
2463
            raise AssertionError(m)
2464

    
2465
        state = self.state
2466
        if state in [self.ACCEPTED, self.LEAVE_REQUESTED,
2467
                     self.PROJECT_DEACTIVATED, self.REMOVED]:
2468
            self.pending_application = None
2469
            self.pending_serial = None
2470
            self.save()
2471
        else:
2472
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2473
            raise AssertionError(m)
2474

    
2475
class Serial(models.Model):
2476
    serial  =   models.AutoField(primary_key=True)
2477

    
2478
def new_serial():
2479
    s = Serial.objects.create()
2480
    serial = s.serial
2481
    s.delete()
2482
    return serial
2483

    
2484
class SyncError(Exception):
2485
    pass
2486

    
2487
def reset_serials(serials):
2488
    objs = ProjectMembership.objects
2489
    q = objs.filter(pending_serial__in=serials).select_for_update()
2490
    memberships = list(q)
2491

    
2492
    if memberships:
2493
        for membership in memberships:
2494
            membership.reset_sync()
2495

    
2496
        transaction.commit()
2497

    
2498
def sync_finish_serials(serials_to_ack=None):
2499
    if serials_to_ack is None:
2500
        serials_to_ack = qh_query_serials([])
2501

    
2502
    serials_to_ack = set(serials_to_ack)
2503
    objs = ProjectMembership.objects
2504
    q = objs.filter(pending_serial__isnull=False).select_for_update()
2505
    memberships = list(q)
2506

    
2507
    if memberships:
2508
        for membership in memberships:
2509
            serial = membership.pending_serial
2510
            if serial in serials_to_ack:
2511
                membership.set_sync()
2512
            else:
2513
                membership.reset_sync()
2514

    
2515
        transaction.commit()
2516

    
2517
    qh_ack_serials(list(serials_to_ack))
2518
    return len(memberships)
2519

    
2520
def pre_sync_projects(sync=True):
2521
    ACCEPTED = ProjectMembership.ACCEPTED
2522
    LEAVE_REQUESTED = ProjectMembership.LEAVE_REQUESTED
2523
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2524
    objs = Project.objects
2525

    
2526
    modified = list(objs.modified_projects().select_for_update())
2527
    if sync:
2528
        for project in modified:
2529
            objects = project.projectmembership_set
2530

    
2531
            memberships = objects.actually_accepted().select_for_update()
2532
            for membership in memberships:
2533
                membership.is_pending = True
2534
                membership.save()
2535

    
2536
    reactivating = list(objs.reactivating_projects().select_for_update())
2537
    if sync:
2538
        for project in reactivating:
2539
            objects = project.projectmembership_set
2540

    
2541
            q = objects.filter(state=PROJECT_DEACTIVATED)
2542
            memberships = q.select_for_update()
2543
            for membership in memberships:
2544
                membership.is_pending = True
2545
                if membership.leave_request_date is None:
2546
                    membership.state = ACCEPTED
2547
                else:
2548
                    membership.state = LEAVE_REQUESTED
2549
                membership.save()
2550

    
2551
    deactivating = list(objs.deactivating_projects().select_for_update())
2552
    if sync:
2553
        for project in deactivating:
2554
            objects = project.projectmembership_set
2555

    
2556
            # Note: we keep a user-level deactivation
2557
            # (e.g. USER_SUSPENDED) intact
2558
            memberships = objects.actually_accepted().select_for_update()
2559
            for membership in memberships:
2560
                membership.is_pending = True
2561
                membership.state = PROJECT_DEACTIVATED
2562
                membership.save()
2563

    
2564
#    transaction.commit()
2565
    return (modified, reactivating, deactivating)
2566

    
2567
def set_sync_projects(exclude=None):
2568

    
2569
    ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED
2570
    objects = ProjectMembership.objects
2571

    
2572
    sub_quota, add_quota = [], []
2573

    
2574
    serial = new_serial()
2575

    
2576
    pending = objects.filter(is_pending=True).select_for_update()
2577
    for membership in pending:
2578

    
2579
        if membership.pending_application:
2580
            m = "%s: impossible: pending_application is not None (%s)" % (
2581
                membership, membership.pending_application)
2582
            raise AssertionError(m)
2583
        if membership.pending_serial:
2584
            m = "%s: impossible: pending_serial is not None (%s)" % (
2585
                membership, membership.pending_serial)
2586
            raise AssertionError(m)
2587

    
2588
        if exclude is not None:
2589
            uuid = membership.person.uuid
2590
            if uuid in exclude:
2591
                logger.warning("Excluded from sync: %s" % uuid)
2592
                continue
2593

    
2594
        if membership.state in ACTUALLY_ACCEPTED:
2595
            membership.pending_application = membership.project.application
2596

    
2597
        membership.pending_serial = serial
2598
        membership.get_diff_quotas(sub_quota, add_quota)
2599
        membership.save()
2600

    
2601
    transaction.commit()
2602
    return serial, sub_quota, add_quota
2603

    
2604
def do_sync_projects():
2605
    serial, sub_quota, add_quota = set_sync_projects()
2606
    r = qh_add_quota(serial, sub_quota, add_quota)
2607
    if not r:
2608
        return serial
2609

    
2610
    m = "cannot sync serial: %d" % serial
2611
    logger.error(m)
2612
    logger.error("Failed: %s" % r)
2613

    
2614
    reset_serials([serial])
2615
    uuids = set(uuid for (uuid, resource) in r)
2616
    serial, sub_quota, add_quota = set_sync_projects(exclude=uuids)
2617
    r = qh_add_quota(serial, sub_quota, add_quota)
2618
    if not r:
2619
        return serial
2620

    
2621
    m = "cannot sync serial: %d" % serial
2622
    logger.error(m)
2623
    logger.error("Failed: %s" % r)
2624
    raise SyncError(m)
2625

    
2626
def post_sync_projects():
2627
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2628
    Q_ACTUALLY_ACCEPTED = ProjectMembership.Q_ACTUALLY_ACCEPTED
2629
    objs = Project.objects
2630

    
2631
    modified = objs.modified_projects().select_for_update()
2632
    for project in modified:
2633
        objects = project.projectmembership_set
2634
        q = objects.filter(Q_ACTUALLY_ACCEPTED & Q(is_pending=True))
2635
        memberships = list(q.select_for_update())
2636
        if not memberships:
2637
            project.is_modified = False
2638
            project.save()
2639

    
2640
    reactivating = objs.reactivating_projects().select_for_update()
2641
    for project in reactivating:
2642
        objects = project.projectmembership_set
2643
        q = objects.filter(Q(state=PROJECT_DEACTIVATED) | Q(is_pending=True))
2644
        memberships = list(q.select_for_update())
2645
        if not memberships:
2646
            project.reactivate()
2647
            project.save()
2648

    
2649
    deactivating = objs.deactivating_projects().select_for_update()
2650
    for project in deactivating:
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.deactivate()
2656
            project.save()
2657

    
2658
    transaction.commit()
2659

    
2660
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2661
    @with_lock(retries, retry_wait)
2662
    def _sync_projects(sync):
2663
        sync_finish_serials()
2664
        # Informative only -- no select_for_update()
2665
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2666

    
2667
        projects_log = pre_sync_projects(sync)
2668
        if sync:
2669
            serial = do_sync_projects()
2670
            sync_finish_serials([serial])
2671
            post_sync_projects()
2672

    
2673
        return (pending, projects_log)
2674
    return _sync_projects(sync)
2675

    
2676
def all_users_quotas(users):
2677
    initial = {}
2678
    quotas = {}
2679
    info = {}
2680
    for user in users:
2681
        uuid = user.uuid
2682
        info[uuid] = user.email
2683
        init = user.initial_quotas()
2684
        initial[uuid] = init
2685
        quotas[user.uuid] = user.all_quotas(initial=init)
2686
    return initial, quotas, info
2687

    
2688
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2689
    @with_lock(retries, retry_wait)
2690
    def _sync_users(users, sync):
2691
        sync_finish_serials()
2692

    
2693
        existing, nonexisting = qh_check_users(users)
2694
        resources = get_resource_names()
2695
        qh_limits, qh_counters = qh_get_quotas(existing, resources)
2696
        astakos_initial, astakos_quotas, info = all_users_quotas(users)
2697

    
2698
        if sync:
2699
            r = register_users(nonexisting)
2700
            r = send_quotas(astakos_quotas)
2701

    
2702
        return (existing, nonexisting,
2703
                qh_limits, qh_counters,
2704
                astakos_initial, astakos_quotas, info)
2705
    return _sync_users(users, sync)
2706

    
2707
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2708
    users = AstakosUser.objects.verified()
2709
    return sync_users(users, sync, retries, retry_wait)
2710

    
2711
class ProjectMembershipHistory(models.Model):
2712
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2713
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2714

    
2715
    person  =   models.BigIntegerField()
2716
    project =   models.BigIntegerField()
2717
    date    =   models.DateField(auto_now_add=True)
2718
    reason  =   models.IntegerField()
2719
    serial  =   models.BigIntegerField()
2720

    
2721
### SIGNALS ###
2722
################
2723

    
2724
def create_astakos_user(u):
2725
    try:
2726
        AstakosUser.objects.get(user_ptr=u.pk)
2727
    except AstakosUser.DoesNotExist:
2728
        extended_user = AstakosUser(user_ptr_id=u.pk)
2729
        extended_user.__dict__.update(u.__dict__)
2730
        extended_user.save()
2731
        if not extended_user.has_auth_provider('local'):
2732
            extended_user.add_auth_provider('local')
2733
    except BaseException, e:
2734
        logger.exception(e)
2735

    
2736
def fix_superusers():
2737
    # Associate superusers with AstakosUser
2738
    admins = User.objects.filter(is_superuser=True)
2739
    for u in admins:
2740
        create_astakos_user(u)
2741

    
2742
def user_post_save(sender, instance, created, **kwargs):
2743
    if not created:
2744
        return
2745
    create_astakos_user(instance)
2746
post_save.connect(user_post_save, sender=User)
2747

    
2748
def astakosuser_post_save(sender, instance, created, **kwargs):
2749
    pass
2750

    
2751
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2752

    
2753
def resource_post_save(sender, instance, created, **kwargs):
2754
    pass
2755

    
2756
post_save.connect(resource_post_save, sender=Resource)
2757

    
2758
def renew_token(sender, instance, **kwargs):
2759
    if not instance.auth_token:
2760
        instance.renew_token()
2761
pre_save.connect(renew_token, sender=AstakosUser)
2762
pre_save.connect(renew_token, sender=Service)