Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 143d8a9d

History | View | Annotate | Download (74 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
_presentation_data = {}
146
def get_presentation(resource):
147
    global _presentation_data
148
    presentation = _presentation_data.get(resource, {})
149
    if not presentation:
150
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
151
        presentation = resource_presentation.get(resource, {})
152
        _presentation_data[resource] = presentation
153
    return presentation
154

    
155
class Resource(models.Model):
156
    name = models.CharField(_('Name'), max_length=255, unique=True)
157
    service = models.ForeignKey(Service)
158
    desc = models.TextField(_('Description'), null=True)
159
    unit = models.CharField(_('Name'), null=True, max_length=255)
160
    group = models.CharField(_('Group'), null=True, max_length=255)
161
    uplimit = intDecimalField(default=0)
162

    
163
    objects = ForUpdateManager()
164

    
165
    def __str__(self):
166
        return self.name
167

    
168
    def full_name(self):
169
        return str(self)
170

    
171
    def get_info(self):
172
        return {'service': str(self.service),
173
                'description': self.desc,
174
                'unit': self.unit,
175
                }
176

    
177
    @property
178
    def help_text(self):
179
        return get_presentation(str(self)).get('help_text', '')
180

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

    
185
    @property
186
    def is_abbreviation(self):
187
        return get_presentation(str(self)).get('is_abbreviation', False)
188

    
189
    @property
190
    def report_desc(self):
191
        return get_presentation(str(self)).get('report_desc', '')
192

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

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

    
201
    @property
202
    def display_name(self):
203
        name = self.verbose_name
204
        if self.is_abbreviation:
205
            name = name.upper()
206
        return name
207

    
208
    @property
209
    def pluralized_display_name(self):
210
        if not self.unit:
211
            return '%ss' % self.display_name
212
        return self.display_name
213

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

    
220

    
221
class AstakosUserManager(UserManager):
222

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

    
232
    def get_by_email(self, email):
233
        return self.get(email=email)
234

    
235
    def get_by_identifier(self, email_or_username, **kwargs):
236
        try:
237
            return self.get(email__iexact=email_or_username, **kwargs)
238
        except AstakosUser.DoesNotExist:
239
            return self.get(username__iexact=email_or_username, **kwargs)
240

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

    
247
    def verified_user_exists(self, email_or_username):
248
        return self.user_exists(email_or_username, email_verified=True)
249

    
250
    def verified(self):
251
        return self.filter(email_verified=True)
252

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

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

    
275

    
276

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

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

    
293

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

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

    
314
    updated = models.DateTimeField(_('Update date'))
315
    is_verified = models.BooleanField(_('Is verified?'), default=False)
316

    
317
    email_verified = models.BooleanField(_('Email verified?'), default=False)
318

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

    
325
    activation_sent = models.DateTimeField(
326
        _('Activation sent data'), null=True, blank=True)
327

    
328
    policy = models.ManyToManyField(
329
        Resource, null=True, through='AstakosUserQuota')
330

    
331
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
332

    
333
    __has_signed_terms = False
334
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
335
                                           default=False, db_index=True)
336

    
337
    objects = AstakosUserManager()
338

    
339
    forupdate = ForUpdateManager()
340

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

    
347
    @property
348
    def realname(self):
349
        return '%s %s' % (self.first_name, self.last_name)
350

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

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

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

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

    
384
    def is_project_admin(self, application_id=None):
385
        return self.uuid in PROJECT_ADMINS
386

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

    
394
    @property
395
    def policies(self):
396
        return self.astakosuserquota_set.select_related().all()
397

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

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

    
424
    def get_resource_policy(self, resource):
425
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
426
        resource = Resource.objects.get(service__name=s, name=r)
427
        default_capacity = resource.uplimit
428
        try:
429
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
430
            return policy, default_capacity
431
        except AstakosUserQuota.DoesNotExist:
432
            return None, default_capacity
433

    
434
    def remove_resource_policy(self, service, resource):
435
        """Raises ObjectDoesNotExist, IntegrityError"""
436
        resource = Resource.objects.get(service__name=service, name=resource)
437
        q = self.policies.get(resource=resource).delete()
438

    
439
    def update_uuid(self):
440
        while not self.uuid:
441
            uuid_val =  str(uuid.uuid4())
442
            try:
443
                AstakosUser.objects.get(uuid=uuid_val)
444
            except AstakosUser.DoesNotExist, e:
445
                self.uuid = uuid_val
446
        return self.uuid
447

    
448
    def save(self, update_timestamps=True, **kwargs):
449
        if update_timestamps:
450
            if not self.id:
451
                self.date_joined = datetime.now()
452
            self.updated = datetime.now()
453

    
454
        # update date_signed_terms if necessary
455
        if self.__has_signed_terms != self.has_signed_terms:
456
            self.date_signed_terms = datetime.now()
457

    
458
        self.update_uuid()
459

    
460
        if self.username != self.email.lower():
461
            # set username
462
            self.username = self.email.lower()
463

    
464
        super(AstakosUser, self).save(**kwargs)
465

    
466
    def renew_token(self, flush_sessions=False, current_key=None):
467
        md5 = hashlib.md5()
468
        md5.update(settings.SECRET_KEY)
469
        md5.update(self.username)
470
        md5.update(self.realname.encode('ascii', 'ignore'))
471
        md5.update(asctime())
472

    
473
        self.auth_token = b64encode(md5.digest())
474
        self.auth_token_created = datetime.now()
475
        self.auth_token_expires = self.auth_token_created + \
476
                                  timedelta(hours=AUTH_TOKEN_DURATION)
477
        if flush_sessions:
478
            self.flush_sessions(current_key)
479
        msg = 'Token renewed for %s' % self.email
480
        logger.log(LOGGING_LEVEL, msg)
481

    
482
    def flush_sessions(self, current_key=None):
483
        q = self.sessions
484
        if current_key:
485
            q = q.exclude(session_key=current_key)
486

    
487
        keys = q.values_list('session_key', flat=True)
488
        if keys:
489
            msg = 'Flushing sessions: %s' % ','.join(keys)
490
            logger.log(LOGGING_LEVEL, msg, [])
491
        engine = import_module(settings.SESSION_ENGINE)
492
        for k in keys:
493
            s = engine.SessionStore(k)
494
            s.flush()
495

    
496
    def __unicode__(self):
497
        return '%s (%s)' % (self.realname, self.email)
498

    
499
    def conflicting_email(self):
500
        q = AstakosUser.objects.exclude(username=self.username)
501
        q = q.filter(email__iexact=self.email)
502
        if q.count() != 0:
503
            return True
504
        return False
505

    
506
    def email_change_is_pending(self):
