Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 0da5e49a

History | View | Annotate | Download (80.5 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, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA,
72
    PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES, PROJECT_ADMINS)
73
from astakos.im import settings as astakos_settings
74
from astakos.im.endpoints.qh import (
75
    send_quotas, qh_get_quotas,
76
    register_resources, qh_add_quota, QuotaLimits,
77
    )
78
from astakos.im import auth_providers as auth
79

    
80
import astakos.im.messages as astakos_messages
81
from synnefo.lib.db.managers import ForUpdateManager
82

    
83
from astakos.quotaholder.api import QH_PRACTICALLY_INFINITE
84
from synnefo.lib.db.intdecimalfield import intDecimalField
85
from synnefo.util.text import uenc, udec
86

    
87
from astakos.im.quotas import get_users_quotas_and_limits, set_user_quota
88

    
89
logger = logging.getLogger(__name__)
90

    
91
DEFAULT_CONTENT_TYPE = None
92
_content_type = None
93

    
94
def get_content_type():
95
    global _content_type
96
    if _content_type is not None:
97
        return _content_type
98

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

    
106
RESOURCE_SEPARATOR = '.'
107

    
108
inf = float('inf')
109

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

    
121
    class Meta:
122
        ordering = ('order', )
123

    
124
    def renew_token(self, expiration_date=None):
125
        md5 = hashlib.md5()
126
        md5.update(self.name.encode('ascii', 'ignore'))
127
        md5.update(self.url.encode('ascii', 'ignore'))
128
        md5.update(asctime())
129

    
130
        self.auth_token = b64encode(md5.digest())
131
        self.auth_token_created = datetime.now()
132
        if expiration_date:
133
            self.auth_token_expires = expiration_date
134
        else:
135
            self.auth_token_expires = None
136

    
137
    def __str__(self):
138
        return self.name
139

    
140
    @property
141
    def resources(self):
142
        return self.resource_set.all()
143

    
144
    @resources.setter
145
    def resources(self, resources):
146
        for s in resources:
147
            self.resource_set.create(**s)
148

    
149

    
150
class ResourceMetadata(models.Model):
151
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
152
    value = models.CharField(_('Value'), max_length=255)
153

    
154
_presentation_data = {}
155
def get_presentation(resource):
156
    global _presentation_data
157
    presentation = _presentation_data.get(resource, {})
158
    if not presentation:
159
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
160
        presentation = resource_presentation.get(resource, {})
161
        _presentation_data[resource] = presentation
162
    return presentation
163

    
164
class Resource(models.Model):
165
    name = models.CharField(_('Name'), max_length=255)
166
    meta = models.ManyToManyField(ResourceMetadata)
167
    service = models.ForeignKey(Service)
168
    desc = models.TextField(_('Description'), null=True)
169
    unit = models.CharField(_('Name'), null=True, max_length=255)
170
    group = models.CharField(_('Group'), null=True, max_length=255)
171
    uplimit = intDecimalField(default=0)
172

    
173
    class Meta:
174
        unique_together = ("service", "name")
175

    
176
    def __str__(self):
177
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
178

    
179
    def full_name(self):
180
        return str(self)
181

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

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

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

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

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

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

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

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

    
219
def load_service_resources():
220
    ss = []
221
    rs = []
222
    counter = 0
223
    for service_name, data in sorted(SERVICES.iteritems()):
224
        url = data.get('url')
225
        order = data.get('order', counter)
226
        counter = order + 1
227
        resources = data.get('resources') or ()
228
        service, created = Service.objects.get_or_create(
229
            name=service_name,
230
            defaults={'url': url, 'order': order}
231
        )
232
        if not created and url is not None:
233
            service.url = url
234
            service.save()
235

    
236
        ss.append(service)
237

    
238
        for resource in resources:
239
            try:
240
                resource_name = resource.pop('name', '')
241
                r, created = Resource.objects.get_or_create(
242
                        service=service, name=resource_name,
243
                        defaults=resource)
244
                if not created:
245
                    r.desc = resource['desc']
246
                    r.unit = resource.get('unit', None)
247
                    r.group = resource['group']
248
                    r.uplimit = resource['uplimit']
249
                    r.save()
250

    
251
                rs.append(r)
252

    
253
            except Exception, e:
254
                print "Cannot create resource ", resource_name
255
                import traceback; traceback.print_exc()
256
                continue
257

    
258
    register_resources(rs)
259

    
260

    
261
def get_default_quota():
262
    _DEFAULT_QUOTA = {}
263
    resources = Resource.objects.select_related('service').all()
264
    for resource in resources:
265
        capacity = resource.uplimit
266
        _DEFAULT_QUOTA[resource.full_name()] = capacity
267

    
268
    return _DEFAULT_QUOTA
269

    
270
def get_resource_names():
271
    _RESOURCE_NAMES = []
272
    resources = Resource.objects.select_related('service').all()
273
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
274
    return _RESOURCE_NAMES
275

    
276

    
277
class AstakosUserManager(UserManager):
278

    
279
    def get_auth_provider_user(self, provider, **kwargs):
280
        """
281
        Retrieve AstakosUser instance associated with the specified third party
282
        id.
283
        """
284
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
285
                          kwargs.iteritems()))
286
        return self.get(auth_providers__module=provider, **kwargs)
287

    
288
    def get_by_email(self, email):
289
        return self.get(email=email)
290

    
291
    def get_by_identifier(self, email_or_username, **kwargs):
292
        try:
293
            return self.get(email__iexact=email_or_username, **kwargs)
294
        except AstakosUser.DoesNotExist:
295
            return self.get(username__iexact=email_or_username, **kwargs)
296

    
297
    def user_exists(self, email_or_username, **kwargs):
298
        qemail = Q(email__iexact=email_or_username)
299
        qusername = Q(username__iexact=email_or_username)
300
        qextra = Q(**kwargs)
301
        return self.filter((qemail | qusername) & qextra).exists()
302

    
303
    def verified_user_exists(self, email_or_username):
304
        return self.user_exists(email_or_username, email_verified=True)
305

    
306
    def verified(self):
307
        return self.filter(email_verified=True)
308

    
309
    def uuid_catalog(self, l=None):
310
        """
311
        Returns a uuid to username mapping for the uuids appearing in l.
312
        If l is None returns the mapping for all existing users.
313
        """
314
        q = self.filter(uuid__in=l) if l != None else self
315
        return dict(q.values_list('uuid', 'username'))
316

    
317
    def displayname_catalog(self, l=None):
318
        """
319
        Returns a username to uuid mapping for the usernames appearing in l.
320
        If l is None returns the mapping for all existing users.
321
        """
322
        if l is not None:
323
            lmap = dict((x.lower(), x) for x in l)
324
            q = self.filter(username__in=lmap.keys())
325
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
326
        else:
327
            q = self
328
            values = self.values_list('username', 'uuid')
329
        return dict(values)
330

    
331

    
332

    
333
class AstakosUser(User):
334
    """
335
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
336
    """
337
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
338
                                   null=True)
339

    
340
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
341
    #                    AstakosUserProvider model.
342
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
343
                                null=True)
344
    # ex. screen_name for twitter, eppn for shibboleth
345
    third_party_identifier = models.CharField(_('Third-party identifier'),
346
                                              max_length=255, null=True,
347
                                              blank=True)
348

    
349

    
350
    #for invitations
351
    user_level = DEFAULT_USER_LEVEL
352
    level = models.IntegerField(_('Inviter level'), default=user_level)
353
    invitations = models.IntegerField(
354
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
355

    
356
    auth_token = models.CharField(_('Authentication Token'),
357
                                  max_length=32,
358
                                  null=True,
359
                                  blank=True,
360
                                  help_text = _('Renew your authentication '
361
                                                'token. Make sure to set the new '
362
                                                'token in any client you may be '
363
                                                'using, to preserve its '
364
                                                'functionality.'))
365
    auth_token_created = models.DateTimeField(_('Token creation date'),
366
                                              null=True)
367
    auth_token_expires = models.DateTimeField(
368
        _('Token expiration date'), null=True)
369

    
370
    updated = models.DateTimeField(_('Update date'))
371
    is_verified = models.BooleanField(_('Is verified?'), default=False)
372

    
373
    email_verified = models.BooleanField(_('Email verified?'), default=False)
374

    
375
    has_credits = models.BooleanField(_('Has credits?'), default=False)
376
    has_signed_terms = models.BooleanField(
377
        _('I agree with the terms'), default=False)
378
    date_signed_terms = models.DateTimeField(
379
        _('Signed terms date'), null=True, blank=True)
380

    
381
    activation_sent = models.DateTimeField(
382
        _('Activation sent data'), null=True, blank=True)
383

    
384
    policy = models.ManyToManyField(
385
        Resource, null=True, through='AstakosUserQuota')
386

    
387
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
388

    
389
    __has_signed_terms = False
390
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
391
                                           default=False, db_index=True)
392

    
393
    objects = AstakosUserManager()
394

    
395
    def __init__(self, *args, **kwargs):
396
        super(AstakosUser, self).__init__(*args, **kwargs)
397
        self.__has_signed_terms = self.has_signed_terms
398
        if not self.id:
399
            self.is_active = False
400

    
401
    @property
402
    def realname(self):
403
        return '%s %s' % (self.first_name, self.last_name)
