Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 26551b92

History | View | Annotate | Download (73.9 kB)

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

    
34
import hashlib
35
import uuid
36
import logging
37
import json
38
import math
39
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
inf = float('inf')
102

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

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

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

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

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

    
133

    
134
_presentation_data = {}
135
def get_presentation(resource):
136
    global _presentation_data
137
    presentation = _presentation_data.get(resource, {})
138
    if not presentation:
139
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
140
        presentation = resource_presentation.get(resource, {})
141
        _presentation_data[resource] = presentation
142
    return presentation
143

    
144
class Resource(models.Model):
145
    name = models.CharField(_('Name'), max_length=255, unique=True)
146
    desc = models.TextField(_('Description'), null=True)
147
    service = models.CharField(_('Service identifier'), max_length=255,
148
                               null=True)
149
    unit = models.CharField(_('Unit'), null=True, max_length=255)
150
    uplimit = intDecimalField(default=0)
151

    
152
    objects = ForUpdateManager()
153

    
154
    def __str__(self):
155
        return self.name
156

    
157
    def full_name(self):
158
        return str(self)
159

    
160
    def get_info(self):
161
        return {'service': str(self.service),
162
                'description': self.desc,
163
                'unit': self.unit,
164
                }
165

    
166
    @property
167
    def group(self):
168
        default = self.name
169
        return get_presentation(str(self)).get('group', default)
170

    
171
    @property
172
    def help_text(self):
173
        default = "%s resource" % self.name
174
        return get_presentation(str(self)).get('help_text', default)
175

    
176
    @property
177
    def help_text_input_each(self):
178
        default = "%s resource" % self.name
179
        return get_presentation(str(self)).get('help_text_input_each', default)
180

    
181
    @property
182
    def is_abbreviation(self):
183
        return get_presentation(str(self)).get('is_abbreviation', False)
184

    
185
    @property
186
    def report_desc(self):
187
        default = "%s resource" % self.name
188
        return get_presentation(str(self)).get('report_desc', default)
189

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

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

    
198
    @property
199
    def display_name(self):
200
        name = self.verbose_name
201
        if self.is_abbreviation:
202
            name = name.upper()
203
        return name
204

    
205
    @property
206
    def pluralized_display_name(self):
207
        if not self.unit:
208
            return '%ss' % self.display_name
209
        return self.display_name
210

    
211
def get_resource_names():
212
    _RESOURCE_NAMES = []
213
    resources = Resource.objects.select_related('service').all()
214
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
215
    return _RESOURCE_NAMES
216

    
217

    
218
class AstakosUserManager(UserManager):
219

    
220
    def get_auth_provider_user(self, provider, **kwargs):
221
        """
222
        Retrieve AstakosUser instance associated with the specified third party
223
        id.
224
        """
225
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
226
                          kwargs.iteritems()))
227
        return self.get(auth_providers__module=provider, **kwargs)
228

    
229
    def get_by_email(self, email):
230
        return self.get(email=email)
231

    
232
    def get_by_identifier(self, email_or_username, **kwargs):
233
        try:
234
            return self.get(email__iexact=email_or_username, **kwargs)
235
        except AstakosUser.DoesNotExist:
236
            return self.get(username__iexact=email_or_username, **kwargs)
237

    
238
    def user_exists(self, email_or_username, **kwargs):
239
        qemail = Q(email__iexact=email_or_username)
240
        qusername = Q(username__iexact=email_or_username)
241
        qextra = Q(**kwargs)
242
        return self.filter((qemail | qusername) & qextra).exists()
243

    
244
    def verified_user_exists(self, email_or_username):
245
        return self.user_exists(email_or_username, email_verified=True)
246

    
247
    def verified(self):
248
        return self.filter(email_verified=True)
249

    
250
    def uuid_catalog(self, l=None):
251
        """
252
        Returns a uuid to username mapping for the uuids appearing in l.
253
        If l is None returns the mapping for all existing users.
254
        """
255
        q = self.filter(uuid__in=l) if l != None else self
256
        return dict(q.values_list('uuid', 'username'))
257

    
258
    def displayname_catalog(self, l=None):
259
        """
260
        Returns a username to uuid mapping for the usernames appearing in l.
261
        If l is None returns the mapping for all existing users.
262
        """
263
        if l is not None:
264
            lmap = dict((x.lower(), x) for x in l)
265
            q = self.filter(username__in=lmap.keys())
266
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
267
        else:
268
            q = self
269
            values = self.values_list('username', 'uuid')
270
        return dict(values)
271

    
272

    
273

    
274
class AstakosUser(User):
275
    """
276
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
277
    """
278
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
279
                                   null=True)
280

    
281
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
282
    #                    AstakosUserProvider model.
283
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
284
                                null=True)
285
    # ex. screen_name for twitter, eppn for shibboleth
286
    third_party_identifier = models.CharField(_('Third-party identifier'),
287
                                              max_length=255, null=True,
288
                                              blank=True)
289

    
290

    
291
    #for invitations
292
    user_level = DEFAULT_USER_LEVEL
293
    level = models.IntegerField(_('Inviter level'), default=user_level)
294
    invitations = models.IntegerField(
295
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
296

    
297
    auth_token = models.CharField(_('Authentication Token'),
298
                                  max_length=32,
299
                                  null=True,
300
                                  blank=True,
301
                                  help_text = _('Renew your authentication '
302
                                                'token. Make sure to set the new '
303
                                                'token in any client you may be '
304
                                                'using, to preserve its '
305
                                                'functionality.'))
306
    auth_token_created = models.DateTimeField(_('Token creation date'),
307
                                              null=True)
308
    auth_token_expires = models.DateTimeField(
309
        _('Token expiration date'), null=True)
310

    
311
    updated = models.DateTimeField(_('Update date'))
312
    is_verified = models.BooleanField(_('Is verified?'), default=False)
313

    
314
    email_verified = models.BooleanField(_('Email verified?'), default=False)
315

    
316
    has_credits = models.BooleanField(_('Has credits?'), default=False)
317
    has_signed_terms = models.BooleanField(
318
        _('I agree with the terms'), default=False)
319
    date_signed_terms = models.DateTimeField(
320
        _('Signed terms date'), null=True, blank=True)
321

    
322
    activation_sent = models.DateTimeField(
323
        _('Activation sent data'), null=True, blank=True)
324

    
325
    policy = models.ManyToManyField(
326
        Resource, null=True, through='AstakosUserQuota')
327

    
328
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
329

    
330
    __has_signed_terms = False
331
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
332
                                           default=False, db_index=True)
333

    
334
    objects = AstakosUserManager()
335

    
336
    forupdate = ForUpdateManager()
337

    
338
    def __init__(self, *args, **kwargs):
339
        super(AstakosUser, self).__init__(*args, **kwargs)
340
        self.__has_signed_terms = self.has_signed_terms
341
        if not self.id:
342
            self.is_active = False
343

    
344
    @property
345
    def realname(self):
346
        return '%s %s' % (self.first_name, self.last_name)
347

    
348
    @property
349
    def log_display(self):
350
        """
351
        Should be used in all logger.* calls that refer to a user so that
352
        user display is consistent across log entries.
353
        """
354
        return '%s::%s' % (self.uuid, self.email)
355

    
356
    @realname.setter
357
    def realname(self, value):
358
        parts = value.split(' ')
359
        if len(parts) == 2:
360
            self.first_name = parts[0]
361
            self.last_name = parts[1]
362
        else:
363
            self.last_name = parts[0]
364

    
365
    def add_permission(self, pname):
366
        if self.has_perm(pname):
367
            return
368
        p, created = Permission.objects.get_or_create(
369
                                    codename=pname,
370
                                    name=pname.capitalize(),
371
                                    content_type=get_content_type())
372
        self.user_permissions.add(p)
373

    
374
    def remove_permission(self, pname):
375
        if self.has_perm(pname):
376
            return
377
        p = Permission.objects.get(codename=pname,
378
                                   content_type=get_content_type())
379
        self.user_permissions.remove(p)
380

    
381
    def is_project_admin(self, application_id=None):
382
        return self.uuid in PROJECT_ADMINS
383

    
384
    @property
385
    def invitation(self):
386
        try:
387
            return Invitation.objects.get(username=self.email)
