Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 95f33116

History | View | Annotate | Download (72.9 kB)

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

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

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

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

    
56
from django.dispatch import Signal
57
from django.db.models import Q, 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
    PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES)
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
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
332
    #                    AstakosUserProvider model.
333
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
334
                                null=True)
335
    # ex. screen_name for twitter, eppn for shibboleth
336
    third_party_identifier = models.CharField(_('Third-party identifier'),
337
                                              max_length=255, null=True,
338
                                              blank=True)
339

    
340

    
341
    #for invitations
342
    user_level = DEFAULT_USER_LEVEL
343
    level = models.IntegerField(_('Inviter level'), default=user_level)
344
    invitations = models.IntegerField(
345
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
346

    
347
    auth_token = models.CharField(_('Authentication Token'),
348
                                  max_length=32,
349
                                  null=True,
350
                                  blank=True,
351
                                  help_text = _('Renew your authentication '
352
                                                'token. Make sure to set the new '
353
                                                'token in any client you may be '
354
                                                'using, to preserve its '
355
                                                'functionality.'))
356
    auth_token_created = models.DateTimeField(_('Token creation date'),
357
                                              null=True)
358
    auth_token_expires = models.DateTimeField(
359
        _('Token expiration date'), null=True)
360

    
361
    updated = models.DateTimeField(_('Update date'))
362
    is_verified = models.BooleanField(_('Is verified?'), default=False)
363

    
364
    email_verified = models.BooleanField(_('Email verified?'), default=False)
365

    
366
    has_credits = models.BooleanField(_('Has credits?'), default=False)
367
    has_signed_terms = models.BooleanField(
368
        _('I agree with the terms'), default=False)
369
    date_signed_terms = models.DateTimeField(
370
        _('Signed terms date'), null=True, blank=True)
371

    
372
    activation_sent = models.DateTimeField(
373
        _('Activation sent data'), null=True, blank=True)
374

    
375
    policy = models.ManyToManyField(
376
        Resource, null=True, through='AstakosUserQuota')
377

    
378
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
379

    
380
    __has_signed_terms = False
381
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
382
                                           default=False, db_index=True)
383

    
384
    objects = AstakosUserManager()
385

    
386
    forupdate = ForUpdateManager()
387

    
388
    def __init__(self, *args, **kwargs):
389
        super(AstakosUser, self).__init__(*args, **kwargs)
390
        self.__has_signed_terms = self.has_signed_terms
391
        if not self.id:
392
            self.is_active = False
393

    
394
    @property
395
    def realname(self):
396
        return '%s %s' % (self.first_name, self.last_name)
397

    
398
    @property
399
    def log_display(self):
400
        """
401
        Should be used in all logger.* calls that refer to a user so that
402
        user display is consistent across log entries.
403
        """
404
        return '%s::%s' % (self.uuid, self.email)
405

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

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

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

    
431
    def add_group(self, gname):
432
        group, _ = Group.objects.get_or_create(name=gname)
433
        self.groups.add(group)
434

    
435
    def is_project_admin(self, application_id=None):
436
        return self.uuid in astakos_settings.PROJECT_ADMINS
437

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

    
445
    @property
446
    def policies(self):
447
        return self.astakosuserquota_set.select_related().all()
448

    
449
    @policies.setter
450
    def policies(self, policies):
451
        for p in policies:
452
            p.setdefault('resource', '')
453
            p.setdefault('capacity', 0)
454
            p.setdefault('update', True)
455
            self.add_resource_policy(**p)
456

    
457
    def add_resource_policy(
458
            self, resource, capacity,
459
            update=True):
460
        """Raises ObjectDoesNotExist, IntegrityError"""
461
        resource = Resource.objects.get(name=resource)
462
        if update:
463
            AstakosUserQuota.objects.update_or_create(
464
                user=self, resource=resource, defaults={
465
                    'capacity':capacity,
466
                    })
467
        else:
468
            q = self.astakosuserquota_set
469
            q.create(
470
                resource=resource, capacity=capacity,
471
                )
472

    
473
    def get_resource_policy(self, resource):
474
        resource = Resource.objects.get(name=resource)
475
        default_capacity = resource.uplimit
476
        try:
477
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
478
            return policy, default_capacity
479
        except AstakosUserQuota.DoesNotExist:
480
            return None, default_capacity
481

    
482
    def remove_resource_policy(self, service, resource):
483
        """Raises ObjectDoesNotExist, IntegrityError"""
484
        resource = Resource.objects.get(name=resource)
485
        q = self.policies.get(resource=resource).delete()
486

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

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

    
502
        # update date_signed_terms if necessary
503
        if self.__has_signed_terms != self.has_signed_terms:
504
            self.date_signed_terms = datetime.now()
505

    
506
        self.update_uuid()
