Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (92 kB)

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

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

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

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

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

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

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

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

    
88
logger = logging.getLogger(__name__)
89

    
90
DEFAULT_CONTENT_TYPE = None
91
_content_type = None
92

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

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

    
105
RESOURCE_SEPARATOR = '.'
106

    
107
inf = float('inf')
108

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

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

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

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

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

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

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

    
148

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
235
        ss.append(service)
236

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

    
250
                rs.append(r)
251

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

    
257
    register_services(ss)
258
    register_resources(rs)
259

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

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

    
275
    return _DEFAULT_QUOTA
276

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

    
283

    
284
class AstakosUserManager(UserManager):
285

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

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

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

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

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

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

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

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

    
338

    
339

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

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

    
356

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

    
363
    auth_token = models.CharField(_('Authentication Token'), 
364
                                  max_length=32,
365
                                  null=True, 
366
                                  blank=True, 
367
                                  help_text = _( 'test' ))
368
    auth_token_created = models.DateTimeField(_('Token creation date'), 
369
                                              null=True)
370
    auth_token_expires = models.DateTimeField(
371
        _('Token expiration date'), null=True)
372

    
373
    updated = models.DateTimeField(_('Update date'))
374
    is_verified = models.BooleanField(_('Is verified?'), default=False)
375

    
376
    email_verified = models.BooleanField(_('Email verified?'), default=False)
377

    
378
    has_credits = models.BooleanField(_('Has credits?'), default=False)
379
    has_signed_terms = models.BooleanField(
380
        _('I agree with the terms'), default=False)
381
    date_signed_terms = models.DateTimeField(
382
        _('Signed terms date'), null=True, blank=True)
383

    
384
    activation_sent = models.DateTimeField(
385
        _('Activation sent data'), null=True, blank=True)
386

    
387
    policy = models.ManyToManyField(
388
        Resource, null=True, through='AstakosUserQuota')
389

    
390
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
391

    
392
    __has_signed_terms = False
393
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
394
                                           default=False, db_index=True)
395

    
396
    objects = AstakosUserManager()
397

    
398
    def __init__(self, *args, **kwargs):
399
        super(AstakosUser, self).__init__(*args, **kwargs)
400
        self.__has_signed_terms = self.has_signed_terms
401
        if not self.id:
402
            self.is_active = False
403

    
404
    @property
405
    def realname(self):
406
        return '%s %s' % (self.first_name, self.last_name)
407

    
408
    @realname.setter
409
    def realname(self, value):
410
        parts = value.split(' ')
411
        if len(parts) == 2:
412
            self.first_name = parts[0]
413
            self.last_name = parts[1]
414
        else:
415
            self.last_name = parts[0]
416

    
417
    def add_permission(self, pname):
418
        if self.has_perm(pname):
419
            return
420
        p, created = Permission.objects.get_or_create(
421
                                    codename=pname,
422
                                    name=pname.capitalize(),
423
                                    content_type=get_content_type())
424
        self.user_permissions.add(p)
425

    
426
    def remove_permission(self, pname):
427
        if self.has_perm(pname):
428
            return
429
        p = Permission.objects.get(codename=pname,
430
                                   content_type=get_content_type())
431
        self.user_permissions.remove(p)
432

    
433
    def is_project_admin(self, application_id=None):
434
        return self.uuid in PROJECT_ADMINS
435

    
436
    @property
437
    def invitation(self):
438
        try:
439
            return Invitation.objects.get(username=self.email)
440
        except Invitation.DoesNotExist:
441
            return None
442

    
443
    def initial_quotas(self):
444
        quotas = dict(get_default_quota())
445
        for user_quota in self.policies:
446
            resource = user_quota.resource.full_name()
447
            quotas[resource] = user_quota.quota_values()
448
        return quotas
449

    
450
    def all_quotas(self, initial=None):
451
        if initial is None:
452
            quotas = self.initial_quotas()
453
        else:
454
            quotas = dict(initial)
455

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

    
465
            grants = application.projectresourcegrant_set.all()
466
            for grant in grants:
467
                resource = grant.resource.full_name()
468
                prev = quotas.get(resource, 0)
469
                new = add_quota_values(prev, grant.member_quota_values())
470
                quotas[resource] = new
471
        return quotas
472

    
473
    @property
474
    def policies(self):
475
        return self.astakosuserquota_set.select_related().all()
476

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

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

    
507
    def remove_resource_policy(self, service, resource):
508
        """Raises ObjectDoesNotExist, IntegrityError"""
509
        resource = Resource.objects.get(service__name=service, name=resource)
510
        q = self.policies.get(resource=resource).delete()
511

    
512
    def update_uuid(self):
513
        while not self.uuid:
514
            uuid_val =  str(uuid.uuid4())
515
            try:
516
                AstakosUser.objects.get(uuid=uuid_val)
517
            except AstakosUser.DoesNotExist, e:
518
                self.uuid = uuid_val
519
        return self.uuid
520

    
521
    def save(self, update_timestamps=True, **kwargs):
522
        if update_timestamps:
523
            if not self.id:
524
                self.date_joined = datetime.now()
525
            self.updated = datetime.now()
526

    
527
        # update date_signed_terms if necessary
528
        if self.__has_signed_terms != self.has_signed_terms:
529
            self.date_signed_terms = datetime.now()
530

    
531
        self.update_uuid()
532

    
533
        if self.username != self.email.lower():
534
            # set username
535
            self.username = self.email.lower()
536

    
537
        super(AstakosUser, self).save(**kwargs)
538

    
539
    def renew_token(self, flush_sessions=False, current_key=None):
540
        md5 = hashlib.md5()
541
        md5.update(settings.SECRET_KEY)
542
        md5.update(self.username)
543
        md5.update(self.realname.encode('ascii', 'ignore'))
544
        md5.update(asctime())
545

    
546
        self.auth_token = b64encode(md5.digest())
547
        self.auth_token_created = datetime.now()
548
        self.auth_token_expires = self.auth_token_created + \
549
                                  timedelta(hours=AUTH_TOKEN_DURATION)
550
        if flush_sessions:
551
            self.flush_sessions(current_key)
552
        msg = 'Token renewed for %s' % self.email
553
        logger.log(LOGGING_LEVEL, msg)
554

    
555
    def flush_sessions(self, current_key=None):
556
        q = self.sessions
557
        if current_key:
558
            q = q.exclude(session_key=current_key)
559

    
560
        keys = q.values_list('session_key', flat=True)
561
        if keys:
562
            msg = 'Flushing sessions: %s' % ','.join(keys)
563
            logger.log(LOGGING_LEVEL, msg, [])
564
        engine = import_module(settings.SESSION_ENGINE)
565
        for k in keys:
566
            s = engine.SessionStore(k)
567
            s.flush()
568

    
569
    def __unicode__(self):
570
        return '%s (%s)' % (self.realname, self.email)
571

    
572
    def conflicting_email(self):
573
        q = AstakosUser.objects.exclude(username=self.username)
574
        q = q.filter(email__iexact=self.email)
575
        if q.count() != 0:
576
            return True
577
        return False
578

    
579
    def email_change_is_pending(self):
580
        return self.emailchanges.count() > 0
581

    
582
    @property
583
    def signed_terms(self):
584
        term = get_latest_terms()
585
        if not term:
586
            return True
587
        if not self.has_signed_terms:
588
            return False
589
        if not self.date_signed_terms:
590
            return False
591
        if self.date_signed_terms < term.date:
592
            self.has_signed_terms = False
593
            self.date_signed_terms = None
594
            self.save()
595
            return False
596
        return True
597

    
598
    def set_invitations_level(self):
599
        """
600
        Update user invitation level
601
        """
602
        level = self.invitation.inviter.level + 1
603
        self.level = level
604
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
605

    
606
    def can_login_with_auth_provider(self, provider):
607
        if not self.has_auth_provider(provider):
608
            return False
609
        else:
610
            return auth_providers.get_provider(provider).is_available_for_login()
611

    
612
    def can_add_auth_provider(self, provider, include_unverified=False, **kwargs):
613
        provider_settings = auth_providers.get_provider(provider)
614

    
615
        if not provider_settings.is_available_for_add():
616
            return False
617

    
618
        if self.has_auth_provider(provider) and \
619
           provider_settings.one_per_user:
620
            return False
621

    
622
        if 'provider_info' in kwargs:
623
            kwargs.pop('provider_info')
624

    
625
        if 'identifier' in kwargs:
626
            try:
627
                # provider with specified params already exist
628
                if not include_unverified:
629
                    kwargs['user__email_verified'] = True
630
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
631
                                                                   **kwargs)
632
            except AstakosUser.DoesNotExist:
633
                return True
634
            else:
635
                return False
636

    
637
        return True
638

    
639
    def can_remove_auth_provider(self, module):
640
        provider = auth_providers.get_provider(module)
641
        existing = self.get_active_auth_providers()
642
        existing_for_provider = self.get_active_auth_providers(module=module)
643

    
644
        if len(existing) <= 1:
645
            return False
646

    
647
        if len(existing_for_provider) == 1 and provider.is_required():
648
            return False
649

    
650
        return provider.is_available_for_remove()
651

    
652
    def can_change_password(self):
653
        return self.has_auth_provider('local', auth_backend='astakos')
654

    
655
    def can_change_email(self):
656
        non_astakos_local = self.get_auth_providers().filter(module='local')
657
        non_astakos_local = non_astakos_local.exclude(auth_backend='astakos')
658
        return non_astakos_local.count() == 0
