Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 4161cb70

History | View | Annotate | Download (74.3 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

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

    
84
logger = logging.getLogger(__name__)
85

    
86
DEFAULT_CONTENT_TYPE = None
87
_content_type = None
88

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

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

    
101
RESOURCE_SEPARATOR = '.'
102

    
103
inf = float('inf')
104

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

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

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

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

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

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

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

    
144

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
220
def get_resource_names():
221
    _RESOURCE_NAMES = []
222
    resources = Resource.objects.select_related('service').all()
223
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
224
    return _RESOURCE_NAMES
225

    
226

    
227
class AstakosUserManager(UserManager):
228

    
229
    def get_auth_provider_user(self, provider, **kwargs):
230
        """
231
        Retrieve AstakosUser instance associated with the specified third party
232
        id.
233
        """
234
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
235
                          kwargs.iteritems()))
236
        return self.get(auth_providers__module=provider, **kwargs)
237

    
238
    def get_by_email(self, email):
239
        return self.get(email=email)
240

    
241
    def get_by_identifier(self, email_or_username, **kwargs):
242
        try:
243
            return self.get(email__iexact=email_or_username, **kwargs)
244
        except AstakosUser.DoesNotExist:
245
            return self.get(username__iexact=email_or_username, **kwargs)
246

    
247
    def user_exists(self, email_or_username, **kwargs):
248
        qemail = Q(email__iexact=email_or_username)
249
        qusername = Q(username__iexact=email_or_username)
250
        qextra = Q(**kwargs)
251
        return self.filter((qemail | qusername) & qextra).exists()
252

    
253
    def verified_user_exists(self, email_or_username):
254
        return self.user_exists(email_or_username, email_verified=True)
255

    
256
    def verified(self):
257
        return self.filter(email_verified=True)
258

    
259
    def uuid_catalog(self, l=None):
260
        """
261
        Returns a uuid to username mapping for the uuids appearing in l.
262
        If l is None returns the mapping for all existing users.
263
        """
264
        q = self.filter(uuid__in=l) if l != None else self
265
        return dict(q.values_list('uuid', 'username'))
266

    
267
    def displayname_catalog(self, l=None):
268
        """
269
        Returns a username to uuid mapping for the usernames appearing in l.
270
        If l is None returns the mapping for all existing users.
271
        """
272
        if l is not None:
273
            lmap = dict((x.lower(), x) for x in l)
274
            q = self.filter(username__in=lmap.keys())
275
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
276
        else:
277
            q = self
278
            values = self.values_list('username', 'uuid')
279
        return dict(values)
280

    
281

    
282

    
283
class AstakosUser(User):
284
    """
285
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
286
    """
287
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
288
                                   null=True)
289

    
290
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
291
    #                    AstakosUserProvider model.
292
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
293
                                null=True)
294
    # ex. screen_name for twitter, eppn for shibboleth
295
    third_party_identifier = models.CharField(_('Third-party identifier'),
296
                                              max_length=255, null=True,
297
                                              blank=True)
298

    
299

    
300
    #for invitations
301
    user_level = DEFAULT_USER_LEVEL
302
    level = models.IntegerField(_('Inviter level'), default=user_level)
303
    invitations = models.IntegerField(
304
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
305

    
306
    auth_token = models.CharField(_('Authentication Token'),
307
                                  max_length=32,
308
                                  null=True,
309
                                  blank=True,
310
                                  help_text = _('Renew your authentication '
311
                                                'token. Make sure to set the new '
312
                                                'token in any client you may be '
313
                                                'using, to preserve its '
314
                                                'functionality.'))
315
    auth_token_created = models.DateTimeField(_('Token creation date'),
316
                                              null=True)
317
    auth_token_expires = models.DateTimeField(
318
        _('Token expiration date'), null=True)
319

    
320
    updated = models.DateTimeField(_('Update date'))
321
    is_verified = models.BooleanField(_('Is verified?'), default=False)
322

    
323
    email_verified = models.BooleanField(_('Email verified?'), default=False)
324

    
325
    has_credits = models.BooleanField(_('Has credits?'), default=False)
326
    has_signed_terms = models.BooleanField(
327
        _('I agree with the terms'), default=False)
328
    date_signed_terms = models.DateTimeField(
329
        _('Signed terms date'), null=True, blank=True)
330

    
331
    activation_sent = models.DateTimeField(
332
        _('Activation sent data'), null=True, blank=True)
333

    
334
    policy = models.ManyToManyField(
335
        Resource, null=True, through='AstakosUserQuota')
336

    
337
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
338

    
339
    __has_signed_terms = False
340
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
341
                                           default=False, db_index=True)
342

    
343
    objects = AstakosUserManager()
344

    
345
    forupdate = ForUpdateManager()
346

    
347
    def __init__(self, *args, **kwargs):
348
        super(AstakosUser, self).__init__(*args, **kwargs)
349
        self.__has_signed_terms = self.has_signed_terms
350
        if not self.id:
351
            self.is_active = False
352

    
353
    @property
354
    def realname(self):
355
        return '%s %s' % (self.first_name, self.last_name)
356

    
357
    @property
358
    def log_display(self):
359
        """
360
        Should be used in all logger.* calls that refer to a user so that
361
        user display is consistent across log entries.
362
        """
363
        return '%s::%s' % (self.uuid, self.email)
364

    
365
    @realname.setter
366
    def realname(self, value):
367
        parts = value.split(' ')
368
        if len(parts) == 2:
369
            self.first_name = parts[0]
370
            self.last_name = parts[1]
371
        else:
372
            self.last_name = parts[0]
373

    
374
    def add_permission(self, pname):
375
        if self.has_perm(pname):
376
            return
377
        p, created = Permission.objects.get_or_create(
378
                                    codename=pname,
379
                                    name=pname.capitalize(),
380
                                    content_type=get_content_type())
381
        self.user_permissions.add(p)
382

    
383
    def remove_permission(self, pname):
384
        if self.has_perm(pname):
385
            return
386
        p = Permission.objects.get(codename=pname,
387
                                   content_type=get_content_type())
388
        self.user_permissions.remove(p)
389

    
390
    def is_project_admin(self, application_id=None):
391
        return self.uuid in PROJECT_ADMINS
392

    
393
    @property
394
    def invitation(self):
395
        try:
396
            return Invitation.objects.get(username=self.email)
397
        except Invitation.DoesNotExist:
398
            return None
399

    
400
    @property
401
    def policies(self):
402
        return self.astakosuserquota_set.select_related().all()
403

    
404
    @policies.setter
405
    def policies(self, policies):
406
        for p in policies:
407
            p.setdefault('resource', '')
408
            p.setdefault('capacity', 0)
409
            p.setdefault('quantity', 0)
410
            p.setdefault('update', True)
411
            self.add_resource_policy(**p)
412

    
413
    def add_resource_policy(
414
            self, resource, capacity,
415
            update=True):
416
        """Raises ObjectDoesNotExist, IntegrityError"""
417
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
418
        resource = Resource.objects.get(service__name=s, name=r)
419
        if update:
420
            AstakosUserQuota.objects.update_or_create(
421
                user=self, resource=resource, defaults={
422
                    'capacity':capacity,
423
                    })
424
        else:
425
            q = self.astakosuserquota_set
426
            q.create(
427
                resource=resource, capacity=capacity, quanity=quantity,
428
                )
429

    
430
    def get_resource_policy(self, resource):
431
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
432
        resource = Resource.objects.get(service__name=s, name=r)
433
        default_capacity = resource.uplimit
434
        try:
435
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
436
            return policy, default_capacity
437
        except AstakosUserQuota.DoesNotExist:
438
            return None, default_capacity
439

    
440
    def remove_resource_policy(self, service, resource):
441
        """Raises ObjectDoesNotExist, IntegrityError"""
442
        resource = Resource.objects.get(service__name=service, name=resource)
443
        q = self.policies.get(resource=resource).delete()
444

    
445
    def update_uuid(self):
446
        while not self.uuid:
447
            uuid_val =  str(uuid.uuid4())
