Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 3715f954

History | View | Annotate | Download (72.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 time import asctime
42
from datetime import datetime, timedelta
43
from base64 import b64encode
44
from urlparse import urlparse
45
from urllib import quote
46
from random import randint
47

    
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, Max
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 synnefo.lib.utils import dict_merge
68

    
69
from astakos.im.settings import (
70
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
71
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
72
    SITENAME, MODERATION_ENABLED,
73
    )
74
from astakos.im import settings as astakos_settings
75
from astakos.im import auth_providers as auth
76

    
77
import astakos.im.messages as astakos_messages
78
from snf_django.lib.db.managers import ForUpdateManager
79
from synnefo.lib.ordereddict import OrderedDict
80

    
81
from snf_django.lib.db.fields import intDecimalField
82
from synnefo.util.text import uenc, udec
83
from astakos.im import presentation
84

    
85
logger = logging.getLogger(__name__)
86

    
87
DEFAULT_CONTENT_TYPE = None
88
_content_type = None
89

    
90

    
91
def get_content_type():
92
    global _content_type
93
    if _content_type is not None:
94
        return _content_type
95

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

    
104
inf = float('inf')
105

    
106

    
107
class Service(models.Model):
108
    name = models.CharField(_('Name'), max_length=255, unique=True,
109
                            db_index=True)
110
    url = models.CharField(_('Service url'), max_length=255, null=True,
111
                           help_text=_("URL the service is accessible from"))
112
    api_url = models.CharField(_('Service API url'), max_length=255, null=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'),
116
                                              null=True)
117
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
118
                                              null=True)
119

    
120
    def renew_token(self, expiration_date=None):
121
        md5 = hashlib.md5()
122
        md5.update(self.name.encode('ascii', 'ignore'))
123
        md5.update(self.api_url.encode('ascii', 'ignore'))
124
        md5.update(asctime())
125

    
126
        self.auth_token = b64encode(md5.digest())
127
        self.auth_token_created = datetime.now()
128
        if expiration_date:
129
            self.auth_token_expires = expiration_date
130
        else:
131
            self.auth_token_expires = None
132

    
133
    def __str__(self):
134
        return self.name
135

    
136
    @classmethod
137
    def catalog(cls, orderfor=None):
138
        catalog = {}
139
        services = list(cls.objects.all())
140
        default_metadata = presentation.SERVICES
141
        metadata = {}
142

    
143
        for service in services:
144
            d = {'api_url': service.api_url,
145
                 'url': service.url,
146
                 'name': service.name}
147
            if service.name in default_metadata:
148
                metadata[service.name] = default_metadata.get(service.name)
149
                metadata[service.name].update(d)
150
            else:
151
                metadata[service.name] = d
152

    
153

    
154
        def service_by_order(s):
155
            return s[1].get('order')
156

    
157
        def service_by_dashbaord_order(s):
158
            return s[1].get('dashboard').get('order')
159

    
160
        metadata = dict_merge(metadata,
161
                              astakos_settings.SERVICES_META)
162

    
163
        for service, info in metadata.iteritems():
164
            default_meta = presentation.service_defaults(service)
165
            base_meta = metadata.get(service, {})
166
            settings_meta = astakos_settings.SERVICES_META.get(service, {})
167
            service_meta = dict_merge(default_meta, base_meta)
168
            meta = dict_merge(service_meta, settings_meta)
169
            catalog[service] = meta
170

    
171
        order_key = service_by_order
172
        if orderfor == 'dashboard':
173
            order_key = service_by_dashbaord_order
174

    
175
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
176
                                             key=order_key))
177
        return ordered_catalog
178

    
179

    
180
_presentation_data = {}
181

    
182

    
183
def get_presentation(resource):
184
    global _presentation_data
185
    resource_presentation = _presentation_data.get(resource, {})
186
    if not resource_presentation:
187
        resources_presentation = presentation.RESOURCES.get('resources', {})
188
        resource_presentation = resources_presentation.get(resource, {})
189
        _presentation_data[resource] = resource_presentation
190
    return resource_presentation
191

    
192

    
193
class Resource(models.Model):
194
    name = models.CharField(_('Name'), max_length=255, unique=True)
195
    desc = models.TextField(_('Description'), null=True)
196
    service = models.ForeignKey(Service)
197
    unit = models.CharField(_('Unit'), null=True, max_length=255)
198
    uplimit = intDecimalField(default=0)
199
    allow_in_projects = models.BooleanField(default=True)
200

    
201
    objects = ForUpdateManager()
202

    
203
    def __str__(self):
204
        return self.name
205

    
206
    def full_name(self):
207
        return str(self)
208

    
209
    def get_info(self):
210
        return {'service': str(self.service),
211
                'description': self.desc,
212
                'unit': self.unit,
213
                'allow_in_projects': self.allow_in_projects,
214
                }
215

    
216
    @property
217
    def group(self):
218
        default = self.name
219
        return get_presentation(str(self)).get('group', default)
220

    
221
    @property
222
    def help_text(self):
223
        default = "%s resource" % self.name
224
        return get_presentation(str(self)).get('help_text', default)
225

    
226
    @property
227
    def help_text_input_each(self):
228
        default = "%s resource" % self.name
229
        return get_presentation(str(self)).get('help_text_input_each', default)
230

    
231
    @property
232
    def is_abbreviation(self):
233
        return get_presentation(str(self)).get('is_abbreviation', False)
234

    
235
    @property
236
    def report_desc(self):
237
        default = "%s resource" % self.name
238
        return get_presentation(str(self)).get('report_desc', default)
239

    
240
    @property
241
    def placeholder(self):
242
        return get_presentation(str(self)).get('placeholder', self.unit)
243

    
244
    @property
245
    def verbose_name(self):
246
        return get_presentation(str(self)).get('verbose_name', self.name)
247

    
248
    @property
249
    def display_name(self):
250
        name = self.verbose_name
251
        if self.is_abbreviation:
252
            name = name.upper()
253
        return name
254

    
255
    @property
256
    def pluralized_display_name(self):
257
        if not self.unit:
258
            return '%ss' % self.display_name
259
        return self.display_name
260

    
261
def get_resource_names():
262
    _RESOURCE_NAMES = []
263
    resources = Resource.objects.select_related('service').all()
264
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
265
    return _RESOURCE_NAMES
266

    
267

    
268
class AstakosUserManager(UserManager):
269

    
270
    def get_auth_provider_user(self, provider, **kwargs):
271
        """
272
        Retrieve AstakosUser instance associated with the specified third party
273
        id.
274
        """
275
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
276
                          kwargs.iteritems()))
277
        return self.get(auth_providers__module=provider, **kwargs)
278

    
279
    def get_by_email(self, email):
280
        return self.get(email=email)
281

    
282
    def get_by_identifier(self, email_or_username, **kwargs):
283
        try:
284
            return self.get(email__iexact=email_or_username, **kwargs)
285
        except AstakosUser.DoesNotExist:
286
            return self.get(username__iexact=email_or_username, **kwargs)
287

    
288
    def user_exists(self, email_or_username, **kwargs):
289
        qemail = Q(email__iexact=email_or_username)
290
        qusername = Q(username__iexact=email_or_username)
291
        qextra = Q(**kwargs)
292
        return self.filter((qemail | qusername) & qextra).exists()
293

    
294
    def verified_user_exists(self, email_or_username):
295
        return self.user_exists(email_or_username, email_verified=True)
296

    
297
    def verified(self):
298
        return self.filter(email_verified=True)
299

    
300
    def uuid_catalog(self, l=None):
301
        """
302
        Returns a uuid to username mapping for the uuids appearing in l.
303
        If l is None returns the mapping for all existing users.
304
        """
305
        q = self.filter(uuid__in=l) if l != None else self
306
        return dict(q.values_list('uuid', 'username'))
307

    
308
    def displayname_catalog(self, l=None):
309
        """
310
        Returns a username to uuid mapping for the usernames appearing in l.
311
        If l is None returns the mapping for all existing users.
312
        """
313
        if l is not None:
314
            lmap = dict((x.lower(), x) for x in l)
315
            q = self.filter(username__in=lmap.keys())
316
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
317
        else:
318
            q = self
319
            values = self.values_list('username', 'uuid')
320
        return dict(values)
321

    
322

    
323

    
324
class AstakosUser(User):
325
    """
326
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
327
    """
328
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
329
                                   null=True)
330

    
331
    #for invitations
332
    user_level = DEFAULT_USER_LEVEL
333
    level = models.IntegerField(_('Inviter level'), default=user_level)
334
    invitations = models.IntegerField(
335
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
336

    
337
    auth_token = models.CharField(_('Authentication Token'),
338
                                  max_length=32,
339
                                  null=True,
340
                                  blank=True,
341
                                  help_text = _('Renew your authentication '
342
                                                'token. Make sure to set the new '
343
                                                'token in any client you may be '
344
                                                'using, to preserve its '
345
                                                'functionality.'))
346
    auth_token_created = models.DateTimeField(_('Token creation date'),
347
                                              null=True)
348
    auth_token_expires = models.DateTimeField(
349
        _('Token expiration date'), null=True)
350

    
351
    updated = models.DateTimeField(_('Update date'))
352

    
353
    # Arbitrary text to identify the reason user got deactivated.
354
    # To be used as a reference from administrators.
355
    deactivated_reason = models.TextField(
356
        _('Reason the user was disabled for'),
357
        default=None, null=True)
358
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
359
                                          blank=True)
360

    
361
    has_credits = models.BooleanField(_('Has credits?'), default=False)
362

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

    
366
    # user email is verified
367
    email_verified = models.BooleanField(_('Email verified?'), default=False)
368

    
369
    # unique string used in user email verification url
370
    verification_code = models.CharField(max_length=255, null=True,
371
                                         blank=False, unique=True)
372

    
373
    # date user email verified
374
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
375
                                       blank=True)
376

    
377
    # email verification notice was sent to the user at this time
378
    activation_sent = models.DateTimeField(_('Activation sent date'),
379
                                           null=True, blank=True)
380

    
381
    # user got rejected during moderation process
382
    is_rejected = models.BooleanField(_('Account rejected'),
383
                                      default=False)
384
    # reason user got rejected
385
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
386
                                       blank=True)
387
    # moderation status
388
    moderated = models.BooleanField(_('User moderated'), default=False)
389
    # date user moderated (either accepted or rejected)
390
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
391
                                        blank=True, null=True)
392
    # a snapshot of user instance the time got moderated
393
    moderated_data = models.TextField(null=True, default=None, blank=True)
394
    # a string which identifies how the user got moderated
395
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
396
                                       default=None, null=True, blank=True)
397
    # the email used to accept the user
398
    accepted_email = models.EmailField(null=True, default=None, blank=True)
399

    
400
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
401
                                           default=False)
402
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
403
                                             null=True, blank=True)
404
    # permanent unique user identifier
405
    uuid = models.CharField(max_length=255, null=True, blank=False,
406
                            unique=True)
407

    
408
    policy = models.ManyToManyField(
409
        Resource, null=True, through='AstakosUserQuota')
410

    
411
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
412
                                           default=False, db_index=True)
413

    
414
    objects = AstakosUserManager()
415
    forupdate = ForUpdateManager()
416

    
417
    def __init__(self, *args, **kwargs):
418
        super(AstakosUser, self).__init__(*args, **kwargs)
419
        if not self.id:
420
            self.is_active = False
421

    
422
    @property
423
    def realname(self):
424
        return '%s %s' % (self.first_name, self.last_name)
425

    
426
    @property
427
    def log_display(self):
428
        """
429
        Should be used in all logger.* calls that refer to a user so that
430
        user display is consistent across log entries.
431
        """
432
        return '%s::%s' % (self.uuid, self.email)
433

    
434
    @realname.setter
435
    def realname(self, value):
436
        parts = value.split(' ')
437
        if len(parts) == 2:
438
            self.first_name = parts[0]
439
            self.last_name = parts[1]
440
        else:
441
            self.last_name = parts[0]
442

    
443
    def add_permission(self, pname):
444
        if self.has_perm(pname):
445
            return
446
        p, created = Permission.objects.get_or_create(
447
                                    codename=pname,
448
                                    name=pname.capitalize(),
449
                                    content_type=get_content_type())
450
        self.user_permissions.add(p)
451

    
452
    def remove_permission(self, pname):
453
        if self.has_perm(pname):
454
            return
455
        p = Permission.objects.get(codename=pname,
456
                                   content_type=get_content_type())
457
        self.user_permissions.remove(p)
458

    
459
    def add_group(self, gname):
460
        group, _ = Group.objects.get_or_create(name=gname)
461
        self.groups.add(group)
462

    
463
    def is_project_admin(self, application_id=None):
464
        return self.uuid in astakos_settings.PROJECT_ADMINS
465

    
466
    @property
467
    def invitation(self):
468
        try:
469
            return Invitation.objects.get(username=self.email)
470
        except Invitation.DoesNotExist:
471
            return None
472

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

    
477
    def get_resource_policy(self, resource):
478
        resource = Resource.objects.get(name=resource)
479
        default_capacity = resource.uplimit
480
        try:
481
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
482
            return policy, default_capacity
483
        except AstakosUserQuota.DoesNotExist:
484
            return None, default_capacity
485

    
486
    def update_uuid(self):
487
        while not self.uuid:
488
            uuid_val = str(uuid.uuid4())
489
            try:
490
                AstakosUser.objects.get(uuid=uuid_val)
491
            except AstakosUser.DoesNotExist, e:
492
                self.uuid = uuid_val
493
        return self.uuid
494

    
495
    def save(self, update_timestamps=True, **kwargs):
496
        if update_timestamps:
497
            if not self.id:
498
                self.date_joined = datetime.now()
499
            self.updated = datetime.now()
500

    
501
        self.update_uuid()
502
        # username currently matches email
503
        if self.username != self.email.lower():
504
            self.username = self.email.lower()
505

    
506
        super(AstakosUser, self).save(**kwargs)
507

    
508
    def renew_verification_code(self):
509
        self.verification_code = str(uuid.uuid4())
510
        logger.info("Verification code renewed for %s" % self.log_display)
511

    
512
    def renew_token(self, flush_sessions=False, current_key=None):
513
        md5 = hashlib.md5()
514
        md5.update(settings.SECRET_KEY)
515
        md5.update(self.username)
516
        md5.update(self.realname.encode('ascii', 'ignore'))
517
        md5.update(asctime())
518

    
519
        self.auth_token = b64encode(md5.digest())
520
        self.auth_token_created = datetime.now()
521
        self.auth_token_expires = self.auth_token_created + \
522
                                  timedelta(hours=AUTH_TOKEN_DURATION)
523
        if flush_sessions:
524
            self.flush_sessions(current_key)
525
        msg = 'Token renewed for %s' % self.log_display
526
        logger.log(LOGGING_LEVEL, msg)
527

    
528
    def flush_sessions(self, current_key=None):
529
        q = self.sessions
530
        if current_key:
531
            q = q.exclude(session_key=current_key)
532

    
533
        keys = q.values_list('session_key', flat=True)
534
        if keys:
535
            msg = 'Flushing sessions: %s' % ','.join(keys)
536
            logger.log(LOGGING_LEVEL, msg, [])
537
        engine = import_module(settings.SESSION_ENGINE)
538
        for k in keys:
539
            s = engine.SessionStore(k)
540
            s.flush()
541

    
542
    def __unicode__(self):
543
        return '%s (%s)' % (self.realname, self.email)
544

    
545
    def conflicting_email(self):
546
        q = AstakosUser.objects.exclude(username=self.username)
547
        q = q.filter(email__iexact=self.email)
548
        if q.count() != 0:
549
            return True
550
        return False
551

    
552
    def email_change_is_pending(self):
553
        return self.emailchanges.count() > 0
554

    
555
    @property
556
    def signed_terms(self):
557
        term = get_latest_terms()
558
        if not term:
559
            return True
560
        if not self.has_signed_terms:
561
            return False
562
        if not self.date_signed_terms:
563
            return False
564
        if self.date_signed_terms < term.date:
565
            self.has_signed_terms = False
566
            self.date_signed_terms = None
567
            self.save()
568
            return False
569
        return True
570

    
571
    def set_invitations_level(self):
572
        """
573
        Update user invitation level
574
        """
575
        level = self.invitation.inviter.level + 1
576
        self.level = level
577
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
578

    
579
    def can_change_password(self):
580
        return self.has_auth_provider('local', auth_backend='astakos')
581

    
582
    def can_change_email(self):
583
        if not self.has_auth_provider('local'):
584
            return True
585

    
586
        local = self.get_auth_provider('local')._instance
587
        return local.auth_backend == 'astakos'
588

    
589
    # Auth providers related methods
590
    def get_auth_provider(self, module=None, identifier=None, **filters):
591
        if not module:
592
            return self.auth_providers.active()[0].settings
593

    
594
        params = {'module': module}
595
        if identifier:
596
            params['identifier'] = identifier
597
        params.update(filters)
598
        return self.auth_providers.active().get(**params).settings
599

    
600
    def has_auth_provider(self, provider, **kwargs):
601
        return bool(self.auth_providers.active().filter(module=provider,
602
                                                        **kwargs).count())
603

    
604
    def get_required_providers(self, **kwargs):
605
        return auth.REQUIRED_PROVIDERS.keys()
606

    
607
    def missing_required_providers(self):
608
        required = self.get_required_providers()
609
        missing = []
610
        for provider in required:
611
            if not self.has_auth_provider(provider):
612
                missing.append(auth.get_provider(provider, self))
613
        return missing
614

    
615
    def get_available_auth_providers(self, **filters):
616
        """
617
        Returns a list of providers available for add by the user.
618
        """
619
        modules = astakos_settings.IM_MODULES
620
        providers = []
621
        for p in modules:
622
            providers.append(auth.get_provider(p, self))
623
        available = []
624

    
625
        for p in providers:
626
            if p.get_add_policy:
627
                available.append(p)
628
        return available
629

    
630
    def get_disabled_auth_providers(self, **filters):
631
        providers = self.get_auth_providers(**filters)
632
        disabled = []
633
        for p in providers:
634
            if not p.get_login_policy:
635
                disabled.append(p)
636
        return disabled
637

    
638
    def get_enabled_auth_providers(self, **filters):
639
        providers = self.get_auth_providers(**filters)
640
        enabled = []
641
        for p in providers:
642
            if p.get_login_policy:
643
                enabled.append(p)
644
        return enabled
645

    
646
    def get_auth_providers(self, **filters):
647
        providers = []
648
        for provider in self.auth_providers.active(**filters):
649
            if provider.settings.module_enabled:
650
                providers.append(provider.settings)
651

    
652
        modules = astakos_settings.IM_MODULES
653

    
654
        def key(p):
655
            if not p.module in modules:
656
                return 100
657
            return modules.index(p.module)
658

    
659
        providers = sorted(providers, key=key)
660
        return providers
661

    
662
    # URL methods
663
    @property
664
    def auth_providers_display(self):
665
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
666
                         self.get_enabled_auth_providers()])
667

    
668
    def add_auth_provider(self, module='local', identifier=None, **params):
669
        provider = auth.get_provider(module, self, identifier, **params)
670
        provider.add_to_user()
671

    
672
    def get_resend_activation_url(self):
673
        return reverse('send_activation', kwargs={'user_id': self.pk})
674

    
675
    def get_activation_url(self, nxt=False):
676
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
677
                                 quote(self.verification_code))
678
        if nxt:
679
            url += "&next=%s" % quote(nxt)
680
        return url
681

    
682
    def get_password_reset_url(self, token_generator=default_token_generator):
683
        return reverse('astakos.im.target.local.password_reset_confirm',
684
                          kwargs={'uidb36':int_to_base36(self.id),
685
                                  'token':token_generator.make_token(self)})
686

    
687
    def get_inactive_message(self, provider_module, identifier=None):
688
        provider = self.get_auth_provider(provider_module, identifier)
689

    
690
        msg_extra = ''
691
        message = ''
692

    
693
        msg_inactive = provider.get_account_inactive_msg
694
        msg_pending = provider.get_pending_activation_msg
695
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
696
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
697
        msg_pending_mod = provider.get_pending_moderation_msg
698
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
699
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
700

    
701
        if not self.email_verified:
702
            message = msg_pending
703
            url = self.get_resend_activation_url()
704
            msg_extra = msg_pending_help + \
705
                        u' ' + \
706
                        '<a href="%s">%s?</a>' % (url, msg_resend)
707
        else:
708
            if not self.moderated:
709
                message = msg_pending_mod
710
            else:
711
                if self.is_rejected:
712
                    message = msg_rejected
713
                else:
714
                    message = msg_inactive
715

    
716
        return mark_safe(message + u' ' + msg_extra)
717

    
718
    def owns_application(self, application):
719
        return application.owner == self
720

    
721
    def owns_project(self, project):
722
        return project.application.owner == self
723

    
724
    def is_associated(self, project):
725
        try:
726
            m = ProjectMembership.objects.get(person=self, project=project)
727
            return m.state in ProjectMembership.ASSOCIATED_STATES
728
        except ProjectMembership.DoesNotExist:
729
            return False
730

    
731
    def get_membership(self, project):
732
        try:
733
            return ProjectMembership.objects.get(
734
                project=project,
735
                person=self)
736
        except ProjectMembership.DoesNotExist:
737
            return None
738

    
739
    def membership_display(self, project):
740
        m = self.get_membership(project)
741
        if m is None:
742
            return _('Not a member')
743
        else:
744
            return m.user_friendly_state_display()
745

    
746
    def non_owner_can_view(self, maybe_project):
747
        if self.is_project_admin():
748
            return True
749
        if maybe_project is None:
750
            return False
751
        project = maybe_project
752
        if self.is_associated(project):
753
            return True
754
        if project.is_deactivated():
755
            return False
756
        return True
757

    
758

    
759
class AstakosUserAuthProviderManager(models.Manager):
760

    
761
    def active(self, **filters):
762
        return self.filter(active=True, **filters)
763

    
764
    def remove_unverified_providers(self, provider, **filters):
765
        try:
766
            existing = self.filter(module=provider, user__email_verified=False,
767
                                   **filters)
768
            for p in existing:
769
                p.user.delete()
770
        except:
771
            pass
772

    
773
    def unverified(self, provider, **filters):
774
        try:
775
            return self.get(module=provider, user__email_verified=False,
776
                            **filters).settings
777
        except AstakosUserAuthProvider.DoesNotExist:
778
            return None
779

    
780
    def verified(self, provider, **filters):
781
        try:
782
            return self.get(module=provider, user__email_verified=True,
783
                            **filters).settings
784
        except AstakosUserAuthProvider.DoesNotExist:
785
            return None
786

    
787

    
788
class AuthProviderPolicyProfileManager(models.Manager):
789

    
790
    def active(self):
791
        return self.filter(active=True)
792

    
793
    def for_user(self, user, provider):
794
        policies = {}
795
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
796
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
797
        exclusive_q = exclusive_q1 | exclusive_q2
798

    
799
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
800
            policies.update(profile.policies)
801

    
802
        user_groups = user.groups.all().values('pk')
803
        for profile in self.active().filter(groups__in=user_groups).filter(
804
                exclusive_q):
805
            policies.update(profile.policies)
806
        return policies
807

    
808
    def add_policy(self, name, provider, group_or_user, exclusive=False,
809
                   **policies):
810
        is_group = isinstance(group_or_user, Group)
811
        profile, created = self.get_or_create(name=name, provider=provider,
812
                                              is_exclusive=exclusive)
813
        profile.is_exclusive = exclusive
814
        profile.save()
815
        if is_group:
816
            profile.groups.add(group_or_user)
817
        else:
818
            profile.users.add(group_or_user)
