Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 673f8f42

History | View | Annotate | Download (74.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, MODERATION_ENABLED,
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
from synnefo.lib.ordereddict import OrderedDict
79

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

    
85
logger = logging.getLogger(__name__)
86

    
87
DEFAULT_CONTENT_TYPE = None
88
_content_type = None
89

    
90

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

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

    
104
inf = float('inf')
105

    
106

    
107
def dict_merge(a, b):
108
    """
109
    http://www.xormedia.com/recursively-merge-dictionaries-in-python/
110
    """
111
    if not isinstance(b, dict):
112
        return b
113
    result = copy.deepcopy(a)
114
    for k, v in b.iteritems():
115
        if k in result and isinstance(result[k], dict):
116
                result[k] = dict_merge(result[k], v)
117
        else:
118
            result[k] = copy.deepcopy(v)
119
    return result
120

    
121

    
122
class Service(models.Model):
123
    name = models.CharField(_('Name'), max_length=255, unique=True,
124
                            db_index=True)
125
    api_url = models.CharField(_('Service API url'), max_length=255, null=True)
126
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
127
                                  null=True, blank=True)
128
    auth_token_created = models.DateTimeField(_('Token creation date'),
129
                                              null=True)
130
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
131
                                              null=True)
132

    
133
    def renew_token(self, expiration_date=None):
134
        md5 = hashlib.md5()
135
        md5.update(self.name.encode('ascii', 'ignore'))
136
        md5.update(self.api_url.encode('ascii', 'ignore'))
137
        md5.update(asctime())
138

    
139
        self.auth_token = b64encode(md5.digest())
140
        self.auth_token_created = datetime.now()
141
        if expiration_date:
142
            self.auth_token_expires = expiration_date
143
        else:
144
            self.auth_token_expires = None
145

    
146
    def __str__(self):
147
        return self.name
148

    
149
    @classmethod
150
    def catalog(cls, orderfor=None):
151
        catalog = {}
152
        services = list(cls.objects.all())
153
        metadata = presentation.SERVICES
154
        metadata = dict_merge(presentation.SERVICES,
155
                              astakos_settings.SERVICES_META)
156
        for service in services:
157
            if service.name in metadata:
158
                d = {'api_url': service.api_url, 'name': service.name}
159
                metadata[service.name].update(d)
160

    
161
        def service_by_order(s):
162
            return s[1].get('order')
163

    
164
        def service_by_dashbaord_order(s):
165
            return s[1].get('dashboard').get('order')
166

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

    
175
        order_key = service_by_order
176
        if orderfor == 'dashboard':
177
            order_key = service_by_dashbaord_order
178

    
179
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
180
                                             key=order_key))
181
        return ordered_catalog
182

    
183

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

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

    
202
    objects = ForUpdateManager()
203

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

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

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

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

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

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

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

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

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

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

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

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

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

    
267

    
268
class AstakosUserManager(UserManager):
269

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

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

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

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

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

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

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

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

    
322

    
323

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

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

    
340

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

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

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

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

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

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

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

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

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

    
384
    objects = AstakosUserManager()
385

    
386
    forupdate = ForUpdateManager()
387

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

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

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

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

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

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

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

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

    
441
    @property
442
    def policies(self):
443
        return self.astakosuserquota_set.select_related().all()
444

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

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

    
470
    def get_resource_policy(self, resource):
