Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 992b81b6

History | View | Annotate | Download (82.2 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
    QuotaValues, add_quota_values)
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
def _quota_values(capacity):
259
    return QuotaValues(
260
        quantity = 0,
261
        capacity = capacity,
262
        import_limit = QH_PRACTICALLY_INFINITE,
263
        export_limit = QH_PRACTICALLY_INFINITE)
264

    
265
def get_default_quota():
266
    _DEFAULT_QUOTA = {}
267
    resources = Resource.objects.select_related('service').all()
268
    for resource in resources:
269
        capacity = resource.uplimit
270
        limits = _quota_values(capacity)
271
        _DEFAULT_QUOTA[resource.full_name()] = limits
272

    
273
    return _DEFAULT_QUOTA
274

    
275
def get_resource_names():
276
    _RESOURCE_NAMES = []
277
    resources = Resource.objects.select_related('service').all()
278
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
279
    return _RESOURCE_NAMES
280

    
281

    
282
class AstakosUserManager(UserManager):
283

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

    
293
    def get_by_email(self, email):
294
        return self.get(email=email)
295

    
296
    def get_by_identifier(self, email_or_username, **kwargs):
297
        try:
298
            return self.get(email__iexact=email_or_username, **kwargs)
299
        except AstakosUser.DoesNotExist:
300
            return self.get(username__iexact=email_or_username, **kwargs)
301

    
302
    def user_exists(self, email_or_username, **kwargs):
303
        qemail = Q(email__iexact=email_or_username)
304
        qusername = Q(username__iexact=email_or_username)
305
        qextra = Q(**kwargs)
306
        return self.filter((qemail | qusername) & qextra).exists()
307

    
308
    def verified_user_exists(self, email_or_username):
309
        return self.user_exists(email_or_username, email_verified=True)
310

    
311
    def verified(self):
312
        return self.filter(email_verified=True)
313

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

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

    
336

    
337

    
338
class AstakosUser(User):
339
    """
340
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
341
    """
342
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
343
                                   null=True)
344

    
345
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
346
    #                    AstakosUserProvider model.
347
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
348
                                null=True)
349
    # ex. screen_name for twitter, eppn for shibboleth
350
    third_party_identifier = models.CharField(_('Third-party identifier'),
351
                                              max_length=255, null=True,
352
                                              blank=True)
353

    
354

    
355
    #for invitations
356
    user_level = DEFAULT_USER_LEVEL
357
    level = models.IntegerField(_('Inviter level'), default=user_level)
358
    invitations = models.IntegerField(
359
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
360

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

    
375
    updated = models.DateTimeField(_('Update date'))
376
    is_verified = models.BooleanField(_('Is verified?'), default=False)
377

    
378
    email_verified = models.BooleanField(_('Email verified?'), default=False)
379

    
380
    has_credits = models.BooleanField(_('Has credits?'), default=False)
381
    has_signed_terms = models.BooleanField(
382
        _('I agree with the terms'), default=False)
383
    date_signed_terms = models.DateTimeField(
384
        _('Signed terms date'), null=True, blank=True)
385

    
386
    activation_sent = models.DateTimeField(
387
        _('Activation sent data'), null=True, blank=True)
388

    
389
    policy = models.ManyToManyField(
390
        Resource, null=True, through='AstakosUserQuota')
391

    
392
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
393

    
394
    __has_signed_terms = False
395
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
396
                                           default=False, db_index=True)
397

    
398
    objects = AstakosUserManager()
399

    
400
    def __init__(self, *args, **kwargs):
401
        super(AstakosUser, self).__init__(*args, **kwargs)
402
        self.__has_signed_terms = self.has_signed_terms
403
        if not self.id:
404
            self.is_active = False
405

    
406
    @property
407
    def realname(self):
408
        return '%s %s' % (self.first_name, self.last_name)
409

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

    
418
    @realname.setter
419
    def realname(self, value):
420
        parts = value.split(' ')
421
        if len(parts) == 2:
422
            self.first_name = parts[0]
423
            self.last_name = parts[1]
424
        else:
425
            self.last_name = parts[0]
426

    
427
    def add_permission(self, pname):
428
        if self.has_perm(pname):
429
            return
430
        p, created = Permission.objects.get_or_create(
431
                                    codename=pname,
432
                                    name=pname.capitalize(),
433
                                    content_type=get_content_type())
434
        self.user_permissions.add(p)
435

    
436
    def remove_permission(self, pname):
437
        if self.has_perm(pname):
438
            return
439
        p = Permission.objects.get(codename=pname,
440
                                   content_type=get_content_type())
441
        self.user_permissions.remove(p)
442

    
443
    def is_project_admin(self, application_id=None):
444
        return self.uuid in PROJECT_ADMINS
445

    
446
    @property
447
    def invitation(self):
448
        try:
449
            return Invitation.objects.get(username=self.email)
450
        except Invitation.DoesNotExist:
451
            return None
452

    
453
    @property
454
    def policies(self):
455
        return self.astakosuserquota_set.select_related().all()
456

    
457
    @policies.setter
458
    def policies(self, policies):
459
        for p in policies:
460
            p.setdefault('resource', '')
461
            p.setdefault('capacity', 0)
462
            p.setdefault('quantity', 0)
463
            p.setdefault('import_limit', 0)
464
            p.setdefault('export_limit', 0)
465
            p.setdefault('update', True)
466
            self.add_resource_policy(**p)
467

    
468
    def add_resource_policy(
469
            self, resource, capacity, quantity, import_limit,
470
            export_limit, update=True):
471
        """Raises ObjectDoesNotExist, IntegrityError"""
472
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
473
        resource = Resource.objects.get(service__name=s, name=r)
474
        if update:
475
            AstakosUserQuota.objects.update_or_create(
476
                user=self, resource=resource, defaults={
477
                    'capacity':capacity,
478
                    'quantity': quantity,
479
                    'import_limit':import_limit,
480
                    'export_limit':export_limit})
481
        else:
482
            q = self.astakosuserquota_set
483
            q.create(
484
                resource=resource, capacity=capacity, quanity=quantity,
485
                import_limit=import_limit, export_limit=export_limit)
486

    
487
    def get_resource_policy(self, resource):