404

    
405
    @property
406
    def log_display(self):
407
        """
408
        Should be used in all logger.* calls that refer to a user so that
409
        user display is consistent across log entries.
410
        """
411
        return '%s::%s' % (self.uuid, self.email)
412

    
413
    @realname.setter
414
    def realname(self, value):
415
        parts = value.split(' ')
416
        if len(parts) == 2:
417
            self.first_name = parts[0]
418
            self.last_name = parts[1]
419
        else:
420
            self.last_name = parts[0]
421

    
422
    def add_permission(self, pname):
423
        if self.has_perm(pname):
424
            return
425
        p, created = Permission.objects.get_or_create(
426
                                    codename=pname,
427
                                    name=pname.capitalize(),
428
                                    content_type=get_content_type())
429
        self.user_permissions.add(p)
430

    
431
    def remove_permission(self, pname):
432
        if self.has_perm(pname):
433
            return
434
        p = Permission.objects.get(codename=pname,
435
                                   content_type=get_content_type())
436
        self.user_permissions.remove(p)
437

    
438
    def is_project_admin(self, application_id=None):
439
        return self.uuid in PROJECT_ADMINS
440

    
441
    @property
442
    def invitation(self):
443
        try:
444
            return Invitation.objects.get(username=self.email)
445
        except Invitation.DoesNotExist:
446
            return None
447

    
448
    @property
449
    def policies(self):
450
        return self.astakosuserquota_set.select_related().all()
451

    
452
    @policies.setter
453
    def policies(self, policies):
454
        for p in policies:
455
            p.setdefault('resource', '')
456
            p.setdefault('capacity', 0)
457
            p.setdefault('quantity', 0)
458
            p.setdefault('update', True)
459
            self.add_resource_policy(**p)
460

    
461
    def add_resource_policy(
462
            self, resource, capacity,
463
            update=True):
464
        """Raises ObjectDoesNotExist, IntegrityError"""
465
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
466
        resource = Resource.objects.get(service__name=s, name=r)
467
        if update:
468
            AstakosUserQuota.objects.update_or_create(
469
                user=self, resource=resource, defaults={
470
                    'capacity':capacity,
471
                    })
472
        else:
473
            q = self.astakosuserquota_set
474
            q.create(
475
                resource=resource, capacity=capacity, quanity=quantity,
476
                )
477

    
478
    def get_resource_policy(self, resource):
479
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
480
        resource = Resource.objects.get(service__name=s, name=r)
481
        default_capacity = resource.uplimit
482
        try:
483
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
484
            return policy, default_capacity
485
        except AstakosUserQuota.DoesNotExist:
486
            return None, default_capacity
487

    
488
    def remove_resource_policy(self, service, resource):
489
        """Raises ObjectDoesNotExist, IntegrityError"""
490
        resource = Resource.objects.get(service__name=service, name=resource)
491
        q = self.policies.get(resource=resource).delete()
492

    
493
    def update_uuid(self):
494
        while not self.uuid:
495
            uuid_val =  str(uuid.uuid4())
496
            try:
497
                AstakosUser.objects.get(uuid=uuid_val)
498
            except AstakosUser.DoesNotExist, e:
499
                self.uuid = uuid_val
500
        return self.uuid
501

    
502
    def save(self, update_timestamps=True, **kwargs):
503
        if update_timestamps:
504
            if not self.id:
505
                self.date_joined = datetime.now()
506
            self.updated = datetime.now()
507

    
508
        # update date_signed_terms if necessary
509
        if self.__has_signed_terms != self.has_signed_terms:
510
            self.date_signed_terms = datetime.now()
511

    
512
        self.update_uuid()
513

    
514
        if self.username != self.email.lower():
515
            # set username
516
            self.username = self.email.lower()
517

    
518
        super(AstakosUser, self).save(**kwargs)
519

    
520
    def renew_token(self, flush_sessions=False, current_key=None):
521
        md5 = hashlib.md5()
522
        md5.update(settings.SECRET_KEY)
523
        md5.update(self.username)
524
        md5.update(self.realname.encode('ascii', 'ignore'))
525
        md5.update(asctime())
526

    
527
        self.auth_token = b64encode(md5.digest())
528
        self.auth_token_created = datetime.now()
529
        self.auth_token_expires = self.auth_token_created + \
530
                                  timedelta(hours=AUTH_TOKEN_DURATION)
531
        if flush_sessions:
532
            self.flush_sessions(current_key)
533
        msg = 'Token renewed for %s' % self.email
534
        logger.log(LOGGING_LEVEL, msg)
535

    
536
    def flush_sessions(self, current_key=None):
537
        q = self.sessions
538
        if current_key:
539
            q = q.exclude(session_key=current_key)
540

    
541
        keys = q.values_list('session_key', flat=True)
542
        if keys:
543
            msg = 'Flushing sessions: %s' % ','.join(keys)
544
            logger.log(LOGGING_LEVEL, msg, [])
545
        engine = import_module(settings.SESSION_ENGINE)
546
        for k in keys:
547
            s = engine.SessionStore(k)
548
            s.flush()
549

    
550
    def __unicode__(self):
551
        return '%s (%s)' % (self.realname, self.email)
552

    
553
    def conflicting_email(self):
554
        q = AstakosUser.objects.exclude(username=self.username)
555
        q = q.filter(email__iexact=self.email)
556
        if q.count() != 0:
557
            return True
558
        return False
559

    
560
    def email_change_is_pending(self):
561
        return self.emailchanges.count() > 0
562

    
563
    @property
564
    def signed_terms(self):
565
        term = get_latest_terms()
566
        if not term:
567
            return True
568
        if not self.has_signed_terms:
569
            return False
570
        if not self.date_signed_terms:
571
            return False
572
        if self.date_signed_terms < term.date:
573
            self.has_signed_terms = False
574
            self.date_signed_terms = None
575
            self.save()
576
            return False
577
        return True
578

    
579
    def set_invitations_level(self):
580
        """
581
        Update user invitation level
582
        """
583
        level = self.invitation.inviter.level + 1
584
        self.level = level
585
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
586

    
587
    def can_change_password(self):
588
        return self.has_auth_provider('local', auth_backend='astakos')
589

    
590
    def can_change_email(self):
591
        if not self.has_auth_provider('local'):
592
            return True
593

    
594
        local = self.get_auth_provider('local')._instance
595
        return local.auth_backend == 'astakos'
596

    
597
    # Auth providers related methods
598
    def get_auth_provider(self, module=None, identifier=None, **filters):
599
        if not module:
600
            return self.auth_providers.active()[0].settings
601

    
602
        params = {'module': module}
603
        if identifier:
604
            params['identifier'] = identifier
605
        params.update(filters)
606
        return self.auth_providers.active().get(**params).settings
607

    
608
    def has_auth_provider(self, provider, **kwargs):
609
        return bool(self.auth_providers.active().filter(module=provider,
610
                                                        **kwargs).count())
611

    
612
    def get_required_providers(self, **kwargs):
613
        return auth.REQUIRED_PROVIDERS.keys()
614

    
615
    def missing_required_providers(self):
616
        required = self.get_required_providers()
617
        missing = []
618
        for provider in required:
619
            if not self.has_auth_provider(provider):
620
                missing.append(auth.get_provider(provider, self))
621
        return missing
622

    
623
    def get_available_auth_providers(self, **filters):
624
        """
625
        Returns a list of providers available for add by the user.
626
        """
627
        modules = astakos_settings.IM_MODULES
628
        providers = []
629
        for p in modules:
630
            providers.append(auth.get_provider(p, self))
631
        available = []
632

    
633
        for p in providers:
634
            if p.get_add_policy:
635
                available.append(p)
636
        return available
637

    
638
    def get_disabled_auth_providers(self, **filters):
639
        providers = self.get_auth_providers(**filters)
640
        disabled = []
641
        for p in providers:
642
            if not p.get_login_policy:
643
                disabled.append(p)
644
        return disabled
645

    
646
    def get_enabled_auth_providers(self, **filters):
647
        providers = self.get_auth_providers(**filters)
648
        enabled = []
649
        for p in providers:
650
            if p.get_login_policy:
651
                enabled.append(p)
652
        return enabled
653

    
654
    def get_auth_providers(self, **filters):
655
        providers = []
656
        for provider in self.auth_providers.active(**filters):
657
            if provider.settings.module_enabled:
658
                providers.append(provider.settings)
659

    
660
        modules = astakos_settings.IM_MODULES
661

    
662
        def key(p):
663
            if not p.module in modules:
664
                return 100
665
            return modules.index(p.module)
666

    
667
        providers = sorted(providers, key=key)
668
        return providers
669

    
670
    # URL methods
671
    @property
672
    def auth_providers_display(self):
673
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
674
                         self.get_enabled_auth_providers()])
675

    
676
    def add_auth_provider(self, module='local', identifier=None, **params):
677
        provider = auth.get_provider(module, self, identifier, **params)
678
        provider.add_to_user()
679

    
680
    def get_resend_activation_url(self):
681
        return reverse('send_activation', kwargs={'user_id': self.pk})
682

    
683
    def get_activation_url(self, nxt=False):
684
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
685
                                 quote(self.auth_token))
686
        if nxt:
687
            url += "&next=%s" % quote(nxt)