819
        profile.set_policies(policies)
820
        profile.save()
821
        return profile
822

    
823

    
824
class AuthProviderPolicyProfile(models.Model):
825
    name = models.CharField(_('Name'), max_length=255, blank=False,
826
                            null=False, db_index=True)
827
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
828
                                null=False)
829

    
830
    # apply policies to all providers excluding the one set in provider field
831
    is_exclusive = models.BooleanField(default=False)
832

    
833
    policy_add = models.NullBooleanField(null=True, default=None)
834
    policy_remove = models.NullBooleanField(null=True, default=None)
835
    policy_create = models.NullBooleanField(null=True, default=None)
836
    policy_login = models.NullBooleanField(null=True, default=None)
837
    policy_limit = models.IntegerField(null=True, default=None)
838
    policy_required = models.NullBooleanField(null=True, default=None)
839
    policy_automoderate = models.NullBooleanField(null=True, default=None)
840
    policy_switch = models.NullBooleanField(null=True, default=None)
841

    
842
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
843
                     'automoderate')
844

    
845
    priority = models.IntegerField(null=False, default=1)
846
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
847
    users = models.ManyToManyField(AstakosUser,
848
                                   related_name='authpolicy_profiles')
849
    active = models.BooleanField(default=True)
850

    
851
    objects = AuthProviderPolicyProfileManager()
852

    
853
    class Meta:
854
        ordering = ['priority']
855

    
856
    @property
857
    def policies(self):
858
        policies = {}
859
        for pkey in self.POLICY_FIELDS:
860
            value = getattr(self, 'policy_%s' % pkey, None)
861
            if value is None:
862
                continue
863
            policies[pkey] = value
864
        return policies
865

    
866
    def set_policies(self, policies_dict):
867
        for key, value in policies_dict.iteritems():
868
            if key in self.POLICY_FIELDS:
869
                setattr(self, 'policy_%s' % key, value)
870
        return self.policies
871

    
872

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

    
891
    objects = AstakosUserAuthProviderManager()
892

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

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

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

    
909
    @property
910
    def settings(self):
911
        extra_data = {}
912

    
913
        info_data = {}
914
        if self.info_data:
915
            info_data = json.loads(self.info_data)
916

    
917
        extra_data['info'] = info_data
918

    
919
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
920
            extra_data[key] = getattr(self, key)
921

    
922
        extra_data['instance'] = self
923
        return auth.get_provider(self.module, self.user,
924
                                           self.identifier, **extra_data)
925

    
926
    def __repr__(self):
927
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
928

    
929
    def __unicode__(self):
930
        if self.identifier:
931
            return "%s:%s" % (self.module, self.identifier)
932
        if self.auth_backend:
933
            return "%s:%s" % (self.module, self.auth_backend)
934
        return self.module
935

    
936
    def save(self, *args, **kwargs):
937
        self.info_data = json.dumps(self.info)
938
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
939

    
940

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

    
968
    update_or_create = _update_or_create
969

    
970

    
971
class AstakosUserQuota(models.Model):
972
    objects = ExtendedManager()
973
    capacity = intDecimalField()
974
    resource = models.ForeignKey(Resource)
975
    user = models.ForeignKey(AstakosUser)
976

    
977
    class Meta:
978
        unique_together = ("resource", "user")
979

    
980

    
981
class ApprovalTerms(models.Model):
982
    """
983
    Model for approval terms
984
    """
985

    
986
    date = models.DateTimeField(
987
        _('Issue date'), db_index=True, auto_now_add=True)
988
    location = models.CharField(_('Terms location'), max_length=255)
989

    
990

    
991
class Invitation(models.Model):
992
    """
993
    Model for registring invitations
994
    """
995
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
996
                                null=True)
997
    realname = models.CharField(_('Real name'), max_length=255)
998
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
999
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1000
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1001
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1002
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1003

    
1004
    def __init__(self, *args, **kwargs):
1005
        super(Invitation, self).__init__(*args, **kwargs)
1006
        if not self.id:
1007
            self.code = _generate_invitation_code()
1008

    
1009
    def consume(self):
1010
        self.is_consumed = True
1011
        self.consumed = datetime.now()
1012
        self.save()
1013

    
1014
    def __unicode__(self):
1015
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1016

    
1017

    
1018
class EmailChangeManager(models.Manager):
1019

    
1020
    @transaction.commit_on_success
1021
    def change_email(self, activation_key):
1022
        """
1023
        Validate an activation key and change the corresponding
1024
        ``User`` if valid.
1025

1026
        If the key is valid and has not expired, return the ``User``
1027
        after activating.
1028

1029
        If the key is not valid or has expired, return ``None``.
1030

1031
        If the key is valid but the ``User`` is already active,
1032
        return ``None``.
1033

1034
        After successful email change the activation record is deleted.
1035

1036
        Throws ValueError if there is already
1037
        """
1038
        try:
1039
            email_change = self.model.objects.get(
1040
                activation_key=activation_key)
1041
            if email_change.activation_key_expired():
1042
                email_change.delete()
1043
                raise EmailChange.DoesNotExist
1044
            # is there an active user with this address?
1045
            try:
1046
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1047
            except AstakosUser.DoesNotExist:
1048
                pass
1049
            else:
1050
                raise ValueError(_('The new email address is reserved.'))
1051
            # update user
1052
            user = AstakosUser.objects.get(pk=email_change.user_id)
1053
            old_email = user.email
1054
            user.email = email_change.new_email_address
1055
            user.save()
1056
            email_change.delete()
1057
            msg = "User %s changed email from %s to %s" % (user.log_display,
1058
                                                           old_email,
1059
                                                           user.email)
1060
            logger.log(LOGGING_LEVEL, msg)
1061
            return user
1062
        except EmailChange.DoesNotExist:
1063
            raise ValueError(_('Invalid activation key.'))
1064

    
1065

    
1066
class EmailChange(models.Model):
1067
    new_email_address = models.EmailField(
1068
        _(u'new e-mail address'),
1069
        help_text=_('Provide a new email address. Until you verify the new '
1070
                    'address by following the activation link that will be '
1071
                    'sent to it, your old email address will remain active.'))
1072
    user = models.ForeignKey(
1073
        AstakosUser, unique=True, related_name='emailchanges')
1074
    requested_at = models.DateTimeField(auto_now_add=True)
1075
    activation_key = models.CharField(
1076
        max_length=40, unique=True, db_index=True)
1077

    
1078
    objects = EmailChangeManager()
1079

    
1080
    def get_url(self):
1081
        return reverse('email_change_confirm',
1082
                      kwargs={'activation_key': self.activation_key})
1083

    
1084
    def activation_key_expired(self):
1085
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1086
        return self.requested_at + expiration_date < datetime.now()
1087

    
1088

    
1089
class AdditionalMail(models.Model):
1090
    """
1091
    Model for registring invitations
1092
    """
1093
    owner = models.ForeignKey(AstakosUser)
1094
    email = models.EmailField()
1095

    
1096

    
1097
def _generate_invitation_code():
1098
    while True:
1099
        code = randint(1, 2L ** 63 - 1)
1100
        try:
1101
            Invitation.objects.get(code=code)
1102
            # An invitation with this code already exists, try again
1103
        except Invitation.DoesNotExist:
1104
            return code
1105

    
1106

    
1107
def get_latest_terms():
1108
    try:
1109
        term = ApprovalTerms.objects.order_by('-id')[0]
1110
        return term
1111
    except IndexError:
1112
        pass
1113
    return None
1114

    
1115

    
1116
class PendingThirdPartyUser(models.Model):
1117
    """
1118
    Model for registring successful third party user authentications
1119
    """
1120
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1121
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1122
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1123
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1124
                                  null=True)
1125
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1126
                                 null=True)
1127
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1128
                                   null=True)
