Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ e496f888

History | View | Annotate | Download (79.9 kB)

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

    
34
import hashlib
35
import uuid
36
import logging
37
import json
38
import math
39
import copy
40

    
41
from time import asctime
42
from datetime import datetime, timedelta
43
from base64 import b64encode
44
from urlparse import urlparse
45
from urllib import quote
46
from random import randint
47
from collections import defaultdict, namedtuple
48

    
49
from django.db import models, IntegrityError, transaction
50
from django.contrib.auth.models import User, UserManager, Group, Permission
51
from django.utils.translation import ugettext as _
52
from django.core.exceptions import ValidationError
53
from django.db.models.signals import (
54
    pre_save, post_save, post_syncdb, post_delete)
55
from django.contrib.contenttypes.models import ContentType
56

    
57
from django.dispatch import Signal
58
from django.db.models import Q, Max
59
from django.core.urlresolvers import reverse
60
from django.utils.http import int_to_base36
61
from django.contrib.auth.tokens import default_token_generator
62
from django.conf import settings
63
from django.utils.importlib import import_module
64
from django.utils.safestring import mark_safe
65
from django.core.validators import email_re
66
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
67

    
68
from astakos.im.settings import (
69
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
70
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
71
    SITENAME, 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
logger = logging.getLogger(__name__)
88

    
89
DEFAULT_CONTENT_TYPE = None
90
_content_type = None
91

    
92
def get_content_type():
93
    global _content_type
94
    if _content_type is not None:
95
        return _content_type
96

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

    
104
RESOURCE_SEPARATOR = '.'
105

    
106
inf = float('inf')
107

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

    
119
    class Meta:
120
        ordering = ('order', )
121

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

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

    
135
    def __str__(self):
136
        return self.name
137

    
138
    @property
139
    def resources(self):
140
        return self.resource_set.all()
141

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

    
147

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

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

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

    
171
    class Meta:
172
        unique_together = ("service", "name")
173

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

    
177
    def full_name(self):
178
        return str(self)
179

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

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

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

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

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

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

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

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

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

    
234
        ss.append(service)
235

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

    
249
                rs.append(r)
250

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

    
256
    register_resources(rs)
257

    
258

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

    
266
    return _DEFAULT_QUOTA
267

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

    
274

    
275
class AstakosUserManager(UserManager):
276

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

    
286
    def get_by_email(self, email):
287
        return self.get(email=email)
288

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

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

    
301
    def verified_user_exists(self, email_or_username):
302
        return self.user_exists(email_or_username, email_verified=True)
303

    
304
    def verified(self):
305
        return self.filter(email_verified=True)
306

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

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

    
329

    
330

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

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

    
347

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

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

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

    
371
    email_verified = models.BooleanField(_('Email verified?'), default=False)
372

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

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

    
382
    policy = models.ManyToManyField(
383
        Resource, null=True, through='AstakosUserQuota')
384

    
385
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
386

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

    
391
    objects = AstakosUserManager()
392

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

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

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

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

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

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

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

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

    
446
    @property
447
    def policies(self):
448
        return self.astakosuserquota_set.select_related().all()
449

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

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

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

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

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

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

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

    
510
        self.update_uuid()
511

    
512
        if self.username != self.email.lower():
513
            # set username
514
            self.username = self.email.lower()
515

    
516
        super(AstakosUser, self).save(**kwargs)
517

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

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

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

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

    
548
    def __unicode__(self):
549
        return '%s (%s)' % (self.realname, self.email)
550

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

    
558
    def email_change_is_pending(self):
559
        return self.emailchanges.count() > 0
560

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

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

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

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

    
592
        local = self.get_auth_provider('local')._instance
593
        return local.auth_backend == 'astakos'
594

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

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

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

    
610
    def get_required_providers(self, **kwargs):
611
        return auth.REQUIRED_PROVIDERS.keys()
612

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

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

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

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

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

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

    
658
        modules = astakos_settings.IM_MODULES
659

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

    
665
        providers = sorted(providers, key=key)
666
        return providers
667

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

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

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

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

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

    
693
    def get_inactive_message(self, provider_module, identifier=None):
694
        provider = self.get_auth_provider(provider_module, identifier)
695

    
696
        msg_extra = ''
697
        message = ''
698

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

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

    
724
        return mark_safe(message + u' '+ msg_extra)
725

    
726
    def owns_application(self, application):
727
        return application.owner == self
728

    
729
    def owns_project(self, project):
730
        return project.application.owner == self
731

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

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

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

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

    
766
    def settings(self):
767
        return UserSetting.objects.filter(user=self)
768

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

    
776

    
777
def initial_quotas(users):
778
    initial = {}
779
    default_quotas = get_default_quota()
780

    
781
    for user in users:
782
        uuid = user.uuid
783
        initial[uuid] = dict(default_quotas)
784

    
785
    objs = AstakosUserQuota.objects.select_related()
786
    orig_quotas = objs.filter(user__in=users)
787
    for user_quota in orig_quotas:
788
        uuid = user_quota.user.uuid
789
        user_init = initial.get(uuid, {})
790
        resource = user_quota.resource.full_name()
791
        user_init[resource] = user_quota.capacity
792
        initial[uuid] = user_init
793

    
794
    return initial
795

    
796

    
797
def users_quotas(users, initial=None):
798
    if initial is None:
799
        quotas = initial_quotas(users)
800
    else:
801
        quotas = copy.deepcopy(initial)
802

    
803
    objs = ProjectMembership.objects.select_related('application', 'person')
804
    memberships = objs.filter(person__in=users)
805

    
806
    apps = set(m.application for m in memberships if m.application is not None)
807
    objs = ProjectResourceGrant.objects.select_related()
808
    grants = objs.filter(project_application__in=apps)
809

    
810
    for membership in memberships:
811
        uuid = membership.person.uuid
812
        userquotas = quotas.get(uuid, {})
813

    
814
        application = membership.application
815
        if application is None:
816
            continue
817

    
818
        for grant in grants:
819
            if grant.project_application_id != application.id:
820
                continue
821
            resource = grant.resource.full_name()
822
            prev = userquotas.get(resource, 0)
823
            new = prev + grant.member_capacity
824
            userquotas[resource] = new
825
        quotas[uuid] = userquotas
826

    
827
    return quotas
828

    
829

    
830
class AstakosUserAuthProviderManager(models.Manager):
831

    
832
    def active(self, **filters):
833
        return self.filter(active=True, **filters)
834

    
835
    def remove_unverified_providers(self, provider, **filters):
836
        try:
837
            existing = self.filter(module=provider, user__email_verified=False,
838
                                   **filters)
839
            for p in existing:
840
                p.user.delete()
841
        except:
842
            pass
843

    
844
    def unverified(self, provider, **filters):
845
        try:
846
            return self.get(module=provider, user__email_verified=False,
847
                            **filters).settings
848
        except AstakosUserAuthProvider.DoesNotExist:
849
            return None
850

    
851
    def verified(self, provider, **filters):
852
        try:
853
            return self.get(module=provider, user__email_verified=True,
854
                            **filters).settings
855
        except AstakosUserAuthProvider.DoesNotExist:
856
            return None
857

    
858

    
859
class AuthProviderPolicyProfileManager(models.Manager):
860

    
861
    def active(self):
862
        return self.filter(active=True)
863

    
864
    def for_user(self, user, provider):
865
        policies = {}
866
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
867
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
868
        exclusive_q = exclusive_q1 | exclusive_q2
869

    
870
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
871
            policies.update(profile.policies)
872

    
873
        user_groups = user.groups.all().values('pk')
874
        for profile in self.active().filter(groups__in=user_groups).filter(
875
                exclusive_q):
876
            policies.update(profile.policies)
877
        return policies
878

    
879
    def add_policy(self, name, provider, group_or_user, exclusive=False,
880
                   **policies):
881
        is_group = isinstance(group_or_user, Group)
882
        profile, created = self.get_or_create(name=name, provider=provider,
883
                                              is_exclusive=exclusive)
884
        profile.is_exclusive = exclusive
885
        profile.save()
886
        if is_group:
887
            profile.groups.add(group_or_user)
888
        else:
889
            profile.users.add(group_or_user)
890
        profile.set_policies(policies)
891
        profile.save()
892
        return profile
893

    
894

    
895
class AuthProviderPolicyProfile(models.Model):
896
    name = models.CharField(_('Name'), max_length=255, blank=False,
897
                            null=False, db_index=True)
898
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
899
                                null=False)