448
            try:
449
                AstakosUser.objects.get(uuid=uuid_val)
450
            except AstakosUser.DoesNotExist, e:
451
                self.uuid = uuid_val
452
        return self.uuid
453

    
454
    def save(self, update_timestamps=True, **kwargs):
455
        if update_timestamps:
456
            if not self.id:
457
                self.date_joined = datetime.now()
458
            self.updated = datetime.now()
459

    
460
        # update date_signed_terms if necessary
461
        if self.__has_signed_terms != self.has_signed_terms:
462
            self.date_signed_terms = datetime.now()
463

    
464
        self.update_uuid()
465

    
466
        if self.username != self.email.lower():
467
            # set username
468
            self.username = self.email.lower()
469

    
470
        super(AstakosUser, self).save(**kwargs)
471

    
472
    def renew_token(self, flush_sessions=False, current_key=None):
473
        md5 = hashlib.md5()
474
        md5.update(settings.SECRET_KEY)
475
        md5.update(self.username)
476
        md5.update(self.realname.encode('ascii', 'ignore'))
477
        md5.update(asctime())
478

    
479
        self.auth_token = b64encode(md5.digest())
480
        self.auth_token_created = datetime.now()
481
        self.auth_token_expires = self.auth_token_created + \
482
                                  timedelta(hours=AUTH_TOKEN_DURATION)
483
        if flush_sessions:
484
            self.flush_sessions(current_key)
485
        msg = 'Token renewed for %s' % self.email
486
        logger.log(LOGGING_LEVEL, msg)
487

    
488
    def flush_sessions(self, current_key=None):
489
        q = self.sessions
490
        if current_key:
491
            q = q.exclude(session_key=current_key)
492

    
493
        keys = q.values_list('session_key', flat=True)
494
        if keys:
495
            msg = 'Flushing sessions: %s' % ','.join(keys)
496
            logger.log(LOGGING_LEVEL, msg, [])
497
        engine = import_module(settings.SESSION_ENGINE)
498
        for k in keys:
499
            s = engine.SessionStore(k)
500
            s.flush()
501

    
502
    def __unicode__(self):
503
        return '%s (%s)' % (self.realname, self.email)
504

    
505
    def conflicting_email(self):
506
        q = AstakosUser.objects.exclude(username=self.username)
507
        q = q.filter(email__iexact=self.email)
508
        if q.count() != 0:
509
            return True
510
        return False
511

    
512
    def email_change_is_pending(self):
513
        return self.emailchanges.count() > 0
514

    
515
    @property
516
    def signed_terms(self):
517
        term = get_latest_terms()
518
        if not term:
519
            return True
520
        if not self.has_signed_terms:
521
            return False
522
        if not self.date_signed_terms:
523
            return False
524
        if self.date_signed_terms < term.date:
525
            self.has_signed_terms = False
526
            self.date_signed_terms = None
527
            self.save()
528
            return False
529
        return True
530

    
531
    def set_invitations_level(self):
532
        """
533
        Update user invitation level
534
        """
535
        level = self.invitation.inviter.level + 1
536
        self.level = level
537
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
538

    
539
    def can_change_password(self):
540
        return self.has_auth_provider('local', auth_backend='astakos')
541

    
542
    def can_change_email(self):
543
        if not self.has_auth_provider('local'):
544
            return True
545

    
546
        local = self.get_auth_provider('local')._instance
547
        return local.auth_backend == 'astakos'
548

    
549
    # Auth providers related methods
550
    def get_auth_provider(self, module=None, identifier=None, **filters):
551
        if not module:
552
            return self.auth_providers.active()[0].settings
553

    
554
        params = {'module': module}
555
        if identifier:
556
            params['identifier'] = identifier
557
        params.update(filters)
558
        return self.auth_providers.active().get(**params).settings
559

    
560
    def has_auth_provider(self, provider, **kwargs):
561
        return bool(self.auth_providers.active().filter(module=provider,
562
                                                        **kwargs).count())
563

    
564
    def get_required_providers(self, **kwargs):
565
        return auth.REQUIRED_PROVIDERS.keys()
566

    
567
    def missing_required_providers(self):
568
        required = self.get_required_providers()
569
        missing = []
570
        for provider in required:
571
            if not self.has_auth_provider(provider):
572
                missing.append(auth.get_provider(provider, self))
573
        return missing
574

    
575
    def get_available_auth_providers(self, **filters):
576
        """
577
        Returns a list of providers available for add by the user.
578
        """
579
        modules = astakos_settings.IM_MODULES
580
        providers = []
581
        for p in modules:
582
            providers.append(auth.get_provider(p, self))
583
        available = []
584

    
585
        for p in providers:
586
            if p.get_add_policy:
587
                available.append(p)
588
        return available
589

    
590
    def get_disabled_auth_providers(self, **filters):
591
        providers = self.get_auth_providers(**filters)
592
        disabled = []
593
        for p in providers:
594
            if not p.get_login_policy:
595
                disabled.append(p)
596
        return disabled
597

    
598
    def get_enabled_auth_providers(self, **filters):
599
        providers = self.get_auth_providers(**filters)
600
        enabled = []
601
        for p in providers:
602
            if p.get_login_policy:
603
                enabled.append(p)
604
        return enabled
605

    
606
    def get_auth_providers(self, **filters):
607
        providers = []
608
        for provider in self.auth_providers.active(**filters):
609
            if provider.settings.module_enabled:
610
                providers.append(provider.settings)
611

    
612
        modules = astakos_settings.IM_MODULES
613

    
614
        def key(p):
615
            if not p.module in modules:
616
                return 100
617
            return modules.index(p.module)
618

    
619
        providers = sorted(providers, key=key)
620
        return providers
621

    
622
    # URL methods
623
    @property
624
    def auth_providers_display(self):
625
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
626
                         self.get_enabled_auth_providers()])
627

    
628
    def add_auth_provider(self, module='local', identifier=None, **params):
629
        provider = auth.get_provider(module, self, identifier, **params)
630
        provider.add_to_user()
631

    
632
    def get_resend_activation_url(self):
633
        return reverse('send_activation', kwargs={'user_id': self.pk})
634

    
635
    def get_activation_url(self, nxt=False):
636
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
637
                                 quote(self.auth_token))
638
        if nxt:
639
            url += "&next=%s" % quote(nxt)
640
        return url
641

    
642
    def get_password_reset_url(self, token_generator=default_token_generator):
643
        return reverse('django.contrib.auth.views.password_reset_confirm',
644
                          kwargs={'uidb36':int_to_base36(self.id),
645
                                  'token':token_generator.make_token(self)})
646

    
647
    def get_inactive_message(self, provider_module, identifier=None):
648
        provider = self.get_auth_provider(provider_module, identifier)
649

    
650
        msg_extra = ''
651
        message = ''
652

    
653
        msg_inactive = provider.get_account_inactive_msg
654
        msg_pending = provider.get_pending_activation_msg
655
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
656
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
657
        msg_pending_mod = provider.get_pending_moderation_msg
658
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
659

    
660
        if self.activation_sent:
661
            if self.email_verified:
662
                message = msg_inactive
663
            else:
664
                message = msg_pending
665
                url = self.get_resend_activation_url()
666
                msg_extra = msg_pending_help + \
667
                            u' ' + \
668
                            '<a href="%s">%s?</a>' % (url, msg_resend)
669
        else:
670
            if astakos_settings.MODERATION_ENABLED:
671
                message = msg_pending_mod
672
            else:
673
                message = msg_pending
674
                url = self.get_resend_activation_url()
675
                msg_extra = '<a href="%s">%s?</a>' % (url, \
676
                                msg_resend)
677

    
678
        return mark_safe(message + u' '+ msg_extra)
679

    
680
    def owns_application(self, application):
681
        return application.owner == self
682

    
683
    def owns_project(self, project):
684
        return project.application.owner == self
685

    
686
    def is_associated(self, project):
687
        try:
688
            m = ProjectMembership.objects.get(person=self, project=project)
689
            return m.state in ProjectMembership.ASSOCIATED_STATES