471
        resource = Resource.objects.get(name=resource)
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(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(name=resource)
1494
        q.create(resource=resource, member_capacity=uplimit)
1495

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

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

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

    
1508
    @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

    
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 expired_projects(self):
1762
        q = (~Q(state=Project.TERMINATED) &
1763
              Q(application__end_date__lt=datetime.now()))
1764
        return self.filter(q)
1765

    
1766
    def search_by_name(self, *search_strings):
1767
        q = Q()
1768
        for s in search_strings:
1769
            q = q | Q(name__icontains=s)
1770
        return self.filter(q)
1771

    
1772

    
1773
class Project(models.Model):
1774

    
1775
    id                          =   models.OneToOneField(Chain,
1776
                                                      related_name='chained_project',
1777
                                                      db_column='id',
1778
                                                      primary_key=True)
1779

    
1780
    application                 =   models.OneToOneField(
1781
                                            ProjectApplication,
1782
                                            related_name='project')
1783
    last_approval_date          =   models.DateTimeField(null=True)
1784

    
1785
    members                     =   models.ManyToManyField(
1786
                                            AstakosUser,
1787
                                            through='ProjectMembership')
1788

    
1789
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1790
    deactivation_date           =   models.DateTimeField(null=True)
1791

    
1792
    creation_date               =   models.DateTimeField(auto_now_add=True)
1793
    name                        =   models.CharField(
1794
                                            max_length=80,
1795
                                            null=True,
1796
                                            db_index=True,
1797
                                            unique=True)
1798

    
1799
    APPROVED    = 1
1800
    SUSPENDED   = 10
1801
    TERMINATED  = 100
1802

    
1803
    state                       =   models.IntegerField(default=APPROVED,
1804
                                                        db_index=True)
1805

    
1806
    objects     =   ProjectManager()
1807

    
1808
    # Compiled queries
1809
    Q_TERMINATED  = Q(state=TERMINATED)
1810
    Q_SUSPENDED   = Q(state=SUSPENDED)
1811
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1812

    
1813
    def __str__(self):
1814
        return uenc(_("<project %s '%s'>") %
1815
                    (self.id, udec(self.application.name)))
1816

    
1817
    __repr__ = __str__
1818

    
1819
    def __unicode__(self):
1820
        return _("<project %s '%s'>") % (self.id, self.application.name)
1821

    
1822
    STATE_DISPLAY = {
1823
        APPROVED   : 'Active',
1824
        SUSPENDED  : 'Suspended',
1825
        TERMINATED : 'Terminated'
1826
        }
1827

    
1828
    def state_display(self):
1829
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1830

    
1831
    def expiration_info(self):
1832
        return (str(self.id), self.name, self.state_display(),
1833
                str(self.application.end_date))
1834

    
1835
    def is_deactivated(self, reason=None):
1836
        if reason is not None:
1837
            return self.state == reason
1838

    
1839
        return self.state != self.APPROVED
1840

    
1841
    ### Deactivation calls
1842

    
1843
    def terminate(self):
1844
        self.deactivation_reason = 'TERMINATED'
1845
        self.deactivation_date = datetime.now()
1846
        self.state = self.TERMINATED
1847
        self.name = None
1848
        self.save()
1849

    
1850
    def suspend(self):
1851
        self.deactivation_reason = 'SUSPENDED'
1852
        self.deactivation_date = datetime.now()
1853
        self.state = self.SUSPENDED
1854
        self.save()
1855

    
1856
    def resume(self):
1857
        self.deactivation_reason = None
1858
        self.deactivation_date = None
1859
        self.state = self.APPROVED
1860
        self.save()
1861

    
1862
    ### Logical checks
1863

    
1864
    def is_inconsistent(self):
1865
        now = datetime.now()
1866
        dates = [self.creation_date,
1867
                 self.last_approval_date,
1868
                 self.deactivation_date]
1869
        return any([date > now for date in dates])
1870

    
1871
    def is_approved(self):
1872
        return self.state == self.APPROVED
1873

    
1874
    @property
1875
    def is_alive(self):
1876
        return not self.is_terminated
1877

    
1878
    @property
1879
    def is_terminated(self):
1880
        return self.is_deactivated(self.TERMINATED)
1881

    
1882
    @property
1883
    def is_suspended(self):
1884
        return self.is_deactivated(self.SUSPENDED)
1885

    
1886
    def violates_resource_grants(self):
1887
        return False
1888

    
1889
    def violates_members_limit(self, adding=0):
1890
        application = self.application
1891
        limit = application.limit_on_members_number
1892
        if limit is None:
1893
            return False
1894
        return (len(self.approved_members) + adding > limit)
1895

    
1896

    
1897
    ### Other
1898

    
1899
    def count_pending_memberships(self):
1900
        memb_set = self.projectmembership_set
1901
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1902
        return memb_count
1903

    
1904
    def members_count(self):
1905
        return self.approved_memberships.count()
1906

    
1907
    @property
1908
    def approved_memberships(self):
1909
        query = ProjectMembership.Q_ACCEPTED_STATES
1910
        return self.projectmembership_set.filter(query)
1911

    
1912
    @property
1913
    def approved_members(self):
1914
        return [m.person for m in self.approved_memberships]
1915

    
1916
    def add_member(self, user):
1917
        """
1918
        Raises:
1919
            django.exceptions.PermissionDenied
1920
            astakos.im.models.AstakosUser.DoesNotExist
1921
        """
1922
        if isinstance(user, (int, long)):
1923
            user = AstakosUser.objects.get(user=user)
1924

    
1925
        m, created = ProjectMembership.objects.get_or_create(
1926
            person=user, project=self
1927
        )
1928
        m.accept()
1929

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

    
1940
        m = ProjectMembership.objects.get(person=user, project=self)
1941
        m.remove()
1942

    
1943

    
1944
CHAIN_STATE = {
1945
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1946
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1947
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1948
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1949
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1950

    
1951
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1952
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1953
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1954
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1955
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1956

    
1957
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1958
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1959
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1960
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1961
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1962

    
1963
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1964
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1965
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1966
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1967
    }
1968

    
1969

    
1970
class ProjectMembershipManager(ForUpdateManager):
1971

    
1972
    def any_accepted(self):
1973
        q = self.model.Q_ACTUALLY_ACCEPTED
1974
        return self.filter(q)
1975

    
1976
    def actually_accepted(self):
1977
        q = self.model.Q_ACTUALLY_ACCEPTED
1978
        return self.filter(q)
1979

    
1980
    def requested(self):
1981
        return self.filter(state=ProjectMembership.REQUESTED)
1982

    
1983
    def suspended(self):
1984
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1985

    
1986
class ProjectMembership(models.Model):
1987

    
1988
    person              =   models.ForeignKey(AstakosUser)
1989
    request_date        =   models.DateField(auto_now_add=True)
1990
    project             =   models.ForeignKey(Project)
1991

    
1992
    REQUESTED           =   0
1993
    ACCEPTED            =   1
1994
    LEAVE_REQUESTED     =   5
1995
    # User deactivation
1996
    USER_SUSPENDED      =   10
1997

    
1998
    REMOVED             =   200
1999

    
2000
    ASSOCIATED_STATES   =   set([REQUESTED,
2001
                                 ACCEPTED,
2002
                                 LEAVE_REQUESTED,
2003
                                 USER_SUSPENDED,
2004
                                 ])
2005

    
2006
    ACCEPTED_STATES     =   set([ACCEPTED,
2007
                                 LEAVE_REQUESTED,
2008
                                 USER_SUSPENDED,
2009
                                 ])
2010

    
2011
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2012

    
2013
    state               =   models.IntegerField(default=REQUESTED,
2014
                                                db_index=True)
2015
    acceptance_date     =   models.DateField(null=True, db_index=True)
2016
    leave_request_date  =   models.DateField(null=True)
2017

    
2018
    objects     =   ProjectMembershipManager()
2019

    
2020
    # Compiled queries
2021
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2022
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2023

    
2024
    MEMBERSHIP_STATE_DISPLAY = {
2025
        REQUESTED           : _('Requested'),
2026
        ACCEPTED            : _('Accepted'),
2027
        LEAVE_REQUESTED     : _('Leave Requested'),
2028
        USER_SUSPENDED      : _('Suspended'),
2029
        REMOVED             : _('Pending removal'),
2030
        }
2031

    
2032
    USER_FRIENDLY_STATE_DISPLAY = {
2033
        REQUESTED           : _('Join requested'),
2034
        ACCEPTED            : _('Accepted member'),
2035
        LEAVE_REQUESTED     : _('Requested to leave'),
2036
        USER_SUSPENDED      : _('Suspended member'),
2037
        REMOVED             : _('Pending removal'),
2038
        }
2039

    
2040
    def state_display(self):
2041
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2042

    
2043
    def user_friendly_state_display(self):
2044
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2045

    
2046
    class Meta:
2047
        unique_together = ("person", "project")
2048
        #index_together = [["project", "state"]]
2049

    
2050
    def __str__(self):
2051
        return uenc(_("<'%s' membership in '%s'>") % (
2052
                self.person.username, self.project))
2053

    
2054
    __repr__ = __str__
2055

    
2056
    def __init__(self, *args, **kwargs):
2057
        self.state = self.REQUESTED
2058
        super(ProjectMembership, self).__init__(*args, **kwargs)
2059

    
2060
    def _set_history_item(self, reason, date=None):
2061
        if isinstance(reason, basestring):
2062
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2063

    
2064
        history_item = ProjectMembershipHistory(
2065
                            serial=self.id,
2066
                            person=self.person_id,
2067
                            project=self.project_id,
2068
                            date=date or datetime.now(),
2069
                            reason=reason)
2070
        history_item.save()
2071
        serial = history_item.id
2072

    
2073
    def can_accept(self):
2074
        return self.state == self.REQUESTED
2075

    
2076
    def accept(self):
2077
        if not self.can_accept():
2078
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2079
            raise AssertionError(m)
2080

    
2081
        now = datetime.now()
2082
        self.acceptance_date = now
2083
        self._set_history_item(reason='ACCEPT', date=now)
2084
        self.state = self.ACCEPTED
2085
        self.save()
2086

    
2087
    def can_leave(self):
2088
        return self.state in self.ACCEPTED_STATES
2089

    
2090
    def leave_request(self):
2091
        if not self.can_leave():
2092
            m = _("%s: attempt to request to leave in state '%s'") % (
2093
                self, self.state)
2094
            raise AssertionError(m)
2095

    
2096
        self.leave_request_date = datetime.now()
2097
        self.state = self.LEAVE_REQUESTED
2098
        self.save()
2099

    
2100
    def can_deny_leave(self):
2101
        return self.state == self.LEAVE_REQUESTED
2102

    
2103
    def leave_request_deny(self):
2104
        if not self.can_deny_leave():
2105
            m = _("%s: attempt to deny leave request in state '%s'") % (
2106
                self, self.state)
2107
            raise AssertionError(m)
2108

    
2109
        self.leave_request_date = None
2110
        self.state = self.ACCEPTED
2111
        self.save()
2112

    
2113
    def can_cancel_leave(self):
2114
        return self.state == self.LEAVE_REQUESTED
2115

    
2116
    def leave_request_cancel(self):
2117
        if not self.can_cancel_leave():
2118
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2119
                self, self.state)
