Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 96efed67

History | View | Annotate | Download (94 kB)

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

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

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

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

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

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

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

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

    
89
logger = logging.getLogger(__name__)
90

    
91
DEFAULT_CONTENT_TYPE = None
92
_content_type = None
93

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

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

    
106
RESOURCE_SEPARATOR = '.'
107

    
108
inf = float('inf')
109

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

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

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

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

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

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

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

    
149

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
236
        ss.append(service)
237

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

    
251
                rs.append(r)
252

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

    
258
    register_services(ss)
259
    register_resources(rs)
260

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

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

    
276
    return _DEFAULT_QUOTA
277

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

    
284

    
285
class AstakosUserManager(UserManager):
286

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

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

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

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

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

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

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

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

    
339

    
340

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

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

    
357

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

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

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

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

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

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

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

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

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

    
401
    objects = AstakosUserManager()
402

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
524
        self.update_uuid()
525

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

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

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

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

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

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

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

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

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

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

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

    
599
    def can_change_password(self):
600
        return self.has_auth_provider('local', auth_backend='astakos')
601

    
602
    def can_change_email(self):
603
        if not self.has_auth_provider('local'):
604
            return True
605

    
606
        local = self.get_auth_provider('local')._instance
607
        return local.auth_backend == 'astakos'
608

    
609
    # Auth providers related methods
610
    def get_auth_provider(self, module=None, identifier=None, **filters):
611
        if not module:
612
            return self.auth_providers.active()[0].settings
613

    
614
        params = {'module': module}
615
        if identifier:
616
            params['identifier'] = identifier
617
        params.update(filters)
618
        return self.auth_providers.active().get(**params).settings
619

    
620
    def has_auth_provider(self, provider, **kwargs):
621
        return bool(self.auth_providers.active().filter(module=provider,
622
                                                        **kwargs).count())
623

    
624
    def get_required_providers(self, **kwargs):
625
        return auth.REQUIRED_PROVIDERS.keys()
626

    
627
    def missing_required_providers(self):
628
        required = self.get_required_providers()
629
        missing = []
630
        for provider in required:
631
            if not self.has_auth_provider(provider):
632
                missing.append(auth.get_provider(provider, self))
633
        return missing
634

    
635
    def get_available_auth_providers(self, **filters):
636
        """
637
        Returns a list of providers available for add by the user.
638
        """
639
        modules = astakos_settings.IM_MODULES
640
        providers = []
641
        for p in modules:
642
            providers.append(auth.get_provider(p, self))
643
        available = []
644

    
645
        for p in providers:
646
            if p.get_add_policy:
647
                available.append(p)
648
        return available
649

    
650
    def get_disabled_auth_providers(self, **filters):
651
        providers = self.get_auth_providers(**filters)
652
        disabled = []
653
        for p in providers:
654
            if not p.get_login_policy:
655
                disabled.append(p)
656
        return disabled
657

    
658
    def get_enabled_auth_providers(self, **filters):
659
        providers = self.get_auth_providers(**filters)
660
        enabled = []
661
        for p in providers:
662
            if p.get_login_policy:
663
                enabled.append(p)
664
        return enabled
665

    
666
    def get_auth_providers(self, **filters):
667
        providers = []
668
        for provider in self.auth_providers.active(**filters):
669
            if provider.settings.module_enabled:
670
                providers.append(provider.settings)
671

    
672
        modules = astakos_settings.IM_MODULES
673

    
674
        def key(p):
675
            if not p.module in modules:
676
                return 100
677
            return modules.index(p.module)
678

    
679
        providers = sorted(providers, key=key)
680
        return providers
681

    
682
    # URL methods
683
    @property
684
    def auth_providers_display(self):
685
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
686
                         self.get_enabled_auth_providers()])
687

    
688
    def add_auth_provider(self, module='local', identifier=None, **params):
689
        provider = auth.get_provider(module, self, identifier, **params)
690
        provider.add_to_user()
691

    
692
    def get_resend_activation_url(self):
693
        return reverse('send_activation', kwargs={'user_id': self.pk})
694

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

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

    
707
    def get_inactive_message(self, provider_module, identifier=None):
708
        provider = self.get_auth_provider(provider_module, identifier)
709

    
710
        msg_extra = ''
711
        message = ''
712

    
713
        msg_inactive = provider.get_account_inactive_msg
714
        msg_pending = provider.get_pending_activation_msg
715
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
716
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
717
        msg_pending_mod = provider.get_pending_moderation_msg
718
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
719

    
720
        if self.activation_sent:
721
            if self.email_verified:
722
                message = msg_inactive
723
            else:
724
                message = msg_pending
725
                url = self.get_resend_activation_url()
726
                msg_extra = msg_pending_help + \
727
                            u' ' + \
728
                            '<a href="%s">%s?</a>' % (url, msg_resend)
729
        else:
730
            if astakos_settings.MODERATION_ENABLED:
731
                message = msg_pending_mod
732
            else:
733
                message = msg_pending
734
                url = self.get_resend_activation_url()
735
                msg_extra = '<a href="%s">%s?</a>' % (url, \
736
                                msg_resend)
737

    
738
        return mark_safe(message + u' '+ msg_extra)
739

    
740
    def owns_application(self, application):
741
        return application.owner == self
742

    
743
    def owns_project(self, project):
744
        return project.application.owner == self
745

    
746
    def is_associated(self, project):
747
        try:
748
            m = ProjectMembership.objects.get(person=self, project=project)
749
            return m.state in ProjectMembership.ASSOCIATED_STATES
750
        except ProjectMembership.DoesNotExist:
751
            return False
752

    
753
    def get_membership(self, project):
754
        try:
755
            return ProjectMembership.objects.get(
756
                project=project,
757
                person=self)
758
        except ProjectMembership.DoesNotExist:
759
            return None
760

    
761
    def membership_display(self, project):
762
        m = self.get_membership(project)
763
        if m is None:
764
            return _('Not a member')
765
        else:
766
            return m.user_friendly_state_display()
767

    
768
    def non_owner_can_view(self, maybe_project):
769
        if self.is_project_admin():
770
            return True
771
        if maybe_project is None:
772
            return False
773
        project = maybe_project
774
        if self.is_associated(project):
775
            return True
776
        if project.is_deactivated():
777
            return False
778
        return True
779

    
780
    def settings(self):
781
        return UserSetting.objects.filter(user=self)
782

    
783
    def all_quotas(self):
784
        quotas = users_quotas([self])
785
        try:
786
            return quotas[self.uuid]
787
        except:
788
            raise ValueError("could not compute quotas")
789

    
790

    
791
def initial_quotas(users):
792
    initial = {}
793
    default_quotas = get_default_quota()
794

    
795
    for user in users:
796
        uuid = user.uuid
797
        initial[uuid] = dict(default_quotas)
798

    
799
    objs = AstakosUserQuota.objects.select_related()
800
    orig_quotas = objs.filter(user__in=users)
801
    for user_quota in orig_quotas:
802
        uuid = user_quota.user.uuid
803
        user_init = initial.get(uuid, {})
804
        resource = user_quota.resource.full_name()
805
        user_init[resource] = user_quota.quota_values()
806
        initial[uuid] = user_init
807

    
808
    return initial
809

    
810

    
811
def users_quotas(users, initial=None):
812
    if initial is None:
813
        quotas = initial_quotas(users)
814
    else:
815
        quotas = copy.deepcopy(initial)
816

    
817
    objs = ProjectMembership.objects.select_related('application', 'person')
818
    memberships = objs.filter(person__in=users, is_active=True)
819

    
820
    apps = set(m.application for m in memberships if m.application is not None)
821
    objs = ProjectResourceGrant.objects.select_related()
822
    grants = objs.filter(project_application__in=apps)
823

    
824
    for membership in memberships:
825
        uuid = membership.person.uuid
826
        userquotas = quotas.get(uuid, {})
827

    
828
        application = membership.application
829
        if application is None:
830
            m = _("missing application for active membership %s"
831
                  % (membership,))
832
            raise AssertionError(m)
833

    
834
        for grant in grants:
835
            if grant.project_application_id != application.id:
836
                continue
837
            resource = grant.resource.full_name()
838
            prev = userquotas.get(resource, 0)
839
            new = add_quota_values(prev, grant.member_quota_values())
840
            userquotas[resource] = new
841
        quotas[uuid] = userquotas
842

    
843
    return quotas
844

    
845

    
846
class AstakosUserAuthProviderManager(models.Manager):
847

    
848
    def active(self, **filters):
849
        return self.filter(active=True, **filters)
850

    
851
    def remove_unverified_providers(self, provider, **filters):
852
        try:
853
            existing = self.filter(module=provider, user__email_verified=False,
854
                                   **filters)
855
            for p in existing:
856
                p.user.delete()
857
        except:
858
            pass
859

    
860
    def unverified(self, provider, **filters):
861
        try:
862
            return self.get(module=provider, user__email_verified=False,
863
                            **filters).settings
864
        except AstakosUserAuthProvider.DoesNotExist:
865
            return None
866

    
867
    def verified(self, provider, **filters):
868
        try:
869
            return self.get(module=provider, user__email_verified=True,
870
                            **filters).settings
871
        except AstakosUserAuthProvider.DoesNotExist:
872
            return None
873

    
874

    
875
class AuthProviderPolicyProfileManager(models.Manager):
876

    
877
    def active(self):
878
        return self.filter(active=True)
879

    
880
    def for_user(self, user, provider):
881
        policies = {}
882
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
883
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
884
        exclusive_q = exclusive_q1 | exclusive_q2
885

    
886
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
887
            policies.update(profile.policies)
888

    
889
        user_groups = user.groups.all().values('pk')
890
        for profile in self.active().filter(groups__in=user_groups).filter(
891
                exclusive_q):
892
            policies.update(profile.policies)
893
        return policies
894

    
895
    def add_policy(self, name, provider, group_or_user, exclusive=False,
896
                   **policies):
897
        is_group = isinstance(group_or_user, Group)
898
        profile, created = self.get_or_create(name=name, provider=provider,
899
                                              is_exclusive=exclusive)
900
        profile.is_exclusive = exclusive
901
        profile.save()
902
        if is_group:
903
            profile.groups.add(group_or_user)
904
        else:
905
            profile.users.add(group_or_user)
906
        profile.set_policies(policies)
907
        profile.save()
908
        return profile
909

    
910

    
911
class AuthProviderPolicyProfile(models.Model):
912
    name = models.CharField(_('Name'), max_length=255, blank=False,
913
                            null=False, db_index=True)
914
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
915
                                null=False)
916

    
917
    # apply policies to all providers excluding the one set in provider field
918
    is_exclusive = models.BooleanField(default=False)
919

    
920
    policy_add = models.NullBooleanField(null=True, default=None)
921
    policy_remove = models.NullBooleanField(null=True, default=None)
922
    policy_create = models.NullBooleanField(null=True, default=None)
923
    policy_login = models.NullBooleanField(null=True, default=None)
924
    policy_limit = models.IntegerField(null=True, default=None)
925
    policy_required = models.NullBooleanField(null=True, default=None)
926
    policy_automoderate = models.NullBooleanField(null=True, default=None)
927
    policy_switch = models.NullBooleanField(null=True, default=None)
928

    
929
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
930
                     'automoderate')
931

    
932
    priority = models.IntegerField(null=False, default=1)
933
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
934
    users = models.ManyToManyField(AstakosUser,
935
                                   related_name='authpolicy_profiles')
936
    active = models.BooleanField(default=True)
937

    
938
    objects = AuthProviderPolicyProfileManager()
939

    
940
    class Meta:
941
        ordering = ['priority']
942

    
943
    @property
944
    def policies(self):
945
        policies = {}
946
        for pkey in self.POLICY_FIELDS:
947
            value = getattr(self, 'policy_%s' % pkey, None)
948
            if value is None:
949
                continue
950
            policies[pkey] = value
951
        return policies
952

    
953
    def set_policies(self, policies_dict):
954
        for key, value in policies_dict.iteritems():
955
            if key in self.POLICY_FIELDS:
956
                setattr(self, 'policy_%s' % key, value)
957
        return self.policies
958

    
959

    
960
class AstakosUserAuthProvider(models.Model):
961
    """
962
    Available user authentication methods.
963
    """
964
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
965
                                   null=True, default=None)
966
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
967
    module = models.CharField(_('Provider'), max_length=255, blank=False,
968
                                default='local')
969
    identifier = models.CharField(_('Third-party identifier'),
970
                                              max_length=255, null=True,
971
                                              blank=True)
972
    active = models.BooleanField(default=True)
973
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
974
                                   default='astakos')
975
    info_data = models.TextField(default="", null=True, blank=True)
976
    created = models.DateTimeField('Creation date', auto_now_add=True)
977

    
978
    objects = AstakosUserAuthProviderManager()
979

    
980
    class Meta:
981
        unique_together = (('identifier', 'module', 'user'), )
982
        ordering = ('module', 'created')
983

    
984
    def __init__(self, *args, **kwargs):
985
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
986
        try:
987
            self.info = json.loads(self.info_data)
988
            if not self.info:
989
                self.info = {}
990
        except Exception, e:
991
            self.info = {}
992

    
993
        for key,value in self.info.iteritems():
994
            setattr(self, 'info_%s' % key, value)
995

    
996
    @property
997
    def settings(self):
998
        extra_data = {}
999

    
1000
        info_data = {}
1001
        if self.info_data:
1002
            info_data = json.loads(self.info_data)
1003

    
1004
        extra_data['info'] = info_data
1005

    
1006
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
1007
            extra_data[key] = getattr(self, key)
1008

    
1009
        extra_data['instance'] = self
1010
        return auth.get_provider(self.module, self.user,
1011
                                           self.identifier, **extra_data)
1012

    
1013
    def __repr__(self):
1014
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
1015

    
1016
    def __unicode__(self):
1017
        if self.identifier:
1018
            return "%s:%s" % (self.module, self.identifier)
1019
        if self.auth_backend:
1020
            return "%s:%s" % (self.module, self.auth_backend)
1021
        return self.module
1022

    
1023
    def save(self, *args, **kwargs):
1024
        self.info_data = json.dumps(self.info)
1025
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1026

    
1027

    
1028
class ExtendedManager(models.Manager):
1029
    def _update_or_create(self, **kwargs):
1030
        assert kwargs, \
1031
            'update_or_create() must be passed at least one keyword argument'
1032
        obj, created = self.get_or_create(**kwargs)
1033
        defaults = kwargs.pop('defaults', {})
1034
        if created:
1035
            return obj, True, False
1036
        else:
1037
            try:
1038
                params = dict(
1039
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
1040
                params.update(defaults)
1041
                for attr, val in params.items():
1042
                    if hasattr(obj, attr):
1043
                        setattr(obj, attr, val)
1044
                sid = transaction.savepoint()
1045
                obj.save(force_update=True)
1046
                transaction.savepoint_commit(sid)
1047
                return obj, False, True
1048
            except IntegrityError, e:
1049
                transaction.savepoint_rollback(sid)
1050
                try:
1051
                    return self.get(**kwargs), False, False
1052
                except self.model.DoesNotExist:
1053
                    raise e
1054

    
1055
    update_or_create = _update_or_create
1056

    
1057

    
1058
class AstakosUserQuota(models.Model):
1059
    objects = ExtendedManager()
1060
    capacity = intDecimalField()
1061
    quantity = intDecimalField(default=0)
1062
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1063
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1064
    resource = models.ForeignKey(Resource)
1065
    user = models.ForeignKey(AstakosUser)
1066

    
1067
    class Meta:
1068
        unique_together = ("resource", "user")
1069

    
1070
    def quota_values(self):
1071
        return QuotaValues(
1072
            quantity = self.quantity,
1073
            capacity = self.capacity,
1074
            import_limit = self.import_limit,
1075
            export_limit = self.export_limit)
1076

    
1077

    
1078
class ApprovalTerms(models.Model):
1079
    """
1080
    Model for approval terms
1081
    """
1082

    
1083
    date = models.DateTimeField(
1084
        _('Issue date'), db_index=True, auto_now_add=True)
1085
    location = models.CharField(_('Terms location'), max_length=255)
1086

    
1087

    
1088
class Invitation(models.Model):
1089
    """
1090
    Model for registring invitations
1091
    """
1092
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1093
                                null=True)
1094
    realname = models.CharField(_('Real name'), max_length=255)
1095
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1096
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1097
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1098
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1099
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1100

    
1101
    def __init__(self, *args, **kwargs):
1102
        super(Invitation, self).__init__(*args, **kwargs)
1103
        if not self.id:
1104
            self.code = _generate_invitation_code()
1105

    
1106
    def consume(self):
1107
        self.is_consumed = True
1108
        self.consumed = datetime.now()
1109
        self.save()
1110

    
1111
    def __unicode__(self):
1112
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1113

    
1114

    
1115
class EmailChangeManager(models.Manager):
1116

    
1117
    @transaction.commit_on_success
1118
    def change_email(self, activation_key):
1119
        """
1120
        Validate an activation key and change the corresponding
1121
        ``User`` if valid.
1122

1123
        If the key is valid and has not expired, return the ``User``
1124
        after activating.
1125

1126
        If the key is not valid or has expired, return ``None``.
1127

1128
        If the key is valid but the ``User`` is already active,
1129
        return ``None``.
1130

1131
        After successful email change the activation record is deleted.
1132

1133
        Throws ValueError if there is already
1134
        """