659

    
660
    def has_required_auth_providers(self):
661
        required = auth_providers.REQUIRED_PROVIDERS
662
        for provider in required:
663
            if not self.has_auth_provider(provider):
664
                return False
665
        return True
666

    
667
    def has_auth_provider(self, provider, **kwargs):
668
        return bool(self.get_auth_providers().filter(module=provider,
669
                                               **kwargs).count())
670

    
671
    def add_auth_provider(self, provider, **kwargs):
672
        info_data = ''
673
        if 'provider_info' in kwargs:
674
            info_data = kwargs.pop('provider_info')
675
            if isinstance(info_data, dict):
676
                info_data = json.dumps(info_data)
677

    
678
        if self.can_add_auth_provider(provider, **kwargs):
679
            if 'identifier' in kwargs:
680
                # clean up third party pending for activation users of the same
681
                # identifier
682
                AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
683
                                                                **kwargs)
684
            self.auth_providers.create(module=provider, active=True,
685
                                       info_data=info_data,
686
                                       **kwargs)
687
        else:
688
            raise Exception('Cannot add provider')
689

    
690
    def add_pending_auth_provider(self, pending):
691
        """
692
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
693
        the current user.
694
        """
695
        if not isinstance(pending, PendingThirdPartyUser):
696
            pending = PendingThirdPartyUser.objects.get(token=pending)
697

    
698
        provider = self.add_auth_provider(pending.provider,
699
                               identifier=pending.third_party_identifier,
700
                                affiliation=pending.affiliation,
701
                                          provider_info=pending.info)
702

    
703
        if email_re.match(pending.email or '') and pending.email != self.email:
704
            self.additionalmail_set.get_or_create(email=pending.email)
705

    
706
        pending.delete()
707
        return provider
708

    
709
    def remove_auth_provider(self, provider, **kwargs):
710
        self.get_auth_providers().get(module=provider, **kwargs).delete()
711

    
712
    # user urls
713
    def get_resend_activation_url(self):
714
        return reverse('send_activation', kwargs={'user_id': self.pk})
715

    
716
    def get_provider_remove_url(self, module, **kwargs):
717
        return reverse('remove_auth_provider', kwargs={
718
            'pk': self.get_auth_providers().get(module=module, **kwargs).pk})
719

    
720
    def get_activation_url(self, nxt=False):
721
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
722
                                 quote(self.auth_token))
723
        if nxt:
724
            url += "&next=%s" % quote(nxt)
725
        return url
726

    
727
    def get_password_reset_url(self, token_generator=default_token_generator):
728
        return reverse('django.contrib.auth.views.password_reset_confirm',
729
                          kwargs={'uidb36':int_to_base36(self.id),
730
                                  'token':token_generator.make_token(self)})
731

    
732
    def get_primary_auth_provider(self):
733
        return self.get_auth_providers().filter()[0]
734

    
735
    def get_auth_providers(self):
736
        return self.auth_providers
737

    
738
    def get_available_auth_providers(self):
739
        """
740
        Returns a list of providers available for user to connect to.
741
        """
742
        providers = []
743
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
744
            if self.can_add_auth_provider(module):
745
                providers.append(provider_settings(self))
746

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

    
755
    def get_active_auth_providers(self, **filters):
756
        providers = []
757
        for provider in self.get_auth_providers().active(**filters):
758
            if auth_providers.get_provider(provider.module).is_available_for_login():
759
                providers.append(provider)
760

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

    
769
    @property
770
    def auth_providers_display(self):
771
        return ",".join(map(lambda x:unicode(x), self.get_auth_providers().active()))
772

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

    
798
        return mark_safe(message + u' '+ msg_extra)
799

    
800
    def owns_application(self, application):
801
        return application.owner == self
802

    
803
    def owns_project(self, project):
804
        return project.application.owner == self
805

    
806
    def is_associated(self, project):
807
        try:
808
            m = ProjectMembership.objects.get(person=self, project=project)
809
            return m.state in ProjectMembership.ASSOCIATED_STATES
810
        except ProjectMembership.DoesNotExist:
811
            return False
812

    
813
    def get_membership(self, project):
814
        try:
815
            return ProjectMembership.objects.get(
816
                project=project,
817
                person=self)
818
        except ProjectMembership.DoesNotExist:
819
            return None
820

    
821
    def membership_display(self, project):
822
        m = self.get_membership(project)
823
        if m is None:
824
            return _('Not a member')
825
        else:
826
            return m.user_friendly_state_display()
827

    
828
    def non_owner_can_view(self, maybe_project):
829
        if self.is_project_admin():
830
            return True
831
        if maybe_project is None:
832
            return False
833
        project = maybe_project
834
        if self.is_associated(project):
835
            return True
836
        if project.is_deactivated():
837
            return False
838
        return True
839

    
840

    
841
class AstakosUserAuthProviderManager(models.Manager):
842

    
843
    def active(self, **filters):
844
        return self.filter(active=True, **filters)
845

    
846
    def remove_unverified_providers(self, provider, **filters):
847
        try:
848
            existing = self.filter(module=provider, user__email_verified=False, **filters)
849
            for p in existing:
850
                p.user.delete()
851
        except:
852
            pass
853

    
854

    
855

    
856
class AstakosUserAuthProvider(models.Model):
857
    """
858
    Available user authentication methods.
859
    """
860
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
861
                                   null=True, default=None)
862
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
863
    module = models.CharField(_('Provider'), max_length=255, blank=False,
864
                                default='local')
865
    identifier = models.CharField(_('Third-party identifier'),
866
                                              max_length=255, null=True,
867
                                              blank=True)
868
    active = models.BooleanField(default=True)
869
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
870
                                   default='astakos')
871
    info_data = models.TextField(default="", null=True, blank=True)
872
    created = models.DateTimeField('Creation date', auto_now_add=True)
873

    
874
    objects = AstakosUserAuthProviderManager()
875

    
876
    class Meta:
877
        unique_together = (('identifier', 'module', 'user'), )
878
        ordering = ('module', 'created')
879

    
880
    def __init__(self, *args, **kwargs):
881
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
882
        try:
883
            self.info = json.loads(self.info_data)
884
            if not self.info:
885
                self.info = {}
886
        except Exception, e:
887
            self.info = {}
888

    
889
        for key,value in self.info.iteritems():
890
            setattr(self, 'info_%s' % key, value)
891

    
892

    
893
    @property
894
    def settings(self):
895
        return auth_providers.get_provider(self.module)
896

    
897
    @property
898
    def details_display(self):
899
        try:
900
            params = self.user.__dict__
901
            params.update(self.__dict__)
902
            return self.settings.get_details_tpl_display % params
903
        except:
904
            return ''
905

    
906
    @property
907
    def title_display(self):
908
        title_tpl = self.settings.get_title_display
909
        try:
910
            if self.settings.get_user_title_display:
911
                title_tpl = self.settings.get_user_title_display
912
        except Exception, e:
913
            pass
914
        try:
915
          return title_tpl % self.__dict__
916
        except:
917
          return self.settings.get_title_display % self.__dict__
918

    
919
    def can_remove(self):
920
        return self.user.can_remove_auth_provider(self.module)
921

    
922
    def delete(self, *args, **kwargs):
923
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
924
        if self.module == 'local':
925
            self.user.set_unusable_password()
926
            self.user.save()
927
        return ret
928

    
929
    def __repr__(self):
930
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
931

    
932
    def __unicode__(self):
933
        if self.identifier:
934
            return "%s:%s" % (self.module, self.identifier)
935
        if self.auth_backend:
936
            return "%s:%s" % (self.module, self.auth_backend)
937
        return self.module
938

    
939
    def save(self, *args, **kwargs):
940
        self.info_data = json.dumps(self.info)
941
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
942

    
943

    
944
class ExtendedManager(models.Manager):
945
    def _update_or_create(self, **kwargs):
946
        assert kwargs, \
947
            'update_or_create() must be passed at least one keyword argument'
948
        obj, created = self.get_or_create(**kwargs)
949
        defaults = kwargs.pop('defaults', {})
950
        if created:
951
            return obj, True, False
952
        else:
953
            try:
954
                params = dict(
955
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
956
                params.update(defaults)
957
                for attr, val in params.items():
958
                    if hasattr(obj, attr):
959
                        setattr(obj, attr, val)
960
                sid = transaction.savepoint()
961
                obj.save(force_update=True)
962
                transaction.savepoint_commit(sid)
963
                return obj, False, True
964
            except IntegrityError, e:
965
                transaction.savepoint_rollback(sid)
966
                try:
967
                    return self.get(**kwargs), False, False
968
                except self.model.DoesNotExist:
969
                    raise e
970

    
971
    update_or_create = _update_or_create
972

    
973

    
974
class AstakosUserQuota(models.Model):
975
    objects = ExtendedManager()
976
    capacity = intDecimalField()
977
    quantity = intDecimalField(default=0)
978
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
979
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
980
    resource = models.ForeignKey(Resource)
981
    user = models.ForeignKey(AstakosUser)
982

    
983
    class Meta:
984
        unique_together = ("resource", "user")
985

    
986
    def quota_values(self):
987
        return QuotaValues(
988
            quantity = self.quantity,
989
            capacity = self.capacity,
990
            import_limit = self.import_limit,
991
            export_limit = self.export_limit)
992

    
993

    
994
class ApprovalTerms(models.Model):
995
    """
996
    Model for approval terms
997
    """
998

    
999
    date = models.DateTimeField(
1000
        _('Issue date'), db_index=True, auto_now_add=True)
1001
    location = models.CharField(_('Terms location'), max_length=255)
1002

    
1003

    
1004
class Invitation(models.Model):
1005
    """
1006
    Model for registring invitations
1007
    """
1008
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1009
                                null=True)
