Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (92.3 kB)

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

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

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

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

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

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

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

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

    
88
logger = logging.getLogger(__name__)
89

    
90
DEFAULT_CONTENT_TYPE = None
91
_content_type = None
92

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

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

    
105
RESOURCE_SEPARATOR = '.'
106

    
107
inf = float('inf')
108

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

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

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

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

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

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

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

    
148

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
235
        ss.append(service)
236

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

    
250
                rs.append(r)
251

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

    
257
    register_services(ss)
258
    register_resources(rs)
259

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

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

    
275
    return _DEFAULT_QUOTA
276

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

    
283

    
284
class AstakosUserManager(UserManager):
285

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

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

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

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

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

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

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

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

    
338

    
339

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

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

    
356

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

    
363
    auth_token = models.CharField(_('Authentication Token'), 
364
                                  max_length=32,
365
                                  null=True, 
366
                                  blank=True, 
367
                                  help_text = _( '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
    def settings(self):
841
        return UserSetting.objects.filter(user=self)
842

    
843

    
844
class AstakosUserAuthProviderManager(models.Manager):
845

    
846
    def active(self, **filters):
847
        return self.filter(active=True, **filters)
848

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

    
857

    
858

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

    
877
    objects = AstakosUserAuthProviderManager()
878

    
879
    class Meta:
880
        unique_together = (('identifier', 'module', 'user'), )
881
        ordering = ('module', 'created')
882

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

    
892
        for key,value in self.info.iteritems():
893
            setattr(self, 'info_%s' % key, value)
894

    
895

    
896
    @property
897
    def settings(self):
898
        return auth_providers.get_provider(self.module)
899

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

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

    
922
    def can_remove(self):
923
        return self.user.can_remove_auth_provider(self.module)
924

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

    
932
    def __repr__(self):
933
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
934

    
935
    def __unicode__(self):
936
        if self.identifier:
937
            return "%s:%s" % (self.module, self.identifier)
938
        if self.auth_backend:
939
            return "%s:%s" % (self.module, self.auth_backend)
940
        return self.module
941

    
942
    def save(self, *args, **kwargs):
943
        self.info_data = json.dumps(self.info)
944
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
945

    
946

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

    
974
    update_or_create = _update_or_create
975

    
976

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

    
986
    class Meta:
987
        unique_together = ("resource", "user")
988

    
989
    def quota_values(self):
990
        return QuotaValues(
991
            quantity = self.quantity,
992
            capacity = self.capacity,
993
            import_limit = self.import_limit,
994
            export_limit = self.export_limit)
995

    
996

    
997
class ApprovalTerms(models.Model):
998
    """
999
    Model for approval terms
1000
    """
1001

    
1002
    date = models.DateTimeField(
1003
        _('Issue date'), db_index=True, auto_now_add=True)
1004
    location = models.CharField(_('Terms location'), max_length=255)
1005

    
1006

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

    
1020
    def __init__(self, *args, **kwargs):
1021
        super(Invitation, self).__init__(*args, **kwargs)
1022
        if not self.id:
1023
            self.code = _generate_invitation_code()
1024

    
1025
    def consume(self):
1026
        self.is_consumed = True
1027
        self.consumed = datetime.now()
1028
        self.save()
1029

    
1030
    def __unicode__(self):
1031
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1032

    
1033

    
1034
class EmailChangeManager(models.Manager):
1035

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

1042
        If the key is valid and has not expired, return the ``User``
1043
        after activating.
1044

1045
        If the key is not valid or has expired, return ``None``.
1046

1047
        If the key is valid but the ``User`` is already active,
1048
        return ``None``.
1049

1050
        After successful email change the activation record is deleted.
1051

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

    
1080

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

    
1093
    objects = EmailChangeManager()
1094

    
1095
    def get_url(self):
1096
        return reverse('email_change_confirm',
1097
                      kwargs={'activation_key': self.activation_key})
1098

    
1099
    def activation_key_expired(self):
1100
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1101
        return self.requested_at + expiration_date < datetime.now()
1102

    
1103

    
1104
class AdditionalMail(models.Model):
1105
    """
1106
    Model for registring invitations
1107
    """
1108
    owner = models.ForeignKey(AstakosUser)
1109
    email = models.EmailField()
1110

    
1111

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

    
1121

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

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

    
1149
    class Meta:
1150
        unique_together = ("provider", "third_party_identifier")
1151

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

    
1161
        return user
1162

    
1163
    @property
1164
    def realname(self):
1165
        return '%s %s' %(self.first_name, self.last_name)
1166

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

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

    
1187
    def generate_token(self):
1188
        self.password = self.third_party_identifier
1189
        self.last_login = datetime.now()
1190
        self.token = default_token_generator.make_token(self)
1191

    
1192
    def existing_user(self):
1193
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1194
                                         auth_providers__identifier=self.third_party_identifier)
1195

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

    
1200

    
1201
class UserSetting(models.Model):
1202
    user = models.ForeignKey(AstakosUser)
1203
    setting = models.CharField(max_length=255)
1204
    value = models.IntegerField()
1205

    
1206
    objects = ForUpdateManager()
1207

    
1208
    class Meta:
1209
        unique_together = ("user", "setting")
1210

    
1211

    
1212
### PROJECTS ###
1213
################
1214

    
1215
def synced_model_metaclass(class_name, class_parents, class_attributes):
1216

    
1217
    new_attributes = {}
1218
    sync_attributes = {}
1219

    
1220
    for name, value in class_attributes.iteritems():
1221
        sync, underscore, rest = name.partition('_')
1222
        if sync == 'sync' and underscore == '_':
1223
            sync_attributes[rest] = value
1224
        else:
1225
            new_attributes[name] = value
1226

    
1227
    if 'prefix' not in sync_attributes:
1228
        m = ("you did not specify a 'sync_prefix' attribute "
1229
             "in class '%s'" % (class_name,))
1230
        raise ValueError(m)
1231

    
1232
    prefix = sync_attributes.pop('prefix')
1233
    class_name = sync_attributes.pop('classname', prefix + '_model')
1234

    
1235
    for name, value in sync_attributes.iteritems():
1236
        newname = prefix + '_' + name
1237
        if newname in new_attributes:
1238
            m = ("class '%s' was specified with prefix '%s' "
1239
                 "but it already has an attribute named '%s'"
1240
                 % (class_name, prefix, newname))
1241
            raise ValueError(m)
1242

    
1243
        new_attributes[newname] = value
1244

    
1245
    newclass = type(class_name, class_parents, new_attributes)
1246
    return newclass
1247

    
1248

    
1249
def make_synced(prefix='sync', name='SyncedState'):
1250

    
1251
    the_name = name
1252
    the_prefix = prefix
1253

    
1254
    class SyncedState(models.Model):
1255

    
1256
        sync_classname      = the_name
1257
        sync_prefix         = the_prefix
1258
        __metaclass__       = synced_model_metaclass
1259

    
1260
        sync_new_state      = models.BigIntegerField(null=True)
1261
        sync_synced_state   = models.BigIntegerField(null=True)
1262
        STATUS_SYNCED       = 0
1263
        STATUS_PENDING      = 1
1264
        sync_status         = models.IntegerField(db_index=True)
1265

    
1266
        class Meta:
1267
            abstract = True
1268

    
1269
        class NotSynced(Exception):
1270
            pass
1271

    
1272
        def sync_init_state(self, state):
1273
            self.sync_synced_state = state
1274
            self.sync_new_state = state
1275
            self.sync_status = self.STATUS_SYNCED
1276

    
1277
        def sync_get_status(self):
1278
            return self.sync_status
1279

    
1280
        def sync_set_status(self):
1281
            if self.sync_new_state != self.sync_synced_state:
1282
                self.sync_status = self.STATUS_PENDING
1283
            else:
1284
                self.sync_status = self.STATUS_SYNCED
1285

    
1286
        def sync_set_synced(self):
1287
            self.sync_synced_state = self.sync_new_state
1288
            self.sync_status = self.STATUS_SYNCED
1289

    
1290
        def sync_get_synced_state(self):
1291
            return self.sync_synced_state
1292

    
1293
        def sync_set_new_state(self, new_state):
1294
            self.sync_new_state = new_state
1295
            self.sync_set_status()
1296

    
1297
        def sync_get_new_state(self):
1298
            return self.sync_new_state
1299

    
1300
        def sync_set_synced_state(self, synced_state):
1301
            self.sync_synced_state = synced_state
1302
            self.sync_set_status()
1303

    
1304
        def sync_get_pending_objects(self):
1305
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1306
            return self.objects.filter(**kw)
1307

    
1308
        def sync_get_synced_objects(self):
1309
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1310
            return self.objects.filter(**kw)
1311

    
1312
        def sync_verify_get_synced_state(self):
1313
            status = self.sync_get_status()
1314
            state = self.sync_get_synced_state()
1315
            verified = (status == self.STATUS_SYNCED)
1316
            return state, verified
1317

    
1318
        def sync_is_synced(self):
1319
            state, verified = self.sync_verify_get_synced_state()
1320
            return verified
1321

    
1322
    return SyncedState
1323

    
1324
SyncedState = make_synced(prefix='sync', name='SyncedState')
1325

    
1326

    
1327
class ChainManager(ForUpdateManager):
1328

    
1329
    def search_by_name(self, *search_strings):
1330
        projects = Project.objects.search_by_name(*search_strings)
1331
        chains = [p.id for p in projects]
1332
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1333
        apps = (app for app in apps if app.is_latest())
1334
        app_chains = [app.chain for app in apps if app.chain not in chains]
1335
        return chains + app_chains
1336

    
1337
    def all_full_state(self):
1338
        d = {}
1339
        chains = self.all()
1340
        for chain in chains:
1341
            d[chain.pk] = chain.full_state()
1342
        return d
1343

    
1344
    def of_project(self, project):
1345
        if project is None:
1346
            return None
1347
        try:
1348
            return self.get(chain=project.id)
1349
        except Chain.DoesNotExist:
1350
            raise AssertionError('project with no chain')
1351

    
1352

    
1353
class Chain(models.Model):
1354
    chain  =   models.AutoField(primary_key=True)
1355

    
1356
    def __str__(self):
1357
        return "%s" % (self.chain,)
1358

    
1359
    objects = ChainManager()
1360

    
1361
    PENDING            = 0
1362
    DENIED             = 3
1363
    DISMISSED          = 4
1364
    CANCELLED          = 5
1365

    
1366
    APPROVED           = 10
1367
    APPROVED_PENDING   = 11
1368
    SUSPENDED          = 12
1369
    SUSPENDED_PENDING  = 13
1370
    TERMINATED         = 14
1371
    TERMINATED_PENDING = 15
1372

    
1373
    PENDING_STATES = [PENDING,
1374
                      APPROVED_PENDING,
1375
                      SUSPENDED_PENDING,
1376
                      TERMINATED_PENDING,
1377
                      ]
1378

    
1379
    MODIFICATION_STATES = [APPROVED_PENDING,
1380
                           SUSPENDED_PENDING,
1381
                           TERMINATED_PENDING,
1382
                           ]
1383

    
1384
    RELEVANT_STATES = [PENDING,
1385
                       DENIED,
1386
                       APPROVED,
1387
                       APPROVED_PENDING,
1388
                       SUSPENDED,
1389
                       SUSPENDED_PENDING,
1390
                       TERMINATED_PENDING,
1391
                       ]
1392

    
1393
    SKIP_STATES = [DISMISSED,
1394
                   CANCELLED,
1395
                   TERMINATED]
1396

    
1397
    STATE_DISPLAY = {
1398
        PENDING            : _("Pending"),
1399
        DENIED             : _("Denied"),
1400
        DISMISSED          : _("Dismissed"),
1401
        CANCELLED          : _("Cancelled"),
1402
        APPROVED           : _("Active"),
1403
        APPROVED_PENDING   : _("Active - Pending"),
1404
        SUSPENDED          : _("Suspended"),
1405
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1406
        TERMINATED         : _("Terminated"),
1407
        TERMINATED_PENDING : _("Terminated - Pending"),
1408
        }
1409

    
1410

    
1411
    @classmethod
1412
    def _chain_state(cls, project_state, app_state):
1413
        s = CHAIN_STATE.get((project_state, app_state), None)
1414
        if s is None:
1415
            raise AssertionError('inconsistent chain state')
1416
        return s
1417

    
1418
    @classmethod
1419
    def chain_state(cls, project, app):
1420
        p_state = project.state if project else None
1421
        return cls._chain_state(p_state, app.state)
1422

    
1423
    @classmethod
1424
    def state_display(cls, s):
1425
        if s is None:
1426
            return _("Unknown")
1427
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1428

    
1429
    def last_application(self):
1430
        return self.chained_apps.order_by('-id')[0]
1431

    
1432
    def get_project(self):
1433
        try:
1434
            return self.chained_project
1435
        except Project.DoesNotExist:
1436
            return None
1437

    
1438
    def get_elements(self):
1439
        project = self.get_project()
1440
        app = self.last_application()
1441
        return project, app
1442

    
1443
    def full_state(self):
1444
        project, app = self.get_elements()
1445
        s = self.chain_state(project, app)
1446
        return s, project, app
1447

    
1448
def new_chain():
1449
    c = Chain.objects.create()
1450
    return c
1451

    
1452

    
1453
class ProjectApplicationManager(ForUpdateManager):
1454

    
1455
    def user_visible_projects(self, *filters, **kw_filters):
1456
        model = self.model
1457
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1458

    
1459
    def user_visible_by_chain(self, flt):
1460
        model = self.model
1461
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1462
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1463
        by_chain = dict(pending.annotate(models.Max('id')))
1464
        by_chain.update(approved.annotate(models.Max('id')))
1465
        return self.filter(flt, id__in=by_chain.values())
1466

    
1467
    def user_accessible_projects(self, user):
1468
        """
1469
        Return projects accessed by specified user.
1470
        """
1471
        if user.is_project_admin():
1472
            participates_filters = Q()
1473
        else:
1474
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1475
                                   Q(project__projectmembership__person=user)
1476

    
1477
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1478

    
1479
    def search_by_name(self, *search_strings):
1480
        q = Q()
1481
        for s in search_strings:
1482
            q = q | Q(name__icontains=s)
1483
        return self.filter(q)
1484

    
1485
    def latest_of_chain(self, chain_id):
1486
        try:
1487
            return self.filter(chain=chain_id).order_by('-id')[0]
1488
        except IndexError:
1489
            return None
1490

    
1491

    
1492
class ProjectApplication(models.Model):
1493
    applicant               =   models.ForeignKey(
1494
                                    AstakosUser,
1495
                                    related_name='projects_applied',
1496
                                    db_index=True)
1497

    
1498
    PENDING     =    0
1499
    APPROVED    =    1
1500
    REPLACED    =    2
1501
    DENIED      =    3
1502
    DISMISSED   =    4
1503
    CANCELLED   =    5
1504

    
1505
    state                   =   models.IntegerField(default=PENDING,
1506
                                                    db_index=True)
1507

    
1508
    owner                   =   models.ForeignKey(
1509
                                    AstakosUser,
1510
                                    related_name='projects_owned',
1511
                                    db_index=True)
1512

    
1513
    chain                   =   models.ForeignKey(Chain,
1514
                                                  related_name='chained_apps',
1515
                                                  db_column='chain')
1516
    precursor_application   =   models.ForeignKey('ProjectApplication',
1517
                                                  null=True,
1518
                                                  blank=True)
1519

    
1520
    name                    =   models.CharField(max_length=80)
1521
    homepage                =   models.URLField(max_length=255, null=True,
1522
                                                verify_exists=False)
1523
    description             =   models.TextField(null=True, blank=True)
1524
    start_date              =   models.DateTimeField(null=True, blank=True)
1525
    end_date                =   models.DateTimeField()
1526
    member_join_policy      =   models.IntegerField()
1527
    member_leave_policy     =   models.IntegerField()
1528
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1529
    resource_grants         =   models.ManyToManyField(
1530
                                    Resource,
1531
                                    null=True,
1532
                                    blank=True,
1533
                                    through='ProjectResourceGrant')
1534
    comments                =   models.TextField(null=True, blank=True)
1535
    issue_date              =   models.DateTimeField(auto_now_add=True)
1536
    response_date           =   models.DateTimeField(null=True, blank=True)
1537

    
1538
    objects                 =   ProjectApplicationManager()
1539

    
1540
    # Compiled queries
1541
    Q_PENDING  = Q(state=PENDING)
1542
    Q_APPROVED = Q(state=APPROVED)
1543
    Q_DENIED   = Q(state=DENIED)
1544

    
1545
    class Meta:
1546
        unique_together = ("chain", "id")
1547

    
1548
    def __unicode__(self):
1549
        return "%s applied by %s" % (self.name, self.applicant)
1550

    
1551
    # TODO: Move to a more suitable place
1552
    APPLICATION_STATE_DISPLAY = {
1553
        PENDING  : _('Pending review'),
1554
        APPROVED : _('Approved'),
1555
        REPLACED : _('Replaced'),
1556
        DENIED   : _('Denied'),
1557
        DISMISSED: _('Dismissed'),
1558
        CANCELLED: _('Cancelled')
1559
    }
1560

    
1561
    def get_project(self):
1562
        try:
1563
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1564
            return Project
1565
        except Project.DoesNotExist, e:
1566
            return None
1567

    
1568
    def state_display(self):
1569
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1570

    
1571
    def project_state_display(self):
1572
        try:
1573
            project = self.project
1574
            return project.state_display()
1575
        except Project.DoesNotExist:
1576
            return self.state_display()
1577

    
1578
    def add_resource_policy(self, service, resource, uplimit):
1579
        """Raises ObjectDoesNotExist, IntegrityError"""
1580
        q = self.projectresourcegrant_set
1581
        resource = Resource.objects.get(service__name=service, name=resource)
1582
        q.create(resource=resource, member_capacity=uplimit)
1583

    
1584
    def members_count(self):
1585
        return self.project.approved_memberships.count()
1586

    
1587
    @property
1588
    def grants(self):
1589
        return self.projectresourcegrant_set.values(
1590
            'member_capacity', 'resource__name', 'resource__service__name')
1591

    
1592
    @property
1593
    def resource_policies(self):
1594
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1595

    
1596
    @resource_policies.setter
1597
    def resource_policies(self, policies):
1598
        for p in policies:
1599
            service = p.get('service', None)
1600
            resource = p.get('resource', None)
1601
            uplimit = p.get('uplimit', 0)
1602
            self.add_resource_policy(service, resource, uplimit)
1603

    
1604
    def pending_modifications_incl_me(self):
1605
        q = self.chained_applications()
1606
        q = q.filter(Q(state=self.PENDING))
1607
        return q
1608

    
1609
    def last_pending_incl_me(self):
1610
        try:
1611
            return self.pending_modifications_incl_me().order_by('-id')[0]
1612
        except IndexError:
1613
            return None
1614

    
1615
    def pending_modifications(self):
1616
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1617

    
1618
    def last_pending(self):
1619
        try:
1620
            return self.pending_modifications().order_by('-id')[0]
1621
        except IndexError:
1622
            return None
1623

    
1624
    def is_modification(self):
1625
        # if self.state != self.PENDING:
1626
        #     return False
1627
        parents = self.chained_applications().filter(id__lt=self.id)
1628
        parents = parents.filter(state__in=[self.APPROVED])
1629
        return parents.count() > 0
1630

    
1631
    def chained_applications(self):
1632
        return ProjectApplication.objects.filter(chain=self.chain)
1633

    
1634
    def is_latest(self):
1635
        return self.chained_applications().order_by('-id')[0] == self
1636

    
1637
    def has_pending_modifications(self):
1638
        return bool(self.last_pending())
1639

    
1640
    def denied_modifications(self):
1641
        q = self.chained_applications()
1642
        q = q.filter(Q(state=self.DENIED))
1643
        q = q.filter(~Q(id=self.id))
1644
        return q
1645

    
1646
    def last_denied(self):
1647
        try:
1648
            return self.denied_modifications().order_by('-id')[0]
1649
        except IndexError:
1650
            return None
1651

    
1652
    def has_denied_modifications(self):
1653
        return bool(self.last_denied())
1654

    
1655
    def is_applied(self):
1656
        try:
1657
            self.project
1658
            return True
1659
        except Project.DoesNotExist:
1660
            return False
1661

    
1662
    def get_project(self):
1663
        try:
1664
            return Project.objects.get(id=self.chain)
1665
        except Project.DoesNotExist:
1666
            return None
1667

    
1668
    def project_exists(self):
1669
        return self.get_project() is not None
1670

    
1671
    def _get_project_for_update(self):
1672
        try:
1673
            objects = Project.objects
1674
            project = objects.get_for_update(id=self.chain)
1675
            return project
1676
        except Project.DoesNotExist:
1677
            return None
1678

    
1679
    def can_cancel(self):
1680
        return self.state == self.PENDING
1681

    
1682
    def cancel(self):
1683
        if not self.can_cancel():
1684
            m = _("cannot cancel: application '%s' in state '%s'") % (
1685
                    self.id, self.state)
1686
            raise AssertionError(m)
1687

    
1688
        self.state = self.CANCELLED
1689
        self.save()
1690

    
1691
    def can_dismiss(self):
1692
        return self.state == self.DENIED
1693

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

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

    
1703
    def can_deny(self):
1704
        return self.state == self.PENDING
1705

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

    
1712
        self.state = self.DENIED
1713
        self.response_date = datetime.now()
1714
        self.save()
1715

    
1716
    def can_approve(self):
1717
        return self.state == self.PENDING
1718

    
1719
    def approve(self, approval_user=None):
1720
        """
1721
        If approval_user then during owner membership acceptance
1722
        it is checked whether the request_user is eligible.
1723

1724
        Raises:
1725
            PermissionDenied
1726
        """
1727

    
1728
        if not transaction.is_managed():
1729
            raise AssertionError("NOPE")
1730

    
1731
        new_project_name = self.name
1732
        if not self.can_approve():
1733
            m = _("cannot approve: project '%s' in state '%s'") % (
1734
                    new_project_name, self.state)
1735
            raise AssertionError(m) # invalid argument
1736

    
1737
        now = datetime.now()
1738
        project = self._get_project_for_update()
1739

    
1740
        try:
1741
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1742
            conflicting_project = Project.objects.get(q)
1743
            if (conflicting_project != project):
1744
                m = (_("cannot approve: project with name '%s' "
1745
                       "already exists (id: %s)") % (
1746
                        new_project_name, conflicting_project.id))
1747
                raise PermissionDenied(m) # invalid argument
1748
        except Project.DoesNotExist:
1749
            pass
1750

    
1751
        new_project = False
1752
        if project is None:
1753
            new_project = True
1754
            project = Project(id=self.chain)
1755

    
1756
        project.name = new_project_name
1757
        project.application = self
1758
        project.last_approval_date = now
1759
        if not new_project:
1760
            project.is_modified = True
1761

    
1762
        project.save()
1763

    
1764
        self.state = self.APPROVED
1765
        self.response_date = now
1766
        self.save()
1767

    
1768
    @property
1769
    def member_join_policy_display(self):
1770
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1771

    
1772
    @property
1773
    def member_leave_policy_display(self):
1774
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1775

    
1776
class ProjectResourceGrant(models.Model):
1777

    
1778
    resource                =   models.ForeignKey(Resource)
1779
    project_application     =   models.ForeignKey(ProjectApplication,
1780
                                                  null=True)
1781
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1782
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1783
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1784
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1785
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1786
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1787

    
1788
    objects = ExtendedManager()
1789

    
1790
    class Meta:
1791
        unique_together = ("resource", "project_application")
1792

    
1793
    def member_quota_values(self):
1794
        return QuotaValues(
1795
            quantity = 0,
1796
            capacity = self.member_capacity,
1797
            import_limit = self.member_import_limit,
1798
            export_limit = self.member_export_limit)
1799

    
1800
    def display_member_capacity(self):
1801
        if self.member_capacity:
1802
            if self.resource.unit:
1803
                return ProjectResourceGrant.display_filesize(
1804
                    self.member_capacity)
1805
            else:
1806
                if math.isinf(self.member_capacity):
1807
                    return 'Unlimited'
1808
                else:
1809
                    return self.member_capacity
1810
        else:
1811
            return 'Unlimited'
1812

    
1813
    def __str__(self):
1814
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1815
                                        self.display_member_capacity())
