Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 6cc50d6a

History | View | Annotate | Download (75.6 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
from collections import defaultdict, namedtuple
48

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

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

    
68
from astakos.im.settings import (
69
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
70
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
71
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA,
72
    PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES, PROJECT_ADMINS)
73
from astakos.im import settings as astakos_settings
74
from astakos.im import auth_providers as auth
75

    
76
import astakos.im.messages as astakos_messages
77
from synnefo.lib.db.managers import ForUpdateManager
78

    
79
from astakos.quotaholder.api import QH_PRACTICALLY_INFINITE
80
from synnefo.lib.db.intdecimalfield import intDecimalField
81
from synnefo.util.text import uenc, udec
82

    
83
logger = logging.getLogger(__name__)
84

    
85
DEFAULT_CONTENT_TYPE = None
86
_content_type = None
87

    
88
def get_content_type():
89
    global _content_type
90
    if _content_type is not None:
91
        return _content_type
92

    
93
    try:
94
        content_type = ContentType.objects.get(app_label='im', model='astakosuser')
95
    except:
96
        content_type = DEFAULT_CONTENT_TYPE
97
    _content_type = content_type
98
    return content_type
99

    
100
RESOURCE_SEPARATOR = '.'
101

    
102
inf = float('inf')
103

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

    
115
    class Meta:
116
        ordering = ('order', )
117

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

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

    
131
    def __str__(self):
132
        return self.name
133

    
134
    @property
135
    def resources(self):
136
        return self.resource_set.all()
137

    
138
    @resources.setter
139
    def resources(self, resources):
140
        for s in resources:
141
            self.resource_set.create(**s)
142

    
143

    
144
class ResourceMetadata(models.Model):
145
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
146
    value = models.CharField(_('Value'), max_length=255)
147

    
148
_presentation_data = {}
149
def get_presentation(resource):
150
    global _presentation_data
151
    presentation = _presentation_data.get(resource, {})
152
    if not presentation:
153
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
154
        presentation = resource_presentation.get(resource, {})
155
        _presentation_data[resource] = presentation
156
    return presentation
157

    
158
class Resource(models.Model):
159
    name = models.CharField(_('Name'), max_length=255)
160
    meta = models.ManyToManyField(ResourceMetadata)
161
    service = models.ForeignKey(Service)
162
    desc = models.TextField(_('Description'), null=True)
163
    unit = models.CharField(_('Name'), null=True, max_length=255)
164
    group = models.CharField(_('Group'), null=True, max_length=255)
165
    uplimit = intDecimalField(default=0)
166

    
167
    class Meta:
168
        unique_together = ("service", "name")
169

    
170
    def __str__(self):
171
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
172

    
173
    def full_name(self):
174
        return str(self)
175

    
176
    def get_info(self):
177
        return {'service': str(self.service),
178
                'description': self.desc,
179
                'unit': self.unit,
180
                }
181

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

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

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

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

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

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

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

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

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

    
236
        ss.append(service)
237

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

    
251
                rs.append(r)
252

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

    
258

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

    
265

    
266
class AstakosUserManager(UserManager):
267

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

    
277
    def get_by_email(self, email):
278
        return self.get(email=email)
279

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

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

    
292
    def verified_user_exists(self, email_or_username):
293
        return self.user_exists(email_or_username, email_verified=True)
294

    
295
    def verified(self):
296
        return self.filter(email_verified=True)
297

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

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

    
320

    
321

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

    
329
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
330
    #                    AstakosUserProvider model.
331
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
332
                                null=True)
333
    # ex. screen_name for twitter, eppn for shibboleth
334
    third_party_identifier = models.CharField(_('Third-party identifier'),
335
                                              max_length=255, null=True,
336
                                              blank=True)
337

    
338

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

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

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

    
362
    email_verified = models.BooleanField(_('Email verified?'), default=False)
363

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

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

    
373
    policy = models.ManyToManyField(
374
        Resource, null=True, through='AstakosUserQuota')
375

    
376
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
377

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

    
382
    objects = AstakosUserManager()
383

    
384
    forupdate = ForUpdateManager()