507

    
508
        if self.username != self.email.lower():
509
            # set username
510
            self.username = self.email.lower()
511

    
512
        super(AstakosUser, self).save(**kwargs)
513

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

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

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

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

    
544
    def __unicode__(self):
545
        return '%s (%s)' % (self.realname, self.email)
546

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

    
554
    def email_change_is_pending(self):
555
        return self.emailchanges.count() > 0
556

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

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

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

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

    
588
        local = self.get_auth_provider('local')._instance
589
        return local.auth_backend == 'astakos'
590

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

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

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

    
606
    def get_required_providers(self, **kwargs):
607
        return auth.REQUIRED_PROVIDERS.keys()
608

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

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

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

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

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

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

    
654
        modules = astakos_settings.IM_MODULES
655

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

    
661
        providers = sorted(providers, key=key)
662
        return providers
663

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

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

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

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

    
684
    def get_password_reset_url(self, token_generator=default_token_generator):
685
        return reverse('django.contrib.auth.views.password_reset_confirm',
686
                          kwargs={'uidb36':int_to_base36(self.id),
687
                                  'token':token_generator.make_token(self)})
688

    
689
    def get_inactive_message(self, provider_module, identifier=None):
690
        provider = self.get_auth_provider(provider_module, identifier)
691

    
692
        msg_extra = ''
693
        message = ''
694

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

    
702
        if self.activation_sent:
703
            if self.email_verified:
704
                message = msg_inactive
705
            else:
706
                message = msg_pending
707
                url = self.get_resend_activation_url()
708
                msg_extra = msg_pending_help + \
709
                            u' ' + \
710
                            '<a href="%s">%s?</a>' % (url, msg_resend)
711
        else:
712
            if astakos_settings.MODERATION_ENABLED:
713
                message = msg_pending_mod
714
            else:
715
                message = msg_pending
716
                url = self.get_resend_activation_url()
717
                msg_extra = '<a href="%s">%s?</a>' % (url, \
718
                                msg_resend)
719

    
720
        return mark_safe(message + u' '+ msg_extra)
721

    
722
    def owns_application(self, application):
723
        return application.owner == self
724

    
725
    def owns_project(self, project):
726
        return project.application.owner == self
727

    
728
    def is_associated(self, project):
729
        try:
730
            m = ProjectMembership.objects.get(person=self, project=project)
731
            return m.state in ProjectMembership.ASSOCIATED_STATES
732
        except ProjectMembership.DoesNotExist:
733
            return False
734

    
735
    def get_membership(self, project):
736
        try:
737
            return ProjectMembership.objects.get(
738
                project=project,
739
                person=self)
740
        except ProjectMembership.DoesNotExist:
741
            return None
742

    
743
    def membership_display(self, project):
744
        m = self.get_membership(project)
745
        if m is None:
746
            return _('Not a member')
747
        else:
748
            return m.user_friendly_state_display()
749

    
750
    def non_owner_can_view(self, maybe_project):
751
        if self.is_project_admin():
752
            return True
753
        if maybe_project is None:
754
            return False
755
        project = maybe_project
756
        if self.is_associated(project):
757
            return True
758
        if project.is_deactivated():
759
            return False
760
        return True
761

    
762
    def settings(self):
763
        return UserSetting.objects.filter(user=self)
764

    
765

    
766
class AstakosUserAuthProviderManager(models.Manager):
767

    
768
    def active(self, **filters):
769
        return self.filter(active=True, **filters)
770

    
771
    def remove_unverified_providers(self, provider, **filters):
772
        try:
773
            existing = self.filter(module=provider, user__email_verified=False,
774
                                   **filters)
775
            for p in existing:
776
                p.user.delete()
777
        except:
778
            pass
779

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

    
787
    def verified(self, provider, **filters):
788
        try:
789
            return self.get(module=provider, user__email_verified=True,
790
                            **filters).settings
791
        except AstakosUserAuthProvider.DoesNotExist:
792
            return None
793

    
794

    
795
class AuthProviderPolicyProfileManager(models.Manager):
796

    
797
    def active(self):
798
        return self.filter(active=True)
799

    
800
    def for_user(self, user, provider):
801
        policies = {}
802
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
803
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
804
        exclusive_q = exclusive_q1 | exclusive_q2
805

    
806
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
807
            policies.update(profile.policies)
808

    
809
        user_groups = user.groups.all().values('pk')
810
        for profile in self.active().filter(groups__in=user_groups).filter(
811
                exclusive_q):
812
            policies.update(profile.policies)
813
        return policies
814

    
815
    def add_policy(self, name, provider, group_or_user, exclusive=False,
816
                   **policies):
817
        is_group = isinstance(group_or_user, Group)
818
        profile, created = self.get_or_create(name=name, provider=provider,
819
                                              is_exclusive=exclusive)