507
        return self.emailchanges.count() > 0
508

    
509
    @property
510
    def signed_terms(self):
511
        term = get_latest_terms()
512
        if not term:
513
            return True
514
        if not self.has_signed_terms:
515
            return False
516
        if not self.date_signed_terms:
517
            return False
518
        if self.date_signed_terms < term.date:
519
            self.has_signed_terms = False
520
            self.date_signed_terms = None
521
            self.save()
522
            return False
523
        return True
524

    
525
    def set_invitations_level(self):
526
        """
527
        Update user invitation level
528
        """
529
        level = self.invitation.inviter.level + 1
530
        self.level = level
531
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
532

    
533
    def can_change_password(self):
534
        return self.has_auth_provider('local', auth_backend='astakos')
535

    
536
    def can_change_email(self):
537
        if not self.has_auth_provider('local'):
538
            return True
539

    
540
        local = self.get_auth_provider('local')._instance
541
        return local.auth_backend == 'astakos'
542

    
543
    # Auth providers related methods
544
    def get_auth_provider(self, module=None, identifier=None, **filters):
545
        if not module:
546
            return self.auth_providers.active()[0].settings
547

    
548
        params = {'module': module}
549
        if identifier:
550
            params['identifier'] = identifier
551
        params.update(filters)
552
        return self.auth_providers.active().get(**params).settings
553

    
554
    def has_auth_provider(self, provider, **kwargs):
555
        return bool(self.auth_providers.active().filter(module=provider,
556
                                                        **kwargs).count())
557

    
558
    def get_required_providers(self, **kwargs):
559
        return auth.REQUIRED_PROVIDERS.keys()
560

    
561
    def missing_required_providers(self):
562
        required = self.get_required_providers()
563
        missing = []
564
        for provider in required:
565
            if not self.has_auth_provider(provider):
566
                missing.append(auth.get_provider(provider, self))
567
        return missing
568

    
569
    def get_available_auth_providers(self, **filters):
570
        """
571
        Returns a list of providers available for add by the user.
572
        """
573
        modules = astakos_settings.IM_MODULES
574
        providers = []
575
        for p in modules:
576
            providers.append(auth.get_provider(p, self))
577
        available = []
578

    
579
        for p in providers:
580
            if p.get_add_policy:
581
                available.append(p)
582
        return available
583

    
584
    def get_disabled_auth_providers(self, **filters):
585
        providers = self.get_auth_providers(**filters)
586
        disabled = []
587
        for p in providers:
588
            if not p.get_login_policy:
589
                disabled.append(p)
590
        return disabled
591

    
592
    def get_enabled_auth_providers(self, **filters):
593
        providers = self.get_auth_providers(**filters)
594
        enabled = []
595
        for p in providers:
596
            if p.get_login_policy:
597
                enabled.append(p)
598
        return enabled
599

    
600
    def get_auth_providers(self, **filters):
601
        providers = []
602
        for provider in self.auth_providers.active(**filters):
603
            if provider.settings.module_enabled:
604
                providers.append(provider.settings)
605

    
606
        modules = astakos_settings.IM_MODULES
607

    
608
        def key(p):
609
            if not p.module in modules:
610
                return 100
611
            return modules.index(p.module)
612

    
613
        providers = sorted(providers, key=key)
614
        return providers
615

    
616
    # URL methods
617
    @property
618
    def auth_providers_display(self):
619
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
620
                         self.get_enabled_auth_providers()])
621

    
622
    def add_auth_provider(self, module='local', identifier=None, **params):
623
        provider = auth.get_provider(module, self, identifier, **params)
624
        provider.add_to_user()
625

    
626
    def get_resend_activation_url(self):
627
        return reverse('send_activation', kwargs={'user_id': self.pk})
628

    
629
    def get_activation_url(self, nxt=False):
630
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
631
                                 quote(self.auth_token))
632
        if nxt:
633
            url += "&next=%s" % quote(nxt)
634
        return url
635

    
636
    def get_password_reset_url(self, token_generator=default_token_generator):
637
        return reverse('django.contrib.auth.views.password_reset_confirm',
638
                          kwargs={'uidb36':int_to_base36(self.id),
639
                                  'token':token_generator.make_token(self)})
640

    
641
    def get_inactive_message(self, provider_module, identifier=None):
642
        provider = self.get_auth_provider(provider_module, identifier)
643

    
644
        msg_extra = ''
645
        message = ''
646

    
647
        msg_inactive = provider.get_account_inactive_msg
648
        msg_pending = provider.get_pending_activation_msg
649
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
650
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
651
        msg_pending_mod = provider.get_pending_moderation_msg
652
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
653

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

    
672
        return mark_safe(message + u' '+ msg_extra)
673

    
674
    def owns_application(self, application):
675
        return application.owner == self
676

    
677
    def owns_project(self, project):
678
        return project.application.owner == self
679

    
680
    def is_associated(self, project):
681
        try:
682
            m = ProjectMembership.objects.get(person=self, project=project)
683
            return m.state in ProjectMembership.ASSOCIATED_STATES
684
        except ProjectMembership.DoesNotExist:
685
            return False
686

    
687
    def get_membership(self, project):
688
        try:
689
            return ProjectMembership.objects.get(
690
                project=project,
691
                person=self)
692
        except ProjectMembership.DoesNotExist:
693
            return None
694

    
695
    def membership_display(self, project):
696
        m = self.get_membership(project)
697
        if m is None:
698
            return _('Not a member')
699
        else:
700
            return m.user_friendly_state_display()
701

    
702
    def non_owner_can_view(self, maybe_project):
703
        if self.is_project_admin():
704
            return True
705
        if maybe_project is None:
706
            return False
707
        project = maybe_project
708
        if self.is_associated(project):
709
            return True
710
        if project.is_deactivated():
711
            return False
712
        return True
713

    
714
    def settings(self):
715
        return UserSetting.objects.filter(user=self)
716

    
717

    
718
class AstakosUserAuthProviderManager(models.Manager):
719

    
720
    def active(self, **filters):
721
        return self.filter(active=True, **filters)
722

    
723
    def remove_unverified_providers(self, provider, **filters):
724
        try:
725
            existing = self.filter(module=provider, user__email_verified=False,
726
                                   **filters)
727
            for p in existing:
728
                p.user.delete()
729
        except:
730
            pass
731

    
732
    def unverified(self, provider, **filters):
733
        try:
734
            return self.get(module=provider, user__email_verified=False,
735
                            **filters).settings
736
        except AstakosUserAuthProvider.DoesNotExist:
737
            return None
738

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

    
746

    
747
class AuthProviderPolicyProfileManager(models.Manager):
748

    
749
    def active(self):
750
        return self.filter(active=True)