385

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

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

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

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

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

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

    
429
    def is_project_admin(self, application_id=None):
430
        return self.uuid in PROJECT_ADMINS
431

    
432
    @property
433
    def invitation(self):
434
        try:
435
            return Invitation.objects.get(username=self.email)
436
        except Invitation.DoesNotExist:
437
            return None
438

    
439
    @property
440
    def policies(self):
441
        return self.astakosuserquota_set.select_related().all()
442

    
443
    @policies.setter
444
    def policies(self, policies):
445
        for p in policies:
446
            p.setdefault('resource', '')
447
            p.setdefault('capacity', 0)
448
            p.setdefault('quantity', 0)
449
            p.setdefault('update', True)
450
            self.add_resource_policy(**p)
451

    
452
    def add_resource_policy(
453
            self, resource, capacity,
454
            update=True):
455
        """Raises ObjectDoesNotExist, IntegrityError"""
456
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
457
        resource = Resource.objects.get(service__name=s, name=r)
458
        if update:
459
            AstakosUserQuota.objects.update_or_create(
460
                user=self, resource=resource, defaults={
461
                    'capacity':capacity,
462
                    })
463
        else:
464
            q = self.astakosuserquota_set
465
            q.create(
466
                resource=resource, capacity=capacity, quanity=quantity,
467
                )
468

    
469
    def get_resource_policy(self, resource):
470
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
471
        resource = Resource.objects.get(service__name=s, name=r)
472
        default_capacity = resource.uplimit
473
        try:
474
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
475
            return policy, default_capacity
476
        except AstakosUserQuota.DoesNotExist:
477
            return None, default_capacity
478

    
479
    def remove_resource_policy(self, service, resource):
480
        """Raises ObjectDoesNotExist, IntegrityError"""
481
        resource = Resource.objects.get(service__name=service, name=resource)
482
        q = self.policies.get(resource=resource).delete()
483

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

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

    
499
        # update date_signed_terms if necessary
500
        if self.__has_signed_terms != self.has_signed_terms:
501
            self.date_signed_terms = datetime.now()
502

    
503
        self.update_uuid()
504

    
505
        if self.username != self.email.lower():
506
            # set username
507
            self.username = self.email.lower()
508

    
509
        super(AstakosUser, self).save(**kwargs)
510

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
651
        modules = astakos_settings.IM_MODULES
652

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

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

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

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

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

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

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

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

    
689
        msg_extra = ''
690
        message = ''
691

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

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

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

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

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

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

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

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

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

    
759
    def settings(self):
760
        return UserSetting.objects.filter(user=self)
761

    
762

    
763
class AstakosUserAuthProviderManager(models.Manager):
764

    
765
    def active(self, **filters):
766
        return self.filter(active=True, **filters)
767

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

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

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

    
791

    
792
class AuthProviderPolicyProfileManager(models.Manager):
793

    
794
    def active(self):
795
        return self.filter(active=True)
796

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

    
803
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
804
            policies.update(profile.policies)
805

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

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

    
827

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

    
834
    # apply policies to all providers excluding the one set in provider field
835
    is_exclusive = models.BooleanField(default=False)
836

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

    
846
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
847
                     'automoderate')
848

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

    
855
    objects = AuthProviderPolicyProfileManager()
856

    
857
    class Meta:
858
        ordering = ['priority']
859

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

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

    
876

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

    
895
    objects = AstakosUserAuthProviderManager()
896

    
897
    class Meta:
898
        unique_together = (('identifier', 'module', 'user'), )
899
        ordering = ('module', 'created')
900

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

    
910
        for key,value in self.info.iteritems():
911
            setattr(self, 'info_%s' % key, value)
912

    
913
    @property
914
    def settings(self):
915
        extra_data = {}
916

    
917
        info_data = {}
918
        if self.info_data:
919
            info_data = json.loads(self.info_data)
920

    
921
        extra_data['info'] = info_data
922

    
923
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
924
            extra_data[key] = getattr(self, key)
925

    
926
        extra_data['instance'] = self