1010
    realname = models.CharField(_('Real name'), max_length=255)
1011
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1012
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1013
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1014
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1015
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1016

    
1017
    def __init__(self, *args, **kwargs):
1018
        super(Invitation, self).__init__(*args, **kwargs)
1019
        if not self.id:
1020
            self.code = _generate_invitation_code()
1021

    
1022
    def consume(self):
1023
        self.is_consumed = True
1024
        self.consumed = datetime.now()
1025
        self.save()
1026

    
1027
    def __unicode__(self):
1028
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1029

    
1030

    
1031
class EmailChangeManager(models.Manager):
1032

    
1033
    @transaction.commit_on_success
1034
    def change_email(self, activation_key):
1035
        """
1036
        Validate an activation key and change the corresponding
1037
        ``User`` if valid.
1038

1039
        If the key is valid and has not expired, return the ``User``
1040
        after activating.
1041

1042
        If the key is not valid or has expired, return ``None``.
1043

1044
        If the key is valid but the ``User`` is already active,
1045
        return ``None``.
1046

1047
        After successful email change the activation record is deleted.
1048

1049
        Throws ValueError if there is already
1050
        """
1051
        try:
1052
            email_change = self.model.objects.get(
1053
                activation_key=activation_key)
1054
            if email_change.activation_key_expired():
1055
                email_change.delete()
1056
                raise EmailChange.DoesNotExist
1057
            # is there an active user with this address?
1058
            try:
1059
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1060
            except AstakosUser.DoesNotExist:
1061
                pass
1062
            else:
1063
                raise ValueError(_('The new email address is reserved.'))
1064
            # update user
1065
            user = AstakosUser.objects.get(pk=email_change.user_id)
1066
            old_email = user.email
1067
            user.email = email_change.new_email_address
1068
            user.save()
1069
            email_change.delete()
1070
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1071
                                                          user.email)
1072
            logger.log(LOGGING_LEVEL, msg)
1073
            return user
1074
        except EmailChange.DoesNotExist:
1075
            raise ValueError(_('Invalid activation key.'))
1076

    
1077

    
1078
class EmailChange(models.Model):
1079
    new_email_address = models.EmailField(
1080
        _(u'new e-mail address'),
1081
        help_text=_('Provide a new email address. Until you verify the new '
1082
                    'address by following the activation link that will be '
1083
                    'sent to it, your old email address will remain active.'))
1084
    user = models.ForeignKey(
1085
        AstakosUser, unique=True, related_name='emailchanges')
1086
    requested_at = models.DateTimeField(auto_now_add=True)
1087
    activation_key = models.CharField(
1088
        max_length=40, unique=True, db_index=True)
1089

    
1090
    objects = EmailChangeManager()
1091

    
1092
    def get_url(self):
1093
        return reverse('email_change_confirm',
1094
                      kwargs={'activation_key': self.activation_key})
1095

    
1096
    def activation_key_expired(self):
1097
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1098
        return self.requested_at + expiration_date < datetime.now()
1099

    
1100

    
1101
class AdditionalMail(models.Model):
1102
    """
1103
    Model for registring invitations
1104
    """
1105
    owner = models.ForeignKey(AstakosUser)
1106
    email = models.EmailField()
1107

    
1108

    
1109
def _generate_invitation_code():
1110
    while True:
1111
        code = randint(1, 2L ** 63 - 1)
1112
        try:
1113
            Invitation.objects.get(code=code)
1114
            # An invitation with this code already exists, try again
1115
        except Invitation.DoesNotExist:
1116
            return code
1117

    
1118

    
1119
def get_latest_terms():
1120
    try:
1121
        term = ApprovalTerms.objects.order_by('-id')[0]
1122
        return term
1123
    except IndexError:
1124
        pass
1125
    return None
1126

    
1127
class PendingThirdPartyUser(models.Model):
1128
    """
1129
    Model for registring successful third party user authentications
1130
    """
1131
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1132
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1133
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1134
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1135
                                  null=True)
1136
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1137
                                 null=True)
1138
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1139
                                   null=True)
1140
    username = models.CharField(_('username'), max_length=30, unique=True,  
1141
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1142
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1143
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1144
    info = models.TextField(default="", null=True, blank=True)
1145

    
1146
    class Meta:
1147
        unique_together = ("provider", "third_party_identifier")
1148

    
1149
    def get_user_instance(self):
1150
        d = self.__dict__
1151
        d.pop('_state', None)
1152
        d.pop('id', None)
1153
        d.pop('token', None)
1154
        d.pop('created', None)
1155
        d.pop('info', None)
1156
        user = AstakosUser(**d)
1157

    
1158
        return user
1159

    
1160
    @property
1161
    def realname(self):
1162
        return '%s %s' %(self.first_name, self.last_name)
1163

    
1164
    @realname.setter
1165
    def realname(self, value):
1166
        parts = value.split(' ')
1167
        if len(parts) == 2:
1168
            self.first_name = parts[0]
1169
            self.last_name = parts[1]
1170
        else:
1171
            self.last_name = parts[0]
1172

    
1173
    def save(self, **kwargs):
1174
        if not self.id:
1175
            # set username
1176
            while not self.username:
1177
                username =  uuid.uuid4().hex[:30]
1178
                try:
1179
                    AstakosUser.objects.get(username = username)
1180
                except AstakosUser.DoesNotExist, e:
1181
                    self.username = username
1182
        super(PendingThirdPartyUser, self).save(**kwargs)
1183

    
1184
    def generate_token(self):
1185
        self.password = self.third_party_identifier
1186
        self.last_login = datetime.now()
1187
        self.token = default_token_generator.make_token(self)
1188

    
1189
    def existing_user(self):
1190
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1191
                                         auth_providers__identifier=self.third_party_identifier)
1192

    
1193
class SessionCatalog(models.Model):
1194
    session_key = models.CharField(_('session key'), max_length=40)
1195
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1196

    
1197

    
1198
### PROJECTS ###
1199
################
1200

    
1201
def synced_model_metaclass(class_name, class_parents, class_attributes):
1202

    
1203
    new_attributes = {}
1204
    sync_attributes = {}
1205

    
1206
    for name, value in class_attributes.iteritems():
1207
        sync, underscore, rest = name.partition('_')
1208
        if sync == 'sync' and underscore == '_':
1209
            sync_attributes[rest] = value
1210
        else:
1211
            new_attributes[name] = value
1212

    
1213
    if 'prefix' not in sync_attributes:
1214
        m = ("you did not specify a 'sync_prefix' attribute "
1215
             "in class '%s'" % (class_name,))
1216
        raise ValueError(m)
1217

    
1218
    prefix = sync_attributes.pop('prefix')
1219
    class_name = sync_attributes.pop('classname', prefix + '_model')
1220

    
1221
    for name, value in sync_attributes.iteritems():
1222
        newname = prefix + '_' + name
1223
        if newname in new_attributes:
1224
            m = ("class '%s' was specified with prefix '%s' "
1225
                 "but it already has an attribute named '%s'"
1226
                 % (class_name, prefix, newname))
1227
            raise ValueError(m)
1228

    
1229
        new_attributes[newname] = value
1230

    
1231
    newclass = type(class_name, class_parents, new_attributes)
1232
    return newclass
1233

    
1234

    
1235
def make_synced(prefix='sync', name='SyncedState'):
1236

    
1237
    the_name = name
1238
    the_prefix = prefix
1239

    
1240
    class SyncedState(models.Model):
1241

    
1242
        sync_classname      = the_name
1243
        sync_prefix         = the_prefix
1244
        __metaclass__       = synced_model_metaclass
1245

    
1246
        sync_new_state      = models.BigIntegerField(null=True)
1247
        sync_synced_state   = models.BigIntegerField(null=True)
1248
        STATUS_SYNCED       = 0
1249
        STATUS_PENDING      = 1
1250
        sync_status         = models.IntegerField(db_index=True)
1251

    
1252
        class Meta:
1253
            abstract = True
1254

    
1255
        class NotSynced(Exception):
1256
            pass
1257

    
1258
        def sync_init_state(self, state):
1259
            self.sync_synced_state = state
1260
            self.sync_new_state = state
1261
            self.sync_status = self.STATUS_SYNCED
1262

    
1263
        def sync_get_status(self):
1264
            return self.sync_status
1265

    
1266
        def sync_set_status(self):
1267
            if self.sync_new_state != self.sync_synced_state:
1268
                self.sync_status = self.STATUS_PENDING
1269
            else:
1270
                self.sync_status = self.STATUS_SYNCED
1271

    
1272
        def sync_set_synced(self):
1273
            self.sync_synced_state = self.sync_new_state
1274
            self.sync_status = self.STATUS_SYNCED
1275

    
1276
        def sync_get_synced_state(self):
1277
            return self.sync_synced_state
1278

    
1279
        def sync_set_new_state(self, new_state):
1280
            self.sync_new_state = new_state
1281
            self.sync_set_status()
1282

    
1283
        def sync_get_new_state(self):
1284
            return self.sync_new_state
1285

    
1286
        def sync_set_synced_state(self, synced_state):
1287
            self.sync_synced_state = synced_state
1288
            self.sync_set_status()
1289

    
1290
        def sync_get_pending_objects(self):
1291
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1292
            return self.objects.filter(**kw)