900

    
901
    # apply policies to all providers excluding the one set in provider field
902
    is_exclusive = models.BooleanField(default=False)
903

    
904
    policy_add = models.NullBooleanField(null=True, default=None)
905
    policy_remove = models.NullBooleanField(null=True, default=None)
906
    policy_create = models.NullBooleanField(null=True, default=None)
907
    policy_login = models.NullBooleanField(null=True, default=None)
908
    policy_limit = models.IntegerField(null=True, default=None)
909
    policy_required = models.NullBooleanField(null=True, default=None)
910
    policy_automoderate = models.NullBooleanField(null=True, default=None)
911
    policy_switch = models.NullBooleanField(null=True, default=None)
912

    
913
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
914
                     'automoderate')
915

    
916
    priority = models.IntegerField(null=False, default=1)
917
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
918
    users = models.ManyToManyField(AstakosUser,
919
                                   related_name='authpolicy_profiles')
920
    active = models.BooleanField(default=True)
921

    
922
    objects = AuthProviderPolicyProfileManager()
923

    
924
    class Meta:
925
        ordering = ['priority']
926

    
927
    @property
928
    def policies(self):
929
        policies = {}
930
        for pkey in self.POLICY_FIELDS:
931
            value = getattr(self, 'policy_%s' % pkey, None)
932
            if value is None:
933
                continue
934
            policies[pkey] = value
935
        return policies
936

    
937
    def set_policies(self, policies_dict):
938
        for key, value in policies_dict.iteritems():
939
            if key in self.POLICY_FIELDS:
940
                setattr(self, 'policy_%s' % key, value)
941
        return self.policies
942

    
943

    
944
class AstakosUserAuthProvider(models.Model):
945
    """
946
    Available user authentication methods.
947
    """
948
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
949
                                   null=True, default=None)
950
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
951
    module = models.CharField(_('Provider'), max_length=255, blank=False,
952
                                default='local')
953
    identifier = models.CharField(_('Third-party identifier'),
954
                                              max_length=255, null=True,
955
                                              blank=True)
956
    active = models.BooleanField(default=True)
957
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
958
                                   default='astakos')
959
    info_data = models.TextField(default="", null=True, blank=True)
960
    created = models.DateTimeField('Creation date', auto_now_add=True)
961

    
962
    objects = AstakosUserAuthProviderManager()
963

    
964
    class Meta:
965
        unique_together = (('identifier', 'module', 'user'), )
966
        ordering = ('module', 'created')
967

    
968
    def __init__(self, *args, **kwargs):
969
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
970
        try:
971
            self.info = json.loads(self.info_data)
972
            if not self.info:
973
                self.info = {}
974
        except Exception, e:
975
            self.info = {}
976

    
977
        for key,value in self.info.iteritems():
978
            setattr(self, 'info_%s' % key, value)
979

    
980
    @property
981
    def settings(self):
982
        extra_data = {}
983

    
984
        info_data = {}
985
        if self.info_data:
986
            info_data = json.loads(self.info_data)
987

    
988
        extra_data['info'] = info_data
989

    
990
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
991
            extra_data[key] = getattr(self, key)
992

    
993
        extra_data['instance'] = self
994
        return auth.get_provider(self.module, self.user,
995
                                           self.identifier, **extra_data)
996

    
997
    def __repr__(self):
998
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
999

    
1000
    def __unicode__(self):
1001
        if self.identifier:
1002
            return "%s:%s" % (self.module, self.identifier)
1003
        if self.auth_backend:
1004
            return "%s:%s" % (self.module, self.auth_backend)
1005
        return self.module
1006

    
1007
    def save(self, *args, **kwargs):
1008
        self.info_data = json.dumps(self.info)
1009
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1010

    
1011

    
1012
class ExtendedManager(models.Manager):
1013
    def _update_or_create(self, **kwargs):
1014
        assert kwargs, \
1015
            'update_or_create() must be passed at least one keyword argument'
1016
        obj, created = self.get_or_create(**kwargs)
1017
        defaults = kwargs.pop('defaults', {})
1018
        if created:
1019
            return obj, True, False
1020
        else:
1021
            try:
1022
                params = dict(
1023
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
1024
                params.update(defaults)
1025
                for attr, val in params.items():
1026
                    if hasattr(obj, attr):
1027
                        setattr(obj, attr, val)
1028
                sid = transaction.savepoint()
1029
                obj.save(force_update=True)
1030
                transaction.savepoint_commit(sid)
1031
                return obj, False, True
1032
            except IntegrityError, e:
1033
                transaction.savepoint_rollback(sid)
1034
                try:
1035
                    return self.get(**kwargs), False, False
1036
                except self.model.DoesNotExist:
1037
                    raise e
1038

    
1039
    update_or_create = _update_or_create
1040

    
1041

    
1042
class AstakosUserQuota(models.Model):
1043
    objects = ExtendedManager()
1044
    capacity = intDecimalField()
1045
    quantity = intDecimalField(default=0)
1046
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1047
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1048
    resource = models.ForeignKey(Resource)
1049
    user = models.ForeignKey(AstakosUser)
1050

    
1051
    class Meta:
1052
        unique_together = ("resource", "user")
1053

    
1054

    
1055
class ApprovalTerms(models.Model):
1056
    """
1057
    Model for approval terms
1058
    """
1059

    
1060
    date = models.DateTimeField(
1061
        _('Issue date'), db_index=True, auto_now_add=True)
1062
    location = models.CharField(_('Terms location'), max_length=255)
1063

    
1064

    
1065
class Invitation(models.Model):
1066
    """
1067
    Model for registring invitations
1068
    """
1069
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1070
                                null=True)
1071
    realname = models.CharField(_('Real name'), max_length=255)