927
        return auth.get_provider(self.module, self.user,
928
                                           self.identifier, **extra_data)
929

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

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

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

    
944

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

    
972
    update_or_create = _update_or_create
973

    
974

    
975
class AstakosUserQuota(models.Model):
976
    objects = ExtendedManager()
977
    capacity = intDecimalField()
978
    quantity = intDecimalField(default=0)
979
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
980
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
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, service, resource, uplimit):
1491
        """Raises ObjectDoesNotExist, IntegrityError"""
1492
        q = self.projectresourcegrant_set
1493
        resource = Resource.objects.get(service__name=service, 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(
1502
            'member_capacity', 'resource__name', 'resource__service__name')
1503

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

    
1508
    @resource_policies.setter
1509
    def resource_policies(self, policies):
1510
        for p in policies:
1511
            service = p.get('service', None)
1512
            resource = p.get('resource', None)
1513
            uplimit = p.get('uplimit', 0)
1514
            self.add_resource_policy(service, resource, uplimit)
1515

    
1516
    def pending_modifications_incl_me(self):
1517
        q = self.chained_applications()
1518
        q = q.filter(Q(state=self.PENDING))
1519
        return q
1520

    
1521
    def last_pending_incl_me(self):
1522
        try:
1523
            return self.pending_modifications_incl_me().order_by('-id')[0]
1524
        except IndexError:
1525
            return None
1526

    
1527
    def pending_modifications(self):
1528
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1529

    
1530
    def last_pending(self):
1531
        try:
1532
            return self.pending_modifications().order_by('-id')[0]
1533
        except IndexError:
1534
            return None
1535

    
1536
    def is_modification(self):
1537
        # if self.state != self.PENDING:
1538
        #     return False
1539
        parents = self.chained_applications().filter(id__lt=self.id)
1540
        parents = parents.filter(state__in=[self.APPROVED])
1541
        return parents.count() > 0
1542

    
1543
    def chained_applications(self):
1544
        return ProjectApplication.objects.filter(chain=self.chain)
1545

    
1546
    def is_latest(self):
1547
        return self.chained_applications().order_by('-id')[0] == self
1548

    
1549
    def has_pending_modifications(self):
1550
        return bool(self.last_pending())
1551

    
1552
    def denied_modifications(self):
1553
        q = self.chained_applications()
1554
        q = q.filter(Q(state=self.DENIED))
1555
        q = q.filter(~Q(id=self.id))
1556
        return q
1557

    
1558
    def last_denied(self):
1559
        try:
1560
            return self.denied_modifications().order_by('-id')[0]
1561
        except IndexError:
1562
            return None
1563

    
1564
    def has_denied_modifications(self):
1565
        return bool(self.last_denied())
1566

    
1567
    def is_applied(self):
1568
        try:
1569
            self.project
1570
            return True
1571
        except Project.DoesNotExist:
1572
            return False
1573

    
1574
    def get_project(self):
1575
        try:
1576
            return Project.objects.get(id=self.chain)
1577
        except Project.DoesNotExist:
1578
            return None
1579

    
1580
    def project_exists(self):
1581
        return self.get_project() is not None
1582

    
1583
    def _get_project_for_update(self):
1584
        try:
1585
            objects = Project.objects
1586
            project = objects.get_for_update(id=self.chain)
1587
            return project
1588
        except Project.DoesNotExist:
1589
            return None
1590

    
1591
    def can_cancel(self):
1592
        return self.state == self.PENDING
1593

    
1594
    def cancel(self):
1595
        if not self.can_cancel():
1596
            m = _("cannot cancel: application '%s' in state '%s'") % (
1597
                    self.id, self.state)
1598
            raise AssertionError(m)
1599

    
1600
        self.state = self.CANCELLED
1601
        self.save()
1602

    
1603
    def can_dismiss(self):
1604
        return self.state == self.DENIED
1605

    
1606
    def dismiss(self):
1607
        if not self.can_dismiss():
1608
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1609
                    self.id, self.state)
1610
            raise AssertionError(m)
1611

    
1612
        self.state = self.DISMISSED
1613
        self.save()