488
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
489
        resource = Resource.objects.get(service__name=s, name=r)
490
        default_capacity = resource.uplimit
491
        try:
492
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
493
            return policy, default_capacity
494
        except AstakosUserQuota.DoesNotExist:
495
            return None, default_capacity
496

    
497
    def remove_resource_policy(self, service, resource):
498
        """Raises ObjectDoesNotExist, IntegrityError"""
499
        resource = Resource.objects.get(service__name=service, name=resource)
500
        q = self.policies.get(resource=resource).delete()
501

    
502
    def update_uuid(self):
503
        while not self.uuid:
504
            uuid_val =  str(uuid.uuid4())
505
            try:
506
                AstakosUser.objects.get(uuid=uuid_val)
507
            except AstakosUser.DoesNotExist, e:
508
                self.uuid = uuid_val
509
        return self.uuid
510

    
511
    def save(self, update_timestamps=True, **kwargs):
512
        if update_timestamps:
513
            if not self.id:
514
                self.date_joined = datetime.now()
515
            self.updated = datetime.now()
516

    
517
        # update date_signed_terms if necessary
518
        if self.__has_signed_terms != self.has_signed_terms:
519
            self.date_signed_terms = datetime.now()
520

    
521
        self.update_uuid()
522

    
523
        if self.username != self.email.lower():
524
            # set username
525
            self.username = self.email.lower()
526

    
527
        super(AstakosUser, self).save(**kwargs)
528

    
529
    def renew_token(self, flush_sessions=False, current_key=None):
530
        md5 = hashlib.md5()
531
        md5.update(settings.SECRET_KEY)
532
        md5.update(self.username)
533
        md5.update(self.realname.encode('ascii', 'ignore'))
534
        md5.update(asctime())
535

    
536
        self.auth_token = b64encode(md5.digest())
537
        self.auth_token_created = datetime.now()
538
        self.auth_token_expires = self.auth_token_created + \
539
                                  timedelta(hours=AUTH_TOKEN_DURATION)
540
        if flush_sessions:
541
            self.flush_sessions(current_key)
542
        msg = 'Token renewed for %s' % self.email
543
        logger.log(LOGGING_LEVEL, msg)
544

    
545
    def flush_sessions(self, current_key=None):
546
        q = self.sessions
547
        if current_key:
548
            q = q.exclude(session_key=current_key)
549

    
550
        keys = q.values_list('session_key', flat=True)
551
        if keys:
552
            msg = 'Flushing sessions: %s' % ','.join(keys)
553
            logger.log(LOGGING_LEVEL, msg, [])
554
        engine = import_module(settings.SESSION_ENGINE)
555
        for k in keys:
556
            s = engine.SessionStore(k)
557
            s.flush()
558

    
559
    def __unicode__(self):
560
        return '%s (%s)' % (self.realname, self.email)
561

    
562
    def conflicting_email(self):
563
        q = AstakosUser.objects.exclude(username=self.username)
564
        q = q.filter(email__iexact=self.email)
565
        if q.count() != 0:
566
            return True
567
        return False
568

    
569
    def email_change_is_pending(self):
570
        return self.emailchanges.count() > 0
571

    
572
    @property
573
    def signed_terms(self):
574
        term = get_latest_terms()
575
        if not term:
576
            return True
577
        if not self.has_signed_terms:
578
            return False
579
        if not self.date_signed_terms:
580
            return False
581
        if self.date_signed_terms < term.date:
582
            self.has_signed_terms = False
583
            self.date_signed_terms = None
584
            self.save()
585
            return False
586
        return True
587

    
588
    def set_invitations_level(self):
589
        """
590
        Update user invitation level
591
        """
592
        level = self.invitation.inviter.level + 1
593
        self.level = level
594
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
595

    
596
    def can_change_password(self):
597
        return self.has_auth_provider('local', auth_backend='astakos')
598

    
599
    def can_change_email(self):
600
        if not self.has_auth_provider('local'):
601
            return True
602

    
603
        local = self.get_auth_provider('local')._instance
604
        return local.auth_backend == 'astakos'
605

    
606
    # Auth providers related methods
607
    def get_auth_provider(self, module=None, identifier=None, **filters):
608
        if not module:
609
            return self.auth_providers.active()[0].settings
610

    
611
        params = {'module': module}
612
        if identifier:
613
            params['identifier'] = identifier
614
        params.update(filters)
615
        return self.auth_providers.active().get(**params).settings
616

    
617
    def has_auth_provider(self, provider, **kwargs):
618
        return bool(self.auth_providers.active().filter(module=provider,
619
                                                        **kwargs).count())
620

    
621
    def get_required_providers(self, **kwargs):
622
        return auth.REQUIRED_PROVIDERS.keys()
623

    
624
    def missing_required_providers(self):
625
        required = self.get_required_providers()
626
        missing = []
627
        for provider in required:
628
            if not self.has_auth_provider(provider):
629
                missing.append(auth.get_provider(provider, self))
630
        return missing
631

    
632
    def get_available_auth_providers(self, **filters):
633
        """
634
        Returns a list of providers available for add by the user.
635
        """
636
        modules = astakos_settings.IM_MODULES
637
        providers = []
638
        for p in modules:
639
            providers.append(auth.get_provider(p, self))
640
        available = []
641

    
642
        for p in providers:
643
            if p.get_add_policy:
644
                available.append(p)
645
        return available
646

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

    
655
    def get_enabled_auth_providers(self, **filters):
656
        providers = self.get_auth_providers(**filters)
657
        enabled = []
658
        for p in providers:
659
            if p.get_login_policy:
660
                enabled.append(p)
661
        return enabled
662

    
663
    def get_auth_providers(self, **filters):
664
        providers = []
665
        for provider in self.auth_providers.active(**filters):
666
            if provider.settings.module_enabled:
667
                providers.append(provider.settings)
668

    
669
        modules = astakos_settings.IM_MODULES
670

    
671
        def key(p):
672
            if not p.module in modules:
673
                return 100
674
            return modules.index(p.module)
675

    
676
        providers = sorted(providers, key=key)
677
        return providers
678

    
679
    # URL methods
680
    @property
681
    def auth_providers_display(self):
682
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
683
                         self.get_enabled_auth_providers()])
