Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 4850202e

History | View | Annotate | Download (75.5 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.endpoints.qh import (
75
    send_quotas, qh_get_quotas,
76
    register_resources, qh_add_quota, QuotaLimits,
77
    )
78
from astakos.im import auth_providers as auth
79

    
80
import astakos.im.messages as astakos_messages
81
from synnefo.lib.db.managers import ForUpdateManager
82

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

    
87
logger = logging.getLogger(__name__)
88

    
89
DEFAULT_CONTENT_TYPE = None
90
_content_type = None
91

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

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

    
104
RESOURCE_SEPARATOR = '.'
105

    
106
inf = float('inf')
107

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

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

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

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

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

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

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

    
147

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
234
        ss.append(service)
235

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

    
249
                rs.append(r)
250

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

    
256

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

    
263

    
264
class AstakosUserManager(UserManager):
265

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

    
275
    def get_by_email(self, email):
276
        return self.get(email=email)
277

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

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

    
290
    def verified_user_exists(self, email_or_username):
291
        return self.user_exists(email_or_username, email_verified=True)
292

    
293
    def verified(self):
294
        return self.filter(email_verified=True)
295

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

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

    
318

    
319

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

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

    
336

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

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

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

    
360
    email_verified = models.BooleanField(_('Email verified?'), default=False)
361

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

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

    
371
    policy = models.ManyToManyField(
372
        Resource, null=True, through='AstakosUserQuota')
373

    
374
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
375

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

    
380
    objects = AstakosUserManager()
381

    
382
    forupdate = ForUpdateManager()
383

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

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

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

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

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

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

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

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

    
437
    @property
438
    def policies(self):
439
        return self.astakosuserquota_set.select_related().all()
440

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

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

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

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

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

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

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

    
501
        self.update_uuid()
502

    
503
        if self.username != self.email.lower():
504
            # set username
505
            self.username = self.email.lower()
506

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

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

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

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

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

    
539
    def __unicode__(self):
540
        return '%s (%s)' % (self.realname, self.email)
541

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

    
549
    def email_change_is_pending(self):
550
        return self.emailchanges.count() > 0
551

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

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

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

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

    
583
        local = self.get_auth_provider('local')._instance
584
        return local.auth_backend == 'astakos'
585

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

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

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

    
601
    def get_required_providers(self, **kwargs):
602
        return auth.REQUIRED_PROVIDERS.keys()
603

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

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

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

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

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

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

    
649
        modules = astakos_settings.IM_MODULES
650

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

    
656
        providers = sorted(providers, key=key)
657
        return providers
658

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

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

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

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

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

    
684
    def get_inactive_message(self, provider_module, identifier=None):
685
        provider = self.get_auth_provider(provider_module, identifier)
686

    
687
        msg_extra = ''
688
        message = ''
689

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

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

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

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

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

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

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

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

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

    
757
    def settings(self):
758
        return UserSetting.objects.filter(user=self)
759

    
760

    
761
class AstakosUserAuthProviderManager(models.Manager):
762

    
763
    def active(self, **filters):
764
        return self.filter(active=True, **filters)
765

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

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

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

    
789

    
790
class AuthProviderPolicyProfileManager(models.Manager):
791

    
792
    def active(self):
793
        return self.filter(active=True)
794

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

    
801
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
802
            policies.update(profile.policies)
803

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

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

    
825

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

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

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

    
844
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
845
                     'automoderate')
846

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

    
853
    objects = AuthProviderPolicyProfileManager()
854

    
855
    class Meta:
856
        ordering = ['priority']
857

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

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

    
874

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

    
893
    objects = AstakosUserAuthProviderManager()
894

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

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

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

    
911
    @property
912
    def settings(self):
913
        extra_data = {}
914

    
915
        info_data = {}
916
        if self.info_data:
917
            info_data = json.loads(self.info_data)
918

    
919
        extra_data['info'] = info_data
920

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

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

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

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

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

    
942

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

    
970
    update_or_create = _update_or_create
971

    
972

    
973
class AstakosUserQuota(models.Model):
974
    objects = ExtendedManager()
975
    capacity = intDecimalField()
976
    quantity = intDecimalField(default=0)
977
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
978
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
979
    resource = models.ForeignKey(Resource)
980
    user = models.ForeignKey(AstakosUser)
981

    
982
    class Meta:
983
        unique_together = ("resource", "user")
984

    
985

    
986
class ApprovalTerms(models.Model):
987
    """
988
    Model for approval terms
989
    """
990

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

    
995

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

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

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

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

    
1022

    
1023
class EmailChangeManager(models.Manager):
1024

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

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

1034
        If the key is not valid or has expired, return ``None``.
1035

1036
        If the key is valid but the ``User`` is already active,
1037
        return ``None``.
1038

1039
        After successful email change the activation record is deleted.
1040

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

    
1070

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

    
1083
    objects = EmailChangeManager()
