Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 1b52192e

History | View | Annotate | Download (68.4 kB)

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

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

    
41
from datetime import datetime, timedelta
42
import base64
43
from urllib import quote
44
from random import randint
45
import os
46

    
47
from django.db import models, IntegrityError, transaction
48
from django.contrib.auth.models import User, UserManager, Group, Permission
49
from django.utils.translation import ugettext as _
50
from django.db.models.signals import pre_save, post_save
51
from django.contrib.contenttypes.models import ContentType
52

    
53
from django.db.models import Q, Max
54
from django.core.urlresolvers import reverse
55
from django.utils.http import int_to_base36
56
from django.contrib.auth.tokens import default_token_generator
57
from django.conf import settings
58
from django.utils.importlib import import_module
59
from django.utils.safestring import mark_safe
60

    
61
from synnefo.lib.utils import dict_merge
62

    
63
from astakos.im import settings as astakos_settings
64
from astakos.im import auth_providers as auth
65

    
66
import astakos.im.messages as astakos_messages
67
from snf_django.lib.db.managers import ForUpdateManager
68
from synnefo.lib.ordereddict import OrderedDict
69

    
70
from snf_django.lib.db.fields import intDecimalField
71
from synnefo.util.text import uenc, udec
72
from astakos.im import presentation
73

    
74
logger = logging.getLogger(__name__)
75

    
76
DEFAULT_CONTENT_TYPE = None
77
_content_type = None
78

    
79

    
80
def get_content_type():
81
    global _content_type
82
    if _content_type is not None:
83
        return _content_type
84

    
85
    try:
86
        content_type = ContentType.objects.get(app_label='im',
87
                                               model='astakosuser')
88
    except:
89
        content_type = DEFAULT_CONTENT_TYPE
90
    _content_type = content_type
91
    return content_type
92

    
93
inf = float('inf')
94

    
95

    
96
def generate_token():
97
    s = os.urandom(32)
98
    return base64.urlsafe_b64encode(s).rstrip('=')
99

    
100

    
101
def _partition_by(f, l):
102
    d = {}
103
    for x in l:
104
        group = f(x)
105
        group_l = d.get(group, [])
106
        group_l.append(x)
107
        d[group] = group_l
108
    return d
109

    
110

    
111
def first_of_group(f, l):
112
    Nothing = type("Nothing", (), {})
113
    last_group = Nothing
114
    d = {}
115
    for x in l:
116
        group = f(x)
117
        if group != last_group:
118
            last_group = group
119
            d[group] = x
120
    return d
121

    
122

    
123
class Component(models.Model):
124
    name = models.CharField(_('Name'), max_length=255, unique=True,
125
                            db_index=True)
126
    url = models.CharField(_('Component url'), max_length=1024, null=True,
127
                           help_text=_("URL the component is accessible from"))
128
    auth_token = models.CharField(_('Authentication Token'), max_length=64,
129
                                  null=True, blank=True, unique=True)
130
    auth_token_created = models.DateTimeField(_('Token creation date'),
131
                                              null=True)
132
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
133
                                              null=True)
134

    
135
    def renew_token(self, expiration_date=None):
136
        for i in range(10):
137
            new_token = generate_token()
138
            count = Component.objects.filter(auth_token=new_token).count()
139
            if count == 0:
140
                break
141
            continue
142
        else:
143
            raise ValueError('Could not generate a token')
144

    
145
        self.auth_token = new_token
146
        self.auth_token_created = datetime.now()
147
        if expiration_date:
148
            self.auth_token_expires = expiration_date
149
        else:
150
            self.auth_token_expires = None
151
        msg = 'Token renewed for component %s' % self.name
152
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
153

    
154
    def __str__(self):
155
        return self.name
156

    
157
    @classmethod
158
    def catalog(cls, orderfor=None):
159
        catalog = {}
160
        components = list(cls.objects.all())
161
        default_metadata = presentation.COMPONENTS
162
        metadata = {}
163

    
164
        for component in components:
165
            d = {'url': component.url,
166
                 'name': component.name}
167
            if component.name in default_metadata:
168
                metadata[component.name] = default_metadata.get(component.name)
169
                metadata[component.name].update(d)
170
            else:
171
                metadata[component.name] = d
172

    
173
        def component_by_order(s):
174
            return s[1].get('order')
175

    
176
        def component_by_dashboard_order(s):
177
            return s[1].get('dashboard').get('order')
178

    
179
        metadata = dict_merge(metadata,
180
                              astakos_settings.COMPONENTS_META)
181

    
182
        for component, info in metadata.iteritems():
183
            default_meta = presentation.component_defaults(component)
184
            base_meta = metadata.get(component, {})
185
            settings_meta = astakos_settings.COMPONENTS_META.get(component, {})
186
            component_meta = dict_merge(default_meta, base_meta)
187
            meta = dict_merge(component_meta, settings_meta)
188
            catalog[component] = meta
189

    
190
        order_key = component_by_order
191
        if orderfor == 'dashboard':
192
            order_key = component_by_dashboard_order
193

    
194
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
195
                                             key=order_key))
196
        return ordered_catalog
197

    
198

    
199
_presentation_data = {}
200

    
201

    
202
def get_presentation(resource):
203
    global _presentation_data
204
    resource_presentation = _presentation_data.get(resource, {})
205
    if not resource_presentation:
206
        resources_presentation = presentation.RESOURCES.get('resources', {})
207
        resource_presentation = resources_presentation.get(resource, {})
208
        _presentation_data[resource] = resource_presentation
209
    return resource_presentation
210

    
211

    
212
class Service(models.Model):
213
    component = models.ForeignKey(Component)
214
    name = models.CharField(max_length=255, unique=True)
215
    type = models.CharField(max_length=255)
216

    
217

    
218
class Endpoint(models.Model):
219
    service = models.ForeignKey(Service, related_name='endpoints')
220

    
221

    
222
class EndpointData(models.Model):
223
    endpoint = models.ForeignKey(Endpoint, related_name='data')
224
    key = models.CharField(max_length=255)
225
    value = models.CharField(max_length=1024)
226

    
227
    class Meta:
228
        unique_together = (('endpoint', 'key'),)
229

    
230

    
231
class Resource(models.Model):
232
    name = models.CharField(_('Name'), max_length=255, unique=True)
233
    desc = models.TextField(_('Description'), null=True)
234
    service_type = models.CharField(_('Type'), max_length=255)
235
    service_origin = models.CharField(max_length=255, db_index=True)
236
    unit = models.CharField(_('Unit'), null=True, max_length=255)
237
    uplimit = intDecimalField(default=0)
238
    allow_in_projects = models.BooleanField(default=True)
239

    
240
    objects = ForUpdateManager()
241

    
242
    def __str__(self):
243
        return self.name
244

    
245
    def full_name(self):
246
        return str(self)
247

    
248
    def get_info(self):
249
        return {'service': self.service_origin,
250
                'description': self.desc,
251
                'unit': self.unit,
252
                'allow_in_projects': self.allow_in_projects,
253
                }
254

    
255
    @property
256
    def group(self):
257
        default = self.name
258
        return get_presentation(str(self)).get('group', default)
259

    
260
    @property
261
    def help_text(self):
262
        default = "%s resource" % self.name
263
        return get_presentation(str(self)).get('help_text', default)
264

    
265
    @property
266
    def help_text_input_each(self):
267
        default = "%s resource" % self.name
268
        return get_presentation(str(self)).get('help_text_input_each', default)
269

    
270
    @property
271
    def is_abbreviation(self):
272
        return get_presentation(str(self)).get('is_abbreviation', False)
273

    
274
    @property
275
    def report_desc(self):
276
        default = "%s resource" % self.name
277
        return get_presentation(str(self)).get('report_desc', default)
278

    
279
    @property
280
    def placeholder(self):