1135
        try:
1136
            email_change = self.model.objects.get(
1137
                activation_key=activation_key)
1138
            if email_change.activation_key_expired():
1139
                email_change.delete()
1140
                raise EmailChange.DoesNotExist
1141
            # is there an active user with this address?
1142
            try:
1143
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1144
            except AstakosUser.DoesNotExist:
1145
                pass
1146
            else:
1147
                raise ValueError(_('The new email address is reserved.'))
1148
            # update user
1149
            user = AstakosUser.objects.get(pk=email_change.user_id)
1150
            old_email = user.email
1151
            user.email = email_change.new_email_address
1152
            user.save()
1153
            email_change.delete()
1154
            msg = "User %s changed email from %s to %s" % (user.log_display,
1155
                                                           old_email,
1156
                                                           user.email)
1157
            logger.log(LOGGING_LEVEL, msg)
1158
            return user
1159
        except EmailChange.DoesNotExist:
1160
            raise ValueError(_('Invalid activation key.'))
1161

    
1162

    
1163
class EmailChange(models.Model):
1164
    new_email_address = models.EmailField(
1165
        _(u'new e-mail address'),
1166
        help_text=_('Provide a new email address. Until you verify the new '
1167
                    'address by following the activation link that will be '
1168
                    'sent to it, your old email address will remain active.'))
1169
    user = models.ForeignKey(
1170
        AstakosUser, unique=True, related_name='emailchanges')
1171
    requested_at = models.DateTimeField(auto_now_add=True)
1172
    activation_key = models.CharField(
1173
        max_length=40, unique=True, db_index=True)
1174

    
1175
    objects = EmailChangeManager()
1176

    
1177
    def get_url(self):
1178
        return reverse('email_change_confirm',
1179
                      kwargs={'activation_key': self.activation_key})
1180

    
1181
    def activation_key_expired(self):
1182
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1183
        return self.requested_at + expiration_date < datetime.now()
1184

    
1185

    
1186
class AdditionalMail(models.Model):
1187
    """
1188
    Model for registring invitations
1189
    """
1190
    owner = models.ForeignKey(AstakosUser)
1191
    email = models.EmailField()
1192

    
1193

    
1194
def _generate_invitation_code():
1195
    while True:
1196
        code = randint(1, 2L ** 63 - 1)
1197
        try:
1198
            Invitation.objects.get(code=code)
1199
            # An invitation with this code already exists, try again
1200
        except Invitation.DoesNotExist:
1201
            return code
1202

    
1203

    
1204
def get_latest_terms():
1205
    try:
1206
        term = ApprovalTerms.objects.order_by('-id')[0]
1207
        return term
1208
    except IndexError:
1209
        pass
1210
    return None
1211

    
1212

    
1213
class PendingThirdPartyUser(models.Model):
1214
    """
1215
    Model for registring successful third party user authentications
1216
    """
1217
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1218
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1219
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1220
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1221
                                  null=True)
1222
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1223
                                 null=True)
1224
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1225
                                   null=True)
1226
    username = models.CharField(_('username'), max_length=30, unique=True,
1227
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1228
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1229
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1230
    info = models.TextField(default="", null=True, blank=True)
1231

    
1232
    class Meta:
1233
        unique_together = ("provider", "third_party_identifier")
1234

    
1235
    def get_user_instance(self):
1236
        d = self.__dict__
1237
        d.pop('_state', None)
1238
        d.pop('id', None)
1239
        d.pop('token', None)
1240
        d.pop('created', None)
1241
        d.pop('info', None)
1242
        user = AstakosUser(**d)
1243

    
1244
        return user
1245

    
1246
    @property
1247
    def realname(self):
1248
        return '%s %s' %(self.first_name, self.last_name)
1249

    
1250
    @realname.setter
1251
    def realname(self, value):
1252
        parts = value.split(' ')
1253
        if len(parts) == 2:
1254
            self.first_name = parts[0]
1255
            self.last_name = parts[1]
1256
        else:
1257
            self.last_name = parts[0]
1258

    
1259
    def save(self, **kwargs):
1260
        if not self.id:
1261
            # set username
1262
            while not self.username:
1263
                username =  uuid.uuid4().hex[:30]
1264
                try:
1265
                    AstakosUser.objects.get(username = username)
1266
                except AstakosUser.DoesNotExist, e:
1267
                    self.username = username
1268
        super(PendingThirdPartyUser, self).save(**kwargs)
1269

    
1270
    def generate_token(self):
1271
        self.password = self.third_party_identifier
1272
        self.last_login = datetime.now()
1273
        self.token = default_token_generator.make_token(self)
1274

    
1275
    def existing_user(self):
1276
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1277
                                         auth_providers__identifier=self.third_party_identifier)
1278

    
1279
    def get_provider(self, user):
1280
        params = {
1281
            'info_data': self.info,
1282
            'affiliation': self.affiliation
1283
        }
1284
        return auth.get_provider(self.provider, user,
1285
                                 self.third_party_identifier, **params)
1286

    
1287
class SessionCatalog(models.Model):
1288
    session_key = models.CharField(_('session key'), max_length=40)
1289
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1290

    
1291

    
1292
class UserSetting(models.Model):
1293
    user = models.ForeignKey(AstakosUser)
1294
    setting = models.CharField(max_length=255)
1295
    value = models.IntegerField()
1296

    
1297
    objects = ForUpdateManager()
1298

    
1299
    class Meta:
1300
        unique_together = ("user", "setting")
1301

    
1302

    
1303
### PROJECTS ###
1304
################
1305

    
1306
def synced_model_metaclass(class_name, class_parents, class_attributes):
1307

    
1308
    new_attributes = {}
1309
    sync_attributes = {}
1310

    
1311
    for name, value in class_attributes.iteritems():
1312
        sync, underscore, rest = name.partition('_')
1313
        if sync == 'sync' and underscore == '_':
1314
            sync_attributes[rest] = value
1315
        else:
1316
            new_attributes[name] = value
1317

    
1318
    if 'prefix' not in sync_attributes:
1319
        m = ("you did not specify a 'sync_prefix' attribute "
1320
             "in class '%s'" % (class_name,))
1321
        raise ValueError(m)
1322

    
1323
    prefix = sync_attributes.pop('prefix')
1324
    class_name = sync_attributes.pop('classname', prefix + '_model')
1325

    
1326
    for name, value in sync_attributes.iteritems():
1327
        newname = prefix + '_' + name
1328
        if newname in new_attributes:
1329
            m = ("class '%s' was specified with prefix '%s' "
1330
                 "but it already has an attribute named '%s'"
1331
                 % (class_name, prefix, newname))
1332
            raise ValueError(m)
1333

    
1334
        new_attributes[newname] = value
1335

    
1336
    newclass = type(class_name, class_parents, new_attributes)
1337
    return newclass
1338

    
1339

    
1340
def make_synced(prefix='sync', name='SyncedState'):
1341

    
1342
    the_name = name
1343
    the_prefix = prefix
1344

    
1345
    class SyncedState(models.Model):
1346

    
1347
        sync_classname      = the_name
1348
        sync_prefix         = the_prefix
1349
        __metaclass__       = synced_model_metaclass
1350

    
1351
        sync_new_state      = models.BigIntegerField(null=True)
1352
        sync_synced_state   = models.BigIntegerField(null=True)
1353
        STATUS_SYNCED       = 0
1354
        STATUS_PENDING      = 1
1355
        sync_status         = models.IntegerField(db_index=True)
1356

    
1357
        class Meta:
1358
            abstract = True
1359

    
1360
        class NotSynced(Exception):
1361
            pass
1362

    
1363
        def sync_init_state(self, state):
1364
            self.sync_synced_state = state
1365
            self.sync_new_state = state
1366
            self.sync_status = self.STATUS_SYNCED
1367

    
1368
        def sync_get_status(self):
1369
            return self.sync_status
1370

    
1371
        def sync_set_status(self):
1372
            if self.sync_new_state != self.sync_synced_state:
1373
                self.sync_status = self.STATUS_PENDING
1374
            else:
1375
                self.sync_status = self.STATUS_SYNCED
1376

    
1377
        def sync_set_synced(self):
1378
            self.sync_synced_state = self.sync_new_state
1379
            self.sync_status = self.STATUS_SYNCED
1380

    
1381
        def sync_get_synced_state(self):
1382
            return self.sync_synced_state
1383

    
1384
        def sync_set_new_state(self, new_state):
1385
            self.sync_new_state = new_state
1386
            self.sync_set_status()
1387

    
1388
        def sync_get_new_state(self):
1389
            return self.sync_new_state
1390

    
1391
        def sync_set_synced_state(self, synced_state):
1392
            self.sync_synced_state = synced_state