388
        except Invitation.DoesNotExist:
389
            return None
390

    
391
    @property
392
    def policies(self):
393
        return self.astakosuserquota_set.select_related().all()
394

    
395
    @policies.setter
396
    def policies(self, policies):
397
        for p in policies:
398
            p.setdefault('resource', '')
399
            p.setdefault('capacity', 0)
400
            p.setdefault('quantity', 0)
401
            p.setdefault('update', True)
402
            self.add_resource_policy(**p)
403

    
404
    def add_resource_policy(
405
            self, resource, capacity,
406
            update=True):
407
        """Raises ObjectDoesNotExist, IntegrityError"""
408
        resource = Resource.objects.get(name=resource)
409
        if update:
410
            AstakosUserQuota.objects.update_or_create(
411
                user=self, resource=resource, defaults={
412
                    'capacity':capacity,
413
                    })
414
        else:
415
            q = self.astakosuserquota_set
416
            q.create(
417
                resource=resource, capacity=capacity, quanity=quantity,
418
                )
419

    
420
    def get_resource_policy(self, resource):
421
        resource = Resource.objects.get(name=resource)
422
        default_capacity = resource.uplimit
423
        try:
424
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
425
            return policy, default_capacity
426
        except AstakosUserQuota.DoesNotExist:
427
            return None, default_capacity
428

    
429
    def remove_resource_policy(self, service, resource):
430
        """Raises ObjectDoesNotExist, IntegrityError"""
431
        resource = Resource.objects.get(name=resource)
432
        q = self.policies.get(resource=resource).delete()
433

    
434
    def update_uuid(self):
435
        while not self.uuid:
436
            uuid_val =  str(uuid.uuid4())
437
            try:
438
                AstakosUser.objects.get(uuid=uuid_val)
439
            except AstakosUser.DoesNotExist, e:
440
                self.uuid = uuid_val
441
        return self.uuid
442

    
443
    def save(self, update_timestamps=True, **kwargs):
444
        if update_timestamps:
445
            if not self.id:
446
                self.date_joined = datetime.now()
447
            self.updated = datetime.now()
448

    
449
        # update date_signed_terms if necessary
450
        if self.__has_signed_terms != self.has_signed_terms:
451
            self.date_signed_terms = datetime.now()
452

    
453
        self.update_uuid()
454

    
455
        if self.username != self.email.lower():
456
            # set username
457
            self.username = self.email.lower()
458

    
459
        super(AstakosUser, self).save(**kwargs)
460

    
461
    def renew_token(self, flush_sessions=False, current_key=None):
462
        md5 = hashlib.md5()
463
        md5.update(settings.SECRET_KEY)
464
        md5.update(self.username)
465
        md5.update(self.realname.encode('ascii', 'ignore'))
466
        md5.update(asctime())
467

    
468
        self.auth_token = b64encode(md5.digest())
469
        self.auth_token_created = datetime.now()
470
        self.auth_token_expires = self.auth_token_created + \
471
                                  timedelta(hours=AUTH_TOKEN_DURATION)
472
        if flush_sessions:
473
            self.flush_sessions(current_key)
474
        msg = 'Token renewed for %s' % self.email
475
        logger.log(LOGGING_LEVEL, msg)
476

    
477
    def flush_sessions(self, current_key=None):
478
        q = self.sessions
479
        if current_key:
480
            q = q.exclude(session_key=current_key)
481

    
482
        keys = q.values_list('session_key', flat=True)
483
        if keys:
484
            msg = 'Flushing sessions: %s' % ','.join(keys)
485
            logger.log(LOGGING_LEVEL, msg, [])
486
        engine = import_module(settings.SESSION_ENGINE)
487
        for k in keys:
488
            s = engine.SessionStore(k)
489
            s.flush()
490

    
491
    def __unicode__(self):
492
        return '%s (%s)' % (self.realname, self.email)
493

    
494
    def conflicting_email(self):
495
        q = AstakosUser.objects.exclude(username=self.username)
496
        q = q.filter(email__iexact=self.email)
497
        if q.count() != 0:
498
            return True
499
        return False
500

    
501
    def email_change_is_pending(self):
502
        return self.emailchanges.count() > 0
503

    
504
    @property
505
    def signed_terms(self):
506
        term = get_latest_terms()
507
        if not term:
508
            return True
509
        if not self.has_signed_terms:
510
            return False
511
        if not self.date_signed_terms:
512
            return False
513
        if self.date_signed_terms < term.date:
514
            self.has_signed_terms = False
515
            self.date_signed_terms = None
516
            self.save()
517
            return False
518
        return True
519

    
520
    def set_invitations_level(self):
521
        """
522
        Update user invitation level
523
        """
524
        level = self.invitation.inviter.level + 1
525
        self.level = level
526
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
527

    
528
    def can_change_password(self):
529
        return self.has_auth_provider('local', auth_backend='astakos')
530

    
531
    def can_change_email(self):
532
        if not self.has_auth_provider('local'):
533
            return True
534

    
535
        local = self.get_auth_provider('local')._instance
536
        return local.auth_backend == 'astakos'
537

    
538
    # Auth providers related methods
539
    def get_auth_provider(self, module=None, identifier=None, **filters):
540
        if not module:
541
            return self.auth_providers.active()[0].settings
542

    
543
        params = {'module': module}
544
        if identifier:
545
            params['identifier'] = identifier
546
        params.update(filters)
547
        return self.auth_providers.active().get(**params).settings
548

    
549
    def has_auth_provider(self, provider, **kwargs):
550
        return bool(self.auth_providers.active().filter(module=provider,
551
                                                        **kwargs).count())
552

    
553
    def get_required_providers(self, **kwargs):
554
        return auth.REQUIRED_PROVIDERS.keys()
555

    
556
    def missing_required_providers(self):
557
        required = self.get_required_providers()
558
        missing = []
559
        for provider in required:
560
            if not self.has_auth_provider(provider):
561
                missing.append(auth.get_provider(provider, self))
562
        return missing
563

    
564
    def get_available_auth_providers(self, **filters):
565
        """
566
        Returns a list of providers available for add by the user.
567
        """
568
        modules = astakos_settings.IM_MODULES
569
        providers = []
570
        for p in modules:
571
            providers.append(auth.get_provider(p, self))
572
        available = []
573

    
574
        for p in providers:
575
            if p.get_add_policy:
576
                available.append(p)
577
        return available
578

    
579
    def get_disabled_auth_providers(self, **filters):
580
        providers = self.get_auth_providers(**filters)
581
        disabled = []
582
        for p in providers:
583
            if not p.get_login_policy:
584
                disabled.append(p)
585
        return disabled
586

    
587
    def get_enabled_auth_providers(self, **filters):
588
        providers = self.get_auth_providers(**filters)
589
        enabled = []
590
        for p in providers:
591
            if p.get_login_policy:
592
                enabled.append(p)
593
        return enabled
594

    
595
    def get_auth_providers(self, **filters):
596
        providers = []
597
        for provider in self.auth_providers.active(**filters):
598
            if provider.settings.module_enabled:
599
                providers.append(provider.settings)
600

    
601
        modules = astakos_settings.IM_MODULES
602

    
603
        def key(p):
604
            if not p.module in modules:
605
                return 100
606
            return modules.index(p.module)
607

    
608
        providers = sorted(providers, key=key)
609
        return providers
610

    
611
    # URL methods
612
    @property
613
    def auth_providers_display(self):
614
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
615
                         self.get_enabled_auth_providers()])
616

    
617
    def add_auth_provider(self, module='local', identifier=None, **params):
618
        provider = auth.get_provider(module, self, identifier, **params)
619
        provider.add_to_user()
620

    
621
    def get_resend_activation_url(self):
622
        return reverse('send_activation', kwargs={'user_id': self.pk})
623

    
624
    def get_activation_url(self, nxt=False):
625
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
626
                                 quote(self.auth_token))
627
        if nxt:
628
            url += "&next=%s" % quote(nxt)
629
        return url
630

    
631
    def get_password_reset_url(self, token_generator=default_token_generator):
632
        return reverse('django.contrib.auth.views.password_reset_confirm',
633
                          kwargs={'uidb36':int_to_base36(self.id),
634
                                  'token':token_generator.make_token(self)})