1072
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1073
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1074
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1075
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1076
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1077

    
1078
    def __init__(self, *args, **kwargs):
1079
        super(Invitation, self).__init__(*args, **kwargs)
1080
        if not self.id:
1081
            self.code = _generate_invitation_code()
1082

    
1083
    def consume(self):
1084
        self.is_consumed = True
1085
        self.consumed = datetime.now()
1086
        self.save()
1087

    
1088
    def __unicode__(self):
1089
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1090

    
1091

    
1092
class EmailChangeManager(models.Manager):
1093

    
1094
    @transaction.commit_on_success
1095
    def change_email(self, activation_key):
1096
        """
1097
        Validate an activation key and change the corresponding
1098
        ``User`` if valid.
1099

1100
        If the key is valid and has not expired, return the ``User``
1101
        after activating.
1102

1103
        If the key is not valid or has expired, return ``None``.
1104

1105
        If the key is valid but the ``User`` is already active,
1106
        return ``None``.
1107

1108
        After successful email change the activation record is deleted.
1109

1110
        Throws ValueError if there is already
1111
        """
1112
        try:
1113
            email_change = self.model.objects.get(
1114
                activation_key=activation_key)
1115
            if email_change.activation_key_expired():
1116
                email_change.delete()
1117
                raise EmailChange.DoesNotExist
1118
            # is there an active user with this address?
1119
            try:
1120
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1121
            except AstakosUser.DoesNotExist:
1122
                pass
1123
            else:
1124
                raise ValueError(_('The new email address is reserved.'))
1125
            # update user
1126
            user = AstakosUser.objects.get(pk=email_change.user_id)
1127
            old_email = user.email
1128
            user.email = email_change.new_email_address
1129
            user.save()
1130
            email_change.delete()
1131
            msg = "User %s changed email from %s to %s" % (user.log_display,
1132
                                                           old_email,
1133
                                                           user.email)
1134
            logger.log(LOGGING_LEVEL, msg)
1135
            return user
1136
        except EmailChange.DoesNotExist:
1137
            raise ValueError(_('Invalid activation key.'))
1138

    
1139

    
1140
class EmailChange(models.Model):
1141
    new_email_address = models.EmailField(
1142
        _(u'new e-mail address'),
1143
        help_text=_('Provide a new email address. Until you verify the new '
1144
                    'address by following the activation link that will be '
1145
                    'sent to it, your old email address will remain active.'))
1146
    user = models.ForeignKey(
1147
        AstakosUser, unique=True, related_name='emailchanges')
1148
    requested_at = models.DateTimeField(auto_now_add=True)
1149
    activation_key = models.CharField(
1150
        max_length=40, unique=True, db_index=True)
1151

    
1152
    objects = EmailChangeManager()
1153

    
1154
    def get_url(self):
1155
        return reverse('email_change_confirm',
1156
                      kwargs={'activation_key': self.activation_key})
1157

    
1158
    def activation_key_expired(self):
1159
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1160
        return self.requested_at + expiration_date < datetime.now()
1161

    
1162

    
1163
class AdditionalMail(models.Model):
1164
    """
1165
    Model for registring invitations
1166
    """
1167
    owner = models.ForeignKey(AstakosUser)
1168
    email = models.EmailField()
1169

    
1170

    
1171
def _generate_invitation_code():
1172
    while True:
1173
        code = randint(1, 2L ** 63 - 1)
1174
        try:
1175
            Invitation.objects.get(code=code)
1176
            # An invitation with this code already exists, try again
1177
        except Invitation.DoesNotExist:
1178
            return code
1179

    
1180

    
1181
def get_latest_terms():
1182
    try:
1183
        term = ApprovalTerms.objects.order_by('-id')[0]
1184
        return term
1185
    except IndexError:
1186
        pass
1187
    return None
1188

    
1189

    
1190
class PendingThirdPartyUser(models.Model):
1191
    """
1192
    Model for registring successful third party user authentications
1193
    """
1194
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1195
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1196
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1197
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1198
                                  null=True)
1199
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1200
                                 null=True)
1201
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1202
                                   null=True)
1203
    username = models.CharField(_('username'), max_length=30, unique=True,
1204
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1205
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1206
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1207
    info = models.TextField(default="", null=True, blank=True)
1208

    
1209
    class Meta:
1210
        unique_together = ("provider", "third_party_identifier")
1211

    
1212
    def get_user_instance(self):
1213
        d = self.__dict__
1214
        d.pop('_state', None)
1215
        d.pop('id', None)
1216
        d.pop('token', None)
1217
        d.pop('created', None)
1218
        d.pop('info', None)
1219
        user = AstakosUser(**d)
1220

    
1221
        return user
1222

    
1223
    @property
1224
    def realname(self):
1225
        return '%s %s' %(self.first_name, self.last_name)
1226

    
1227
    @realname.setter
1228
    def realname(self, value):
1229
        parts = value.split(' ')
1230
        if len(parts) == 2:
1231
            self.first_name = parts[0]
1232
            self.last_name = parts[1]
1233
        else:
1234
            self.last_name = parts[0]
1235

    
1236
    def save(self, **kwargs):
1237
        if not self.id:
1238
            # set username
1239
            while not self.username:
1240
                username =  uuid.uuid4().hex[:30]
1241
                try:
1242
                    AstakosUser.objects.get(username = username)
1243
                except AstakosUser.DoesNotExist, e:
1244
                    self.username = username
1245
        super(PendingThirdPartyUser, self).save(**kwargs)
1246

    
1247
    def generate_token(self):
1248
        self.password = self.third_party_identifier
1249
        self.last_login = datetime.now()
1250
        self.token = default_token_generator.make_token(self)
1251

    
1252
    def existing_user(self):
1253
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1254
                                         auth_providers__identifier=self.third_party_identifier)
1255

    
1256
    def get_provider(self, user):
1257
        params = {
1258
            'info_data': self.info,
1259
            'affiliation': self.affiliation
1260
        }
1261
        return auth.get_provider(self.provider, user,
1262
                                 self.third_party_identifier, **params)
1263

    
1264
class SessionCatalog(models.Model):
1265
    session_key = models.CharField(_('session key'), max_length=40)
1266
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1267

    
1268

    
1269
class UserSetting(models.Model):
1270
    user = models.ForeignKey(AstakosUser)
1271
    setting = models.CharField(max_length=255)
1272
    value = models.IntegerField()
1273

    
1274
    objects = ForUpdateManager()
1275

    
1276
    class Meta:
1277
        unique_together = ("user", "setting")
1278

    
1279

    
1280
### PROJECTS ###
1281
################
1282

    
1283
class ChainManager(ForUpdateManager):
1284

    
1285
    def search_by_name(self, *search_strings):
1286
        projects = Project.objects.search_by_name(*search_strings)
1287
        chains = [p.id for p in projects]
1288
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1289
        apps = (app for app in apps if app.is_latest())
1290
        app_chains = [app.chain for app in apps if app.chain not in chains]
1291
        return chains + app_chains
1292

    
1293
    def all_full_state(self):
1294
        chains = self.all()
1295
        cids = [c.chain for c in chains]
1296
        projects = Project.objects.select_related('application').in_bulk(cids)