1393
            self.sync_set_status()
1394

    
1395
        def sync_get_pending_objects(self):
1396
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1397
            return self.objects.filter(**kw)
1398

    
1399
        def sync_get_synced_objects(self):
1400
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1401
            return self.objects.filter(**kw)
1402

    
1403
        def sync_verify_get_synced_state(self):
1404
            status = self.sync_get_status()
1405
            state = self.sync_get_synced_state()
1406
            verified = (status == self.STATUS_SYNCED)
1407
            return state, verified
1408

    
1409
        def sync_is_synced(self):
1410
            state, verified = self.sync_verify_get_synced_state()
1411
            return verified
1412

    
1413
    return SyncedState
1414

    
1415
SyncedState = make_synced(prefix='sync', name='SyncedState')
1416

    
1417

    
1418
class ChainManager(ForUpdateManager):
1419

    
1420
    def search_by_name(self, *search_strings):
1421
        projects = Project.objects.search_by_name(*search_strings)
1422
        chains = [p.id for p in projects]
1423
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1424
        apps = (app for app in apps if app.is_latest())
1425
        app_chains = [app.chain for app in apps if app.chain not in chains]
1426
        return chains + app_chains
1427

    
1428
    def all_full_state(self):
1429
        chains = self.all()
1430
        cids = [c.chain for c in chains]
1431
        projects = Project.objects.select_related('application').in_bulk(cids)
1432

    
1433
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1434
        chain_latest = dict(objs.values_list('chain', 'latest'))
1435

    
1436
        objs = ProjectApplication.objects.select_related('applicant')
1437
        apps = objs.in_bulk(chain_latest.values())
1438

    
1439
        d = {}
1440
        for chain in chains:
1441
            pk = chain.pk
1442
            project = projects.get(pk, None)
1443
            app = apps[chain_latest[pk]]
1444
            d[chain.pk] = chain.get_state(project, app)
1445

    
1446
        return d
1447

    
1448
    def of_project(self, project):
1449
        if project is None:
1450
            return None
1451
        try:
1452
            return self.get(chain=project.id)
1453
        except Chain.DoesNotExist:
1454
            raise AssertionError('project with no chain')
1455

    
1456

    
1457
class Chain(models.Model):
1458
    chain  =   models.AutoField(primary_key=True)
1459

    
1460
    def __str__(self):
1461
        return "%s" % (self.chain,)
1462

    
1463
    objects = ChainManager()
1464

    
1465
    PENDING            = 0
1466
    DENIED             = 3
1467
    DISMISSED          = 4
1468
    CANCELLED          = 5
1469

    
1470
    APPROVED           = 10
1471
    APPROVED_PENDING   = 11
1472
    SUSPENDED          = 12
1473
    SUSPENDED_PENDING  = 13
1474
    TERMINATED         = 14
1475
    TERMINATED_PENDING = 15
1476

    
1477
    PENDING_STATES = [PENDING,
1478
                      APPROVED_PENDING,
1479
                      SUSPENDED_PENDING,
1480
                      TERMINATED_PENDING,
1481
                      ]
1482

    
1483
    MODIFICATION_STATES = [APPROVED_PENDING,
1484
                           SUSPENDED_PENDING,
1485
                           TERMINATED_PENDING,
1486
                           ]
1487

    
1488
    RELEVANT_STATES = [PENDING,
1489
                       DENIED,
1490
                       APPROVED,
1491
                       APPROVED_PENDING,
1492
                       SUSPENDED,
1493
                       SUSPENDED_PENDING,
1494
                       TERMINATED_PENDING,
1495
                       ]
1496

    
1497
    SKIP_STATES = [DISMISSED,
1498
                   CANCELLED,
1499
                   TERMINATED]
1500

    
1501
    STATE_DISPLAY = {
1502
        PENDING            : _("Pending"),
1503
        DENIED             : _("Denied"),
1504
        DISMISSED          : _("Dismissed"),
1505
        CANCELLED          : _("Cancelled"),
1506
        APPROVED           : _("Active"),
1507
        APPROVED_PENDING   : _("Active - Pending"),
1508
        SUSPENDED          : _("Suspended"),
1509
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1510
        TERMINATED         : _("Terminated"),
1511
        TERMINATED_PENDING : _("Terminated - Pending"),
1512
        }
1513

    
1514

    
1515
    @classmethod
1516
    def _chain_state(cls, project_state, app_state):
1517
        s = CHAIN_STATE.get((project_state, app_state), None)
1518
        if s is None:
1519
            raise AssertionError('inconsistent chain state')
1520
        return s
1521

    
1522
    @classmethod
1523
    def chain_state(cls, project, app):
1524
        p_state = project.state if project else None
1525
        return cls._chain_state(p_state, app.state)
1526

    
1527
    @classmethod
1528
    def state_display(cls, s):
1529
        if s is None:
1530
            return _("Unknown")
1531
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1532

    
1533
    def last_application(self):
1534
        return self.chained_apps.order_by('-id')[0]
1535

    
1536
    def get_project(self):
1537
        try:
1538
            return self.chained_project
1539
        except Project.DoesNotExist:
1540
            return None
1541

    
1542
    def get_elements(self):
1543
        project = self.get_project()
1544
        app = self.last_application()
1545
        return project, app
1546

    
1547
    def get_state(self, project, app):
1548
        s = self.chain_state(project, app)
1549
        return s, project, app
1550

    
1551
    def full_state(self):
1552
        project, app = self.get_elements()
1553
        return self.get_state(project, app)
1554

    
1555

    
1556
def new_chain():
1557
    c = Chain.objects.create()
1558
    return c
1559

    
1560

    
1561
class ProjectApplicationManager(ForUpdateManager):
1562

    
1563
    def user_visible_projects(self, *filters, **kw_filters):
1564
        model = self.model
1565
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1566

    
1567
    def user_visible_by_chain(self, flt):
1568
        model = self.model
1569
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1570
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1571
        by_chain = dict(pending.annotate(models.Max('id')))
1572
        by_chain.update(approved.annotate(models.Max('id')))
1573
        return self.filter(flt, id__in=by_chain.values())
1574

    
1575
    def user_accessible_projects(self, user):
1576
        """
1577
        Return projects accessed by specified user.
1578
        """
1579
        if user.is_project_admin():
1580
            participates_filters = Q()
1581
        else:
1582
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1583
                                   Q(project__projectmembership__person=user)
1584

    
1585
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1586

    
1587
    def search_by_name(self, *search_strings):
1588
        q = Q()
1589
        for s in search_strings:
1590
            q = q | Q(name__icontains=s)
1591
        return self.filter(q)
1592

    
1593
    def latest_of_chain(self, chain_id):
1594
        try:
1595
            return self.filter(chain=chain_id).order_by('-id')[0]
1596
        except IndexError:
1597
            return None
1598

    
1599

    
1600
class ProjectApplication(models.Model):
1601
    applicant               =   models.ForeignKey(
1602
                                    AstakosUser,
1603
                                    related_name='projects_applied',
1604
                                    db_index=True)
1605

    
1606
    PENDING     =    0
1607
    APPROVED    =    1
1608
    REPLACED    =    2
1609
    DENIED      =    3
1610
    DISMISSED   =    4
1611
    CANCELLED   =    5
1612

    
1613
    state                   =   models.IntegerField(default=PENDING,
1614
                                                    db_index=True)
1615

    
1616
    owner                   =   models.ForeignKey(
1617
                                    AstakosUser,
1618
                                    related_name='projects_owned',
1619
                                    db_index=True)
1620

    
1621
    chain                   =   models.ForeignKey(Chain,
1622
                                                  related_name='chained_apps',
1623
                                                  db_column='chain')
1624
    precursor_application   =   models.ForeignKey('ProjectApplication',
1625
                                                  null=True,
1626
                                                  blank=True)
1627

    
1628
    name                    =   models.CharField(max_length=80)
1629
    homepage                =   models.URLField(max_length=255, null=True,
1630
                                                verify_exists=False)
1631
    description             =   models.TextField(null=True, blank=True)
1632
    start_date              =   models.DateTimeField(null=True, blank=True)
1633
    end_date                =   models.DateTimeField()
1634
    member_join_policy      =   models.IntegerField()
1635
    member_leave_policy     =   models.IntegerField()
1636
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1637
    resource_grants         =   models.ManyToManyField(
1638
                                    Resource,
1639
                                    null=True,
1640
                                    blank=True,
1641
                                    through='ProjectResourceGrant')
1642
    comments                =   models.TextField(null=True, blank=True)
1643
    issue_date              =   models.DateTimeField(auto_now_add=True)
1644
    response_date           =   models.DateTimeField(null=True, blank=True)
1645
    response                =   models.TextField(null=True, blank=True)
1646

    
1647
    objects                 =   ProjectApplicationManager()