688
        return url
689

    
690
    def get_password_reset_url(self, token_generator=default_token_generator):
691
        return reverse('django.contrib.auth.views.password_reset_confirm',
692
                          kwargs={'uidb36':int_to_base36(self.id),
693
                                  'token':token_generator.make_token(self)})
694

    
695
    def get_inactive_message(self, provider_module, identifier=None):
696
        provider = self.get_auth_provider(provider_module, identifier)
697

    
698
        msg_extra = ''
699
        message = ''
700

    
701
        msg_inactive = provider.get_account_inactive_msg
702
        msg_pending = provider.get_pending_activation_msg
703
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
704
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
705
        msg_pending_mod = provider.get_pending_moderation_msg
706
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
707

    
708
        if self.activation_sent:
709
            if self.email_verified:
710
                message = msg_inactive
711
            else:
712
                message = msg_pending
713
                url = self.get_resend_activation_url()
714
                msg_extra = msg_pending_help + \
715
                            u' ' + \
716
                            '<a href="%s">%s?</a>' % (url, msg_resend)
717
        else:
718
            if astakos_settings.MODERATION_ENABLED:
719
                message = msg_pending_mod
720
            else:
721
                message = msg_pending
722
                url = self.get_resend_activation_url()
723
                msg_extra = '<a href="%s">%s?</a>' % (url, \
724
                                msg_resend)
725

    
726
        return mark_safe(message + u' '+ msg_extra)
727

    
728
    def owns_application(self, application):
729
        return application.owner == self
730

    
731
    def owns_project(self, project):
732
        return project.application.owner == self
733

    
734
    def is_associated(self, project):
735
        try:
736
            m = ProjectMembership.objects.get(person=self, project=project)
737
            return m.state in ProjectMembership.ASSOCIATED_STATES
738
        except ProjectMembership.DoesNotExist:
739
            return False
740

    
741
    def get_membership(self, project):
742
        try:
743
            return ProjectMembership.objects.get(
744
                project=project,
745
                person=self)
746
        except ProjectMembership.DoesNotExist:
747
            return None
748

    
749
    def membership_display(self, project):
750
        m = self.get_membership(project)
751
        if m is None:
752
            return _('Not a member')
753
        else:
754
            return m.user_friendly_state_display()
755

    
756
    def non_owner_can_view(self, maybe_project):
757
        if self.is_project_admin():
758
            return True
759
        if maybe_project is None:
760
            return False
761
        project = maybe_project
762
        if self.is_associated(project):
763
            return True
764
        if project.is_deactivated():
765
            return False
766
        return True
767

    
768
    def settings(self):
769
        return UserSetting.objects.filter(user=self)
770

    
771
    def all_quotas(self):
772
        quotas = users_quotas([self])
773
        try:
774
            return quotas[self.uuid]
775
        except:
776
            raise ValueError("could not compute quotas")
777

    
778

    
779
SYSTEM = 'system'
780

    
781
def initial_quotas(users):
782
    initial = {}
783
    default_quotas = get_default_quota()
784

    
785
    for user in users:
786
        uuid = user.uuid
787
        source_quota = {SYSTEM: dict(default_quotas)}
788
        initial[uuid] = source_quota
789

    
790
    objs = AstakosUserQuota.objects.select_related()
791
    orig_quotas = objs.filter(user__in=users)
792
    for user_quota in orig_quotas:
793
        uuid = user_quota.user.uuid
794
        user_init = initial.get(uuid, {})
795
        resource = user_quota.resource.full_name()
796
        user_init[resource] = user_quota.capacity
797
        initial[uuid] = user_init
798

    
799
    return initial
800

    
801

    
802
def get_grant_source(grant):
803
    return SYSTEM
804

    
805

    
806
def users_quotas(users, initial=None):
807
    if initial is None:
808
        quotas = initial_quotas(users)
809
    else:
810
        quotas = copy.deepcopy(initial)
811

    
812
    ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED
813
    objs = ProjectMembership.objects.select_related('project', 'person')
814
    memberships = objs.filter(person__in=users,
815
                              state__in=ACTUALLY_ACCEPTED,
816
                              project__state=Project.APPROVED)
817

    
818
    project_ids = set(m.project_id for m in memberships)
819
    objs = ProjectApplication.objects.select_related('project')
820
    apps = objs.filter(project__in=project_ids)
821

    
822
    project_dict = {}
823
    for app in apps:
824
        project_dict[app.project] = app
825

    
826
    objs = ProjectResourceGrant.objects.select_related()
827
    grants = objs.filter(project_application__in=apps)
828

    
829
    for membership in memberships:
830
        uuid = membership.person.uuid
831
        userquotas = quotas.get(uuid, {})
832

    
833
        application = project_dict[membership.project]
834

    
835
        for grant in grants:
836
            if grant.project_application_id != application.id:
837
                continue
838

    
839
            source = get_grant_source(grant)
840
            source_quotas = userquotas.get(source, {})
841

    
842
            resource = grant.resource.full_name()
843
            prev = source_quotas.get(resource, 0)
844
            new = prev + grant.member_capacity
845
            source_quotas[resource] = new
846
            userquotas[source] = source_quotas
847
        quotas[uuid] = userquotas
848

    
849
    return quotas
850

    
851

    
852
class AstakosUserAuthProviderManager(models.Manager):
853

    
854
    def active(self, **filters):
855
        return self.filter(active=True, **filters)
856

    
857
    def remove_unverified_providers(self, provider, **filters):
858
        try:
859
            existing = self.filter(module=provider, user__email_verified=False,
860
                                   **filters)
861
            for p in existing:
862
                p.user.delete()
863
        except:
864
            pass
865

    
866
    def unverified(self, provider, **filters):
867
        try:
868
            return self.get(module=provider, user__email_verified=False,
869
                            **filters).settings
870
        except AstakosUserAuthProvider.DoesNotExist:
871
            return None
872

    
873
    def verified(self, provider, **filters):
874
        try:
875
            return self.get(module=provider, user__email_verified=True,
876
                            **filters).settings
877
        except AstakosUserAuthProvider.DoesNotExist:
878
            return None
879

    
880

    
881
class AuthProviderPolicyProfileManager(models.Manager):
882

    
883
    def active(self):
884
        return self.filter(active=True)
885

    
886
    def for_user(self, user, provider):
887
        policies = {}
888
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
889
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
890
        exclusive_q = exclusive_q1 | exclusive_q2
891

    
892
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
893
            policies.update(profile.policies)
894

    
895
        user_groups = user.groups.all().values('pk')
896
        for profile in self.active().filter(groups__in=user_groups).filter(
897
                exclusive_q):
898
            policies.update(profile.policies)
899
        return policies
900

    
901
    def add_policy(self, name, provider, group_or_user, exclusive=False,
902
                   **policies):
903
        is_group = isinstance(group_or_user, Group)
904
        profile, created = self.get_or_create(name=name, provider=provider,
905
                                              is_exclusive=exclusive)
906
        profile.is_exclusive = exclusive
907
        profile.save()
908
        if is_group:
909
            profile.groups.add(group_or_user)
910
        else:
911
            profile.users.add(group_or_user)
912
        profile.set_policies(policies)
913
        profile.save()
914
        return profile
915

    
916

    
917
class AuthProviderPolicyProfile(models.Model):
918
    name = models.CharField(_('Name'), max_length=255, blank=False,
919
                            null=False, db_index=True)
920
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
921
                                null=False)
922

    
923
    # apply policies to all providers excluding the one set in provider field
924
    is_exclusive = models.BooleanField(default=False)
925

    
926
    policy_add = models.NullBooleanField(null=True, default=None)
927
    policy_remove = models.NullBooleanField(null=True, default=None)
928
    policy_create = models.NullBooleanField(null=True, default=None)
929
    policy_login = models.NullBooleanField(null=True, default=None)
930
    policy_limit = models.IntegerField(null=True, default=None)
931
    policy_required = models.NullBooleanField(null=True, default=None)
932
    policy_automoderate = models.NullBooleanField(null=True, default=None)
933
    policy_switch = models.NullBooleanField(null=True, default=None)
934

    
935
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
936
                     'automoderate')
937

    
938
    priority = models.IntegerField(null=False, default=1)
939
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
940
    users = models.ManyToManyField(AstakosUser,
941
                                   related_name='authpolicy_profiles')
942
    active = models.BooleanField(default=True)
943

    
944
    objects = AuthProviderPolicyProfileManager()
945

    
946
    class Meta:
947
        ordering = ['priority']
948

    
949
    @property
950
    def policies(self):
951
        policies = {}
952
        for pkey in self.POLICY_FIELDS:
953
            value = getattr(self, 'policy_%s' % pkey, None)
954
            if value is None:
955
                continue
956
            policies[pkey] = value
957
        return policies
958

    
959
    def set_policies(self, policies_dict):
960
        for key, value in policies_dict.iteritems():
961
            if key in self.POLICY_FIELDS:
962
                setattr(self, 'policy_%s' % key, value)
963
        return self.policies
964

    
965

    
966
class AstakosUserAuthProvider(models.Model):
967
    """
968
    Available user authentication methods.
969
    """
970
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
971
                                   null=True, default=None)
972
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
973
    module = models.CharField(_('Provider'), max_length=255, blank=False,
974
                                default='local')
