Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 34d3883a

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
    def __str__(self):
164
        return self.name
165

    
166
    def full_name(self):
167
        return str(self)
168

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

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

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

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

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

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

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

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

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

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

    
218

    
219
class AstakosUserManager(UserManager):
220

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

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

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

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

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

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

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

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

    
273

    
274

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

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

    
291

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

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

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

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

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

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

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

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

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

    
335
    objects = AstakosUserManager()
336

    
337
    forupdate = ForUpdateManager()
338

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
456
        self.update_uuid()
457

    
458
        if self.username != self.email.lower():
459
            # set username
460
            self.username = self.email.lower()
461

    
462
        super(AstakosUser, self).save(**kwargs)
463

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

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

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

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

    
494
    def __unicode__(self):
495
        return '%s (%s)' % (self.realname, self.email)
496

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

    
504
    def email_change_is_pending(self):
505
        return self.emailchanges.count() > 0
506

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

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

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

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

    
538
        local = self.get_auth_provider('local')._instance
539
        return local.auth_backend == 'astakos'
540

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

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

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

    
556
    def get_required_providers(self, **kwargs):
557
        return auth.REQUIRED_PROVIDERS.keys()
558

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

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

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

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

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

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

    
604
        modules = astakos_settings.IM_MODULES
605

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

    
611
        providers = sorted(providers, key=key)
612
        return providers
613

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

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

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

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

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

    
639
    def get_inactive_message(self, provider_module, identifier=None):
640
        provider = self.get_auth_provider(provider_module, identifier)
641

    
642
        msg_extra = ''
643
        message = ''
644

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

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

    
670
        return mark_safe(message + u' '+ msg_extra)
671

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

    
675
    def owns_project(self, project):
676
        return project.application.owner == self
677

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

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

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

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

    
712
    def settings(self):
713
        return UserSetting.objects.filter(user=self)
714

    
715

    
716
class AstakosUserAuthProviderManager(models.Manager):
717

    
718
    def active(self, **filters):
719
        return self.filter(active=True, **filters)
720

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

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

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

    
744

    
745
class AuthProviderPolicyProfileManager(models.Manager):
746

    
747
    def active(self):
748
        return self.filter(active=True)
749

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

    
756
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
757
            policies.update(profile.policies)
758

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

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

    
780

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

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

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

    
799
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
800
                     'automoderate')
801

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

    
808
    objects = AuthProviderPolicyProfileManager()
809

    
810
    class Meta:
811
        ordering = ['priority']
812

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

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

    
829

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

    
848
    objects = AstakosUserAuthProviderManager()
849

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

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

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

    
866
    @property
867
    def settings(self):
868
        extra_data = {}
869

    
870
        info_data = {}
871
        if self.info_data:
872
            info_data = json.loads(self.info_data)
873

    
874
        extra_data['info'] = info_data
875

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

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

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

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

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

    
897

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

    
925
    update_or_create = _update_or_create
926

    
927

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

    
937
    class Meta:
938
        unique_together = ("resource", "user")
939

    
940

    
941
class ApprovalTerms(models.Model):
942
    """
943
    Model for approval terms
944
    """
945

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

    
950

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

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

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

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

    
977

    
978
class EmailChangeManager(models.Manager):
979

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

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

989
        If the key is not valid or has expired, return ``None``.
990

991
        If the key is valid but the ``User`` is already active,
992
        return ``None``.
993

994
        After successful email change the activation record is deleted.
995

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

    
1025

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

    
1038
    objects = EmailChangeManager()
1039

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

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

    
1048

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

    
1056

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

    
1066

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

    
1075

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

    
1095
    class Meta:
1096
        unique_together = ("provider", "third_party_identifier")
1097

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

    
1107
        return user
1108

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

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

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

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

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

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

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

    
1154

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

    
1160
    objects = ForUpdateManager()
1161

    
1162
    class Meta:
1163
        unique_together = ("user", "setting")
1164

    
1165

    
1166
### PROJECTS ###
1167
################
1168

    
1169
class ChainManager(ForUpdateManager):
1170

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

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

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

    
1187
        objs = ProjectApplication.objects.select_related('applicant')
1188
        apps = objs.in_bulk(chain_latest.values())
1189

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

    
1197
        return d