690
        except ProjectMembership.DoesNotExist:
691
            return False
692

    
693
    def get_membership(self, project):
694
        try:
695
            return ProjectMembership.objects.get(
696
                project=project,
697
                person=self)
698
        except ProjectMembership.DoesNotExist:
699
            return None
700

    
701
    def membership_display(self, project):
702
        m = self.get_membership(project)
703
        if m is None:
704
            return _('Not a member')
705
        else:
706
            return m.user_friendly_state_display()
707

    
708
    def non_owner_can_view(self, maybe_project):
709
        if self.is_project_admin():
710
            return True
711
        if maybe_project is None:
712
            return False
713
        project = maybe_project
714
        if self.is_associated(project):
715
            return True
716
        if project.is_deactivated():
717
            return False
718
        return True
719

    
720
    def settings(self):
721
        return UserSetting.objects.filter(user=self)
722

    
723

    
724
class AstakosUserAuthProviderManager(models.Manager):
725

    
726
    def active(self, **filters):
727
        return self.filter(active=True, **filters)
728

    
729
    def remove_unverified_providers(self, provider, **filters):
730
        try:
731
            existing = self.filter(module=provider, user__email_verified=False,
732
                                   **filters)
733
            for p in existing:
734
                p.user.delete()
735
        except:
736
            pass
737

    
738
    def unverified(self, provider, **filters):
739
        try:
740
            return self.get(module=provider, user__email_verified=False,
741
                            **filters).settings
742
        except AstakosUserAuthProvider.DoesNotExist:
743
            return None
744

    
745
    def verified(self, provider, **filters):
746
        try:
747
            return self.get(module=provider, user__email_verified=True,
748
                            **filters).settings
749
        except AstakosUserAuthProvider.DoesNotExist:
750
            return None
751

    
752

    
753
class AuthProviderPolicyProfileManager(models.Manager):
754

    
755
    def active(self):
756
        return self.filter(active=True)
757

    
758
    def for_user(self, user, provider):
759
        policies = {}
760
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
761
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
762
        exclusive_q = exclusive_q1 | exclusive_q2
763

    
764
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
765
            policies.update(profile.policies)
766

    
767
        user_groups = user.groups.all().values('pk')
768
        for profile in self.active().filter(groups__in=user_groups).filter(
769
                exclusive_q):
770
            policies.update(profile.policies)
771
        return policies
772

    
773
    def add_policy(self, name, provider, group_or_user, exclusive=False,
774
                   **policies):
775
        is_group = isinstance(group_or_user, Group)
776
        profile, created = self.get_or_create(name=name, provider=provider,
777
                                              is_exclusive=exclusive)
778
        profile.is_exclusive = exclusive
779
        profile.save()
780
        if is_group:
781
            profile.groups.add(group_or_user)
782
        else:
783
            profile.users.add(group_or_user)
784
        profile.set_policies(policies)
785
        profile.save()
786
        return profile
787

    
788

    
789
class AuthProviderPolicyProfile(models.Model):
790
    name = models.CharField(_('Name'), max_length=255, blank=False,
791
                            null=False, db_index=True)
792
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
793
                                null=False)
794

    
795
    # apply policies to all providers excluding the one set in provider field
796
    is_exclusive = models.BooleanField(default=False)
797

    
798
    policy_add = models.NullBooleanField(null=True, default=None)
799
    policy_remove = models.NullBooleanField(null=True, default=None)
800
    policy_create = models.NullBooleanField(null=True, default=None)
801
    policy_login = models.NullBooleanField(null=True, default=None)
802
    policy_limit = models.IntegerField(null=True, default=None)
803
    policy_required = models.NullBooleanField(null=True, default=None)
804
    policy_automoderate = models.NullBooleanField(null=True, default=None)
805
    policy_switch = models.NullBooleanField(null=True, default=None)
806

    
807
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
808
                     'automoderate')
809

    
810
    priority = models.IntegerField(null=False, default=1)
811
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
812
    users = models.ManyToManyField(AstakosUser,
813
                                   related_name='authpolicy_profiles')
814
    active = models.BooleanField(default=True)
815

    
816
    objects = AuthProviderPolicyProfileManager()
817

    
818
    class Meta:
819
        ordering = ['priority']
820

    
821
    @property
822
    def policies(self):
823
        policies = {}
824
        for pkey in self.POLICY_FIELDS:
825
            value = getattr(self, 'policy_%s' % pkey, None)
826
            if value is None:
827
                continue
828
            policies[pkey] = value
829
        return policies
830

    
831
    def set_policies(self, policies_dict):
832
        for key, value in policies_dict.iteritems():
833
            if key in self.POLICY_FIELDS:
834
                setattr(self, 'policy_%s' % key, value)
835
        return self.policies
836

    
837

    
838
class AstakosUserAuthProvider(models.Model):
839
    """
840
    Available user authentication methods.
841
    """
842
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
843
                                   null=True, default=None)
844
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
845
    module = models.CharField(_('Provider'), max_length=255, blank=False,
846
                                default='local')
847
    identifier = models.CharField(_('Third-party identifier'),
848
                                              max_length=255, null=True,
849
                                              blank=True)
850
    active = models.BooleanField(default=True)
851
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
852
                                   default='astakos')
853
    info_data = models.TextField(default="", null=True, blank=True)
854
    created = models.DateTimeField('Creation date', auto_now_add=True)
855

    
856
    objects = AstakosUserAuthProviderManager()
857

    
858
    class Meta:
859
        unique_together = (('identifier', 'module', 'user'), )
860
        ordering = ('module', 'created')
861

    
862
    def __init__(self, *args, **kwargs):
863
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
864
        try:
865
            self.info = json.loads(self.info_data)
866
            if not self.info:
867
                self.info = {}
868
        except Exception, e:
869
            self.info = {}
870

    
871
        for key,value in self.info.iteritems():
872
            setattr(self, 'info_%s' % key, value)
873

    
874
    @property
875
    def settings(self):
876
        extra_data = {}
877

    
878
        info_data = {}
879
        if self.info_data:
880
            info_data = json.loads(self.info_data)
881

    
882
        extra_data['info'] = info_data
883

    
884
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
885
            extra_data[key] = getattr(self, key)
886

    
887
        extra_data['instance'] = self
888
        return auth.get_provider(self.module, self.user,
889
                                           self.identifier, **extra_data)
890

    
891
    def __repr__(self):
892
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
893

    
894
    def __unicode__(self):
895
        if self.identifier:
896
            return "%s:%s" % (self.module, self.identifier)
897
        if self.auth_backend:
898
            return "%s:%s" % (self.module, self.auth_backend)
899
        return self.module
900

    
901
    def save(self, *args, **kwargs):
902
        self.info_data = json.dumps(self.info)
903
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
904

    
905

    
906
class ExtendedManager(models.Manager):
907
    def _update_or_create(self, **kwargs):
908
        assert kwargs, \
909
            'update_or_create() must be passed at least one keyword argument'
910
        obj, created = self.get_or_create(**kwargs)
911
        defaults = kwargs.pop('defaults', {})
912
        if created:
913
            return obj, True, False
914
        else:
915
            try:
916
                params = dict(
917
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
918
                params.update(defaults)
919
                for attr, val in params.items():
920
                    if hasattr(obj, attr):
921
                        setattr(obj, attr, val)
922
                sid = transaction.savepoint()
923
                obj.save(force_update=True)
924
                transaction.savepoint_commit(sid)
925
                return obj, False, True
926
            except IntegrityError, e:
927
                transaction.savepoint_rollback(sid)
928
                try:
929
                    return self.get(**kwargs), False, False
930
                except self.model.DoesNotExist:
931
                    raise e
932

    
933
    update_or_create = _update_or_create
934

    
935

    
936
class AstakosUserQuota(models.Model):
937
    objects = ExtendedManager()
938
    capacity = intDecimalField()
939
    quantity = intDecimalField(default=0)
940
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
941
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
942
    resource = models.ForeignKey(Resource)
943
    user = models.ForeignKey(AstakosUser)
944

    
945
    class Meta:
946
        unique_together = ("resource", "user")
947

    
948

    
949
class ApprovalTerms(models.Model):
950
    """
951
    Model for approval terms
952
    """
953

    
954
    date = models.DateTimeField(
955
        _('Issue date'), db_index=True, auto_now_add=True)
956
    location = models.CharField(_('Terms location'), max_length=255)
957

    
958

    
959
class Invitation(models.Model):
960
    """
961
    Model for registring invitations
962
    """
963
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
964
                                null=True)