1297

    
1298
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1299
        chain_latest = dict(objs.values_list('chain', 'latest'))
1300

    
1301
        objs = ProjectApplication.objects.select_related('applicant')
1302
        apps = objs.in_bulk(chain_latest.values())
1303

    
1304
        d = {}
1305
        for chain in chains:
1306
            pk = chain.pk
1307
            project = projects.get(pk, None)
1308
            app = apps[chain_latest[pk]]
1309
            d[chain.pk] = chain.get_state(project, app)
1310

    
1311
        return d
1312

    
1313
    def of_project(self, project):
1314
        if project is None:
1315
            return None
1316
        try:
1317
            return self.get(chain=project.id)
1318
        except Chain.DoesNotExist:
1319
            raise AssertionError('project with no chain')
1320

    
1321

    
1322
class Chain(models.Model):
1323
    chain  =   models.AutoField(primary_key=True)
1324

    
1325
    def __str__(self):
1326
        return "%s" % (self.chain,)
1327

    
1328
    objects = ChainManager()
1329

    
1330
    PENDING            = 0
1331
    DENIED             = 3
1332
    DISMISSED          = 4
1333
    CANCELLED          = 5
1334

    
1335
    APPROVED           = 10
1336
    APPROVED_PENDING   = 11
1337
    SUSPENDED          = 12
1338
    SUSPENDED_PENDING  = 13
1339
    TERMINATED         = 14
1340
    TERMINATED_PENDING = 15
1341

    
1342
    PENDING_STATES = [PENDING,
1343
                      APPROVED_PENDING,
1344
                      SUSPENDED_PENDING,
1345
                      TERMINATED_PENDING,
1346
                      ]
1347

    
1348
    MODIFICATION_STATES = [APPROVED_PENDING,
1349
                           SUSPENDED_PENDING,
1350
                           TERMINATED_PENDING,
1351
                           ]
1352

    
1353
    RELEVANT_STATES = [PENDING,
1354
                       DENIED,
1355
                       APPROVED,
1356
                       APPROVED_PENDING,
1357
                       SUSPENDED,
1358
                       SUSPENDED_PENDING,
1359
                       TERMINATED_PENDING,
1360
                       ]
1361

    
1362
    SKIP_STATES = [DISMISSED,
1363
                   CANCELLED,
1364
                   TERMINATED]
1365

    
1366
    STATE_DISPLAY = {
1367
        PENDING            : _("Pending"),
1368
        DENIED             : _("Denied"),
1369
        DISMISSED          : _("Dismissed"),
1370
        CANCELLED          : _("Cancelled"),
1371
        APPROVED           : _("Active"),
1372
        APPROVED_PENDING   : _("Active - Pending"),
1373
        SUSPENDED          : _("Suspended"),
1374
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1375
        TERMINATED         : _("Terminated"),
1376
        TERMINATED_PENDING : _("Terminated - Pending"),
1377
        }
1378

    
1379

    
1380
    @classmethod
1381
    def _chain_state(cls, project_state, app_state):
1382
        s = CHAIN_STATE.get((project_state, app_state), None)
1383
        if s is None:
1384
            raise AssertionError('inconsistent chain state')
1385
        return s
1386

    
1387
    @classmethod
1388
    def chain_state(cls, project, app):
1389
        p_state = project.state if project else None
1390
        return cls._chain_state(p_state, app.state)
1391

    
1392
    @classmethod
1393
    def state_display(cls, s):
1394
        if s is None:
1395
            return _("Unknown")
1396
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1397

    
1398
    def last_application(self):
1399
        return self.chained_apps.order_by('-id')[0]
1400

    
1401
    def get_project(self):
1402
        try:
1403
            return self.chained_project
1404
        except Project.DoesNotExist:
1405
            return None
1406

    
1407
    def get_elements(self):
1408
        project = self.get_project()
1409
        app = self.last_application()
1410
        return project, app
1411

    
1412
    def get_state(self, project, app):
1413
        s = self.chain_state(project, app)
1414
        return s, project, app
1415

    
1416
    def full_state(self):
1417
        project, app = self.get_elements()
1418
        return self.get_state(project, app)
1419

    
1420

    
1421
def new_chain():
1422
    c = Chain.objects.create()
1423
    return c
1424

    
1425

    
1426
class ProjectApplicationManager(ForUpdateManager):
1427

    
1428
    def user_visible_projects(self, *filters, **kw_filters):
1429
        model = self.model
1430
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1431

    
1432
    def user_visible_by_chain(self, flt):
1433
        model = self.model
1434
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1435
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1436
        by_chain = dict(pending.annotate(models.Max('id')))
1437
        by_chain.update(approved.annotate(models.Max('id')))
1438
        return self.filter(flt, id__in=by_chain.values())
1439

    
1440
    def user_accessible_projects(self, user):
1441
        """
1442
        Return projects accessed by specified user.
1443
        """
1444
        if user.is_project_admin():
1445
            participates_filters = Q()
1446
        else:
1447
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1448
                                   Q(project__projectmembership__person=user)
1449

    
1450
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1451

    
1452
    def search_by_name(self, *search_strings):
1453
        q = Q()
1454
        for s in search_strings:
1455
            q = q | Q(name__icontains=s)
1456
        return self.filter(q)
1457

    
1458
    def latest_of_chain(self, chain_id):
1459
        try:
1460
            return self.filter(chain=chain_id).order_by('-id')[0]
1461
        except IndexError:
1462
            return None
1463

    
1464

    
1465
class ProjectApplication(models.Model):
1466
    applicant               =   models.ForeignKey(
1467
                                    AstakosUser,
1468
                                    related_name='projects_applied',
1469
                                    db_index=True)
1470

    
1471
    PENDING     =    0
1472
    APPROVED    =    1
1473
    REPLACED    =    2
1474
    DENIED      =    3
1475
    DISMISSED   =    4
1476
    CANCELLED   =    5
1477

    
1478
    state                   =   models.IntegerField(default=PENDING,
1479
                                                    db_index=True)
1480

    
1481
    owner                   =   models.ForeignKey(
1482
                                    AstakosUser,
1483
                                    related_name='projects_owned',
1484
                                    db_index=True)
1485

    
1486
    chain                   =   models.ForeignKey(Chain,
1487
                                                  related_name='chained_apps',
1488
                                                  db_column='chain')
1489
    precursor_application   =   models.ForeignKey('ProjectApplication',
1490
                                                  null=True,
1491
                                                  blank=True)
1492

    
1493
    name                    =   models.CharField(max_length=80)
1494
    homepage                =   models.URLField(max_length=255, null=True,
1495
                                                verify_exists=False)
1496
    description             =   models.TextField(null=True, blank=True)
1497
    start_date              =   models.DateTimeField(null=True, blank=True)
1498
    end_date                =   models.DateTimeField()
1499
    member_join_policy      =   models.IntegerField()
1500
    member_leave_policy     =   models.IntegerField()
1501
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1502
    resource_grants         =   models.ManyToManyField(
1503
                                    Resource,
1504
                                    null=True,
1505
                                    blank=True,
1506
                                    through='ProjectResourceGrant')