1129
    username = models.CharField(_('username'), max_length=30, unique=True,
1130
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1131
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1132
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1133
    info = models.TextField(default="", null=True, blank=True)
1134

    
1135
    class Meta:
1136
        unique_together = ("provider", "third_party_identifier")
1137

    
1138
    def get_user_instance(self):
1139
        """
1140
        Create a new AstakosUser instance based on details provided when user
1141
        initially signed up.
1142
        """
1143
        d = copy.copy(self.__dict__)
1144
        d.pop('_state', None)
1145
        d.pop('id', None)
1146
        d.pop('token', None)
1147
        d.pop('created', None)
1148
        d.pop('info', None)
1149
        d.pop('affiliation', None)
1150
        d.pop('provider', None)
1151
        d.pop('third_party_identifier', None)
1152
        user = AstakosUser(**d)
1153

    
1154
        return user
1155

    
1156
    @property
1157
    def realname(self):
1158
        return '%s %s' %(self.first_name, self.last_name)
1159

    
1160
    @realname.setter
1161
    def realname(self, value):
1162
        parts = value.split(' ')
1163
        if len(parts) == 2:
1164
            self.first_name = parts[0]
1165
            self.last_name = parts[1]
1166
        else:
1167
            self.last_name = parts[0]
1168

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

    
1180
    def generate_token(self):
1181
        self.password = self.third_party_identifier
1182
        self.last_login = datetime.now()
1183
        self.token = default_token_generator.make_token(self)
1184

    
1185
    def existing_user(self):
1186
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1187
                                         auth_providers__identifier=self.third_party_identifier)
1188

    
1189
    def get_provider(self, user):
1190
        params = {
1191
            'info_data': self.info,
1192
            'affiliation': self.affiliation
1193
        }
1194
        return auth.get_provider(self.provider, user,
1195
                                 self.third_party_identifier, **params)
1196

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

    
1201

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

    
1207
    objects = ForUpdateManager()
1208

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

    
1212

    
1213
### PROJECTS ###
1214
################
1215

    
1216
class ChainManager(ForUpdateManager):
1217

    
1218
    def search_by_name(self, *search_strings):
1219
        projects = Project.objects.search_by_name(*search_strings)
1220
        chains = [p.id for p in projects]
1221
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1222
        apps = (app for app in apps if app.is_latest())
1223
        app_chains = [app.chain for app in apps if app.chain not in chains]
1224
        return chains + app_chains
1225

    
1226
    def all_full_state(self):
1227
        chains = self.all()
1228
        cids = [c.chain for c in chains]
1229
        projects = Project.objects.select_related('application').in_bulk(cids)
1230

    
1231
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1232
        chain_latest = dict(objs.values_list('chain', 'latest'))
1233

    
1234
        objs = ProjectApplication.objects.select_related('applicant')
1235
        apps = objs.in_bulk(chain_latest.values())
1236

    
1237
        d = {}
1238
        for chain in chains:
1239
            pk = chain.pk
1240
            project = projects.get(pk, None)
1241
            app = apps[chain_latest[pk]]
1242
            d[chain.pk] = chain.get_state(project, app)
1243

    
1244
        return d
1245

    
1246
    def of_project(self, project):
1247
        if project is None:
1248
            return None
1249
        try:
1250
            return self.get(chain=project.id)
1251
        except Chain.DoesNotExist:
1252
            raise AssertionError('project with no chain')
1253

    
1254

    
1255
class Chain(models.Model):
1256
    chain  =   models.AutoField(primary_key=True)
1257

    
1258
    def __str__(self):
1259
        return "%s" % (self.chain,)
1260

    
1261
    objects = ChainManager()
1262

    
1263
    PENDING            = 0
1264
    DENIED             = 3
1265
    DISMISSED          = 4
1266
    CANCELLED          = 5
1267

    
1268
    APPROVED           = 10
1269
    APPROVED_PENDING   = 11
1270
    SUSPENDED          = 12
1271
    SUSPENDED_PENDING  = 13
1272
    TERMINATED         = 14
1273
    TERMINATED_PENDING = 15
1274

    
1275
    PENDING_STATES = [PENDING,
1276
                      APPROVED_PENDING,
1277
                      SUSPENDED_PENDING,
1278
                      TERMINATED_PENDING,
1279
                      ]
1280

    
1281
    MODIFICATION_STATES = [APPROVED_PENDING,
1282
                           SUSPENDED_PENDING,
1283
                           TERMINATED_PENDING,
1284
                           ]
1285

    
1286
    RELEVANT_STATES = [PENDING,
1287
                       DENIED,
1288
                       APPROVED,
1289
                       APPROVED_PENDING,
1290
                       SUSPENDED,
1291
                       SUSPENDED_PENDING,
1292
                       TERMINATED_PENDING,
1293
                       ]
1294

    
1295
    SKIP_STATES = [DISMISSED,
1296
                   CANCELLED,
1297
                   TERMINATED]
1298

    
1299
    STATE_DISPLAY = {
1300
        PENDING            : _("Pending"),
1301
        DENIED             : _("Denied"),
1302
        DISMISSED          : _("Dismissed"),
1303
        CANCELLED          : _("Cancelled"),
1304
        APPROVED           : _("Active"),
1305
        APPROVED_PENDING   : _("Active - Pending"),
1306
        SUSPENDED          : _("Suspended"),
1307
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1308
        TERMINATED         : _("Terminated"),
1309
        TERMINATED_PENDING : _("Terminated - Pending"),
1310
        }
1311

    
1312

    
1313
    @classmethod
1314
    def _chain_state(cls, project_state, app_state):
1315
        s = CHAIN_STATE.get((project_state, app_state), None)
1316
        if s is None:
1317
            raise AssertionError('inconsistent chain state')
1318
        return s
1319

    
1320
    @classmethod
1321
    def chain_state(cls, project, app):
1322
        p_state = project.state if project else None
1323
        return cls._chain_state(p_state, app.state)
1324

    
1325
    @classmethod
1326
    def state_display(cls, s):
1327
        if s is None:
1328
            return _("Unknown")
1329
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1330

    
1331
    def last_application(self):
1332
        return self.chained_apps.order_by('-id')[0]
1333

    
1334
    def get_project(self):
1335
        try:
1336
            return self.chained_project
1337
        except Project.DoesNotExist:
1338
            return None
1339

    
1340
    def get_elements(self):
1341
        project = self.get_project()
1342
        app = self.last_application()
1343
        return project, app
1344

    
1345
    def get_state(self, project, app):
1346
        s = self.chain_state(project, app)
1347
        return s, project, app
1348

    
1349
    def full_state(self):
1350
        project, app = self.get_elements()
1351
        return self.get_state(project, app)
1352

    
1353

    
1354
def new_chain():
1355
    c = Chain.objects.create()
1356
    return c
1357

    
1358

    
1359
class ProjectApplicationManager(ForUpdateManager):
1360

    
1361
    def user_visible_projects(self, *filters, **kw_filters):
1362
        model = self.model
1363
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1364

    
1365
    def user_visible_by_chain(self, flt):
1366
        model = self.model
1367
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1368
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1369
        by_chain = dict(pending.annotate(models.Max('id')))
1370
        by_chain.update(approved.annotate(models.Max('id')))
1371
        return self.filter(flt, id__in=by_chain.values())
1372

    
1373
    def user_accessible_projects(self, user):
1374
        """
1375
        Return projects accessed by specified user.
1376
        """
1377
        if user.is_project_admin():
1378
            participates_filters = Q()
1379
        else:
1380
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1381
                                   Q(project__projectmembership__person=user)
1382

    
1383
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1384

    
1385
    def search_by_name(self, *search_strings):
1386
        q = Q()
1387
        for s in search_strings:
1388
            q = q | Q(name__icontains=s)
1389
        return self.filter(q)
1390

    
1391
    def latest_of_chain(self, chain_id):
1392
        try:
1393
            return self.filter(chain=chain_id).order_by('-id')[0]
1394
        except IndexError:
1395
            return None
1396

    
1397

    
1398
class ProjectApplication(models.Model):
1399
    applicant               =   models.ForeignKey(
1400
                                    AstakosUser,
1401
                                    related_name='projects_applied',
1402
                                    db_index=True)
1403

    
1404
    PENDING     =    0
1405
    APPROVED    =    1