635

    
636
    def get_inactive_message(self, provider_module, identifier=None):
637
        provider = self.get_auth_provider(provider_module, identifier)
638

    
639
        msg_extra = ''
640
        message = ''
641

    
642
        msg_inactive = provider.get_account_inactive_msg
643
        msg_pending = provider.get_pending_activation_msg
644
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
645
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
646
        msg_pending_mod = provider.get_pending_moderation_msg
647
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
648

    
649
        if self.activation_sent:
650
            if self.email_verified:
651
                message = msg_inactive
652
            else:
653
                message = msg_pending
654
                url = self.get_resend_activation_url()
655
                msg_extra = msg_pending_help + \
656
                            u' ' + \
657
                            '<a href="%s">%s?</a>' % (url, msg_resend)
658
        else:
659
            if astakos_settings.MODERATION_ENABLED:
660
                message = msg_pending_mod
661
            else:
662
                message = msg_pending
663
                url = self.get_resend_activation_url()
664
                msg_extra = '<a href="%s">%s?</a>' % (url, \
665
                                msg_resend)
666

    
667
        return mark_safe(message + u' '+ msg_extra)
668

    
669
    def owns_application(self, application):
670
        return application.owner == self
671

    
672
    def owns_project(self, project):
673
        return project.application.owner == self
674

    
675
    def is_associated(self, project):
676
        try:
677
            m = ProjectMembership.objects.get(person=self, project=project)
678
            return m.state in ProjectMembership.ASSOCIATED_STATES
679
        except ProjectMembership.DoesNotExist:
680
            return False
681

    
682
    def get_membership(self, project):
683
        try:
684
            return ProjectMembership.objects.get(
685
                project=project,
686
                person=self)
687
        except ProjectMembership.DoesNotExist:
688
            return None
689

    
690
    def membership_display(self, project):
691
        m = self.get_membership(project)
692
        if m is None:
693
            return _('Not a member')
694
        else:
695
            return m.user_friendly_state_display()
696

    
697
    def non_owner_can_view(self, maybe_project):
698
        if self.is_project_admin():
699
            return True
700
        if maybe_project is None:
701
            return False
702
        project = maybe_project
703
        if self.is_associated(project):
704
            return True
705
        if project.is_deactivated():
706
            return False
707
        return True
708

    
709
    def settings(self):
710
        return UserSetting.objects.filter(user=self)
711

    
712

    
713
class AstakosUserAuthProviderManager(models.Manager):
714

    
715
    def active(self, **filters):
716
        return self.filter(active=True, **filters)
717

    
718
    def remove_unverified_providers(self, provider, **filters):
719
        try:
720
            existing = self.filter(module=provider, user__email_verified=False,
721
                                   **filters)
722
            for p in existing:
723
                p.user.delete()
724
        except:
725
            pass
726

    
727
    def unverified(self, provider, **filters):
728
        try:
729
            return self.get(module=provider, user__email_verified=False,
730
                            **filters).settings
731
        except AstakosUserAuthProvider.DoesNotExist:
732
            return None
733

    
734
    def verified(self, provider, **filters):
735
        try:
736
            return self.get(module=provider, user__email_verified=True,
737
                            **filters).settings
738
        except AstakosUserAuthProvider.DoesNotExist:
739
            return None
740

    
741

    
742
class AuthProviderPolicyProfileManager(models.Manager):
743

    
744
    def active(self):
745
        return self.filter(active=True)
746

    
747
    def for_user(self, user, provider):
748
        policies = {}
749
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
750
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
751
        exclusive_q = exclusive_q1 | exclusive_q2
752

    
753
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
754
            policies.update(profile.policies)
755

    
756
        user_groups = user.groups.all().values('pk')
757
        for profile in self.active().filter(groups__in=user_groups).filter(
758
                exclusive_q):
759
            policies.update(profile.policies)
760
        return policies
761

    
762
    def add_policy(self, name, provider, group_or_user, exclusive=False,
763
                   **policies):
764
        is_group = isinstance(group_or_user, Group)
765
        profile, created = self.get_or_create(name=name, provider=provider,
766
                                              is_exclusive=exclusive)
767
        profile.is_exclusive = exclusive
768
        profile.save()
769
        if is_group:
770
            profile.groups.add(group_or_user)
771
        else:
772
            profile.users.add(group_or_user)
773
        profile.set_policies(policies)
774
        profile.save()
775
        return profile
776

    
777

    
778
class AuthProviderPolicyProfile(models.Model):
779
    name = models.CharField(_('Name'), max_length=255, blank=False,
780
                            null=False, db_index=True)
781
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
782
                                null=False)
783

    
784
    # apply policies to all providers excluding the one set in provider field
785
    is_exclusive = models.BooleanField(default=False)
786

    
787
    policy_add = models.NullBooleanField(null=True, default=None)
788
    policy_remove = models.NullBooleanField(null=True, default=None)
789
    policy_create = models.NullBooleanField(null=True, default=None)
790
    policy_login = models.NullBooleanField(null=True, default=None)
791
    policy_limit = models.IntegerField(null=True, default=None)
792
    policy_required = models.NullBooleanField(null=True, default=None)
793
    policy_automoderate = models.NullBooleanField(null=True, default=None)
794
    policy_switch = models.NullBooleanField(null=True, default=None)
795

    
796
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
797
                     'automoderate')
798

    
799
    priority = models.IntegerField(null=False, default=1)
800
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
801
    users = models.ManyToManyField(AstakosUser,
802
                                   related_name='authpolicy_profiles')
803
    active = models.BooleanField(default=True)
804

    
805
    objects = AuthProviderPolicyProfileManager()
806

    
807
    class Meta:
808
        ordering = ['priority']
809

    
810
    @property
811
    def policies(self):
812
        policies = {}
813
        for pkey in self.POLICY_FIELDS:
814
            value = getattr(self, 'policy_%s' % pkey, None)
815
            if value is None:
816
                continue
817
            policies[pkey] = value
818
        return policies
819

    
820
    def set_policies(self, policies_dict):
821
        for key, value in policies_dict.iteritems():
822
            if key in self.POLICY_FIELDS:
823
                setattr(self, 'policy_%s' % key, value)
824
        return self.policies
825

    
826

    
827
class AstakosUserAuthProvider(models.Model):
828
    """
829
    Available user authentication methods.
830
    """
831
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
832
                                   null=True, default=None)
833
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
834
    module = models.CharField(_('Provider'), max_length=255, blank=False,
835
                                default='local')
836
    identifier = models.CharField(_('Third-party identifier'),
837
                                              max_length=255, null=True,
838
                                              blank=True)
839
    active = models.BooleanField(default=True)
840
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
841
                                   default='astakos')
842
    info_data = models.TextField(default="", null=True, blank=True)
843
    created = models.DateTimeField('Creation date', auto_now_add=True)
844

    
845
    objects = AstakosUserAuthProviderManager()
846

    
847
    class Meta:
848
        unique_together = (('identifier', 'module', 'user'), )
849
        ordering = ('module', 'created')
850

    
851
    def __init__(self, *args, **kwargs):
852
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
853
        try:
854
            self.info = json.loads(self.info_data)
855
            if not self.info:
856
                self.info = {}
857
        except Exception, e:
858
            self.info = {}
859

    
860
        for key,value in self.info.iteritems():
861
            setattr(self, 'info_%s' % key, value)
862

    
863
    @property
864
    def settings(self):
865
        extra_data = {}
866

    
867
        info_data = {}
868
        if self.info_data:
869
            info_data = json.loads(self.info_data)
870

    
871
        extra_data['info'] = info_data
872

    
873
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
874
            extra_data[key] = getattr(self, key)
875

    
876
        extra_data['instance'] = self
877
        return auth.get_provider(self.module, self.user,
878
                                           self.identifier, **extra_data)
879

    
880
    def __repr__(self):
881
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
882

    
883
    def __unicode__(self):
884
        if self.identifier:
885
            return "%s:%s" % (self.module, self.identifier)
886
        if self.auth_backend:
887
            return "%s:%s" % (self.module, self.auth_backend)
888
        return self.module
889

    
890
    def save(self, *args, **kwargs):
891
        self.info_data = json.dumps(self.info)