1507
    comments                =   models.TextField(null=True, blank=True)
1508
    issue_date              =   models.DateTimeField(auto_now_add=True)
1509
    response_date           =   models.DateTimeField(null=True, blank=True)
1510
    response                =   models.TextField(null=True, blank=True)
1511

    
1512
    objects                 =   ProjectApplicationManager()
1513

    
1514
    # Compiled queries
1515
    Q_PENDING  = Q(state=PENDING)
1516
    Q_APPROVED = Q(state=APPROVED)
1517
    Q_DENIED   = Q(state=DENIED)
1518

    
1519
    class Meta:
1520
        unique_together = ("chain", "id")
1521

    
1522
    def __unicode__(self):
1523
        return "%s applied by %s" % (self.name, self.applicant)
1524

    
1525
    # TODO: Move to a more suitable place
1526
    APPLICATION_STATE_DISPLAY = {
1527
        PENDING  : _('Pending review'),
1528
        APPROVED : _('Approved'),
1529
        REPLACED : _('Replaced'),
1530
        DENIED   : _('Denied'),
1531
        DISMISSED: _('Dismissed'),
1532
        CANCELLED: _('Cancelled')
1533
    }
1534

    
1535
    @property
1536
    def log_display(self):
1537
        return "application %s (%s) for project %s" % (
1538
            self.id, self.name, self.chain)
1539

    
1540
    def get_project(self):
1541
        try:
1542
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1543
            return Project
1544
        except Project.DoesNotExist, e:
1545
            return None
1546

    
1547
    def state_display(self):
1548
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1549

    
1550
    def project_state_display(self):
1551
        try:
1552
            project = self.project
1553
            return project.state_display()
1554
        except Project.DoesNotExist:
1555
            return self.state_display()
1556

    
1557
    def add_resource_policy(self, service, resource, uplimit):
1558
        """Raises ObjectDoesNotExist, IntegrityError"""
1559
        q = self.projectresourcegrant_set
1560
        resource = Resource.objects.get(service__name=service, name=resource)
1561
        q.create(resource=resource, member_capacity=uplimit)
1562

    
1563
    def members_count(self):
1564
        return self.project.approved_memberships.count()
1565

    
1566
    @property
1567
    def grants(self):
1568
        return self.projectresourcegrant_set.values(
1569
            'member_capacity', 'resource__name', 'resource__service__name')
1570

    
1571
    @property
1572
    def resource_policies(self):
1573
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1574

    
1575
    @resource_policies.setter
1576
    def resource_policies(self, policies):
1577
        for p in policies:
1578
            service = p.get('service', None)
1579
            resource = p.get('resource', None)
1580
            uplimit = p.get('uplimit', 0)
1581
            self.add_resource_policy(service, resource, uplimit)
1582

    
1583
    def pending_modifications_incl_me(self):
1584
        q = self.chained_applications()
1585
        q = q.filter(Q(state=self.PENDING))
1586
        return q
1587

    
1588
    def last_pending_incl_me(self):
1589
        try:
1590
            return self.pending_modifications_incl_me().order_by('-id')[0]
1591
        except IndexError:
1592
            return None
1593

    
1594
    def pending_modifications(self):
1595
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1596

    
1597
    def last_pending(self):
1598
        try:
1599
            return self.pending_modifications().order_by('-id')[0]
1600
        except IndexError:
1601
            return None
1602

    
1603
    def is_modification(self):
1604
        # if self.state != self.PENDING:
1605
        #     return False
1606
        parents = self.chained_applications().filter(id__lt=self.id)
1607
        parents = parents.filter(state__in=[self.APPROVED])
1608
        return parents.count() > 0
1609

    
1610
    def chained_applications(self):
1611
        return ProjectApplication.objects.filter(chain=self.chain)
1612

    
1613
    def is_latest(self):
1614
        return self.chained_applications().order_by('-id')[0] == self
1615

    
1616
    def has_pending_modifications(self):
1617
        return bool(self.last_pending())
1618

    
1619
    def denied_modifications(self):
1620
        q = self.chained_applications()
1621
        q = q.filter(Q(state=self.DENIED))
1622
        q = q.filter(~Q(id=self.id))
1623
        return q
1624

    
1625
    def last_denied(self):
1626
        try:
1627
            return self.denied_modifications().order_by('-id')[0]
1628
        except IndexError:
1629
            return None
1630

    
1631
    def has_denied_modifications(self):
1632
        return bool(self.last_denied())
1633

    
1634
    def is_applied(self):
1635
        try:
1636
            self.project
1637
            return True
1638
        except Project.DoesNotExist:
1639
            return False
1640

    
1641
    def get_project(self):
1642
        try:
1643
            return Project.objects.get(id=self.chain)
1644
        except Project.DoesNotExist:
1645
            return None
1646

    
1647
    def project_exists(self):
1648
        return self.get_project() is not None
1649

    
1650
    def _get_project_for_update(self):
1651
        try:
1652
            objects = Project.objects
1653
            project = objects.get_for_update(id=self.chain)
1654
            return project
1655
        except Project.DoesNotExist:
1656
            return None
1657

    
1658
    def can_cancel(self):
1659
        return self.state == self.PENDING
1660

    
1661
    def cancel(self):
1662
        if not self.can_cancel():
1663
            m = _("cannot cancel: application '%s' in state '%s'") % (
1664
                    self.id, self.state)
1665
            raise AssertionError(m)
1666

    
1667
        self.state = self.CANCELLED
1668
        self.save()
1669

    
1670
    def can_dismiss(self):
1671
        return self.state == self.DENIED
1672

    
1673
    def dismiss(self):
1674
        if not self.can_dismiss():
1675
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1676
                    self.id, self.state)
1677
            raise AssertionError(m)
1678

    
1679
        self.state = self.DISMISSED
1680
        self.save()
1681

    
1682
    def can_deny(self):
1683
        return self.state == self.PENDING
1684

    
1685
    def deny(self, reason):
1686
        if not self.can_deny():
1687
            m = _("cannot deny: application '%s' in state '%s'") % (
1688
                    self.id, self.state)
1689
            raise AssertionError(m)
1690

    
1691
        self.state = self.DENIED
1692
        self.response_date = datetime.now()
1693
        self.response = reason
1694
        self.save()
1695

    
1696
    def can_approve(self):
1697
        return self.state == self.PENDING
1698

    
1699
    def approve(self, approval_user=None):
1700
        """
1701
        If approval_user then during owner membership acceptance
1702
        it is checked whether the request_user is eligible.
1703

1704
        Raises:
1705
            PermissionDenied
1706
        """
1707

    
1708
        if not transaction.is_managed():
1709
            raise AssertionError("NOPE")
1710

    
1711
        new_project_name = self.name
1712
        if not self.can_approve():
1713
            m = _("cannot approve: project '%s' in state '%s'") % (
1714
                    new_project_name, self.state)
1715
            raise AssertionError(m) # invalid argument
1716

    
1717
        now = datetime.now()
1718
        project = self._get_project_for_update()
1719

    
1720
        try:
1721
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1722
            conflicting_project = Project.objects.get(q)