684

    
685
    def add_auth_provider(self, module='local', identifier=None, **params):
686
        provider = auth.get_provider(module, self, identifier, **params)
687
        provider.add_to_user()
688

    
689
    def get_resend_activation_url(self):
690
        return reverse('send_activation', kwargs={'user_id': self.pk})
691

    
692
    def get_activation_url(self, nxt=False):
693
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
694
                                 quote(self.auth_token))
695
        if nxt:
696
            url += "&next=%s" % quote(nxt)
697
        return url
698

    
699
    def get_password_reset_url(self, token_generator=default_token_generator):
700
        return reverse('django.contrib.auth.views.password_reset_confirm',
701
                          kwargs={'uidb36':int_to_base36(self.id),
702
                                  'token':token_generator.make_token(self)})
703

    
704
    def get_inactive_message(self, provider_module, identifier=None):
705
        provider = self.get_auth_provider(provider_module, identifier)
706

    
707
        msg_extra = ''
708
        message = ''
709

    
710
        msg_inactive = provider.get_account_inactive_msg
711
        msg_pending = provider.get_pending_activation_msg
712
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
713
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
714
        msg_pending_mod = provider.get_pending_moderation_msg
715
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
716

    
717
        if self.activation_sent:
718
            if self.email_verified:
719
                message = msg_inactive
720
            else:
721
                message = msg_pending
722
                url = self.get_resend_activation_url()
723
                msg_extra = msg_pending_help + \
724
                            u' ' + \
725
                            '<a href="%s">%s?</a>' % (url, msg_resend)
726
        else:
727
            if astakos_settings.MODERATION_ENABLED:
728
                message = msg_pending_mod
729
            else:
730
                message = msg_pending
731
                url = self.get_resend_activation_url()
732
                msg_extra = '<a href="%s">%s?</a>' % (url, \
733
                                msg_resend)
734

    
735
        return mark_safe(message + u' '+ msg_extra)
736

    
737
    def owns_application(self, application):
738
        return application.owner == self
739

    
740
    def owns_project(self, project):
741
        return project.application.owner == self
742

    
743
    def is_associated(self, project):
744
        try:
745
            m = ProjectMembership.objects.get(person=self, project=project)
746
            return m.state in ProjectMembership.ASSOCIATED_STATES
747
        except ProjectMembership.DoesNotExist:
748
            return False
749

    
750
    def get_membership(self, project):
751
        try:
752
            return ProjectMembership.objects.get(
753
                project=project,
754
                person=self)
755
        except ProjectMembership.DoesNotExist:
756
            return None
757

    
758
    def membership_display(self, project):
759
        m = self.get_membership(project)
760
        if m is None:
761
            return _('Not a member')
762
        else:
763
            return m.user_friendly_state_display()
764

    
765
    def non_owner_can_view(self, maybe_project):
766
        if self.is_project_admin():
767
            return True
768
        if maybe_project is None:
769
            return False
770
        project = maybe_project
771
        if self.is_associated(project):
772
            return True
773
        if project.is_deactivated():
774
            return False
775
        return True
776

    
777
    def settings(self):
778
        return UserSetting.objects.filter(user=self)
779

    
780
    def all_quotas(self):
781
        quotas = users_quotas([self])
782
        try:
783
            return quotas[self.uuid]
784
        except:
785
            raise ValueError("could not compute quotas")
786

    
787

    
788
def initial_quotas(users):
789
    initial = {}
790
    default_quotas = get_default_quota()
791

    
792
    for user in users:
793
        uuid = user.uuid
794
        initial[uuid] = dict(default_quotas)
795

    
796
    objs = AstakosUserQuota.objects.select_related()
797
    orig_quotas = objs.filter(user__in=users)
798
    for user_quota in orig_quotas:
799
        uuid = user_quota.user.uuid
800
        user_init = initial.get(uuid, {})
801
        resource = user_quota.resource.full_name()
802
        user_init[resource] = user_quota.quota_values()
803
        initial[uuid] = user_init
804

    
805
    return initial
806

    
807

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

    
814
    objs = ProjectMembership.objects.select_related('application', 'person')
815
    memberships = objs.filter(person__in=users, is_active=True)
816

    
817
    apps = set(m.application for m in memberships if m.application is not None)
818
    objs = ProjectResourceGrant.objects.select_related()
819
    grants = objs.filter(project_application__in=apps)
820

    
821
    for membership in memberships:
822
        uuid = membership.person.uuid
823
        userquotas = quotas.get(uuid, {})
824

    
825
        application = membership.application
826
        if application is None:
827
            m = _("missing application for active membership %s"
828
                  % (membership,))
829
            raise AssertionError(m)
830

    
831
        for grant in grants:
832
            if grant.project_application_id != application.id:
833
                continue
834
            resource = grant.resource.full_name()
835
            prev = userquotas.get(resource, 0)
836
            new = add_quota_values(prev, grant.member_quota_values())
837
            userquotas[resource] = new
838
        quotas[uuid] = userquotas
839

    
840
    return quotas
841

    
842

    
843
class AstakosUserAuthProviderManager(models.Manager):
844

    
845
    def active(self, **filters):
846
        return self.filter(active=True, **filters)
847

    
848
    def remove_unverified_providers(self, provider, **filters):
849
        try:
850
            existing = self.filter(module=provider, user__email_verified=False,
851
                                   **filters)
852
            for p in existing:
853
                p.user.delete()
854
        except:
855
            pass
856

    
857
    def unverified(self, provider, **filters):
858
        try:
859
            return self.get(module=provider, user__email_verified=False,
860
                            **filters).settings
861
        except AstakosUserAuthProvider.DoesNotExist:
862
            return None
863

    
864
    def verified(self, provider, **filters):
865
        try:
866
            return self.get(module=provider, user__email_verified=True,
867
                            **filters).settings
868
        except AstakosUserAuthProvider.DoesNotExist:
869
            return None
870

    
871

    
872
class AuthProviderPolicyProfileManager(models.Manager):
873

    
874
    def active(self):
875
        return self.filter(active=True)
876

    
877
    def for_user(self, user, provider):
878
        policies = {}
879
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
880
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
881
        exclusive_q = exclusive_q1 | exclusive_q2