892
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
893

    
894

    
895
class ExtendedManager(models.Manager):
896
    def _update_or_create(self, **kwargs):
897
        assert kwargs, \
898
            'update_or_create() must be passed at least one keyword argument'
899
        obj, created = self.get_or_create(**kwargs)
900
        defaults = kwargs.pop('defaults', {})
901
        if created:
902
            return obj, True, False
903
        else:
904
            try:
905
                params = dict(
906
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
907
                params.update(defaults)
908
                for attr, val in params.items():
909
                    if hasattr(obj, attr):
910
                        setattr(obj, attr, val)
911
                sid = transaction.savepoint()
912
                obj.save(force_update=True)
913
                transaction.savepoint_commit(sid)
914
                return obj, False, True
915
            except IntegrityError, e:
916
                transaction.savepoint_rollback(sid)
917
                try:
918
                    return self.get(**kwargs), False, False
919
                except self.model.DoesNotExist:
920
                    raise e
921

    
922
    update_or_create = _update_or_create
923

    
924

    
925
class AstakosUserQuota(models.Model):
926
    objects = ExtendedManager()
927
    capacity = intDecimalField()
928
    quantity = intDecimalField(default=0)
929
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
930
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
931
    resource = models.ForeignKey(Resource)
932
    user = models.ForeignKey(AstakosUser)
933

    
934
    class Meta:
935
        unique_together = ("resource", "user")
936

    
937

    
938
class ApprovalTerms(models.Model):
939
    """
940
    Model for approval terms
941
    """
942

    
943
    date = models.DateTimeField(
944
        _('Issue date'), db_index=True, auto_now_add=True)
945
    location = models.CharField(_('Terms location'), max_length=255)
946

    
947

    
948
class Invitation(models.Model):
949
    """
950
    Model for registring invitations
951
    """
952
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
953
                                null=True)
954
    realname = models.CharField(_('Real name'), max_length=255)
955
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
956
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
957
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
958
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
959
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
960

    
961
    def __init__(self, *args, **kwargs):
962
        super(Invitation, self).__init__(*args, **kwargs)
963
        if not self.id:
964
            self.code = _generate_invitation_code()
965

    
966
    def consume(self):
967
        self.is_consumed = True
968
        self.consumed = datetime.now()
969
        self.save()
970

    
971
    def __unicode__(self):
972
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
973

    
974

    
975
class EmailChangeManager(models.Manager):
976

    
977
    @transaction.commit_on_success
978
    def change_email(self, activation_key):
979
        """
980
        Validate an activation key and change the corresponding
981
        ``User`` if valid.
982

983
        If the key is valid and has not expired, return the ``User``
984
        after activating.
985

986
        If the key is not valid or has expired, return ``None``.
987

988
        If the key is valid but the ``User`` is already active,
989
        return ``None``.
990

991
        After successful email change the activation record is deleted.
992

993
        Throws ValueError if there is already
994
        """
995
        try:
996
            email_change = self.model.objects.get(
997
                activation_key=activation_key)
998
            if email_change.activation_key_expired():
999
                email_change.delete()
1000
                raise EmailChange.DoesNotExist
1001
            # is there an active user with this address?
1002
            try:
1003
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1004
            except AstakosUser.DoesNotExist:
1005
                pass
1006
            else:
1007
                raise ValueError(_('The new email address is reserved.'))
1008
            # update user
1009
            user = AstakosUser.objects.get(pk=email_change.user_id)
1010
            old_email = user.email
1011
            user.email = email_change.new_email_address
1012
            user.save()
1013
            email_change.delete()
1014
            msg = "User %s changed email from %s to %s" % (user.log_display,
1015
                                                           old_email,
1016
                                                           user.email)
1017
            logger.log(LOGGING_LEVEL, msg)
1018
            return user
1019
        except EmailChange.DoesNotExist:
1020
            raise ValueError(_('Invalid activation key.'))
1021

    
1022

    
1023
class EmailChange(models.Model):
1024
    new_email_address = models.EmailField(
1025
        _(u'new e-mail address'),
1026
        help_text=_('Provide a new email address. Until you verify the new '
1027
                    'address by following the activation link that will be '
1028
                    'sent to it, your old email address will remain active.'))
1029
    user = models.ForeignKey(
1030
        AstakosUser, unique=True, related_name='emailchanges')
1031
    requested_at = models.DateTimeField(auto_now_add=True)
1032
    activation_key = models.CharField(
1033
        max_length=40, unique=True, db_index=True)
1034

    
1035
    objects = EmailChangeManager()
1036

    
1037
    def get_url(self):
1038
        return reverse('email_change_confirm',
1039
                      kwargs={'activation_key': self.activation_key})
1040

    
1041
    def activation_key_expired(self):
1042
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1043
        return self.requested_at + expiration_date < datetime.now()
1044

    
1045

    
1046
class AdditionalMail(models.Model):
1047
    """
1048
    Model for registring invitations
1049
    """
1050
    owner = models.ForeignKey(AstakosUser)
1051
    email = models.EmailField()
1052

    
1053

    
1054
def _generate_invitation_code():
1055
    while True:
1056
        code = randint(1, 2L ** 63 - 1)
1057
        try:
1058
            Invitation.objects.get(code=code)
1059
            # An invitation with this code already exists, try again
1060
        except Invitation.DoesNotExist:
1061
            return code
1062

    
1063

    
1064
def get_latest_terms():
1065
    try:
1066
        term = ApprovalTerms.objects.order_by('-id')[0]
1067
        return term
1068
    except IndexError:
1069
        pass
1070
    return None
1071

    
1072

    
1073
class PendingThirdPartyUser(models.Model):
1074
    """
1075
    Model for registring successful third party user authentications
1076
    """
1077
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1078
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1079
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1080
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1081
                                  null=True)
1082
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1083
                                 null=True)
1084
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1085
                                   null=True)
1086
    username = models.CharField(_('username'), max_length=30, unique=True,
1087
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1088
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1089
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1090
    info = models.TextField(default="", null=True, blank=True)
1091

    
1092
    class Meta:
1093
        unique_together = ("provider", "third_party_identifier")
1094

    
1095
    def get_user_instance(self):
1096
        d = self.__dict__
1097
        d.pop('_state', None)
1098
        d.pop('id', None)
1099
        d.pop('token', None)
1100
        d.pop('created', None)
1101
        d.pop('info', None)
1102
        user = AstakosUser(**d)
1103

    
1104
        return user
1105

    
1106
    @property
1107
    def realname(self):
1108
        return '%s %s' %(self.first_name, self.last_name)
1109

    
1110
    @realname.setter
1111
    def realname(self, value):
1112
        parts = value.split(' ')
1113
        if len(parts) == 2:
1114
            self.first_name = parts[0]
1115
            self.last_name = parts[1]
1116
        else:
1117
            self.last_name = parts[0]
1118

    
1119
    def save(self, **kwargs):
1120
        if not self.id:
1121
            # set username
1122
            while not self.username:
1123
                username =  uuid.uuid4().hex[:30]
1124
                try:
1125
                    AstakosUser.objects.get(username = username)
1126
                except AstakosUser.DoesNotExist, e:
1127
                    self.username = username
1128
        super(PendingThirdPartyUser, self).save(**kwargs)
1129

    
1130
    def generate_token(self):
1131
        self.password = self.third_party_identifier
1132
        self.last_login = datetime.now()
1133
        self.token = default_token_generator.make_token(self)
1134

    
1135
    def existing_user(self):
1136
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1137
                                         auth_providers__identifier=self.third_party_identifier)
1138

    
1139
    def get_provider(self, user):
1140
        params = {
1141
            'info_data': self.info,
1142
            'affiliation': self.affiliation
1143
        }
1144
        return auth.get_provider(self.provider, user,
1145
                                 self.third_party_identifier, **params)
1146

    
1147
class SessionCatalog(models.Model):
1148
    session_key = models.CharField(_('session key'), max_length=40)
1149
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1150

    
1151

    
1152
class UserSetting(models.Model):
1153
    user = models.ForeignKey(AstakosUser)
1154
    setting = models.CharField(max_length=255)
1155
    value = models.IntegerField()
1156

    
1157
    objects = ForUpdateManager()