1723
            if (conflicting_project != project):
1724
                m = (_("cannot approve: project with name '%s' "
1725
                       "already exists (id: %s)") % (
1726
                        new_project_name, conflicting_project.id))
1727
                raise PermissionDenied(m) # invalid argument
1728
        except Project.DoesNotExist:
1729
            pass
1730

    
1731
        new_project = False
1732
        if project is None:
1733
            new_project = True
1734
            project = Project(id=self.chain)
1735

    
1736
        project.name = new_project_name
1737
        project.application = self
1738
        project.last_approval_date = now
1739
        if not new_project:
1740
            project.is_modified = True
1741

    
1742
        project.save()
1743

    
1744
        self.state = self.APPROVED
1745
        self.response_date = now
1746
        self.save()
1747
        return project
1748

    
1749
    @property
1750
    def member_join_policy_display(self):
1751
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1752

    
1753
    @property
1754
    def member_leave_policy_display(self):
1755
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1756

    
1757
class ProjectResourceGrant(models.Model):
1758

    
1759
    resource                =   models.ForeignKey(Resource)
1760
    project_application     =   models.ForeignKey(ProjectApplication,
1761
                                                  null=True)
1762
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1763
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1764
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1765
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1766
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1767
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1768

    
1769
    objects = ExtendedManager()
1770

    
1771
    class Meta:
1772
        unique_together = ("resource", "project_application")
1773

    
1774
    def display_member_capacity(self):
1775
        if self.member_capacity:
1776
            if self.resource.unit:
1777
                return ProjectResourceGrant.display_filesize(
1778
                    self.member_capacity)
1779
            else:
1780
                if math.isinf(self.member_capacity):
1781
                    return 'Unlimited'
1782
                else:
1783
                    return self.member_capacity
1784
        else:
1785
            return 'Unlimited'
1786

    
1787
    def __str__(self):
1788
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1789
                                        self.display_member_capacity())
1790

    
1791
    @classmethod
1792
    def display_filesize(cls, value):
1793
        try:
1794
            value = float(value)
1795
        except:
1796
            return
1797
        else:
1798
            if math.isinf(value):
1799
                return 'Unlimited'
1800
            if value > 1:
1801
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1802
                                [0, 0, 0, 0, 0, 0])
1803
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1804
                quotient = float(value) / 1024**exponent
1805
                unit, value_decimals = unit_list[exponent]
1806
                format_string = '{0:.%sf} {1}' % (value_decimals)
1807
                return format_string.format(quotient, unit)
1808
            if value == 0:
1809
                return '0 bytes'
1810
            if value == 1:
1811
                return '1 byte'
1812
            else:
1813
               return '0'
1814

    
1815

    
1816
class ProjectManager(ForUpdateManager):
1817

    
1818
    def terminated_projects(self):
1819
        q = self.model.Q_TERMINATED
1820
        return self.filter(q)
1821

    
1822
    def not_terminated_projects(self):
1823
        q = ~self.model.Q_TERMINATED
1824
        return self.filter(q)
1825

    
1826
    def deactivated_projects(self):
1827
        q = self.model.Q_DEACTIVATED
1828
        return self.filter(q)
1829

    
1830
    def modified_projects(self):
1831
        return self.filter(is_modified=True)
1832

    
1833
    def expired_projects(self):
1834
        q = (~Q(state=Project.TERMINATED) &
1835
              Q(application__end_date__lt=datetime.now()))
1836
        return self.filter(q)
1837

    
1838
    def search_by_name(self, *search_strings):
1839
        q = Q()
1840
        for s in search_strings:
1841
            q = q | Q(name__icontains=s)
1842
        return self.filter(q)
1843

    
1844

    
1845
class Project(models.Model):
1846

    
1847
    id                          =   models.OneToOneField(Chain,
1848
                                                      related_name='chained_project',
1849
                                                      db_column='id',
1850
                                                      primary_key=True)
1851

    
1852
    application                 =   models.OneToOneField(
1853
                                            ProjectApplication,
1854
                                            related_name='project')
1855
    last_approval_date          =   models.DateTimeField(null=True)
1856

    
1857
    members                     =   models.ManyToManyField(
1858
                                            AstakosUser,
1859
                                            through='ProjectMembership')
1860

    
1861
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1862
    deactivation_date           =   models.DateTimeField(null=True)
1863

    
1864
    creation_date               =   models.DateTimeField(auto_now_add=True)
1865
    name                        =   models.CharField(
1866
                                            max_length=80,
1867
                                            null=True,
1868
                                            db_index=True,
1869
                                            unique=True)
1870

    
1871
    APPROVED    = 1
1872
    SUSPENDED   = 10
1873
    TERMINATED  = 100
1874

    
1875
    is_modified                 =   models.BooleanField(default=False,
1876
                                                        db_index=True)
1877
    is_active                   =   models.BooleanField(default=True,
1878
                                                        db_index=True)
1879
    state                       =   models.IntegerField(default=APPROVED,
1880
                                                        db_index=True)
1881

    
1882
    objects     =   ProjectManager()
1883

    
1884
    # Compiled queries
1885
    Q_TERMINATED  = Q(state=TERMINATED)
1886
    Q_SUSPENDED   = Q(state=SUSPENDED)
1887
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1888

    
1889
    def __str__(self):
1890
        return uenc(_("<project %s '%s'>") %
1891
                    (self.id, udec(self.application.name)))
1892

    
1893
    __repr__ = __str__
1894

    
1895
    def __unicode__(self):
1896
        return _("<project %s '%s'>") % (self.id, self.application.name)
1897

    
1898
    STATE_DISPLAY = {
1899
        APPROVED   : 'Active',
1900
        SUSPENDED  : 'Suspended',
1901
        TERMINATED : 'Terminated'
1902
        }
1903

    
1904
    def state_display(self):
1905
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1906

    
1907
    def expiration_info(self):
1908
        return (str(self.id), self.name, self.state_display(),
1909
                str(self.application.end_date))
1910

    
1911
    def is_deactivated(self, reason=None):
1912
        if reason is not None:
1913
            return self.state == reason
1914

    
1915
        return self.state != self.APPROVED
1916

    
1917
    ### Deactivation calls
1918

    
1919
    def terminate(self):
1920
        self.deactivation_reason = 'TERMINATED'
1921
        self.deactivation_date = datetime.now()
1922
        self.state = self.TERMINATED
1923
        self.name = None
1924
        self.save()
1925

    
1926
    def suspend(self):
1927
        self.deactivation_reason = 'SUSPENDED'
1928
        self.deactivation_date = datetime.now()
1929
        self.state = self.SUSPENDED
1930
        self.save()
1931

    
1932
    def resume(self):
1933
        self.deactivation_reason = None
1934
        self.deactivation_date = None
1935
        self.state = self.APPROVED
1936
        self.save()
1937

    
1938
    ### Logical checks
1939

    
1940
    def is_inconsistent(self):
1941
        now = datetime.now()
1942
        dates = [self.creation_date,
1943
                 self.last_approval_date,
1944
                 self.deactivation_date]
1945
        return any([date > now for date in dates])
1946

    
1947
    def is_active_strict(self):