1406
    REPLACED    =    2
1407
    DENIED      =    3
1408
    DISMISSED   =    4
1409
    CANCELLED   =    5
1410

    
1411
    state                   =   models.IntegerField(default=PENDING,
1412
                                                    db_index=True)
1413

    
1414
    owner                   =   models.ForeignKey(
1415
                                    AstakosUser,
1416
                                    related_name='projects_owned',
1417
                                    db_index=True)
1418

    
1419
    chain                   =   models.ForeignKey(Chain,
1420
                                                  related_name='chained_apps',
1421
                                                  db_column='chain')
1422
    precursor_application   =   models.ForeignKey('ProjectApplication',
1423
                                                  null=True,
1424
                                                  blank=True)
1425

    
1426
    name                    =   models.CharField(max_length=80)
1427
    homepage                =   models.URLField(max_length=255, null=True,
1428
                                                verify_exists=False)
1429
    description             =   models.TextField(null=True, blank=True)
1430
    start_date              =   models.DateTimeField(null=True, blank=True)
1431
    end_date                =   models.DateTimeField()
1432
    member_join_policy      =   models.IntegerField()
1433
    member_leave_policy     =   models.IntegerField()
1434
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1435
    resource_grants         =   models.ManyToManyField(
1436
                                    Resource,
1437
                                    null=True,
1438
                                    blank=True,
1439
                                    through='ProjectResourceGrant')
1440
    comments                =   models.TextField(null=True, blank=True)
1441
    issue_date              =   models.DateTimeField(auto_now_add=True)
1442
    response_date           =   models.DateTimeField(null=True, blank=True)
1443
    response                =   models.TextField(null=True, blank=True)
1444

    
1445
    objects                 =   ProjectApplicationManager()
1446

    
1447
    # Compiled queries
1448
    Q_PENDING  = Q(state=PENDING)
1449
    Q_APPROVED = Q(state=APPROVED)
1450
    Q_DENIED   = Q(state=DENIED)
1451

    
1452
    class Meta:
1453
        unique_together = ("chain", "id")
1454

    
1455
    def __unicode__(self):
1456
        return "%s applied by %s" % (self.name, self.applicant)
1457

    
1458
    # TODO: Move to a more suitable place
1459
    APPLICATION_STATE_DISPLAY = {
1460
        PENDING  : _('Pending review'),
1461
        APPROVED : _('Approved'),
1462
        REPLACED : _('Replaced'),
1463
        DENIED   : _('Denied'),
1464
        DISMISSED: _('Dismissed'),
1465
        CANCELLED: _('Cancelled')
1466
    }
1467

    
1468
    @property
1469
    def log_display(self):
1470
        return "application %s (%s) for project %s" % (
1471
            self.id, self.name, self.chain)
1472

    
1473
    def state_display(self):
1474
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1475

    
1476
    def project_state_display(self):
1477
        try:
1478
            project = self.project
1479
            return project.state_display()
1480
        except Project.DoesNotExist:
1481
            return self.state_display()
1482

    
1483
    def add_resource_policy(self, resource, uplimit):
1484
        """Raises ObjectDoesNotExist, IntegrityError"""
1485
        q = self.projectresourcegrant_set
1486
        resource = Resource.objects.get(name=resource)
1487
        q.create(resource=resource, member_capacity=uplimit)
1488

    
1489
    def members_count(self):
1490
        return self.project.approved_memberships.count()
1491

    
1492
    @property
1493
    def grants(self):
1494
        return self.projectresourcegrant_set.values('member_capacity',
1495
                                                    'resource__name')
1496

    
1497
    @property
1498
    def resource_policies(self):
1499
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1500

    
1501
    def set_resource_policies(self, policies):
1502
        for resource, uplimit in policies:
1503
            self.add_resource_policy(resource, uplimit)
1504

    
1505
    def pending_modifications_incl_me(self):
1506
        q = self.chained_applications()
1507
        q = q.filter(Q(state=self.PENDING))
1508
        return q
1509

    
1510
    def last_pending_incl_me(self):
1511
        try:
1512
            return self.pending_modifications_incl_me().order_by('-id')[0]
1513
        except IndexError:
1514
            return None
1515

    
1516
    def pending_modifications(self):
1517
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1518

    
1519
    def last_pending(self):
1520
        try:
1521
            return self.pending_modifications().order_by('-id')[0]
1522
        except IndexError:
1523
            return None
1524

    
1525
    def is_modification(self):
1526
        # if self.state != self.PENDING:
1527
        #     return False
1528
        parents = self.chained_applications().filter(id__lt=self.id)
1529
        parents = parents.filter(state__in=[self.APPROVED])
1530
        return parents.count() > 0
1531

    
1532
    def chained_applications(self):
1533
        return ProjectApplication.objects.filter(chain=self.chain)
1534

    
1535
    def is_latest(self):
1536
        return self.chained_applications().order_by('-id')[0] == self
1537

    
1538
    def has_pending_modifications(self):
1539
        return bool(self.last_pending())
1540

    
1541
    def denied_modifications(self):
1542
        q = self.chained_applications()
1543
        q = q.filter(Q(state=self.DENIED))
1544
        q = q.filter(~Q(id=self.id))
1545
        return q
1546

    
1547
    def last_denied(self):
1548
        try:
1549
            return self.denied_modifications().order_by('-id')[0]
1550
        except IndexError:
1551
            return None
1552

    
1553
    def has_denied_modifications(self):
1554
        return bool(self.last_denied())
1555

    
1556
    def is_applied(self):
1557
        try:
1558
            self.project
1559
            return True
1560
        except Project.DoesNotExist:
1561
            return False
1562

    
1563
    def get_project(self):
1564
        try:
1565
            return Project.objects.get(id=self.chain)
1566
        except Project.DoesNotExist:
1567
            return None
1568

    
1569
    def project_exists(self):
1570
        return self.get_project() is not None
1571

    
1572
    def can_cancel(self):
1573
        return self.state == self.PENDING
1574

    
1575
    def cancel(self):
1576
        if not self.can_cancel():
1577
            m = _("cannot cancel: application '%s' in state '%s'") % (
1578
                    self.id, self.state)
1579
            raise AssertionError(m)
1580

    
1581
        self.state = self.CANCELLED
1582
        self.save()
1583

    
1584
    def can_dismiss(self):
1585
        return self.state == self.DENIED
1586

    
1587
    def dismiss(self):
1588
        if not self.can_dismiss():
1589
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1590
                    self.id, self.state)
1591
            raise AssertionError(m)
1592

    
1593
        self.state = self.DISMISSED
1594
        self.save()
1595

    
1596
    def can_deny(self):
1597
        return self.state == self.PENDING
1598

    
1599
    def deny(self, reason):
1600
        if not self.can_deny():
1601
            m = _("cannot deny: application '%s' in state '%s'") % (
1602
                    self.id, self.state)
1603
            raise AssertionError(m)
1604

    
1605
        self.state = self.DENIED
1606
        self.response_date = datetime.now()
1607
        self.response = reason
1608
        self.save()
1609

    
1610
    def can_approve(self):
1611
        return self.state == self.PENDING
1612

    
1613
    def approve(self, reason):
1614
        if not self.can_approve():
1615
            m = _("cannot approve: project '%s' in state '%s'") % (
1616
                    self.name, self.state)
1617
            raise AssertionError(m) # invalid argument
1618

    
1619
        now = datetime.now()
1620
        self.state = self.APPROVED
1621
        self.response_date = now
1622
        self.response = reason
1623
        self.save()
1624

    
1625
        project = self.get_project()
1626
        if project is None:
1627
            project = Project(id=self.chain)
1628

    
1629
        project.name = self.name
1630
        project.application = self
1631
        project.last_approval_date = now
1632
        project.save()
1633
        return project
1634

    
1635
    @property
1636
    def member_join_policy_display(self):
1637
        policy = self.member_join_policy
1638
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1639

    
1640
    @property
1641
    def member_leave_policy_display(self):
1642
        policy = self.member_leave_policy