975
    identifier = models.CharField(_('Third-party identifier'),
976
                                              max_length=255, null=True,
977
                                              blank=True)
978
    active = models.BooleanField(default=True)
979
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
980
                                   default='astakos')
981
    info_data = models.TextField(default="", null=True, blank=True)
982
    created = models.DateTimeField('Creation date', auto_now_add=True)
983

    
984
    objects = AstakosUserAuthProviderManager()
985

    
986
    class Meta:
987
        unique_together = (('identifier', 'module', 'user'), )
988
        ordering = ('module', 'created')
989

    
990
    def __init__(self, *args, **kwargs):
991
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
992
        try:
993
            self.info = json.loads(self.info_data)
994
            if not self.info:
995
                self.info = {}
996
        except Exception, e:
997
            self.info = {}
998

    
999
        for key,value in self.info.iteritems():
1000
            setattr(self, 'info_%s' % key, value)
1001

    
1002
    @property
1003
    def settings(self):
1004
        extra_data = {}
1005

    
1006
        info_data = {}
1007
        if self.info_data:
1008
            info_data = json.loads(self.info_data)
1009

    
1010
        extra_data['info'] = info_data
1011

    
1012
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
1013
            extra_data[key] = getattr(self, key)
1014

    
1015
        extra_data['instance'] = self
1016
        return auth.get_provider(self.module, self.user,
1017
                                           self.identifier, **extra_data)
1018

    
1019
    def __repr__(self):
1020
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
1021

    
1022
    def __unicode__(self):
1023
        if self.identifier:
1024
            return "%s:%s" % (self.module, self.identifier)
1025
        if self.auth_backend:
1026
            return "%s:%s" % (self.module, self.auth_backend)
1027
        return self.module
1028

    
1029
    def save(self, *args, **kwargs):
1030
        self.info_data = json.dumps(self.info)
1031
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1032

    
1033

    
1034
class ExtendedManager(models.Manager):
1035
    def _update_or_create(self, **kwargs):
1036
        assert kwargs, \
1037
            'update_or_create() must be passed at least one keyword argument'
1038
        obj, created = self.get_or_create(**kwargs)
1039
        defaults = kwargs.pop('defaults', {})
1040
        if created:
1041
            return obj, True, False
1042
        else:
1043
            try:
1044
                params = dict(
1045
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
1046
                params.update(defaults)
1047
                for attr, val in params.items():
1048
                    if hasattr(obj, attr):
1049
                        setattr(obj, attr, val)
1050
                sid = transaction.savepoint()
1051
                obj.save(force_update=True)
1052
                transaction.savepoint_commit(sid)
1053
                return obj, False, True
1054
            except IntegrityError, e:
1055
                transaction.savepoint_rollback(sid)
1056
                try:
1057
                    return self.get(**kwargs), False, False
1058
                except self.model.DoesNotExist:
1059
                    raise e
1060

    
1061
    update_or_create = _update_or_create
1062

    
1063

    
1064
class AstakosUserQuota(models.Model):
1065
    objects = ExtendedManager()
1066
    capacity = intDecimalField()
1067
    quantity = intDecimalField(default=0)
1068
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1069
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1070
    resource = models.ForeignKey(Resource)
1071
    user = models.ForeignKey(AstakosUser)
1072

    
1073
    class Meta:
1074
        unique_together = ("resource", "user")
1075

    
1076

    
1077
class ApprovalTerms(models.Model):
1078
    """
1079
    Model for approval terms
1080
    """
1081

    
1082
    date = models.DateTimeField(
1083
        _('Issue date'), db_index=True, auto_now_add=True)
1084
    location = models.CharField(_('Terms location'), max_length=255)
1085

    
1086

    
1087
class Invitation(models.Model):
1088
    """
1089
    Model for registring invitations
1090
    """
1091
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1092
                                null=True)
1093
    realname = models.CharField(_('Real name'), max_length=255)
1094
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1095
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1096
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1097
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1098
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1099

    
1100
    def __init__(self, *args, **kwargs):
1101
        super(Invitation, self).__init__(*args, **kwargs)
1102
        if not self.id:
1103
            self.code = _generate_invitation_code()
1104

    
1105
    def consume(self):
1106
        self.is_consumed = True
1107
        self.consumed = datetime.now()
1108
        self.save()
1109

    
1110
    def __unicode__(self):
1111
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1112

    
1113

    
1114
class EmailChangeManager(models.Manager):
1115

    
1116
    @transaction.commit_on_success
1117
    def change_email(self, activation_key):
1118
        """
1119
        Validate an activation key and change the corresponding
1120
        ``User`` if valid.
1121

1122
        If the key is valid and has not expired, return the ``User``
1123
        after activating.
1124

1125
        If the key is not valid or has expired, return ``None``.
1126

1127
        If the key is valid but the ``User`` is already active,
1128
        return ``None``.
1129

1130
        After successful email change the activation record is deleted.
1131

1132
        Throws ValueError if there is already
1133
        """
1134
        try:
1135
            email_change = self.model.objects.get(
1136
                activation_key=activation_key)
1137
            if email_change.activation_key_expired():
1138
                email_change.delete()
1139
                raise EmailChange.DoesNotExist
1140
            # is there an active user with this address?
1141
            try:
1142
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1143
            except AstakosUser.DoesNotExist:
1144
                pass
1145
            else:
1146
                raise ValueError(_('The new email address is reserved.'))
1147
            # update user
1148
            user = AstakosUser.objects.get(pk=email_change.user_id)
1149
            old_email = user.email
1150
            user.email = email_change.new_email_address
1151
            user.save()
1152
            email_change.delete()
1153
            msg = "User %s changed email from %s to %s" % (user.log_display,
1154
                                                           old_email,
1155
                                                           user.email)
1156
            logger.log(LOGGING_LEVEL, msg)
1157
            return user
1158
        except EmailChange.DoesNotExist:
1159
            raise ValueError(_('Invalid activation key.'))
1160

    
1161

    
1162
class EmailChange(models.Model):
1163
    new_email_address = models.EmailField(
1164
        _(u'new e-mail address'),
1165
        help_text=_('Provide a new email address. Until you verify the new '
1166
                    'address by following the activation link that will be '
1167
                    'sent to it, your old email address will remain active.'))
1168
    user = models.ForeignKey(
1169
        AstakosUser, unique=True, related_name='emailchanges')
1170
    requested_at = models.DateTimeField(auto_now_add=True)
1171
    activation_key = models.CharField(
1172
        max_length=40, unique=True, db_index=True)
1173

    
1174
    objects = EmailChangeManager()
1175

    
1176
    def get_url(self):
1177
        return reverse('email_change_confirm',
1178
                      kwargs={'activation_key': self.activation_key})
1179

    
1180
    def activation_key_expired(self):
1181
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1182
        return self.requested_at + expiration_date < datetime.now()
1183

    
1184

    
1185
class AdditionalMail(models.Model):
1186
    """
1187
    Model for registring invitations
1188
    """
1189
    owner = models.ForeignKey(AstakosUser)
1190
    email = models.EmailField()
1191

    
1192

    
1193
def _generate_invitation_code():
1194
    while True:
1195
        code = randint(1, 2L ** 63 - 1)
1196
        try:
1197
            Invitation.objects.get(code=code)
1198
            # An invitation with this code already exists, try again
1199
        except Invitation.DoesNotExist:
1200
            return code
1201

    
1202

    
1203
def get_latest_terms():
1204
    try:
1205
        term = ApprovalTerms.objects.order_by('-id')[0]
1206
        return term
1207
    except IndexError:
1208
        pass
1209
    return None
1210

    
1211

    
1212
class PendingThirdPartyUser(models.Model):
1213
    """
1214
    Model for registring successful third party user authentications
1215
    """
1216
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1217
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1218
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1219
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1220
                                  null=True)
1221
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1222
                                 null=True)
1223
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1224
                                   null=True)
1225
    username = models.CharField(_('username'), max_length=30, unique=True,
1226
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1227
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1228
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1229
    info = models.TextField(default="", null=True, blank=True)
1230

    
1231
    class Meta:
1232
        unique_together = ("provider", "third_party_identifier")
1233

    
1234
    def get_user_instance(self):
1235
        d = self.__dict__
1236
        d.pop('_state', None)
1237
        d.pop('id', None)
1238
        d.pop('token', None)
1239
        d.pop('created', None)
1240
        d.pop('info', None)
1241
        user = AstakosUser(**d)
1242

    
1243
        return user
1244

    
1245
    @property
1246
    def realname(self):
1247
        return '%s %s' %(self.first_name, self.last_name)
1248

    
1249
    @realname.setter
1250
    def realname(self, value):
1251
        parts = value.split(' ')
1252
        if len(parts) == 2:
1253
            self.first_name = parts[0]
1254
            self.last_name = parts[1]
1255
        else:
1256
            self.last_name = parts[0]
1257

    
1258
    def save(self, **kwargs):
1259
        if not self.id:
1260
            # set username
1261
            while not self.username:
1262
                username =  uuid.uuid4().hex[:30]
1263
                try:
1264
                    AstakosUser.objects.get(username = username)
1265
                except AstakosUser.DoesNotExist, e:
1266
                    self.username = username
1267
        super(PendingThirdPartyUser, self).save(**kwargs)
1268

    
1269
    def generate_token(self):
1270
        self.password = self.third_party_identifier
1271
        self.last_login = datetime.now()
1272
        self.token = default_token_generator.make_token(self)
1273

    
1274
    def existing_user(self):
1275
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1276
                                         auth_providers__identifier=self.third_party_identifier)