751

    
752
    def for_user(self, user, provider):
753
        policies = {}
754
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
755
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
756
        exclusive_q = exclusive_q1 | exclusive_q2
757

    
758
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
759
            policies.update(profile.policies)
760

    
761
        user_groups = user.groups.all().values('pk')
762
        for profile in self.active().filter(groups__in=user_groups).filter(
763
                exclusive_q):
764
            policies.update(profile.policies)
765
        return policies
766

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

    
782

    
783
class AuthProviderPolicyProfile(models.Model):
784
    name = models.CharField(_('Name'), max_length=255, blank=False,
785
                            null=False, db_index=True)
786
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
787
                                null=False)
788

    
789
    # apply policies to all providers excluding the one set in provider field
790
    is_exclusive = models.BooleanField(default=False)
791

    
792
    policy_add = models.NullBooleanField(null=True, default=None)
793
    policy_remove = models.NullBooleanField(null=True, default=None)
794
    policy_create = models.NullBooleanField(null=True, default=None)
795
    policy_login = models.NullBooleanField(null=True, default=None)
796
    policy_limit = models.IntegerField(null=True, default=None)
797
    policy_required = models.NullBooleanField(null=True, default=None)
798
    policy_automoderate = models.NullBooleanField(null=True, default=None)
799
    policy_switch = models.NullBooleanField(null=True, default=None)
800

    
801
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
802
                     'automoderate')
803

    
804
    priority = models.IntegerField(null=False, default=1)
805
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
806
    users = models.ManyToManyField(AstakosUser,
807
                                   related_name='authpolicy_profiles')
808
    active = models.BooleanField(default=True)
809

    
810
    objects = AuthProviderPolicyProfileManager()
811

    
812
    class Meta:
813
        ordering = ['priority']
814

    
815
    @property
816
    def policies(self):
817
        policies = {}
818
        for pkey in self.POLICY_FIELDS:
819
            value = getattr(self, 'policy_%s' % pkey, None)
820
            if value is None:
821
                continue
822
            policies[pkey] = value
823
        return policies
824

    
825
    def set_policies(self, policies_dict):
826
        for key, value in policies_dict.iteritems():
827
            if key in self.POLICY_FIELDS:
828
                setattr(self, 'policy_%s' % key, value)
829
        return self.policies
830

    
831

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

    
850
    objects = AstakosUserAuthProviderManager()
851

    
852
    class Meta:
853
        unique_together = (('identifier', 'module', 'user'), )
854
        ordering = ('module', 'created')
855

    
856
    def __init__(self, *args, **kwargs):
857
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
858
        try:
859
            self.info = json.loads(self.info_data)
860
            if not self.info:
861
                self.info = {}
862
        except Exception, e:
863
            self.info = {}
864

    
865
        for key,value in self.info.iteritems():
866
            setattr(self, 'info_%s' % key, value)
867

    
868
    @property
869
    def settings(self):
870
        extra_data = {}
871

    
872
        info_data = {}
873
        if self.info_data:
874
            info_data = json.loads(self.info_data)
875

    
876
        extra_data['info'] = info_data
877

    
878
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
879
            extra_data[key] = getattr(self, key)
880

    
881
        extra_data['instance'] = self
882
        return auth.get_provider(self.module, self.user,
883
                                           self.identifier, **extra_data)
884

    
885
    def __repr__(self):
886
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
887

    
888
    def __unicode__(self):
889
        if self.identifier:
890
            return "%s:%s" % (self.module, self.identifier)
891
        if self.auth_backend:
892
            return "%s:%s" % (self.module, self.auth_backend)
893
        return self.module
894

    
895
    def save(self, *args, **kwargs):
896
        self.info_data = json.dumps(self.info)
897
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
898

    
899

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

    
927
    update_or_create = _update_or_create
928

    
929

    
930
class AstakosUserQuota(models.Model):
931
    objects = ExtendedManager()
932
    capacity = intDecimalField()
933
    quantity = intDecimalField(default=0)
934
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
935
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
936
    resource = models.ForeignKey(Resource)
937
    user = models.ForeignKey(AstakosUser)
938

    
939
    class Meta:
940
        unique_together = ("resource", "user")
941

    
942

    
943
class ApprovalTerms(models.Model):
944
    """
945
    Model for approval terms
946
    """
947

    
948
    date = models.DateTimeField(
949
        _('Issue date'), db_index=True, auto_now_add=True)
950
    location = models.CharField(_('Terms location'), max_length=255)
951

    
952

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

    
966
    def __init__(self, *args, **kwargs):
967
        super(Invitation, self).__init__(*args, **kwargs)
968
        if not self.id:
969
            self.code = _generate_invitation_code()
970

    
971
    def consume(self):
972
        self.is_consumed = True
973
        self.consumed = datetime.now()
974
        self.save()
975

    
976
    def __unicode__(self):
977
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
978

    
979

    
980
class EmailChangeManager(models.Manager):
981

    
982
    @transaction.commit_on_success
983
    def change_email(self, activation_key):
984
        """
985
        Validate an activation key and change the corresponding
986
        ``User`` if valid.
987

988
        If the key is valid and has not expired, return the ``User``
989
        after activating.
990

991
        If the key is not valid or has expired, return ``None``.
992

993
        If the key is valid but the ``User`` is already active,
994
        return ``None``.
995

996
        After successful email change the activation record is deleted.
997

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

    
1027

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

    
1040
    objects = EmailChangeManager()
1041

    
1042
    def get_url(self):
1043
        return reverse('email_change_confirm',
1044
                      kwargs={'activation_key': self.activation_key})
1045

    
1046
    def activation_key_expired(self):
1047
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1048
        return self.requested_at + expiration_date < datetime.now()
1049

    
1050

    
1051
class AdditionalMail(models.Model):
1052
    """
1053
    Model for registring invitations