1648

    
1649
    # Compiled queries
1650
    Q_PENDING  = Q(state=PENDING)
1651
    Q_APPROVED = Q(state=APPROVED)
1652
    Q_DENIED   = Q(state=DENIED)
1653

    
1654
    class Meta:
1655
        unique_together = ("chain", "id")
1656

    
1657
    def __unicode__(self):
1658
        return "%s applied by %s" % (self.name, self.applicant)
1659

    
1660
    # TODO: Move to a more suitable place
1661
    APPLICATION_STATE_DISPLAY = {
1662
        PENDING  : _('Pending review'),
1663
        APPROVED : _('Approved'),
1664
        REPLACED : _('Replaced'),
1665
        DENIED   : _('Denied'),
1666
        DISMISSED: _('Dismissed'),
1667
        CANCELLED: _('Cancelled')
1668
    }
1669

    
1670
    @property
1671
    def log_display(self):
1672
        return "application %s (%s) for project %s" % (
1673
            self.id, self.name, self.chain)
1674

    
1675
    def get_project(self):
1676
        try:
1677
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1678
            return Project
1679
        except Project.DoesNotExist, e:
1680
            return None
1681

    
1682
    def state_display(self):
1683
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1684

    
1685
    def project_state_display(self):
1686
        try:
1687
            project = self.project
1688
            return project.state_display()
1689
        except Project.DoesNotExist:
1690
            return self.state_display()
1691

    
1692
    def add_resource_policy(self, service, resource, uplimit):
1693
        """Raises ObjectDoesNotExist, IntegrityError"""
1694
        q = self.projectresourcegrant_set
1695
        resource = Resource.objects.get(service__name=service, name=resource)
1696
        q.create(resource=resource, member_capacity=uplimit)
1697

    
1698
    def members_count(self):
1699
        return self.project.approved_memberships.count()
1700

    
1701
    @property
1702
    def grants(self):
1703
        return self.projectresourcegrant_set.values(
1704
            'member_capacity', 'resource__name', 'resource__service__name')
1705

    
1706
    @property
1707
    def resource_policies(self):
1708
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1709

    
1710
    @resource_policies.setter
1711
    def resource_policies(self, policies):
1712
        for p in policies:
1713
            service = p.get('service', None)
1714
            resource = p.get('resource', None)
1715
            uplimit = p.get('uplimit', 0)
1716
            self.add_resource_policy(service, resource, uplimit)
1717

    
1718
    def pending_modifications_incl_me(self):
1719
        q = self.chained_applications()
1720
        q = q.filter(Q(state=self.PENDING))
1721
        return q
1722

    
1723
    def last_pending_incl_me(self):
1724
        try:
1725
            return self.pending_modifications_incl_me().order_by('-id')[0]
1726
        except IndexError:
1727
            return None
1728

    
1729
    def pending_modifications(self):
1730
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1731

    
1732
    def last_pending(self):
1733
        try:
1734
            return self.pending_modifications().order_by('-id')[0]
1735
        except IndexError:
1736
            return None
1737

    
1738
    def is_modification(self):
1739
        # if self.state != self.PENDING:
1740
        #     return False
1741
        parents = self.chained_applications().filter(id__lt=self.id)
1742
        parents = parents.filter(state__in=[self.APPROVED])
1743
        return parents.count() > 0
1744

    
1745
    def chained_applications(self):
1746
        return ProjectApplication.objects.filter(chain=self.chain)
1747

    
1748
    def is_latest(self):
1749
        return self.chained_applications().order_by('-id')[0] == self
1750

    
1751
    def has_pending_modifications(self):
1752
        return bool(self.last_pending())
1753

    
1754
    def denied_modifications(self):
1755
        q = self.chained_applications()
1756
        q = q.filter(Q(state=self.DENIED))
1757
        q = q.filter(~Q(id=self.id))
1758
        return q
1759

    
1760
    def last_denied(self):
1761
        try:
1762
            return self.denied_modifications().order_by('-id')[0]
1763
        except IndexError:
1764
            return None
1765

    
1766
    def has_denied_modifications(self):
1767
        return bool(self.last_denied())
1768

    
1769
    def is_applied(self):
1770
        try:
1771
            self.project
1772
            return True
1773
        except Project.DoesNotExist:
1774
            return False
1775

    
1776
    def get_project(self):
1777
        try:
1778
            return Project.objects.get(id=self.chain)
1779
        except Project.DoesNotExist:
1780
            return None
1781

    
1782
    def project_exists(self):
1783
        return self.get_project() is not None
1784

    
1785
    def _get_project_for_update(self):
1786
        try:
1787
            objects = Project.objects
1788
            project = objects.get_for_update(id=self.chain)
1789
            return project
1790
        except Project.DoesNotExist:
1791
            return None
1792

    
1793
    def can_cancel(self):
1794
        return self.state == self.PENDING
1795

    
1796
    def cancel(self):
1797
        if not self.can_cancel():
1798
            m = _("cannot cancel: application '%s' in state '%s'") % (
1799
                    self.id, self.state)
1800
            raise AssertionError(m)
1801

    
1802
        self.state = self.CANCELLED
1803
        self.save()
1804

    
1805
    def can_dismiss(self):
1806
        return self.state == self.DENIED
1807

    
1808
    def dismiss(self):
1809
        if not self.can_dismiss():
1810
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1811
                    self.id, self.state)
1812
            raise AssertionError(m)
1813

    
1814
        self.state = self.DISMISSED
1815
        self.save()
1816

    
1817
    def can_deny(self):
1818
        return self.state == self.PENDING
1819

    
1820
    def deny(self, reason):
1821
        if not self.can_deny():
1822
            m = _("cannot deny: application '%s' in state '%s'") % (
1823
                    self.id, self.state)
1824
            raise AssertionError(m)
1825

    
1826
        self.state = self.DENIED
1827
        self.response_date = datetime.now()
1828
        self.response = reason
1829
        self.save()
1830

    
1831
    def can_approve(self):
1832
        return self.state == self.PENDING
1833

    
1834
    def approve(self, approval_user=None):
1835
        """
1836
        If approval_user then during owner membership acceptance
1837
        it is checked whether the request_user is eligible.
1838

1839
        Raises:
1840
            PermissionDenied
1841
        """
1842

    
1843
        if not transaction.is_managed():
1844
            raise AssertionError("NOPE")
1845

    
1846
        new_project_name = self.name
1847
        if not self.can_approve():
1848
            m = _("cannot approve: project '%s' in state '%s'") % (
1849
                    new_project_name, self.state)
1850
            raise AssertionError(m) # invalid argument
1851

    
1852
        now = datetime.now()
1853
        project = self._get_project_for_update()
1854

    
1855
        try:
1856
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1857
            conflicting_project = Project.objects.get(q)
1858
            if (conflicting_project != project):
1859
                m = (_("cannot approve: project with name '%s' "
1860
                       "already exists (id: %s)") % (
1861
                        new_project_name, conflicting_project.id))
1862
                raise PermissionDenied(m) # invalid argument
1863
        except Project.DoesNotExist:
1864
            pass
1865

    
1866
        new_project = False
1867
        if project is None:
1868
            new_project = True
1869
            project = Project(id=self.chain)
1870

    
1871
        project.name = new_project_name
1872
        project.application = self
1873
        project.last_approval_date = now
1874
        if not new_project:
1875
            project.is_modified = True
1876

    
1877
        project.save()
1878

    
1879
        self.state = self.APPROVED
1880
        self.response_date = now
1881
        self.save()
1882

    
1883
    @property
1884
    def member_join_policy_display(self):
1885
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1886

    
1887
    @property
1888
    def member_leave_policy_display(self):
1889
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1890

    
1891
class ProjectResourceGrant(models.Model):
1892

    
1893
    resource                =   models.ForeignKey(Resource)
1894
    project_application     =   models.ForeignKey(ProjectApplication,
1895
                                                  null=True)
1896
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1897
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1898
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1899
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1900
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1901
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1902

    
1903
    objects = ExtendedManager()
1904

    
1905
    class Meta:
1906
        unique_together = ("resource", "project_application")
1907

    
1908
    def member_quota_values(self):
1909
        return QuotaValues(
1910
            quantity = 0,
1911
            capacity = self.member_capacity,
1912
            import_limit = self.member_import_limit,
1913
            export_limit = self.member_export_limit)
1914

    
1915
    def display_member_capacity(self):
1916
        if self.member_capacity:
1917
            if self.resource.unit:
1918
                return ProjectResourceGrant.display_filesize(
1919
                    self.member_capacity)
1920
            else:
1921
                if math.isinf(self.member_capacity):
1922
                    return 'Unlimited'
1923
                else:
1924
                    return self.member_capacity
1925
        else:
1926
            return 'Unlimited'
1927

    
1928
    def __str__(self):