1277

    
1278
    def get_provider(self, user):
1279
        params = {
1280
            'info_data': self.info,
1281
            'affiliation': self.affiliation
1282
        }
1283
        return auth.get_provider(self.provider, user,
1284
                                 self.third_party_identifier, **params)
1285

    
1286
class SessionCatalog(models.Model):
1287
    session_key = models.CharField(_('session key'), max_length=40)
1288
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1289

    
1290

    
1291
class UserSetting(models.Model):
1292
    user = models.ForeignKey(AstakosUser)
1293
    setting = models.CharField(max_length=255)
1294
    value = models.IntegerField()
1295

    
1296
    objects = ForUpdateManager()
1297

    
1298
    class Meta:
1299
        unique_together = ("user", "setting")
1300

    
1301

    
1302
### PROJECTS ###
1303
################
1304

    
1305
class ChainManager(ForUpdateManager):
1306

    
1307
    def search_by_name(self, *search_strings):
1308
        projects = Project.objects.search_by_name(*search_strings)
1309
        chains = [p.id for p in projects]
1310
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1311
        apps = (app for app in apps if app.is_latest())
1312
        app_chains = [app.chain for app in apps if app.chain not in chains]
1313
        return chains + app_chains
1314

    
1315
    def all_full_state(self):
1316
        chains = self.all()
1317
        cids = [c.chain for c in chains]
1318
        projects = Project.objects.select_related('application').in_bulk(cids)
1319

    
1320
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1321
        chain_latest = dict(objs.values_list('chain', 'latest'))
1322

    
1323
        objs = ProjectApplication.objects.select_related('applicant')
1324
        apps = objs.in_bulk(chain_latest.values())
1325

    
1326
        d = {}
1327
        for chain in chains:
1328
            pk = chain.pk
1329
            project = projects.get(pk, None)
1330
            app = apps[chain_latest[pk]]
1331
            d[chain.pk] = chain.get_state(project, app)
1332

    
1333
        return d
1334

    
1335
    def of_project(self, project):
1336
        if project is None:
1337
            return None
1338
        try:
1339
            return self.get(chain=project.id)
1340
        except Chain.DoesNotExist:
1341
            raise AssertionError('project with no chain')
1342

    
1343

    
1344
class Chain(models.Model):
1345
    chain  =   models.AutoField(primary_key=True)
1346

    
1347
    def __str__(self):
1348
        return "%s" % (self.chain,)
1349

    
1350
    objects = ChainManager()
1351

    
1352
    PENDING            = 0
1353
    DENIED             = 3
1354
    DISMISSED          = 4
1355
    CANCELLED          = 5
1356

    
1357
    APPROVED           = 10
1358
    APPROVED_PENDING   = 11
1359
    SUSPENDED          = 12
1360
    SUSPENDED_PENDING  = 13
1361
    TERMINATED         = 14
1362
    TERMINATED_PENDING = 15
1363

    
1364
    PENDING_STATES = [PENDING,
1365
                      APPROVED_PENDING,
1366
                      SUSPENDED_PENDING,
1367
                      TERMINATED_PENDING,
1368
                      ]
1369

    
1370
    MODIFICATION_STATES = [APPROVED_PENDING,
1371
                           SUSPENDED_PENDING,
1372
                           TERMINATED_PENDING,
1373
                           ]
1374

    
1375
    RELEVANT_STATES = [PENDING,
1376
                       DENIED,
1377
                       APPROVED,
1378
                       APPROVED_PENDING,
1379
                       SUSPENDED,
1380
                       SUSPENDED_PENDING,
1381
                       TERMINATED_PENDING,
1382
                       ]
1383

    
1384
    SKIP_STATES = [DISMISSED,
1385
                   CANCELLED,
1386
                   TERMINATED]
1387

    
1388
    STATE_DISPLAY = {
1389
        PENDING            : _("Pending"),
1390
        DENIED             : _("Denied"),
1391
        DISMISSED          : _("Dismissed"),
1392
        CANCELLED          : _("Cancelled"),
1393
        APPROVED           : _("Active"),
1394
        APPROVED_PENDING   : _("Active - Pending"),
1395
        SUSPENDED          : _("Suspended"),
1396
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1397
        TERMINATED         : _("Terminated"),
1398
        TERMINATED_PENDING : _("Terminated - Pending"),
1399
        }
1400

    
1401

    
1402
    @classmethod
1403
    def _chain_state(cls, project_state, app_state):
1404
        s = CHAIN_STATE.get((project_state, app_state), None)
1405
        if s is None:
1406
            raise AssertionError('inconsistent chain state')
1407
        return s
1408

    
1409
    @classmethod
1410
    def chain_state(cls, project, app):
1411
        p_state = project.state if project else None
1412
        return cls._chain_state(p_state, app.state)
1413

    
1414
    @classmethod
1415
    def state_display(cls, s):
1416
        if s is None:
1417
            return _("Unknown")
1418
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1419

    
1420
    def last_application(self):
1421
        return self.chained_apps.order_by('-id')[0]
1422

    
1423
    def get_project(self):
1424
        try:
1425
            return self.chained_project
1426
        except Project.DoesNotExist:
1427
            return None
1428

    
1429
    def get_elements(self):
1430
        project = self.get_project()
1431
        app = self.last_application()
1432
        return project, app
1433

    
1434
    def get_state(self, project, app):
1435
        s = self.chain_state(project, app)
1436
        return s, project, app
1437

    
1438
    def full_state(self):
1439
        project, app = self.get_elements()
1440
        return self.get_state(project, app)
1441

    
1442

    
1443
def new_chain():
1444
    c = Chain.objects.create()
1445
    return c
1446

    
1447

    
1448
class ProjectApplicationManager(ForUpdateManager):
1449

    
1450
    def user_visible_projects(self, *filters, **kw_filters):
1451
        model = self.model
1452
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1453

    
1454
    def user_visible_by_chain(self, flt):
1455
        model = self.model
1456
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1457
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1458
        by_chain = dict(pending.annotate(models.Max('id')))
1459
        by_chain.update(approved.annotate(models.Max('id')))
1460
        return self.filter(flt, id__in=by_chain.values())
1461

    
1462
    def user_accessible_projects(self, user):
1463
        """
1464
        Return projects accessed by specified user.
1465
        """
1466
        if user.is_project_admin():
1467
            participates_filters = Q()
1468
        else:
1469
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1470
                                   Q(project__projectmembership__person=user)
1471

    
1472
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1473

    
1474
    def search_by_name(self, *search_strings):
1475
        q = Q()
1476
        for s in search_strings:
1477
            q = q | Q(name__icontains=s)
1478
        return self.filter(q)
1479

    
1480
    def latest_of_chain(self, chain_id):
1481
        try:
1482
            return self.filter(chain=chain_id).order_by('-id')[0]
1483
        except IndexError:
1484
            return None
1485

    
1486

    
1487
class ProjectApplication(models.Model):
1488
    applicant               =   models.ForeignKey(
1489
                                    AstakosUser,
1490
                                    related_name='projects_applied',
1491
                                    db_index=True)
1492

    
1493
    PENDING     =    0
1494
    APPROVED    =    1
1495
    REPLACED    =    2
1496
    DENIED      =    3
1497
    DISMISSED   =    4
1498
    CANCELLED   =    5
1499

    
1500
    state                   =   models.IntegerField(default=PENDING,
1501
                                                    db_index=True)
1502

    
1503
    owner                   =   models.ForeignKey(
1504
                                    AstakosUser,
1505
                                    related_name='projects_owned',
1506
                                    db_index=True)
1507

    
1508
    chain                   =   models.ForeignKey(Chain,
1509
                                                  related_name='chained_apps',
1510
                                                  db_column='chain')
1511
    precursor_application   =   models.ForeignKey('ProjectApplication',
1512
                                                  null=True,
1513
                                                  blank=True)
1514

    
1515
    name                    =   models.CharField(max_length=80)
1516
    homepage                =   models.URLField(max_length=255, null=True,
1517
                                                verify_exists=False)
1518
    description             =   models.TextField(null=True, blank=True)
1519
    start_date              =   models.DateTimeField(null=True, blank=True)
1520
    end_date                =   models.DateTimeField()
1521
    member_join_policy      =   models.IntegerField()
1522
    member_leave_policy     =   models.IntegerField()
1523
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1524
    resource_grants         =   models.ManyToManyField(
1525
                                    Resource,
1526
                                    null=True,
1527
                                    blank=True,
1528
                                    through='ProjectResourceGrant')
1529
    comments                =   models.TextField(null=True, blank=True)
1530
    issue_date              =   models.DateTimeField(auto_now_add=True)
1531
    response_date           =   models.DateTimeField(null=True, blank=True)
1532
    response                =   models.TextField(null=True, blank=True)
1533

    
1534
    objects                 =   ProjectApplicationManager()
1535

    
1536
    # Compiled queries
1537
    Q_PENDING  = Q(state=PENDING)
1538
    Q_APPROVED = Q(state=APPROVED)