1816

    
1817
    @classmethod
1818
    def display_filesize(cls, value):
1819
        try:
1820
            value = float(value)
1821
        except:
1822
            return
1823
        else:
1824
            if math.isinf(value):
1825
                return 'Unlimited'
1826
            if value > 1:
1827
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1828
                                [0, 0, 0, 0, 0, 0])
1829
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1830
                quotient = float(value) / 1024**exponent
1831
                unit, value_decimals = unit_list[exponent]
1832
                format_string = '{0:.%sf} {1}' % (value_decimals)
1833
                return format_string.format(quotient, unit)
1834
            if value == 0:
1835
                return '0 bytes'
1836
            if value == 1:
1837
                return '1 byte'
1838
            else:
1839
               return '0'
1840

    
1841

    
1842
class ProjectManager(ForUpdateManager):
1843

    
1844
    def terminated_projects(self):
1845
        q = self.model.Q_TERMINATED
1846
        return self.filter(q)
1847

    
1848
    def not_terminated_projects(self):
1849
        q = ~self.model.Q_TERMINATED
1850
        return self.filter(q)
1851

    
1852
    def terminating_projects(self):
1853
        q = self.model.Q_TERMINATED & Q(is_active=True)
1854
        return self.filter(q)
1855

    
1856
    def deactivated_projects(self):