965
    realname = models.CharField(_('Real name'), max_length=255)
966
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
967
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
968
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
969
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
970
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
971

    
972
    def __init__(self, *args, **kwargs):
973
        super(Invitation, self).__init__(*args, **kwargs)
974
        if not self.id:
975
            self.code = _generate_invitation_code()
976

    
977
    def consume(self):
978
        self.is_consumed = True
979
        self.consumed = datetime.now()
980
        self.save()
981

    
982
    def __unicode__(self):
983
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
984

    
985

    
986
class EmailChangeManager(models.Manager):
987

    
988
    @transaction.commit_on_success
989
    def change_email(self, activation_key):
990
        """
991
        Validate an activation key and change the corresponding
992
        ``User`` if valid.
993

994
        If the key is valid and has not expired, return the ``User``
995
        after activating.
996

997
        If the key is not valid or has expired, return ``None``.
998

999
        If the key is valid but the ``User`` is already active,
1000
        return ``None``.
1001

1002
        After successful email change the activation record is deleted.
1003

1004
        Throws ValueError if there is already
1005
        """
1006
        try:
1007
            email_change = self.model.objects.get(
1008
                activation_key=activation_key)
1009
            if email_change.activation_key_expired():
1010
                email_change.delete()
1011
                raise EmailChange.DoesNotExist
1012
            # is there an active user with this address?
1013
            try:
1014
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1015
            except AstakosUser.DoesNotExist:
1016
                pass
1017
            else:
1018
                raise ValueError(_('The new email address is reserved.'))
1019
            # update user
1020
            user = AstakosUser.objects.get(pk=email_change.user_id)
1021
            old_email = user.email
1022
            user.email = email_change.new_email_address
1023
            user.save()
1024
            email_change.delete()
1025
            msg = "User %s changed email from %s to %s" % (user.log_display,
1026
                                                           old_email,
1027
                                                           user.email)
1028
            logger.log(LOGGING_LEVEL, msg)
1029
            return user
1030
        except EmailChange.DoesNotExist:
1031
            raise ValueError(_('Invalid activation key.'))
1032

    
1033

    
1034
class EmailChange(models.Model):
1035
    new_email_address = models.EmailField(
1036
        _(u'new e-mail address'),
1037
        help_text=_('Provide a new email address. Until you verify the new '
1038
                    'address by following the activation link that will be '
1039
                    'sent to it, your old email address will remain active.'))
1040
    user = models.ForeignKey(
1041
        AstakosUser, unique=True, related_name='emailchanges')
1042
    requested_at = models.DateTimeField(auto_now_add=True)
1043
    activation_key = models.CharField(
1044
        max_length=40, unique=True, db_index=True)
1045

    
1046
    objects = EmailChangeManager()
1047

    
1048
    def get_url(self):
1049
        return reverse('email_change_confirm',
1050
                      kwargs={'activation_key': self.activation_key})
1051

    
1052
    def activation_key_expired(self):
1053
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1054
        return self.requested_at + expiration_date < datetime.now()
1055

    
1056

    
1057
class AdditionalMail(models.Model):
1058
    """
1059
    Model for registring invitations
1060
    """
1061
    owner = models.ForeignKey(AstakosUser)
1062
    email = models.EmailField()
1063

    
1064

    
1065
def _generate_invitation_code():
1066
    while True:
1067
        code = randint(1, 2L ** 63 - 1)
1068
        try:
1069
            Invitation.objects.get(code=code)
1070
            # An invitation with this code already exists, try again
1071
        except Invitation.DoesNotExist:
1072
            return code
1073

    
1074

    
1075
def get_latest_terms():
1076
    try:
1077
        term = ApprovalTerms.objects.order_by('-id')[0]
1078
        return term
1079
    except IndexError:
1080
        pass
1081
    return None
1082

    
1083

    
1084
class PendingThirdPartyUser(models.Model):
1085
    """
1086
    Model for registring successful third party user authentications
1087
    """
1088
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1089
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1090
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1091
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1092
                                  null=True)
1093
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1094
                                 null=True)
1095
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1096
                                   null=True)
1097
    username = models.CharField(_('username'), max_length=30, unique=True,
1098
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1099
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1100
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1101
    info = models.TextField(default="", null=True, blank=True)
1102

    
1103
    class Meta:
1104
        unique_together = ("provider", "third_party_identifier")
1105

    
1106
    def get_user_instance(self):
1107
        d = self.__dict__
1108
        d.pop('_state', None)
1109
        d.pop('id', None)
1110
        d.pop('token', None)
1111
        d.pop('created', None)
1112
        d.pop('info', None)
1113
        user = AstakosUser(**d)
1114

    
1115
        return user
1116

    
1117
    @property
1118
    def realname(self):
1119
        return '%s %s' %(self.first_name, self.last_name)
1120

    
1121
    @realname.setter
1122
    def realname(self, value):
1123
        parts = value.split(' ')
1124
        if len(parts) == 2:
1125
            self.first_name = parts[0]
1126
            self.last_name = parts[1]
1127
        else:
1128
            self.last_name = parts[0]
1129

    
1130
    def save(self, **kwargs):
1131
        if not self.id:
1132
            # set username
1133
            while not self.username:
1134
                username =  uuid.uuid4().hex[:30]
1135
                try:
1136
                    AstakosUser.objects.get(username = username)
1137
                except AstakosUser.DoesNotExist, e:
1138
                    self.username = username
1139
        super(PendingThirdPartyUser, self).save(**kwargs)
1140

    
1141
    def generate_token(self):
1142
        self.password = self.third_party_identifier
1143
        self.last_login = datetime.now()
1144
        self.token = default_token_generator.make_token(self)
1145

    
1146
    def existing_user(self):
1147
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1148
                                         auth_providers__identifier=self.third_party_identifier)
1149

    
1150
    def get_provider(self, user):
1151
        params = {
1152
            'info_data': self.info,
1153
            'affiliation': self.affiliation
1154
        }
1155
        return auth.get_provider(self.provider, user,
1156
                                 self.third_party_identifier, **params)
1157

    
1158
class SessionCatalog(models.Model):
1159
    session_key = models.CharField(_('session key'), max_length=40)
1160
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1161

    
1162

    
1163
class UserSetting(models.Model):
1164
    user = models.ForeignKey(AstakosUser)
1165
    setting = models.CharField(max_length=255)
1166
    value = models.IntegerField()
1167

    
1168
    objects = ForUpdateManager()
1169

    
1170
    class Meta:
1171
        unique_together = ("user", "setting")
1172

    
1173

    
1174
### PROJECTS ###
1175
################
1176

    
1177
class ChainManager(ForUpdateManager):
1178

    
1179
    def search_by_name(self, *search_strings):
1180
        projects = Project.objects.search_by_name(*search_strings)
1181
        chains = [p.id for p in projects]
1182
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1183
        apps = (app for app in apps if app.is_latest())
1184
        app_chains = [app.chain for app in apps if app.chain not in chains]
1185
        return chains + app_chains
1186

    
1187
    def all_full_state(self):
1188
        chains = self.all()
1189
        cids = [c.chain for c in chains]
1190
        projects = Project.objects.select_related('application').in_bulk(cids)
1191

    
1192
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1193
        chain_latest = dict(objs.values_list('chain', 'latest'))
1194

    
1195
        objs = ProjectApplication.objects.select_related('applicant')
1196
        apps = objs.in_bulk(chain_latest.values())
1197

    
1198
        d = {}
1199
        for chain in chains:
1200
            pk = chain.pk