281
        return get_presentation(str(self)).get('placeholder', self.unit)
282

    
283
    @property
284
    def verbose_name(self):
285
        return get_presentation(str(self)).get('verbose_name', self.name)
286

    
287
    @property
288
    def display_name(self):
289
        name = self.verbose_name
290
        if self.is_abbreviation:
291
            name = name.upper()
292
        return name
293

    
294
    @property
295
    def pluralized_display_name(self):
296
        if not self.unit:
297
            return '%ss' % self.display_name
298
        return self.display_name
299

    
300

    
301
def get_resource_names():
302
    _RESOURCE_NAMES = []
303
    resources = Resource.objects.select_related('service').all()
304
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
305
    return _RESOURCE_NAMES
306

    
307

    
308
class AstakosUserManager(UserManager):
309

    
310
    def get_auth_provider_user(self, provider, **kwargs):
311
        """
312
        Retrieve AstakosUser instance associated with the specified third party
313
        id.
314
        """
315
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
316
                          kwargs.iteritems()))
317
        return self.get(auth_providers__module=provider, **kwargs)
318

    
319
    def get_by_email(self, email):
320
        return self.get(email=email)
321

    
322
    def get_by_identifier(self, email_or_username, **kwargs):
323
        try:
324
            return self.get(email__iexact=email_or_username, **kwargs)
325
        except AstakosUser.DoesNotExist:
326
            return self.get(username__iexact=email_or_username, **kwargs)
327

    
328
    def user_exists(self, email_or_username, **kwargs):
329
        qemail = Q(email__iexact=email_or_username)
330
        qusername = Q(username__iexact=email_or_username)
331
        qextra = Q(**kwargs)
332
        return self.filter((qemail | qusername) & qextra).exists()
333

    
334
    def verified_user_exists(self, email_or_username):
335
        return self.user_exists(email_or_username, email_verified=True)
336

    
337
    def verified(self):
338
        return self.filter(email_verified=True)
339

    
340
    def uuid_catalog(self, l=None):
341
        """
342
        Returns a uuid to username mapping for the uuids appearing in l.
343
        If l is None returns the mapping for all existing users.
344
        """
345
        q = self.filter(uuid__in=l) if l is not None else self
346
        return dict(q.values_list('uuid', 'username'))
347

    
348
    def displayname_catalog(self, l=None):
349
        """
350
        Returns a username to uuid mapping for the usernames appearing in l.
351
        If l is None returns the mapping for all existing users.
352
        """
353
        if l is not None:
354
            lmap = dict((x.lower(), x) for x in l)
355
            q = self.filter(username__in=lmap.keys())
356
            values = ((lmap[n], u)
357
                      for n, u in q.values_list('username', 'uuid'))
358
        else:
359
            q = self
360
            values = self.values_list('username', 'uuid')
361
        return dict(values)
362

    
363

    
364
class AstakosUser(User):
365
    """
366
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
367
    """
368
    affiliation = models.CharField(_('Affiliation'), max_length=255,
369
                                   blank=True, null=True)
370

    
371
    #for invitations
372
    user_level = astakos_settings.DEFAULT_USER_LEVEL
373
    level = models.IntegerField(_('Inviter level'), default=user_level)
374
    invitations = models.IntegerField(
375
        _('Invitations left'),
376
        default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
377

    
378
    auth_token = models.CharField(
379
        _('Authentication Token'),
380
        max_length=64,
381
        unique=True,
382
        null=True,
383
        blank=True,
384
        help_text=_('Renew your authentication '
385
                    'token. Make sure to set the new '
386
                    'token in any client you may be '
387
                    'using, to preserve its '
388
                    'functionality.'))
389
    auth_token_created = models.DateTimeField(_('Token creation date'),
390
                                              null=True)
391
    auth_token_expires = models.DateTimeField(
392
        _('Token expiration date'), null=True)
393

    
394
    updated = models.DateTimeField(_('Update date'))
395

    
396
    # Arbitrary text to identify the reason user got deactivated.
397
    # To be used as a reference from administrators.
398
    deactivated_reason = models.TextField(
399
        _('Reason the user was disabled for'),
400
        default=None, null=True)
401
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
402
                                          blank=True)
403

    
404
    has_credits = models.BooleanField(_('Has credits?'), default=False)
405

    
406
    # this is set to True when user profile gets updated for the first time
407
    is_verified = models.BooleanField(_('Is verified?'), default=False)
408

    
409
    # user email is verified
410
    email_verified = models.BooleanField(_('Email verified?'), default=False)
411

    
412
    # unique string used in user email verification url
413
    verification_code = models.CharField(max_length=255, null=True,
414
                                         blank=False, unique=True)
415

    
416
    # date user email verified
417
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
418
                                       blank=True)
419

    
420
    # email verification notice was sent to the user at this time
421
    activation_sent = models.DateTimeField(_('Activation sent date'),
422
                                           null=True, blank=True)
423

    
424
    # user got rejected during moderation process
425
    is_rejected = models.BooleanField(_('Account rejected'),
426
                                      default=False)
427
    # reason user got rejected
428
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
429
                                       blank=True)
430
    # moderation status
431
    moderated = models.BooleanField(_('User moderated'), default=False)
432
    # date user moderated (either accepted or rejected)
433
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
434
                                        blank=True, null=True)
435
    # a snapshot of user instance the time got moderated
436
    moderated_data = models.TextField(null=True, default=None, blank=True)
437
    # a string which identifies how the user got moderated
438
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
439
                                       default=None, null=True, blank=True)
440
    # the email used to accept the user
441
    accepted_email = models.EmailField(null=True, default=None, blank=True)
442

    
443
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
444
                                           default=False)
445
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
446
                                             null=True, blank=True)
447
    # permanent unique user identifier
448
    uuid = models.CharField(max_length=255, null=True, blank=False,
449
                            unique=True)
450

    
451
    policy = models.ManyToManyField(
452
        Resource, null=True, through='AstakosUserQuota')
453

    
454
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
455
                                          default=False, db_index=True)
456

    
457
    objects = AstakosUserManager()
458
    forupdate = ForUpdateManager()
459

    
460
    def __init__(self, *args, **kwargs):
461
        super(AstakosUser, self).__init__(*args, **kwargs)
462
        if not self.id:
463
            self.is_active = False
464

    
465
    @property
466
    def realname(self):
467
        return '%s %s' % (self.first_name, self.last_name)
468

    
469
    @property
470
    def log_display(self):
471
        """
472
        Should be used in all logger.* calls that refer to a user so that
473
        user display is consistent across log entries.
474
        """
475
        return '%s::%s' % (self.uuid, self.email)
476

    
477
    @realname.setter
478
    def realname(self, value):
479
        parts = value.split(' ')
480
        if len(parts) == 2:
481
            self.first_name = parts[0]
482
            self.last_name = parts[1]
483
        else:
484
            self.last_name = parts[0]
485

    
486
    def add_permission(self, pname):
487
        if self.has_perm(pname):
488
            return
489
        p, created = Permission.objects.get_or_create(
490
            codename=pname,
491
            name=pname.capitalize(),
492
            content_type=get_content_type())
493
        self.user_permissions.add(p)
494

    
495
    def remove_permission(self, pname):
496
        if self.has_perm(pname):
497
            return
498
        p = Permission.objects.get(codename=pname,
499
                                   content_type=get_content_type())
500
        self.user_permissions.remove(p)
501

    
502
    def add_group(self, gname):
503
        group, _ = Group.objects.get_or_create(name=gname)
504
        self.groups.add(group)
505

    
506
    def is_project_admin(self, application_id=None):
507
        return self.uuid in astakos_settings.PROJECT_ADMINS
508

    
509
    @property
510
    def invitation(self):
511
        try:
512
            return Invitation.objects.get(username=self.email)
513
        except Invitation.DoesNotExist:
514
            return None
515

    
516
    @property
517
    def policies(self):
518
        return self.astakosuserquota_set.select_related().all()
519

    
520
    def get_resource_policy(self, resource):
521
        resource = Resource.objects.get(name=resource)
522
        default_capacity = resource.uplimit
523
        try:
524
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
525
            return policy, default_capacity
526
        except AstakosUserQuota.DoesNotExist:
527
            return None, default_capacity
528

    
529
    def update_uuid(self):
530
        while not self.uuid:
531
            uuid_val = str(uuid.uuid4())
532
            try:
533
                AstakosUser.objects.get(uuid=uuid_val)
534
            except AstakosUser.DoesNotExist, e:
535
                self.uuid = uuid_val
536
        return self.uuid
537

    
538
    def save(self, update_timestamps=True, **kwargs):
539
        if update_timestamps:
540
            if not self.id:
541
                self.date_joined = datetime.now()
542
            self.updated = datetime.now()
543

    
544
        self.update_uuid()
545

    
546
        if not self.verification_code:
547
            self.renew_verification_code()
548

    
549
        # username currently matches email
550
        if self.username != self.email.lower():
551
            self.username = self.email.lower()
552

    
553
        super(AstakosUser, self).save(**kwargs)
554

    
555
    def renew_verification_code(self):
556
        self.verification_code = str(uuid.uuid4())
557
        logger.info("Verification code renewed for %s" % self.log_display)
558

    
559
    def renew_token(self, flush_sessions=False, current_key=None):
560
        for i in range(10):
561
            new_token = generate_token()
562
            count = AstakosUser.objects.filter(auth_token=new_token).count()
563
            if count == 0:
564
                break
565
            continue
566
        else:
567
            raise ValueError('Could not generate a token')
568

    
569
        self.auth_token = new_token
570
        self.auth_token_created = datetime.now()
571
        self.auth_token_expires = self.auth_token_created + \
572
            timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
573
        if flush_sessions:
574
            self.flush_sessions(current_key)
575
        msg = 'Token renewed for %s' % self.log_display
576
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
577

    
578
    def token_expired(self):
579
        return self.auth_token_expires < datetime.now()
580

    
581
    def flush_sessions(self, current_key=None):
582
        q = self.sessions
583
        if current_key:
584
            q = q.exclude(session_key=current_key)
585

    
586
        keys = q.values_list('session_key', flat=True)
587
        if keys:
588
            msg = 'Flushing sessions: %s' % ','.join(keys)
589
            logger.log(astakos_settings.LOGGING_LEVEL, msg, [])
590
        engine = import_module(settings.SESSION_ENGINE)
591
        for k in keys:
592
            s = engine.SessionStore(k)
593
            s.flush()
594

    
595
    def __unicode__(self):
596
        return '%s (%s)' % (self.realname, self.email)
597

    
598
    def conflicting_email(self):
599
        q = AstakosUser.objects.exclude(username=self.username)
600
        q = q.filter(email__iexact=self.email)
601
        if q.count() != 0:
602
            return True
603
        return False
604

    
605
    def email_change_is_pending(self):
606
        return self.emailchanges.count() > 0
607

    
608
    @property
609
    def status_display(self):
610
        msg = ""
611
        append = None
612
        if self.is_active:
613
            msg = "Accepted/Active"
614
        if self.is_rejected:
615
            msg = "Rejected"
616
            if self.rejected_reason:
617
                msg += " (%s)" % self.rejected_reason
618
        if not self.email_verified:
619
            msg = "Pending email verification"
620
        if not self.moderated:
621
            msg = "Pending moderation"
622
        if not self.is_active and self.email_verified:
623
            msg = "Accepted/Inactive"
624
            if self.deactivated_reason:
625
                msg += " (%s)" % (self.deactivated_reason)
626

    
627
        if self.moderated and not self.is_rejected:
628
            if self.accepted_policy == 'manual':
629
                msg += " (manually accepted)"
630
            else:
631
                msg += " (accepted policy: %s)" % \
632
                    self.accepted_policy
633
        return msg
634

    
635
    @property
636
    def signed_terms(self):
637
        term = get_latest_terms()
638
        if not term:
639
            return True
640
        if not self.has_signed_terms:
641
            return False
642
        if not self.date_signed_terms:
643
            return False
644
        if self.date_signed_terms < term.date:
645
            self.has_signed_terms = False
646
            self.date_signed_terms = None
647
            self.save()
648
            return False
649
        return True
650

    
651
    def set_invitations_level(self):
652
        """
653
        Update user invitation level
654
        """
655
        level = self.invitation.inviter.level + 1
656
        self.level = level
657
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
658

    
659
    def can_change_password(self):
660
        return self.has_auth_provider('local', auth_backend='astakos')
661

    
662
    def can_change_email(self):
663
        if not self.has_auth_provider('local'):
664
            return True
665

    
666
        local = self.get_auth_provider('local')._instance
667
        return local.auth_backend == 'astakos'
668

    
669
    # Auth providers related methods
670
    def get_auth_provider(self, module=None, identifier=None, **filters):
671
        if not module:
672
            return self.auth_providers.active()[0].settings
673

    
674
        params = {'module': module}
675
        if identifier:
676
            params['identifier'] = identifier
677
        params.update(filters)
678
        return self.auth_providers.active().get(**params).settings
679

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

    
684
    def get_required_providers(self, **kwargs):
685
        return auth.REQUIRED_PROVIDERS.keys()
686

    
687
    def missing_required_providers(self):
688
        required = self.get_required_providers()
689
        missing = []
690
        for provider in required:
691
            if not self.has_auth_provider(provider):
692
                missing.append(auth.get_provider(provider, self))
693
        return missing
694

    
695
    def get_available_auth_providers(self, **filters):
696
        """
697
        Returns a list of providers available for add by the user.
698
        """
699
        modules = astakos_settings.IM_MODULES
700
        providers = []
701
        for p in modules:
702
            providers.append(auth.get_provider(p, self))
703
        available = []
704

    
705
        for p in providers:
706
            if p.get_add_policy:
707
                available.append(p)
708
        return available
709

    
710
    def get_disabled_auth_providers(self, **filters):
711
        providers = self.get_auth_providers(**filters)
712
        disabled = []
713
        for p in providers:
714
            if not p.get_login_policy:
715
                disabled.append(p)
716
        return disabled
717

    
718
    def get_enabled_auth_providers(self, **filters):
719
        providers = self.get_auth_providers(**filters)
720
        enabled = []
721
        for p in providers:
722
            if p.get_login_policy:
723
                enabled.append(p)
724
        return enabled
725

    
726
    def get_auth_providers(self, **filters):
727
        providers = []
728
        for provider in self.auth_providers.active(**filters):
729
            if provider.settings.module_enabled:
730
                providers.append(provider.settings)
731

    
732
        modules = astakos_settings.IM_MODULES
733

    
734
        def key(p):
735
            if not p.module in modules:
736
                return 100
737
            return modules.index(p.module)
738

    
739
        providers = sorted(providers, key=key)
740
        return providers
741

    
742
    # URL methods
743
    @property
744
    def auth_providers_display(self):
745
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
746
                         self.get_enabled_auth_providers()])
747

    
748
    def add_auth_provider(self, module='local', identifier=None, **params):
749
        provider = auth.get_provider(module, self, identifier, **params)
750
        provider.add_to_user()
751

    
752
    def get_resend_activation_url(self):
753
        return reverse('send_activation', kwargs={'user_id': self.pk})
754

    
755
    def get_activation_url(self, nxt=False):
756
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
757
                              quote(self.verification_code))
758
        if nxt:
759
            url += "&next=%s" % quote(nxt)
760
        return url
761

    
762
    def get_password_reset_url(self, token_generator=default_token_generator):