1539
    Q_DENIED   = Q(state=DENIED)
1540

    
1541
    class Meta:
1542
        unique_together = ("chain", "id")
1543

    
1544
    def __unicode__(self):
1545
        return "%s applied by %s" % (self.name, self.applicant)
1546

    
1547
    # TODO: Move to a more suitable place
1548
    APPLICATION_STATE_DISPLAY = {
1549
        PENDING  : _('Pending review'),
1550
        APPROVED : _('Approved'),
1551
        REPLACED : _('Replaced'),
1552
        DENIED   : _('Denied'),
1553
        DISMISSED: _('Dismissed'),
1554
        CANCELLED: _('Cancelled')
1555
    }
1556

    
1557
    @property
1558
    def log_display(self):
1559
        return "application %s (%s) for project %s" % (
1560
            self.id, self.name, self.chain)
1561

    
1562
    def get_project(self):
1563
        try:
1564
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1565
            return Project
1566
        except Project.DoesNotExist, e:
1567
            return None
1568

    
1569
    def state_display(self):
1570
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1571

    
1572
    def project_state_display(self):
1573
        try:
1574
            project = self.project
1575
            return project.state_display()
1576
        except Project.DoesNotExist:
1577
            return self.state_display()
1578

    
1579
    def add_resource_policy(self, service, resource, uplimit):
1580
        """Raises ObjectDoesNotExist, IntegrityError"""
1581
        q = self.projectresourcegrant_set
1582
        resource = Resource.objects.get(service__name=service, name=resource)
1583
        q.create(resource=resource, member_capacity=uplimit)
1584

    
1585
    def members_count(self):
1586
        return self.project.approved_memberships.count()
1587

    
1588
    @property
1589
    def grants(self):
1590
        return self.projectresourcegrant_set.values(
1591
            'member_capacity', 'resource__name', 'resource__service__name')
1592

    
1593
    @property
1594
    def resource_policies(self):
1595
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1596

    
1597
    @resource_policies.setter
1598
    def resource_policies(self, policies):
1599
        for p in policies:
1600
            service = p.get('service', None)
1601
            resource = p.get('resource', None)
1602
            uplimit = p.get('uplimit', 0)
1603
            self.add_resource_policy(service, resource, uplimit)
1604

    
1605
    def pending_modifications_incl_me(self):
1606
        q = self.chained_applications()
1607
        q = q.filter(Q(state=self.PENDING))
1608
        return q
1609

    
1610
    def last_pending_incl_me(self):
1611
        try:
1612
            return self.pending_modifications_incl_me().order_by('-id')[0]
1613
        except IndexError:
1614
            return None
1615

    
1616
    def pending_modifications(self):
1617
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1618

    
1619
    def last_pending(self):
1620
        try:
1621
            return self.pending_modifications().order_by('-id')[0]
1622
        except IndexError:
1623
            return None
1624

    
1625
    def is_modification(self):
1626
        # if self.state != self.PENDING:
1627
        #     return False
1628
        parents = self.chained_applications().filter(id__lt=self.id)
1629
        parents = parents.filter(state__in=[self.APPROVED])
1630
        return parents.count() > 0
1631

    
1632
    def chained_applications(self):
1633
        return ProjectApplication.objects.filter(chain=self.chain)
1634

    
1635
    def is_latest(self):
1636
        return self.chained_applications().order_by('-id')[0] == self
1637

    
1638
    def has_pending_modifications(self):
1639
        return bool(self.last_pending())
1640

    
1641
    def denied_modifications(self):
1642
        q = self.chained_applications()
1643
        q = q.filter(Q(state=self.DENIED))
1644
        q = q.filter(~Q(id=self.id))
1645
        return q
1646

    
1647
    def last_denied(self):
1648
        try:
1649
            return self.denied_modifications().order_by('-id')[0]
1650
        except IndexError:
1651
            return None
1652

    
1653
    def has_denied_modifications(self):
1654
        return bool(self.last_denied())
1655

    
1656
    def is_applied(self):
1657
        try:
1658
            self.project
1659
            return True
1660
        except Project.DoesNotExist:
1661
            return False
1662

    
1663
    def get_project(self):
1664
        try:
1665
            return Project.objects.get(id=self.chain)
1666
        except Project.DoesNotExist:
1667
            return None
1668

    
1669
    def project_exists(self):
1670
        return self.get_project() is not None
1671

    
1672
    def _get_project_for_update(self):
1673
        try:
1674
            objects = Project.objects
1675
            project = objects.get_for_update(id=self.chain)
1676
            return project
1677
        except Project.DoesNotExist:
1678
            return None
1679

    
1680
    def can_cancel(self):
1681
        return self.state == self.PENDING
1682

    
1683
    def cancel(self):
1684
        if not self.can_cancel():
1685
            m = _("cannot cancel: application '%s' in state '%s'") % (
1686
                    self.id, self.state)
1687
            raise AssertionError(m)
1688

    
1689
        self.state = self.CANCELLED
1690
        self.save()
1691

    
1692
    def can_dismiss(self):
1693
        return self.state == self.DENIED
1694

    
1695
    def dismiss(self):
1696
        if not self.can_dismiss():
1697
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1698
                    self.id, self.state)
1699
            raise AssertionError(m)
1700

    
1701
        self.state = self.DISMISSED
1702
        self.save()
1703

    
1704
    def can_deny(self):
1705
        return self.state == self.PENDING
1706

    
1707
    def deny(self, reason):
1708
        if not self.can_deny():
1709
            m = _("cannot deny: application '%s' in state '%s'") % (
1710
                    self.id, self.state)
1711
            raise AssertionError(m)
1712

    
1713
        self.state = self.DENIED
1714
        self.response_date = datetime.now()
1715
        self.response = reason
1716
        self.save()
1717

    
1718
    def can_approve(self):
1719
        return self.state == self.PENDING
1720

    
1721
    def approve(self, approval_user=None):
1722
        """
1723
        If approval_user then during owner membership acceptance
1724
        it is checked whether the request_user is eligible.
1725

1726
        Raises:
1727
            PermissionDenied
1728
        """
1729

    
1730
        if not transaction.is_managed():
1731
            raise AssertionError("NOPE")
1732

    
1733
        new_project_name = self.name
1734
        if not self.can_approve():
1735
            m = _("cannot approve: project '%s' in state '%s'") % (
1736
                    new_project_name, self.state)
1737
            raise AssertionError(m) # invalid argument
1738

    
1739
        now = datetime.now()
1740
        project = self._get_project_for_update()
1741

    
1742
        try:
1743
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1744
            conflicting_project = Project.objects.get(q)
1745
            if (conflicting_project != project):
1746
                m = (_("cannot approve: project with name '%s' "
1747
                       "already exists (id: %s)") % (
1748
                        new_project_name, conflicting_project.id))
1749
                raise PermissionDenied(m) # invalid argument
1750
        except Project.DoesNotExist:
1751
            pass
1752

    
1753
        new_project = False
1754
        if project is None:
1755
            new_project = True
1756
            project = Project(id=self.chain)
1757

    
1758
        project.name = new_project_name
1759
        project.application = self
1760
        project.last_approval_date = now
1761
        if not new_project:
1762
            project.is_modified = True
1763

    
1764
        project.save()
1765

    
1766
        self.state = self.APPROVED
1767
        self.response_date = now
1768
        self.save()
1769
        return project
1770

    
1771
    @property
1772
    def member_join_policy_display(self):
1773
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1774

    
1775
    @property
1776
    def member_leave_policy_display(self):
1777
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1778

    
1779
class ProjectResourceGrant(models.Model):
1780

    
1781
    resource                =   models.ForeignKey(Resource)
1782
    project_application     =   models.ForeignKey(ProjectApplication,
1783
                                                  null=True)
1784
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1785
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1786
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1787
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1788
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1789
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1790

    
1791
    objects = ExtendedManager()
1792

    
1793
    class Meta:
1794
        unique_together = ("resource", "project_application")
1795

    
1796
    def display_member_capacity(self):
1797
        if self.member_capacity:
1798
            if self.resource.unit:
1799
                return ProjectResourceGrant.display_filesize(
1800
                    self.member_capacity)
1801
            else:
1802
                if math.isinf(self.member_capacity):
1803
                    return 'Unlimited'
1804
                else:
1805
                    return self.member_capacity
1806
        else:
1807
            return 'Unlimited'
1808

    
1809
    def __str__(self):
1810
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1811
                                        self.display_member_capacity())
1812

    
1813
    @classmethod
1814
    def display_filesize(cls, value):
1815
        try:
1816
            value = float(value)
1817
        except:
1818
            return
1819
        else:
1820
            if math.isinf(value):
1821
                return 'Unlimited'
1822
            if value > 1:
1823
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1824
                                [0, 0, 0, 0, 0, 0])
1825
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1826
                quotient = float(value) / 1024**exponent
1827
                unit, value_decimals = unit_list[exponent]
1828
                format_string = '{0:.%sf} {1}' % (value_decimals)
1829
                return format_string.format(quotient, unit)
1830
            if value == 0:
1831
                return '0 bytes'
1832
            if value == 1:
1833
                return '1 byte'
1834
            else:
1835
               return '0'
1836

    
1837

    
1838
class ProjectManager(ForUpdateManager):
1839

    
1840
    def terminated_projects(self):