1857
        q = self.model.Q_DEACTIVATED
1858
        return self.filter(q)
1859

    
1860
    def deactivating_projects(self):
1861
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1862
        return self.filter(q)
1863

    
1864
    def modified_projects(self):
1865
        return self.filter(is_modified=True)
1866

    
1867
    def reactivating_projects(self):
1868
        return self.filter(state=Project.APPROVED, is_active=False)
1869

    
1870
    def expired_projects(self):
1871
        q = (~Q(state=Project.TERMINATED) &
1872
              Q(application__end_date__lt=datetime.now()))
1873
        return self.filter(q)
1874

    
1875
    def search_by_name(self, *search_strings):
1876
        q = Q()
1877
        for s in search_strings:
1878
            q = q | Q(name__icontains=s)
1879
        return self.filter(q)
1880

    
1881

    
1882
class Project(models.Model):
1883

    
1884
    id                          =   models.OneToOneField(Chain,
1885
                                                      related_name='chained_project',
1886
                                                      db_column='id',
1887
                                                      primary_key=True)
1888

    
1889
    application                 =   models.OneToOneField(
1890
                                            ProjectApplication,
1891
                                            related_name='project')
1892
    last_approval_date          =   models.DateTimeField(null=True)
1893

    
1894
    members                     =   models.ManyToManyField(
1895
                                            AstakosUser,
1896
                                            through='ProjectMembership')