1614

    
1615
    def can_deny(self):
1616
        return self.state == self.PENDING
1617

    
1618
    def deny(self, reason):
1619
        if not self.can_deny():
1620
            m = _("cannot deny: application '%s' in state '%s'") % (
1621
                    self.id, self.state)
1622
            raise AssertionError(m)
1623

    
1624
        self.state = self.DENIED
1625
        self.response_date = datetime.now()
1626
        self.response = reason
1627
        self.save()
1628

    
1629
    def can_approve(self):
1630
        return self.state == self.PENDING
1631

    
1632
    def approve(self, approval_user=None):
1633
        """
1634
        If approval_user then during owner membership acceptance
1635
        it is checked whether the request_user is eligible.
1636

1637
        Raises:
1638
            PermissionDenied
1639
        """
1640

    
1641
        if not transaction.is_managed():
1642
            raise AssertionError("NOPE")
1643

    
1644
        new_project_name = self.name
1645
        if not self.can_approve():
1646
            m = _("cannot approve: project '%s' in state '%s'") % (
1647
                    new_project_name, self.state)
1648
            raise AssertionError(m) # invalid argument
1649

    
1650
        now = datetime.now()
1651
        project = self._get_project_for_update()
1652

    
1653
        try:
1654
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1655
            conflicting_project = Project.objects.get(q)
1656
            if (conflicting_project != project):
1657
                m = (_("cannot approve: project with name '%s' "
1658
                       "already exists (id: %s)") % (
1659
                        new_project_name, conflicting_project.id))
1660
                raise PermissionDenied(m) # invalid argument
1661
        except Project.DoesNotExist:
1662
            pass
1663

    
1664
        new_project = False
1665
        if project is None:
1666
            new_project = True
1667
            project = Project(id=self.chain)
1668

    
1669
        project.name = new_project_name
1670
        project.application = self
1671
        project.last_approval_date = now
1672
        if not new_project:
1673
            project.is_modified = True
1674

    
1675
        project.save()
1676

    
1677
        self.state = self.APPROVED
1678
        self.response_date = now
1679
        self.save()
1680
        return project
1681

    
1682
    @property
1683
    def member_join_policy_display(self):
1684
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1685

    
1686
    @property
1687
    def member_leave_policy_display(self):
1688
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1689

    
1690
class ProjectResourceGrant(models.Model):
1691

    
1692
    resource                =   models.ForeignKey(Resource)
1693
    project_application     =   models.ForeignKey(ProjectApplication,
1694
                                                  null=True)
1695
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1696
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1697
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1698
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1699
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1700
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1701

    
1702
    objects = ExtendedManager()
1703

    
1704
    class Meta:
1705
        unique_together = ("resource", "project_application")
1706

    
1707
    def display_member_capacity(self):
1708
        if self.member_capacity:
1709
            if self.resource.unit:
1710
                return ProjectResourceGrant.display_filesize(
1711
                    self.member_capacity)
1712
            else:
1713
                if math.isinf(self.member_capacity):
1714
                    return 'Unlimited'
1715
                else:
1716
                    return self.member_capacity
1717
        else:
1718
            return 'Unlimited'
1719

    
1720
    def __str__(self):
1721
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1722
                                        self.display_member_capacity())
1723

    
1724
    @classmethod
1725
    def display_filesize(cls, value):
1726
        try:
1727
            value = float(value)
1728
        except:
1729
            return
1730
        else:
1731
            if math.isinf(value):
1732
                return 'Unlimited'
1733
            if value > 1:
1734
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1735
                                [0, 0, 0, 0, 0, 0])
1736
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1737
                quotient = float(value) / 1024**exponent
1738
                unit, value_decimals = unit_list[exponent]
1739
                format_string = '{0:.%sf} {1}' % (value_decimals)
1740
                return format_string.format(quotient, unit)
1741
            if value == 0:
1742
                return '0 bytes'
1743
            if value == 1:
1744
                return '1 byte'
1745
            else:
1746
               return '0'
1747

    
1748

    
1749
class ProjectManager(ForUpdateManager):
1750

    
1751
    def terminated_projects(self):