820
        profile.is_exclusive = exclusive
821
        profile.save()
822
        if is_group:
823
            profile.groups.add(group_or_user)
824
        else:
825
            profile.users.add(group_or_user)
826
        profile.set_policies(policies)
827
        profile.save()
828
        return profile
829

    
830

    
831
class AuthProviderPolicyProfile(models.Model):
832
    name = models.CharField(_('Name'), max_length=255, blank=False,
833
                            null=False, db_index=True)
834
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
835
                                null=False)
836

    
837
    # apply policies to all providers excluding the one set in provider field
838
    is_exclusive = models.BooleanField(default=False)
839

    
840
    policy_add = models.NullBooleanField(null=True, default=None)
841
    policy_remove = models.NullBooleanField(null=True, default=None)
842
    policy_create = models.NullBooleanField(null=True, default=None)
843
    policy_login = models.NullBooleanField(null=True, default=None)
844
    policy_limit = models.IntegerField(null=True, default=None)
845
    policy_required = models.NullBooleanField(null=True, default=None)
846
    policy_automoderate = models.NullBooleanField(null=True, default=None)
847
    policy_switch = models.NullBooleanField(null=True, default=None)
848

    
849
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
850
                     'automoderate')
851

    
852
    priority = models.IntegerField(null=False, default=1)
853
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
854
    users = models.ManyToManyField(AstakosUser,
855
                                   related_name='authpolicy_profiles')
856
    active = models.BooleanField(default=True)
857

    
858
    objects = AuthProviderPolicyProfileManager()
859

    
860
    class Meta:
861
        ordering = ['priority']
862

    
863
    @property
864
    def policies(self):
865
        policies = {}
866
        for pkey in self.POLICY_FIELDS:
867
            value = getattr(self, 'policy_%s' % pkey, None)
868
            if value is None:
869
                continue
870
            policies[pkey] = value
871
        return policies
872

    
873
    def set_policies(self, policies_dict):
874
        for key, value in policies_dict.iteritems():
875
            if key in self.POLICY_FIELDS:
876
                setattr(self, 'policy_%s' % key, value)
877
        return self.policies
878

    
879

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

    
898
    objects = AstakosUserAuthProviderManager()
899

    
900
    class Meta:
901
        unique_together = (('identifier', 'module', 'user'), )
902
        ordering = ('module', 'created')
903

    
904
    def __init__(self, *args, **kwargs):
905
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
906
        try:
907
            self.info = json.loads(self.info_data)
908
            if not self.info:
909
                self.info = {}
910
        except Exception, e:
911
            self.info = {}
912

    
913
        for key,value in self.info.iteritems():
914
            setattr(self, 'info_%s' % key, value)
915

    
916
    @property
917
    def settings(self):
918
        extra_data = {}
919

    
920
        info_data = {}
921
        if self.info_data:
922
            info_data = json.loads(self.info_data)
923

    
924
        extra_data['info'] = info_data
925

    
926
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
927
            extra_data[key] = getattr(self, key)
928

    
929
        extra_data['instance'] = self
930
        return auth.get_provider(self.module, self.user,
931
                                           self.identifier, **extra_data)
932

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

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

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

    
947

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

    
975
    update_or_create = _update_or_create
976

    
977

    
978
class AstakosUserQuota(models.Model):
979
    objects = ExtendedManager()
980
    capacity = intDecimalField()
981
    resource = models.ForeignKey(Resource)
982
    user = models.ForeignKey(AstakosUser)
983

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

    
987

    
988
class ApprovalTerms(models.Model):
989
    """
990
    Model for approval terms
991
    """
992

    
993
    date = models.DateTimeField(
994
        _('Issue date'), db_index=True, auto_now_add=True)
995
    location = models.CharField(_('Terms location'), max_length=255)
996

    
997

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

    
1011
    def __init__(self, *args, **kwargs):
1012
        super(Invitation, self).__init__(*args, **kwargs)
1013
        if not self.id:
1014
            self.code = _generate_invitation_code()
1015

    
1016
    def consume(self):
1017
        self.is_consumed = True
1018
        self.consumed = datetime.now()
1019
        self.save()
1020

    
1021
    def __unicode__(self):
1022
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1023

    
1024

    
1025
class EmailChangeManager(models.Manager):
1026

    
1027
    @transaction.commit_on_success
1028
    def change_email(self, activation_key):