1054
    """
1055
    owner = models.ForeignKey(AstakosUser)
1056
    email = models.EmailField()
1057

    
1058

    
1059
def _generate_invitation_code():
1060
    while True:
1061
        code = randint(1, 2L ** 63 - 1)
1062
        try:
1063
            Invitation.objects.get(code=code)
1064
            # An invitation with this code already exists, try again
1065
        except Invitation.DoesNotExist:
1066
            return code
1067

    
1068

    
1069
def get_latest_terms():
1070
    try:
1071
        term = ApprovalTerms.objects.order_by('-id')[0]
1072
        return term
1073
    except IndexError:
1074
        pass
1075
    return None
1076

    
1077

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

    
1097
    class Meta:
1098
        unique_together = ("provider", "third_party_identifier")
1099

    
1100
    def get_user_instance(self):
1101
        d = self.__dict__
1102
        d.pop('_state', None)
1103
        d.pop('id', None)
1104
        d.pop('token', None)
1105
        d.pop('created', None)
1106
        d.pop('info', None)
1107
        user = AstakosUser(**d)
1108

    
1109
        return user
1110

    
1111
    @property
1112
    def realname(self):
1113
        return '%s %s' %(self.first_name, self.last_name)
1114

    
1115
    @realname.setter
1116
    def realname(self, value):
1117
        parts = value.split(' ')
1118
        if len(parts) == 2:
1119
            self.first_name = parts[0]
1120
            self.last_name = parts[1]
1121
        else:
1122
            self.last_name = parts[0]
1123

    
1124
    def save(self, **kwargs):
1125
        if not self.id:
1126
            # set username
1127
            while not self.username:
1128
                username =  uuid.uuid4().hex[:30]
1129
                try:
1130
                    AstakosUser.objects.get(username = username)
1131
                except AstakosUser.DoesNotExist, e:
1132
                    self.username = username
1133
        super(PendingThirdPartyUser, self).save(**kwargs)
1134

    
1135
    def generate_token(self):
1136
        self.password = self.third_party_identifier
1137
        self.last_login = datetime.now()
1138
        self.token = default_token_generator.make_token(self)
1139

    
1140
    def existing_user(self):
1141
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1142
                                         auth_providers__identifier=self.third_party_identifier)
1143

    
1144
    def get_provider(self, user):
1145
        params = {
1146
            'info_data': self.info,
1147
            'affiliation': self.affiliation
1148
        }
1149
        return auth.get_provider(self.provider, user,
1150
                                 self.third_party_identifier, **params)
1151

    
1152
class SessionCatalog(models.Model):
1153
    session_key = models.CharField(_('session key'), max_length=40)
1154
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1155

    
1156

    
1157
class UserSetting(models.Model):
1158
    user = models.ForeignKey(AstakosUser)
1159
    setting = models.CharField(max_length=255)
1160
    value = models.IntegerField()
1161

    
1162
    objects = ForUpdateManager()
1163

    
1164
    class Meta:
1165
        unique_together = ("user", "setting")
1166

    
1167

    
1168
### PROJECTS ###
1169
################
1170

    
1171
class ChainManager(ForUpdateManager):
1172

    
1173
    def search_by_name(self, *search_strings):
1174
        projects = Project.objects.search_by_name(*search_strings)
1175
        chains = [p.id for p in projects]
1176
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1177
        apps = (app for app in apps if app.is_latest())
1178
        app_chains = [app.chain for app in apps if app.chain not in chains]
1179
        return chains + app_chains
1180

    
1181
    def all_full_state(self):
1182
        chains = self.all()
1183
        cids = [c.chain for c in chains]
1184
        projects = Project.objects.select_related('application').in_bulk(cids)
1185

    
1186
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1187
        chain_latest = dict(objs.values_list('chain', 'latest'))
1188

    
1189
        objs = ProjectApplication.objects.select_related('applicant')
1190
        apps = objs.in_bulk(chain_latest.values())
1191

    
1192
        d = {}
1193
        for chain in chains:
1194
            pk = chain.pk
1195
            project = projects.get(pk, None)
1196
            app = apps[chain_latest[pk]]
1197
            d[chain.pk] = chain.get_state(project, app)
1198

    
1199
        return d
1200

    
1201
    def of_project(self, project):
1202
        if project is None:
1203
            return None
1204
        try:
1205
            return self.get(chain=project.id)
1206
        except Chain.DoesNotExist:
1207
            raise AssertionError('project with no chain')
1208

    
1209

    
1210
class Chain(models.Model):
1211
    chain  =   models.AutoField(primary_key=True)
1212

    
1213
    def __str__(self):
1214
        return "%s" % (self.chain,)
1215

    
1216
    objects = ChainManager()
1217

    
1218
    PENDING            = 0
1219
    DENIED             = 3
1220
    DISMISSED          = 4
1221
    CANCELLED          = 5
1222

    
1223
    APPROVED           = 10
1224
    APPROVED_PENDING   = 11
1225
    SUSPENDED          = 12
1226
    SUSPENDED_PENDING  = 13
1227
    TERMINATED         = 14
1228
    TERMINATED_PENDING = 15
1229

    
1230
    PENDING_STATES = [PENDING,
1231
                      APPROVED_PENDING,
1232
                      SUSPENDED_PENDING,
1233
                      TERMINATED_PENDING,
1234
                      ]
1235

    
1236
    MODIFICATION_STATES = [APPROVED_PENDING,
1237
                           SUSPENDED_PENDING,
1238
                           TERMINATED_PENDING,
1239
                           ]
1240

    
1241
    RELEVANT_STATES = [PENDING,
1242
                       DENIED,
1243
                       APPROVED,
1244
                       APPROVED_PENDING,
1245
                       SUSPENDED,
1246
                       SUSPENDED_PENDING,
1247
                       TERMINATED_PENDING,
1248
                       ]
1249

    
1250
    SKIP_STATES = [DISMISSED,
1251
                   CANCELLED,
1252
                   TERMINATED]
1253

    
1254
    STATE_DISPLAY = {
1255
        PENDING            : _("Pending"),
1256
        DENIED             : _("Denied"),
1257
        DISMISSED          : _("Dismissed"),
1258
        CANCELLED          : _("Cancelled"),
1259
        APPROVED           : _("Active"),
1260
        APPROVED_PENDING   : _("Active - Pending"),
1261
        SUSPENDED          : _("Suspended"),
1262
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1263
        TERMINATED         : _("Terminated"),
1264
        TERMINATED_PENDING : _("Terminated - Pending"),
1265
        }
1266

    
1267

    
1268
    @classmethod
1269
    def _chain_state(cls, project_state, app_state):
1270
        s = CHAIN_STATE.get((project_state, app_state), None)
1271
        if s is None:
1272
            raise AssertionError('inconsistent chain state')
1273
        return s
1274

    
1275
    @classmethod
1276
    def chain_state(cls, project, app):
1277
        p_state = project.state if project else None
1278
        return cls._chain_state(p_state, app.state)
1279

    
1280
    @classmethod
1281
    def state_display(cls, s):
1282
        if s is None:
1283
            return _("Unknown")
1284
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1285

    
1286
    def last_application(self):
1287
        return self.chained_apps.order_by('-id')[0]
1288

    
1289
    def get_project(self):
1290
        try:
1291
            return self.chained_project
1292
        except Project.DoesNotExist:
1293
            return None
1294

    
1295
    def get_elements(self):
1296
        project = self.get_project()
1297
        app = self.last_application()
1298
        return project, app
1299

    
1300
    def get_state(self, project, app):
1301
        s = self.chain_state(project, app)
1302
        return s, project, app
1303

    
1304
    def full_state(self):
1305
        project, app = self.get_elements()
1306
        return self.get_state(project, app)
1307

    
1308

    
1309
def new_chain():
1310
    c = Chain.objects.create()
1311
    return c
1312

    
1313

    
1314
class ProjectApplicationManager(ForUpdateManager):
1315

    
1316
    def user_visible_projects(self, *filters, **kw_filters):
1317
        model = self.model
1318
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1319

    
1320
    def user_visible_by_chain(self, flt):
1321
        model = self.model
1322
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1323
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1324
        by_chain = dict(pending.annotate(models.Max('id')))
1325
        by_chain.update(approved.annotate(models.Max('id')))
1326
        return self.filter(flt, id__in=by_chain.values())
1327

    
1328
    def user_accessible_projects(self, user):
1329
        """