1293

    
1294
        def sync_get_synced_objects(self):
1295
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1296
            return self.objects.filter(**kw)
1297

    
1298
        def sync_verify_get_synced_state(self):
1299
            status = self.sync_get_status()
1300
            state = self.sync_get_synced_state()
1301
            verified = (status == self.STATUS_SYNCED)
1302
            return state, verified
1303

    
1304
        def sync_is_synced(self):
1305
            state, verified = self.sync_verify_get_synced_state()
1306
            return verified
1307

    
1308
    return SyncedState
1309

    
1310
SyncedState = make_synced(prefix='sync', name='SyncedState')
1311

    
1312

    
1313
class ChainManager(ForUpdateManager):
1314

    
1315
    def search_by_name(self, *search_strings):
1316
        projects = Project.objects.search_by_name(*search_strings)
1317
        chains = [p.id for p in projects]
1318
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1319
        apps = (app for app in apps if app.is_latest())
1320
        app_chains = [app.chain for app in apps if app.chain not in chains]
1321
        return chains + app_chains
1322

    
1323
    def all_full_state(self):
1324
        d = {}
1325
        chains = self.all()
1326
        for chain in chains:
1327
            d[chain.pk] = chain.full_state()
1328
        return d
1329

    
1330
    def of_project(self, project):
1331
        if project is None:
1332
            return None
1333
        try:
1334
            return self.get(chain=project.id)
1335
        except Chain.DoesNotExist:
1336
            raise AssertionError('project with no chain')
1337

    
1338

    
1339
class Chain(models.Model):
1340
    chain  =   models.AutoField(primary_key=True)
1341

    
1342
    def __str__(self):
1343
        return "%s" % (self.chain,)
1344

    
1345
    objects = ChainManager()
1346

    
1347
    PENDING            = 0
1348
    DENIED             = 3
1349
    DISMISSED          = 4
1350
    CANCELLED          = 5
1351

    
1352
    APPROVED           = 10
1353
    APPROVED_PENDING   = 11
1354
    SUSPENDED          = 12
1355
    SUSPENDED_PENDING  = 13
1356
    TERMINATED         = 14
1357
    TERMINATED_PENDING = 15
1358

    
1359
    PENDING_STATES = [PENDING,
1360
                      APPROVED_PENDING,
1361
                      SUSPENDED_PENDING,
1362
                      TERMINATED_PENDING,
1363
                      ]
1364

    
1365
    MODIFICATION_STATES = [APPROVED_PENDING,
1366
                           SUSPENDED_PENDING,
1367
                           TERMINATED_PENDING,
1368
                           ]
1369

    
1370
    RELEVANT_STATES = [PENDING,
1371
                       DENIED,
1372
                       APPROVED,
1373
                       APPROVED_PENDING,
1374
                       SUSPENDED,
1375
                       SUSPENDED_PENDING,
1376
                       TERMINATED_PENDING,
1377
                       ]
1378

    
1379
    SKIP_STATES = [DISMISSED,
1380
                   CANCELLED,
1381
                   TERMINATED]
1382

    
1383
    STATE_DISPLAY = {
1384
        PENDING            : _("Pending"),
1385
        DENIED             : _("Denied"),
1386
        DISMISSED          : _("Dismissed"),
1387
        CANCELLED          : _("Cancelled"),
1388
        APPROVED           : _("Active"),
1389
        APPROVED_PENDING   : _("Active - Pending"),
1390
        SUSPENDED          : _("Suspended"),
1391
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1392
        TERMINATED         : _("Terminated"),
1393
        TERMINATED_PENDING : _("Terminated - Pending"),
1394
        }
1395

    
1396

    
1397
    @classmethod
1398
    def _chain_state(cls, project_state, app_state):
1399
        s = CHAIN_STATE.get((project_state, app_state), None)
1400
        if s is None:
1401
            raise AssertionError('inconsistent chain state')
1402
        return s
1403

    
1404
    @classmethod
1405
    def chain_state(cls, project, app):
1406
        p_state = project.state if project else None
1407
        return cls._chain_state(p_state, app.state)
1408

    
1409
    @classmethod
1410
    def state_display(cls, s):
1411
        if s is None:
1412
            return _("Unknown")
1413
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1414

    
1415
    def last_application(self):
1416
        return self.chained_apps.order_by('-id')[0]
1417

    
1418
    def get_project(self):
1419
        try:
1420
            return self.chained_project
1421
        except Project.DoesNotExist:
1422
            return None
1423

    
1424
    def get_elements(self):
1425
        project = self.get_project()
1426
        app = self.last_application()
1427
        return project, app
1428

    
1429
    def full_state(self):
1430
        project, app = self.get_elements()
1431
        s = self.chain_state(project, app)
1432
        return s, project, app
1433

    
1434
def new_chain():
1435
    c = Chain.objects.create()
1436
    return c
1437

    
1438

    
1439
class ProjectApplicationManager(ForUpdateManager):
1440

    
1441
    def user_visible_projects(self, *filters, **kw_filters):
1442
        model = self.model
1443
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1444

    
1445
    def user_visible_by_chain(self, flt):
1446
        model = self.model
1447
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1448
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1449
        by_chain = dict(pending.annotate(models.Max('id')))
1450
        by_chain.update(approved.annotate(models.Max('id')))
1451
        return self.filter(flt, id__in=by_chain.values())
1452

    
1453
    def user_accessible_projects(self, user):
1454
        """
1455
        Return projects accessed by specified user.
1456
        """
1457
        if user.is_project_admin():
1458
            participates_filters = Q()
1459
        else:
1460
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1461
                                   Q(project__projectmembership__person=user)
1462

    
1463
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1464

    
1465
    def search_by_name(self, *search_strings):
1466
        q = Q()
1467
        for s in search_strings:
1468
            q = q | Q(name__icontains=s)
1469
        return self.filter(q)
1470

    
1471
    def latest_of_chain(self, chain_id):
1472
        try:
1473
            return self.filter(chain=chain_id).order_by('-id')[0]
1474
        except IndexError:
1475
            return None
1476

    
1477

    
1478
class ProjectApplication(models.Model):
1479
    applicant               =   models.ForeignKey(
1480
                                    AstakosUser,
1481
                                    related_name='projects_applied',
1482
                                    db_index=True)
1483

    
1484
    PENDING     =    0
1485
    APPROVED    =    1
1486
    REPLACED    =    2
1487
    DENIED      =    3
1488
    DISMISSED   =    4
1489
    CANCELLED   =    5
1490

    
1491
    state                   =   models.IntegerField(default=PENDING,
1492
                                                    db_index=True)
1493

    
1494
    owner                   =   models.ForeignKey(
1495
                                    AstakosUser,
1496
                                    related_name='projects_owned',
1497
                                    db_index=True)
1498

    
1499
    chain                   =   models.ForeignKey(Chain,
1500
                                                  related_name='chained_apps',
1501
                                                  db_column='chain')
1502
    precursor_application   =   models.ForeignKey('ProjectApplication',
1503
                                                  null=True,
1504
                                                  blank=True)
1505

    
1506
    name                    =   models.CharField(max_length=80)
1507
    homepage                =   models.URLField(max_length=255, null=True,
1508
                                                verify_exists=False)
1509
    description             =   models.TextField(null=True, blank=True)
1510
    start_date              =   models.DateTimeField(null=True, blank=True)
1511
    end_date                =   models.DateTimeField()
1512
    member_join_policy      =   models.IntegerField()
1513
    member_leave_policy     =   models.IntegerField()
1514
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1515
    resource_grants         =   models.ManyToManyField(
1516
                                    Resource,
1517
                                    null=True,
1518
                                    blank=True,
1519
                                    through='ProjectResourceGrant')
1520
    comments                =   models.TextField(null=True, blank=True)
1521
    issue_date              =   models.DateTimeField(auto_now_add=True)
1522
    response_date           =   models.DateTimeField(null=True, blank=True)
1523

    
1524
    objects                 =   ProjectApplicationManager()
1525

    
1526
    # Compiled queries
1527
    Q_PENDING  = Q(state=PENDING)
1528
    Q_APPROVED = Q(state=APPROVED)
1529
    Q_DENIED   = Q(state=DENIED)
1530

    
1531
    class Meta:
1532
        unique_together = ("chain", "id")
1533

    
1534
    def __unicode__(self):
1535
        return "%s applied by %s" % (self.name, self.applicant)
1536

    
1537
    # TODO: Move to a more suitable place
1538
    APPLICATION_STATE_DISPLAY = {
1539
        PENDING  : _('Pending review'),
1540
        APPROVED : _('Approved'),
1541
        REPLACED : _('Replaced'),
1542
        DENIED   : _('Denied'),
1543
        DISMISSED: _('Dismissed'),
1544
        CANCELLED: _('Cancelled')
1545
    }
1546

    
1547
    def get_project(self):
1548
        try:
1549
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1550
            return Project
1551
        except Project.DoesNotExist, e:
1552
            return None
1553

    
1554
    def state_display(self):
1555
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1556

    
1557
    def project_state_display(self):
1558
        try:
1559
            project = self.project
1560
            return project.state_display()
1561
        except Project.DoesNotExist:
1562
            return self.state_display()
1563

    
1564
    def add_resource_policy(self, service, resource, uplimit):
1565
        """Raises ObjectDoesNotExist, IntegrityError"""
1566
        q = self.projectresourcegrant_set
1567
        resource = Resource.objects.get(service__name=service, name=resource)
1568
        q.create(resource=resource, member_capacity=uplimit)