1897

    
1898
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1899
    deactivation_date           =   models.DateTimeField(null=True)
1900

    
1901
    creation_date               =   models.DateTimeField(auto_now_add=True)
1902
    name                        =   models.CharField(
1903
                                            max_length=80,
1904
                                            null=True,
1905
                                            db_index=True,
1906
                                            unique=True)
1907

    
1908
    APPROVED    = 1
1909
    SUSPENDED   = 10
1910
    TERMINATED  = 100
1911

    
1912
    is_modified                 =   models.BooleanField(default=False,
1913
                                                        db_index=True)
1914
    is_active                   =   models.BooleanField(default=True,
1915
                                                        db_index=True)
1916
    state                       =   models.IntegerField(default=APPROVED,
1917
                                                        db_index=True)
1918

    
1919
    objects     =   ProjectManager()
1920

    
1921
    # Compiled queries
1922
    Q_TERMINATED  = Q(state=TERMINATED)
1923
    Q_SUSPENDED   = Q(state=SUSPENDED)
1924
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1925

    
1926
    def __str__(self):
1927
        return uenc(_("<project %s '%s'>") %
1928
                    (self.id, udec(self.application.name)))
1929

    
1930
    __repr__ = __str__
1931

    
1932
    def __unicode__(self):
1933
        return _("<project %s '%s'>") % (self.id, self.application.name)