1841
        q = self.model.Q_TERMINATED
1842
        return self.filter(q)
1843

    
1844
    def not_terminated_projects(self):
1845
        q = ~self.model.Q_TERMINATED
1846
        return self.filter(q)
1847

    
1848
    def deactivated_projects(self):
1849
        q = self.model.Q_DEACTIVATED
1850
        return self.filter(q)
1851

    
1852
    def modified_projects(self):
1853
        return self.filter(is_modified=True)
1854

    
1855
    def expired_projects(self):
1856
        q = (~Q(state=Project.TERMINATED) &
1857
              Q(application__end_date__lt=datetime.now()))
1858
        return self.filter(q)
1859

    
1860
    def search_by_name(self, *search_strings):
1861
        q = Q()
1862
        for s in search_strings:
1863
            q = q | Q(name__icontains=s)
1864
        return self.filter(q)
1865

    
1866

    
1867
class Project(models.Model):
1868

    
1869
    id                          =   models.OneToOneField(Chain,
1870
                                                      related_name='chained_project',
1871
                                                      db_column='id',
1872
                                                      primary_key=True)
1873

    
1874
    application                 =   models.OneToOneField(
1875
                                            ProjectApplication,
1876
                                            related_name='project')
1877
    last_approval_date          =   models.DateTimeField(null=True)
1878

    
1879
    members                     =   models.ManyToManyField(
1880
                                            AstakosUser,
1881
                                            through='ProjectMembership')
1882

    
1883
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1884
    deactivation_date           =   models.DateTimeField(null=True)
1885

    
1886
    creation_date               =   models.DateTimeField(auto_now_add=True)
1887
    name                        =   models.CharField(
1888
                                            max_length=80,
1889
                                            null=True,
1890
                                            db_index=True,
1891
                                            unique=True)
1892

    
1893
    APPROVED    = 1
1894
    SUSPENDED   = 10
1895
    TERMINATED  = 100
1896

    
1897
    is_modified                 =   models.BooleanField(default=False,
1898
                                                        db_index=True)
1899
    is_active                   =   models.BooleanField(default=True,
1900
                                                        db_index=True)
1901
    state                       =   models.IntegerField(default=APPROVED,
1902
                                                        db_index=True)
1903

    
1904
    objects     =   ProjectManager()
1905

    
1906
    # Compiled queries
1907
    Q_TERMINATED  = Q(state=TERMINATED)
1908
    Q_SUSPENDED   = Q(state=SUSPENDED)
1909
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1910

    
1911
    def __str__(self):
1912
        return uenc(_("<project %s '%s'>") %
1913
                    (self.id, udec(self.application.name)))
1914

    
1915
    __repr__ = __str__
1916

    
1917
    def __unicode__(self):
1918
        return _("<project %s '%s'>") % (self.id, self.application.name)
1919

    
1920
    STATE_DISPLAY = {
1921
        APPROVED   : 'Active',
1922
        SUSPENDED  : 'Suspended',
1923
        TERMINATED : 'Terminated'
1924
        }
1925

    
1926
    def state_display(self):
1927
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1928

    
1929
    def expiration_info(self):
1930
        return (str(self.id), self.name, self.state_display(),
1931
                str(self.application.end_date))
1932

    
1933
    def is_deactivated(self, reason=None):
1934
        if reason is not None:
1935
            return self.state == reason
1936

    
1937
        return self.state != self.APPROVED
1938

    
1939
    ### Deactivation calls
1940

    
1941
    def terminate(self):
1942
        self.deactivation_reason = 'TERMINATED'
1943
        self.deactivation_date = datetime.now()
1944
        self.state = self.TERMINATED
1945
        self.name = None
1946
        self.save()
1947

    
1948
    def suspend(self):
1949
        self.deactivation_reason = 'SUSPENDED'
1950
        self.deactivation_date = datetime.now()
1951
        self.state = self.SUSPENDED
1952
        self.save()
1953

    
1954
    def resume(self):
1955
        self.deactivation_reason = None
1956
        self.deactivation_date = None
1957
        self.state = self.APPROVED
1958
        self.save()
1959

    
1960
    ### Logical checks
1961

    
1962
    def is_inconsistent(self):
1963
        now = datetime.now()
1964
        dates = [self.creation_date,
1965
                 self.last_approval_date,
1966
                 self.deactivation_date]
1967
        return any([date > now for date in dates])
1968

    
1969
    def is_active_strict(self):
1970
        return self.is_active and self.state == self.APPROVED
1971

    
1972
    def is_approved(self):
1973
        return self.state == self.APPROVED
1974

    
1975
    @property
1976
    def is_alive(self):
1977
        return not self.is_terminated
1978

    
1979
    @property
1980
    def is_terminated(self):
1981
        return self.is_deactivated(self.TERMINATED)
1982

    
1983
    @property
1984
    def is_suspended(self):
1985
        return self.is_deactivated(self.SUSPENDED)
1986

    
1987
    def violates_resource_grants(self):
1988
        return False
1989

    
1990
    def violates_members_limit(self, adding=0):
1991
        application = self.application
1992
        limit = application.limit_on_members_number
1993
        if limit is None:
1994
            return False
1995
        return (len(self.approved_members) + adding > limit)
1996

    
1997

    
1998
    ### Other
1999

    
2000
    def count_pending_memberships(self):
2001
        memb_set = self.projectmembership_set
2002
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2003
        return memb_count
2004

    
2005
    def members_count(self):
2006
        return self.approved_memberships.count()
2007

    
2008
    @property
2009
    def approved_memberships(self):
2010
        query = ProjectMembership.Q_ACCEPTED_STATES
2011
        return self.projectmembership_set.filter(query)
2012

    
2013
    @property
2014
    def approved_members(self):
2015
        return [m.person for m in self.approved_memberships]
2016

    
2017
    def add_member(self, user):
2018
        """
2019
        Raises:
2020
            django.exceptions.PermissionDenied
2021
            astakos.im.models.AstakosUser.DoesNotExist
2022
        """
2023
        if isinstance(user, (int, long)):
2024
            user = AstakosUser.objects.get(user=user)
2025

    
2026
        m, created = ProjectMembership.objects.get_or_create(
2027
            person=user, project=self
2028
        )
2029
        m.accept()
2030

    
2031
    def remove_member(self, user):
2032
        """
2033
        Raises:
2034
            django.exceptions.PermissionDenied
2035
            astakos.im.models.AstakosUser.DoesNotExist
2036
            astakos.im.models.ProjectMembership.DoesNotExist
2037
        """
2038
        if isinstance(user, (int, long)):
2039
            user = AstakosUser.objects.get(user=user)
2040

    
2041
        m = ProjectMembership.objects.get(person=user, project=self)
2042
        m.remove()
2043

    
2044

    
2045
CHAIN_STATE = {
2046
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2047
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2048
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2049
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2050
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2051

    
2052
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2053
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2054
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2055
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2056
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2057

    
2058
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2059
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2060
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2061
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2062
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2063

    
2064
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2065
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2066
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2067
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2068
    }
2069

    
2070

    
2071
class ProjectMembershipManager(ForUpdateManager):
2072

    
2073
    def any_accepted(self):
2074
        q = self.model.Q_ACTUALLY_ACCEPTED
2075
        return self.filter(q)
2076

    
2077
    def actually_accepted(self):
2078
        q = self.model.Q_ACTUALLY_ACCEPTED
2079
        return self.filter(q)
2080

    
2081
    def requested(self):
2082
        return self.filter(state=ProjectMembership.REQUESTED)
2083

    
2084
    def suspended(self):
2085
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2086

    
2087
class ProjectMembership(models.Model):
2088

    
2089
    person              =   models.ForeignKey(AstakosUser)
2090
    request_date        =   models.DateField(auto_now_add=True)
2091
    project             =   models.ForeignKey(Project)
2092

    
2093
    REQUESTED           =   0
2094
    ACCEPTED            =   1
2095
    LEAVE_REQUESTED     =   5
2096
    # User deactivation
2097
    USER_SUSPENDED      =   10
2098

    
2099
    REMOVED             =   200
2100

    
2101
    ASSOCIATED_STATES   =   set([REQUESTED,
2102
                                 ACCEPTED,
2103
                                 LEAVE_REQUESTED,
2104
                                 USER_SUSPENDED,
2105
                                 ])
2106

    
2107
    ACCEPTED_STATES     =   set([ACCEPTED,
2108
                                 LEAVE_REQUESTED,
2109
                                 USER_SUSPENDED,
2110
                                 ])
2111

    
2112
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2113

    
2114
    state               =   models.IntegerField(default=REQUESTED,
2115
                                                db_index=True)
2116
    is_pending          =   models.BooleanField(default=False, db_index=True)
2117
    is_active           =   models.BooleanField(default=False, db_index=True)
2118
    application         =   models.ForeignKey(
2119
                                ProjectApplication,
2120
                                null=True,
2121
                                related_name='memberships')
2122
    pending_application =   models.ForeignKey(
2123
                                ProjectApplication,
2124
                                null=True,
2125
                                related_name='pending_memberships')
2126
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2127

    
2128
    acceptance_date     =   models.DateField(null=True, db_index=True)
2129
    leave_request_date  =   models.DateField(null=True)