882

    
883
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
884
            policies.update(profile.policies)
885

    
886
        user_groups = user.groups.all().values('pk')
887
        for profile in self.active().filter(groups__in=user_groups).filter(
888
                exclusive_q):
889
            policies.update(profile.policies)
890
        return policies
891

    
892
    def add_policy(self, name, provider, group_or_user, exclusive=False,
893
                   **policies):
894
        is_group = isinstance(group_or_user, Group)
895
        profile, created = self.get_or_create(name=name, provider=provider,
896
                                              is_exclusive=exclusive)
897
        profile.is_exclusive = exclusive
898
        profile.save()
899
        if is_group:
900
            profile.groups.add(group_or_user)
901
        else:
902
            profile.users.add(group_or_user)
903
        profile.set_policies(policies)
904
        profile.save()
905
        return profile
906

    
907

    
908
class AuthProviderPolicyProfile(models.Model):
909
    name = models.CharField(_('Name'), max_length=255, blank=False,
910
                            null=False, db_index=True)
911
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
912
                                null=False)
913

    
914
    # apply policies to all providers excluding the one set in provider field
915
    is_exclusive = models.BooleanField(default=False)
916

    
917
    policy_add = models.NullBooleanField(null=True, default=None)
918
    policy_remove = models.NullBooleanField(null=True, default=None)
919
    policy_create = models.NullBooleanField(null=True, default=None)
920
    policy_login = models.NullBooleanField(null=True, default=None)
921
    policy_limit = models.IntegerField(null=True, default=None)
922
    policy_required = models.NullBooleanField(null=True, default=None)
923
    policy_automoderate = models.NullBooleanField(null=True, default=None)
924
    policy_switch = models.NullBooleanField(null=True, default=None)
925

    
926
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
927
                     'automoderate')
928

    
929
    priority = models.IntegerField(null=False, default=1)
930
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
931
    users = models.ManyToManyField(AstakosUser,
932
                                   related_name='authpolicy_profiles')
933
    active = models.BooleanField(default=True)
934

    
935
    objects = AuthProviderPolicyProfileManager()
936

    
937
    class Meta:
938
        ordering = ['priority']
939

    
940
    @property
941
    def policies(self):
942
        policies = {}
943
        for pkey in self.POLICY_FIELDS:
944
            value = getattr(self, 'policy_%s' % pkey, None)
945
            if value is None:
946
                continue
947
            policies[pkey] = value
948
        return policies
949

    
950
    def set_policies(self, policies_dict):
951
        for key, value in policies_dict.iteritems():
952
            if key in self.POLICY_FIELDS:
953
                setattr(self, 'policy_%s' % key, value)
954
        return self.policies
955

    
956

    
957
class AstakosUserAuthProvider(models.Model):
958
    """
959
    Available user authentication methods.
960
    """
961
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
962
                                   null=True, default=None)
963
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
964
    module = models.CharField(_('Provider'), max_length=255, blank=False,
965
                                default='local')
966
    identifier = models.CharField(_('Third-party identifier'),
967
                                              max_length=255, null=True,
968
                                              blank=True)
969
    active = models.BooleanField(default=True)
970
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
971
                                   default='astakos')
972
    info_data = models.TextField(default="", null=True, blank=True)
973
    created = models.DateTimeField('Creation date', auto_now_add=True)
974

    
975
    objects = AstakosUserAuthProviderManager()
976

    
977
    class Meta:
978
        unique_together = (('identifier', 'module', 'user'), )
979
        ordering = ('module', 'created')
980

    
981
    def __init__(self, *args, **kwargs):
982
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
983
        try:
984
            self.info = json.loads(self.info_data)
985
            if not self.info:
986
                self.info = {}
987
        except Exception, e:
988
            self.info = {}
989

    
990
        for key,value in self.info.iteritems():
991
            setattr(self, 'info_%s' % key, value)
992

    
993
    @property
994
    def settings(self):
995
        extra_data = {}
996

    
997
        info_data = {}
998
        if self.info_data:
999
            info_data = json.loads(self.info_data)
1000

    
1001
        extra_data['info'] = info_data
1002

    
1003
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
1004
            extra_data[key] = getattr(self, key)
1005

    
1006
        extra_data['instance'] = self
1007
        return auth.get_provider(self.module, self.user,
1008
                                           self.identifier, **extra_data)
1009

    
1010
    def __repr__(self):
1011
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
1012

    
1013
    def __unicode__(self):
1014
        if self.identifier:
1015
            return "%s:%s" % (self.module, self.identifier)
1016
        if self.auth_backend:
1017
            return "%s:%s" % (self.module, self.auth_backend)
1018
        return self.module
1019

    
1020
    def save(self, *args, **kwargs):
1021
        self.info_data = json.dumps(self.info)
1022
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1023

    
1024

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

    
1052
    update_or_create = _update_or_create
1053

    
1054

    
1055
class AstakosUserQuota(models.Model):
1056
    objects = ExtendedManager()
1057
    capacity = intDecimalField()
1058
    quantity = intDecimalField(default=0)
1059
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1060
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1061
    resource = models.ForeignKey(Resource)
1062
    user = models.ForeignKey(AstakosUser)
1063

    
1064
    class Meta:
1065
        unique_together = ("resource", "user")
1066

    
1067
    def quota_values(self):
1068
        return QuotaValues(
1069
            quantity = self.quantity,
1070
            capacity = self.capacity,
1071
            import_limit = self.import_limit,
1072
            export_limit = self.export_limit)
1073

    
1074

    
1075
class ApprovalTerms(models.Model):
1076
    """
1077
    Model for approval terms
1078
    """
1079

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

    
1084

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

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

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

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

    
1111

    
1112
class EmailChangeManager(models.Manager):
1113

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

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

1123
        If the key is not valid or has expired, return ``None``.
1124

1125
        If the key is valid but the ``User`` is already active,
1126
        return ``None``.
1127

1128
        After successful email change the activation record is deleted.
1129

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

    
1159

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

    
1172
    objects = EmailChangeManager()
1173

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

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

    
1182

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

    
1190

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

    
1200

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

    
1209

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

    
1229
    class Meta:
1230
        unique_together = ("provider", "third_party_identifier")