1752
        q = self.model.Q_TERMINATED
1753
        return self.filter(q)
1754

    
1755
    def not_terminated_projects(self):
1756
        q = ~self.model.Q_TERMINATED
1757
        return self.filter(q)
1758

    
1759
    def deactivated_projects(self):
1760
        q = self.model.Q_DEACTIVATED
1761
        return self.filter(q)
1762

    
1763
    def modified_projects(self):
1764
        return self.filter(is_modified=True)
1765

    
1766
    def expired_projects(self):
1767
        q = (~Q(state=Project.TERMINATED) &
1768
              Q(application__end_date__lt=datetime.now()))
1769
        return self.filter(q)
1770

    
1771
    def search_by_name(self, *search_strings):
1772
        q = Q()
1773
        for s in search_strings:
1774
            q = q | Q(name__icontains=s)
1775
        return self.filter(q)
1776

    
1777

    
1778
class Project(models.Model):
1779

    
1780
    id                          =   models.OneToOneField(Chain,
1781
                                                      related_name='chained_project',
1782
                                                      db_column='id',
1783
                                                      primary_key=True)
1784

    
1785
    application                 =   models.OneToOneField(
1786
                                            ProjectApplication,
1787
                                            related_name='project')
1788
    last_approval_date          =   models.DateTimeField(null=True)
1789

    
1790
    members                     =   models.ManyToManyField(
1791
                                            AstakosUser,
1792
                                            through='ProjectMembership')
1793

    
1794
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1795
    deactivation_date           =   models.DateTimeField(null=True)
1796

    
1797
    creation_date               =   models.DateTimeField(auto_now_add=True)
1798
    name                        =   models.CharField(
1799
                                            max_length=80,
1800
                                            null=True,
1801
                                            db_index=True,
1802
                                            unique=True)
1803

    
1804
    APPROVED    = 1
1805
    SUSPENDED   = 10
1806
    TERMINATED  = 100
1807

    
1808
    is_modified                 =   models.BooleanField(default=False,
1809
                                                        db_index=True)
1810
    is_active                   =   models.BooleanField(default=True,
1811
                                                        db_index=True)
1812
    state                       =   models.IntegerField(default=APPROVED,
1813
                                                        db_index=True)
1814

    
1815
    objects     =   ProjectManager()
1816

    
1817
    # Compiled queries
1818
    Q_TERMINATED  = Q(state=TERMINATED)
1819
    Q_SUSPENDED   = Q(state=SUSPENDED)
1820
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1821

    
1822
    def __str__(self):
1823
        return uenc(_("<project %s '%s'>") %
1824
                    (self.id, udec(self.application.name)))
1825

    
1826
    __repr__ = __str__
1827

    
1828
    def __unicode__(self):
1829
        return _("<project %s '%s'>") % (self.id, self.application.name)
1830

    
1831
    STATE_DISPLAY = {
1832
        APPROVED   : 'Active',
1833
        SUSPENDED  : 'Suspended',
1834
        TERMINATED : 'Terminated'
1835
        }
1836

    
1837
    def state_display(self):
1838
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1839

    
1840
    def expiration_info(self):
1841
        return (str(self.id), self.name, self.state_display(),
1842
                str(self.application.end_date))
1843

    
1844
    def is_deactivated(self, reason=None):
1845
        if reason is not None:
1846
            return self.state == reason
1847

    
1848
        return self.state != self.APPROVED
1849

    
1850
    ### Deactivation calls
1851

    
1852
    def terminate(self):
1853
        self.deactivation_reason = 'TERMINATED'
1854
        self.deactivation_date = datetime.now()
1855
        self.state = self.TERMINATED
1856
        self.name = None
1857
        self.save()
1858

    
1859
    def suspend(self):
1860
        self.deactivation_reason = 'SUSPENDED'
1861
        self.deactivation_date = datetime.now()
1862
        self.state = self.SUSPENDED
1863
        self.save()
1864

    
1865
    def resume(self):
1866
        self.deactivation_reason = None
1867
        self.deactivation_date = None