763
        return reverse('astakos.im.views.target.local.password_reset_confirm',
764
                       kwargs={'uidb36': int_to_base36(self.id),
765
                               'token': token_generator.make_token(self)})
766

    
767
    def get_inactive_message(self, provider_module, identifier=None):
768
        provider = self.get_auth_provider(provider_module, identifier)
769

    
770
        msg_extra = ''
771
        message = ''
772

    
773
        msg_inactive = provider.get_account_inactive_msg
774
        msg_pending = provider.get_pending_activation_msg
775
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
776
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
777
        msg_pending_mod = provider.get_pending_moderation_msg
778
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
779
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
780

    
781
        if not self.email_verified:
782
            message = msg_pending
783
            url = self.get_resend_activation_url()
784
            msg_extra = msg_pending_help + \
785
                u' ' + \
786
                '<a href="%s">%s?</a>' % (url, msg_resend)
787
        else:
788
            if not self.moderated:
789
                message = msg_pending_mod
790
            else:
791
                if self.is_rejected:
792
                    message = msg_rejected
793
                else:
794
                    message = msg_inactive
795

    
796
        return mark_safe(message + u' ' + msg_extra)
797

    
798
    def owns_application(self, application):
799
        return application.owner == self
800

    
801
    def owns_project(self, project):
802
        return project.application.owner == self
803

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

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

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

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

    
838

    
839
class AstakosUserAuthProviderManager(models.Manager):
840

    
841
    def active(self, **filters):
842
        return self.filter(active=True, **filters)
843

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

    
853
    def unverified(self, provider, **filters):
854
        try:
855
            return self.get(module=provider, user__email_verified=False,
856
                            **filters).settings
857
        except AstakosUserAuthProvider.DoesNotExist:
858
            return None
859

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

    
867

    
868
class AuthProviderPolicyProfileManager(models.Manager):
869

    
870
    def active(self):
871
        return self.filter(active=True)
872

    
873
    def for_user(self, user, provider):
874
        policies = {}
875
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
876
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
877
        exclusive_q = exclusive_q1 | exclusive_q2
878

    
879
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
880
            policies.update(profile.policies)
881

    
882
        user_groups = user.groups.all().values('pk')
883
        for profile in self.active().filter(groups__in=user_groups).filter(
884
                exclusive_q):
885
            policies.update(profile.policies)
886
        return policies
887

    
888
    def add_policy(self, name, provider, group_or_user, exclusive=False,
889
                   **policies):
890
        is_group = isinstance(group_or_user, Group)
891
        profile, created = self.get_or_create(name=name, provider=provider,
892
                                              is_exclusive=exclusive)
893
        profile.is_exclusive = exclusive
894
        profile.save()
895
        if is_group:
896
            profile.groups.add(group_or_user)
897
        else:
898
            profile.users.add(group_or_user)
899
        profile.set_policies(policies)
900
        profile.save()
901
        return profile
902

    
903

    
904
class AuthProviderPolicyProfile(models.Model):
905
    name = models.CharField(_('Name'), max_length=255, blank=False,
906
                            null=False, db_index=True)
907
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
908
                                null=False)
909

    
910
    # apply policies to all providers excluding the one set in provider field
911
    is_exclusive = models.BooleanField(default=False)
912

    
913
    policy_add = models.NullBooleanField(null=True, default=None)
914
    policy_remove = models.NullBooleanField(null=True, default=None)
915
    policy_create = models.NullBooleanField(null=True, default=None)
916
    policy_login = models.NullBooleanField(null=True, default=None)
917
    policy_limit = models.IntegerField(null=True, default=None)
918
    policy_required = models.NullBooleanField(null=True, default=None)
919
    policy_automoderate = models.NullBooleanField(null=True, default=None)
920
    policy_switch = models.NullBooleanField(null=True, default=None)
921

    
922
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
923
                     'automoderate')
924

    
925
    priority = models.IntegerField(null=False, default=1)
926
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
927
    users = models.ManyToManyField(AstakosUser,
928
                                   related_name='authpolicy_profiles')
929
    active = models.BooleanField(default=True)
930

    
931
    objects = AuthProviderPolicyProfileManager()
932

    
933
    class Meta:
934
        ordering = ['priority']
935

    
936
    @property
937
    def policies(self):
938
        policies = {}
939
        for pkey in self.POLICY_FIELDS:
940
            value = getattr(self, 'policy_%s' % pkey, None)
941
            if value is None:
942
                continue
943
            policies[pkey] = value
944
        return policies
945

    
946
    def set_policies(self, policies_dict):
947
        for key, value in policies_dict.iteritems():
948
            if key in self.POLICY_FIELDS:
949
                setattr(self, 'policy_%s' % key, value)
950
        return self.policies
951

    
952

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

    
971
    objects = AstakosUserAuthProviderManager()
972

    
973
    class Meta:
974
        unique_together = (('identifier', 'module', 'user'), )
975
        ordering = ('module', 'created')
976

    
977
    def __init__(self, *args, **kwargs):
978
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
979
        try:
980
            self.info = json.loads(self.info_data)
981
            if not self.info:
982
                self.info = {}
983
        except Exception, e:
984
            self.info = {}
985

    
986
        for key, value in self.info.iteritems():
987
            setattr(self, 'info_%s' % key, value)
988

    
989
    @property
990
    def settings(self):
991
        extra_data = {}
992

    
993
        info_data = {}
994
        if self.info_data:
995
            info_data = json.loads(self.info_data)
996

    
997
        extra_data['info'] = info_data
998

    
999
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
1000
            extra_data[key] = getattr(self, key)
1001

    
1002
        extra_data['instance'] = self
1003
        return auth.get_provider(self.module, self.user,
1004
                                 self.identifier, **extra_data)
1005

    
1006
    def __repr__(self):
1007
        return '<AstakosUserAuthProvider %s:%s>' % (
1008
            self.module, self.identifier)
1009

    
1010
    def __unicode__(self):
1011
        if self.identifier:
1012
            return "%s:%s" % (self.module, self.identifier)
1013
        if self.auth_backend:
1014
            return "%s:%s" % (self.module, self.auth_backend)
1015
        return self.module
1016

    
1017
    def save(self, *args, **kwargs):
1018
        self.info_data = json.dumps(self.info)
1019
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1020

    
1021

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

    
1049
    update_or_create = _update_or_create
1050

    
1051

    
1052
class AstakosUserQuota(models.Model):
1053
    objects = ExtendedManager()
1054
    capacity = intDecimalField()
1055
    resource = models.ForeignKey(Resource)
1056
    user = models.ForeignKey(AstakosUser)
1057

    
1058
    class Meta:
1059
        unique_together = ("resource", "user")
1060

    
1061

    
1062
class ApprovalTerms(models.Model):
1063
    """
1064
    Model for approval terms
1065
    """
1066

    
1067
    date = models.DateTimeField(
1068
        _('Issue date'), db_index=True, auto_now_add=True)
1069
    location = models.CharField(_('Terms location'), max_length=255)
1070

    
1071

    
1072
class Invitation(models.Model):
1073
    """
1074
    Model for registring invitations
1075
    """
1076
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1077
                                null=True)
1078
    realname = models.CharField(_('Real name'), max_length=255)
1079
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1080
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1081
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1082
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1083
    consumed = models.DateTimeField(_('Consumption date'),
1084
                                    null=True, blank=True)
1085

    
1086
    def __init__(self, *args, **kwargs):
1087
        super(Invitation, self).__init__(*args, **kwargs)
1088
        if not self.id:
1089
            self.code = _generate_invitation_code()
1090

    
1091
    def consume(self):
1092
        self.is_consumed = True
1093
        self.consumed = datetime.now()
1094
        self.save()
1095

    
1096
    def __unicode__(self):
1097
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1098

    
1099

    
1100
class EmailChangeManager(models.Manager):
1101

    
1102
    @transaction.commit_on_success