1231

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

    
1241
        return user
1242

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

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

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

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

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

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

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

    
1288

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

    
1294
    objects = ForUpdateManager()
1295

    
1296
    class Meta:
1297
        unique_together = ("user", "setting")
1298

    
1299

    
1300
### PROJECTS ###
1301
################
1302

    
1303
class ChainManager(ForUpdateManager):
1304

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

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

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

    
1321
        objs = ProjectApplication.objects.select_related('applicant')
1322
        apps = objs.in_bulk(chain_latest.values())
1323

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

    
1331
        return d
1332

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

    
1341

    
1342
class Chain(models.Model):
1343
    chain  =   models.AutoField(primary_key=True)
1344

    
1345
    def __str__(self):
1346
        return "%s" % (self.chain,)
1347

    
1348
    objects = ChainManager()
1349

    
1350
    PENDING            = 0
1351
    DENIED             = 3
1352
    DISMISSED          = 4
1353
    CANCELLED          = 5
1354

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

    
1362
    PENDING_STATES = [PENDING,
1363
                      APPROVED_PENDING,
1364
                      SUSPENDED_PENDING,
1365
                      TERMINATED_PENDING,
1366
                      ]
1367

    
1368
    MODIFICATION_STATES = [APPROVED_PENDING,
1369
                           SUSPENDED_PENDING,
1370
                           TERMINATED_PENDING,
1371
                           ]
1372

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

    
1382
    SKIP_STATES = [DISMISSED,
1383
                   CANCELLED,
1384
                   TERMINATED]
1385

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

    
1399

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

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

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

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

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

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

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

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

    
1440

    
1441
def new_chain():
1442
    c = Chain.objects.create()
1443
    return c
1444

    
1445

    
1446
class ProjectApplicationManager(ForUpdateManager):
1447

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

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

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

    
1470
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1471

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

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

    
1484

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

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

    
1498
    state                   =   models.IntegerField(default=PENDING,
1499
                                                    db_index=True)
1500

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

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

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

    
1532
    objects                 =   ProjectApplicationManager()
1533

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

    
1539
    class Meta:
1540
        unique_together = ("chain", "id")
1541

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

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

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

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

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

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

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

    
1583
    def members_count(self):
1584
        return self.project.approved_memberships.count()
1585

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

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

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

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

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

    
1614
    def pending_modifications(self):
1615
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1616

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

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

    
1630
    def chained_applications(self):
1631
        return ProjectApplication.objects.filter(chain=self.chain)
1632

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

    
1636
    def has_pending_modifications(self):
1637
        return bool(self.last_pending())
1638

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

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

    
1651
    def has_denied_modifications(self):
1652
        return bool(self.last_denied())
1653

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

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

    
1667
    def project_exists(self):
1668
        return self.get_project() is not None
1669

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

    
1678
    def can_cancel(self):
1679
        return self.state == self.PENDING
1680

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

    
1687
        self.state = self.CANCELLED
1688
        self.save()
1689

    
1690
    def can_dismiss(self):
1691
        return self.state == self.DENIED
1692

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

    
1699
        self.state = self.DISMISSED
1700
        self.save()
1701

    
1702
    def can_deny(self):
1703
        return self.state == self.PENDING
1704

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

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

    
1716
    def can_approve(self):
1717
        return self.state == self.PENDING
1718

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

1724
        Raises:
1725
            PermissionDenied