2120
            raise AssertionError(m)
2121

    
2122
        self.leave_request_date = None
2123
        self.state = self.ACCEPTED
2124
        self.save()
2125

    
2126
    def can_remove(self):
2127
        return self.state in self.ACCEPTED_STATES
2128

    
2129
    def remove(self):
2130
        if not self.can_remove():
2131
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2132
            raise AssertionError(m)
2133

    
2134
        self._set_history_item(reason='REMOVE')
2135
        self.delete()
2136

    
2137
    def can_reject(self):
2138
        return self.state == self.REQUESTED
2139

    
2140
    def reject(self):
2141
        if not self.can_reject():
2142
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2143
            raise AssertionError(m)
2144

    
2145
        # rejected requests don't need sync,
2146
        # because they were never effected
2147
        self._set_history_item(reason='REJECT')
2148
        self.delete()
2149

    
2150
    def can_cancel(self):
2151
        return self.state == self.REQUESTED
2152

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

    
2158
        # rejected requests don't need sync,
2159
        # because they were never effected
2160
        self._set_history_item(reason='CANCEL')
2161
        self.delete()
2162

    
2163

    
2164
class Serial(models.Model):
2165
    serial  =   models.AutoField(primary_key=True)
2166

    
2167

    
2168
class ProjectMembershipHistory(models.Model):
2169
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2170
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2171

    
2172
    person  =   models.BigIntegerField()