1569

    
1570
    def members_count(self):
1571
        return self.project.approved_memberships.count()
1572

    
1573
    @property
1574
    def grants(self):
1575
        return self.projectresourcegrant_set.values(
1576
            'member_capacity', 'resource__name', 'resource__service__name')
1577

    
1578
    @property
1579
    def resource_policies(self):
1580
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1581

    
1582
    @resource_policies.setter
1583
    def resource_policies(self, policies):
1584
        for p in policies:
1585
            service = p.get('service', None)
1586
            resource = p.get('resource', None)
1587
            uplimit = p.get('uplimit', 0)
1588
            self.add_resource_policy(service, resource, uplimit)
1589

    
1590
    def pending_modifications_incl_me(self):
1591
        q = self.chained_applications()
1592
        q = q.filter(Q(state=self.PENDING))
1593
        return q
1594

    
1595
    def last_pending_incl_me(self):
1596
        try:
1597
            return self.pending_modifications_incl_me().order_by('-id')[0]
1598
        except IndexError:
1599
            return None
1600

    
1601
    def pending_modifications(self):
1602
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1603

    
1604
    def last_pending(self):
1605
        try:
1606
            return self.pending_modifications().order_by('-id')[0]
1607
        except IndexError:
1608
            return None
1609

    
1610
    def is_modification(self):
1611
        # if self.state != self.PENDING:
1612
        #     return False
1613
        parents = self.chained_applications().filter(id__lt=self.id)
1614
        parents = parents.filter(state__in=[self.APPROVED])
1615
        return parents.count() > 0
1616

    
1617
    def chained_applications(self):
1618
        return ProjectApplication.objects.filter(chain=self.chain)
1619

    
1620
    def is_latest(self):
1621
        return self.chained_applications().order_by('-id')[0] == self
1622

    
1623
    def has_pending_modifications(self):
1624
        return bool(self.last_pending())
1625

    
1626
    def denied_modifications(self):
1627
        q = self.chained_applications()
1628
        q = q.filter(Q(state=self.DENIED))
1629
        q = q.filter(~Q(id=self.id))
1630
        return q
1631

    
1632
    def last_denied(self):
1633
        try:
1634
            return self.denied_modifications().order_by('-id')[0]
1635
        except IndexError:
1636
            return None
1637

    
1638
    def has_denied_modifications(self):
1639
        return bool(self.last_denied())
1640

    
1641
    def is_applied(self):
1642
        try:
1643
            self.project
1644
            return True
1645
        except Project.DoesNotExist:
1646
            return False
1647

    
1648
    def get_project(self):
1649
        try:
1650
            return Project.objects.get(id=self.chain)
1651
        except Project.DoesNotExist:
1652
            return None
1653

    
1654
    def project_exists(self):
1655
        return self.get_project() is not None
1656

    
1657
    def _get_project_for_update(self):
1658
        try:
1659
            objects = Project.objects
1660
            project = objects.get_for_update(id=self.chain)
1661
            return project
1662
        except Project.DoesNotExist:
1663
            return None
1664

    
1665
    def can_cancel(self):
1666
        return self.state == self.PENDING
1667

    
1668
    def cancel(self):
1669
        if not self.can_cancel():
1670
            m = _("cannot cancel: application '%s' in state '%s'") % (
1671
                    self.id, self.state)
1672
            raise AssertionError(m)
1673

    
1674
        self.state = self.CANCELLED
1675
        self.save()
1676

    
1677
    def can_dismiss(self):
1678
        return self.state == self.DENIED
1679

    
1680
    def dismiss(self):
1681
        if not self.can_dismiss():
1682
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1683
                    self.id, self.state)
1684
            raise AssertionError(m)
1685

    
1686
        self.state = self.DISMISSED
1687
        self.save()
1688

    
1689
    def can_deny(self):
1690
        return self.state == self.PENDING
1691

    
1692
    def deny(self):
1693
        if not self.can_deny():
1694
            m = _("cannot deny: application '%s' in state '%s'") % (
1695
                    self.id, self.state)
1696
            raise AssertionError(m)
1697

    
1698
        self.state = self.DENIED
1699
        self.response_date = datetime.now()
1700
        self.save()
1701

    
1702
    def can_approve(self):
1703
        return self.state == self.PENDING
1704

    
1705
    def approve(self, approval_user=None):
1706
        """
1707
        If approval_user then during owner membership acceptance
1708
        it is checked whether the request_user is eligible.
1709

1710
        Raises:
1711
            PermissionDenied
1712
        """
1713

    
1714
        if not transaction.is_managed():
1715
            raise AssertionError("NOPE")
1716

    
1717
        new_project_name = self.name
1718
        if not self.can_approve():
1719
            m = _("cannot approve: project '%s' in state '%s'") % (
1720
                    new_project_name, self.state)
1721
            raise AssertionError(m) # invalid argument
1722

    
1723
        now = datetime.now()
1724
        project = self._get_project_for_update()
1725

    
1726
        try:
1727
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1728
            conflicting_project = Project.objects.get(q)
1729
            if (conflicting_project != project):
1730
                m = (_("cannot approve: project with name '%s' "
1731
                       "already exists (id: %s)") % (
1732
                        new_project_name, conflicting_project.id))
1733
                raise PermissionDenied(m) # invalid argument
1734
        except Project.DoesNotExist:
1735
            pass
1736

    
1737
        new_project = False
1738
        if project is None:
1739
            new_project = True
1740
            project = Project(id=self.chain)
1741

    
1742
        project.name = new_project_name
1743
        project.application = self
1744
        project.last_approval_date = now
1745
        if not new_project:
1746
            project.is_modified = True
1747

    
1748
        project.save()
1749

    
1750
        self.state = self.APPROVED
1751
        self.response_date = now
1752
        self.save()
1753

    
1754
    @property
1755
    def member_join_policy_display(self):
1756
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1757

    
1758
    @property
1759
    def member_leave_policy_display(self):
1760
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1761

    
1762
class ProjectResourceGrant(models.Model):
1763

    
1764
    resource                =   models.ForeignKey(Resource)
1765
    project_application     =   models.ForeignKey(ProjectApplication,
1766
                                                  null=True)
1767
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1768
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1769
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1770
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1771
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1772
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1773

    
1774
    objects = ExtendedManager()
1775

    
1776
    class Meta:
1777
        unique_together = ("resource", "project_application")
1778

    
1779
    def member_quota_values(self):
1780
        return QuotaValues(
1781
            quantity = 0,
1782
            capacity = self.member_capacity,
1783
            import_limit = self.member_import_limit,
1784
            export_limit = self.member_export_limit)
1785

    
1786
    def display_member_capacity(self):
1787
        if self.member_capacity:
1788
            if self.resource.unit:
1789
                return ProjectResourceGrant.display_filesize(
1790
                    self.member_capacity)
1791
            else:
1792
                if math.isinf(self.member_capacity):
1793
                    return 'Unlimited'
1794
                else:
1795
                    return self.member_capacity
1796
        else:
1797
            return 'Unlimited'
1798

    
1799
    def __str__(self):
1800
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1801
                                        self.display_member_capacity())
1802

    
1803
    @classmethod
1804
    def display_filesize(cls, value):
1805
        try:
1806
            value = float(value)
1807
        except:
1808
            return
1809
        else:
1810
            if math.isinf(value):
1811
                return 'Unlimited'
1812
            if value > 1:
1813
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1814
                                [0, 0, 0, 0, 0, 0])
1815
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1816
                quotient = float(value) / 1024**exponent
1817
                unit, value_decimals = unit_list[exponent]
1818
                format_string = '{0:.%sf} {1}' % (value_decimals)
1819
                return format_string.format(quotient, unit)
1820
            if value == 0:
1821
                return '0 bytes'
1822
            if value == 1:
1823
                return '1 byte'
1824
            else:
1825
               return '0'
1826

    
1827

    
1828
class ProjectManager(ForUpdateManager):
1829

    
1830
    def terminated_projects(self):
1831
        q = self.model.Q_TERMINATED
1832
        return self.filter(q)
1833

    
1834
    def not_terminated_projects(self):
1835
        q = ~self.model.Q_TERMINATED
1836
        return self.filter(q)
1837

    
1838
    def terminating_projects(self):
1839
        q = self.model.Q_TERMINATED & Q(is_active=True)
1840
        return self.filter(q)
1841

    
1842
    def deactivated_projects(self):
1843
        q = self.model.Q_DEACTIVATED
1844
        return self.filter(q)
1845

    
1846
    def deactivating_projects(self):
1847
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1848
        return self.filter(q)
1849

    
1850
    def modified_projects(self):
1851
        return self.filter(is_modified=True)
1852

    
1853
    def reactivating_projects(self):
1854
        return self.filter(state=Project.APPROVED, is_active=False)
1855

    
1856
    def expired_projects(self):
1857
        q = (~Q(state=Project.TERMINATED) &
1858
              Q(application__end_date__lt=datetime.now()))
1859
        return self.filter(q)
1860

    
1861
    def search_by_name(self, *search_strings):
1862
        q = Q()
1863
        for s in search_strings:
1864
            q = q | Q(name__icontains=s)
1865
        return self.filter(q)
1866

    
1867

    
1868
class Project(models.Model):
1869

    
1870
    id                          =   models.OneToOneField(Chain,
1871
                                                      related_name='chained_project',
1872
                                                      db_column='id',
1873
                                                      primary_key=True)
1874

    
1875
    application                 =   models.OneToOneField(
1876
                                            ProjectApplication,
1877
                                            related_name='project')