1201
            project = projects.get(pk, None)
1202
            app = apps[chain_latest[pk]]
1203
            d[chain.pk] = chain.get_state(project, app)
1204

    
1205
        return d
1206

    
1207
    def of_project(self, project):
1208
        if project is None:
1209
            return None
1210
        try:
1211
            return self.get(chain=project.id)
1212
        except Chain.DoesNotExist:
1213
            raise AssertionError('project with no chain')
1214

    
1215

    
1216
class Chain(models.Model):
1217
    chain  =   models.AutoField(primary_key=True)
1218

    
1219
    def __str__(self):
1220
        return "%s" % (self.chain,)
1221

    
1222
    objects = ChainManager()
1223

    
1224
    PENDING            = 0
1225
    DENIED             = 3
1226
    DISMISSED          = 4
1227
    CANCELLED          = 5
1228

    
1229
    APPROVED           = 10
1230
    APPROVED_PENDING   = 11
1231
    SUSPENDED          = 12
1232
    SUSPENDED_PENDING  = 13
1233
    TERMINATED         = 14
1234
    TERMINATED_PENDING = 15
1235

    
1236
    PENDING_STATES = [PENDING,
1237
                      APPROVED_PENDING,
1238
                      SUSPENDED_PENDING,
1239
                      TERMINATED_PENDING,
1240
                      ]
1241

    
1242
    MODIFICATION_STATES = [APPROVED_PENDING,
1243
                           SUSPENDED_PENDING,
1244
                           TERMINATED_PENDING,
1245
                           ]
1246

    
1247
    RELEVANT_STATES = [PENDING,
1248
                       DENIED,
1249
                       APPROVED,
1250
                       APPROVED_PENDING,
1251
                       SUSPENDED,
1252
                       SUSPENDED_PENDING,
1253
                       TERMINATED_PENDING,
1254
                       ]
1255

    
1256
    SKIP_STATES = [DISMISSED,
1257
                   CANCELLED,
1258
                   TERMINATED]
1259

    
1260
    STATE_DISPLAY = {
1261
        PENDING            : _("Pending"),
1262
        DENIED             : _("Denied"),
1263
        DISMISSED          : _("Dismissed"),
1264
        CANCELLED          : _("Cancelled"),
1265
        APPROVED           : _("Active"),
1266
        APPROVED_PENDING   : _("Active - Pending"),
1267
        SUSPENDED          : _("Suspended"),
1268
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1269
        TERMINATED         : _("Terminated"),
1270
        TERMINATED_PENDING : _("Terminated - Pending"),
1271
        }
1272

    
1273

    
1274
    @classmethod
1275
    def _chain_state(cls, project_state, app_state):
1276
        s = CHAIN_STATE.get((project_state, app_state), None)
1277
        if s is None:
1278
            raise AssertionError('inconsistent chain state')
1279
        return s
1280

    
1281
    @classmethod
1282
    def chain_state(cls, project, app):
1283
        p_state = project.state if project else None
1284
        return cls._chain_state(p_state, app.state)
1285

    
1286
    @classmethod
1287
    def state_display(cls, s):
1288
        if s is None:
1289
            return _("Unknown")
1290
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1291

    
1292
    def last_application(self):
1293
        return self.chained_apps.order_by('-id')[0]
1294

    
1295
    def get_project(self):
1296
        try:
1297
            return self.chained_project
1298
        except Project.DoesNotExist:
1299
            return None
1300

    
1301
    def get_elements(self):
1302
        project = self.get_project()
1303
        app = self.last_application()
1304
        return project, app
1305

    
1306
    def get_state(self, project, app):
1307
        s = self.chain_state(project, app)
1308
        return s, project, app
1309

    
1310
    def full_state(self):
1311
        project, app = self.get_elements()
1312
        return self.get_state(project, app)
1313

    
1314

    
1315
def new_chain():
1316
    c = Chain.objects.create()
1317
    return c
1318

    
1319

    
1320
class ProjectApplicationManager(ForUpdateManager):
1321

    
1322
    def user_visible_projects(self, *filters, **kw_filters):
1323
        model = self.model
1324
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1325

    
1326
    def user_visible_by_chain(self, flt):
1327
        model = self.model
1328
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1329
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1330
        by_chain = dict(pending.annotate(models.Max('id')))
1331
        by_chain.update(approved.annotate(models.Max('id')))
1332
        return self.filter(flt, id__in=by_chain.values())
1333

    
1334
    def user_accessible_projects(self, user):
1335
        """
1336
        Return projects accessed by specified user.
1337
        """
1338
        if user.is_project_admin():
1339
            participates_filters = Q()
1340
        else:
1341
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1342
                                   Q(project__projectmembership__person=user)
1343

    
1344
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1345

    
1346
    def search_by_name(self, *search_strings):
1347
        q = Q()
1348
        for s in search_strings:
1349
            q = q | Q(name__icontains=s)
1350
        return self.filter(q)
1351

    
1352
    def latest_of_chain(self, chain_id):
1353
        try:
1354
            return self.filter(chain=chain_id).order_by('-id')[0]
1355
        except IndexError:
1356
            return None
1357

    
1358

    
1359
class ProjectApplication(models.Model):
1360
    applicant               =   models.ForeignKey(
1361
                                    AstakosUser,
1362
                                    related_name='projects_applied',
1363
                                    db_index=True)
1364

    
1365
    PENDING     =    0
1366
    APPROVED    =    1
1367
    REPLACED    =    2
1368
    DENIED      =    3
1369
    DISMISSED   =    4
1370
    CANCELLED   =    5
1371

    
1372
    state                   =   models.IntegerField(default=PENDING,
1373
                                                    db_index=True)
1374

    
1375
    owner                   =   models.ForeignKey(
1376
                                    AstakosUser,
1377
                                    related_name='projects_owned',
1378
                                    db_index=True)
1379

    
1380
    chain                   =   models.ForeignKey(Chain,
1381
                                                  related_name='chained_apps',
1382
                                                  db_column='chain')
1383
    precursor_application   =   models.ForeignKey('ProjectApplication',
1384
                                                  null=True,
1385
                                                  blank=True)
1386

    
1387
    name                    =   models.CharField(max_length=80)
1388
    homepage                =   models.URLField(max_length=255, null=True,
1389
                                                verify_exists=False)
1390
    description             =   models.TextField(null=True, blank=True)
1391
    start_date              =   models.DateTimeField(null=True, blank=True)
1392
    end_date                =   models.DateTimeField()
1393
    member_join_policy      =   models.IntegerField()
1394
    member_leave_policy     =   models.IntegerField()
1395
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1396
    resource_grants         =   models.ManyToManyField(
1397
                                    Resource,
1398
                                    null=True,
1399
                                    blank=True,
1400
                                    through='ProjectResourceGrant')
1401
    comments                =   models.TextField(null=True, blank=True)
1402
    issue_date              =   models.DateTimeField(auto_now_add=True)
1403
    response_date           =   models.DateTimeField(null=True, blank=True)
1404
    response                =   models.TextField(null=True, blank=True)
1405

    
1406
    objects                 =   ProjectApplicationManager()
1407

    
1408
    # Compiled queries
1409
    Q_PENDING  = Q(state=PENDING)
1410
    Q_APPROVED = Q(state=APPROVED)
1411
    Q_DENIED   = Q(state=DENIED)
1412

    
1413
    class Meta:
1414
        unique_together = ("chain", "id")
1415

    
1416
    def __unicode__(self):
1417
        return "%s applied by %s" % (self.name, self.applicant)
1418

    
1419
    # TODO: Move to a more suitable place
1420
    APPLICATION_STATE_DISPLAY = {
1421
        PENDING  : _('Pending review'),
1422
        APPROVED : _('Approved'),
1423
        REPLACED : _('Replaced'),
1424
        DENIED   : _('Denied'),
1425
        DISMISSED: _('Dismissed'),
1426
        CANCELLED: _('Cancelled')
1427
    }
1428

    
1429
    @property
1430
    def log_display(self):