1158

    
1159
    class Meta:
1160
        unique_together = ("user", "setting")
1161

    
1162

    
1163
### PROJECTS ###
1164
################
1165

    
1166
class ChainManager(ForUpdateManager):
1167

    
1168
    def search_by_name(self, *search_strings):
1169
        projects = Project.objects.search_by_name(*search_strings)
1170
        chains = [p.id for p in projects]
1171
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1172
        apps = (app for app in apps if app.is_latest())
1173
        app_chains = [app.chain for app in apps if app.chain not in chains]
1174
        return chains + app_chains
1175

    
1176
    def all_full_state(self):
1177
        chains = self.all()
1178
        cids = [c.chain for c in chains]
1179
        projects = Project.objects.select_related('application').in_bulk(cids)
1180

    
1181
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1182
        chain_latest = dict(objs.values_list('chain', 'latest'))
1183

    
1184
        objs = ProjectApplication.objects.select_related('applicant')
1185
        apps = objs.in_bulk(chain_latest.values())
1186

    
1187
        d = {}
1188
        for chain in chains:
1189
            pk = chain.pk
1190
            project = projects.get(pk, None)
1191
            app = apps[chain_latest[pk]]
1192
            d[chain.pk] = chain.get_state(project, app)
1193

    
1194
        return d
1195

    
1196
    def of_project(self, project):
1197
        if project is None:
1198
            return None
1199
        try:
1200
            return self.get(chain=project.id)
1201
        except Chain.DoesNotExist:
1202
            raise AssertionError('project with no chain')
1203

    
1204

    
1205
class Chain(models.Model):
1206
    chain  =   models.AutoField(primary_key=True)
1207

    
1208
    def __str__(self):
1209
        return "%s" % (self.chain,)
1210

    
1211
    objects = ChainManager()
1212

    
1213
    PENDING            = 0
1214
    DENIED             = 3
1215
    DISMISSED          = 4
1216
    CANCELLED          = 5
1217

    
1218
    APPROVED           = 10
1219
    APPROVED_PENDING   = 11
1220
    SUSPENDED          = 12
1221
    SUSPENDED_PENDING  = 13
1222
    TERMINATED         = 14
1223
    TERMINATED_PENDING = 15
1224

    
1225
    PENDING_STATES = [PENDING,
1226
                      APPROVED_PENDING,
1227
                      SUSPENDED_PENDING,
1228
                      TERMINATED_PENDING,
1229
                      ]
1230

    
1231
    MODIFICATION_STATES = [APPROVED_PENDING,
1232
                           SUSPENDED_PENDING,
1233
                           TERMINATED_PENDING,
1234
                           ]
1235

    
1236
    RELEVANT_STATES = [PENDING,
1237
                       DENIED,
1238
                       APPROVED,
1239
                       APPROVED_PENDING,
1240
                       SUSPENDED,
1241
                       SUSPENDED_PENDING,
1242
                       TERMINATED_PENDING,
1243
                       ]
1244

    
1245
    SKIP_STATES = [DISMISSED,
1246
                   CANCELLED,
1247
                   TERMINATED]
1248

    
1249
    STATE_DISPLAY = {
1250
        PENDING            : _("Pending"),
1251
        DENIED             : _("Denied"),
1252
        DISMISSED          : _("Dismissed"),
1253
        CANCELLED          : _("Cancelled"),
1254
        APPROVED           : _("Active"),
1255
        APPROVED_PENDING   : _("Active - Pending"),
1256
        SUSPENDED          : _("Suspended"),
1257
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1258
        TERMINATED         : _("Terminated"),
1259
        TERMINATED_PENDING : _("Terminated - Pending"),
1260
        }
1261

    
1262

    
1263
    @classmethod
1264
    def _chain_state(cls, project_state, app_state):
1265
        s = CHAIN_STATE.get((project_state, app_state), None)
1266
        if s is None:
1267
            raise AssertionError('inconsistent chain state')
1268
        return s
1269

    
1270
    @classmethod
1271
    def chain_state(cls, project, app):
1272
        p_state = project.state if project else None
1273
        return cls._chain_state(p_state, app.state)
1274

    
1275
    @classmethod
1276
    def state_display(cls, s):
1277
        if s is None:
1278
            return _("Unknown")
1279
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1280

    
1281
    def last_application(self):
1282
        return self.chained_apps.order_by('-id')[0]
1283

    
1284
    def get_project(self):
1285
        try:
1286
            return self.chained_project
1287
        except Project.DoesNotExist:
1288
            return None
1289

    
1290
    def get_elements(self):
1291
        project = self.get_project()
1292
        app = self.last_application()
1293
        return project, app
1294

    
1295
    def get_state(self, project, app):
1296
        s = self.chain_state(project, app)
1297
        return s, project, app
1298

    
1299
    def full_state(self):
1300
        project, app = self.get_elements()
1301
        return self.get_state(project, app)
1302

    
1303

    
1304
def new_chain():
1305
    c = Chain.objects.create()
1306
    return c
1307

    
1308

    
1309
class ProjectApplicationManager(ForUpdateManager):
1310

    
1311
    def user_visible_projects(self, *filters, **kw_filters):
1312
        model = self.model
1313
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1314

    
1315
    def user_visible_by_chain(self, flt):
1316
        model = self.model
1317
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1318
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1319
        by_chain = dict(pending.annotate(models.Max('id')))
1320
        by_chain.update(approved.annotate(models.Max('id')))
1321
        return self.filter(flt, id__in=by_chain.values())
1322

    
1323
    def user_accessible_projects(self, user):
1324
        """
1325
        Return projects accessed by specified user.
1326
        """
1327
        if user.is_project_admin():
1328
            participates_filters = Q()
1329
        else:
1330
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1331
                                   Q(project__projectmembership__person=user)
1332

    
1333
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1334

    
1335
    def search_by_name(self, *search_strings):
1336
        q = Q()
1337
        for s in search_strings:
1338
            q = q | Q(name__icontains=s)
1339
        return self.filter(q)
1340

    
1341
    def latest_of_chain(self, chain_id):
1342
        try:
1343
            return self.filter(chain=chain_id).order_by('-id')[0]
1344
        except IndexError:
1345
            return None
1346

    
1347

    
1348
class ProjectApplication(models.Model):
1349
    applicant               =   models.ForeignKey(
1350
                                    AstakosUser,
1351
                                    related_name='projects_applied',
1352
                                    db_index=True)
1353

    
1354
    PENDING     =    0
1355
    APPROVED    =    1
1356
    REPLACED    =    2
1357
    DENIED      =    3
1358
    DISMISSED   =    4
1359
    CANCELLED   =    5
1360

    
1361
    state                   =   models.IntegerField(default=PENDING,
1362
                                                    db_index=True)
1363

    
1364
    owner                   =   models.ForeignKey(
1365
                                    AstakosUser,
1366
                                    related_name='projects_owned',
1367
                                    db_index=True)
1368

    
1369
    chain                   =   models.ForeignKey(Chain,
1370
                                                  related_name='chained_apps',
1371
                                                  db_column='chain')
1372
    precursor_application   =   models.ForeignKey('ProjectApplication',
1373
                                                  null=True,
1374
                                                  blank=True)
1375

    
1376
    name                    =   models.CharField(max_length=80)
1377
    homepage                =   models.URLField(max_length=255, null=True,
1378
                                                verify_exists=False)
1379
    description             =   models.TextField(null=True, blank=True)
1380
    start_date              =   models.DateTimeField(null=True, blank=True)
1381
    end_date                =   models.DateTimeField()
1382
    member_join_policy      =   models.IntegerField()
1383
    member_leave_policy     =   models.IntegerField()
1384
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1385
    resource_grants         =   models.ManyToManyField(
1386
                                    Resource,
1387
                                    null=True,
1388
                                    blank=True,
1389
                                    through='ProjectResourceGrant')
1390
    comments                =   models.TextField(null=True, blank=True)
1391
    issue_date              =   models.DateTimeField(auto_now_add=True)
1392
    response_date           =   models.DateTimeField(null=True, blank=True)
1393
    response                =   models.TextField(null=True, blank=True)
1394

    
1395
    objects                 =   ProjectApplicationManager()
1396

    
1397
    # Compiled queries