1868
        self.state = self.APPROVED
1869
        self.save()
1870

    
1871
    ### Logical checks
1872

    
1873
    def is_inconsistent(self):
1874
        now = datetime.now()
1875
        dates = [self.creation_date,
1876
                 self.last_approval_date,
1877
                 self.deactivation_date]
1878
        return any([date > now for date in dates])
1879

    
1880
    def is_active_strict(self):
1881
        return self.is_active and self.state == self.APPROVED
1882

    
1883
    def is_approved(self):
1884
        return self.state == self.APPROVED
1885

    
1886
    @property
1887
    def is_alive(self):
1888
        return not self.is_terminated
1889

    
1890
    @property
1891
    def is_terminated(self):
1892
        return self.is_deactivated(self.TERMINATED)
1893

    
1894
    @property
1895
    def is_suspended(self):
1896
        return self.is_deactivated(self.SUSPENDED)
1897

    
1898
    def violates_resource_grants(self):
1899
        return False
1900

    
1901
    def violates_members_limit(self, adding=0):
1902
        application = self.application
1903
        limit = application.limit_on_members_number
1904
        if limit is None:
1905
            return False
1906
        return (len(self.approved_members) + adding > limit)
1907

    
1908

    
1909
    ### Other
1910

    
1911
    def count_pending_memberships(self):
1912
        memb_set = self.projectmembership_set
1913
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1914
        return memb_count
1915

    
1916
    def members_count(self):
1917
        return self.approved_memberships.count()
1918

    
1919
    @property
1920
    def approved_memberships(self):
1921
        query = ProjectMembership.Q_ACCEPTED_STATES
1922
        return self.projectmembership_set.filter(query)
1923

    
1924
    @property
1925
    def approved_members(self):
1926
        return [m.person for m in self.approved_memberships]
1927

    
1928
    def add_member(self, user):
1929
        """
1930
        Raises:
1931
            django.exceptions.PermissionDenied
1932
            astakos.im.models.AstakosUser.DoesNotExist
1933
        """
1934
        if isinstance(user, (int, long)):
1935
            user = AstakosUser.objects.get(user=user)
1936

    
1937
        m, created = ProjectMembership.objects.get_or_create(
1938
            person=user, project=self
1939
        )
1940
        m.accept()
1941

    
1942
    def remove_member(self, user):
1943
        """
1944
        Raises:
1945
            django.exceptions.PermissionDenied
1946
            astakos.im.models.AstakosUser.DoesNotExist
1947
            astakos.im.models.ProjectMembership.DoesNotExist
1948
        """
1949
        if isinstance(user, (int, long)):
1950
            user = AstakosUser.objects.get(user=user)
1951

    
1952
        m = ProjectMembership.objects.get(person=user, project=self)
1953
        m.remove()
1954

    
1955

    
1956
CHAIN_STATE = {
1957
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1958
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1959
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1960
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1961
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1962

    
1963
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1964
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1965
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1966
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1967
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1968

    
1969
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1970
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1971
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1972
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1973
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1974

    
1975
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1976
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1977
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1978
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1979
    }
1980

    
1981

    
1982
class ProjectMembershipManager(ForUpdateManager):
1983

    
1984
    def any_accepted(self):
1985
        q = self.model.Q_ACTUALLY_ACCEPTED
1986
        return self.filter(q)
1987

    
1988
    def actually_accepted(self):
1989
        q = self.model.Q_ACTUALLY_ACCEPTED
1990
        return self.filter(q)
1991

    
1992
    def requested(self):
1993
        return self.filter(state=ProjectMembership.REQUESTED)
1994

    
1995
    def suspended(self):
1996
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1997

    
1998
class ProjectMembership(models.Model):
1999

    
2000
    person              =   models.ForeignKey(AstakosUser)
2001
    request_date        =   models.DateField(auto_now_add=True)
2002
    project             =   models.ForeignKey(Project)
2003

    
2004
    REQUESTED           =   0
2005
    ACCEPTED            =   1
2006
    LEAVE_REQUESTED     =   5
2007
    # User deactivation
2008
    USER_SUSPENDED      =   10