1431
        return "application %s (%s) for project %s" % (
1432
            self.id, self.name, self.chain)
1433

    
1434
    def get_project(self):
1435
        try:
1436
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1437
            return Project
1438
        except Project.DoesNotExist, e:
1439
            return None
1440

    
1441
    def state_display(self):
1442
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1443

    
1444
    def project_state_display(self):
1445
        try:
1446
            project = self.project
1447
            return project.state_display()
1448
        except Project.DoesNotExist:
1449
            return self.state_display()
1450

    
1451
    def add_resource_policy(self, service, resource, uplimit):
1452
        """Raises ObjectDoesNotExist, IntegrityError"""
1453
        q = self.projectresourcegrant_set
1454
        resource = Resource.objects.get(service__name=service, name=resource)
1455
        q.create(resource=resource, member_capacity=uplimit)
1456

    
1457
    def members_count(self):
1458
        return self.project.approved_memberships.count()
1459

    
1460
    @property
1461
    def grants(self):
1462
        return self.projectresourcegrant_set.values(
1463
            'member_capacity', 'resource__name', 'resource__service__name')
1464

    
1465
    @property
1466
    def resource_policies(self):
1467
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1468

    
1469
    @resource_policies.setter
1470
    def resource_policies(self, policies):
1471
        for p in policies:
1472
            service = p.get('service', None)
1473
            resource = p.get('resource', None)
1474
            uplimit = p.get('uplimit', 0)
1475
            self.add_resource_policy(service, resource, uplimit)
1476

    
1477
    def pending_modifications_incl_me(self):
1478
        q = self.chained_applications()
1479
        q = q.filter(Q(state=self.PENDING))
1480
        return q
1481

    
1482
    def last_pending_incl_me(self):
1483
        try:
1484
            return self.pending_modifications_incl_me().order_by('-id')[0]
1485
        except IndexError:
1486
            return None
1487

    
1488
    def pending_modifications(self):
1489
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1490

    
1491
    def last_pending(self):
1492
        try:
1493
            return self.pending_modifications().order_by('-id')[0]
1494
        except IndexError:
1495
            return None
1496

    
1497
    def is_modification(self):
1498
        # if self.state != self.PENDING:
1499
        #     return False
1500
        parents = self.chained_applications().filter(id__lt=self.id)
1501
        parents = parents.filter(state__in=[self.APPROVED])
1502
        return parents.count() > 0
1503

    
1504
    def chained_applications(self):
1505
        return ProjectApplication.objects.filter(chain=self.chain)
1506

    
1507
    def is_latest(self):
1508
        return self.chained_applications().order_by('-id')[0] == self
1509

    
1510
    def has_pending_modifications(self):
1511
        return bool(self.last_pending())
1512

    
1513
    def denied_modifications(self):
1514
        q = self.chained_applications()
1515
        q = q.filter(Q(state=self.DENIED))
1516
        q = q.filter(~Q(id=self.id))
1517
        return q
1518

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

    
1525
    def has_denied_modifications(self):
1526
        return bool(self.last_denied())
1527

    
1528
    def is_applied(self):
1529
        try:
1530
            self.project
1531
            return True
1532
        except Project.DoesNotExist:
1533
            return False
1534

    
1535
    def get_project(self):
1536
        try:
1537
            return Project.objects.get(id=self.chain)
1538
        except Project.DoesNotExist:
1539
            return None
1540

    
1541
    def project_exists(self):
1542
        return self.get_project() is not None
1543

    
1544
    def _get_project_for_update(self):
1545
        try:
1546
            objects = Project.objects
1547
            project = objects.get_for_update(id=self.chain)
1548
            return project
1549
        except Project.DoesNotExist:
1550
            return None
1551

    
1552
    def can_cancel(self):
1553
        return self.state == self.PENDING
1554

    
1555
    def cancel(self):
1556
        if not self.can_cancel():
1557
            m = _("cannot cancel: application '%s' in state '%s'") % (
1558
                    self.id, self.state)
1559
            raise AssertionError(m)
1560

    
1561
        self.state = self.CANCELLED
1562
        self.save()
1563

    
1564
    def can_dismiss(self):
1565
        return self.state == self.DENIED
1566

    
1567
    def dismiss(self):
1568
        if not self.can_dismiss():
1569
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1570
                    self.id, self.state)
1571
            raise AssertionError(m)
1572

    
1573
        self.state = self.DISMISSED
1574
        self.save()
1575

    
1576
    def can_deny(self):
1577
        return self.state == self.PENDING
1578

    
1579
    def deny(self, reason):
1580
        if not self.can_deny():
1581
            m = _("cannot deny: application '%s' in state '%s'") % (
1582
                    self.id, self.state)
1583
            raise AssertionError(m)
1584

    
1585
        self.state = self.DENIED
1586
        self.response_date = datetime.now()
1587
        self.response = reason
1588
        self.save()
1589

    
1590
    def can_approve(self):
1591
        return self.state == self.PENDING
1592

    
1593
    def approve(self, approval_user=None):
1594
        """
1595
        If approval_user then during owner membership acceptance
1596
        it is checked whether the request_user is eligible.
1597

1598
        Raises:
1599
            PermissionDenied
1600
        """
1601

    
1602
        if not transaction.is_managed():
1603
            raise AssertionError("NOPE")
1604

    
1605
        new_project_name = self.name
1606
        if not self.can_approve():
1607
            m = _("cannot approve: project '%s' in state '%s'") % (
1608
                    new_project_name, self.state)
1609
            raise AssertionError(m) # invalid argument
1610

    
1611
        now = datetime.now()
1612
        project = self._get_project_for_update()
1613

    
1614
        try:
1615
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1616
            conflicting_project = Project.objects.get(q)
1617
            if (conflicting_project != project):
1618
                m = (_("cannot approve: project with name '%s' "
1619
                       "already exists (id: %s)") % (
1620
                        new_project_name, conflicting_project.id))
1621
                raise PermissionDenied(m) # invalid argument
1622
        except Project.DoesNotExist:
1623
            pass
1624

    
1625
        new_project = False
1626
        if project is None:
1627
            new_project = True
1628
            project = Project(id=self.chain)
1629

    
1630
        project.name = new_project_name
1631
        project.application = self
1632
        project.last_approval_date = now
1633
        if not new_project:
1634
            project.is_modified = True
1635

    
1636
        project.save()
1637

    
1638
        self.state = self.APPROVED
1639
        self.response_date = now
1640
        self.save()
1641
        return project
1642

    
1643
    @property
1644
    def member_join_policy_display(self):
1645
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1646

    
1647
    @property
1648
    def member_leave_policy_display(self):
1649
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1650

    
1651
class ProjectResourceGrant(models.Model):
1652

    
1653
    resource                =   models.ForeignKey(Resource)
1654
    project_application     =   models.ForeignKey(ProjectApplication,
1655
                                                  null=True)
1656
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1657
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1658
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1659
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1660
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1661
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1662

    
1663
    objects = ExtendedManager()
1664

    
1665
    class Meta:
1666
        unique_together = ("resource", "project_application")
1667

    
1668
    def display_member_capacity(self):
1669
        if self.member_capacity:
1670
            if self.resource.unit:
1671
                return ProjectResourceGrant.display_filesize(
1672
                    self.member_capacity)
1673
            else:
1674
                if math.isinf(self.member_capacity):
1675
                    return 'Unlimited'
1676
                else:
1677
                    return self.member_capacity
1678
        else:
1679
            return 'Unlimited'
1680

    
1681
    def __str__(self):
1682
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1683
                                        self.display_member_capacity())
1684

    
1685
    @classmethod
1686
    def display_filesize(cls, value):
1687
        try:
1688
            value = float(value)
1689
        except:
1690
            return
1691
        else:
1692
            if math.isinf(value):
1693
                return 'Unlimited'
1694
            if value > 1:
1695
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1696
                                [0, 0, 0, 0, 0, 0])
1697
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1698
                quotient = float(value) / 1024**exponent
1699
                unit, value_decimals = unit_list[exponent]