1398
    Q_PENDING  = Q(state=PENDING)
1399
    Q_APPROVED = Q(state=APPROVED)
1400
    Q_DENIED   = Q(state=DENIED)
1401

    
1402
    class Meta:
1403
        unique_together = ("chain", "id")
1404

    
1405
    def __unicode__(self):
1406
        return "%s applied by %s" % (self.name, self.applicant)
1407

    
1408
    # TODO: Move to a more suitable place
1409
    APPLICATION_STATE_DISPLAY = {
1410
        PENDING  : _('Pending review'),
1411
        APPROVED : _('Approved'),
1412
        REPLACED : _('Replaced'),
1413
        DENIED   : _('Denied'),
1414
        DISMISSED: _('Dismissed'),
1415
        CANCELLED: _('Cancelled')
1416
    }
1417

    
1418
    @property
1419
    def log_display(self):
1420
        return "application %s (%s) for project %s" % (
1421
            self.id, self.name, self.chain)
1422

    
1423
    def get_project(self):
1424
        try:
1425
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1426
            return Project
1427
        except Project.DoesNotExist, e:
1428
            return None
1429

    
1430
    def state_display(self):
1431
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1432

    
1433
    def project_state_display(self):
1434
        try:
1435
            project = self.project
1436
            return project.state_display()
1437
        except Project.DoesNotExist:
1438
            return self.state_display()
1439

    
1440
    def add_resource_policy(self, service, resource, uplimit):
1441
        """Raises ObjectDoesNotExist, IntegrityError"""
1442
        q = self.projectresourcegrant_set
1443
        resource = Resource.objects.get(name=resource)
1444
        q.create(resource=resource, member_capacity=uplimit)
1445

    
1446
    def members_count(self):
1447
        return self.project.approved_memberships.count()
1448

    
1449
    @property
1450
    def grants(self):
1451
        return self.projectresourcegrant_set.values('member_capacity',
1452
                                                    'resource__name')
1453

    
1454
    @property
1455
    def resource_policies(self):
1456
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1457

    
1458
    @resource_policies.setter
1459
    def resource_policies(self, policies):
1460
        for p in policies:
1461
            service = p.get('service', None)
1462
            resource = p.get('resource', None)
1463
            uplimit = p.get('uplimit', 0)
1464
            self.add_resource_policy(service, resource, uplimit)
1465

    
1466
    def pending_modifications_incl_me(self):
1467
        q = self.chained_applications()
1468
        q = q.filter(Q(state=self.PENDING))
1469
        return q
1470

    
1471
    def last_pending_incl_me(self):
1472
        try:
1473
            return self.pending_modifications_incl_me().order_by('-id')[0]
1474
        except IndexError:
1475
            return None
1476

    
1477
    def pending_modifications(self):
1478
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1479

    
1480
    def last_pending(self):
1481
        try:
1482
            return self.pending_modifications().order_by('-id')[0]
1483
        except IndexError:
1484
            return None
1485

    
1486
    def is_modification(self):
1487
        # if self.state != self.PENDING:
1488
        #     return False
1489
        parents = self.chained_applications().filter(id__lt=self.id)
1490
        parents = parents.filter(state__in=[self.APPROVED])
1491
        return parents.count() > 0
1492

    
1493
    def chained_applications(self):
1494
        return ProjectApplication.objects.filter(chain=self.chain)
1495

    
1496
    def is_latest(self):
1497
        return self.chained_applications().order_by('-id')[0] == self
1498

    
1499
    def has_pending_modifications(self):
1500
        return bool(self.last_pending())
1501

    
1502
    def denied_modifications(self):
1503
        q = self.chained_applications()
1504
        q = q.filter(Q(state=self.DENIED))
1505
        q = q.filter(~Q(id=self.id))
1506
        return q
1507

    
1508
    def last_denied(self):
1509
        try:
1510
            return self.denied_modifications().order_by('-id')[0]
1511
        except IndexError:
1512
            return None
1513

    
1514
    def has_denied_modifications(self):
1515
        return bool(self.last_denied())
1516

    
1517
    def is_applied(self):
1518
        try:
1519
            self.project
1520
            return True
1521
        except Project.DoesNotExist:
1522
            return False
1523

    
1524
    def get_project(self):
1525
        try:
1526
            return Project.objects.get(id=self.chain)
1527
        except Project.DoesNotExist:
1528
            return None
1529

    
1530
    def project_exists(self):
1531
        return self.get_project() is not None
1532

    
1533
    def _get_project_for_update(self):
1534
        try:
1535
            objects = Project.objects
1536
            project = objects.get_for_update(id=self.chain)
1537
            return project
1538
        except Project.DoesNotExist:
1539
            return None
1540

    
1541
    def can_cancel(self):
1542
        return self.state == self.PENDING
1543

    
1544
    def cancel(self):
1545
        if not self.can_cancel():
1546
            m = _("cannot cancel: application '%s' in state '%s'") % (
1547
                    self.id, self.state)
1548
            raise AssertionError(m)
1549

    
1550
        self.state = self.CANCELLED
1551
        self.save()
1552

    
1553
    def can_dismiss(self):
1554
        return self.state == self.DENIED
1555

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

    
1562
        self.state = self.DISMISSED
1563
        self.save()
1564

    
1565
    def can_deny(self):
1566
        return self.state == self.PENDING
1567

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

    
1574
        self.state = self.DENIED
1575
        self.response_date = datetime.now()
1576
        self.response = reason
1577
        self.save()
1578

    
1579
    def can_approve(self):
1580
        return self.state == self.PENDING
1581

    
1582
    def approve(self, approval_user=None):
1583
        """
1584
        If approval_user then during owner membership acceptance
1585
        it is checked whether the request_user is eligible.
1586

1587
        Raises:
1588
            PermissionDenied
1589
        """
1590

    
1591
        if not transaction.is_managed():
1592
            raise AssertionError("NOPE")
1593

    
1594
        new_project_name = self.name
1595
        if not self.can_approve():
1596
            m = _("cannot approve: project '%s' in state '%s'") % (
1597
                    new_project_name, self.state)
1598
            raise AssertionError(m) # invalid argument
1599

    
1600
        now = datetime.now()
1601
        project = self._get_project_for_update()
1602

    
1603
        try:
1604
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1605
            conflicting_project = Project.objects.get(q)
1606
            if (conflicting_project != project):
1607
                m = (_("cannot approve: project with name '%s' "
1608
                       "already exists (id: %s)") % (
1609
                        new_project_name, conflicting_project.id))
1610
                raise PermissionDenied(m) # invalid argument
1611
        except Project.DoesNotExist:
1612
            pass
1613

    
1614
        new_project = False
1615
        if project is None:
1616
            new_project = True
1617
            project = Project(id=self.chain)
1618

    
1619
        project.name = new_project_name
1620
        project.application = self
1621
        project.last_approval_date = now
1622
        if not new_project:
1623
            project.is_modified = True
1624

    
1625
        project.save()
1626

    
1627
        self.state = self.APPROVED
1628
        self.response_date = now
1629
        self.save()
1630
        return project
1631

    
1632
    @property
1633
    def member_join_policy_display(self):
1634
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1635

    
1636
    @property
1637
    def member_leave_policy_display(self):
1638
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1639

    
1640
class ProjectResourceGrant(models.Model):
1641

    
1642
    resource                =   models.ForeignKey(Resource)
1643
    project_application     =   models.ForeignKey(ProjectApplication,
1644
                                                  null=True)
1645
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1646
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1647
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1648
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1649
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1650
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1651

    
1652
    objects = ExtendedManager()
1653

    
1654
    class Meta:
1655
        unique_together = ("resource", "project_application")
1656

    
1657
    def display_member_capacity(self):
1658
        if self.member_capacity:
1659
            if self.resource.unit:
1660
                return ProjectResourceGrant.display_filesize(
1661
                    self.member_capacity)
1662
            else:
1663
                if math.isinf(self.member_capacity):
1664
                    return 'Unlimited'
1665
                else:
1666
                    return self.member_capacity
1667
        else:
1668
            return 'Unlimited'
1669

    
1670
    def __str__(self):
1671
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1672
                                        self.display_member_capacity())
1673

    
1674
    @classmethod