1330
        Return projects accessed by specified user.
1331
        """
1332
        if user.is_project_admin():
1333
            participates_filters = Q()
1334
        else:
1335
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1336
                                   Q(project__projectmembership__person=user)
1337

    
1338
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1339

    
1340
    def search_by_name(self, *search_strings):
1341
        q = Q()
1342
        for s in search_strings:
1343
            q = q | Q(name__icontains=s)
1344
        return self.filter(q)
1345

    
1346
    def latest_of_chain(self, chain_id):
1347
        try:
1348
            return self.filter(chain=chain_id).order_by('-id')[0]
1349
        except IndexError:
1350
            return None
1351

    
1352

    
1353
class ProjectApplication(models.Model):
1354
    applicant               =   models.ForeignKey(
1355
                                    AstakosUser,
1356
                                    related_name='projects_applied',
1357
                                    db_index=True)
1358

    
1359
    PENDING     =    0
1360
    APPROVED    =    1
1361
    REPLACED    =    2
1362
    DENIED      =    3
1363
    DISMISSED   =    4
1364
    CANCELLED   =    5
1365

    
1366
    state                   =   models.IntegerField(default=PENDING,
1367
                                                    db_index=True)
1368

    
1369
    owner                   =   models.ForeignKey(
1370
                                    AstakosUser,
1371
                                    related_name='projects_owned',
1372
                                    db_index=True)
1373

    
1374
    chain                   =   models.ForeignKey(Chain,
1375
                                                  related_name='chained_apps',
1376
                                                  db_column='chain')
1377
    precursor_application   =   models.ForeignKey('ProjectApplication',
1378
                                                  null=True,
1379
                                                  blank=True)
1380

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

    
1400
    objects                 =   ProjectApplicationManager()
1401

    
1402
    # Compiled queries
1403
    Q_PENDING  = Q(state=PENDING)
1404
    Q_APPROVED = Q(state=APPROVED)
1405
    Q_DENIED   = Q(state=DENIED)
1406

    
1407
    class Meta:
1408
        unique_together = ("chain", "id")
1409

    
1410
    def __unicode__(self):
1411
        return "%s applied by %s" % (self.name, self.applicant)
1412

    
1413
    # TODO: Move to a more suitable place
1414
    APPLICATION_STATE_DISPLAY = {
1415
        PENDING  : _('Pending review'),
1416
        APPROVED : _('Approved'),
1417
        REPLACED : _('Replaced'),
1418
        DENIED   : _('Denied'),
1419
        DISMISSED: _('Dismissed'),
1420
        CANCELLED: _('Cancelled')
1421
    }
1422

    
1423
    @property
1424
    def log_display(self):
1425
        return "application %s (%s) for project %s" % (
1426
            self.id, self.name, self.chain)
1427

    
1428
    def get_project(self):
1429
        try:
1430
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1431
            return Project
1432
        except Project.DoesNotExist, e:
1433
            return None
1434

    
1435
    def state_display(self):
1436
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1437

    
1438
    def project_state_display(self):
1439
        try:
1440
            project = self.project
1441
            return project.state_display()
1442
        except Project.DoesNotExist:
1443
            return self.state_display()
1444

    
1445
    def add_resource_policy(self, service, resource, uplimit):
1446
        """Raises ObjectDoesNotExist, IntegrityError"""
1447
        q = self.projectresourcegrant_set
1448
        resource = Resource.objects.get(service__name=service, name=resource)
1449
        q.create(resource=resource, member_capacity=uplimit)
1450

    
1451
    def members_count(self):
1452
        return self.project.approved_memberships.count()
1453

    
1454
    @property
1455
    def grants(self):
1456
        return self.projectresourcegrant_set.values(
1457
            'member_capacity', 'resource__name', 'resource__service__name')
1458

    
1459
    @property
1460
    def resource_policies(self):
1461
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1462

    
1463
    @resource_policies.setter
1464
    def resource_policies(self, policies):
1465
        for p in policies:
1466
            service = p.get('service', None)
1467
            resource = p.get('resource', None)
1468
            uplimit = p.get('uplimit', 0)
1469
            self.add_resource_policy(service, resource, uplimit)
1470

    
1471
    def pending_modifications_incl_me(self):
1472
        q = self.chained_applications()
1473
        q = q.filter(Q(state=self.PENDING))
1474
        return q
1475

    
1476
    def last_pending_incl_me(self):
1477
        try:
1478
            return self.pending_modifications_incl_me().order_by('-id')[0]
1479
        except IndexError:
1480
            return None
1481

    
1482
    def pending_modifications(self):
1483
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1484

    
1485
    def last_pending(self):
1486
        try:
1487
            return self.pending_modifications().order_by('-id')[0]
1488
        except IndexError:
1489
            return None
1490

    
1491
    def is_modification(self):
1492
        # if self.state != self.PENDING:
1493
        #     return False
1494
        parents = self.chained_applications().filter(id__lt=self.id)
1495
        parents = parents.filter(state__in=[self.APPROVED])
1496
        return parents.count() > 0
1497

    
1498
    def chained_applications(self):
1499
        return ProjectApplication.objects.filter(chain=self.chain)
1500

    
1501
    def is_latest(self):
1502
        return self.chained_applications().order_by('-id')[0] == self
1503

    
1504
    def has_pending_modifications(self):
1505
        return bool(self.last_pending())
1506

    
1507
    def denied_modifications(self):
1508
        q = self.chained_applications()
1509
        q = q.filter(Q(state=self.DENIED))
1510
        q = q.filter(~Q(id=self.id))
1511
        return q
1512

    
1513
    def last_denied(self):
1514
        try:
1515
            return self.denied_modifications().order_by('-id')[0]
1516
        except IndexError:
1517
            return None
1518

    
1519
    def has_denied_modifications(self):
1520
        return bool(self.last_denied())
1521

    
1522
    def is_applied(self):
1523
        try:
1524
            self.project
1525
            return True
1526
        except Project.DoesNotExist:
1527
            return False
1528

    
1529
    def get_project(self):
1530
        try:
1531
            return Project.objects.get(id=self.chain)
1532
        except Project.DoesNotExist:
1533
            return None
1534

    
1535
    def project_exists(self):
1536
        return self.get_project() is not None
1537

    
1538
    def _get_project_for_update(self):
1539
        try:
1540
            objects = Project.objects
1541
            project = objects.get_for_update(id=self.chain)
1542
            return project
1543
        except Project.DoesNotExist:
1544
            return None
1545

    
1546
    def can_cancel(self):
1547
        return self.state == self.PENDING
1548

    
1549
    def cancel(self):
1550
        if not self.can_cancel():
1551
            m = _("cannot cancel: application '%s' in state '%s'") % (
1552
                    self.id, self.state)
1553
            raise AssertionError(m)
1554

    
1555
        self.state = self.CANCELLED
1556
        self.save()
1557

    
1558
    def can_dismiss(self):
1559
        return self.state == self.DENIED
1560

    
1561
    def dismiss(self):
1562
        if not self.can_dismiss():
1563
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1564
                    self.id, self.state)
1565
            raise AssertionError(m)
1566

    
1567
        self.state = self.DISMISSED
1568
        self.save()
1569

    
1570
    def can_deny(self):
1571
        return self.state == self.PENDING
1572

    
1573
    def deny(self, reason):
1574
        if not self.can_deny():
1575
            m = _("cannot deny: application '%s' in state '%s'") % (
1576
                    self.id, self.state)
1577
            raise AssertionError(m)
1578

    
1579
        self.state = self.DENIED
1580
        self.response_date = datetime.now()
1581
        self.response = reason
1582
        self.save()
1583

    
1584
    def can_approve(self):
1585
        return self.state == self.PENDING
1586

    
1587
    def approve(self, approval_user=None):
1588
        """