1726
        """
1727

    
1728
        if not transaction.is_managed():
1729
            raise AssertionError("NOPE")
1730

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

    
1737
        now = datetime.now()
1738
        project = self._get_project_for_update()
1739

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

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

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

    
1762
        project.save()
1763

    
1764
        self.state = self.APPROVED
1765
        self.response_date = now
1766
        self.save()
1767

    
1768
    @property
1769
    def member_join_policy_display(self):
1770
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1771

    
1772
    @property
1773
    def member_leave_policy_display(self):
1774
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1775

    
1776
class ProjectResourceGrant(models.Model):
1777

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

    
1788
    objects = ExtendedManager()
1789

    
1790
    class Meta:
1791
        unique_together = ("resource", "project_application")
1792

    
1793
    def member_quota_values(self):
1794
        return QuotaValues(
1795
            quantity = 0,
1796
            capacity = self.member_capacity,
1797
            import_limit = self.member_import_limit,
1798
            export_limit = self.member_export_limit)
1799

    
1800
    def display_member_capacity(self):
1801
        if self.member_capacity:
1802
            if self.resource.unit:
1803
                return ProjectResourceGrant.display_filesize(
1804
                    self.member_capacity)
1805
            else:
1806
                if math.isinf(self.member_capacity):
1807
                    return 'Unlimited'
1808
                else:
1809
                    return self.member_capacity
1810
        else:
1811
            return 'Unlimited'
1812

    
1813
    def __str__(self):
1814
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1815
                                        self.display_member_capacity())
1816

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

    
1841

    
1842
class ProjectManager(ForUpdateManager):
1843

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

    
1848
    def not_terminated_projects(self):
1849
        q = ~self.model.Q_TERMINATED
1850
        return self.filter(q)
1851

    
1852
    def terminating_projects(self):
1853
        q = self.model.Q_TERMINATED & Q(is_active=True)
1854
        return self.filter(q)
1855

    
1856
    def deactivated_projects(self):
1857
        q = self.model.Q_DEACTIVATED
1858
        return self.filter(q)
1859

    
1860
    def deactivating_projects(self):
1861
        q = self.model.Q_DEACTIVATED & Q(is_active=True)
1862
        return self.filter(q)
1863

    
1864
    def modified_projects(self):
1865
        return self.filter(is_modified=True)
1866

    
1867
    def reactivating_projects(self):
1868
        return self.filter(state=Project.APPROVED, is_active=False)
1869

    
1870
    def expired_projects(self):
1871
        q = (~Q(state=Project.TERMINATED) &
1872
              Q(application__end_date__lt=datetime.now()))
1873
        return self.filter(q)
1874

    
1875
    def search_by_name(self, *search_strings):
1876
        q = Q()
1877
        for s in search_strings:
1878
            q = q | Q(name__icontains=s)
1879
        return self.filter(q)
1880

    
1881

    
1882
class Project(models.Model):
1883

    
1884
    id                          =   models.OneToOneField(Chain,
1885
                                                      related_name='chained_project',
1886
                                                      db_column='id',
1887
                                                      primary_key=True)
1888

    
1889
    application                 =   models.OneToOneField(
1890
                                            ProjectApplication,
1891
                                            related_name='project')
1892
    last_approval_date          =   models.DateTimeField(null=True)
1893

    
1894
    members                     =   models.ManyToManyField(
1895
                                            AstakosUser,
1896
                                            through='ProjectMembership')
1897

    
1898
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1899
    deactivation_date           =   models.DateTimeField(null=True)
1900

    
1901
    creation_date               =   models.DateTimeField(auto_now_add=True)
1902
    name                        =   models.CharField(
1903
                                            max_length=80,
1904
                                            null=True,
1905
                                            db_index=True,
1906
                                            unique=True)
1907

    
1908
    APPROVED    = 1
1909
    SUSPENDED   = 10
1910
    TERMINATED  = 100
1911

    
1912
    is_modified                 =   models.BooleanField(default=False,
1913
                                                        db_index=True)
1914
    is_active                   =   models.BooleanField(default=True,
1915
                                                        db_index=True)
1916
    state                       =   models.IntegerField(default=APPROVED,
1917
                                                        db_index=True)
1918

    
1919
    objects     =   ProjectManager()
1920

    
1921
    # Compiled queries
1922
    Q_TERMINATED  = Q(state=TERMINATED)
1923
    Q_SUSPENDED   = Q(state=SUSPENDED)
1924
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1925

    
1926
    def __str__(self):
1927
        return uenc(_("<project %s '%s'>") %
1928
                    (self.id, udec(self.application.name)))
1929

    
1930
    __repr__ = __str__
1931

    
1932
    def __unicode__(self):
1933
        return _("<project %s '%s'>") % (self.id, self.application.name)
1934

    
1935
    STATE_DISPLAY = {
1936
        APPROVED   : 'Active',
1937
        SUSPENDED  : 'Suspended',
1938
        TERMINATED : 'Terminated'
1939
        }
1940

    
1941
    def state_display(self):
1942
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1943

    
1944
    def admin_state_display(self):
1945
        s = self.state_display()
1946
        if self.sync_pending():
1947
            s += ' (sync pending)'
1948
        return s
1949

    
1950
    def sync_pending(self):
1951
        if self.state != self.APPROVED:
1952
            return self.is_active
1953
        return not self.is_active or self.is_modified
1954

    
1955
    def expiration_info(self):
1956
        return (str(self.id), self.name, self.state_display(),
1957
                str(self.application.end_date))
1958

    
1959
    def is_deactivated(self, reason=None):
1960
        if reason is not None:
1961
            return self.state == reason
1962

    
1963
        return self.state != self.APPROVED
1964

    
1965
    def is_deactivating(self, reason=None):
1966
        if not self.is_active:
1967
            return False
1968

    
1969
        return self.is_deactivated(reason)
1970

    
1971
    def is_deactivated_strict(self, reason=None):
1972
        if self.is_active:
1973
            return False
1974

    
1975
        return self.is_deactivated(reason)
1976

    
1977
    ### Deactivation calls
1978

    
1979
    def unset_modified(self):
1980
        self.is_modified = False
1981
        self.save()
1982

    
1983
    def deactivate(self):
1984
        self.deactivation_date = datetime.now()
1985
        self.is_active = False
1986
        self.save()
1987

    
1988
    def reactivate(self):
1989
        self.deactivation_date = None
1990
        self.is_active = True
1991
        self.save()
1992

    
1993
    def terminate(self):
1994
        self.deactivation_reason = 'TERMINATED'
1995
        self.state = self.TERMINATED
1996
        self.name = None
1997
        self.save()
1998

    
1999
    def suspend(self):
2000
        self.deactivation_reason = 'SUSPENDED'
2001
        self.state = self.SUSPENDED
2002
        self.save()
2003

    
2004
    def resume(self):
2005
        self.deactivation_reason = None
2006
        self.state = self.APPROVED
2007
        self.save()
2008

    
2009
    ### Logical checks
2010

    
2011
    def is_inconsistent(self):
2012
        now = datetime.now()
2013
        dates = [self.creation_date,
2014
                 self.last_approval_date,
2015
                 self.deactivation_date]
2016
        return any([date > now for date in dates])
2017

    
2018
    def is_active_strict(self):
2019
        return self.is_active and self.state == self.APPROVED
2020

    
2021
    def is_approved(self):
2022
        return self.state == self.APPROVED
2023

    
2024
    @property
2025
    def is_alive(self):
2026
        return not self.is_terminated
2027

    
2028
    @property
2029
    def is_terminated(self):
2030
        return self.is_deactivated(self.TERMINATED)
2031

    
2032
    @property
2033
    def is_suspended(self):
2034
        return self.is_deactivated(self.SUSPENDED)
2035

    
2036
    def violates_resource_grants(self):
2037
        return False
2038

    
2039
    def violates_members_limit(self, adding=0):
2040
        application = self.application
2041
        limit = application.limit_on_members_number
2042
        if limit is None:
2043
            return False
2044
        return (len(self.approved_members) + adding > limit)
2045

    
2046

    
2047
    ### Other
2048

    
2049
    def count_pending_memberships(self):
2050
        memb_set = self.projectmembership_set
2051
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2052
        return memb_count
2053

    
2054
    def members_count(self):
2055
        return self.approved_memberships.count()
2056

    
2057
    @property
2058
    def approved_memberships(self):
2059
        query = ProjectMembership.Q_ACCEPTED_STATES
2060
        return self.projectmembership_set.filter(query)
2061

    
2062
    @property
2063
    def approved_members(self):
2064
        return [m.person for m in self.approved_memberships]
2065

    
2066
    def add_member(self, user):
2067
        """
2068
        Raises:
2069
            django.exceptions.PermissionDenied
2070
            astakos.im.models.AstakosUser.DoesNotExist
2071
        """
2072
        if isinstance(user, (int, long)):
2073
            user = AstakosUser.objects.get(user=user)
2074

    
2075
        m, created = ProjectMembership.objects.get_or_create(
2076
            person=user, project=self
2077
        )
2078
        m.accept()
2079

    
2080
    def remove_member(self, user):
2081
        """