1675
    def display_filesize(cls, value):
1676
        try:
1677
            value = float(value)
1678
        except:
1679
            return
1680
        else:
1681
            if math.isinf(value):
1682
                return 'Unlimited'
1683
            if value > 1:
1684
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1685
                                [0, 0, 0, 0, 0, 0])
1686
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1687
                quotient = float(value) / 1024**exponent
1688
                unit, value_decimals = unit_list[exponent]
1689
                format_string = '{0:.%sf} {1}' % (value_decimals)
1690
                return format_string.format(quotient, unit)
1691
            if value == 0:
1692
                return '0 bytes'
1693
            if value == 1:
1694
                return '1 byte'
1695
            else:
1696
               return '0'
1697

    
1698

    
1699
class ProjectManager(ForUpdateManager):
1700

    
1701
    def terminated_projects(self):
1702
        q = self.model.Q_TERMINATED
1703
        return self.filter(q)
1704

    
1705
    def not_terminated_projects(self):
1706
        q = ~self.model.Q_TERMINATED
1707
        return self.filter(q)
1708

    
1709
    def deactivated_projects(self):
1710
        q = self.model.Q_DEACTIVATED
1711
        return self.filter(q)
1712

    
1713
    def modified_projects(self):
1714
        return self.filter(is_modified=True)
1715

    
1716
    def expired_projects(self):
1717
        q = (~Q(state=Project.TERMINATED) &
1718
              Q(application__end_date__lt=datetime.now()))
1719
        return self.filter(q)
1720

    
1721
    def search_by_name(self, *search_strings):
1722
        q = Q()
1723
        for s in search_strings:
1724
            q = q | Q(name__icontains=s)
1725
        return self.filter(q)
1726

    
1727

    
1728
class Project(models.Model):
1729

    
1730
    id                          =   models.OneToOneField(Chain,
1731
                                                      related_name='chained_project',
1732
                                                      db_column='id',
1733
                                                      primary_key=True)
1734

    
1735
    application                 =   models.OneToOneField(
1736
                                            ProjectApplication,
1737
                                            related_name='project')
1738
    last_approval_date          =   models.DateTimeField(null=True)
1739

    
1740
    members                     =   models.ManyToManyField(
1741
                                            AstakosUser,
1742
                                            through='ProjectMembership')
1743

    
1744
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1745
    deactivation_date           =   models.DateTimeField(null=True)
1746

    
1747
    creation_date               =   models.DateTimeField(auto_now_add=True)
1748
    name                        =   models.CharField(
1749
                                            max_length=80,
1750
                                            null=True,
1751
                                            db_index=True,
1752
                                            unique=True)
1753

    
1754
    APPROVED    = 1
1755
    SUSPENDED   = 10
1756
    TERMINATED  = 100
1757

    
1758
    is_modified                 =   models.BooleanField(default=False,
1759
                                                        db_index=True)
1760
    is_active                   =   models.BooleanField(default=True,
1761
                                                        db_index=True)
1762
    state                       =   models.IntegerField(default=APPROVED,
1763
                                                        db_index=True)
1764

    
1765
    objects     =   ProjectManager()
1766

    
1767
    # Compiled queries
1768
    Q_TERMINATED  = Q(state=TERMINATED)
1769
    Q_SUSPENDED   = Q(state=SUSPENDED)
1770
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1771

    
1772
    def __str__(self):
1773
        return uenc(_("<project %s '%s'>") %
1774
                    (self.id, udec(self.application.name)))
1775

    
1776
    __repr__ = __str__
1777

    
1778
    def __unicode__(self):
1779
        return _("<project %s '%s'>") % (self.id, self.application.name)
1780

    
1781
    STATE_DISPLAY = {
1782
        APPROVED   : 'Active',
1783
        SUSPENDED  : 'Suspended',
1784
        TERMINATED : 'Terminated'
1785
        }
1786

    
1787
    def state_display(self):
1788
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1789

    
1790
    def expiration_info(self):
1791
        return (str(self.id), self.name, self.state_display(),
1792
                str(self.application.end_date))
1793

    
1794
    def is_deactivated(self, reason=None):
1795
        if reason is not None:
1796
            return self.state == reason
1797

    
1798
        return self.state != self.APPROVED
1799

    
1800
    ### Deactivation calls
1801

    
1802
    def terminate(self):
1803
        self.deactivation_reason = 'TERMINATED'
1804
        self.deactivation_date = datetime.now()
1805
        self.state = self.TERMINATED
1806
        self.name = None
1807
        self.save()
1808

    
1809
    def suspend(self):
1810
        self.deactivation_reason = 'SUSPENDED'
1811
        self.deactivation_date = datetime.now()
1812
        self.state = self.SUSPENDED
1813
        self.save()
1814

    
1815
    def resume(self):
1816
        self.deactivation_reason = None
1817
        self.deactivation_date = None
1818
        self.state = self.APPROVED
1819
        self.save()
1820

    
1821
    ### Logical checks
1822

    
1823
    def is_inconsistent(self):
1824
        now = datetime.now()
1825
        dates = [self.creation_date,
1826
                 self.last_approval_date,
1827
                 self.deactivation_date]
1828
        return any([date > now for date in dates])
1829

    
1830
    def is_active_strict(self):
1831
        return self.is_active and self.state == self.APPROVED
1832

    
1833
    def is_approved(self):
1834
        return self.state == self.APPROVED
1835

    
1836
    @property
1837
    def is_alive(self):
1838
        return not self.is_terminated
1839

    
1840
    @property
1841
    def is_terminated(self):
1842
        return self.is_deactivated(self.TERMINATED)
1843

    
1844
    @property
1845
    def is_suspended(self):
1846
        return self.is_deactivated(self.SUSPENDED)
1847

    
1848
    def violates_resource_grants(self):
1849
        return False
1850

    
1851
    def violates_members_limit(self, adding=0):
1852
        application = self.application
1853
        limit = application.limit_on_members_number
1854
        if limit is None:
1855
            return False
1856
        return (len(self.approved_members) + adding > limit)
1857

    
1858

    
1859
    ### Other
1860

    
1861
    def count_pending_memberships(self):
1862
        memb_set = self.projectmembership_set
1863
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1864
        return memb_count
1865

    
1866
    def members_count(self):
1867
        return self.approved_memberships.count()
1868

    
1869
    @property
1870
    def approved_memberships(self):
1871
        query = ProjectMembership.Q_ACCEPTED_STATES
1872
        return self.projectmembership_set.filter(query)
1873

    
1874
    @property
1875
    def approved_members(self):
1876
        return [m.person for m in self.approved_memberships]
1877

    
1878
    def add_member(self, user):
1879
        """
1880
        Raises:
1881
            django.exceptions.PermissionDenied
1882
            astakos.im.models.AstakosUser.DoesNotExist
1883
        """
1884
        if isinstance(user, (int, long)):
1885
            user = AstakosUser.objects.get(user=user)
1886

    
1887
        m, created = ProjectMembership.objects.get_or_create(
1888
            person=user, project=self
1889
        )
1890
        m.accept()
1891

    
1892
    def remove_member(self, user):
1893
        """
1894
        Raises:
1895
            django.exceptions.PermissionDenied
1896
            astakos.im.models.AstakosUser.DoesNotExist
1897
            astakos.im.models.ProjectMembership.DoesNotExist
1898
        """
1899
        if isinstance(user, (int, long)):
1900
            user = AstakosUser.objects.get(user=user)
1901

    
1902
        m = ProjectMembership.objects.get(person=user, project=self)
1903
        m.remove()
1904

    
1905

    
1906
CHAIN_STATE = {
1907
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1908
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1909
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1910
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1911
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1912

    
1913
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1914
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1915
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1916
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1917
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1918

    
1919
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1920
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1921
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1922
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1923
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1924

    
1925
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1926
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1927
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1928
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1929
    }
1930

    
1931

    
1932
class ProjectMembershipManager(ForUpdateManager):
1933

    
1934
    def any_accepted(self):
1935
        q = self.model.Q_ACTUALLY_ACCEPTED
1936
        return self.filter(q)
1937

    
1938
    def actually_accepted(self):
1939
        q = self.model.Q_ACTUALLY_ACCEPTED