2009

    
2010
    REMOVED             =   200
2011

    
2012
    ASSOCIATED_STATES   =   set([REQUESTED,
2013
                                 ACCEPTED,
2014
                                 LEAVE_REQUESTED,
2015
                                 USER_SUSPENDED,
2016
                                 ])
2017

    
2018
    ACCEPTED_STATES     =   set([ACCEPTED,
2019
                                 LEAVE_REQUESTED,
2020
                                 USER_SUSPENDED,
2021
                                 ])
2022

    
2023
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2024

    
2025
    state               =   models.IntegerField(default=REQUESTED,
2026
                                                db_index=True)
2027
    is_pending          =   models.BooleanField(default=False, db_index=True)
2028
    is_active           =   models.BooleanField(default=False, db_index=True)
2029
    application         =   models.ForeignKey(
2030
                                ProjectApplication,
2031
                                null=True,
2032
                                related_name='memberships')
2033
    pending_application =   models.ForeignKey(
2034
                                ProjectApplication,
2035
                                null=True,
2036
                                related_name='pending_memberships')
2037
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2038

    
2039
    acceptance_date     =   models.DateField(null=True, db_index=True)
2040
    leave_request_date  =   models.DateField(null=True)
2041

    
2042
    objects     =   ProjectMembershipManager()
2043

    
2044
    # Compiled queries
2045
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2046
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2047

    
2048
    MEMBERSHIP_STATE_DISPLAY = {
2049
        REQUESTED           : _('Requested'),
2050
        ACCEPTED            : _('Accepted'),
2051
        LEAVE_REQUESTED     : _('Leave Requested'),
2052
        USER_SUSPENDED      : _('Suspended'),
2053
        REMOVED             : _('Pending removal'),
2054
        }
2055

    
2056
    USER_FRIENDLY_STATE_DISPLAY = {
2057
        REQUESTED           : _('Join requested'),
2058
        ACCEPTED            : _('Accepted member'),
2059
        LEAVE_REQUESTED     : _('Requested to leave'),
2060
        USER_SUSPENDED      : _('Suspended member'),
2061
        REMOVED             : _('Pending removal'),
2062
        }
2063

    
2064
    def state_display(self):
2065
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2066

    
2067
    def user_friendly_state_display(self):
2068
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2069

    
2070
    class Meta:
2071
        unique_together = ("person", "project")
2072
        #index_together = [["project", "state"]]
2073

    
2074
    def __str__(self):
2075
        return uenc(_("<'%s' membership in '%s'>") % (
2076
                self.person.username, self.project))
2077

    
2078
    __repr__ = __str__
2079

    
2080
    def __init__(self, *args, **kwargs):
2081
        self.state = self.REQUESTED
2082
        super(ProjectMembership, self).__init__(*args, **kwargs)
2083

    
2084
    def _set_history_item(self, reason, date=None):
2085
        if isinstance(reason, basestring):
2086
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2087

    
2088
        history_item = ProjectMembershipHistory(
2089
                            serial=self.id,
2090
                            person=self.person_id,
2091
                            project=self.project_id,
2092
                            date=date or datetime.now(),
2093
                            reason=reason)
2094
        history_item.save()
2095
        serial = history_item.id
2096

    
2097
    def can_accept(self):
2098
        return self.state == self.REQUESTED
2099

    
2100
    def accept(self):
2101
        if not self.can_accept():
2102
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2103
            raise AssertionError(m)
2104

    
2105
        now = datetime.now()
2106
        self.acceptance_date = now
2107
        self._set_history_item(reason='ACCEPT', date=now)
2108
        self.state = self.ACCEPTED
2109
        self.save()
2110

    
2111
    def can_leave(self):
2112
        return self.state in self.ACCEPTED_STATES
2113

    
2114
    def leave_request(self):
2115
        if not self.can_leave():
2116
            m = _("%s: attempt to request to leave in state '%s'") % (
2117
                self, self.state)
2118
            raise AssertionError(m)
2119

    
2120
        self.leave_request_date = datetime.now()