1934

    
1935
    STATE_DISPLAY = {
1936
        APPROVED   : 'Active',
1937
        SUSPENDED  : 'Suspended',
1938
        TERMINATED : 'Terminated'
1939
        }
1940

    
1941
    def state_display(self):
1942
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1943

    
1944
    def admin_state_display(self):
1945
        s = self.state_display()
1946
        if self.sync_pending():
1947
            s += ' (sync pending)'
1948
        return s
1949

    
1950
    def sync_pending(self):
1951
        if self.state != self.APPROVED:
1952
            return self.is_active
1953
        return not self.is_active or self.is_modified
1954

    
1955
    def expiration_info(self):
1956
        return (str(self.id), self.name, self.state_display(),
1957
                str(self.application.end_date))
1958

    
1959
    def is_deactivated(self, reason=None):
1960
        if reason is not None:
1961
            return self.state == reason
1962

    
1963
        return self.state != self.APPROVED
1964

    
1965
    def is_deactivating(self, reason=None):
1966
        if not self.is_active:
1967
            return False
1968

    
1969
        return self.is_deactivated(reason)
1970

    
1971
    def is_deactivated_strict(self, reason=None):
1972
        if self.is_active:
1973
            return False
1974

    
1975
        return self.is_deactivated(reason)
1976

    
1977
    ### Deactivation calls
1978

    
1979
    def deactivate(self):
1980
        self.deactivation_date = datetime.now()
1981
        self.is_active = False
1982

    
1983
    def reactivate(self):
1984
        self.deactivation_date = None
1985
        self.is_active = True
1986

    
1987
    def terminate(self):
1988
        self.deactivation_reason = 'TERMINATED'
1989
        self.state = self.TERMINATED
1990
        self.name = None
1991
        self.save()
1992

    
1993
    def suspend(self):
1994
        self.deactivation_reason = 'SUSPENDED'
1995
        self.state = self.SUSPENDED
1996
        self.save()
1997

    
1998
    def resume(self):
1999
        self.deactivation_reason = None
2000
        self.state = self.APPROVED
2001
        self.save()
2002

    
2003
    ### Logical checks
2004

    
2005
    def is_inconsistent(self):
2006
        now = datetime.now()
2007
        dates = [self.creation_date,
2008
                 self.last_approval_date,
2009
                 self.deactivation_date]
2010
        return any([date > now for date in dates])
2011

    
2012
    def is_active_strict(self):
2013
        return self.is_active and self.state == self.APPROVED
2014

    
2015
    def is_approved(self):
2016
        return self.state == self.APPROVED
2017

    
2018
    @property
2019
    def is_alive(self):
2020
        return not self.is_terminated
2021

    
2022
    @property
2023
    def is_terminated(self):
2024
        return self.is_deactivated(self.TERMINATED)
2025

    
2026
    @property
2027
    def is_suspended(self):
2028
        return self.is_deactivated(self.SUSPENDED)
2029

    
2030
    def violates_resource_grants(self):
2031
        return False
2032

    
2033
    def violates_members_limit(self, adding=0):
2034
        application = self.application
2035
        limit = application.limit_on_members_number
2036
        if limit is None:
2037
            return False
2038
        return (len(self.approved_members) + adding > limit)
2039

    
2040

    
2041
    ### Other
2042

    
2043
    def count_pending_memberships(self):
2044
        memb_set = self.projectmembership_set
2045
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2046
        return memb_count
2047

    
2048
    def members_count(self):
2049
        return self.approved_memberships.count()
2050

    
2051
    @property
2052
    def approved_memberships(self):
2053
        query = ProjectMembership.Q_ACCEPTED_STATES
2054
        return self.projectmembership_set.filter(query)
2055

    
2056
    @property
2057
    def approved_members(self):
2058
        return [m.person for m in self.approved_memberships]
2059

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

    
2069
        m, created = ProjectMembership.objects.get_or_create(
2070
            person=user, project=self
2071
        )
2072
        m.accept()
2073

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

    
2084
        m = ProjectMembership.objects.get(person=user, project=self)
2085
        m.remove()
2086

    
2087

    
2088
CHAIN_STATE = {
2089
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2090
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2091
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2092
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2093
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2094

    
2095
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2096
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2097
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2098
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2099
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2100

    
2101
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2102
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2103
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2104
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2105
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2106

    
2107
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2108
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2109
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2110
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2111
    }
2112

    
2113

    
2114
class PendingMembershipError(Exception):
2115
    pass
2116

    
2117

    
2118
class ProjectMembershipManager(ForUpdateManager):
2119

    
2120
    def any_accepted(self):
2121
        q = (Q(state=ProjectMembership.ACCEPTED) |
2122
             Q(state=ProjectMembership.PROJECT_DEACTIVATED))
2123
        return self.filter(q)
2124

    
2125
    def actually_accepted(self):
2126
        q = self.model.Q_ACTUALLY_ACCEPTED
2127
        return self.filter(q)
2128

    
2129
    def requested(self):
2130
        return self.filter(state=ProjectMembership.REQUESTED)
2131

    
2132
    def suspended(self):
2133
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2134

    
2135
class ProjectMembership(models.Model):
2136

    
2137
    person              =   models.ForeignKey(AstakosUser)
2138
    request_date        =   models.DateField(auto_now_add=True)
2139
    project             =   models.ForeignKey(Project)
2140

    
2141
    REQUESTED           =   0
2142
    ACCEPTED            =   1
2143
    LEAVE_REQUESTED     =   5
2144
    # User deactivation
2145
    USER_SUSPENDED      =   10
2146
    # Project deactivation
2147
    PROJECT_DEACTIVATED =   100
2148

    
2149
    REMOVED             =   200
2150

    
2151
    ASSOCIATED_STATES   =   set([REQUESTED,
2152
                                 ACCEPTED,
2153
                                 LEAVE_REQUESTED,
2154
                                 USER_SUSPENDED,
2155
                                 PROJECT_DEACTIVATED])
2156

    
2157
    ACCEPTED_STATES     =   set([ACCEPTED,
2158
                                 LEAVE_REQUESTED,
2159
                                 USER_SUSPENDED,
2160
                                 PROJECT_DEACTIVATED])
2161

    
2162
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2163

    
2164
    state               =   models.IntegerField(default=REQUESTED,
2165
                                                db_index=True)