1643
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1644

    
1645
class ProjectResourceGrant(models.Model):
1646

    
1647
    resource                =   models.ForeignKey(Resource)
1648
    project_application     =   models.ForeignKey(ProjectApplication,
1649
                                                  null=True)
1650
    project_capacity        =   intDecimalField(null=True)
1651
    member_capacity         =   intDecimalField(default=0)
1652

    
1653
    objects = ExtendedManager()
1654

    
1655
    class Meta:
1656
        unique_together = ("resource", "project_application")
1657

    
1658
    def display_member_capacity(self):
1659
        if self.member_capacity:
1660
            if self.resource.unit:
1661
                return ProjectResourceGrant.display_filesize(
1662
                    self.member_capacity)
1663
            else:
1664
                if math.isinf(self.member_capacity):
1665
                    return 'Unlimited'
1666
                else:
1667
                    return self.member_capacity
1668
        else:
1669
            return 'Unlimited'
1670

    
1671
    def __str__(self):
1672
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1673
                                        self.display_member_capacity())
1674

    
1675
    @classmethod
1676
    def display_filesize(cls, value):
1677
        try:
1678
            value = float(value)
1679
        except:
1680
            return
1681
        else:
1682
            if math.isinf(value):
1683
                return 'Unlimited'
1684
            if value > 1:
1685
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1686
                                [0, 0, 0, 0, 0, 0])
1687
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1688
                quotient = float(value) / 1024**exponent
1689
                unit, value_decimals = unit_list[exponent]
1690
                format_string = '{0:.%sf} {1}' % (value_decimals)
1691
                return format_string.format(quotient, unit)
1692
            if value == 0:
1693
                return '0 bytes'
1694
            if value == 1:
1695
                return '1 byte'
1696
            else:
1697
               return '0'
1698

    
1699

    
1700
class ProjectManager(ForUpdateManager):
1701

    
1702
    def terminated_projects(self):
1703
        q = self.model.Q_TERMINATED
1704
        return self.filter(q)
1705

    
1706
    def not_terminated_projects(self):
1707
        q = ~self.model.Q_TERMINATED
1708
        return self.filter(q)
1709

    
1710
    def deactivated_projects(self):
1711
        q = self.model.Q_DEACTIVATED
1712
        return self.filter(q)
1713

    
1714
    def expired_projects(self):
1715
        q = (~Q(state=Project.TERMINATED) &
1716
              Q(application__end_date__lt=datetime.now()))
1717
        return self.filter(q)
1718

    
1719
    def search_by_name(self, *search_strings):
1720
        q = Q()
1721
        for s in search_strings:
1722
            q = q | Q(name__icontains=s)
1723
        return self.filter(q)
1724

    
1725

    
1726
class Project(models.Model):
1727

    
1728
    id                          =   models.OneToOneField(Chain,
1729
                                                      related_name='chained_project',
1730
                                                      db_column='id',
1731
                                                      primary_key=True)
1732

    
1733
    application                 =   models.OneToOneField(
1734
                                            ProjectApplication,
1735
                                            related_name='project')
1736
    last_approval_date          =   models.DateTimeField(null=True)
1737

    
1738
    members                     =   models.ManyToManyField(
1739
                                            AstakosUser,
1740
                                            through='ProjectMembership')
1741

    
1742
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1743
    deactivation_date           =   models.DateTimeField(null=True)
1744

    
1745
    creation_date               =   models.DateTimeField(auto_now_add=True)
1746
    name                        =   models.CharField(
1747
                                            max_length=80,
1748
                                            null=True,
1749
                                            db_index=True,
1750
                                            unique=True)
1751

    
1752
    APPROVED    = 1
1753
    SUSPENDED   = 10
1754
    TERMINATED  = 100
1755

    
1756
    state                       =   models.IntegerField(default=APPROVED,
1757
                                                        db_index=True)
1758

    
1759
    objects     =   ProjectManager()
1760

    
1761
    # Compiled queries
1762
    Q_TERMINATED  = Q(state=TERMINATED)
1763
    Q_SUSPENDED   = Q(state=SUSPENDED)
1764
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1765

    
1766
    def __str__(self):
1767
        return uenc(_("<project %s '%s'>") %
1768
                    (self.id, udec(self.application.name)))
1769

    
1770
    __repr__ = __str__
1771

    
1772
    def __unicode__(self):
1773
        return _("<project %s '%s'>") % (self.id, self.application.name)
1774

    
1775
    STATE_DISPLAY = {
1776
        APPROVED   : 'Active',
1777
        SUSPENDED  : 'Suspended',
1778
        TERMINATED : 'Terminated'
1779
        }
1780

    
1781
    def state_display(self):
1782
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1783

    
1784
    def expiration_info(self):
1785
        return (str(self.id), self.name, self.state_display(),
1786
                str(self.application.end_date))
1787

    
1788
    def is_deactivated(self, reason=None):
1789
        if reason is not None:
1790
            return self.state == reason
1791

    
1792
        return self.state != self.APPROVED
1793

    
1794
    ### Deactivation calls
1795

    
1796
    def terminate(self):
1797
        self.deactivation_reason = 'TERMINATED'
1798
        self.deactivation_date = datetime.now()
1799
        self.state = self.TERMINATED
1800
        self.name = None
1801
        self.save()
1802

    
1803
    def suspend(self):
1804
        self.deactivation_reason = 'SUSPENDED'
1805
        self.deactivation_date = datetime.now()
1806
        self.state = self.SUSPENDED
1807
        self.save()
1808

    
1809
    def resume(self):
1810
        self.deactivation_reason = None
1811
        self.deactivation_date = None
1812
        self.state = self.APPROVED
1813
        self.save()
1814

    
1815
    ### Logical checks
1816

    
1817
    def is_inconsistent(self):
1818
        now = datetime.now()
1819
        dates = [self.creation_date,
1820
                 self.last_approval_date,
1821
                 self.deactivation_date]
1822
        return any([date > now for date in dates])
1823

    
1824
    def is_approved(self):
1825
        return self.state == self.APPROVED
1826

    
1827
    @property
1828
    def is_alive(self):
1829
        return not self.is_terminated
1830

    
1831
    @property
1832
    def is_terminated(self):
1833
        return self.is_deactivated(self.TERMINATED)
1834

    
1835
    @property
1836
    def is_suspended(self):
1837
        return self.is_deactivated(self.SUSPENDED)
1838

    
1839
    def violates_resource_grants(self):
1840
        return False
1841

    
1842
    def violates_members_limit(self, adding=0):
1843
        application = self.application
1844
        limit = application.limit_on_members_number
1845
        if limit is None:
1846
            return False
1847
        return (len(self.approved_members) + adding > limit)
1848

    
1849

    
1850
    ### Other
1851

    
1852
    def count_pending_memberships(self):
1853
        memb_set = self.projectmembership_set
1854
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1855
        return memb_count
1856

    
1857
    def members_count(self):
1858
        return self.approved_memberships.count()
1859

    
1860
    @property
1861
    def approved_memberships(self):
1862
        query = ProjectMembership.Q_ACCEPTED_STATES
1863
        return self.projectmembership_set.filter(query)
1864

    
1865
    @property
1866
    def approved_members(self):
1867
        return [m.person for m in self.approved_memberships]
1868

    
1869

    
1870
CHAIN_STATE = {
1871
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1872
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1873
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1874
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1875
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1876

    
1877
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1878
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1879
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1880
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1881
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1882

    
1883
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1884
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1885
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1886
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1887
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1888

    
1889
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1890
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1891
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1892
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1893
    }
1894

    
1895

    
1896
class ProjectMembershipManager(ForUpdateManager):
1897

    
1898
    def any_accepted(self):
1899
        q = self.model.Q_ACTUALLY_ACCEPTED
1900
        return self.filter(q)
1901

    
1902
    def actually_accepted(self):
1903
        q = self.model.Q_ACTUALLY_ACCEPTED
1904
        return self.filter(q)
1905

    
1906
    def requested(self):
1907
        return self.filter(state=ProjectMembership.REQUESTED)