1103
    def change_email(self, activation_key):
1104
        """
1105
        Validate an activation key and change the corresponding
1106
        ``User`` if valid.
1107

1108
        If the key is valid and has not expired, return the ``User``
1109
        after activating.
1110

1111
        If the key is not valid or has expired, return ``None``.
1112

1113
        If the key is valid but the ``User`` is already active,
1114
        return ``None``.
1115

1116
        After successful email change the activation record is deleted.
1117

1118
        Throws ValueError if there is already
1119
        """
1120
        try:
1121
            email_change = self.model.objects.get(
1122
                activation_key=activation_key)
1123
            if email_change.activation_key_expired():
1124
                email_change.delete()
1125
                raise EmailChange.DoesNotExist
1126
            # is there an active user with this address?
1127
            try:
1128
                AstakosUser.objects.get(
1129
                    email__iexact=email_change.new_email_address)
1130
            except AstakosUser.DoesNotExist:
1131
                pass
1132
            else:
1133
                raise ValueError(_('The new email address is reserved.'))
1134
            # update user
1135
            user = AstakosUser.objects.get(pk=email_change.user_id)
1136
            old_email = user.email
1137
            user.email = email_change.new_email_address
1138
            user.save()
1139
            email_change.delete()
1140
            msg = "User %s changed email from %s to %s" % (user.log_display,
1141
                                                           old_email,
1142
                                                           user.email)
1143
            logger.log(astakos_settings.LOGGING_LEVEL, msg)
1144
            return user
1145
        except EmailChange.DoesNotExist:
1146
            raise ValueError(_('Invalid activation key.'))
1147

    
1148

    
1149
class EmailChange(models.Model):
1150
    new_email_address = models.EmailField(
1151
        _(u'new e-mail address'),
1152
        help_text=_('Provide a new email address. Until you verify the new '
1153
                    'address by following the activation link that will be '
1154
                    'sent to it, your old email address will remain active.'))
1155
    user = models.ForeignKey(
1156
        AstakosUser, unique=True, related_name='emailchanges')
1157
    requested_at = models.DateTimeField(auto_now_add=True)
1158
    activation_key = models.CharField(
1159
        max_length=40, unique=True, db_index=True)
1160

    
1161
    objects = EmailChangeManager()
1162

    
1163
    def get_url(self):
1164
        return reverse('email_change_confirm',
1165
                       kwargs={'activation_key': self.activation_key})
1166

    
1167
    def activation_key_expired(self):
1168
        expiration_date = timedelta(
1169
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1170
        return self.requested_at + expiration_date < datetime.now()
1171

    
1172

    
1173
class AdditionalMail(models.Model):
1174
    """
1175
    Model for registring invitations
1176
    """
1177
    owner = models.ForeignKey(AstakosUser)
1178
    email = models.EmailField()
1179

    
1180

    
1181
def _generate_invitation_code():
1182
    while True:
1183
        code = randint(1, 2L ** 63 - 1)
1184
        try:
1185
            Invitation.objects.get(code=code)
1186
            # An invitation with this code already exists, try again
1187
        except Invitation.DoesNotExist:
1188
            return code
1189

    
1190

    
1191
def get_latest_terms():
1192
    try:
1193
        term = ApprovalTerms.objects.order_by('-id')[0]
1194
        return term
1195
    except IndexError:
1196
        pass
1197
    return None
1198

    
1199

    
1200
class PendingThirdPartyUser(models.Model):
1201
    """
1202
    Model for registring successful third party user authentications
1203
    """
1204
    third_party_identifier = models.CharField(
1205
        _('Third-party identifier'), max_length=255, null=True, blank=True)
1206
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1207
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1208
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1209
                                  null=True)
1210
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1211
                                 null=True)
1212
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1213
                                   null=True)
1214
    username = models.CharField(
1215
        _('username'), max_length=30, unique=True,
1216
        help_text=_("Required. 30 characters or fewer. "
1217
                    "Letters, numbers and @/./+/-/_ characters"))
1218
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1219
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1220
    info = models.TextField(default="", null=True, blank=True)
1221

    
1222
    class Meta:
1223
        unique_together = ("provider", "third_party_identifier")
1224

    
1225
    def get_user_instance(self):
1226
        """
1227
        Create a new AstakosUser instance based on details provided when user
1228
        initially signed up.
1229
        """
1230
        d = copy.copy(self.__dict__)
1231
        d.pop('_state', None)
1232
        d.pop('id', None)
1233
        d.pop('token', None)
1234
        d.pop('created', None)
1235
        d.pop('info', None)
1236
        d.pop('affiliation', None)
1237
        d.pop('provider', None)
1238
        d.pop('third_party_identifier', None)
1239
        user = AstakosUser(**d)
1240

    
1241
        return user
1242

    
1243
    @property
1244
    def realname(self):
1245
        return '%s %s' % (self.first_name, self.last_name)
1246

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

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

    
1267
    def generate_token(self):
1268
        self.password = self.third_party_identifier
1269
        self.last_login = datetime.now()
1270
        self.token = default_token_generator.make_token(self)
1271

    
1272
    def existing_user(self):
1273
        return AstakosUser.objects.filter(
1274
            auth_providers__module=self.provider,
1275
            auth_providers__identifier=self.third_party_identifier)
1276

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

    
1285

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

    
1290

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

    
1296
    objects = ForUpdateManager()
1297

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

    
1301

    
1302
### PROJECTS ###
1303
################
1304

    
1305
class Chain(models.Model):
1306
    chain = models.AutoField(primary_key=True)
1307
    objects = ForUpdateManager()
1308

    
1309
    def __str__(self):
1310
        return "%s" % (self.chain,)
1311

    
1312

    
1313
def new_chain():
1314
    c = Chain.objects.create()
1315
    return c
1316

    
1317

    
1318
class ProjectApplicationManager(ForUpdateManager):
1319
    pass
1320

    
1321

    
1322
class ProjectApplication(models.Model):
1323
    applicant = models.ForeignKey(
1324
        AstakosUser,
1325
        related_name='projects_applied',
1326
        db_index=True)
1327

    
1328
    PENDING = 0
1329
    APPROVED = 1
1330
    REPLACED = 2
1331
    DENIED = 3
1332
    DISMISSED = 4
1333
    CANCELLED = 5
1334

    
1335
    state = models.IntegerField(default=PENDING,
1336
                                db_index=True)
1337
    owner = models.ForeignKey(
1338
        AstakosUser,
1339
        related_name='projects_owned',
1340
        db_index=True)
1341
    chain = models.ForeignKey('Project',
1342
                              related_name='chained_apps',
1343
                              db_column='chain')
1344
    name = models.CharField(max_length=80)
1345
    homepage = models.URLField(max_length=255, null=True,
1346
                               verify_exists=False)
1347
    description = models.TextField(null=True, blank=True)
1348
    start_date = models.DateTimeField(null=True, blank=True)
1349
    end_date = models.DateTimeField()
1350
    member_join_policy = models.IntegerField()
1351
    member_leave_policy = models.IntegerField()
1352
    limit_on_members_number = models.PositiveIntegerField(null=True)
1353
    resource_grants = models.ManyToManyField(
1354
        Resource,
1355
        null=True,
1356
        blank=True,
1357
        through='ProjectResourceGrant')
1358
    comments = models.TextField(null=True, blank=True)
1359
    issue_date = models.DateTimeField(auto_now_add=True)
1360
    response_date = models.DateTimeField(null=True, blank=True)
1361
    response = models.TextField(null=True, blank=True)
1362
    response_actor = models.ForeignKey(AstakosUser, null=True,
1363
                                       related_name='responded_apps')
1364
    waive_date = models.DateTimeField(null=True, blank=True)
1365
    waive_reason = models.TextField(null=True, blank=True)