1929
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1930
                                        self.display_member_capacity())
1931

    
1932
    @classmethod
1933
    def display_filesize(cls, value):
1934
        try:
1935
            value = float(value)
1936
        except:
1937
            return
1938
        else:
1939
            if math.isinf(value):
1940
                return 'Unlimited'
1941
            if value > 1:
1942
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1943
                                [0, 0, 0, 0, 0, 0])
1944
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1945
                quotient = float(value) / 1024**exponent
1946
                unit, value_decimals = unit_list[exponent]
1947
                format_string = '{0:.%sf} {1}' % (value_decimals)
1948
                return format_string.format(quotient, unit)
1949
            if value == 0:
1950
                return '0 bytes'
1951
            if value == 1:
1952
                return '1 byte'
1953
            else:
1954
               return '0'
1955

    
1956

    
1957
class ProjectManager(ForUpdateManager):
1958

    
1959
    def terminated_projects(self):
1960
        q = self.model.Q_TERMINATED
1961
        return self.filter(q)
1962

    
1963
    def not_terminated_projects(self):
1964
        q = ~self.model.Q_TERMINATED
1965
        return self.filter(q)
1966

    
1967
    def terminating_projects(self):
1968
        q = self.model.Q_TERMINATED & Q(is_active=True)
1969
        return self.filter(q)
1970

    
1971
    def deactivated_projects(self):
1972
        q = self.model.Q_DEACTIVATED
1973
        return self.filter(q)
1974

    
1975
    def deactivating_projects(self):
1976
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1977
        return self.filter(q)
1978

    
1979
    def modified_projects(self):
1980
        return self.filter(is_modified=True)
1981

    
1982
    def reactivating_projects(self):
1983
        return self.filter(state=Project.APPROVED, is_active=False)
1984

    
1985
    def expired_projects(self):
1986
        q = (~Q(state=Project.TERMINATED) &
1987
              Q(application__end_date__lt=datetime.now()))
1988
        return self.filter(q)
1989

    
1990
    def search_by_name(self, *search_strings):
1991
        q = Q()
1992
        for s in search_strings:
1993
            q = q | Q(name__icontains=s)
1994
        return self.filter(q)
1995

    
1996

    
1997
class Project(models.Model):
1998

    
1999
    id                          =   models.OneToOneField(Chain,
2000
                                                      related_name='chained_project',
2001
                                                      db_column='id',
2002
                                                      primary_key=True)
2003

    
2004
    application                 =   models.OneToOneField(
2005
                                            ProjectApplication,
2006
                                            related_name='project')
2007
    last_approval_date          =   models.DateTimeField(null=True)
2008

    
2009
    members                     =   models.ManyToManyField(
2010
                                            AstakosUser,
2011
                                            through='ProjectMembership')
2012

    
2013
    deactivation_reason         =   models.CharField(max_length=255, null=True)
2014
    deactivation_date           =   models.DateTimeField(null=True)
2015

    
2016
    creation_date               =   models.DateTimeField(auto_now_add=True)
2017
    name                        =   models.CharField(
2018
                                            max_length=80,
2019
                                            null=True,
2020
                                            db_index=True,
2021
                                            unique=True)
2022

    
2023
    APPROVED    = 1
2024
    SUSPENDED   = 10
2025
    TERMINATED  = 100
2026

    
2027
    is_modified                 =   models.BooleanField(default=False,
2028
                                                        db_index=True)
2029
    is_active                   =   models.BooleanField(default=True,
2030
                                                        db_index=True)
2031
    state                       =   models.IntegerField(default=APPROVED,
2032
                                                        db_index=True)
2033

    
2034
    objects     =   ProjectManager()
2035

    
2036
    # Compiled queries
2037
    Q_TERMINATED  = Q(state=TERMINATED)
2038
    Q_SUSPENDED   = Q(state=SUSPENDED)
2039
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
2040

    
2041
    def __str__(self):
2042
        return uenc(_("<project %s '%s'>") %
2043
                    (self.id, udec(self.application.name)))
2044

    
2045
    __repr__ = __str__
2046

    
2047
    def __unicode__(self):
2048
        return _("<project %s '%s'>") % (self.id, self.application.name)
2049

    
2050
    STATE_DISPLAY = {
2051
        APPROVED   : 'Active',
2052
        SUSPENDED  : 'Suspended',
2053
        TERMINATED : 'Terminated'
2054
        }
2055

    
2056
    def state_display(self):
2057
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
2058

    
2059
    def admin_state_display(self):
2060
        s = self.state_display()
2061
        if self.sync_pending():
2062
            s += ' (sync pending)'
2063
        return s
2064

    
2065
    def sync_pending(self):
2066
        if self.state != self.APPROVED:
2067
            return self.is_active
2068
        return not self.is_active or self.is_modified
2069

    
2070
    def expiration_info(self):
2071
        return (str(self.id), self.name, self.state_display(),
2072
                str(self.application.end_date))
2073

    
2074
    def is_deactivated(self, reason=None):
2075
        if reason is not None:
2076
            return self.state == reason
2077

    
2078
        return self.state != self.APPROVED
2079

    
2080
    def is_deactivating(self, reason=None):
2081
        if not self.is_active:
2082
            return False
2083

    
2084
        return self.is_deactivated(reason)
2085

    
2086
    def is_deactivated_strict(self, reason=None):
2087
        if self.is_active:
2088
            return False
2089

    
2090
        return self.is_deactivated(reason)
2091

    
2092
    ### Deactivation calls
2093

    
2094
    def unset_modified(self):
2095
        self.is_modified = False
2096
        self.save()
2097

    
2098
    def deactivate(self):
2099
        self.deactivation_date = datetime.now()
2100
        self.is_active = False
2101
        self.save()
2102

    
2103
    def reactivate(self):
2104
        self.deactivation_date = None
2105
        self.is_active = True
2106
        self.save()
2107

    
2108
    def terminate(self):
2109
        self.deactivation_reason = 'TERMINATED'
2110
        self.state = self.TERMINATED
2111
        self.name = None
2112
        self.save()
2113

    
2114
    def suspend(self):
2115
        self.deactivation_reason = 'SUSPENDED'
2116
        self.state = self.SUSPENDED
2117
        self.save()
2118

    
2119
    def resume(self):
2120
        self.deactivation_reason = None
2121
        self.state = self.APPROVED
2122
        self.save()
2123

    
2124
    ### Logical checks
2125

    
2126
    def is_inconsistent(self):
2127
        now = datetime.now()
2128
        dates = [self.creation_date,
2129
                 self.last_approval_date,
2130
                 self.deactivation_date]
2131
        return any([date > now for date in dates])
2132

    
2133
    def is_active_strict(self):
2134
        return self.is_active and self.state == self.APPROVED
2135

    
2136
    def is_approved(self):
2137
        return self.state == self.APPROVED
2138

    
2139
    @property
2140
    def is_alive(self):
2141
        return not self.is_terminated
2142

    
2143
    @property
2144
    def is_terminated(self):
2145
        return self.is_deactivated(self.TERMINATED)
2146

    
2147
    @property
2148
    def is_suspended(self):
2149
        return self.is_deactivated(self.SUSPENDED)
2150

    
2151
    def violates_resource_grants(self):
2152
        return False
2153

    
2154
    def violates_members_limit(self, adding=0):
2155
        application = self.application
2156
        limit = application.limit_on_members_number
2157
        if limit is None:
2158
            return False
2159
        return (len(self.approved_members) + adding > limit)
2160

    
2161

    
2162
    ### Other
2163

    
2164
    def count_pending_memberships(self):
2165
        memb_set = self.projectmembership_set
2166
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2167
        return memb_count
2168

    
2169
    def members_count(self):
2170
        return self.approved_memberships.count()
2171

    
2172
    @property
2173
    def approved_memberships(self):
2174
        query = ProjectMembership.Q_ACCEPTED_STATES
2175
        return self.projectmembership_set.filter(query)
2176

    
2177
    @property
2178
    def approved_members(self):
2179
        return [m.person for m in self.approved_memberships]
2180

    
2181
    def add_member(self, user):
2182
        """
2183
        Raises:
2184
            django.exceptions.PermissionDenied
2185
            astakos.im.models.AstakosUser.DoesNotExist
2186
        """
2187
        if isinstance(user, (int, long)):
2188
            user = AstakosUser.objects.get(user=user)
2189

    
2190
        m, created = ProjectMembership.objects.get_or_create(
2191
            person=user, project=self
2192
        )
2193
        m.accept()
2194

    
2195
    def remove_member(self, user):
2196
        """
2197
        Raises:
2198
            django.exceptions.PermissionDenied
2199
            astakos.im.models.AstakosUser.DoesNotExist
2200
            astakos.im.models.ProjectMembership.DoesNotExist
2201
        """