1948
        return self.is_active and self.state == self.APPROVED
1949

    
1950
    def is_approved(self):
1951
        return self.state == self.APPROVED
1952

    
1953
    @property
1954
    def is_alive(self):
1955
        return not self.is_terminated
1956

    
1957
    @property
1958
    def is_terminated(self):
1959
        return self.is_deactivated(self.TERMINATED)
1960

    
1961
    @property
1962
    def is_suspended(self):
1963
        return self.is_deactivated(self.SUSPENDED)
1964

    
1965
    def violates_resource_grants(self):
1966
        return False
1967

    
1968
    def violates_members_limit(self, adding=0):
1969
        application = self.application
1970
        limit = application.limit_on_members_number
1971
        if limit is None:
1972
            return False
1973
        return (len(self.approved_members) + adding > limit)
1974

    
1975

    
1976
    ### Other
1977

    
1978
    def count_pending_memberships(self):
1979
        memb_set = self.projectmembership_set
1980
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1981
        return memb_count
1982

    
1983
    def members_count(self):
1984
        return self.approved_memberships.count()
1985

    
1986
    @property
1987
    def approved_memberships(self):
1988
        query = ProjectMembership.Q_ACCEPTED_STATES
1989
        return self.projectmembership_set.filter(query)
1990

    
1991
    @property
1992
    def approved_members(self):
1993
        return [m.person for m in self.approved_memberships]
1994

    
1995
    def add_member(self, user):
1996
        """
1997
        Raises:
1998
            django.exceptions.PermissionDenied
1999
            astakos.im.models.AstakosUser.DoesNotExist
2000
        """
2001
        if isinstance(user, (int, long)):
2002
            user = AstakosUser.objects.get(user=user)
2003

    
2004
        m, created = ProjectMembership.objects.get_or_create(
2005
            person=user, project=self
2006
        )
2007
        m.accept()
2008

    
2009
    def remove_member(self, user):
2010
        """
2011
        Raises:
2012
            django.exceptions.PermissionDenied
2013
            astakos.im.models.AstakosUser.DoesNotExist
2014
            astakos.im.models.ProjectMembership.DoesNotExist
2015
        """
2016
        if isinstance(user, (int, long)):
2017
            user = AstakosUser.objects.get(user=user)
2018

    
2019
        m = ProjectMembership.objects.get(person=user, project=self)
2020
        m.remove()
2021

    
2022

    
2023
CHAIN_STATE = {
2024
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2025
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2026
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2027
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2028
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2029

    
2030
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2031
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2032
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2033
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2034
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2035

    
2036
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2037
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2038
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2039
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2040
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2041

    
2042
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2043
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2044
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2045
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2046
    }
2047

    
2048

    
2049
class ProjectMembershipManager(ForUpdateManager):
2050

    
2051
    def any_accepted(self):
2052
        q = self.model.Q_ACTUALLY_ACCEPTED
2053
        return self.filter(q)
2054

    
2055
    def actually_accepted(self):
2056
        q = self.model.Q_ACTUALLY_ACCEPTED
2057
        return self.filter(q)
2058

    
2059
    def requested(self):
2060
        return self.filter(state=ProjectMembership.REQUESTED)
2061

    
2062
    def suspended(self):
2063
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2064

    
2065
class ProjectMembership(models.Model):
2066

    
2067
    person              =   models.ForeignKey(AstakosUser)
2068
    request_date        =   models.DateField(auto_now_add=True)
2069
    project             =   models.ForeignKey(Project)
2070

    
2071
    REQUESTED           =   0
2072
    ACCEPTED            =   1
2073
    LEAVE_REQUESTED     =   5
2074
    # User deactivation
2075
    USER_SUSPENDED      =   10
2076

    
2077
    REMOVED             =   200
2078

    
2079
    ASSOCIATED_STATES   =   set([REQUESTED,
2080
                                 ACCEPTED,
2081
                                 LEAVE_REQUESTED,
2082
                                 USER_SUSPENDED,
2083
                                 ])
2084

    
2085
    ACCEPTED_STATES     =   set([ACCEPTED,
2086
                                 LEAVE_REQUESTED,
2087
                                 USER_SUSPENDED,
2088
                                 ])
2089

    
2090
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2091

    
2092
    state               =   models.IntegerField(default=REQUESTED,
2093
                                                db_index=True)
2094
    is_pending          =   models.BooleanField(default=False, db_index=True)
2095
    is_active           =   models.BooleanField(default=False, db_index=True)
2096
    application         =   models.ForeignKey(
2097
                                ProjectApplication,
2098
                                null=True,
2099
                                related_name='memberships')
2100
    pending_application =   models.ForeignKey(
2101
                                ProjectApplication,
2102
                                null=True,
2103
                                related_name='pending_memberships')
2104
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2105

    
2106
    acceptance_date     =   models.DateField(null=True, db_index=True)
2107
    leave_request_date  =   models.DateField(null=True)
2108

    
2109
    objects     =   ProjectMembershipManager()
2110

    
2111
    # Compiled queries
2112
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2113
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2114

    
2115
    MEMBERSHIP_STATE_DISPLAY = {
2116
        REQUESTED           : _('Requested'),
2117
        ACCEPTED            : _('Accepted'),
2118
        LEAVE_REQUESTED     : _('Leave Requested'),
2119
        USER_SUSPENDED      : _('Suspended'),
2120
        REMOVED             : _('Pending removal'),
2121
        }
2122

    
2123
    USER_FRIENDLY_STATE_DISPLAY = {
2124
        REQUESTED           : _('Join requested'),
2125
        ACCEPTED            : _('Accepted member'),
2126
        LEAVE_REQUESTED     : _('Requested to leave'),
2127
        USER_SUSPENDED      : _('Suspended member'),
2128
        REMOVED             : _('Pending removal'),
2129
        }
2130

    
2131
    def state_display(self):
2132
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2133

    
2134
    def user_friendly_state_display(self):
2135
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2136

    
2137
    class Meta:
2138
        unique_together = ("person", "project")
2139
        #index_together = [["project", "state"]]
2140

    
2141
    def __str__(self):
2142
        return uenc(_("<'%s' membership in '%s'>") % (
2143
                self.person.username, self.project))
2144

    
2145
    __repr__ = __str__
2146

    
2147
    def __init__(self, *args, **kwargs):
2148
        self.state = self.REQUESTED
2149
        super(ProjectMembership, self).__init__(*args, **kwargs)
2150

    
2151
    def _set_history_item(self, reason, date=None):
2152
        if isinstance(reason, basestring):
2153
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2154

    
2155
        history_item = ProjectMembershipHistory(
2156
                            serial=self.id,
2157
                            person=self.person_id,
2158
                            project=self.project_id,
2159
                            date=date or datetime.now(),
2160
                            reason=reason)
2161
        history_item.save()
2162
        serial = history_item.id
2163

    
2164
    def can_accept(self):
2165
        return self.state == self.REQUESTED
2166

    
2167
    def accept(self):
2168
        if not self.can_accept():
2169
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2170
            raise AssertionError(m)
2171

    
2172
        now = datetime.now()