1366
    waive_actor = models.ForeignKey(AstakosUser, null=True,
1367
                                    related_name='waived_apps')
1368

    
1369
    objects = ProjectApplicationManager()
1370

    
1371
    # Compiled queries
1372
    Q_PENDING = Q(state=PENDING)
1373
    Q_APPROVED = Q(state=APPROVED)
1374
    Q_DENIED = Q(state=DENIED)
1375

    
1376
    class Meta:
1377
        unique_together = ("chain", "id")
1378

    
1379
    def __unicode__(self):
1380
        return "%s applied by %s" % (self.name, self.applicant)
1381

    
1382
    # TODO: Move to a more suitable place
1383
    APPLICATION_STATE_DISPLAY = {
1384
        PENDING:   _('Pending review'),
1385
        APPROVED:  _('Approved'),
1386
        REPLACED:  _('Replaced'),
1387
        DENIED:    _('Denied'),
1388
        DISMISSED: _('Dismissed'),
1389
        CANCELLED: _('Cancelled')
1390
    }
1391

    
1392
    @property
1393
    def log_display(self):
1394
        return "application %s (%s) for project %s" % (
1395
            self.id, self.name, self.chain)
1396

    
1397
    def state_display(self):
1398
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1399

    
1400
    @property
1401
    def grants(self):
1402
        return self.projectresourcegrant_set.values('member_capacity',
1403
                                                    'resource__name')
1404

    
1405
    @property
1406
    def resource_policies(self):
1407
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1408

    
1409
    def is_modification(self):
1410
        # if self.state != self.PENDING:
1411
        #     return False
1412
        parents = self.chained_applications().filter(id__lt=self.id)
1413
        parents = parents.filter(state__in=[self.APPROVED])
1414
        return parents.count() > 0
1415

    
1416
    def chained_applications(self):
1417
        return ProjectApplication.objects.filter(chain=self.chain)
1418

    
1419
    def denied_modifications(self):
1420
        q = self.chained_applications()
1421
        q = q.filter(Q(state=self.DENIED))
1422
        q = q.filter(~Q(id=self.id))
1423
        return q
1424

    
1425
    def last_denied(self):
1426
        try:
1427
            return self.denied_modifications().order_by('-id')[0]
1428
        except IndexError:
1429
            return None
1430

    
1431
    def has_denied_modifications(self):
1432
        return bool(self.last_denied())
1433

    
1434
    def can_cancel(self):
1435
        return self.state == self.PENDING
1436

    
1437
    def cancel(self, actor=None, reason=None):
1438
        if not self.can_cancel():
1439
            m = _("cannot cancel: application '%s' in state '%s'") % (
1440
                self.id, self.state)
1441
            raise AssertionError(m)
1442

    
1443
        self.state = self.CANCELLED
1444
        self.waive_date = datetime.now()
1445
        self.waive_reason = reason
1446
        self.waive_actor = actor
1447
        self.save()
1448

    
1449
    def can_dismiss(self):
1450
        return self.state == self.DENIED
1451

    
1452
    def dismiss(self, actor=None, reason=None):
1453
        if not self.can_dismiss():
1454
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1455
                self.id, self.state)
1456
            raise AssertionError(m)
1457

    
1458
        self.state = self.DISMISSED
1459
        self.waive_date = datetime.now()
1460
        self.waive_reason = reason
1461
        self.waive_actor = actor
1462
        self.save()
1463

    
1464
    def can_deny(self):
1465
        return self.state == self.PENDING
1466

    
1467
    def deny(self, actor=None, reason=None):
1468
        if not self.can_deny():
1469
            m = _("cannot deny: application '%s' in state '%s'") % (
1470
                self.id, self.state)
1471
            raise AssertionError(m)
1472

    
1473
        self.state = self.DENIED
1474
        self.response_date = datetime.now()
1475
        self.response = reason
1476
        self.response_actor = actor
1477
        self.save()
1478

    
1479
    def can_approve(self):
1480
        return self.state == self.PENDING
1481

    
1482
    def approve(self, actor=None, reason=None):
1483
        if not self.can_approve():
1484
            m = _("cannot approve: project '%s' in state '%s'") % (
1485
                self.name, self.state)
1486
            raise AssertionError(m)  # invalid argument
1487

    
1488
        now = datetime.now()
1489
        self.state = self.APPROVED
1490
        self.response_date = now
1491
        self.response = reason
1492
        self.response_actor = actor
1493
        self.save()
1494

    
1495
    @property
1496
    def member_join_policy_display(self):
1497
        policy = self.member_join_policy
1498
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1499

    
1500
    @property
1501
    def member_leave_policy_display(self):
1502
        policy = self.member_leave_policy
1503
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1504

    
1505

    
1506
class ProjectResourceGrant(models.Model):
1507

    
1508
    resource = models.ForeignKey(Resource)
1509
    project_application = models.ForeignKey(ProjectApplication,
1510
                                            null=True)
1511
    project_capacity = intDecimalField(null=True)
1512
    member_capacity = intDecimalField(default=0)
1513

    
1514
    objects = ExtendedManager()
1515

    
1516
    class Meta:
1517
        unique_together = ("resource", "project_application")
1518

    
1519
    def display_member_capacity(self):
1520
        if self.member_capacity:
1521
            if self.resource.unit:
1522
                return ProjectResourceGrant.display_filesize(
1523
                    self.member_capacity)
1524
            else:
1525
                if math.isinf(self.member_capacity):
1526
                    return 'Unlimited'
1527
                else:
1528
                    return self.member_capacity
1529
        else:
1530
            return 'Unlimited'
1531

    
1532
    def __str__(self):
1533
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1534
                                        self.display_member_capacity())
1535

    
1536
    @classmethod
1537
    def display_filesize(cls, value):
1538
        try:
1539
            value = float(value)
1540
        except:
1541
            return
1542
        else:
1543
            if math.isinf(value):
1544
                return 'Unlimited'
1545
            if value > 1:
1546
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1547
                                [0, 0, 0, 0, 0, 0])
1548
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1549
                quotient = float(value) / 1024**exponent
1550
                unit, value_decimals = unit_list[exponent]
1551
                format_string = '{0:.%sf} {1}' % (value_decimals)
1552
                return format_string.format(quotient, unit)
1553
            if value == 0:
1554
                return '0 bytes'
1555
            if value == 1:
1556
                return '1 byte'
1557
            else:
1558
                return '0'
1559

    
1560

    
1561
def _distinct(f, l):
1562
    d = {}
1563
    last = None
1564
    for x in l:
1565
        group = f(x)
1566
        if group == last:
1567
            continue
1568
        last = group
1569
        d[group] = x
1570
    return d
1571

    
1572

    
1573
def invert_dict(d):
1574
    return dict((v, k) for k, v in d.iteritems())
1575

    
1576

    
1577
class ProjectManager(ForUpdateManager):
1578

    
1579
    def all_with_pending(self, flt=None):
1580
        flt = Q() if flt is None else flt
1581
        projects = list(self.select_related(
1582
            'application', 'application__owner').filter(flt))
1583

    
1584
        objs = ProjectApplication.objects.select_related('owner')
1585
        apps = objs.filter(state=ProjectApplication.PENDING,
1586
                           chain__in=projects).order_by('chain', '-id')
1587
        app_d = _distinct(lambda app: app.chain_id, apps)
1588
        return [(project, app_d.get(project.pk)) for project in projects]
1589

    
1590
    def expired_projects(self):
1591
        model = self.model
1592
        q = ((model.o_state_q(model.O_ACTIVE) |
1593
              model.o_state_q(model.O_SUSPENDED)) &
1594
             Q(application__end_date__lt=datetime.now()))
1595
        return self.filter(q)
1596

    
1597
    def user_accessible_projects(self, user):
1598
        """
1599
        Return projects accessible by specified user.
1600
        """