2202
        if isinstance(user, (int, long)):
2203
            user = AstakosUser.objects.get(user=user)
2204

    
2205
        m = ProjectMembership.objects.get(person=user, project=self)
2206
        m.remove()
2207

    
2208

    
2209
CHAIN_STATE = {
2210
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2211
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2212
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2213
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2214
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2215

    
2216
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2217
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2218
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2219
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2220
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2221

    
2222
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2223
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2224
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2225
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2226
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2227

    
2228
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2229
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2230
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2231
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2232
    }
2233

    
2234

    
2235
class PendingMembershipError(Exception):
2236
    pass
2237

    
2238

    
2239
class ProjectMembershipManager(ForUpdateManager):
2240

    
2241
    def any_accepted(self):
2242
        q = self.model.Q_ACTUALLY_ACCEPTED
2243
        return self.filter(q)
2244

    
2245
    def actually_accepted(self):
2246
        q = self.model.Q_ACTUALLY_ACCEPTED
2247
        return self.filter(q)
2248

    
2249
    def requested(self):
2250
        return self.filter(state=ProjectMembership.REQUESTED)
2251

    
2252
    def suspended(self):
2253
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2254

    
2255
class ProjectMembership(models.Model):
2256

    
2257
    person              =   models.ForeignKey(AstakosUser)
2258
    request_date        =   models.DateField(auto_now_add=True)
2259
    project             =   models.ForeignKey(Project)
2260

    
2261
    REQUESTED           =   0
2262
    ACCEPTED            =   1
2263
    LEAVE_REQUESTED     =   5
2264
    # User deactivation
2265
    USER_SUSPENDED      =   10
2266

    
2267
    REMOVED             =   200
2268

    
2269
    ASSOCIATED_STATES   =   set([REQUESTED,
2270
                                 ACCEPTED,
2271
                                 LEAVE_REQUESTED,
2272
                                 USER_SUSPENDED,
2273
                                 ])
2274

    
2275
    ACCEPTED_STATES     =   set([ACCEPTED,
2276
                                 LEAVE_REQUESTED,
2277
                                 USER_SUSPENDED,
2278
                                 ])
2279

    
2280
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2281

    
2282
    state               =   models.IntegerField(default=REQUESTED,
2283
                                                db_index=True)
2284
    is_pending          =   models.BooleanField(default=False, db_index=True)
2285
    is_active           =   models.BooleanField(default=False, db_index=True)
2286
    application         =   models.ForeignKey(
2287
                                ProjectApplication,
2288
                                null=True,
2289
                                related_name='memberships')
2290
    pending_application =   models.ForeignKey(
2291
                                ProjectApplication,
2292
                                null=True,
2293
                                related_name='pending_memberships')
2294
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2295

    
2296
    acceptance_date     =   models.DateField(null=True, db_index=True)
2297
    leave_request_date  =   models.DateField(null=True)
2298

    
2299
    objects     =   ProjectMembershipManager()
2300

    
2301
    # Compiled queries
2302
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2303
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2304

    
2305
    MEMBERSHIP_STATE_DISPLAY = {
2306
        REQUESTED           : _('Requested'),
2307
        ACCEPTED            : _('Accepted'),
2308
        LEAVE_REQUESTED     : _('Leave Requested'),
2309
        USER_SUSPENDED      : _('Suspended'),
2310
        REMOVED             : _('Pending removal'),
2311
        }
2312

    
2313
    USER_FRIENDLY_STATE_DISPLAY = {
2314
        REQUESTED           : _('Join requested'),
2315
        ACCEPTED            : _('Accepted member'),
2316
        LEAVE_REQUESTED     : _('Requested to leave'),
2317
        USER_SUSPENDED      : _('Suspended member'),
2318
        REMOVED             : _('Pending removal'),
2319
        }
2320

    
2321
    def state_display(self):
2322
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2323

    
2324
    def user_friendly_state_display(self):
2325
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2326

    
2327
    def get_combined_state(self):
2328
        return self.state, self.is_active, self.is_pending
2329

    
2330
    class Meta:
2331
        unique_together = ("person", "project")
2332
        #index_together = [["project", "state"]]
2333

    
2334
    def __str__(self):
2335
        return uenc(_("<'%s' membership in '%s'>") % (
2336
                self.person.username, self.project))
2337

    
2338
    __repr__ = __str__
2339

    
2340
    def __init__(self, *args, **kwargs):
2341
        self.state = self.REQUESTED
2342
        super(ProjectMembership, self).__init__(*args, **kwargs)
2343

    
2344
    def _set_history_item(self, reason, date=None):
2345
        if isinstance(reason, basestring):
2346
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2347

    
2348
        history_item = ProjectMembershipHistory(
2349
                            serial=self.id,
2350
                            person=self.person_id,
2351
                            project=self.project_id,
2352
                            date=date or datetime.now(),
2353
                            reason=reason)
2354
        history_item.save()
2355
        serial = history_item.id
2356

    
2357
    def can_accept(self):
2358
        return self.state == self.REQUESTED
2359

    
2360
    def accept(self):
2361
        if self.is_pending:
2362
            m = _("%s: attempt to accept while is pending") % (self,)
2363
            raise AssertionError(m)
2364

    
2365
        if not self.can_accept():
2366
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2367
            raise AssertionError(m)
2368

    
2369
        now = datetime.now()
2370
        self.acceptance_date = now
2371
        self._set_history_item(reason='ACCEPT', date=now)
2372
        self.state = self.ACCEPTED
2373
        self.is_pending = True
2374
        self.save()
2375

    
2376
    def can_leave(self):
2377
        return self.state in self.ACCEPTED_STATES
2378

    
2379
    def leave_request(self):
2380
        if self.is_pending:
2381
            m = _("%s: attempt to request to leave while is pending") % (self,)
2382
            raise AssertionError(m)
2383

    
2384
        if not self.can_leave():
2385
            m = _("%s: attempt to request to leave in state '%s'") % (
2386
                self, self.state)
2387
            raise AssertionError(m)
2388

    
2389
        self.leave_request_date = datetime.now()
2390
        self.state = self.LEAVE_REQUESTED
2391
        self.save()
2392

    
2393
    def can_deny_leave(self):
2394
        return self.state == self.LEAVE_REQUESTED
2395

    
2396
    def leave_request_deny(self):
2397
        if self.is_pending:
2398
            m = _("%s: attempt to deny leave request while is pending") % (
2399
                self,)
2400
            raise AssertionError(m)
2401

    
2402
        if not self.can_deny_leave():
2403
            m = _("%s: attempt to deny leave request in state '%s'") % (
2404
                self, self.state)
2405
            raise AssertionError(m)
2406

    
2407
        self.leave_request_date = None
2408
        self.state = self.ACCEPTED
2409
        self.save()
2410

    
2411
    def can_cancel_leave(self):
2412
        return self.state == self.LEAVE_REQUESTED
2413

    
2414
    def leave_request_cancel(self):
2415
        if self.is_pending:
2416
            m = _("%s: attempt to cancel leave request while is pending") % (
2417
                self,)
2418
            raise AssertionError(m)
2419

    
2420
        if not self.can_cancel_leave():
2421
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2422
                self, self.state)
2423
            raise AssertionError(m)
2424

    
2425
        self.leave_request_date = None
2426
        self.state = self.ACCEPTED
2427
        self.save()
2428

    
2429
    def can_remove(self):
2430
        return self.state in self.ACCEPTED_STATES
2431

    
2432
    def remove(self):
2433
        if self.is_pending:
2434
            m = _("%s: attempt to remove while is pending") % (self,)
2435
            raise AssertionError(m)
2436

    
2437
        if not self.can_remove():
2438
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2439
            raise AssertionError(m)
2440

    
2441
        self._set_history_item(reason='REMOVE')
2442
        self.state = self.REMOVED
2443
        self.is_pending = True
2444
        self.save()
2445

    
2446
    def can_reject(self):
2447
        return self.state == self.REQUESTED
2448

    
2449
    def reject(self):
2450
        if self.is_pending:
2451
            m = _("%s: attempt to reject while is pending") % (self,)
2452
            raise AssertionError(m)
2453

    
2454
        if not self.can_reject():
2455
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2456
            raise AssertionError(m)
2457

    
2458
        # rejected requests don't need sync,
2459
        # because they were never effected
2460
        self._set_history_item(reason='REJECT')
2461
        self.delete()
2462

    
2463
    def can_cancel(self):
2464
        return self.state == self.REQUESTED
2465

    
2466
    def cancel(self):
2467
        if self.is_pending:
2468
            m = _("%s: attempt to cancel while is pending") % (self,)
2469
            raise AssertionError(m)
2470

    
2471
        if not self.can_cancel():
2472
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2473
            raise AssertionError(m)