2130

    
2131
    objects     =   ProjectMembershipManager()
2132

    
2133
    # Compiled queries
2134
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2135
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2136

    
2137
    MEMBERSHIP_STATE_DISPLAY = {
2138
        REQUESTED           : _('Requested'),
2139
        ACCEPTED            : _('Accepted'),
2140
        LEAVE_REQUESTED     : _('Leave Requested'),
2141
        USER_SUSPENDED      : _('Suspended'),
2142
        REMOVED             : _('Pending removal'),
2143
        }
2144

    
2145
    USER_FRIENDLY_STATE_DISPLAY = {
2146
        REQUESTED           : _('Join requested'),
2147
        ACCEPTED            : _('Accepted member'),
2148
        LEAVE_REQUESTED     : _('Requested to leave'),
2149
        USER_SUSPENDED      : _('Suspended member'),
2150
        REMOVED             : _('Pending removal'),
2151
        }
2152

    
2153
    def state_display(self):
2154
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2155

    
2156
    def user_friendly_state_display(self):
2157
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2158

    
2159
    class Meta:
2160
        unique_together = ("person", "project")
2161
        #index_together = [["project", "state"]]
2162

    
2163
    def __str__(self):
2164
        return uenc(_("<'%s' membership in '%s'>") % (
2165
                self.person.username, self.project))
2166

    
2167
    __repr__ = __str__
2168

    
2169
    def __init__(self, *args, **kwargs):
2170
        self.state = self.REQUESTED
2171
        super(ProjectMembership, self).__init__(*args, **kwargs)
2172

    
2173
    def _set_history_item(self, reason, date=None):
2174
        if isinstance(reason, basestring):
2175
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2176

    
2177
        history_item = ProjectMembershipHistory(
2178
                            serial=self.id,
2179
                            person=self.person_id,
2180
                            project=self.project_id,
2181
                            date=date or datetime.now(),
2182
                            reason=reason)
2183
        history_item.save()
2184
        serial = history_item.id
2185

    
2186
    def can_accept(self):
2187
        return self.state == self.REQUESTED
2188

    
2189
    def accept(self):
2190
        if not self.can_accept():
2191
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2192
            raise AssertionError(m)
2193

    
2194
        now = datetime.now()
2195
        self.acceptance_date = now
2196
        self._set_history_item(reason='ACCEPT', date=now)
2197
        self.state = self.ACCEPTED
2198
        self.save()
2199

    
2200
    def can_leave(self):
2201
        return self.state in self.ACCEPTED_STATES
2202

    
2203
    def leave_request(self):
2204
        if not self.can_leave():
2205
            m = _("%s: attempt to request to leave in state '%s'") % (
2206
                self, self.state)
2207
            raise AssertionError(m)
2208

    
2209
        self.leave_request_date = datetime.now()
2210
        self.state = self.LEAVE_REQUESTED
2211
        self.save()
2212

    
2213
    def can_deny_leave(self):
2214
        return self.state == self.LEAVE_REQUESTED
2215

    
2216
    def leave_request_deny(self):
2217
        if not self.can_deny_leave():
2218
            m = _("%s: attempt to deny leave request in state '%s'") % (
2219
                self, self.state)
2220
            raise AssertionError(m)
2221

    
2222
        self.leave_request_date = None
2223
        self.state = self.ACCEPTED
2224
        self.save()
2225

    
2226
    def can_cancel_leave(self):
2227
        return self.state == self.LEAVE_REQUESTED
2228

    
2229
    def leave_request_cancel(self):
2230
        if not self.can_cancel_leave():
2231
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2232
                self, self.state)
2233
            raise AssertionError(m)
2234

    
2235
        self.leave_request_date = None
2236
        self.state = self.ACCEPTED
2237
        self.save()
2238

    
2239
    def can_remove(self):
2240
        return self.state in self.ACCEPTED_STATES
2241

    
2242
    def remove(self):
2243
        if not self.can_remove():
2244
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2245
            raise AssertionError(m)
2246

    
2247
        self._set_history_item(reason='REMOVE')
2248
        self.state = self.REMOVED
2249
        self.save()
2250

    
2251
    def can_reject(self):
2252
        return self.state == self.REQUESTED
2253

    
2254
    def reject(self):
2255
        if not self.can_reject():
2256
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2257
            raise AssertionError(m)
2258

    
2259
        # rejected requests don't need sync,
2260
        # because they were never effected
2261
        self._set_history_item(reason='REJECT')
2262
        self.delete()
2263

    
2264
    def can_cancel(self):
2265
        return self.state == self.REQUESTED
2266

    
2267
    def cancel(self):
2268
        if not self.can_cancel():
2269
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2270
            raise AssertionError(m)
2271

    
2272
        # rejected requests don't need sync,
2273
        # because they were never effected
2274
        self._set_history_item(reason='CANCEL')
2275
        self.delete()
2276

    
2277
    def get_diff_quotas(self, sub_list=None, add_list=None,
2278
                        pending_application=None):
2279
        if sub_list is None:
2280
            sub_list = []
2281

    
2282
        if add_list is None:
2283
            add_list = []
2284

    
2285
        sub_append = sub_list.append
2286
        add_append = add_list.append
2287
        holder = self.person.uuid
2288

    
2289
        synced_application = self.application
2290
        if synced_application is not None:
2291
            cur_grants = synced_application.projectresourcegrant_set.all()
2292
            for grant in cur_grants:
2293
                sub_append(QuotaLimits(
2294
                               holder       = holder,
2295
                               resource     = str(grant.resource),
2296
                               capacity     = grant.member_capacity,
2297
                               ))
2298

    
2299
        if pending_application is not None:
2300
            new_grants = pending_application.projectresourcegrant_set.all()
2301
            for new_grant in new_grants:
2302
                add_append(QuotaLimits(
2303
                               holder       = holder,
2304
                               resource     = str(new_grant.resource),
2305
                               capacity     = new_grant.member_capacity,
2306
                               ))
2307

    
2308
        return (sub_list, add_list)
2309

    
2310
    def get_pending_application(self):
2311
        project = self.project
2312
        if project.is_deactivated():
2313
            return None
2314
        if self.state not in self.ACTUALLY_ACCEPTED:
2315
            return None
2316
        return project.application
2317

    
2318

    
2319
class Serial(models.Model):
2320
    serial  =   models.AutoField(primary_key=True)
2321

    
2322

    
2323
def sync_users(users, sync=True):
2324
    def _sync_users(users, sync):
2325

    
2326
        info = {}
2327
        for user in users:
2328
            info[user.uuid] = user.email
2329

    
2330
        qh_quotas, qh_limits = get_users_quotas_and_limits(users)
2331
        astakos_initial = initial_quotas(users)
2332
        astakos_quotas = users_quotas(users)
2333

    
2334
        diff_quotas = {}
2335
        for holder, local in astakos_quotas.iteritems():
2336
            registered = qh_limits.get(holder, None)
2337
            if local != registered:
2338
                diff_quotas[holder] = dict(local)
2339

    
2340
        if sync:
2341
            r = set_user_quota(diff_quotas)
2342

    
2343
        return (qh_limits, qh_quotas,
2344
                astakos_initial, diff_quotas, info)
2345

    
2346
    return _sync_users(users, sync)
2347

    
2348

    
2349
def sync_all_users(sync=True):
2350
    users = AstakosUser.objects.verified()
2351
    return sync_users(users, sync)
2352

    
2353
class ProjectMembershipHistory(models.Model):
2354
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2355
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2356

    
2357
    person  =   models.BigIntegerField()
2358
    project =   models.BigIntegerField()
2359
    date    =   models.DateField(auto_now_add=True)
2360
    reason  =   models.IntegerField()
2361
    serial  =   models.BigIntegerField()
2362

    
2363
### SIGNALS ###
2364
################
2365

    
2366
def create_astakos_user(u):
2367
    try:
2368
        AstakosUser.objects.get(user_ptr=u.pk)
2369
    except AstakosUser.DoesNotExist:
2370
        extended_user = AstakosUser(user_ptr_id=u.pk)
2371
        extended_user.__dict__.update(u.__dict__)
2372
        extended_user.save()
2373
        if not extended_user.has_auth_provider('local'):
2374
            extended_user.add_auth_provider('local')
2375
    except BaseException, e:
2376
        logger.exception(e)
2377

    
2378
def fix_superusers():
2379
    # Associate superusers with AstakosUser
2380
    admins = User.objects.filter(is_superuser=True)
2381
    for u in admins:
2382
        create_astakos_user(u)
2383

    
2384
def user_post_save(sender, instance, created, **kwargs):
2385
    if not created:
2386
        return
2387
    create_astakos_user(instance)
2388
post_save.connect(user_post_save, sender=User)
2389

    
2390
def astakosuser_post_save(sender, instance, created, **kwargs):
2391
    pass
2392

    
2393
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2394

    
2395
def resource_post_save(sender, instance, created, **kwargs):
2396
    pass
2397

    
2398
post_save.connect(resource_post_save, sender=Resource)
2399

    
2400
def renew_token(sender, instance, **kwargs):
2401
    if not instance.auth_token:
2402
        instance.renew_token()
2403
pre_save.connect(renew_token, sender=AstakosUser)
2404
pre_save.connect(renew_token, sender=Service)