1029
        """
1030
        Validate an activation key and change the corresponding
1031
        ``User`` if valid.
1032

1033
        If the key is valid and has not expired, return the ``User``
1034
        after activating.
1035

1036
        If the key is not valid or has expired, return ``None``.
1037

1038
        If the key is valid but the ``User`` is already active,
1039
        return ``None``.
1040

1041
        After successful email change the activation record is deleted.
1042

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

    
1072

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

    
1085
    objects = EmailChangeManager()
1086

    
1087
    def get_url(self):
1088
        return reverse('email_change_confirm',
1089
                      kwargs={'activation_key': self.activation_key})
1090

    
1091
    def activation_key_expired(self):
1092
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1093
        return self.requested_at + expiration_date < datetime.now()
1094

    
1095

    
1096
class AdditionalMail(models.Model):
1097
    """
1098
    Model for registring invitations
1099
    """
1100
    owner = models.ForeignKey(AstakosUser)
1101
    email = models.EmailField()
1102

    
1103

    
1104
def _generate_invitation_code():
1105
    while True:
1106
        code = randint(1, 2L ** 63 - 1)
1107
        try:
1108
            Invitation.objects.get(code=code)
1109
            # An invitation with this code already exists, try again
1110
        except Invitation.DoesNotExist:
1111
            return code
1112

    
1113

    
1114
def get_latest_terms():
1115
    try:
1116
        term = ApprovalTerms.objects.order_by('-id')[0]
1117
        return term
1118
    except IndexError:
1119
        pass
1120
    return None
1121

    
1122

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

    
1142
    class Meta:
1143
        unique_together = ("provider", "third_party_identifier")
1144

    
1145
    def get_user_instance(self):
1146
        d = self.__dict__
1147
        d.pop('_state', None)
1148
        d.pop('id', None)
1149
        d.pop('token', None)
1150
        d.pop('created', None)
1151
        d.pop('info', 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 get_project(self):
1474
        try:
1475
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1476
            return Project
1477
        except Project.DoesNotExist, e:
1478
            return None
1479

    
1480
    def state_display(self):
1481
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1482

    
1483
    def project_state_display(self):
1484
        try:
1485
            project = self.project
1486
            return project.state_display()
1487
        except Project.DoesNotExist:
1488
            return self.state_display()
1489

    
1490
    def add_resource_policy(self, resource, uplimit):
1491
        """Raises ObjectDoesNotExist, IntegrityError"""
1492
        q = self.projectresourcegrant_set
1493
        resource = Resource.objects.get(name=resource)
1494
        q.create(resource=resource, member_capacity=uplimit)
1495

    
1496
    def members_count(self):
1497
        return self.project.approved_memberships.count()
1498

    
1499
    @property
1500
    def grants(self):
1501
        return self.projectresourcegrant_set.values('member_capacity',
1502
                                                    'resource__name')
1503

    
1504
    @property
1505
    def resource_policies(self):
1506
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1507

    
1508
    def set_resource_policies(self, policies):
1509
        for resource, uplimit in policies:
1510
            self.add_resource_policy(resource, uplimit)
1511

    
1512
    def pending_modifications_incl_me(self):
1513
        q = self.chained_applications()
1514
        q = q.filter(Q(state=self.PENDING))
1515
        return q
1516

    
1517
    def last_pending_incl_me(self):
1518
        try:
1519
            return self.pending_modifications_incl_me().order_by('-id')[0]
1520
        except IndexError:
1521
            return None
1522

    
1523
    def pending_modifications(self):
1524
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1525

    
1526
    def last_pending(self):
1527
        try:
1528
            return self.pending_modifications().order_by('-id')[0]
1529
        except IndexError:
1530
            return None
1531

    
1532
    def is_modification(self):
1533
        # if self.state != self.PENDING:
1534
        #     return False
1535
        parents = self.chained_applications().filter(id__lt=self.id)
1536
        parents = parents.filter(state__in=[self.APPROVED])
1537
        return parents.count() > 0
1538

    
1539
    def chained_applications(self):
1540
        return ProjectApplication.objects.filter(chain=self.chain)
1541

    
1542
    def is_latest(self):
1543
        return self.chained_applications().order_by('-id')[0] == self
1544

    
1545
    def has_pending_modifications(self):
1546
        return bool(self.last_pending())
1547

    
1548
    def denied_modifications(self):
1549
        q = self.chained_applications()
1550
        q = q.filter(Q(state=self.DENIED))
1551
        q = q.filter(~Q(id=self.id))
1552
        return q
1553

    
1554
    def last_denied(self):
1555
        try:
1556
            return self.denied_modifications().order_by('-id')[0]
1557
        except IndexError:
1558
            return None
1559

    
1560
    def has_denied_modifications(self):
1561
        return bool(self.last_denied())
1562

    
1563
    def is_applied(self):
1564
        try:
1565
            self.project
1566
            return True
1567
        except Project.DoesNotExist:
1568
            return False
1569

    
1570
    def get_project(self):
1571
        try:
1572
            return Project.objects.get(id=self.chain)
1573
        except Project.DoesNotExist:
1574
            return None
1575

    
1576
    def project_exists(self):
1577
        return self.get_project() is not None
1578

    
1579
    def _get_project_for_update(self):
1580
        try:
1581
            objects = Project.objects
1582
            project = objects.get_for_update(id=self.chain)
1583
            return project
1584
        except Project.DoesNotExist:
1585
            return None
1586

    
1587
    def can_cancel(self):
1588
        return self.state == self.PENDING
1589

    
1590
    def cancel(self):
1591
        if not self.can_cancel():
1592
            m = _("cannot cancel: application '%s' in state '%s'") % (
1593
                    self.id, self.state)
1594
            raise AssertionError(m)
1595

    
1596
        self.state = self.CANCELLED
1597
        self.save()
1598

    
1599
    def can_dismiss(self):
1600
        return self.state == self.DENIED
1601

    
1602
    def dismiss(self):
1603
        if not self.can_dismiss():
1604
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1605
                    self.id, self.state)
1606
            raise AssertionError(m)
1607

    
1608
        self.state = self.DISMISSED
1609
        self.save()
1610

    
1611
    def can_deny(self):
1612
        return self.state == self.PENDING
1613

    
1614
    def deny(self, reason):
1615
        if not self.can_deny():
1616
            m = _("cannot deny: application '%s' in state '%s'") % (
1617
                    self.id, self.state)
1618
            raise AssertionError(m)
1619

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

    
1625
    def can_approve(self):
1626
        return self.state == self.PENDING
1627

    
1628
    def approve(self, reason):
1629
        new_project_name = self.name
1630
        if not self.can_approve():
1631
            m = _("cannot approve: project '%s' in state '%s'") % (
1632
                    new_project_name, self.state)
1633
            raise AssertionError(m) # invalid argument
1634

    
1635
        now = datetime.now()
1636
        project = self._get_project_for_update()
1637

    
1638
        try:
1639
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1640
            conflicting_project = Project.objects.get(q)
1641
            if (conflicting_project != project):
1642
                m = (_("cannot approve: project with name '%s' "
1643
                       "already exists (id: %s)") % (
1644
                        new_project_name, conflicting_project.id))
1645
                raise PermissionDenied(m) # invalid argument
1646
        except Project.DoesNotExist:
1647
            pass
1648

    
1649
        new_project = False
1650
        if project is None:
1651
            new_project = True
1652
            project = Project(id=self.chain)
1653

    
1654
        project.name = new_project_name
1655
        project.application = self
1656
        project.last_approval_date = now
1657

    
1658
        project.save()
1659

    
1660
        self.state = self.APPROVED
1661
        self.response_date = now
1662
        self.response = reason
1663
        self.save()
1664
        return project
1665

    
1666
    @property
1667
    def member_join_policy_display(self):
1668
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1669

    
1670
    @property
1671
    def member_leave_policy_display(self):
1672
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1673

    
1674
class ProjectResourceGrant(models.Model):
1675

    
1676
    resource                =   models.ForeignKey(Resource)
1677
    project_application     =   models.ForeignKey(ProjectApplication,
1678
                                                  null=True)
1679
    project_capacity        =   intDecimalField(null=True)
1680
    member_capacity         =   intDecimalField(default=0)
1681

    
1682
    objects = ExtendedManager()
1683

    
1684
    class Meta:
1685
        unique_together = ("resource", "project_application")
1686

    
1687
    def display_member_capacity(self):
1688
        if self.member_capacity:
1689
            if self.resource.unit:
1690
                return ProjectResourceGrant.display_filesize(
1691
                    self.member_capacity)
1692
            else:
1693
                if math.isinf(self.member_capacity):
1694
                    return 'Unlimited'
1695
                else:
1696
                    return self.member_capacity
1697
        else:
1698
            return 'Unlimited'
1699

    
1700
    def __str__(self):
1701
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1702
                                        self.display_member_capacity())
1703

    
1704
    @classmethod
1705
    def display_filesize(cls, value):
1706
        try:
1707
            value = float(value)
1708
        except:
1709
            return
1710
        else:
1711
            if math.isinf(value):
1712
                return 'Unlimited'
1713
            if value > 1:
1714
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1715
                                [0, 0, 0, 0, 0, 0])
1716
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1717
                quotient = float(value) / 1024**exponent
1718
                unit, value_decimals = unit_list[exponent]
1719
                format_string = '{0:.%sf} {1}' % (value_decimals)
1720
                return format_string.format(quotient, unit)
1721
            if value == 0:
1722
                return '0 bytes'
1723
            if value == 1:
1724
                return '1 byte'
1725
            else:
1726
               return '0'
1727

    
1728

    
1729
class ProjectManager(ForUpdateManager):
1730

    
1731
    def terminated_projects(self):
1732
        q = self.model.Q_TERMINATED
1733
        return self.filter(q)
1734

    
1735
    def not_terminated_projects(self):
1736
        q = ~self.model.Q_TERMINATED
1737
        return self.filter(q)
1738

    
1739
    def deactivated_projects(self):
1740
        q = self.model.Q_DEACTIVATED
1741
        return self.filter(q)
1742

    
1743
    def expired_projects(self):
1744
        q = (~Q(state=Project.TERMINATED) &
1745
              Q(application__end_date__lt=datetime.now()))
1746
        return self.filter(q)
1747

    
1748
    def search_by_name(self, *search_strings):
1749
        q = Q()
1750
        for s in search_strings:
1751
            q = q | Q(name__icontains=s)
1752
        return self.filter(q)
1753

    
1754

    
1755
class Project(models.Model):
1756

    
1757
    id                          =   models.OneToOneField(Chain,
1758
                                                      related_name='chained_project',
1759
                                                      db_column='id',
1760
                                                      primary_key=True)
1761

    
1762
    application                 =   models.OneToOneField(
1763
                                            ProjectApplication,
1764
                                            related_name='project')
1765
    last_approval_date          =   models.DateTimeField(null=True)
1766

    
1767
    members                     =   models.ManyToManyField(
1768
                                            AstakosUser,
1769
                                            through='ProjectMembership')
1770

    
1771
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1772
    deactivation_date           =   models.DateTimeField(null=True)
1773

    
1774
    creation_date               =   models.DateTimeField(auto_now_add=True)
1775
    name                        =   models.CharField(
1776
                                            max_length=80,
1777
                                            null=True,
1778
                                            db_index=True,
1779
                                            unique=True)
1780

    
1781
    APPROVED    = 1
1782
    SUSPENDED   = 10
1783
    TERMINATED  = 100
1784

    
1785
    state                       =   models.IntegerField(default=APPROVED,
1786
                                                        db_index=True)
1787

    
1788
    objects     =   ProjectManager()
1789

    
1790
    # Compiled queries
1791
    Q_TERMINATED  = Q(state=TERMINATED)
1792
    Q_SUSPENDED   = Q(state=SUSPENDED)
1793
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1794

    
1795
    def __str__(self):
1796
        return uenc(_("<project %s '%s'>") %
1797
                    (self.id, udec(self.application.name)))
1798

    
1799
    __repr__ = __str__
1800

    
1801
    def __unicode__(self):
1802
        return _("<project %s '%s'>") % (self.id, self.application.name)
1803

    
1804
    STATE_DISPLAY = {
1805
        APPROVED   : 'Active',
1806
        SUSPENDED  : 'Suspended',
1807
        TERMINATED : 'Terminated'
1808
        }
1809

    
1810
    def state_display(self):
1811
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1812

    
1813
    def expiration_info(self):
1814
        return (str(self.id), self.name, self.state_display(),
1815
                str(self.application.end_date))
1816

    
1817
    def is_deactivated(self, reason=None):
1818
        if reason is not None:
1819
            return self.state == reason
1820

    
1821
        return self.state != self.APPROVED
1822

    
1823
    ### Deactivation calls
1824

    
1825
    def terminate(self):
1826
        self.deactivation_reason = 'TERMINATED'
1827
        self.deactivation_date = datetime.now()
1828
        self.state = self.TERMINATED
1829
        self.name = None
1830
        self.save()
1831

    
1832
    def suspend(self):
1833
        self.deactivation_reason = 'SUSPENDED'
1834
        self.deactivation_date = datetime.now()
1835
        self.state = self.SUSPENDED
1836
        self.save()
1837

    
1838
    def resume(self):
1839
        self.deactivation_reason = None
1840
        self.deactivation_date = None
1841
        self.state = self.APPROVED
1842
        self.save()
1843

    
1844
    ### Logical checks
1845

    
1846
    def is_inconsistent(self):
1847
        now = datetime.now()
1848
        dates = [self.creation_date,
1849
                 self.last_approval_date,
1850
                 self.deactivation_date]
1851
        return any([date > now for date in dates])
1852

    
1853
    def is_approved(self):
1854
        return self.state == self.APPROVED
1855

    
1856
    @property
1857
    def is_alive(self):
1858
        return not self.is_terminated
1859

    
1860
    @property
1861
    def is_terminated(self):
1862
        return self.is_deactivated(self.TERMINATED)
1863

    
1864
    @property
1865
    def is_suspended(self):
1866
        return self.is_deactivated(self.SUSPENDED)
1867

    
1868
    def violates_resource_grants(self):
1869
        return False
1870

    
1871
    def violates_members_limit(self, adding=0):
1872
        application = self.application
1873
        limit = application.limit_on_members_number
1874
        if limit is None:
1875
            return False
1876
        return (len(self.approved_members) + adding > limit)
1877

    
1878

    
1879
    ### Other
1880

    
1881
    def count_pending_memberships(self):
1882
        memb_set = self.projectmembership_set
1883
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1884
        return memb_count
1885

    
1886
    def members_count(self):
1887
        return self.approved_memberships.count()
1888

    
1889
    @property
1890
    def approved_memberships(self):
1891
        query = ProjectMembership.Q_ACCEPTED_STATES
1892
        return self.projectmembership_set.filter(query)
1893

    
1894
    @property
1895
    def approved_members(self):
1896
        return [m.person for m in self.approved_memberships]
1897

    
1898

    
1899
CHAIN_STATE = {
1900
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1901
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1902
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1903
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1904
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1905

    
1906
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1907
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1908
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1909
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1910
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1911

    
1912
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1913
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1914
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1915
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1916
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1917

    
1918
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1919
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1920
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1921
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1922
    }
1923

    
1924

    
1925
class ProjectMembershipManager(ForUpdateManager):
1926

    
1927
    def any_accepted(self):
1928
        q = self.model.Q_ACTUALLY_ACCEPTED
1929
        return self.filter(q)
1930

    
1931
    def actually_accepted(self):
1932
        q = self.model.Q_ACTUALLY_ACCEPTED
1933
        return self.filter(q)
1934

    
1935
    def requested(self):
1936
        return self.filter(state=ProjectMembership.REQUESTED)
1937

    
1938
    def suspended(self):
1939
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1940

    
1941
class ProjectMembership(models.Model):
1942

    
1943
    person              =   models.ForeignKey(AstakosUser)
1944
    request_date        =   models.DateTimeField(auto_now_add=True)
1945
    project             =   models.ForeignKey(Project)
1946

    
1947
    REQUESTED           =   0
1948
    ACCEPTED            =   1
1949
    LEAVE_REQUESTED     =   5
1950
    # User deactivation
1951
    USER_SUSPENDED      =   10
1952

    
1953
    REMOVED             =   200
1954

    
1955
    ASSOCIATED_STATES   =   set([REQUESTED,
1956
                                 ACCEPTED,
1957
                                 LEAVE_REQUESTED,
1958
                                 USER_SUSPENDED,
1959
                                 ])
1960

    
1961
    ACCEPTED_STATES     =   set([ACCEPTED,
1962
                                 LEAVE_REQUESTED,
1963
                                 USER_SUSPENDED,
1964
                                 ])
1965

    
1966
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1967

    
1968
    state               =   models.IntegerField(default=REQUESTED,
1969
                                                db_index=True)
1970
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
1971
    leave_request_date  =   models.DateTimeField(null=True)
1972

    
1973
    objects     =   ProjectMembershipManager()
1974

    
1975
    # Compiled queries
1976
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1977
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1978

    
1979
    MEMBERSHIP_STATE_DISPLAY = {
1980
        REQUESTED           : _('Requested'),
1981
        ACCEPTED            : _('Accepted'),
1982
        LEAVE_REQUESTED     : _('Leave Requested'),
1983
        USER_SUSPENDED      : _('Suspended'),
1984
        REMOVED             : _('Pending removal'),
1985
        }
1986

    
1987
    USER_FRIENDLY_STATE_DISPLAY = {
1988
        REQUESTED           : _('Join requested'),
1989
        ACCEPTED            : _('Accepted member'),
1990
        LEAVE_REQUESTED     : _('Requested to leave'),
1991
        USER_SUSPENDED      : _('Suspended member'),
1992
        REMOVED             : _('Pending removal'),
1993
        }
1994

    
1995
    def state_display(self):
1996
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1997

    
1998
    def user_friendly_state_display(self):
1999
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2000

    
2001
    class Meta:
2002
        unique_together = ("person", "project")
2003
        #index_together = [["project", "state"]]
2004

    
2005
    def __str__(self):
2006
        return uenc(_("<'%s' membership in '%s'>") % (
2007
                self.person.username, self.project))
2008

    
2009
    __repr__ = __str__
2010

    
2011
    def __init__(self, *args, **kwargs):
2012
        self.state = self.REQUESTED
2013
        super(ProjectMembership, self).__init__(*args, **kwargs)
2014

    
2015
    def _set_history_item(self, reason, date=None):
2016
        if isinstance(reason, basestring):
2017
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2018

    
2019
        history_item = ProjectMembershipHistory(
2020
                            serial=self.id,
2021
                            person=self.person_id,
2022
                            project=self.project_id,
2023
                            date=date or datetime.now(),
2024
                            reason=reason)
2025
        history_item.save()
2026
        serial = history_item.id
2027

    
2028
    def can_accept(self):
2029
        return self.state == self.REQUESTED
2030

    
2031
    def accept(self):
2032
        if not self.can_accept():
2033
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2034
            raise AssertionError(m)
2035

    
2036
        now = datetime.now()
2037
        self.acceptance_date = now
2038
        self._set_history_item(reason='ACCEPT', date=now)
2039
        self.state = self.ACCEPTED
2040
        self.save()
2041

    
2042
    def can_leave(self):
2043
        return self.state in self.ACCEPTED_STATES
2044

    
2045
    def leave_request(self):
2046
        if not self.can_leave():
2047
            m = _("%s: attempt to request to leave in state '%s'") % (
2048
                self, self.state)
2049
            raise AssertionError(m)
2050

    
2051
        self.leave_request_date = datetime.now()
2052
        self.state = self.LEAVE_REQUESTED
2053
        self.save()
2054

    
2055
    def can_deny_leave(self):
2056
        return self.state == self.LEAVE_REQUESTED
2057

    
2058
    def leave_request_deny(self):
2059
        if not self.can_deny_leave():
2060
            m = _("%s: attempt to deny leave request in state '%s'") % (
2061
                self, self.state)
2062
            raise AssertionError(m)
2063

    
2064
        self.leave_request_date = None
2065
        self.state = self.ACCEPTED
2066
        self.save()
2067

    
2068
    def can_cancel_leave(self):
2069
        return self.state == self.LEAVE_REQUESTED
2070

    
2071
    def leave_request_cancel(self):
2072
        if not self.can_cancel_leave():
2073
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2074
                self, self.state)
2075
            raise AssertionError(m)
2076

    
2077
        self.leave_request_date = None
2078
        self.state = self.ACCEPTED
2079
        self.save()
2080

    
2081
    def can_remove(self):
2082
        return self.state in self.ACCEPTED_STATES
2083

    
2084
    def remove(self):
2085
        if not self.can_remove():
2086
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2087
            raise AssertionError(m)
2088

    
2089
        self._set_history_item(reason='REMOVE')
2090
        self.delete()
2091

    
2092
    def can_reject(self):
2093
        return self.state == self.REQUESTED
2094

    
2095
    def reject(self):
2096
        if not self.can_reject():
2097
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2098
            raise AssertionError(m)
2099

    
2100
        # rejected requests don't need sync,
2101
        # because they were never effected
2102
        self._set_history_item(reason='REJECT')
2103
        self.delete()
2104

    
2105
    def can_cancel(self):
2106
        return self.state == self.REQUESTED
2107

    
2108
    def cancel(self):
2109
        if not self.can_cancel():
2110
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2111
            raise AssertionError(m)
2112

    
2113
        # rejected requests don't need sync,
2114
        # because they were never effected
2115
        self._set_history_item(reason='CANCEL')
2116
        self.delete()
2117

    
2118

    
2119
class Serial(models.Model):
2120
    serial  =   models.AutoField(primary_key=True)
2121

    
2122

    
2123
class ProjectMembershipHistory(models.Model):
2124
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2125
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2126

    
2127
    person  =   models.BigIntegerField()
2128
    project =   models.BigIntegerField()
2129
    date    =   models.DateTimeField(auto_now_add=True)
2130
    reason  =   models.IntegerField()
2131
    serial  =   models.BigIntegerField()
2132

    
2133
### SIGNALS ###
2134
################
2135

    
2136
def create_astakos_user(u):
2137
    try:
2138
        AstakosUser.objects.get(user_ptr=u.pk)
2139
    except AstakosUser.DoesNotExist:
2140
        extended_user = AstakosUser(user_ptr_id=u.pk)
2141
        extended_user.__dict__.update(u.__dict__)
2142
        extended_user.save()
2143
        if not extended_user.has_auth_provider('local'):
2144
            extended_user.add_auth_provider('local')
2145
    except BaseException, e:
2146
        logger.exception(e)
2147

    
2148
def fix_superusers():
2149
    # Associate superusers with AstakosUser
2150
    admins = User.objects.filter(is_superuser=True)
2151
    for u in admins:
2152
        create_astakos_user(u)
2153

    
2154
def user_post_save(sender, instance, created, **kwargs):
2155
    if not created:
2156
        return
2157
    create_astakos_user(instance)
2158
post_save.connect(user_post_save, sender=User)
2159

    
2160
def astakosuser_post_save(sender, instance, created, **kwargs):
2161
    pass
2162

    
2163
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2164

    
2165
def resource_post_save(sender, instance, created, **kwargs):
2166
    pass
2167

    
2168
post_save.connect(resource_post_save, sender=Resource)
2169

    
2170
def renew_token(sender, instance, **kwargs):
2171
    if not instance.auth_token:
2172
        instance.renew_token()
2173
pre_save.connect(renew_token, sender=AstakosUser)
2174
pre_save.connect(renew_token, sender=Service)