2474

    
2475
        # rejected requests don't need sync,
2476
        # because they were never effected
2477
        self._set_history_item(reason='CANCEL')
2478
        self.delete()
2479

    
2480
    def get_diff_quotas(self, sub_list=None, add_list=None):
2481
        if sub_list is None:
2482
            sub_list = []
2483

    
2484
        if add_list is None:
2485
            add_list = []
2486

    
2487
        sub_append = sub_list.append
2488
        add_append = add_list.append
2489
        holder = self.person.uuid
2490

    
2491
        synced_application = self.application
2492
        if synced_application is not None:
2493
            cur_grants = synced_application.projectresourcegrant_set.all()
2494
            for grant in cur_grants:
2495
                sub_append(QuotaLimits(
2496
                               holder       = holder,
2497
                               resource     = str(grant.resource),
2498
                               capacity     = grant.member_capacity,
2499
                               import_limit = grant.member_import_limit,
2500
                               export_limit = grant.member_export_limit))
2501

    
2502
        pending_application = self.pending_application
2503
        if pending_application is not None:
2504
            new_grants = pending_application.projectresourcegrant_set.all()
2505
            for new_grant in new_grants:
2506
                add_append(QuotaLimits(
2507
                               holder       = holder,
2508
                               resource     = str(new_grant.resource),
2509
                               capacity     = new_grant.member_capacity,
2510
                               import_limit = new_grant.member_import_limit,
2511
                               export_limit = new_grant.member_export_limit))
2512

    
2513
        return (sub_list, add_list)
2514

    
2515
    def is_fully_applied(self, project=None):
2516
        if project is None:
2517
            project = self.project
2518
        if project.is_deactivated():
2519
            return self.application is None
2520
        else:
2521
            return self.application_id == project.application_id
2522

    
2523
    def set_sync(self):
2524
        if not self.is_pending:
2525
            m = _("%s: attempt to sync a non pending membership") % (self,)
2526
            raise AssertionError(m)
2527

    
2528
        state = self.state
2529
        if state in self.ACTUALLY_ACCEPTED:
2530
            pending_application = self.pending_application
2531

    
2532
            self.application = pending_application
2533
            self.is_active = (self.application is not None)
2534

    
2535
            self.pending_application = None
2536
            self.pending_serial = None
2537

    
2538
            # project.application may have changed in the meantime,
2539
            # in which case we stay PENDING;
2540
            # we are safe to check due to select_for_update
2541
            self.is_pending = not self.is_fully_applied()
2542
            self.save()
2543

    
2544
        elif state == self.REMOVED:
2545
            self.delete()
2546

    
2547
        else:
2548
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2549
            raise AssertionError(m)
2550

    
2551
    def reset_sync(self):
2552
        if not self.is_pending:
2553
            m = _("%s: attempt to reset a non pending membership") % (self,)
2554
            raise AssertionError(m)
2555

    
2556
        state = self.state
2557
        if state in [self.ACCEPTED, self.LEAVE_REQUESTED, self.REMOVED]:
2558
            self.pending_application = None
2559
            self.pending_serial = None
2560
            self.save()
2561
        else:
2562
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2563
            raise AssertionError(m)
2564

    
2565
class Serial(models.Model):
2566
    serial  =   models.AutoField(primary_key=True)
2567

    
2568
def new_serial():
2569
    s = Serial.objects.create()
2570
    serial = s.serial
2571
    s.delete()
2572
    return serial
2573

    
2574
class SyncError(Exception):
2575
    pass
2576

    
2577
def reset_serials(serials):
2578
    objs = ProjectMembership.objects
2579
    q = objs.filter(pending_serial__in=serials).select_for_update()
2580
    memberships = list(q)
2581

    
2582
    if memberships:
2583
        for membership in memberships:
2584
            membership.reset_sync()
2585

    
2586
        transaction.commit()
2587

    
2588
def sync_finish_serials(serials_to_ack=None):
2589
    if serials_to_ack is None:
2590
        serials_to_ack = qh_query_serials([])
2591

    
2592
    serials_to_ack = set(serials_to_ack)
2593
    objs = ProjectMembership.objects
2594
    q = objs.filter(pending_serial__isnull=False).select_for_update()
2595
    memberships = list(q)
2596

    
2597
    if memberships:
2598
        for membership in memberships:
2599
            serial = membership.pending_serial
2600
            if serial in serials_to_ack:
2601
                membership.set_sync()
2602
            else:
2603
                membership.reset_sync()
2604

    
2605
        transaction.commit()
2606

    
2607
    qh_ack_serials(list(serials_to_ack))
2608
    return len(memberships)
2609

    
2610
def _pre_sync_projects(projects):
2611
    for project in projects:
2612
        objects = project.projectmembership_set
2613
        memberships = objects.actually_accepted().select_for_update()
2614
        for membership in memberships:
2615
            if not membership.is_fully_applied(project):
2616
                membership.is_pending = True
2617
                membership.save()
2618

    
2619
def pre_sync_projects(sync=True):
2620
    objs = Project.objects
2621

    
2622
    modified = list(objs.modified_projects().select_for_update())
2623
    reactivating = list(objs.reactivating_projects().select_for_update())
2624
    deactivating = list(objs.deactivating_projects().select_for_update())
2625

    
2626
    if sync:
2627
        _pre_sync_projects(modified)
2628
        _pre_sync_projects(reactivating)
2629
        _pre_sync_projects(deactivating)
2630

    
2631
#    transaction.commit()
2632
    return (modified, reactivating, deactivating)
2633

    
2634
def set_sync_projects(exclude=None):
2635

    
2636
    ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED
2637
    objects = ProjectMembership.objects
2638

    
2639
    sub_quota, add_quota = [], []
2640

    
2641
    serial = new_serial()
2642

    
2643
    pending = objects.filter(is_pending=True).select_for_update()
2644
    for membership in pending:
2645

    
2646
        if membership.pending_application:
2647
            m = "%s: impossible: pending_application is not None (%s)" % (
2648
                membership, membership.pending_application)
2649
            raise AssertionError(m)
2650
        if membership.pending_serial:
2651
            m = "%s: impossible: pending_serial is not None (%s)" % (
2652
                membership, membership.pending_serial)
2653
            raise AssertionError(m)
2654

    
2655
        if exclude is not None:
2656
            uuid = membership.person.uuid
2657
            if uuid in exclude:
2658
                logger.warning("Excluded from sync: %s" % uuid)
2659
                continue
2660

    
2661
        project = membership.project
2662
        if (membership.state in ACTUALLY_ACCEPTED and
2663
            not project.is_deactivated()):
2664
            membership.pending_application = project.application
2665

    
2666
        membership.pending_serial = serial
2667
        membership.get_diff_quotas(sub_quota, add_quota)
2668
        membership.save()
2669

    
2670
    transaction.commit()
2671
    return serial, sub_quota, add_quota
2672

    
2673
def do_sync_projects():
2674
    serial, sub_quota, add_quota = set_sync_projects()
2675
    r = qh_add_quota(serial, sub_quota, add_quota)
2676
    if not r:
2677
        return serial
2678

    
2679
    m = "cannot sync serial: %d" % serial
2680
    logger.error(m)
2681
    logger.error("Failed: %s" % r)
2682

    
2683
    reset_serials([serial])
2684
    uuids = set(uuid for (uuid, resource) in r)
2685
    serial, sub_quota, add_quota = set_sync_projects(exclude=uuids)
2686
    r = qh_add_quota(serial, sub_quota, add_quota)
2687
    if not r:
2688
        return serial
2689

    
2690
    m = "cannot sync serial: %d" % serial
2691
    logger.error(m)
2692
    logger.error("Failed: %s" % r)
2693
    raise SyncError(m)
2694

    
2695
def _post_sync_projects(projects, action):
2696
    for project in projects:
2697
        objects = project.projectmembership_set
2698
        memberships = objects.actually_accepted().select_for_update()
2699
        for membership in memberships:
2700
            if not membership.is_fully_applied(project):
2701
                break
2702
        else:
2703
            action(project)
2704

    
2705
def post_sync_projects():
2706
    objs = Project.objects
2707

    
2708
    modified = objs.modified_projects().select_for_update()
2709
    _post_sync_projects(modified, lambda p: p.unset_modified())
2710

    
2711
    reactivating = objs.reactivating_projects().select_for_update()
2712
    _post_sync_projects(reactivating, lambda p: p.reactivate())
2713

    
2714
    deactivating = objs.deactivating_projects().select_for_update()
2715
    _post_sync_projects(deactivating, lambda p: p.deactivate())
2716

    
2717
    transaction.commit()
2718

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

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

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

    
2735

    
2736

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

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

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

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

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

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

    
2767

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

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

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

    
2782
### SIGNALS ###
2783
################
2784

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

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

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

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

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

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

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

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