1878
    last_approval_date          =   models.DateTimeField(null=True)
1879

    
1880
    members                     =   models.ManyToManyField(
1881
                                            AstakosUser,
1882
                                            through='ProjectMembership')
1883

    
1884
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1885
    deactivation_date           =   models.DateTimeField(null=True)
1886

    
1887
    creation_date               =   models.DateTimeField(auto_now_add=True)
1888
    name                        =   models.CharField(
1889
                                            max_length=80,
1890
                                            null=True,
1891
                                            db_index=True,
1892
                                            unique=True)
1893

    
1894
    APPROVED    = 1
1895
    SUSPENDED   = 10
1896
    TERMINATED  = 100
1897

    
1898
    is_modified                 =   models.BooleanField(default=False,
1899
                                                        db_index=True)
1900
    is_active                   =   models.BooleanField(default=True,
1901
                                                        db_index=True)
1902
    state                       =   models.IntegerField(default=APPROVED,
1903
                                                        db_index=True)
1904

    
1905
    objects     =   ProjectManager()
1906

    
1907
    # Compiled queries
1908
    Q_TERMINATED  = Q(state=TERMINATED)
1909
    Q_SUSPENDED   = Q(state=SUSPENDED)
1910
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1911

    
1912
    def __str__(self):
1913
        return uenc(_("<project %s '%s'>") %
1914
                    (self.id, udec(self.application.name)))
1915

    
1916
    __repr__ = __str__
1917

    
1918
    def __unicode__(self):
1919
        return _("<project %s '%s'>") % (self.id, self.application.name)
1920

    
1921
    STATE_DISPLAY = {
1922
        APPROVED   : 'Active',
1923
        SUSPENDED  : 'Suspended',
1924
        TERMINATED : 'Terminated'
1925
        }
1926

    
1927
    def state_display(self):
1928
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1929

    
1930
    def admin_state_display(self):
1931
        s = self.state_display()
1932
        if self.sync_pending():
1933
            s += ' (sync pending)'
1934
        return s
1935

    
1936
    def sync_pending(self):
1937
        if self.state != self.APPROVED:
1938
            return self.is_active
1939
        return not self.is_active or self.is_modified
1940

    
1941
    def expiration_info(self):
1942
        return (str(self.id), self.name, self.state_display(),
1943
                str(self.application.end_date))
1944

    
1945
    def is_deactivated(self, reason=None):
1946
        if reason is not None:
1947
            return self.state == reason
1948

    
1949
        return self.state != self.APPROVED
1950

    
1951
    def is_deactivating(self, reason=None):
1952
        if not self.is_active:
1953
            return False
1954

    
1955
        return self.is_deactivated(reason)
1956

    
1957
    def is_deactivated_strict(self, reason=None):
1958
        if self.is_active:
1959
            return False
1960

    
1961
        return self.is_deactivated(reason)
1962

    
1963
    ### Deactivation calls
1964

    
1965
    def deactivate(self):
1966
        self.deactivation_date = datetime.now()
1967
        self.is_active = False
1968

    
1969
    def reactivate(self):
1970
        self.deactivation_date = None
1971
        self.is_active = True
1972

    
1973
    def terminate(self):
1974
        self.deactivation_reason = 'TERMINATED'
1975
        self.state = self.TERMINATED
1976
        self.name = None
1977
        self.save()
1978

    
1979
    def suspend(self):
1980
        self.deactivation_reason = 'SUSPENDED'
1981
        self.state = self.SUSPENDED
1982
        self.save()
1983

    
1984
    def resume(self):
1985
        self.deactivation_reason = None
1986
        self.state = self.APPROVED
1987
        self.save()
1988

    
1989
    ### Logical checks
1990

    
1991
    def is_inconsistent(self):
1992
        now = datetime.now()
1993
        dates = [self.creation_date,
1994
                 self.last_approval_date,
1995
                 self.deactivation_date]
1996
        return any([date > now for date in dates])
1997

    
1998
    def is_active_strict(self):
1999
        return self.is_active and self.state == self.APPROVED
2000

    
2001
    def is_approved(self):
2002
        return self.state == self.APPROVED
2003

    
2004
    @property
2005
    def is_alive(self):
2006
        return not self.is_terminated
2007

    
2008
    @property
2009
    def is_terminated(self):
2010
        return self.is_deactivated(self.TERMINATED)
2011

    
2012
    @property
2013
    def is_suspended(self):
2014
        return self.is_deactivated(self.SUSPENDED)
2015

    
2016
    def violates_resource_grants(self):
2017
        return False
2018

    
2019
    def violates_members_limit(self, adding=0):
2020
        application = self.application
2021
        limit = application.limit_on_members_number
2022
        if limit is None:
2023
            return False
2024
        return (len(self.approved_members) + adding > limit)
2025

    
2026

    
2027
    ### Other
2028

    
2029
    def count_pending_memberships(self):
2030
        memb_set = self.projectmembership_set
2031
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2032
        return memb_count
2033

    
2034
    def members_count(self):
2035
        return self.approved_memberships.count()
2036

    
2037
    @property
2038
    def approved_memberships(self):
2039
        query = ProjectMembership.Q_ACCEPTED_STATES
2040
        return self.projectmembership_set.filter(query)
2041

    
2042
    @property
2043
    def approved_members(self):
2044
        return [m.person for m in self.approved_memberships]
2045

    
2046
    def add_member(self, user):
2047
        """
2048
        Raises:
2049
            django.exceptions.PermissionDenied
2050
            astakos.im.models.AstakosUser.DoesNotExist
2051
        """
2052
        if isinstance(user, (int, long)):
2053
            user = AstakosUser.objects.get(user=user)
2054

    
2055
        m, created = ProjectMembership.objects.get_or_create(
2056
            person=user, project=self
2057
        )
2058
        m.accept()
2059

    
2060
    def remove_member(self, user):
2061
        """
2062
        Raises:
2063
            django.exceptions.PermissionDenied
2064
            astakos.im.models.AstakosUser.DoesNotExist
2065
            astakos.im.models.ProjectMembership.DoesNotExist
2066
        """
2067
        if isinstance(user, (int, long)):
2068
            user = AstakosUser.objects.get(user=user)
2069

    
2070
        m = ProjectMembership.objects.get(person=user, project=self)
2071
        m.remove()
2072

    
2073

    
2074
CHAIN_STATE = {
2075
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2076
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2077
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2078
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2079
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2080

    
2081
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2082
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2083
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2084
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2085
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2086

    
2087
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2088
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2089
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2090
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2091
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2092

    
2093
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2094
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2095
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2096
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2097
    }
2098

    
2099

    
2100
class PendingMembershipError(Exception):
2101
    pass
2102

    
2103

    
2104
class ProjectMembershipManager(ForUpdateManager):
2105

    
2106
    def any_accepted(self):