2082
        Raises:
2083
            django.exceptions.PermissionDenied
2084
            astakos.im.models.AstakosUser.DoesNotExist
2085
            astakos.im.models.ProjectMembership.DoesNotExist
2086
        """
2087
        if isinstance(user, (int, long)):
2088
            user = AstakosUser.objects.get(user=user)
2089

    
2090
        m = ProjectMembership.objects.get(person=user, project=self)
2091
        m.remove()
2092

    
2093

    
2094
CHAIN_STATE = {
2095
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2096
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2097
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2098
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2099
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2100

    
2101
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2102
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2103
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2104
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2105
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2106

    
2107
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2108
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2109
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2110
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2111
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2112

    
2113
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2114
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2115
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2116
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2117
    }
2118

    
2119

    
2120
class ProjectMembershipManager(ForUpdateManager):
2121

    
2122
    def any_accepted(self):
2123
        q = self.model.Q_ACTUALLY_ACCEPTED
2124
        return self.filter(q)
2125

    
2126
    def actually_accepted(self):
2127
        q = self.model.Q_ACTUALLY_ACCEPTED
2128
        return self.filter(q)
2129

    
2130
    def requested(self):
2131
        return self.filter(state=ProjectMembership.REQUESTED)
2132

    
2133
    def suspended(self):
2134
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2135

    
2136
class ProjectMembership(models.Model):
2137

    
2138
    person              =   models.ForeignKey(AstakosUser)
2139
    request_date        =   models.DateField(auto_now_add=True)
2140
    project             =   models.ForeignKey(Project)
2141

    
2142
    REQUESTED           =   0
2143
    ACCEPTED            =   1
2144
    LEAVE_REQUESTED     =   5
2145
    # User deactivation
2146
    USER_SUSPENDED      =   10
2147

    
2148
    REMOVED             =   200
2149

    
2150
    ASSOCIATED_STATES   =   set([REQUESTED,
2151
                                 ACCEPTED,
2152
                                 LEAVE_REQUESTED,
2153
                                 USER_SUSPENDED,
2154
                                 ])
2155

    
2156
    ACCEPTED_STATES     =   set([ACCEPTED,
2157
                                 LEAVE_REQUESTED,
2158
                                 USER_SUSPENDED,
2159
                                 ])
2160

    
2161
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2162

    
2163
    state               =   models.IntegerField(default=REQUESTED,
2164
                                                db_index=True)
2165
    is_pending          =   models.BooleanField(default=False, db_index=True)
2166
    is_active           =   models.BooleanField(default=False, db_index=True)
2167
    application         =   models.ForeignKey(
2168
                                ProjectApplication,
2169
                                null=True,
2170
                                related_name='memberships')
2171
    pending_application =   models.ForeignKey(
2172
                                ProjectApplication,
2173
                                null=True,
2174
                                related_name='pending_memberships')
2175
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
2176

    
2177
    acceptance_date     =   models.DateField(null=True, db_index=True)
2178
    leave_request_date  =   models.DateField(null=True)
2179

    
2180
    objects     =   ProjectMembershipManager()
2181

    
2182
    # Compiled queries
2183
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2184
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2185

    
2186
    MEMBERSHIP_STATE_DISPLAY = {
2187
        REQUESTED           : _('Requested'),
2188
        ACCEPTED            : _('Accepted'),
2189
        LEAVE_REQUESTED     : _('Leave Requested'),
2190
        USER_SUSPENDED      : _('Suspended'),
2191
        REMOVED             : _('Pending removal'),
2192
        }
2193

    
2194
    USER_FRIENDLY_STATE_DISPLAY = {
2195
        REQUESTED           : _('Join requested'),
2196
        ACCEPTED            : _('Accepted member'),
2197
        LEAVE_REQUESTED     : _('Requested to leave'),
2198
        USER_SUSPENDED      : _('Suspended member'),
2199
        REMOVED             : _('Pending removal'),
2200
        }
2201

    
2202
    def state_display(self):
2203
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2204

    
2205
    def user_friendly_state_display(self):
2206
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2207

    
2208
    class Meta:
2209
        unique_together = ("person", "project")
2210
        #index_together = [["project", "state"]]
2211

    
2212
    def __str__(self):
2213
        return uenc(_("<'%s' membership in '%s'>") % (
2214
                self.person.username, self.project))
2215

    
2216
    __repr__ = __str__
2217

    
2218
    def __init__(self, *args, **kwargs):
2219
        self.state = self.REQUESTED
2220
        super(ProjectMembership, self).__init__(*args, **kwargs)
2221

    
2222
    def _set_history_item(self, reason, date=None):
2223
        if isinstance(reason, basestring):
2224
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2225

    
2226
        history_item = ProjectMembershipHistory(
2227
                            serial=self.id,
2228
                            person=self.person_id,
2229
                            project=self.project_id,
2230
                            date=date or datetime.now(),
2231
                            reason=reason)
2232
        history_item.save()
2233
        serial = history_item.id
2234

    
2235
    def can_accept(self):
2236
        return self.state == self.REQUESTED
2237

    
2238
    def accept(self):
2239
        if not self.can_accept():
2240
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2241
            raise AssertionError(m)
2242

    
2243
        now = datetime.now()
2244
        self.acceptance_date = now
2245
        self._set_history_item(reason='ACCEPT', date=now)
2246
        self.state = self.ACCEPTED
2247
        self.save()
2248

    
2249
    def can_leave(self):
2250
        return self.state in self.ACCEPTED_STATES
2251

    
2252
    def leave_request(self):
2253
        if not self.can_leave():
2254
            m = _("%s: attempt to request to leave in state '%s'") % (
2255
                self, self.state)
2256
            raise AssertionError(m)
2257

    
2258
        self.leave_request_date = datetime.now()
2259
        self.state = self.LEAVE_REQUESTED
2260
        self.save()
2261

    
2262
    def can_deny_leave(self):
2263
        return self.state == self.LEAVE_REQUESTED
2264

    
2265
    def leave_request_deny(self):
2266
        if not self.can_deny_leave():
2267
            m = _("%s: attempt to deny leave request in state '%s'") % (
2268
                self, self.state)
2269
            raise AssertionError(m)
2270

    
2271
        self.leave_request_date = None
2272
        self.state = self.ACCEPTED
2273
        self.save()
2274

    
2275
    def can_cancel_leave(self):
2276
        return self.state == self.LEAVE_REQUESTED
2277

    
2278
    def leave_request_cancel(self):
2279
        if not self.can_cancel_leave():
2280
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2281
                self, self.state)
2282
            raise AssertionError(m)
2283

    
2284
        self.leave_request_date = None
2285
        self.state = self.ACCEPTED
2286
        self.save()
2287

    
2288
    def can_remove(self):
2289
        return self.state in self.ACCEPTED_STATES
2290

    
2291
    def remove(self):
2292
        if not self.can_remove():
2293
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2294
            raise AssertionError(m)
2295

    
2296
        self._set_history_item(reason='REMOVE')
2297
        self.state = self.REMOVED
2298
        self.save()
2299

    
2300
    def can_reject(self):
2301
        return self.state == self.REQUESTED
2302

    
2303
    def reject(self):
2304
        if not self.can_reject():
2305
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2306
            raise AssertionError(m)
2307

    
2308
        # rejected requests don't need sync,
2309
        # because they were never effected
2310
        self._set_history_item(reason='REJECT')
2311
        self.delete()
2312

    
2313
    def can_cancel(self):
2314
        return self.state == self.REQUESTED
2315

    
2316
    def cancel(self):
2317
        if not self.can_cancel():
2318
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2319
            raise AssertionError(m)
2320

    
2321
        # rejected requests don't need sync,
2322
        # because they were never effected
2323
        self._set_history_item(reason='CANCEL')
2324
        self.delete()
2325

    
2326
    def get_diff_quotas(self, sub_list=None, add_list=None):
2327
        if sub_list is None:
2328
            sub_list = []
2329

    
2330
        if add_list is None:
2331
            add_list = []
2332

    
2333
        sub_append = sub_list.append
2334
        add_append = add_list.append
2335
        holder = self.person.uuid
2336

    
2337
        synced_application = self.application
2338
        if synced_application is not None:
2339
            cur_grants = synced_application.projectresourcegrant_set.all()
2340
            for grant in cur_grants:
2341
                sub_append(QuotaLimits(
2342
                               holder       = holder,
2343
                               resource     = str(grant.resource),
2344
                               capacity     = grant.member_capacity,
2345
                               import_limit = grant.member_import_limit,
2346
                               export_limit = grant.member_export_limit))
2347

    
2348
        pending_application = self.pending_application
2349
        if pending_application is not None:
2350
            new_grants = pending_application.projectresourcegrant_set.all()
2351
            for new_grant in new_grants:
2352
                add_append(QuotaLimits(
2353
                               holder       = holder,
2354
                               resource     = str(new_grant.resource),
2355
                               capacity     = new_grant.member_capacity,
2356
                               import_limit = new_grant.member_import_limit,
2357
                               export_limit = new_grant.member_export_limit))
2358

    
2359
        return (sub_list, add_list)
2360

    
2361

    
2362
class Serial(models.Model):
2363
    serial  =   models.AutoField(primary_key=True)
2364

    
2365

    
2366
def sync_users(users, sync=True):
2367
    def _sync_users(users, sync):
2368

    
2369
        info = {}
2370
        for user in users:
2371
            info[user.uuid] = user.email
2372

    
2373
        resources = get_resource_names()
2374
        qh_limits, qh_counters = qh_get_quotas(users, resources)
2375
        astakos_initial = initial_quotas(users)
2376
        astakos_quotas = users_quotas(users)
2377

    
2378
        diff_quotas = {}
2379
        for holder, local in astakos_quotas.iteritems():
2380
            registered = qh_limits.get(holder, None)
2381
            if local != registered:
2382
                diff_quotas[holder] = dict(local)
2383

    
2384
        if sync:
2385
            r = send_quotas(diff_quotas)
2386

    
2387
        return (qh_limits, qh_counters,
2388
                astakos_initial, diff_quotas, info)
2389

    
2390
    return _sync_users(users, sync)
2391

    
2392

    
2393
def sync_all_users(sync=True):
2394
    users = AstakosUser.objects.verified()
2395
    return sync_users(users, sync)
2396

    
2397
class ProjectMembershipHistory(models.Model):
2398
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2399
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2400

    
2401
    person  =   models.BigIntegerField()
2402
    project =   models.BigIntegerField()
2403
    date    =   models.DateField(auto_now_add=True)
2404
    reason  =   models.IntegerField()
2405
    serial  =   models.BigIntegerField()
2406

    
2407
### SIGNALS ###
2408
################
2409

    
2410
def create_astakos_user(u):
2411
    try:
2412
        AstakosUser.objects.get(user_ptr=u.pk)
2413
    except AstakosUser.DoesNotExist:
2414
        extended_user = AstakosUser(user_ptr_id=u.pk)
2415
        extended_user.__dict__.update(u.__dict__)
2416
        extended_user.save()
2417
        if not extended_user.has_auth_provider('local'):
2418
            extended_user.add_auth_provider('local')
2419
    except BaseException, e:
2420
        logger.exception(e)
2421

    
2422
def fix_superusers():
2423
    # Associate superusers with AstakosUser
2424
    admins = User.objects.filter(is_superuser=True)
2425
    for u in admins:
2426
        create_astakos_user(u)
2427

    
2428
def user_post_save(sender, instance, created, **kwargs):
2429
    if not created:
2430
        return
2431
    create_astakos_user(instance)
2432
post_save.connect(user_post_save, sender=User)
2433

    
2434
def astakosuser_post_save(sender, instance, created, **kwargs):
2435
    pass
2436

    
2437
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2438

    
2439
def resource_post_save(sender, instance, created, **kwargs):
2440
    pass
2441

    
2442
post_save.connect(resource_post_save, sender=Resource)
2443

    
2444
def renew_token(sender, instance, **kwargs):
2445
    if not instance.auth_token:
2446
        instance.renew_token()
2447
pre_save.connect(renew_token, sender=AstakosUser)
2448
pre_save.connect(renew_token, sender=Service)