2121
        self.state = self.LEAVE_REQUESTED
2122
        self.save()
2123

    
2124
    def can_deny_leave(self):
2125
        return self.state == self.LEAVE_REQUESTED
2126

    
2127
    def leave_request_deny(self):
2128
        if not self.can_deny_leave():
2129
            m = _("%s: attempt to deny leave request in state '%s'") % (
2130
                self, self.state)
2131
            raise AssertionError(m)
2132

    
2133
        self.leave_request_date = None
2134
        self.state = self.ACCEPTED
2135
        self.save()
2136

    
2137
    def can_cancel_leave(self):
2138
        return self.state == self.LEAVE_REQUESTED
2139

    
2140
    def leave_request_cancel(self):
2141
        if not self.can_cancel_leave():
2142
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2143
                self, self.state)
2144
            raise AssertionError(m)
2145

    
2146
        self.leave_request_date = None
2147
        self.state = self.ACCEPTED
2148
        self.save()
2149

    
2150
    def can_remove(self):
2151
        return self.state in self.ACCEPTED_STATES
2152

    
2153
    def remove(self):
2154
        if not self.can_remove():
2155
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2156
            raise AssertionError(m)
2157

    
2158
        self._set_history_item(reason='REMOVE')
2159
        self.delete()
2160

    
2161
    def can_reject(self):
2162
        return self.state == self.REQUESTED
2163

    
2164
    def reject(self):
2165
        if not self.can_reject():
2166
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2167
            raise AssertionError(m)
2168

    
2169
        # rejected requests don't need sync,
2170
        # because they were never effected
2171
        self._set_history_item(reason='REJECT')
2172
        self.delete()
2173

    
2174
    def can_cancel(self):
2175
        return self.state == self.REQUESTED
2176

    
2177
    def cancel(self):
2178
        if not self.can_cancel():
2179
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2180
            raise AssertionError(m)
2181

    
2182
        # rejected requests don't need sync,
2183
        # because they were never effected
2184
        self._set_history_item(reason='CANCEL')
2185
        self.delete()
2186

    
2187

    
2188
class Serial(models.Model):
2189
    serial  =   models.AutoField(primary_key=True)
2190

    
2191

    
2192
class ProjectMembershipHistory(models.Model):
2193
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2194
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2195

    
2196
    person  =   models.BigIntegerField()
2197
    project =   models.BigIntegerField()
2198
    date    =   models.DateField(auto_now_add=True)
2199
    reason  =   models.IntegerField()
2200
    serial  =   models.BigIntegerField()
2201

    
2202
### SIGNALS ###
2203
################
2204

    
2205
def create_astakos_user(u):
2206
    try:
2207
        AstakosUser.objects.get(user_ptr=u.pk)
2208
    except AstakosUser.DoesNotExist:
2209
        extended_user = AstakosUser(user_ptr_id=u.pk)
2210
        extended_user.__dict__.update(u.__dict__)
2211
        extended_user.save()
2212
        if not extended_user.has_auth_provider('local'):
2213
            extended_user.add_auth_provider('local')
2214
    except BaseException, e:
2215
        logger.exception(e)
2216

    
2217
def fix_superusers():
2218
    # Associate superusers with AstakosUser
2219
    admins = User.objects.filter(is_superuser=True)
2220
    for u in admins:
2221
        create_astakos_user(u)
2222

    
2223
def user_post_save(sender, instance, created, **kwargs):
2224
    if not created:
2225
        return
2226
    create_astakos_user(instance)
2227
post_save.connect(user_post_save, sender=User)
2228

    
2229
def astakosuser_post_save(sender, instance, created, **kwargs):
2230
    pass
2231

    
2232
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2233

    
2234
def resource_post_save(sender, instance, created, **kwargs):
2235
    pass
2236

    
2237
post_save.connect(resource_post_save, sender=Resource)
2238

    
2239
def renew_token(sender, instance, **kwargs):
2240
    if not instance.auth_token:
2241
        instance.renew_token()
2242
pre_save.connect(renew_token, sender=AstakosUser)
2243
pre_save.connect(renew_token, sender=Service)