1908

    
1909
    def suspended(self):
1910
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1911

    
1912
class ProjectMembership(models.Model):
1913

    
1914
    person              =   models.ForeignKey(AstakosUser)
1915
    request_date        =   models.DateTimeField(auto_now_add=True)
1916
    project             =   models.ForeignKey(Project)
1917

    
1918
    REQUESTED           =   0
1919
    ACCEPTED            =   1
1920
    LEAVE_REQUESTED     =   5
1921
    # User deactivation
1922
    USER_SUSPENDED      =   10
1923

    
1924
    REMOVED             =   200
1925

    
1926
    ASSOCIATED_STATES   =   set([REQUESTED,
1927
                                 ACCEPTED,
1928
                                 LEAVE_REQUESTED,
1929
                                 USER_SUSPENDED,
1930
                                 ])
1931

    
1932
    ACCEPTED_STATES     =   set([ACCEPTED,
1933
                                 LEAVE_REQUESTED,
1934
                                 USER_SUSPENDED,
1935
                                 ])
1936

    
1937
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1938

    
1939
    state               =   models.IntegerField(default=REQUESTED,
1940
                                                db_index=True)
1941
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
1942
    leave_request_date  =   models.DateTimeField(null=True)
1943

    
1944
    objects     =   ProjectMembershipManager()
1945

    
1946
    # Compiled queries
1947
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1948
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1949

    
1950
    MEMBERSHIP_STATE_DISPLAY = {
1951
        REQUESTED           : _('Requested'),
1952
        ACCEPTED            : _('Accepted'),
1953
        LEAVE_REQUESTED     : _('Leave Requested'),
1954
        USER_SUSPENDED      : _('Suspended'),
1955
        REMOVED             : _('Pending removal'),
1956
        }
1957

    
1958
    USER_FRIENDLY_STATE_DISPLAY = {
1959
        REQUESTED           : _('Join requested'),
1960
        ACCEPTED            : _('Accepted member'),
1961
        LEAVE_REQUESTED     : _('Requested to leave'),
1962
        USER_SUSPENDED      : _('Suspended member'),
1963
        REMOVED             : _('Pending removal'),
1964
        }
1965

    
1966
    def state_display(self):
1967
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1968

    
1969
    def user_friendly_state_display(self):
1970
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1971

    
1972
    class Meta:
1973
        unique_together = ("person", "project")
1974
        #index_together = [["project", "state"]]
1975

    
1976
    def __str__(self):
1977
        return uenc(_("<'%s' membership in '%s'>") % (
1978
                self.person.username, self.project))
1979

    
1980
    __repr__ = __str__
1981

    
1982
    def __init__(self, *args, **kwargs):
1983
        self.state = self.REQUESTED
1984
        super(ProjectMembership, self).__init__(*args, **kwargs)
1985

    
1986
    def _set_history_item(self, reason, date=None):
1987
        if isinstance(reason, basestring):
1988
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1989

    
1990
        history_item = ProjectMembershipHistory(
1991
                            serial=self.id,
1992
                            person=self.person_id,
1993
                            project=self.project_id,
1994
                            date=date or datetime.now(),
1995
                            reason=reason)
1996
        history_item.save()
1997
        serial = history_item.id
1998

    
1999
    def can_accept(self):
2000
        return self.state == self.REQUESTED
2001

    
2002
    def accept(self):
2003
        if not self.can_accept():
2004
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2005
            raise AssertionError(m)
2006

    
2007
        now = datetime.now()
2008
        self.acceptance_date = now
2009
        self._set_history_item(reason='ACCEPT', date=now)
2010
        self.state = self.ACCEPTED
2011
        self.save()
2012

    
2013
    def can_leave(self):
2014
        return self.state in self.ACCEPTED_STATES
2015

    
2016
    def leave_request(self):
2017
        if not self.can_leave():
2018
            m = _("%s: attempt to request to leave in state '%s'") % (
2019
                self, self.state)
2020
            raise AssertionError(m)
2021

    
2022
        self.leave_request_date = datetime.now()
2023
        self.state = self.LEAVE_REQUESTED
2024
        self.save()
2025

    
2026
    def can_deny_leave(self):
2027
        return self.state == self.LEAVE_REQUESTED
2028

    
2029
    def leave_request_deny(self):
2030
        if not self.can_deny_leave():
2031
            m = _("%s: attempt to deny leave request in state '%s'") % (
2032
                self, self.state)
2033
            raise AssertionError(m)
2034

    
2035
        self.leave_request_date = None
2036
        self.state = self.ACCEPTED
2037
        self.save()
2038

    
2039
    def can_cancel_leave(self):
2040
        return self.state == self.LEAVE_REQUESTED
2041

    
2042
    def leave_request_cancel(self):
2043
        if not self.can_cancel_leave():
2044
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2045
                self, self.state)
2046
            raise AssertionError(m)
2047

    
2048
        self.leave_request_date = None
2049
        self.state = self.ACCEPTED
2050
        self.save()
2051

    
2052
    def can_remove(self):
2053
        return self.state in self.ACCEPTED_STATES
2054

    
2055
    def remove(self):
2056
        if not self.can_remove():
2057
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2058
            raise AssertionError(m)
2059

    
2060
        self._set_history_item(reason='REMOVE')
2061
        self.delete()
2062

    
2063
    def can_reject(self):
2064
        return self.state == self.REQUESTED
2065

    
2066
    def reject(self):
2067
        if not self.can_reject():
2068
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2069
            raise AssertionError(m)
2070

    
2071
        # rejected requests don't need sync,
2072
        # because they were never effected
2073
        self._set_history_item(reason='REJECT')
2074
        self.delete()
2075

    
2076
    def can_cancel(self):
2077
        return self.state == self.REQUESTED
2078

    
2079
    def cancel(self):
2080
        if not self.can_cancel():
2081
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2082
            raise AssertionError(m)
2083

    
2084
        # rejected requests don't need sync,
2085
        # because they were never effected
2086
        self._set_history_item(reason='CANCEL')
2087
        self.delete()
2088

    
2089

    
2090
class Serial(models.Model):
2091
    serial  =   models.AutoField(primary_key=True)
2092

    
2093

    
2094
class ProjectMembershipHistory(models.Model):
2095
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2096
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2097

    
2098
    person  =   models.BigIntegerField()
2099
    project =   models.BigIntegerField()
2100
    date    =   models.DateTimeField(auto_now_add=True)
2101
    reason  =   models.IntegerField()
2102
    serial  =   models.BigIntegerField()
2103

    
2104
### SIGNALS ###
2105
################
2106

    
2107
def create_astakos_user(u):
2108
    try:
2109
        AstakosUser.objects.get(user_ptr=u.pk)
2110
    except AstakosUser.DoesNotExist:
2111
        extended_user = AstakosUser(user_ptr_id=u.pk)
2112
        extended_user.__dict__.update(u.__dict__)
2113
        extended_user.save()
2114
        if not extended_user.has_auth_provider('local'):
2115
            extended_user.add_auth_provider('local')
2116
    except BaseException, e:
2117
        logger.exception(e)
2118

    
2119
def fix_superusers():
2120
    # Associate superusers with AstakosUser
2121
    admins = User.objects.filter(is_superuser=True)
2122
    for u in admins:
2123
        create_astakos_user(u)
2124

    
2125
def user_post_save(sender, instance, created, **kwargs):
2126
    if not created:
2127
        return
2128
    create_astakos_user(instance)
2129
post_save.connect(user_post_save, sender=User)
2130

    
2131
def astakosuser_post_save(sender, instance, created, **kwargs):
2132
    pass
2133

    
2134
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2135

    
2136
def resource_post_save(sender, instance, created, **kwargs):
2137
    pass
2138

    
2139
post_save.connect(resource_post_save, sender=Resource)
2140

    
2141
def renew_token(sender, instance, **kwargs):
2142
    if not instance.auth_token:
2143
        instance.renew_token()
2144
pre_save.connect(renew_token, sender=AstakosUser)
2145
pre_save.connect(renew_token, sender=Service)