1084

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

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

    
1093

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

    
1101

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

    
1111

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

    
1120

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

    
1140
    class Meta:
1141
        unique_together = ("provider", "third_party_identifier")
1142

    
1143
    def get_user_instance(self):
1144
        d = self.__dict__
1145
        d.pop('_state', None)
1146
        d.pop('id', None)
1147
        d.pop('token', None)
1148
        d.pop('created', None)
1149
        d.pop('info', None)
1150
        user = AstakosUser(**d)
1151

    
1152
        return user
1153

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

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

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

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

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

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

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

    
1199

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

    
1205
    objects = ForUpdateManager()
1206

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

    
1210

    
1211
### PROJECTS ###
1212
################
1213

    
1214
class ChainManager(ForUpdateManager):
1215

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

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

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

    
1232
        objs = ProjectApplication.objects.select_related('applicant')
1233
        apps = objs.in_bulk(chain_latest.values())
1234

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

    
1242
        return d
1243

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

    
1252

    
1253
class Chain(models.Model):
1254
    chain  =   models.AutoField(primary_key=True)
1255

    
1256
    def __str__(self):
1257
        return "%s" % (self.chain,)
1258

    
1259
    objects = ChainManager()
1260

    
1261
    PENDING            = 0
1262
    DENIED             = 3
1263
    DISMISSED          = 4
1264
    CANCELLED          = 5
1265

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

    
1273
    PENDING_STATES = [PENDING,
1274
                      APPROVED_PENDING,
1275
                      SUSPENDED_PENDING,
1276
                      TERMINATED_PENDING,
1277
                      ]
1278

    
1279
    MODIFICATION_STATES = [APPROVED_PENDING,
1280
                           SUSPENDED_PENDING,
1281
                           TERMINATED_PENDING,
1282
                           ]
1283

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

    
1293
    SKIP_STATES = [DISMISSED,
1294
                   CANCELLED,
1295
                   TERMINATED]
1296

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

    
1310

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

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

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

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

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

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

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

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

    
1351

    
1352
def new_chain():
1353
    c = Chain.objects.create()
1354
    return c
1355

    
1356

    
1357
class ProjectApplicationManager(ForUpdateManager):
1358

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

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

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

    
1381
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1382

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

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

    
1395

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

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

    
1409
    state                   =   models.IntegerField(default=PENDING,
1410
                                                    db_index=True)
1411

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

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

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

    
1443
    objects                 =   ProjectApplicationManager()
1444

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

    
1450
    class Meta:
1451
        unique_together = ("chain", "id")
1452

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

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

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

    
1471
    def get_project(self):
1472
        try:
1473
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1474
            return Project
1475
        except Project.DoesNotExist, e:
1476
            return None
1477

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

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

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

    
1494
    def members_count(self):
1495
        return self.project.approved_memberships.count()
1496

    
1497
    @property
1498
    def grants(self):
1499
        return self.projectresourcegrant_set.values(
1500
            'member_capacity', 'resource__name', 'resource__service__name')
1501

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

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

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

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

    
1525
    def pending_modifications(self):
1526
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1527

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

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

    
1541
    def chained_applications(self):
1542
        return ProjectApplication.objects.filter(chain=self.chain)
1543

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

    
1547
    def has_pending_modifications(self):
1548
        return bool(self.last_pending())
1549

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

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

    
1562
    def has_denied_modifications(self):
1563
        return bool(self.last_denied())
1564

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

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

    
1578
    def project_exists(self):
1579
        return self.get_project() is not None
1580

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

    
1589
    def can_cancel(self):
1590
        return self.state == self.PENDING
1591

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

    
1598
        self.state = self.CANCELLED
1599
        self.save()
1600

    
1601
    def can_dismiss(self):
1602
        return self.state == self.DENIED
1603

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

    
1610
        self.state = self.DISMISSED
1611
        self.save()
1612

    
1613
    def can_deny(self):
1614
        return self.state == self.PENDING
1615

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

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

    
1627
    def can_approve(self):
1628
        return self.state == self.PENDING
1629

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

1635
        Raises:
1636
            PermissionDenied