1198

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

    
1207

    
1208
class Chain(models.Model):
1209
    chain  =   models.AutoField(primary_key=True)
1210

    
1211
    def __str__(self):
1212
        return "%s" % (self.chain,)
1213

    
1214
    objects = ChainManager()
1215

    
1216
    PENDING            = 0
1217
    DENIED             = 3
1218
    DISMISSED          = 4
1219
    CANCELLED          = 5
1220

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

    
1228
    PENDING_STATES = [PENDING,
1229
                      APPROVED_PENDING,
1230
                      SUSPENDED_PENDING,
1231
                      TERMINATED_PENDING,
1232
                      ]
1233

    
1234
    MODIFICATION_STATES = [APPROVED_PENDING,
1235
                           SUSPENDED_PENDING,
1236
                           TERMINATED_PENDING,
1237
                           ]
1238

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

    
1248
    SKIP_STATES = [DISMISSED,
1249
                   CANCELLED,
1250
                   TERMINATED]
1251

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

    
1265

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

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

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

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

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

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

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

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

    
1306

    
1307
def new_chain():
1308
    c = Chain.objects.create()
1309
    return c
1310

    
1311

    
1312
class ProjectApplicationManager(ForUpdateManager):
1313

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

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

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

    
1336
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1337

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

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

    
1350

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

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

    
1364
    state                   =   models.IntegerField(default=PENDING,
1365
                                                    db_index=True)
1366

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

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

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

    
1398
    objects                 =   ProjectApplicationManager()
1399

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

    
1405
    class Meta:
1406
        unique_together = ("chain", "id")
1407

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

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

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

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

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

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

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

    
1449
    def members_count(self):
1450
        return self.project.approved_memberships.count()
1451

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

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

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

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

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

    
1480
    def pending_modifications(self):
1481
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1482

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

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

    
1496
    def chained_applications(self):
1497
        return ProjectApplication.objects.filter(chain=self.chain)
1498

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

    
1502
    def has_pending_modifications(self):
1503
        return bool(self.last_pending())
1504

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

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

    
1517
    def has_denied_modifications(self):
1518
        return bool(self.last_denied())
1519

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

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

    
1533
    def project_exists(self):
1534
        return self.get_project() is not None
1535

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

    
1544
    def can_cancel(self):
1545
        return self.state == self.PENDING
1546

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

    
1553
        self.state = self.CANCELLED
1554
        self.save()
1555

    
1556
    def can_dismiss(self):
1557
        return self.state == self.DENIED
1558

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

    
1565
        self.state = self.DISMISSED
1566
        self.save()
1567

    
1568
    def can_deny(self):
1569
        return self.state == self.PENDING
1570

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

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

    
1582
    def can_approve(self):
1583
        return self.state == self.PENDING
1584

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

1590
        Raises:
1591
            PermissionDenied