1700
                format_string = '{0:.%sf} {1}' % (value_decimals)
1701
                return format_string.format(quotient, unit)
1702
            if value == 0:
1703
                return '0 bytes'
1704
            if value == 1:
1705
                return '1 byte'
1706
            else:
1707
               return '0'
1708

    
1709

    
1710
class ProjectManager(ForUpdateManager):
1711

    
1712
    def terminated_projects(self):
1713
        q = self.model.Q_TERMINATED
1714
        return self.filter(q)
1715

    
1716
    def not_terminated_projects(self):
1717
        q = ~self.model.Q_TERMINATED
1718
        return self.filter(q)
1719

    
1720
    def deactivated_projects(self):
1721
        q = self.model.Q_DEACTIVATED
1722
        return self.filter(q)
1723

    
1724
    def modified_projects(self):
1725
        return self.filter(is_modified=True)
1726

    
1727
    def expired_projects(self):
1728
        q = (~Q(state=Project.TERMINATED) &
1729
              Q(application__end_date__lt=datetime.now()))
1730
        return self.filter(q)
1731

    
1732
    def search_by_name(self, *search_strings):
1733
        q = Q()
1734
        for s in search_strings:
1735
            q = q | Q(name__icontains=s)
1736
        return self.filter(q)
1737

    
1738

    
1739
class Project(models.Model):
1740

    
1741
    id                          =   models.OneToOneField(Chain,
1742
                                                      related_name='chained_project',
1743
                                                      db_column='id',
1744
                                                      primary_key=True)
1745

    
1746
    application                 =   models.OneToOneField(
1747
                                            ProjectApplication,
1748
                                            related_name='project')
1749
    last_approval_date          =   models.DateTimeField(null=True)
1750

    
1751
    members                     =   models.ManyToManyField(
1752
                                            AstakosUser,
1753
                                            through='ProjectMembership')
1754

    
1755
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1756
    deactivation_date           =   models.DateTimeField(null=True)
1757

    
1758
    creation_date               =   models.DateTimeField(auto_now_add=True)
1759
    name                        =   models.CharField(
1760
                                            max_length=80,
1761
                                            null=True,
1762
                                            db_index=True,
1763
                                            unique=True)
1764

    
1765
    APPROVED    = 1
1766
    SUSPENDED   = 10
1767
    TERMINATED  = 100
1768

    
1769
    is_modified                 =   models.BooleanField(default=False,
1770
                                                        db_index=True)
1771
    is_active                   =   models.BooleanField(default=True,
1772
                                                        db_index=True)
1773
    state                       =   models.IntegerField(default=APPROVED,
1774
                                                        db_index=True)
1775

    
1776
    objects     =   ProjectManager()
1777

    
1778
    # Compiled queries
1779
    Q_TERMINATED  = Q(state=TERMINATED)
1780
    Q_SUSPENDED   = Q(state=SUSPENDED)
1781
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1782

    
1783
    def __str__(self):
1784
        return uenc(_("<project %s '%s'>") %
1785
                    (self.id, udec(self.application.name)))
1786

    
1787
    __repr__ = __str__
1788

    
1789
    def __unicode__(self):
1790
        return _("<project %s '%s'>") % (self.id, self.application.name)
1791

    
1792
    STATE_DISPLAY = {
1793
        APPROVED   : 'Active',
1794
        SUSPENDED  : 'Suspended',
1795
        TERMINATED : 'Terminated'
1796
        }
1797

    
1798
    def state_display(self):
1799
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1800

    
1801
    def expiration_info(self):
1802
        return (str(self.id), self.name, self.state_display(),
1803
                str(self.application.end_date))
1804

    
1805
    def is_deactivated(self, reason=None):
1806
        if reason is not None:
1807
            return self.state == reason
1808

    
1809
        return self.state != self.APPROVED
1810

    
1811
    ### Deactivation calls
1812

    
1813
    def terminate(self):
1814
        self.deactivation_reason = 'TERMINATED'
1815
        self.deactivation_date = datetime.now()
1816
        self.state = self.TERMINATED
1817
        self.name = None
1818
        self.save()
1819

    
1820
    def suspend(self):
1821
        self.deactivation_reason = 'SUSPENDED'
1822
        self.deactivation_date = datetime.now()
1823
        self.state = self.SUSPENDED
1824
        self.save()
1825

    
1826
    def resume(self):
1827
        self.deactivation_reason = None
1828
        self.deactivation_date = None
1829
        self.state = self.APPROVED
1830
        self.save()
1831

    
1832
    ### Logical checks
1833

    
1834
    def is_inconsistent(self):
1835
        now = datetime.now()
1836
        dates = [self.creation_date,
1837
                 self.last_approval_date,
1838
                 self.deactivation_date]
1839
        return any([date > now for date in dates])
1840

    
1841
    def is_active_strict(self):
1842
        return self.is_active and self.state == self.APPROVED
1843

    
1844
    def is_approved(self):
1845
        return self.state == self.APPROVED
1846

    
1847
    @property
1848
    def is_alive(self):
1849
        return not self.is_terminated
1850

    
1851
    @property
1852
    def is_terminated(self):
1853
        return self.is_deactivated(self.TERMINATED)
1854

    
1855
    @property
1856
    def is_suspended(self):
1857
        return self.is_deactivated(self.SUSPENDED)
1858

    
1859
    def violates_resource_grants(self):
1860
        return False
1861

    
1862
    def violates_members_limit(self, adding=0):
1863
        application = self.application
1864
        limit = application.limit_on_members_number
1865
        if limit is None:
1866
            return False
1867
        return (len(self.approved_members) + adding > limit)
1868

    
1869

    
1870
    ### Other
1871

    
1872
    def count_pending_memberships(self):
1873
        memb_set = self.projectmembership_set
1874
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1875
        return memb_count
1876

    
1877
    def members_count(self):
1878
        return self.approved_memberships.count()
1879

    
1880
    @property
1881
    def approved_memberships(self):
1882
        query = ProjectMembership.Q_ACCEPTED_STATES
1883
        return self.projectmembership_set.filter(query)
1884

    
1885
    @property
1886
    def approved_members(self):
1887
        return [m.person for m in self.approved_memberships]
1888

    
1889
    def add_member(self, user):
1890
        """
1891
        Raises:
1892
            django.exceptions.PermissionDenied
1893
            astakos.im.models.AstakosUser.DoesNotExist
1894
        """
1895
        if isinstance(user, (int, long)):
1896
            user = AstakosUser.objects.get(user=user)
1897

    
1898
        m, created = ProjectMembership.objects.get_or_create(
1899
            person=user, project=self
1900
        )
1901
        m.accept()
1902

    
1903
    def remove_member(self, user):
1904
        """
1905
        Raises:
1906
            django.exceptions.PermissionDenied
1907
            astakos.im.models.AstakosUser.DoesNotExist
1908
            astakos.im.models.ProjectMembership.DoesNotExist
1909
        """
1910
        if isinstance(user, (int, long)):
1911
            user = AstakosUser.objects.get(user=user)
1912

    
1913
        m = ProjectMembership.objects.get(person=user, project=self)
1914
        m.remove()
1915

    
1916

    
1917
CHAIN_STATE = {
1918
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1919
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1920
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1921
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1922
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1923

    
1924
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1925
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1926
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1927
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1928
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1929

    
1930
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1931
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1932
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1933
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1934
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1935

    
1936
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1937
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1938
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1939
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1940
    }
1941

    
1942

    
1943
class ProjectMembershipManager(ForUpdateManager):
1944

    
1945
    def any_accepted(self):
1946
        q = self.model.Q_ACTUALLY_ACCEPTED
1947
        return self.filter(q)
1948

    
1949
    def actually_accepted(self):
1950
        q = self.model.Q_ACTUALLY_ACCEPTED
1951
        return self.filter(q)
1952

    
1953
    def requested(self):
1954
        return self.filter(state=ProjectMembership.REQUESTED)
1955

    
1956
    def suspended(self):
1957
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1958

    
1959
class ProjectMembership(models.Model):
1960

    
1961
    person              =   models.ForeignKey(AstakosUser)