1589
        If approval_user then during owner membership acceptance
1590
        it is checked whether the request_user is eligible.
1591

1592
        Raises:
1593
            PermissionDenied
1594
        """
1595

    
1596
        if not transaction.is_managed():
1597
            raise AssertionError("NOPE")
1598

    
1599
        new_project_name = self.name
1600
        if not self.can_approve():
1601
            m = _("cannot approve: project '%s' in state '%s'") % (
1602
                    new_project_name, self.state)
1603
            raise AssertionError(m) # invalid argument
1604

    
1605
        now = datetime.now()
1606
        project = self._get_project_for_update()
1607

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

    
1619
        new_project = False
1620
        if project is None:
1621
            new_project = True
1622
            project = Project(id=self.chain)
1623

    
1624
        project.name = new_project_name
1625
        project.application = self
1626
        project.last_approval_date = now
1627
        if not new_project:
1628
            project.is_modified = True
1629

    
1630
        project.save()
1631

    
1632
        self.state = self.APPROVED
1633
        self.response_date = now
1634
        self.save()
1635
        return project
1636

    
1637
    @property
1638
    def member_join_policy_display(self):
1639
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1640

    
1641
    @property
1642
    def member_leave_policy_display(self):
1643
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1644

    
1645
class ProjectResourceGrant(models.Model):
1646

    
1647
    resource                =   models.ForeignKey(Resource)
1648
    project_application     =   models.ForeignKey(ProjectApplication,
1649
                                                  null=True)
1650
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1651
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1652
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1653
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1654
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1655
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1656

    
1657
    objects = ExtendedManager()
1658

    
1659
    class Meta:
1660
        unique_together = ("resource", "project_application")
1661

    
1662
    def display_member_capacity(self):
1663
        if self.member_capacity:
1664
            if self.resource.unit:
1665
                return ProjectResourceGrant.display_filesize(
1666
                    self.member_capacity)
1667
            else:
1668
                if math.isinf(self.member_capacity):
1669
                    return 'Unlimited'
1670
                else:
1671
                    return self.member_capacity
1672
        else:
1673
            return 'Unlimited'
1674

    
1675
    def __str__(self):
1676
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1677
                                        self.display_member_capacity())
1678

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

    
1703

    
1704
class ProjectManager(ForUpdateManager):
1705

    
1706
    def terminated_projects(self):
1707
        q = self.model.Q_TERMINATED
1708
        return self.filter(q)
1709

    
1710
    def not_terminated_projects(self):
1711
        q = ~self.model.Q_TERMINATED
1712
        return self.filter(q)
1713

    
1714
    def deactivated_projects(self):
1715
        q = self.model.Q_DEACTIVATED
1716
        return self.filter(q)
1717

    
1718
    def modified_projects(self):
1719
        return self.filter(is_modified=True)
1720

    
1721
    def expired_projects(self):
1722
        q = (~Q(state=Project.TERMINATED) &
1723
              Q(application__end_date__lt=datetime.now()))
1724
        return self.filter(q)
1725

    
1726
    def search_by_name(self, *search_strings):
1727
        q = Q()
1728
        for s in search_strings:
1729
            q = q | Q(name__icontains=s)
1730
        return self.filter(q)
1731

    
1732

    
1733
class Project(models.Model):
1734

    
1735
    id                          =   models.OneToOneField(Chain,
1736
                                                      related_name='chained_project',
1737
                                                      db_column='id',
1738
                                                      primary_key=True)
1739

    
1740
    application                 =   models.OneToOneField(
1741
                                            ProjectApplication,
1742
                                            related_name='project')
1743
    last_approval_date          =   models.DateTimeField(null=True)
1744

    
1745
    members                     =   models.ManyToManyField(
1746
                                            AstakosUser,
1747
                                            through='ProjectMembership')
1748

    
1749
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1750
    deactivation_date           =   models.DateTimeField(null=True)
1751

    
1752
    creation_date               =   models.DateTimeField(auto_now_add=True)
1753
    name                        =   models.CharField(
1754
                                            max_length=80,
1755
                                            null=True,
1756
                                            db_index=True,
1757
                                            unique=True)
1758

    
1759
    APPROVED    = 1
1760
    SUSPENDED   = 10
1761
    TERMINATED  = 100
1762

    
1763
    is_modified                 =   models.BooleanField(default=False,
1764
                                                        db_index=True)
1765
    is_active                   =   models.BooleanField(default=True,
1766
                                                        db_index=True)
1767
    state                       =   models.IntegerField(default=APPROVED,
1768
                                                        db_index=True)
1769

    
1770
    objects     =   ProjectManager()
1771

    
1772
    # Compiled queries
1773
    Q_TERMINATED  = Q(state=TERMINATED)
1774
    Q_SUSPENDED   = Q(state=SUSPENDED)
1775
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1776

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

    
1781
    __repr__ = __str__
1782

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

    
1786
    STATE_DISPLAY = {
1787
        APPROVED   : 'Active',
1788
        SUSPENDED  : 'Suspended',
1789
        TERMINATED : 'Terminated'
1790
        }
1791

    
1792
    def state_display(self):
1793
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1794

    
1795
    def expiration_info(self):
1796
        return (str(self.id), self.name, self.state_display(),
1797
                str(self.application.end_date))
1798

    
1799
    def is_deactivated(self, reason=None):
1800
        if reason is not None:
1801
            return self.state == reason
1802

    
1803
        return self.state != self.APPROVED
1804

    
1805
    ### Deactivation calls
1806

    
1807
    def terminate(self):
1808
        self.deactivation_reason = 'TERMINATED'
1809
        self.deactivation_date = datetime.now()
1810
        self.state = self.TERMINATED
1811
        self.name = None
1812
        self.save()
1813

    
1814
    def suspend(self):
1815
        self.deactivation_reason = 'SUSPENDED'
1816
        self.deactivation_date = datetime.now()
1817
        self.state = self.SUSPENDED
1818
        self.save()
1819

    
1820
    def resume(self):
1821
        self.deactivation_reason = None
1822
        self.deactivation_date = None
1823
        self.state = self.APPROVED
1824
        self.save()
1825

    
1826
    ### Logical checks
1827

    
1828
    def is_inconsistent(self):
1829
        now = datetime.now()
1830
        dates = [self.creation_date,
1831
                 self.last_approval_date,
1832
                 self.deactivation_date]
1833
        return any([date > now for date in dates])
1834

    
1835
    def is_active_strict(self):
1836
        return self.is_active and self.state == self.APPROVED
1837

    
1838
    def is_approved(self):
1839
        return self.state == self.APPROVED
1840

    
1841
    @property
1842
    def is_alive(self):
1843
        return not self.is_terminated
1844

    
1845
    @property
1846
    def is_terminated(self):
1847
        return self.is_deactivated(self.TERMINATED)
1848

    
1849
    @property
1850
    def is_suspended(self):
1851
        return self.is_deactivated(self.SUSPENDED)
1852

    
1853
    def violates_resource_grants(self):
1854
        return False
1855

    
1856
    def violates_members_limit(self, adding=0):
1857
        application = self.application
1858
        limit = application.limit_on_members_number
1859
        if limit is None:
1860
            return False
1861
        return (len(self.approved_members) + adding > limit)
1862

    
1863

    
1864
    ### Other
1865

    
1866
    def count_pending_memberships(self):
1867
        memb_set = self.projectmembership_set
1868
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1869
        return memb_count
1870

    
1871
    def members_count(self):
1872
        return self.approved_memberships.count()
1873

    
1874
    @property
1875
    def approved_memberships(self):
1876
        query = ProjectMembership.Q_ACCEPTED_STATES
1877
        return self.projectmembership_set.filter(query)
1878

    
1879
    @property
1880
    def approved_members(self):
1881
        return [m.person for m in self.approved_memberships]
1882

    
1883
    def add_member(self, user):
1884
        """