1592
        """
1593

    
1594
        if not transaction.is_managed():
1595
            raise AssertionError("NOPE")
1596

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

    
1603
        now = datetime.now()
1604
        project = self._get_project_for_update()
1605

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

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

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

    
1628
        project.save()
1629

    
1630
        self.state = self.APPROVED
1631
        self.response_date = now
1632
        self.save()
1633
        return project
1634

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

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

    
1643
class ProjectResourceGrant(models.Model):
1644

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

    
1655
    objects = ExtendedManager()
1656

    
1657
    class Meta:
1658
        unique_together = ("resource", "project_application")
1659

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

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

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

    
1701

    
1702
class ProjectManager(ForUpdateManager):
1703

    
1704
    def terminated_projects(self):
1705
        q = self.model.Q_TERMINATED
1706
        return self.filter(q)
1707

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

    
1712
    def deactivated_projects(self):
1713
        q = self.model.Q_DEACTIVATED
1714
        return self.filter(q)
1715

    
1716
    def modified_projects(self):
1717
        return self.filter(is_modified=True)
1718

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

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

    
1730

    
1731
class Project(models.Model):
1732

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

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

    
1743
    members                     =   models.ManyToManyField(
1744
                                            AstakosUser,
1745
                                            through='ProjectMembership')
1746

    
1747
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1748
    deactivation_date           =   models.DateTimeField(null=True)
1749

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

    
1757
    APPROVED    = 1
1758
    SUSPENDED   = 10
1759
    TERMINATED  = 100
1760

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

    
1768
    objects     =   ProjectManager()
1769

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

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

    
1779
    __repr__ = __str__
1780

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

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

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

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

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

    
1801
        return self.state != self.APPROVED
1802

    
1803
    ### Deactivation calls
1804

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

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

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

    
1824
    ### Logical checks
1825

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

    
1833
    def is_active_strict(self):
1834
        return self.is_active and self.state == self.APPROVED
1835

    
1836
    def is_approved(self):
1837
        return self.state == self.APPROVED
1838

    
1839
    @property
1840
    def is_alive(self):
1841
        return not self.is_terminated
1842

    
1843
    @property
1844
    def is_terminated(self):
1845
        return self.is_deactivated(self.TERMINATED)
1846

    
1847
    @property
1848
    def is_suspended(self):
1849
        return self.is_deactivated(self.SUSPENDED)
1850

    
1851
    def violates_resource_grants(self):
1852
        return False
1853

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

    
1861

    
1862
    ### Other
1863

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

    
1869
    def members_count(self):
1870
        return self.approved_memberships.count()
1871

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

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

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

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

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

    
1905
        m = ProjectMembership.objects.get(person=user, project=self)
1906
        m.remove()
1907

    
1908

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

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

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

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

    
1934

    
1935
class ProjectMembershipManager(ForUpdateManager):
1936

    
1937
    def any_accepted(self):
1938
        q = self.model.Q_ACTUALLY_ACCEPTED
1939
        return self.filter(q)
1940

    
1941
    def actually_accepted(self):
1942
        q = self.model.Q_ACTUALLY_ACCEPTED
1943
        return self.filter(q)
1944

    
1945
    def requested(self):
1946
        return self.filter(state=ProjectMembership.REQUESTED)
1947

    
1948
    def suspended(self):
1949
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1950

    
1951
class ProjectMembership(models.Model):
1952

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

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

    
1963
    REMOVED             =   200
1964

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

    
1971
    ACCEPTED_STATES     =   set([ACCEPTED,
1972
                                 LEAVE_REQUESTED,
1973
                                 USER_SUSPENDED,
1974
                                 ])
1975

    
1976
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1977

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

    
1992
    acceptance_date     =   models.DateField(null=True, db_index=True)
1993
    leave_request_date  =   models.DateField(null=True)
1994

    
1995
    objects     =   ProjectMembershipManager()
1996

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

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

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

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

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

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

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

    
2031
    __repr__ = __str__
2032

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

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

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

    
2050
    def can_accept(self):
2051
        return self.state == self.REQUESTED
2052

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

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

    
2064
    def can_leave(self):
2065
        return self.state in self.ACCEPTED_STATES
2066

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

    
2073
        self.leave_request_date = datetime.now()
2074
        self.state = self.LEAVE_REQUESTED
2075
        self.save()
2076

    
2077
    def can_deny_leave(self):
2078
        return self.state == self.LEAVE_REQUESTED
2079

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

    
2086
        self.leave_request_date = None
2087
        self.state = self.ACCEPTED
2088
        self.save()
2089

    
2090
    def can_cancel_leave(self):
2091
        return self.state == self.LEAVE_REQUESTED
2092

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

    
2099
        self.leave_request_date = None
2100
        self.state = self.ACCEPTED
2101
        self.save()
2102

    
2103
    def can_remove(self):
2104
        return self.state in self.ACCEPTED_STATES
2105

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

    
2111
        self._set_history_item(reason='REMOVE')
2112
        self.delete()
2113

    
2114
    def can_reject(self):
2115
        return self.state == self.REQUESTED
2116

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

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

    
2127
    def can_cancel(self):
2128
        return self.state == self.REQUESTED
2129

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

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

    
2140

    
2141
class Serial(models.Model):
2142
    serial  =   models.AutoField(primary_key=True)
2143

    
2144

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

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

    
2155
### SIGNALS ###
2156
################
2157

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

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

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

    
2182
def astakosuser_post_save(sender, instance, created, **kwargs):
2183
    pass
2184

    
2185
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2186

    
2187
def resource_post_save(sender, instance, created, **kwargs):
2188
    pass
2189

    
2190
post_save.connect(resource_post_save, sender=Resource)
2191

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