1962
    request_date        =   models.DateField(auto_now_add=True)
1963
    project             =   models.ForeignKey(Project)
1964

    
1965
    REQUESTED           =   0
1966
    ACCEPTED            =   1
1967
    LEAVE_REQUESTED     =   5
1968
    # User deactivation
1969
    USER_SUSPENDED      =   10
1970

    
1971
    REMOVED             =   200
1972

    
1973
    ASSOCIATED_STATES   =   set([REQUESTED,
1974
                                 ACCEPTED,
1975
                                 LEAVE_REQUESTED,
1976
                                 USER_SUSPENDED,
1977
                                 ])
1978

    
1979
    ACCEPTED_STATES     =   set([ACCEPTED,
1980
                                 LEAVE_REQUESTED,
1981
                                 USER_SUSPENDED,
1982
                                 ])
1983

    
1984
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1985

    
1986
    state               =   models.IntegerField(default=REQUESTED,
1987
                                                db_index=True)
1988
    is_pending          =   models.BooleanField(default=False, db_index=True)
1989
    is_active           =   models.BooleanField(default=False, db_index=True)
1990
    application         =   models.ForeignKey(
1991
                                ProjectApplication,
1992
                                null=True,
1993
                                related_name='memberships')
1994
    pending_application =   models.ForeignKey(
1995
                                ProjectApplication,
1996
                                null=True,
1997
                                related_name='pending_memberships')
1998
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1999

    
2000
    acceptance_date     =   models.DateField(null=True, db_index=True)
2001
    leave_request_date  =   models.DateField(null=True)
2002

    
2003
    objects     =   ProjectMembershipManager()
2004

    
2005
    # Compiled queries
2006
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2007
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2008

    
2009
    MEMBERSHIP_STATE_DISPLAY = {
2010
        REQUESTED           : _('Requested'),
2011
        ACCEPTED            : _('Accepted'),
2012
        LEAVE_REQUESTED     : _('Leave Requested'),
2013
        USER_SUSPENDED      : _('Suspended'),
2014
        REMOVED             : _('Pending removal'),
2015
        }
2016

    
2017
    USER_FRIENDLY_STATE_DISPLAY = {
2018
        REQUESTED           : _('Join requested'),
2019
        ACCEPTED            : _('Accepted member'),
2020
        LEAVE_REQUESTED     : _('Requested to leave'),
2021
        USER_SUSPENDED      : _('Suspended member'),
2022
        REMOVED             : _('Pending removal'),
2023
        }
2024

    
2025
    def state_display(self):
2026
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2027

    
2028
    def user_friendly_state_display(self):
2029
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2030

    
2031
    class Meta:
2032
        unique_together = ("person", "project")
2033
        #index_together = [["project", "state"]]
2034

    
2035
    def __str__(self):
2036
        return uenc(_("<'%s' membership in '%s'>") % (
2037
                self.person.username, self.project))
2038

    
2039
    __repr__ = __str__
2040

    
2041
    def __init__(self, *args, **kwargs):
2042
        self.state = self.REQUESTED
2043
        super(ProjectMembership, self).__init__(*args, **kwargs)
2044

    
2045
    def _set_history_item(self, reason, date=None):
2046
        if isinstance(reason, basestring):
2047
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2048

    
2049
        history_item = ProjectMembershipHistory(
2050
                            serial=self.id,
2051
                            person=self.person_id,
2052
                            project=self.project_id,
2053
                            date=date or datetime.now(),
2054
                            reason=reason)
2055
        history_item.save()
2056
        serial = history_item.id
2057

    
2058
    def can_accept(self):
2059
        return self.state == self.REQUESTED
2060

    
2061
    def accept(self):
2062
        if not self.can_accept():
2063
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2064
            raise AssertionError(m)
2065

    
2066
        now = datetime.now()
2067
        self.acceptance_date = now
2068
        self._set_history_item(reason='ACCEPT', date=now)
2069
        self.state = self.ACCEPTED
2070
        self.save()
2071

    
2072
    def can_leave(self):
2073
        return self.state in self.ACCEPTED_STATES
2074

    
2075
    def leave_request(self):
2076
        if not self.can_leave():
2077
            m = _("%s: attempt to request to leave in state '%s'") % (
2078
                self, self.state)
2079
            raise AssertionError(m)
2080

    
2081
        self.leave_request_date = datetime.now()
2082
        self.state = self.LEAVE_REQUESTED
2083
        self.save()
2084

    
2085
    def can_deny_leave(self):
2086
        return self.state == self.LEAVE_REQUESTED
2087

    
2088
    def leave_request_deny(self):
2089
        if not self.can_deny_leave():
2090
            m = _("%s: attempt to deny leave request in state '%s'") % (
2091
                self, self.state)
2092
            raise AssertionError(m)
2093

    
2094
        self.leave_request_date = None
2095
        self.state = self.ACCEPTED
2096
        self.save()
2097

    
2098
    def can_cancel_leave(self):
2099
        return self.state == self.LEAVE_REQUESTED
2100

    
2101
    def leave_request_cancel(self):
2102
        if not self.can_cancel_leave():
2103
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2104
                self, self.state)
2105
            raise AssertionError(m)
2106

    
2107
        self.leave_request_date = None
2108
        self.state = self.ACCEPTED
2109
        self.save()
2110

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

    
2114
    def remove(self):
2115
        if not self.can_remove():
2116
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2117
            raise AssertionError(m)
2118

    
2119
        self._set_history_item(reason='REMOVE')
2120
        self.delete()
2121

    
2122
    def can_reject(self):
2123
        return self.state == self.REQUESTED
2124

    
2125
    def reject(self):
2126
        if not self.can_reject():
2127
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2128
            raise AssertionError(m)
2129

    
2130
        # rejected requests don't need sync,
2131
        # because they were never effected
2132
        self._set_history_item(reason='REJECT')
2133
        self.delete()
2134

    
2135
    def can_cancel(self):
2136
        return self.state == self.REQUESTED
2137

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

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

    
2148

    
2149
class Serial(models.Model):
2150
    serial  =   models.AutoField(primary_key=True)
2151

    
2152

    
2153
class ProjectMembershipHistory(models.Model):
2154
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2155
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2156

    
2157
    person  =   models.BigIntegerField()
2158
    project =   models.BigIntegerField()
2159
    date    =   models.DateField(auto_now_add=True)
2160
    reason  =   models.IntegerField()
2161
    serial  =   models.BigIntegerField()
2162

    
2163
### SIGNALS ###
2164
################
2165

    
2166
def create_astakos_user(u):
2167
    try:
2168
        AstakosUser.objects.get(user_ptr=u.pk)
2169
    except AstakosUser.DoesNotExist:
2170
        extended_user = AstakosUser(user_ptr_id=u.pk)
2171
        extended_user.__dict__.update(u.__dict__)
2172
        extended_user.save()
2173
        if not extended_user.has_auth_provider('local'):
2174
            extended_user.add_auth_provider('local')
2175
    except BaseException, e:
2176
        logger.exception(e)
2177

    
2178
def fix_superusers():
2179
    # Associate superusers with AstakosUser
2180
    admins = User.objects.filter(is_superuser=True)
2181
    for u in admins:
2182
        create_astakos_user(u)
2183

    
2184
def user_post_save(sender, instance, created, **kwargs):
2185
    if not created:
2186
        return
2187
    create_astakos_user(instance)
2188
post_save.connect(user_post_save, sender=User)
2189

    
2190
def astakosuser_post_save(sender, instance, created, **kwargs):
2191
    pass
2192

    
2193
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2194

    
2195
def resource_post_save(sender, instance, created, **kwargs):
2196
    pass
2197

    
2198
post_save.connect(resource_post_save, sender=Resource)
2199

    
2200
def renew_token(sender, instance, **kwargs):
2201
    if not instance.auth_token:
2202
        instance.renew_token()
2203
pre_save.connect(renew_token, sender=AstakosUser)
2204
pre_save.connect(renew_token, sender=Service)