1601
        model = self.model
1602
        if user.is_project_admin():
1603
            flt = Q()
1604
        else:
1605
            membs = user.projectmembership_set.associated()
1606
            memb_projects = membs.values_list("project", flat=True)
1607
            flt = (Q(application__owner=user) |
1608
                   Q(application__applicant=user) |
1609
                   Q(id__in=memb_projects))
1610

    
1611
        relevant = model.o_states_q(model.RELEVANT_STATES)
1612
        return self.filter(flt, relevant).order_by(
1613
            'application__issue_date').select_related(
1614
                'application', 'application__owner', 'application__applicant')
1615

    
1616
    def search_by_name(self, *search_strings):
1617
        q = Q()
1618
        for s in search_strings:
1619
            q = q | Q(name__icontains=s)
1620
        return self.filter(q)
1621

    
1622

    
1623
class Project(models.Model):
1624

    
1625
    id = models.BigIntegerField(db_column='id', primary_key=True)
1626

    
1627
    application = models.OneToOneField(
1628
        ProjectApplication,
1629
        related_name='project')
1630

    
1631
    members = models.ManyToManyField(
1632
        AstakosUser,
1633
        through='ProjectMembership')
1634

    
1635
    creation_date = models.DateTimeField(auto_now_add=True)
1636
    name = models.CharField(
1637
        max_length=80,
1638
        null=True,
1639
        db_index=True,
1640
        unique=True)
1641

    
1642
    NORMAL = 1
1643
    SUSPENDED = 10
1644
    TERMINATED = 100
1645

    
1646
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1647

    
1648
    state = models.IntegerField(default=NORMAL,
1649
                                db_index=True)
1650

    
1651
    objects = ProjectManager()
1652

    
1653
    def __str__(self):
1654
        return uenc(_("<project %s '%s'>") %
1655
                    (self.id, udec(self.application.name)))
1656

    
1657
    __repr__ = __str__
1658

    
1659
    def __unicode__(self):
1660
        return _("<project %s '%s'>") % (self.id, self.application.name)
1661

    
1662
    O_PENDING = 0
1663
    O_ACTIVE = 1
1664
    O_DENIED = 3
1665
    O_DISMISSED = 4
1666
    O_CANCELLED = 5
1667
    O_SUSPENDED = 10
1668
    O_TERMINATED = 100
1669

    
1670
    O_STATE_DISPLAY = {
1671
        O_PENDING:    _("Pending"),
1672
        O_ACTIVE:     _("Active"),
1673
        O_DENIED:     _("Denied"),
1674
        O_DISMISSED:  _("Dismissed"),
1675
        O_CANCELLED:  _("Cancelled"),
1676
        O_SUSPENDED:  _("Suspended"),
1677
        O_TERMINATED: _("Terminated"),
1678
    }
1679

    
1680
    OVERALL_STATE = {
1681
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1682
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1683
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1684
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1685
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1686
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1687
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1688
    }
1689

    
1690
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1691

    
1692
    @classmethod
1693
    def o_state_q(cls, o_state):
1694
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1695
        return Q(state=p_state, application__state=a_state)
1696

    
1697
    @classmethod
1698
    def o_states_q(cls, o_states):
1699
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1700

    
1701
    INITIALIZED_STATES = [O_ACTIVE,
1702
                          O_SUSPENDED,
1703
                          O_TERMINATED,
1704
                          ]
1705

    
1706
    RELEVANT_STATES = [O_PENDING,
1707
                       O_DENIED,
1708
                       O_ACTIVE,
1709
                       O_SUSPENDED,
1710
                       O_TERMINATED,
1711
                       ]
1712

    
1713
    SKIP_STATES = [O_DISMISSED,
1714
                   O_CANCELLED,
1715
                   O_TERMINATED,
1716
                   ]
1717

    
1718
    @classmethod
1719
    def _overall_state(cls, project_state, app_state):
1720
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1721

    
1722
    def overall_state(self):
1723
        return self._overall_state(self.state, self.application.state)
1724

    
1725
    def last_pending_application(self):
1726
        apps = self.chained_apps.filter(
1727
            state=ProjectApplication.PENDING).order_by('-id')
1728
        if apps:
1729
            return apps[0]
1730
        return None
1731

    
1732
    def last_pending_modification(self):
1733
        last_pending = self.last_pending_application()
1734
        if last_pending == self.application:
1735
            return None
1736
        return last_pending
1737

    
1738
    def has_pending_modifications(self):
1739
        last_pending = self.last_pending_modification()
1740
        return last_pending is not None
1741

    
1742
    def state_display(self):
1743
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1744

    
1745
    def expiration_info(self):
1746
        return (str(self.id), self.name, self.state_display(),
1747
                str(self.application.end_date))
1748

    
1749
    def last_deactivation(self):
1750
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1751
        ls = objs.order_by("-date")
1752
        if not ls:
1753
            return None
1754
        return ls[0]
1755

    
1756
    def is_deactivated(self, reason=None):
1757
        if reason is not None:
1758
            return self.state == reason
1759

    
1760
        return self.state != self.NORMAL
1761

    
1762
    def is_active(self):
1763
        return self.overall_state() == self.O_ACTIVE
1764

    
1765
    def is_initialized(self):
1766
        return self.overall_state() in self.INITIALIZED_STATES
1767

    
1768
    ### Deactivation calls
1769

    
1770
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1771
                    comments=None):
1772
        now = datetime.now()
1773
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1774
                        actor=actor, reason=reason, comments=comments)
1775

    
1776
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1777
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1778
                         comments=comments)
1779
        self.state = to_state
1780
        self.save()
1781

    
1782
    def terminate(self, actor=None, reason=None):
1783
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1784
        self.name = None
1785
        self.save()
1786

    
1787
    def suspend(self, actor=None, reason=None):
1788
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1789

    
1790
    def resume(self, actor=None, reason=None):
1791
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1792
        if self.name is None:
1793
            self.name = self.application.name
1794
            self.save()
1795

    
1796
    ### Logical checks
1797

    
1798
    @property
1799
    def is_alive(self):
1800
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1801

    
1802
    @property
1803
    def is_terminated(self):
1804
        return self.is_deactivated(self.TERMINATED)
1805

    
1806
    @property
1807
    def is_suspended(self):
1808
        return self.is_deactivated(self.SUSPENDED)
1809

    
1810
    def violates_members_limit(self, adding=0):
1811
        application = self.application
1812
        limit = application.limit_on_members_number
1813
        if limit is None:
1814
            return False
1815
        return (len(self.approved_members) + adding > limit)
1816

    
1817
    ### Other
1818

    
1819
    def count_pending_memberships(self):
1820
        return self.projectmembership_set.requested().count()
1821

    
1822
    def members_count(self):
1823
        return self.approved_memberships.count()
1824

    
1825
    @property
1826
    def approved_memberships(self):
1827
        query = ProjectMembership.Q_ACCEPTED_STATES
1828
        return self.projectmembership_set.filter(query)
1829

    
1830
    @property
1831
    def approved_members(self):
1832
        return [m.person for m in self.approved_memberships]
1833

    
1834

    
1835
class ProjectLogManager(models.Manager):
1836
    def last_deactivations(self, projects):