2107
        q = (Q(state=ProjectMembership.ACCEPTED) |
2108
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
2109
        return self.filter(q)
2110

    
2111
    def actually_accepted(self):
2112
        q = self.model.Q_ACTUALLY_ACCEPTED
2113
        return self.filter(q)
2114

    
2115
    def requested(self):
2116
        return self.filter(state=ProjectMembership.REQUESTED)
2117

    
2118
    def suspended(self):
2119
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2120

    
2121
class ProjectMembership(models.Model):
2122

    
2123
    person              =   models.ForeignKey(AstakosUser)
2124
    request_date        =   models.DateField(auto_now_add=True)
2125
    project             =   models.ForeignKey(Project)
2126

    
2127
    REQUESTED           =   0
2128
    ACCEPTED            =   1
2129
    LEAVE_REQUESTED     =   5
2130
    # User deactivation
2131
    USER_SUSPENDED      =   10
2132
    # Project deactivation
2133
    PROJECT_DEACTIVATED =   100
2134

    
2135
    REMOVED             =   200
2136

    
2137
    ASSOCIATED_STATES   =   set([REQUESTED,
2138
                                 ACCEPTED,
2139
                                 LEAVE_REQUESTED,
2140
                                 USER_SUSPENDED,
2141
                                 PROJECT_DEACTIVATED])
2142

    
2143
    ACCEPTED_STATES     =   set([ACCEPTED,
2144
                                 LEAVE_REQUESTED,
2145
                                 USER_SUSPENDED,
2146
                                 PROJECT_DEACTIVATED])
2147

    
2148
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2149

    
2150
    state               =   models.IntegerField(default=REQUESTED,
2151
                                                db_index=True)
2152
    is_pending          =   models.BooleanField(default=False, db_index=True)
2153
    is_active           =   models.BooleanField(default=False, db_index=True)
2154
    application         =   models.ForeignKey(
2155
                                ProjectApplication,
2156
                                null=True,
2157
                                related_name='memberships')
2158
    pending_application =   models.ForeignKey(
2159
                                ProjectApplication,
2160
                                null=True,
2161
                                related_name='pending_memberships')
2162
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2163

    
2164
    acceptance_date     =   models.DateField(null=True, db_index=True)
2165
    leave_request_date  =   models.DateField(null=True)
2166

    
2167
    objects     =   ProjectMembershipManager()
2168

    
2169
    # Compiled queries
2170
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2171
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2172

    
2173
    MEMBERSHIP_STATE_DISPLAY = {
2174
        REQUESTED           : _('Requested'),
2175
        ACCEPTED            : _('Accepted'),
2176
        LEAVE_REQUESTED     : _('Leave Requested'),
2177
        USER_SUSPENDED      : _('Suspended'),
2178
        PROJECT_DEACTIVATED : _('Accepted'), # sic
2179
        REMOVED             : _('Pending removal'),
2180
        }
2181

    
2182
    USER_FRIENDLY_STATE_DISPLAY = {
2183
        REQUESTED           : _('Join requested'),
2184
        ACCEPTED            : _('Accepted member'),
2185
        LEAVE_REQUESTED     : _('Requested to leave'),
2186
        USER_SUSPENDED      : _('Suspended member'),
2187
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
2188
        REMOVED             : _('Pending removal'),
2189
        }
2190

    
2191
    def state_display(self):
2192
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2193

    
2194
    def user_friendly_state_display(self):
2195
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2196

    
2197
    def get_combined_state(self):
2198
        return self.state, self.is_active, self.is_pending
2199

    
2200
    class Meta:
2201
        unique_together = ("person", "project")
2202
        #index_together = [["project", "state"]]
2203

    
2204
    def __str__(self):
2205
        return uenc(_("<'%s' membership in '%s'>") % (
2206
                self.person.username, self.project))
2207

    
2208
    __repr__ = __str__
2209

    
2210
    def __init__(self, *args, **kwargs):
2211
        self.state = self.REQUESTED
2212
        super(ProjectMembership, self).__init__(*args, **kwargs)
2213

    
2214
    def _set_history_item(self, reason, date=None):
2215
        if isinstance(reason, basestring):
2216
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2217

    
2218
        history_item = ProjectMembershipHistory(
2219
                            serial=self.id,
2220
                            person=self.person_id,
2221
                            project=self.project_id,
2222
                            date=date or datetime.now(),
2223
                            reason=reason)
2224
        history_item.save()
2225
        serial = history_item.id
2226

    
2227
    def can_accept(self):
2228
        return self.state == self.REQUESTED
2229

    
2230
    def accept(self):
2231
        if self.is_pending:
2232
            m = _("%s: attempt to accept while is pending") % (self,)
2233
            raise AssertionError(m)
2234

    
2235
        if not self.can_accept():
2236
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2237
            raise AssertionError(m)
2238

    
2239
        now = datetime.now()
2240
        self.acceptance_date = now
2241
        self._set_history_item(reason='ACCEPT', date=now)
2242
        if self.project.is_approved():
2243
            self.state = self.ACCEPTED
2244
            self.is_pending = True
2245
        else:
2246
            self.state = self.PROJECT_DEACTIVATED
2247

    
2248
        self.save()
2249

    
2250
    def can_leave(self):
2251
        return self.state in self.ACCEPTED_STATES
2252

    
2253
    def leave_request(self):
2254
        if self.is_pending:
2255
            m = _("%s: attempt to request to leave while is pending") % (self,)
2256
            raise AssertionError(m)
2257

    
2258
        if not self.can_leave():
2259
            m = _("%s: attempt to request to leave in state '%s'") % (
2260
                self, self.state)
2261
            raise AssertionError(m)
2262

    
2263
        self.leave_request_date = datetime.now()
2264
        self.state = self.LEAVE_REQUESTED
2265
        self.save()
2266

    
2267
    def can_deny_leave(self):
2268
        return self.state == self.LEAVE_REQUESTED
2269

    
2270
    def leave_request_deny(self):
2271
        if self.is_pending:
2272
            m = _("%s: attempt to deny leave request while is pending") % (
2273
                self,)
2274
            raise AssertionError(m)
2275

    
2276
        if not self.can_deny_leave():
2277
            m = _("%s: attempt to deny leave request in state '%s'") % (
2278
                self, self.state)
2279
            raise AssertionError(m)
2280

    
2281
        self.leave_request_date = None
2282
        self.state = self.ACCEPTED
2283
        self.save()
2284

    
2285
    def can_cancel_leave(self):
2286
        return self.state == self.LEAVE_REQUESTED
2287

    
2288
    def leave_request_cancel(self):
2289
        if self.is_pending:
2290
            m = _("%s: attempt to cancel leave request while is pending") % (
2291
                self,)
2292
            raise AssertionError(m)
2293

    
2294
        if not self.can_cancel_leave():
2295
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2296
                self, self.state)
2297
            raise AssertionError(m)
2298

    
2299
        self.leave_request_date = None
2300
        self.state = self.ACCEPTED
2301
        self.save()
2302

    
2303
    def can_remove(self):
2304
        return self.state in self.ACCEPTED_STATES
2305

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

    
2311
        if not self.can_remove():
2312
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2313
            raise AssertionError(m)
2314

    
2315
        self._set_history_item(reason='REMOVE')
2316
        self.state = self.REMOVED
2317
        self.is_pending = True
2318
        self.save()
2319

    
2320
    def can_reject(self):
2321
        return self.state == self.REQUESTED
2322

    
2323
    def reject(self):
2324
        if self.is_pending:
2325
            m = _("%s: attempt to reject while is pending") % (self,)
2326
            raise AssertionError(m)
2327

    
2328
        if not self.can_reject():
2329
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2330
            raise AssertionError(m)
2331

    
2332
        # rejected requests don't need sync,
2333
        # because they were never effected
2334
        self._set_history_item(reason='REJECT')
2335
        self.delete()
2336

    
2337
    def can_cancel(self):
2338
        return self.state == self.REQUESTED
2339

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

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

    
2349
        # rejected requests don't need sync,
2350
        # because they were never effected
2351
        self._set_history_item(reason='CANCEL')
2352
        self.delete()
2353

    
2354
    def get_diff_quotas(self, sub_list=None, add_list=None):
2355
        if sub_list is None:
2356
            sub_list = []
2357

    
2358
        if add_list is None:
2359
            add_list = []
2360

    
2361
        sub_append = sub_list.append
2362
        add_append = add_list.append
2363
        holder = self.person.uuid
2364

    
2365
        synced_application = self.application
2366
        if synced_application is not None:
2367
            cur_grants = synced_application.projectresourcegrant_set.all()
2368
            for grant in cur_grants:
2369
                sub_append(QuotaLimits(
2370
                               holder       = holder,
2371
                               resource     = str(grant.resource),
2372
                               capacity     = grant.member_capacity,
2373
                               import_limit = grant.member_import_limit,
2374
                               export_limit = grant.member_export_limit))
2375

    
2376
        pending_application = self.pending_application
2377
        if pending_application is not None:
2378
            new_grants = pending_application.projectresourcegrant_set.all()
2379
            for new_grant in new_grants:
2380
                add_append(QuotaLimits(
2381
                               holder       = holder,
2382
                               resource     = str(new_grant.resource),
2383
                               capacity     = new_grant.member_capacity,
2384
                               import_limit = new_grant.member_import_limit,
2385
                               export_limit = new_grant.member_export_limit))
2386

    
2387
        return (sub_list, add_list)
2388

    
2389
    def set_sync(self):
2390
        if not self.is_pending:
2391
            m = _("%s: attempt to sync a non pending membership") % (self,)
2392
            raise AssertionError(m)
2393

    
2394
        state = self.state
2395
        if state in self.ACTUALLY_ACCEPTED:
2396
            pending_application = self.pending_application
2397
            if pending_application is None:
2398
                m = _("%s: attempt to sync an empty pending application") % (
2399
                    self,)
2400
                raise AssertionError(m)
2401

    
2402
            self.application = pending_application
2403
            self.is_active = True
2404

    
2405
            self.pending_application = None
2406
            self.pending_serial = None
2407

    
2408
            # project.application may have changed in the meantime,
2409
            # in which case we stay PENDING;
2410
            # we are safe to check due to select_for_update
2411
            if self.application == self.project.application:
2412
                self.is_pending = False
2413
            self.save()
2414

    
2415
        elif state == self.PROJECT_DEACTIVATED:
2416
            if self.pending_application:
2417
                m = _("%s: attempt to sync in state '%s' "
2418
                      "with a pending application") % (self, state)
2419
                raise AssertionError(m)
2420

    
2421
            self.application = None
2422
            self.is_active = False
2423
            self.pending_serial = None
2424
            self.is_pending = False
2425
            self.save()
2426

    
2427
        elif state == self.REMOVED:
2428
            self.delete()
2429

    
2430
        else:
2431
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2432
            raise AssertionError(m)
2433

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

    
2439
        state = self.state
2440
        if state in [self.ACCEPTED, self.LEAVE_REQUESTED,
2441
                     self.PROJECT_DEACTIVATED, self.REMOVED]:
2442
            self.pending_application = None
2443
            self.pending_serial = None
2444
            self.save()
2445
        else:
2446
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2447
            raise AssertionError(m)
2448

    
2449
class Serial(models.Model):
2450
    serial  =   models.AutoField(primary_key=True)
2451

    
2452
def new_serial():
2453
    s = Serial.objects.create()
2454
    serial = s.serial
2455
    s.delete()
2456
    return serial
2457

    
2458
class SyncError(Exception):
2459
    pass
2460

    
2461
def reset_serials(serials):
2462
    objs = ProjectMembership.objects
2463
    q = objs.filter(pending_serial__in=serials).select_for_update()
2464
    memberships = list(q)
2465

    
2466
    if memberships:
2467
        for membership in memberships:
2468
            membership.reset_sync()
2469

    
2470
        transaction.commit()
2471

    
2472
def sync_finish_serials(serials_to_ack=None):
2473
    if serials_to_ack is None:
2474
        serials_to_ack = qh_query_serials([])
2475

    
2476
    serials_to_ack = set(serials_to_ack)