2166
    is_pending          =   models.BooleanField(default=False, db_index=True)
2167
    is_active           =   models.BooleanField(default=False, db_index=True)
2168
    application         =   models.ForeignKey(
2169
                                ProjectApplication,
2170
                                null=True,
2171
                                related_name='memberships')
2172
    pending_application =   models.ForeignKey(
2173
                                ProjectApplication,
2174
                                null=True,
2175
                                related_name='pending_memberships')
2176
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2177

    
2178
    acceptance_date     =   models.DateField(null=True, db_index=True)
2179
    leave_request_date  =   models.DateField(null=True)
2180

    
2181
    objects     =   ProjectMembershipManager()
2182

    
2183
    # Compiled queries
2184
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2185
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2186

    
2187
    MEMBERSHIP_STATE_DISPLAY = {
2188
        REQUESTED           : _('Requested'),
2189
        ACCEPTED            : _('Accepted'),
2190
        LEAVE_REQUESTED     : _('Leave Requested'),
2191
        USER_SUSPENDED      : _('Suspended'),
2192
        PROJECT_DEACTIVATED : _('Accepted'), # sic
2193
        REMOVED             : _('Pending removal'),
2194
        }
2195

    
2196
    USER_FRIENDLY_STATE_DISPLAY = {
2197
        REQUESTED           : _('Join requested'),
2198
        ACCEPTED            : _('Accepted member'),
2199
        LEAVE_REQUESTED     : _('Requested to leave'),
2200
        USER_SUSPENDED      : _('Suspended member'),
2201
        PROJECT_DEACTIVATED : _('Accepted member'), # sic
2202
        REMOVED             : _('Pending removal'),
2203
        }
2204

    
2205
    def state_display(self):
2206
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2207

    
2208
    def user_friendly_state_display(self):
2209
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2210

    
2211
    def get_combined_state(self):
2212
        return self.state, self.is_active, self.is_pending
2213

    
2214
    class Meta:
2215
        unique_together = ("person", "project")
2216
        #index_together = [["project", "state"]]
2217

    
2218
    def __str__(self):
2219
        return uenc(_("<'%s' membership in '%s'>") % (
2220
                self.person.username, self.project))
2221

    
2222
    __repr__ = __str__
2223

    
2224
    def __init__(self, *args, **kwargs):
2225
        self.state = self.REQUESTED
2226
        super(ProjectMembership, self).__init__(*args, **kwargs)
2227

    
2228
    def _set_history_item(self, reason, date=None):
2229
        if isinstance(reason, basestring):
2230
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2231

    
2232
        history_item = ProjectMembershipHistory(
2233
                            serial=self.id,
2234
                            person=self.person_id,
2235
                            project=self.project_id,
2236
                            date=date or datetime.now(),
2237
                            reason=reason)
2238
        history_item.save()
2239
        serial = history_item.id
2240

    
2241
    def can_accept(self):
2242
        return self.state == self.REQUESTED
2243

    
2244
    def accept(self):
2245
        if self.is_pending:
2246
            m = _("%s: attempt to accept while is pending") % (self,)
2247
            raise AssertionError(m)
2248

    
2249
        if not self.can_accept():
2250
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2251
            raise AssertionError(m)
2252

    
2253
        now = datetime.now()
2254
        self.acceptance_date = now
2255
        self._set_history_item(reason='ACCEPT', date=now)
2256
        if self.project.is_approved():
2257
            self.state = self.ACCEPTED
2258
            self.is_pending = True
2259
        else:
2260
            self.state = self.PROJECT_DEACTIVATED
2261

    
2262
        self.save()
2263

    
2264
    def can_leave(self):
2265
        return self.state in self.ACCEPTED_STATES
2266

    
2267
    def leave_request(self):
2268
        if self.is_pending:
2269
            m = _("%s: attempt to request to leave while is pending") % (self,)
2270
            raise AssertionError(m)
2271

    
2272
        if not self.can_leave():
2273
            m = _("%s: attempt to request to leave in state '%s'") % (
2274
                self, self.state)
2275
            raise AssertionError(m)
2276

    
2277
        self.leave_request_date = datetime.now()
2278
        self.state = self.LEAVE_REQUESTED
2279
        self.save()
2280

    
2281
    def can_deny_leave(self):
2282
        return self.state == self.LEAVE_REQUESTED
2283

    
2284
    def leave_request_deny(self):
2285
        if self.is_pending:
2286
            m = _("%s: attempt to deny leave request while is pending") % (
2287
                self,)
2288
            raise AssertionError(m)
2289

    
2290
        if not self.can_deny_leave():
2291
            m = _("%s: attempt to deny leave request in state '%s'") % (
2292
                self, self.state)
2293
            raise AssertionError(m)
2294

    
2295
        self.leave_request_date = None
2296
        self.state = self.ACCEPTED
2297
        self.save()
2298

    
2299
    def can_cancel_leave(self):
2300
        return self.state == self.LEAVE_REQUESTED
2301

    
2302
    def leave_request_cancel(self):
2303
        if self.is_pending:
2304
            m = _("%s: attempt to cancel leave request while is pending") % (
2305
                self,)
2306
            raise AssertionError(m)
2307

    
2308
        if not self.can_cancel_leave():
2309
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2310
                self, self.state)
2311
            raise AssertionError(m)
2312

    
2313
        self.leave_request_date = None
2314
        self.state = self.ACCEPTED
2315
        self.save()
2316

    
2317
    def can_remove(self):
2318
        return self.state in self.ACCEPTED_STATES
2319

    
2320
    def remove(self):
2321
        if self.is_pending:
2322
            m = _("%s: attempt to remove while is pending") % (self,)
2323
            raise AssertionError(m)
2324

    
2325
        if not self.can_remove():
2326
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2327
            raise AssertionError(m)
2328

    
2329
        self._set_history_item(reason='REMOVE')
2330
        self.state = self.REMOVED
2331
        self.is_pending = True
2332
        self.save()
2333

    
2334
    def can_reject(self):
2335
        return self.state == self.REQUESTED
2336

    
2337
    def reject(self):
2338
        if self.is_pending:
2339
            m = _("%s: attempt to reject while is pending") % (self,)
2340
            raise AssertionError(m)
2341

    
2342
        if not self.can_reject():
2343
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2344
            raise AssertionError(m)
2345

    
2346
        # rejected requests don't need sync,
2347
        # because they were never effected
2348
        self._set_history_item(reason='REJECT')
2349
        self.delete()
2350

    
2351
    def can_cancel(self):
2352
        return self.state == self.REQUESTED
2353

    
2354
    def cancel(self):
2355
        if self.is_pending:
2356
            m = _("%s: attempt to cancel while is pending") % (self,)
2357
            raise AssertionError(m)
2358

    
2359
        if not self.can_cancel():
2360
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2361
            raise AssertionError(m)
2362

    
2363
        # rejected requests don't need sync,
2364
        # because they were never effected
2365
        self._set_history_item(reason='CANCEL')
2366
        self.delete()