1885
        Raises:
1886
            django.exceptions.PermissionDenied
1887
            astakos.im.models.AstakosUser.DoesNotExist
1888
        """
1889
        if isinstance(user, (int, long)):
1890
            user = AstakosUser.objects.get(user=user)
1891

    
1892
        m, created = ProjectMembership.objects.get_or_create(
1893
            person=user, project=self
1894
        )
1895
        m.accept()
1896

    
1897
    def remove_member(self, user):
1898
        """
1899
        Raises:
1900
            django.exceptions.PermissionDenied
1901
            astakos.im.models.AstakosUser.DoesNotExist
1902
            astakos.im.models.ProjectMembership.DoesNotExist
1903
        """
1904
        if isinstance(user, (int, long)):
1905
            user = AstakosUser.objects.get(user=user)
1906

    
1907
        m = ProjectMembership.objects.get(person=user, project=self)
1908
        m.remove()
1909

    
1910

    
1911
CHAIN_STATE = {
1912
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1913
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1914
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1915
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1916
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1917

    
1918
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1919
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1920
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1921
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1922
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1923

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

    
1930
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1931
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1932
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1933
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1934
    }
1935

    
1936

    
1937
class ProjectMembershipManager(ForUpdateManager):
1938

    
1939
    def any_accepted(self):
1940
        q = self.model.Q_ACTUALLY_ACCEPTED
1941
        return self.filter(q)
1942

    
1943
    def actually_accepted(self):
1944
        q = self.model.Q_ACTUALLY_ACCEPTED
1945
        return self.filter(q)
1946

    
1947
    def requested(self):
1948
        return self.filter(state=ProjectMembership.REQUESTED)
1949

    
1950
    def suspended(self):
1951
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1952

    
1953
class ProjectMembership(models.Model):
1954

    
1955
    person              =   models.ForeignKey(AstakosUser)
1956
    request_date        =   models.DateField(auto_now_add=True)
1957
    project             =   models.ForeignKey(Project)
1958

    
1959
    REQUESTED           =   0
1960
    ACCEPTED            =   1
1961
    LEAVE_REQUESTED     =   5
1962
    # User deactivation
1963
    USER_SUSPENDED      =   10
1964

    
1965
    REMOVED             =   200
1966

    
1967
    ASSOCIATED_STATES   =   set([REQUESTED,
1968
                                 ACCEPTED,
1969
                                 LEAVE_REQUESTED,
1970
                                 USER_SUSPENDED,
1971
                                 ])
1972

    
1973
    ACCEPTED_STATES     =   set([ACCEPTED,
1974
                                 LEAVE_REQUESTED,
1975
                                 USER_SUSPENDED,
1976
                                 ])
1977

    
1978
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1979

    
1980
    state               =   models.IntegerField(default=REQUESTED,
1981
                                                db_index=True)
1982
    is_pending          =   models.BooleanField(default=False, db_index=True)
1983
    is_active           =   models.BooleanField(default=False, db_index=True)
1984
    application         =   models.ForeignKey(
1985
                                ProjectApplication,
1986
                                null=True,
1987
                                related_name='memberships')
1988
    pending_application =   models.ForeignKey(
1989
                                ProjectApplication,
1990
                                null=True,
1991
                                related_name='pending_memberships')
1992
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1993

    
1994
    acceptance_date     =   models.DateField(null=True, db_index=True)
1995
    leave_request_date  =   models.DateField(null=True)
1996

    
1997
    objects     =   ProjectMembershipManager()
1998

    
1999
    # Compiled queries
2000
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2001
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2002

    
2003
    MEMBERSHIP_STATE_DISPLAY = {
2004
        REQUESTED           : _('Requested'),
2005
        ACCEPTED            : _('Accepted'),
2006
        LEAVE_REQUESTED     : _('Leave Requested'),
2007
        USER_SUSPENDED      : _('Suspended'),
2008
        REMOVED             : _('Pending removal'),
2009
        }
2010

    
2011
    USER_FRIENDLY_STATE_DISPLAY = {
2012
        REQUESTED           : _('Join requested'),
2013
        ACCEPTED            : _('Accepted member'),
2014
        LEAVE_REQUESTED     : _('Requested to leave'),
2015
        USER_SUSPENDED      : _('Suspended member'),
2016
        REMOVED             : _('Pending removal'),
2017
        }
2018

    
2019
    def state_display(self):
2020
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2021

    
2022
    def user_friendly_state_display(self):
2023
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2024

    
2025
    class Meta:
2026
        unique_together = ("person", "project")
2027
        #index_together = [["project", "state"]]
2028

    
2029
    def __str__(self):
2030
        return uenc(_("<'%s' membership in '%s'>") % (
2031
                self.person.username, self.project))
2032

    
2033
    __repr__ = __str__
2034

    
2035
    def __init__(self, *args, **kwargs):
2036
        self.state = self.REQUESTED
2037
        super(ProjectMembership, self).__init__(*args, **kwargs)
2038

    
2039
    def _set_history_item(self, reason, date=None):
2040
        if isinstance(reason, basestring):
2041
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2042

    
2043
        history_item = ProjectMembershipHistory(
2044
                            serial=self.id,
2045
                            person=self.person_id,
2046
                            project=self.project_id,
2047
                            date=date or datetime.now(),
2048
                            reason=reason)
2049
        history_item.save()
2050
        serial = history_item.id
2051

    
2052
    def can_accept(self):
2053
        return self.state == self.REQUESTED
2054

    
2055
    def accept(self):
2056
        if not self.can_accept():
2057
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2058
            raise AssertionError(m)
2059

    
2060
        now = datetime.now()
2061
        self.acceptance_date = now
2062
        self._set_history_item(reason='ACCEPT', date=now)
2063
        self.state = self.ACCEPTED
2064
        self.save()
2065

    
2066
    def can_leave(self):
2067
        return self.state in self.ACCEPTED_STATES
2068

    
2069
    def leave_request(self):
2070
        if not self.can_leave():
2071
            m = _("%s: attempt to request to leave in state '%s'") % (
2072
                self, self.state)
2073
            raise AssertionError(m)
2074

    
2075
        self.leave_request_date = datetime.now()
2076
        self.state = self.LEAVE_REQUESTED
2077
        self.save()
2078

    
2079
    def can_deny_leave(self):
2080
        return self.state == self.LEAVE_REQUESTED
2081

    
2082
    def leave_request_deny(self):
2083
        if not self.can_deny_leave():
2084
            m = _("%s: attempt to deny leave request in state '%s'") % (
2085
                self, self.state)
2086
            raise AssertionError(m)
2087

    
2088
        self.leave_request_date = None
2089
        self.state = self.ACCEPTED
2090
        self.save()
2091

    
2092
    def can_cancel_leave(self):
2093
        return self.state == self.LEAVE_REQUESTED
2094

    
2095
    def leave_request_cancel(self):
2096
        if not self.can_cancel_leave():
2097
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2098
                self, self.state)
2099
            raise AssertionError(m)
2100

    
2101
        self.leave_request_date = None
2102
        self.state = self.ACCEPTED
2103
        self.save()
2104

    
2105
    def can_remove(self):
2106
        return self.state in self.ACCEPTED_STATES
2107

    
2108
    def remove(self):
2109
        if not self.can_remove():
2110
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2111
            raise AssertionError(m)
2112

    
2113
        self._set_history_item(reason='REMOVE')
2114
        self.delete()
2115

    
2116
    def can_reject(self):
2117
        return self.state == self.REQUESTED
2118

    
2119
    def reject(self):
2120
        if not self.can_reject():
2121
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2122
            raise AssertionError(m)
2123

    
2124
        # rejected requests don't need sync,
2125
        # because they were never effected
2126
        self._set_history_item(reason='REJECT')
2127
        self.delete()
2128

    
2129
    def can_cancel(self):
2130
        return self.state == self.REQUESTED
2131

    
2132
    def cancel(self):
2133
        if not self.can_cancel():
2134
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2135
            raise AssertionError(m)
2136

    
2137
        # rejected requests don't need sync,
2138
        # because they were never effected
2139
        self._set_history_item(reason='CANCEL')
2140
        self.delete()
2141

    
2142

    
2143
class Serial(models.Model):
2144
    serial  =   models.AutoField(primary_key=True)
2145

    
2146

    
2147
class ProjectMembershipHistory(models.Model):
2148
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2149
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2150

    
2151
    person  =   models.BigIntegerField()
2152
    project =   models.BigIntegerField()
2153
    date    =   models.DateField(auto_now_add=True)
2154
    reason  =   models.IntegerField()
2155
    serial  =   models.BigIntegerField()
2156

    
2157
### SIGNALS ###
2158
################
2159

    
2160
def create_astakos_user(u):
2161
    try:
2162
        AstakosUser.objects.get(user_ptr=u.pk)
2163
    except AstakosUser.DoesNotExist:
2164
        extended_user = AstakosUser(user_ptr_id=u.pk)
2165
        extended_user.__dict__.update(u.__dict__)
2166
        extended_user.save()
2167
        if not extended_user.has_auth_provider('local'):
2168
            extended_user.add_auth_provider('local')
2169
    except BaseException, e:
2170
        logger.exception(e)
2171

    
2172
def fix_superusers():
2173
    # Associate superusers with AstakosUser
2174
    admins = User.objects.filter(is_superuser=True)
2175
    for u in admins:
2176
        create_astakos_user(u)
2177

    
2178
def user_post_save(sender, instance, created, **kwargs):
2179
    if not created:
2180
        return
2181
    create_astakos_user(instance)
2182
post_save.connect(user_post_save, sender=User)
2183

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

    
2187
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2188

    
2189
def resource_post_save(sender, instance, created, **kwargs):
2190
    pass
2191

    
2192
post_save.connect(resource_post_save, sender=Resource)
2193

    
2194
def renew_token(sender, instance, **kwargs):
2195
    if not instance.auth_token:
2196
        instance.renew_token()
2197
pre_save.connect(renew_token, sender=AstakosUser)
2198
pre_save.connect(renew_token, sender=Service)