2477
    objs = ProjectMembership.objects
2478
    q = objs.filter(pending_serial__isnull=False).select_for_update()
2479
    memberships = list(q)
2480

    
2481
    if memberships:
2482
        for membership in memberships:
2483
            serial = membership.pending_serial
2484
            if serial in serials_to_ack:
2485
                membership.set_sync()
2486
            else:
2487
                membership.reset_sync()
2488

    
2489
        transaction.commit()
2490

    
2491
    qh_ack_serials(list(serials_to_ack))
2492
    return len(memberships)
2493

    
2494
def pre_sync_projects(sync=True):
2495
    ACCEPTED = ProjectMembership.ACCEPTED
2496
    LEAVE_REQUESTED = ProjectMembership.LEAVE_REQUESTED
2497
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2498
    objs = Project.objects
2499

    
2500
    modified = list(objs.modified_projects().select_for_update())
2501
    if sync:
2502
        for project in modified:
2503
            objects = project.projectmembership_set
2504

    
2505
            memberships = objects.actually_accepted().select_for_update()
2506
            for membership in memberships:
2507
                membership.is_pending = True
2508
                membership.save()
2509

    
2510
    reactivating = list(objs.reactivating_projects().select_for_update())
2511
    if sync:
2512
        for project in reactivating:
2513
            objects = project.projectmembership_set
2514

    
2515
            q = objects.filter(state=PROJECT_DEACTIVATED)
2516
            memberships = q.select_for_update()
2517
            for membership in memberships:
2518
                membership.is_pending = True
2519
                if membership.leave_request_date is None:
2520
                    membership.state = ACCEPTED
2521
                else:
2522
                    membership.state = LEAVE_REQUESTED
2523
                membership.save()
2524

    
2525
    deactivating = list(objs.deactivating_projects().select_for_update())
2526
    if sync:
2527
        for project in deactivating:
2528
            objects = project.projectmembership_set
2529

    
2530
            # Note: we keep a user-level deactivation
2531
            # (e.g. USER_SUSPENDED) intact
2532
            memberships = objects.actually_accepted().select_for_update()
2533
            for membership in memberships:
2534
                membership.is_pending = True
2535
                membership.state = PROJECT_DEACTIVATED
2536
                membership.save()
2537

    
2538
#    transaction.commit()
2539
    return (modified, reactivating, deactivating)
2540

    
2541
def set_sync_projects(exclude=None):
2542

    
2543
    ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED
2544
    objects = ProjectMembership.objects
2545

    
2546
    sub_quota, add_quota = [], []
2547

    
2548
    serial = new_serial()
2549

    
2550
    pending = objects.filter(is_pending=True).select_for_update()
2551
    for membership in pending:
2552

    
2553
        if membership.pending_application:
2554
            m = "%s: impossible: pending_application is not None (%s)" % (
2555
                membership, membership.pending_application)
2556
            raise AssertionError(m)
2557
        if membership.pending_serial:
2558
            m = "%s: impossible: pending_serial is not None (%s)" % (
2559
                membership, membership.pending_serial)
2560
            raise AssertionError(m)
2561

    
2562
        if exclude is not None:
2563
            uuid = membership.person.uuid
2564
            if uuid in exclude:
2565
                logger.warning("Excluded from sync: %s" % uuid)
2566
                continue
2567

    
2568
        if membership.state in ACTUALLY_ACCEPTED:
2569
            membership.pending_application = membership.project.application
2570

    
2571
        membership.pending_serial = serial
2572
        membership.get_diff_quotas(sub_quota, add_quota)
2573
        membership.save()
2574

    
2575
    transaction.commit()
2576
    return serial, sub_quota, add_quota
2577

    
2578
def do_sync_projects():
2579
    serial, sub_quota, add_quota = set_sync_projects()
2580
    r = qh_add_quota(serial, sub_quota, add_quota)
2581
    if not r:
2582
        return serial
2583

    
2584
    m = "cannot sync serial: %d" % serial
2585
    logger.error(m)
2586
    logger.error("Failed: %s" % r)
2587

    
2588
    reset_serials([serial])
2589
    uuids = set(uuid for (uuid, resource) in r)
2590
    serial, sub_quota, add_quota = set_sync_projects(exclude=uuids)
2591
    r = qh_add_quota(serial, sub_quota, add_quota)
2592
    if not r:
2593
        return serial
2594

    
2595
    m = "cannot sync serial: %d" % serial
2596
    logger.error(m)
2597
    logger.error("Failed: %s" % r)
2598
    raise SyncError(m)
2599

    
2600
def post_sync_projects():
2601
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2602
    Q_ACTUALLY_ACCEPTED = ProjectMembership.Q_ACTUALLY_ACCEPTED
2603
    objs = Project.objects
2604

    
2605
    modified = objs.modified_projects().select_for_update()
2606
    for project in modified:
2607
        objects = project.projectmembership_set
2608
        q = objects.filter(Q_ACTUALLY_ACCEPTED & Q(is_pending=True))
2609
        memberships = list(q.select_for_update())
2610
        if not memberships:
2611
            project.is_modified = False
2612
            project.save()
2613

    
2614
    reactivating = objs.reactivating_projects().select_for_update()
2615
    for project in reactivating:
2616
        objects = project.projectmembership_set
2617
        q = objects.filter(Q(state=PROJECT_DEACTIVATED) | Q(is_pending=True))
2618
        memberships = list(q.select_for_update())
2619
        if not memberships:
2620
            project.reactivate()
2621
            project.save()
2622

    
2623
    deactivating = objs.deactivating_projects().select_for_update()
2624
    for project in deactivating:
2625
        objects = project.projectmembership_set
2626
        q = objects.filter(Q_ACTUALLY_ACCEPTED | Q(is_pending=True))
2627
        memberships = list(q.select_for_update())
2628
        if not memberships:
2629
            project.deactivate()
2630
            project.save()
2631

    
2632
    transaction.commit()
2633

    
2634
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2635
    @with_lock(retries, retry_wait)
2636
    def _sync_projects(sync):
2637
        sync_finish_serials()
2638
        # Informative only -- no select_for_update()
2639
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2640

    
2641
        projects_log = pre_sync_projects(sync)
2642
        if sync:
2643
            serial = do_sync_projects()
2644
            sync_finish_serials([serial])
2645
            post_sync_projects()
2646

    
2647
        return (pending, projects_log)
2648
    return _sync_projects(sync)
2649

    
2650
def all_users_quotas(users):
2651
    initial = {}
2652
    quotas = {}
2653
    info = {}
2654
    for user in users:
2655
        uuid = user.uuid
2656
        info[uuid] = user.email
2657
        init = user.initial_quotas()
2658
        initial[uuid] = init
2659
        quotas[user.uuid] = user.all_quotas(initial=init)
2660
    return initial, quotas, info
2661

    
2662
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2663
    @with_lock(retries, retry_wait)
2664
    def _sync_users(users, sync):
2665
        sync_finish_serials()
2666

    
2667
        existing, nonexisting = qh_check_users(users)
2668
        resources = get_resource_names()
2669
        qh_limits, qh_counters = qh_get_quotas(existing, resources)
2670
        astakos_initial, astakos_quotas, info = all_users_quotas(users)
2671

    
2672
        if sync:
2673
            r = register_users(nonexisting)
2674
            r = send_quotas(astakos_quotas)
2675

    
2676
        return (existing, nonexisting,
2677
                qh_limits, qh_counters,
2678
                astakos_initial, astakos_quotas, info)
2679
    return _sync_users(users, sync)
2680

    
2681
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2682
    users = AstakosUser.objects.verified()
2683
    return sync_users(users, sync, retries, retry_wait)
2684

    
2685
class ProjectMembershipHistory(models.Model):
2686
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2687
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2688

    
2689
    person  =   models.BigIntegerField()
2690
    project =   models.BigIntegerField()
2691
    date    =   models.DateField(auto_now_add=True)
2692
    reason  =   models.IntegerField()
2693
    serial  =   models.BigIntegerField()
2694

    
2695
### SIGNALS ###
2696
################
2697

    
2698
def create_astakos_user(u):
2699
    try:
2700
        AstakosUser.objects.get(user_ptr=u.pk)
2701
    except AstakosUser.DoesNotExist:
2702
        extended_user = AstakosUser(user_ptr_id=u.pk)
2703
        extended_user.__dict__.update(u.__dict__)
2704
        extended_user.save()
2705
        if not extended_user.has_auth_provider('local'):
2706
            extended_user.add_auth_provider('local')
2707
    except BaseException, e:
2708
        logger.exception(e)
2709

    
2710
def fix_superusers():
2711
    # Associate superusers with AstakosUser
2712
    admins = User.objects.filter(is_superuser=True)
2713
    for u in admins:
2714
        create_astakos_user(u)
2715

    
2716
def user_post_save(sender, instance, created, **kwargs):
2717
    if not created:
2718
        return
2719
    create_astakos_user(instance)
2720
post_save.connect(user_post_save, sender=User)
2721

    
2722
def astakosuser_post_save(sender, instance, created, **kwargs):
2723
    pass
2724

    
2725
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2726

    
2727
def resource_post_save(sender, instance, created, **kwargs):
2728
    pass
2729

    
2730
post_save.connect(resource_post_save, sender=Resource)
2731

    
2732
def renew_token(sender, instance, **kwargs):
2733
    if not instance.auth_token:
2734
        instance.renew_token()
2735
pre_save.connect(renew_token, sender=AstakosUser)
2736
pre_save.connect(renew_token, sender=Service)