1837
        logs = self.filter(
1838
            project__in=projects,
1839
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1840
        return first_of_group(lambda l: l.project_id, logs)
1841

    
1842

    
1843
class ProjectLog(models.Model):
1844
    project = models.ForeignKey(Project, related_name="log")
1845
    from_state = models.IntegerField(null=True)
1846
    to_state = models.IntegerField()
1847
    date = models.DateTimeField()
1848
    actor = models.ForeignKey(AstakosUser, null=True)
1849
    reason = models.TextField(null=True)
1850
    comments = models.TextField(null=True)
1851

    
1852
    objects = ProjectLogManager()
1853

    
1854

    
1855
class ProjectLock(models.Model):
1856
    objects = ForUpdateManager()
1857

    
1858

    
1859
class ProjectMembershipManager(ForUpdateManager):
1860

    
1861
    def any_accepted(self):
1862
        q = self.model.Q_ACCEPTED_STATES
1863
        return self.filter(q)
1864

    
1865
    def actually_accepted(self):
1866
        q = self.model.Q_ACTUALLY_ACCEPTED
1867
        return self.filter(q)
1868

    
1869
    def requested(self):
1870
        return self.filter(state=ProjectMembership.REQUESTED)
1871

    
1872
    def suspended(self):
1873
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1874

    
1875
    def associated(self):
1876
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1877

    
1878

    
1879
class ProjectMembership(models.Model):
1880

    
1881
    person = models.ForeignKey(AstakosUser)
1882
    project = models.ForeignKey(Project)
1883

    
1884
    REQUESTED = 0
1885
    ACCEPTED = 1
1886
    LEAVE_REQUESTED = 5
1887
    # User deactivation
1888
    USER_SUSPENDED = 10
1889
    REJECTED = 100
1890
    CANCELLED = 101
1891
    REMOVED = 200
1892

    
1893
    ASSOCIATED_STATES = set([REQUESTED,
1894
                             ACCEPTED,
1895
                             LEAVE_REQUESTED,
1896
                             USER_SUSPENDED,
1897
                             ])
1898

    
1899
    ACCEPTED_STATES = set([ACCEPTED,
1900
                           LEAVE_REQUESTED,
1901
                           USER_SUSPENDED,
1902
                           ])
1903

    
1904
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1905

    
1906
    state = models.IntegerField(default=REQUESTED,
1907
                                db_index=True)
1908

    
1909
    objects = ProjectMembershipManager()
1910

    
1911
    # Compiled queries
1912
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1913
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1914

    
1915
    MEMBERSHIP_STATE_DISPLAY = {
1916
        REQUESTED:       _('Requested'),
1917
        ACCEPTED:        _('Accepted'),
1918
        LEAVE_REQUESTED: _('Leave Requested'),
1919
        USER_SUSPENDED:  _('Suspended'),
1920
        REJECTED:        _('Rejected'),
1921
        CANCELLED:       _('Cancelled'),
1922
        REMOVED:         _('Removed'),
1923
    }
1924

    
1925
    USER_FRIENDLY_STATE_DISPLAY = {
1926
        REQUESTED:       _('Join requested'),
1927
        ACCEPTED:        _('Accepted member'),
1928
        LEAVE_REQUESTED: _('Requested to leave'),
1929
        USER_SUSPENDED:  _('Suspended member'),
1930
        REJECTED:        _('Request rejected'),
1931
        CANCELLED:       _('Request cancelled'),
1932
        REMOVED:         _('Removed member'),
1933
    }
1934

    
1935
    def state_display(self):
1936
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1937

    
1938
    def user_friendly_state_display(self):
1939
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1940

    
1941
    class Meta:
1942
        unique_together = ("person", "project")
1943
        #index_together = [["project", "state"]]
1944

    
1945
    def __str__(self):
1946
        return uenc(_("<'%s' membership in '%s'>") %
1947
                    (self.person.username, self.project))
1948

    
1949
    __repr__ = __str__
1950

    
1951
    def latest_log(self):
1952
        logs = self.log.all()
1953
        logs_d = _partition_by(lambda l: l.to_state, logs)
1954
        for s, s_logs in logs_d.iteritems():
1955
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1956
        return logs_d
1957

    
1958
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1959
                    comments=None):
1960
        now = datetime.now()
1961
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1962
                        actor=actor, reason=reason, comments=comments)
1963

    
1964
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1965
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1966
                         comments=comments)
1967
        self.state = to_state
1968
        self.save()
1969

    
1970
    ACTION_CHECKS = {
1971
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
1972
        "accept": lambda m: m.state == m.REQUESTED,
1973
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
1974
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
1975
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
1976
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1977
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1978
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
1979
        "reject": lambda m: m.state == m.REQUESTED,
1980
        "cancel": lambda m: m.state == m.REQUESTED,
1981
    }
1982

    
1983
    ACTION_STATES = {
1984
        "join":          REQUESTED,
1985
        "accept":        ACCEPTED,
1986
        "leave_request": LEAVE_REQUESTED,
1987
        "deny_leave":    ACCEPTED,
1988
        "cancel_leave":  ACCEPTED,
1989
        "remove":        REMOVED,
1990
        "reject":        REJECTED,
1991
        "cancel":        CANCELLED,
1992
    }
1993

    
1994
    def check_action(self, action):
1995
        try:
1996
            check = self.ACTION_CHECKS[action]
1997
        except KeyError:
1998
            raise ValueError("No check found for action '%s'" % action)
1999
        return check(self)
2000

    
2001
    def perform_action(self, action, actor=None, reason=None):
2002
        if not self.check_action(action):
2003
            m = _("%s: attempted action '%s' in state '%s'") % (
2004
                self, action, self.state)
2005
            raise AssertionError(m)
2006
        try:
2007
            s = self.ACTION_STATES[action]
2008
        except KeyError:
2009
            raise ValueError("No such action '%s'" % action)
2010
        return self.set_state(s, actor=actor, reason=reason)
2011

    
2012

    
2013
class ProjectMembershipLogManager(models.Manager):
2014
    def last_logs(self, memberships):
2015
        logs = self.filter(membership__in=memberships).order_by("-date")
2016
        logs = _partition_by(lambda l: l.membership_id, logs)
2017

    
2018
        for memb_id, m_logs in logs.iteritems():
2019
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
2020
        return logs
2021

    
2022

    
2023
class ProjectMembershipLog(models.Model):
2024
    membership = models.ForeignKey(ProjectMembership, related_name="log")
2025
    from_state = models.IntegerField(null=True)
2026
    to_state = models.IntegerField()
2027
    date = models.DateTimeField()
2028
    actor = models.ForeignKey(AstakosUser, null=True)
2029
    reason = models.TextField(null=True)
2030
    comments = models.TextField(null=True)
2031

    
2032
    objects = ProjectMembershipLogManager()
2033

    
2034

    
2035
### SIGNALS ###
2036
################
2037

    
2038
def create_astakos_user(u):
2039
    try:
2040
        AstakosUser.objects.get(user_ptr=u.pk)
2041
    except AstakosUser.DoesNotExist:
2042
        extended_user = AstakosUser(user_ptr_id=u.pk)
2043
        extended_user.__dict__.update(u.__dict__)
2044
        extended_user.save()
2045
        if not extended_user.has_auth_provider('local'):
2046
            extended_user.add_auth_provider('local')
2047
    except BaseException, e:
2048
        logger.exception(e)
2049

    
2050

    
2051
def fix_superusers():
2052
    # Associate superusers with AstakosUser
2053
    admins = User.objects.filter(is_superuser=True)
2054
    for u in admins:
2055
        create_astakos_user(u)
2056

    
2057

    
2058
def user_post_save(sender, instance, created, **kwargs):
2059
    if not created:
2060
        return
2061
    create_astakos_user(instance)
2062
post_save.connect(user_post_save, sender=User)
2063

    
2064

    
2065
def astakosuser_post_save(sender, instance, created, **kwargs):
2066
    pass
2067

    
2068
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2069

    
2070

    
2071
def resource_post_save(sender, instance, created, **kwargs):
2072
    pass
2073

    
2074
post_save.connect(resource_post_save, sender=Resource)
2075

    
2076

    
2077
def renew_token(sender, instance, **kwargs):
2078
    if not instance.auth_token:
2079
        instance.renew_token()
2080
pre_save.connect(renew_token, sender=AstakosUser)
2081
pre_save.connect(renew_token, sender=Component)