1940
        return self.filter(q)
1941

    
1942
    def requested(self):
1943
        return self.filter(state=ProjectMembership.REQUESTED)
1944

    
1945
    def suspended(self):
1946
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1947

    
1948
class ProjectMembership(models.Model):
1949

    
1950
    person              =   models.ForeignKey(AstakosUser)
1951
    request_date        =   models.DateField(auto_now_add=True)
1952
    project             =   models.ForeignKey(Project)
1953

    
1954
    REQUESTED           =   0
1955
    ACCEPTED            =   1
1956
    LEAVE_REQUESTED     =   5
1957
    # User deactivation
1958
    USER_SUSPENDED      =   10
1959

    
1960
    REMOVED             =   200
1961

    
1962
    ASSOCIATED_STATES   =   set([REQUESTED,
1963
                                 ACCEPTED,
1964
                                 LEAVE_REQUESTED,
1965
                                 USER_SUSPENDED,
1966
                                 ])
1967

    
1968
    ACCEPTED_STATES     =   set([ACCEPTED,
1969
                                 LEAVE_REQUESTED,
1970
                                 USER_SUSPENDED,
1971
                                 ])
1972

    
1973
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1974

    
1975
    state               =   models.IntegerField(default=REQUESTED,
1976
                                                db_index=True)
1977
    is_pending          =   models.BooleanField(default=False, db_index=True)
1978
    is_active           =   models.BooleanField(default=False, db_index=True)
1979
    application         =   models.ForeignKey(
1980
                                ProjectApplication,
1981
                                null=True,
1982
                                related_name='memberships')
1983
    pending_application =   models.ForeignKey(
1984
                                ProjectApplication,
1985
                                null=True,
1986
                                related_name='pending_memberships')
1987
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1988

    
1989
    acceptance_date     =   models.DateField(null=True, db_index=True)
1990
    leave_request_date  =   models.DateField(null=True)
1991

    
1992
    objects     =   ProjectMembershipManager()
1993

    
1994
    # Compiled queries
1995
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1996
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1997

    
1998
    MEMBERSHIP_STATE_DISPLAY = {
1999
        REQUESTED           : _('Requested'),
2000
        ACCEPTED            : _('Accepted'),
2001
        LEAVE_REQUESTED     : _('Leave Requested'),
2002
        USER_SUSPENDED      : _('Suspended'),
2003
        REMOVED             : _('Pending removal'),
2004
        }
2005

    
2006
    USER_FRIENDLY_STATE_DISPLAY = {
2007
        REQUESTED           : _('Join requested'),
2008
        ACCEPTED            : _('Accepted member'),
2009
        LEAVE_REQUESTED     : _('Requested to leave'),
2010
        USER_SUSPENDED      : _('Suspended member'),
2011
        REMOVED             : _('Pending removal'),
2012
        }
2013

    
2014
    def state_display(self):
2015
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2016

    
2017
    def user_friendly_state_display(self):
2018
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2019

    
2020
    class Meta:
2021
        unique_together = ("person", "project")
2022
        #index_together = [["project", "state"]]
2023

    
2024
    def __str__(self):
2025
        return uenc(_("<'%s' membership in '%s'>") % (
2026
                self.person.username, self.project))
2027

    
2028
    __repr__ = __str__
2029

    
2030
    def __init__(self, *args, **kwargs):
2031
        self.state = self.REQUESTED
2032
        super(ProjectMembership, self).__init__(*args, **kwargs)
2033

    
2034
    def _set_history_item(self, reason, date=None):
2035
        if isinstance(reason, basestring):
2036
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2037

    
2038
        history_item = ProjectMembershipHistory(
2039
                            serial=self.id,
2040
                            person=self.person_id,
2041
                            project=self.project_id,
2042
                            date=date or datetime.now(),
2043
                            reason=reason)
2044
        history_item.save()
2045
        serial = history_item.id
2046

    
2047
    def can_accept(self):
2048
        return self.state == self.REQUESTED
2049

    
2050
    def accept(self):
2051
        if not self.can_accept():
2052
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2053
            raise AssertionError(m)
2054

    
2055
        now = datetime.now()
2056
        self.acceptance_date = now
2057
        self._set_history_item(reason='ACCEPT', date=now)
2058
        self.state = self.ACCEPTED
2059
        self.save()
2060

    
2061
    def can_leave(self):
2062
        return self.state in self.ACCEPTED_STATES
2063

    
2064
    def leave_request(self):
2065
        if not self.can_leave():
2066
            m = _("%s: attempt to request to leave in state '%s'") % (
2067
                self, self.state)
2068
            raise AssertionError(m)
2069

    
2070
        self.leave_request_date = datetime.now()
2071
        self.state = self.LEAVE_REQUESTED
2072
        self.save()
2073

    
2074
    def can_deny_leave(self):
2075
        return self.state == self.LEAVE_REQUESTED
2076

    
2077
    def leave_request_deny(self):
2078
        if not self.can_deny_leave():
2079
            m = _("%s: attempt to deny leave request in state '%s'") % (
2080
                self, self.state)
2081
            raise AssertionError(m)
2082

    
2083
        self.leave_request_date = None
2084
        self.state = self.ACCEPTED
2085
        self.save()
2086

    
2087
    def can_cancel_leave(self):
2088
        return self.state == self.LEAVE_REQUESTED
2089

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

    
2096
        self.leave_request_date = None
2097
        self.state = self.ACCEPTED
2098
        self.save()
2099

    
2100
    def can_remove(self):
2101
        return self.state in self.ACCEPTED_STATES
2102

    
2103
    def remove(self):
2104
        if not self.can_remove():
2105
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2106
            raise AssertionError(m)
2107

    
2108
        self._set_history_item(reason='REMOVE')
2109
        self.delete()
2110

    
2111
    def can_reject(self):
2112
        return self.state == self.REQUESTED
2113

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

    
2119
        # rejected requests don't need sync,
2120
        # because they were never effected
2121
        self._set_history_item(reason='REJECT')
2122
        self.delete()
2123

    
2124
    def can_cancel(self):
2125
        return self.state == self.REQUESTED
2126

    
2127
    def cancel(self):
2128
        if not self.can_cancel():
2129
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2130
            raise AssertionError(m)
2131

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

    
2137

    
2138
class Serial(models.Model):
2139
    serial  =   models.AutoField(primary_key=True)
2140

    
2141

    
2142
class ProjectMembershipHistory(models.Model):
2143
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2144
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2145

    
2146
    person  =   models.BigIntegerField()
2147
    project =   models.BigIntegerField()
2148
    date    =   models.DateField(auto_now_add=True)
2149
    reason  =   models.IntegerField()
2150
    serial  =   models.BigIntegerField()
2151

    
2152
### SIGNALS ###
2153
################
2154

    
2155
def create_astakos_user(u):
2156
    try:
2157
        AstakosUser.objects.get(user_ptr=u.pk)
2158
    except AstakosUser.DoesNotExist:
2159
        extended_user = AstakosUser(user_ptr_id=u.pk)
2160
        extended_user.__dict__.update(u.__dict__)
2161
        extended_user.save()
2162
        if not extended_user.has_auth_provider('local'):
2163
            extended_user.add_auth_provider('local')
2164
    except BaseException, e:
2165
        logger.exception(e)
2166

    
2167
def fix_superusers():
2168
    # Associate superusers with AstakosUser
2169
    admins = User.objects.filter(is_superuser=True)
2170
    for u in admins:
2171
        create_astakos_user(u)
2172

    
2173
def user_post_save(sender, instance, created, **kwargs):
2174
    if not created:
2175
        return
2176
    create_astakos_user(instance)
2177
post_save.connect(user_post_save, sender=User)
2178

    
2179
def astakosuser_post_save(sender, instance, created, **kwargs):
2180
    pass
2181

    
2182
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2183

    
2184
def resource_post_save(sender, instance, created, **kwargs):
2185
    pass
2186

    
2187
post_save.connect(resource_post_save, sender=Resource)
2188

    
2189
def renew_token(sender, instance, **kwargs):
2190
    if not instance.auth_token:
2191
        instance.renew_token()
2192
pre_save.connect(renew_token, sender=AstakosUser)
2193
pre_save.connect(renew_token, sender=Service)