2367

    
2368
    def get_diff_quotas(self, sub_list=None, add_list=None):
2369
        if sub_list is None:
2370
            sub_list = []
2371

    
2372
        if add_list is None:
2373
            add_list = []
2374

    
2375
        sub_append = sub_list.append
2376
        add_append = add_list.append
2377
        holder = self.person.uuid
2378

    
2379
        synced_application = self.application
2380
        if synced_application is not None:
2381
            cur_grants = synced_application.projectresourcegrant_set.all()
2382
            for grant in cur_grants:
2383
                sub_append(QuotaLimits(
2384
                               holder       = holder,
2385
                               resource     = str(grant.resource),
2386
                               capacity     = grant.member_capacity,
2387
                               import_limit = grant.member_import_limit,
2388
                               export_limit = grant.member_export_limit))
2389

    
2390
        pending_application = self.pending_application
2391
        if pending_application is not None:
2392
            new_grants = pending_application.projectresourcegrant_set.all()
2393
            for new_grant in new_grants:
2394
                add_append(QuotaLimits(
2395
                               holder       = holder,
2396
                               resource     = str(new_grant.resource),
2397
                               capacity     = new_grant.member_capacity,
2398
                               import_limit = new_grant.member_import_limit,
2399
                               export_limit = new_grant.member_export_limit))
2400

    
2401
        return (sub_list, add_list)
2402

    
2403
    def set_sync(self):
2404
        if not self.is_pending:
2405
            m = _("%s: attempt to sync a non pending membership") % (self,)
2406
            raise AssertionError(m)
2407

    
2408
        state = self.state
2409
        if state in self.ACTUALLY_ACCEPTED:
2410
            pending_application = self.pending_application
2411
            if pending_application is None:
2412
                m = _("%s: attempt to sync an empty pending application") % (
2413
                    self,)
2414
                raise AssertionError(m)
2415

    
2416
            self.application = pending_application
2417
            self.is_active = True
2418

    
2419
            self.pending_application = None
2420
            self.pending_serial = None
2421

    
2422
            # project.application may have changed in the meantime,
2423
            # in which case we stay PENDING;
2424
            # we are safe to check due to select_for_update
2425
            if self.application == self.project.application:
2426
                self.is_pending = False
2427
            self.save()
2428

    
2429
        elif state == self.PROJECT_DEACTIVATED:
2430
            if self.pending_application:
2431
                m = _("%s: attempt to sync in state '%s' "
2432
                      "with a pending application") % (self, state)
2433
                raise AssertionError(m)
2434

    
2435
            self.application = None
2436
            self.is_active = False
2437
            self.pending_serial = None
2438
            self.is_pending = False
2439
            self.save()
2440

    
2441
        elif state == self.REMOVED:
2442
            self.delete()
2443

    
2444
        else:
2445
            m = _("%s: attempt to sync in state '%s'") % (self, state)
2446
            raise AssertionError(m)
2447

    
2448
    def reset_sync(self):
2449
        if not self.is_pending:
2450
            m = _("%s: attempt to reset a non pending membership") % (self,)
2451
            raise AssertionError(m)
2452

    
2453
        state = self.state
2454
        if state in [self.ACCEPTED, self.LEAVE_REQUESTED,
2455
                     self.PROJECT_DEACTIVATED, self.REMOVED]:
2456
            self.pending_application = None
2457
            self.pending_serial = None
2458
            self.save()
2459
        else:
2460
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
2461
            raise AssertionError(m)
2462

    
2463
class Serial(models.Model):
2464
    serial  =   models.AutoField(primary_key=True)
2465

    
2466
def new_serial():
2467
    s = Serial.objects.create()
2468
    serial = s.serial
2469
    s.delete()
2470
    return serial
2471

    
2472
class SyncError(Exception):
2473
    pass
2474

    
2475
def reset_serials(serials):
2476
    objs = ProjectMembership.objects
2477
    q = objs.filter(pending_serial__in=serials).select_for_update()
2478
    memberships = list(q)
2479

    
2480
    if memberships:
2481
        for membership in memberships:
2482
            membership.reset_sync()
2483

    
2484
        transaction.commit()
2485

    
2486
def sync_finish_serials(serials_to_ack=None):
2487
    if serials_to_ack is None:
2488
        serials_to_ack = qh_query_serials([])
2489

    
2490
    serials_to_ack = set(serials_to_ack)
2491
    objs = ProjectMembership.objects
2492
    q = objs.filter(pending_serial__isnull=False).select_for_update()
2493
    memberships = list(q)
2494

    
2495
    if memberships:
2496
        for membership in memberships:
2497
            serial = membership.pending_serial
2498
            if serial in serials_to_ack:
2499
                membership.set_sync()
2500
            else:
2501
                membership.reset_sync()
2502

    
2503
        transaction.commit()
2504

    
2505
    qh_ack_serials(list(serials_to_ack))
2506
    return len(memberships)
2507

    
2508
def pre_sync_projects(sync=True):
2509
    ACCEPTED = ProjectMembership.ACCEPTED
2510
    LEAVE_REQUESTED = ProjectMembership.LEAVE_REQUESTED
2511
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2512
    objs = Project.objects
2513

    
2514
    modified = list(objs.modified_projects().select_for_update())
2515
    if sync:
2516
        for project in modified:
2517
            objects = project.projectmembership_set
2518

    
2519
            memberships = objects.actually_accepted().select_for_update()
2520
            for membership in memberships:
2521
                membership.is_pending = True
2522
                membership.save()
2523

    
2524
    reactivating = list(objs.reactivating_projects().select_for_update())
2525
    if sync:
2526
        for project in reactivating:
2527
            objects = project.projectmembership_set
2528

    
2529
            q = objects.filter(state=PROJECT_DEACTIVATED)
2530
            memberships = q.select_for_update()
2531
            for membership in memberships:
2532
                membership.is_pending = True
2533
                if membership.leave_request_date is None:
2534
                    membership.state = ACCEPTED
2535
                else:
2536
                    membership.state = LEAVE_REQUESTED
2537
                membership.save()
2538

    
2539
    deactivating = list(objs.deactivating_projects().select_for_update())
2540
    if sync:
2541
        for project in deactivating:
2542
            objects = project.projectmembership_set
2543

    
2544
            # Note: we keep a user-level deactivation
2545
            # (e.g. USER_SUSPENDED) intact
2546
            memberships = objects.actually_accepted().select_for_update()
2547
            for membership in memberships:
2548
                membership.is_pending = True
2549
                membership.state = PROJECT_DEACTIVATED
2550
                membership.save()
2551

    
2552
#    transaction.commit()
2553
    return (modified, reactivating, deactivating)
2554

    
2555
def set_sync_projects(exclude=None):
2556

    
2557
    ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED
2558
    objects = ProjectMembership.objects
2559

    
2560
    sub_quota, add_quota = [], []
2561

    
2562
    serial = new_serial()
2563

    
2564
    pending = objects.filter(is_pending=True).select_for_update()
2565
    for membership in pending:
2566

    
2567
        if membership.pending_application:
2568
            m = "%s: impossible: pending_application is not None (%s)" % (
2569
                membership, membership.pending_application)
2570
            raise AssertionError(m)
2571
        if membership.pending_serial:
2572
            m = "%s: impossible: pending_serial is not None (%s)" % (
2573
                membership, membership.pending_serial)
2574
            raise AssertionError(m)
2575

    
2576
        if exclude is not None:
2577
            uuid = membership.person.uuid
2578
            if uuid in exclude:
2579
                logger.warning("Excluded from sync: %s" % uuid)
2580
                continue
2581

    
2582
        if membership.state in ACTUALLY_ACCEPTED:
2583
            membership.pending_application = membership.project.application
2584

    
2585
        membership.pending_serial = serial
2586
        membership.get_diff_quotas(sub_quota, add_quota)
2587
        membership.save()
2588

    
2589
    transaction.commit()
2590
    return serial, sub_quota, add_quota
2591

    
2592
def do_sync_projects():
2593
    serial, sub_quota, add_quota = set_sync_projects()
2594
    r = qh_add_quota(serial, sub_quota, add_quota)
2595
    if not r:
2596
        return serial
2597

    
2598
    m = "cannot sync serial: %d" % serial
2599
    logger.error(m)
2600
    logger.error("Failed: %s" % r)
2601

    
2602
    reset_serials([serial])
2603
    uuids = set(uuid for (uuid, resource) in r)
2604
    serial, sub_quota, add_quota = set_sync_projects(exclude=uuids)
2605
    r = qh_add_quota(serial, sub_quota, add_quota)
2606
    if not r:
2607
        return serial
2608

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

    
2614
def post_sync_projects():
2615
    PROJECT_DEACTIVATED = ProjectMembership.PROJECT_DEACTIVATED
2616
    Q_ACTUALLY_ACCEPTED = ProjectMembership.Q_ACTUALLY_ACCEPTED
2617
    objs = Project.objects
2618

    
2619
    modified = objs.modified_projects().select_for_update()
2620
    for project in modified:
2621
        objects = project.projectmembership_set
2622
        q = objects.filter(Q_ACTUALLY_ACCEPTED & Q(is_pending=True))
2623
        memberships = list(q.select_for_update())
2624
        if not memberships:
2625
            project.is_modified = False
2626
            project.save()
2627

    
2628
    reactivating = objs.reactivating_projects().select_for_update()
2629
    for project in reactivating:
2630
        objects = project.projectmembership_set
2631
        q = objects.filter(Q(state=PROJECT_DEACTIVATED) | Q(is_pending=True))
2632
        memberships = list(q.select_for_update())
2633
        if not memberships:
2634
            project.reactivate()
2635
            project.save()
2636

    
2637
    deactivating = objs.deactivating_projects().select_for_update()
2638
    for project in deactivating:
2639
        objects = project.projectmembership_set
2640
        q = objects.filter(Q_ACTUALLY_ACCEPTED | Q(is_pending=True))
2641
        memberships = list(q.select_for_update())
2642
        if not memberships:
2643
            project.deactivate()
2644
            project.save()
2645

    
2646
    transaction.commit()
2647

    
2648
def sync_projects(sync=True, retries=3, retry_wait=1.0):
2649
    @with_lock(retries, retry_wait)
2650
    def _sync_projects(sync):
2651
        sync_finish_serials()
2652
        # Informative only -- no select_for_update()
2653
        pending = list(ProjectMembership.objects.filter(is_pending=True))
2654

    
2655
        projects_log = pre_sync_projects(sync)
2656
        if sync:
2657
            serial = do_sync_projects()
2658
            sync_finish_serials([serial])
2659
            post_sync_projects()
2660

    
2661
        return (pending, projects_log)
2662
    return _sync_projects(sync)
2663

    
2664
def all_users_quotas(users):
2665
    initial = {}
2666
    quotas = {}
2667
    info = {}
2668
    for user in users:
2669
        uuid = user.uuid
2670
        info[uuid] = user.email
2671
        init = user.initial_quotas()
2672
        initial[uuid] = init
2673
        quotas[user.uuid] = user.all_quotas(initial=init)
2674
    return initial, quotas, info
2675

    
2676
def sync_users(users, sync=True, retries=3, retry_wait=1.0):
2677
    @with_lock(retries, retry_wait)
2678
    def _sync_users(users, sync):
2679
        sync_finish_serials()
2680

    
2681
        existing, nonexisting = qh_check_users(users)
2682
        resources = get_resource_names()
2683
        qh_limits, qh_counters = qh_get_quotas(existing, resources)
2684
        astakos_initial, astakos_quotas, info = all_users_quotas(users)
2685

    
2686
        if sync:
2687
            r = register_users(nonexisting)
2688
            r = send_quotas(astakos_quotas)
2689

    
2690
        return (existing, nonexisting,
2691
                qh_limits, qh_counters,
2692
                astakos_initial, astakos_quotas, info)
2693
    return _sync_users(users, sync)
2694

    
2695
def sync_all_users(sync=True, retries=3, retry_wait=1.0):
2696
    users = AstakosUser.objects.verified()
2697
    return sync_users(users, sync, retries, retry_wait)
2698

    
2699
class ProjectMembershipHistory(models.Model):
2700
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2701
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2702

    
2703
    person  =   models.BigIntegerField()
2704
    project =   models.BigIntegerField()
2705
    date    =   models.DateField(auto_now_add=True)
2706
    reason  =   models.IntegerField()
2707
    serial  =   models.BigIntegerField()
2708

    
2709
### SIGNALS ###
2710
################
2711

    
2712
def create_astakos_user(u):
2713
    try:
2714
        AstakosUser.objects.get(user_ptr=u.pk)
2715
    except AstakosUser.DoesNotExist:
2716
        extended_user = AstakosUser(user_ptr_id=u.pk)
2717
        extended_user.__dict__.update(u.__dict__)
2718
        extended_user.save()
2719
        if not extended_user.has_auth_provider('local'):
2720
            extended_user.add_auth_provider('local')
2721
    except BaseException, e:
2722
        logger.exception(e)
2723

    
2724
def fix_superusers():
2725
    # Associate superusers with AstakosUser
2726
    admins = User.objects.filter(is_superuser=True)
2727
    for u in admins:
2728
        create_astakos_user(u)
2729

    
2730
def user_post_save(sender, instance, created, **kwargs):
2731
    if not created:
2732
        return
2733
    create_astakos_user(instance)
2734
post_save.connect(user_post_save, sender=User)
2735

    
2736
def astakosuser_post_save(sender, instance, created, **kwargs):
2737
    pass
2738

    
2739
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2740

    
2741
def resource_post_save(sender, instance, created, **kwargs):
2742
    pass
2743

    
2744
post_save.connect(resource_post_save, sender=Resource)
2745

    
2746
def renew_token(sender, instance, **kwargs):
2747
    if not instance.auth_token:
2748
        instance.renew_token()
2749
pre_save.connect(renew_token, sender=AstakosUser)
2750
pre_save.connect(renew_token, sender=Service)