1637
        """
1638

    
1639
        if not transaction.is_managed():
1640
            raise AssertionError("NOPE")
1641

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

    
1648
        now = datetime.now()
1649
        project = self._get_project_for_update()
1650

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

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

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

    
1673
        project.save()
1674

    
1675
        self.state = self.APPROVED
1676
        self.response_date = now
1677
        self.save()
1678
        return project
1679

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

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

    
1688
class ProjectResourceGrant(models.Model):
1689

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

    
1700
    objects = ExtendedManager()
1701

    
1702
    class Meta:
1703
        unique_together = ("resource", "project_application")
1704

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

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

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

    
1746

    
1747
class ProjectManager(ForUpdateManager):
1748

    
1749
    def terminated_projects(self):
1750
        q = self.model.Q_TERMINATED
1751
        return self.filter(q)
1752

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

    
1757
    def deactivated_projects(self):
1758
        q = self.model.Q_DEACTIVATED
1759
        return self.filter(q)
1760

    
1761
    def modified_projects(self):
1762
        return self.filter(is_modified=True)
1763

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

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

    
1775

    
1776
class Project(models.Model):
1777

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

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

    
1788
    members                     =   models.ManyToManyField(
1789
                                            AstakosUser,
1790
                                            through='ProjectMembership')
1791

    
1792
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1793
    deactivation_date           =   models.DateTimeField(null=True)
1794

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

    
1802
    APPROVED    = 1
1803
    SUSPENDED   = 10
1804
    TERMINATED  = 100
1805

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

    
1813
    objects     =   ProjectManager()
1814

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

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

    
1824
    __repr__ = __str__
1825

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

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

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

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

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

    
1846
        return self.state != self.APPROVED
1847

    
1848
    ### Deactivation calls
1849

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

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

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

    
1869
    ### Logical checks
1870

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

    
1878
    def is_active_strict(self):
1879
        return self.is_active and self.state == self.APPROVED
1880

    
1881
    def is_approved(self):
1882
        return self.state == self.APPROVED
1883

    
1884
    @property
1885
    def is_alive(self):
1886
        return not self.is_terminated
1887

    
1888
    @property
1889
    def is_terminated(self):
1890
        return self.is_deactivated(self.TERMINATED)
1891

    
1892
    @property
1893
    def is_suspended(self):
1894
        return self.is_deactivated(self.SUSPENDED)
1895

    
1896
    def violates_resource_grants(self):
1897
        return False
1898

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

    
1906

    
1907
    ### Other
1908

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

    
1914
    def members_count(self):
1915
        return self.approved_memberships.count()
1916

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

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

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

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

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

    
1950
        m = ProjectMembership.objects.get(person=user, project=self)
1951
        m.remove()
1952

    
1953

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

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

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

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

    
1979

    
1980
class ProjectMembershipManager(ForUpdateManager):
1981

    
1982
    def any_accepted(self):
1983
        q = self.model.Q_ACTUALLY_ACCEPTED
1984
        return self.filter(q)
1985

    
1986
    def actually_accepted(self):
1987
        q = self.model.Q_ACTUALLY_ACCEPTED
1988
        return self.filter(q)
1989

    
1990
    def requested(self):
1991
        return self.filter(state=ProjectMembership.REQUESTED)
1992

    
1993
    def suspended(self):
1994
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1995

    
1996
class ProjectMembership(models.Model):
1997

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

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

    
2008
    REMOVED             =   200
2009

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

    
2016
    ACCEPTED_STATES     =   set([ACCEPTED,
2017
                                 LEAVE_REQUESTED,
2018
                                 USER_SUSPENDED,
2019
                                 ])
2020

    
2021
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2022

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

    
2037
    acceptance_date     =   models.DateField(null=True, db_index=True)
2038
    leave_request_date  =   models.DateField(null=True)
2039

    
2040
    objects     =   ProjectMembershipManager()
2041

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

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

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

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

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

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

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

    
2076
    __repr__ = __str__
2077

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

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

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

    
2095
    def can_accept(self):
2096
        return self.state == self.REQUESTED
2097

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

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

    
2109
    def can_leave(self):
2110
        return self.state in self.ACCEPTED_STATES
2111

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

    
2118
        self.leave_request_date = datetime.now()
2119
        self.state = self.LEAVE_REQUESTED
2120
        self.save()
2121

    
2122
    def can_deny_leave(self):
2123
        return self.state == self.LEAVE_REQUESTED
2124

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

    
2131
        self.leave_request_date = None
2132
        self.state = self.ACCEPTED
2133
        self.save()
2134

    
2135
    def can_cancel_leave(self):
2136
        return self.state == self.LEAVE_REQUESTED
2137

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

    
2144
        self.leave_request_date = None
2145
        self.state = self.ACCEPTED
2146
        self.save()
2147

    
2148
    def can_remove(self):
2149
        return self.state in self.ACCEPTED_STATES
2150

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

    
2156
        self._set_history_item(reason='REMOVE')
2157
        self.delete()
2158

    
2159
    def can_reject(self):
2160
        return self.state == self.REQUESTED
2161

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

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

    
2172
    def can_cancel(self):
2173
        return self.state == self.REQUESTED
2174

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

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

    
2185

    
2186
class Serial(models.Model):
2187
    serial  =   models.AutoField(primary_key=True)
2188

    
2189

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

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

    
2200
### SIGNALS ###
2201
################
2202

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

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

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

    
2227
def astakosuser_post_save(sender, instance, created, **kwargs):
2228
    pass
2229

    
2230
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2231

    
2232
def resource_post_save(sender, instance, created, **kwargs):
2233
    pass
2234

    
2235
post_save.connect(resource_post_save, sender=Resource)
2236

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