2173
    project =   models.BigIntegerField()
2174
    date    =   models.DateField(auto_now_add=True)
2175
    reason  =   models.IntegerField()
2176
    serial  =   models.BigIntegerField()
2177

    
2178
### SIGNALS ###
2179
################
2180

    
2181
def create_astakos_user(u):
2182
    try:
2183
        AstakosUser.objects.get(user_ptr=u.pk)
2184
    except AstakosUser.DoesNotExist:
2185
        extended_user = AstakosUser(user_ptr_id=u.pk)
2186
        extended_user.__dict__.update(u.__dict__)
2187
        extended_user.save()
2188
        if not extended_user.has_auth_provider('local'):
2189
            extended_user.add_auth_provider('local')
2190
    except BaseException, e:
2191
        logger.exception(e)
2192

    
2193
def fix_superusers():
2194
    # Associate superusers with AstakosUser
2195
    admins = User.objects.filter(is_superuser=True)
2196
    for u in admins:
2197
        create_astakos_user(u)
2198

    
2199
def user_post_save(sender, instance, created, **kwargs):
2200
    if not created:
2201
        return
2202
    create_astakos_user(instance)
2203
post_save.connect(user_post_save, sender=User)
2204

    
2205
def astakosuser_post_save(sender, instance, created, **kwargs):
2206
    pass
2207

    
2208
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2209

    
2210
def resource_post_save(sender, instance, created, **kwargs):
2211
    pass
2212

    
2213
post_save.connect(resource_post_save, sender=Resource)
2214

    
2215
def renew_token(sender, instance, **kwargs):
2216
    if not instance.auth_token:
2217
        instance.renew_token()
2218
pre_save.connect(renew_token, sender=AstakosUser)
2219
pre_save.connect(renew_token, sender=Service)