2173
        self.acceptance_date = now
2174
        self._set_history_item(reason='ACCEPT', date=now)
2175
        self.state = self.ACCEPTED
2176
        self.save()
2177

    
2178
    def can_leave(self):
2179
        return self.state in self.ACCEPTED_STATES
2180

    
2181
    def leave_request(self):
2182
        if not self.can_leave():
2183
            m = _("%s: attempt to request to leave in state '%s'") % (
2184
                self, self.state)
2185
            raise AssertionError(m)
2186

    
2187
        self.leave_request_date = datetime.now()
2188
        self.state = self.LEAVE_REQUESTED
2189
        self.save()
2190

    
2191
    def can_deny_leave(self):
2192
        return self.state == self.LEAVE_REQUESTED
2193

    
2194
    def leave_request_deny(self):
2195
        if not self.can_deny_leave():
2196
            m = _("%s: attempt to deny leave request in state '%s'") % (
2197
                self, self.state)
2198
            raise AssertionError(m)
2199

    
2200
        self.leave_request_date = None
2201
        self.state = self.ACCEPTED
2202
        self.save()
2203

    
2204
    def can_cancel_leave(self):
2205
        return self.state == self.LEAVE_REQUESTED
2206

    
2207
    def leave_request_cancel(self):
2208
        if not self.can_cancel_leave():
2209
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2210
                self, self.state)
2211
            raise AssertionError(m)
2212

    
2213
        self.leave_request_date = None
2214
        self.state = self.ACCEPTED
2215
        self.save()
2216

    
2217
    def can_remove(self):
2218
        return self.state in self.ACCEPTED_STATES
2219

    
2220
    def remove(self):
2221
        if not self.can_remove():
2222
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2223
            raise AssertionError(m)
2224

    
2225
        self._set_history_item(reason='REMOVE')
2226
        self.state = self.REMOVED
2227
        self.save()
2228

    
2229
    def can_reject(self):
2230
        return self.state == self.REQUESTED
2231

    
2232
    def reject(self):
2233
        if not self.can_reject():
2234
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2235
            raise AssertionError(m)
2236

    
2237
        # rejected requests don't need sync,
2238
        # because they were never effected
2239
        self._set_history_item(reason='REJECT')
2240
        self.delete()
2241

    
2242
    def can_cancel(self):
2243
        return self.state == self.REQUESTED
2244

    
2245
    def cancel(self):
2246
        if not self.can_cancel():
2247
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2248
            raise AssertionError(m)
2249

    
2250
        # rejected requests don't need sync,
2251
        # because they were never effected
2252
        self._set_history_item(reason='CANCEL')
2253
        self.delete()
2254

    
2255
    def get_diff_quotas(self, sub_list=None, add_list=None,
2256
                        pending_application=None):
2257
        if sub_list is None:
2258
            sub_list = []
2259

    
2260
        if add_list is None:
2261
            add_list = []
2262

    
2263
        sub_append = sub_list.append
2264
        add_append = add_list.append
2265
        holder = self.person.uuid
2266

    
2267
        synced_application = self.application
2268
        if synced_application is not None:
2269
            cur_grants = synced_application.projectresourcegrant_set.all()
2270
            for grant in cur_grants:
2271
                sub_append(QuotaLimits(
2272
                               holder       = holder,
2273
                               resource     = str(grant.resource),
2274
                               capacity     = grant.member_capacity,
2275
                               ))
2276

    
2277
        if pending_application is not None:
2278
            new_grants = pending_application.projectresourcegrant_set.all()
2279
            for new_grant in new_grants:
2280
                add_append(QuotaLimits(
2281
                               holder       = holder,
2282
                               resource     = str(new_grant.resource),
2283
                               capacity     = new_grant.member_capacity,
2284
                               ))
2285

    
2286
        return (sub_list, add_list)
2287

    
2288
    def get_pending_application(self):
2289
        project = self.project
2290
        if project.is_deactivated():
2291
            return None
2292
        if self.state not in self.ACTUALLY_ACCEPTED:
2293
            return None
2294
        return project.application
2295

    
2296

    
2297
class Serial(models.Model):
2298
    serial  =   models.AutoField(primary_key=True)
2299

    
2300

    
2301
def sync_users(users, sync=True):
2302
    def _sync_users(users, sync):
2303

    
2304
        info = {}
2305
        for user in users:
2306
            info[user.uuid] = user.email
2307

    
2308
        resources = get_resource_names()
2309
        qh_limits, qh_counters = qh_get_quotas(users, resources)
2310
        astakos_initial = initial_quotas(users)
2311
        astakos_quotas = users_quotas(users)
2312

    
2313
        diff_quotas = {}
2314
        for holder, local in astakos_quotas.iteritems():
2315
            registered = qh_limits.get(holder, None)
2316
            if local != registered:
2317
                diff_quotas[holder] = dict(local)
2318

    
2319
        if sync:
2320
            r = send_quotas(diff_quotas)
2321

    
2322
        return (qh_limits, qh_counters,
2323
                astakos_initial, diff_quotas, info)
2324

    
2325
    return _sync_users(users, sync)
2326

    
2327

    
2328
def sync_all_users(sync=True):
2329
    users = AstakosUser.objects.verified()
2330
    return sync_users(users, sync)
2331

    
2332
class ProjectMembershipHistory(models.Model):
2333
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2334
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2335

    
2336
    person  =   models.BigIntegerField()
2337
    project =   models.BigIntegerField()
2338
    date    =   models.DateField(auto_now_add=True)
2339
    reason  =   models.IntegerField()
2340
    serial  =   models.BigIntegerField()
2341

    
2342
### SIGNALS ###
2343
################
2344

    
2345
def create_astakos_user(u):
2346
    try:
2347
        AstakosUser.objects.get(user_ptr=u.pk)
2348
    except AstakosUser.DoesNotExist:
2349
        extended_user = AstakosUser(user_ptr_id=u.pk)
2350
        extended_user.__dict__.update(u.__dict__)
2351
        extended_user.save()
2352
        if not extended_user.has_auth_provider('local'):
2353
            extended_user.add_auth_provider('local')
2354
    except BaseException, e:
2355
        logger.exception(e)
2356

    
2357
def fix_superusers():
2358
    # Associate superusers with AstakosUser
2359
    admins = User.objects.filter(is_superuser=True)
2360
    for u in admins:
2361
        create_astakos_user(u)
2362

    
2363
def user_post_save(sender, instance, created, **kwargs):
2364
    if not created:
2365
        return
2366
    create_astakos_user(instance)
2367
post_save.connect(user_post_save, sender=User)
2368

    
2369
def astakosuser_post_save(sender, instance, created, **kwargs):
2370
    pass
2371

    
2372
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2373

    
2374
def resource_post_save(sender, instance, created, **kwargs):
2375
    pass
2376

    
2377
post_save.connect(resource_post_save, sender=Resource)
2378

    
2379
def renew_token(sender, instance, **kwargs):
2380
    if not instance.auth_token:
2381
        instance.renew_token()
2382
pre_save.connect(renew_token, sender=AstakosUser)
2383
pre_save.connect(renew_token, sender=Service)