Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 570015d2

History | View | Annotate | Download (80.4 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
        )
263

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

    
272
    return _DEFAULT_QUOTA
273

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

    
280

    
281
class AstakosUserManager(UserManager):
282

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

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

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

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

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

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

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

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

    
335

    
336

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

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

    
353

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

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

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

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

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

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

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

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

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

    
397
    objects = AstakosUserManager()
398

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

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

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

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

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

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

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

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

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

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

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

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

    
493
    def remove_resource_policy(self, service, resource):
494
        """Raises ObjectDoesNotExist, IntegrityError"""
495
        resource = Resource.objects.get(service__name=service, name=resource)
496
        q = self.policies.get(resource=resource).delete()
497

    
498
    def update_uuid(self):
499
        while not self.uuid:
500
            uuid_val =  str(uuid.uuid4())
501
            try:
502
                AstakosUser.objects.get(uuid=uuid_val)
503
            except AstakosUser.DoesNotExist, e:
504
                self.uuid = uuid_val
505
        return self.uuid
506

    
507
    def save(self, update_timestamps=True, **kwargs):
508
        if update_timestamps:
509
            if not self.id:
510
                self.date_joined = datetime.now()
511
            self.updated = datetime.now()
512

    
513
        # update date_signed_terms if necessary
514
        if self.__has_signed_terms != self.has_signed_terms:
515
            self.date_signed_terms = datetime.now()
516

    
517
        self.update_uuid()
518

    
519
        if self.username != self.email.lower():
520
            # set username
521
            self.username = self.email.lower()
522

    
523
        super(AstakosUser, self).save(**kwargs)
524

    
525
    def renew_token(self, flush_sessions=False, current_key=None):
526
        md5 = hashlib.md5()
527
        md5.update(settings.SECRET_KEY)
528
        md5.update(self.username)
529
        md5.update(self.realname.encode('ascii', 'ignore'))
530
        md5.update(asctime())
531

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

    
541
    def flush_sessions(self, current_key=None):
542
        q = self.sessions
543
        if current_key:
544
            q = q.exclude(session_key=current_key)
545

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

    
555
    def __unicode__(self):
556
        return '%s (%s)' % (self.realname, self.email)
557

    
558
    def conflicting_email(self):
559
        q = AstakosUser.objects.exclude(username=self.username)
560
        q = q.filter(email__iexact=self.email)
561
        if q.count() != 0:
562
            return True
563
        return False
564

    
565
    def email_change_is_pending(self):
566
        return self.emailchanges.count() > 0
567

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

    
584
    def set_invitations_level(self):
585
        """
586
        Update user invitation level
587
        """
588
        level = self.invitation.inviter.level + 1
589
        self.level = level
590
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
591

    
592
    def can_change_password(self):
593
        return self.has_auth_provider('local', auth_backend='astakos')
594

    
595
    def can_change_email(self):
596
        if not self.has_auth_provider('local'):
597
            return True
598

    
599
        local = self.get_auth_provider('local')._instance
600
        return local.auth_backend == 'astakos'
601

    
602
    # Auth providers related methods
603
    def get_auth_provider(self, module=None, identifier=None, **filters):
604
        if not module:
605
            return self.auth_providers.active()[0].settings
606

    
607
        params = {'module': module}
608
        if identifier:
609
            params['identifier'] = identifier
610
        params.update(filters)
611
        return self.auth_providers.active().get(**params).settings
612

    
613
    def has_auth_provider(self, provider, **kwargs):
614
        return bool(self.auth_providers.active().filter(module=provider,
615
                                                        **kwargs).count())
616

    
617
    def get_required_providers(self, **kwargs):
618
        return auth.REQUIRED_PROVIDERS.keys()
619

    
620
    def missing_required_providers(self):
621
        required = self.get_required_providers()
622
        missing = []
623
        for provider in required:
624
            if not self.has_auth_provider(provider):
625
                missing.append(auth.get_provider(provider, self))
626
        return missing
627

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

    
638
        for p in providers:
639
            if p.get_add_policy:
640
                available.append(p)
641
        return available
642

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

    
651
    def get_enabled_auth_providers(self, **filters):
652
        providers = self.get_auth_providers(**filters)
653
        enabled = []
654
        for p in providers:
655
            if p.get_login_policy:
656
                enabled.append(p)
657
        return enabled
658

    
659
    def get_auth_providers(self, **filters):
660
        providers = []
661
        for provider in self.auth_providers.active(**filters):
662
            if provider.settings.module_enabled:
663
                providers.append(provider.settings)
664

    
665
        modules = astakos_settings.IM_MODULES
666

    
667
        def key(p):
668
            if not p.module in modules:
669
                return 100
670
            return modules.index(p.module)
671

    
672
        providers = sorted(providers, key=key)
673
        return providers
674

    
675
    # URL methods
676
    @property
677
    def auth_providers_display(self):
678
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
679
                         self.get_enabled_auth_providers()])
680

    
681
    def add_auth_provider(self, module='local', identifier=None, **params):
682
        provider = auth.get_provider(module, self, identifier, **params)
683
        provider.add_to_user()
684

    
685
    def get_resend_activation_url(self):
686
        return reverse('send_activation', kwargs={'user_id': self.pk})
687

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

    
695
    def get_password_reset_url(self, token_generator=default_token_generator):
696
        return reverse('django.contrib.auth.views.password_reset_confirm',
697
                          kwargs={'uidb36':int_to_base36(self.id),
698
                                  'token':token_generator.make_token(self)})
699

    
700
    def get_inactive_message(self, provider_module, identifier=None):
701
        provider = self.get_auth_provider(provider_module, identifier)
702

    
703
        msg_extra = ''
704
        message = ''
705

    
706
        msg_inactive = provider.get_account_inactive_msg
707
        msg_pending = provider.get_pending_activation_msg
708
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
709
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
710
        msg_pending_mod = provider.get_pending_moderation_msg
711
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
712

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

    
731
        return mark_safe(message + u' '+ msg_extra)
732

    
733
    def owns_application(self, application):
734
        return application.owner == self
735

    
736
    def owns_project(self, project):
737
        return project.application.owner == self
738

    
739
    def is_associated(self, project):
740
        try:
741
            m = ProjectMembership.objects.get(person=self, project=project)
742
            return m.state in ProjectMembership.ASSOCIATED_STATES
743
        except ProjectMembership.DoesNotExist:
744
            return False
745

    
746
    def get_membership(self, project):
747
        try:
748
            return ProjectMembership.objects.get(
749
                project=project,
750
                person=self)
751
        except ProjectMembership.DoesNotExist:
752
            return None
753

    
754
    def membership_display(self, project):
755
        m = self.get_membership(project)
756
        if m is None:
757
            return _('Not a member')
758
        else:
759
            return m.user_friendly_state_display()
760

    
761
    def non_owner_can_view(self, maybe_project):
762
        if self.is_project_admin():
763
            return True
764
        if maybe_project is None:
765
            return False
766
        project = maybe_project
767
        if self.is_associated(project):
768
            return True
769
        if project.is_deactivated():
770
            return False
771
        return True
772

    
773
    def settings(self):
774
        return UserSetting.objects.filter(user=self)
775

    
776
    def all_quotas(self):
777
        quotas = users_quotas([self])
778
        try:
779
            return quotas[self.uuid]
780
        except:
781
            raise ValueError("could not compute quotas")
782

    
783

    
784
def initial_quotas(users):
785
    initial = {}
786
    default_quotas = get_default_quota()
787

    
788
    for user in users:
789
        uuid = user.uuid
790
        initial[uuid] = dict(default_quotas)
791

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

    
801
    return initial
802

    
803

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

    
810
    objs = ProjectMembership.objects.select_related('application', 'person')
811
    memberships = objs.filter(person__in=users)
812

    
813
    apps = set(m.application for m in memberships if m.application is not None)
814
    objs = ProjectResourceGrant.objects.select_related()
815
    grants = objs.filter(project_application__in=apps)
816

    
817
    for membership in memberships:
818
        uuid = membership.person.uuid
819
        userquotas = quotas.get(uuid, {})
820

    
821
        application = membership.application
822
        if application is None:
823
            continue
824

    
825
        for grant in grants:
826
            if grant.project_application_id != application.id:
827
                continue
828
            resource = grant.resource.full_name()
829
            prev = userquotas.get(resource, 0)
830
            new = add_quota_values(prev, grant.member_quota_values())
831
            userquotas[resource] = new
832
        quotas[uuid] = userquotas
833

    
834
    return quotas
835

    
836

    
837
class AstakosUserAuthProviderManager(models.Manager):
838

    
839
    def active(self, **filters):
840
        return self.filter(active=True, **filters)
841

    
842
    def remove_unverified_providers(self, provider, **filters):
843
        try:
844
            existing = self.filter(module=provider, user__email_verified=False,
845
                                   **filters)
846
            for p in existing:
847
                p.user.delete()
848
        except:
849
            pass
850

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

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

    
865

    
866
class AuthProviderPolicyProfileManager(models.Manager):
867

    
868
    def active(self):
869
        return self.filter(active=True)
870

    
871
    def for_user(self, user, provider):
872
        policies = {}
873
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
874
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
875
        exclusive_q = exclusive_q1 | exclusive_q2
876

    
877
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
878
            policies.update(profile.policies)
879

    
880
        user_groups = user.groups.all().values('pk')
881
        for profile in self.active().filter(groups__in=user_groups).filter(
882
                exclusive_q):
883
            policies.update(profile.policies)
884
        return policies
885

    
886
    def add_policy(self, name, provider, group_or_user, exclusive=False,
887
                   **policies):
888
        is_group = isinstance(group_or_user, Group)
889
        profile, created = self.get_or_create(name=name, provider=provider,
890
                                              is_exclusive=exclusive)
891
        profile.is_exclusive = exclusive
892
        profile.save()
893
        if is_group:
894
            profile.groups.add(group_or_user)
895
        else:
896
            profile.users.add(group_or_user)
897
        profile.set_policies(policies)
898
        profile.save()
899
        return profile
900

    
901

    
902
class AuthProviderPolicyProfile(models.Model):
903
    name = models.CharField(_('Name'), max_length=255, blank=False,
904
                            null=False, db_index=True)
905
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
906
                                null=False)
907

    
908
    # apply policies to all providers excluding the one set in provider field
909
    is_exclusive = models.BooleanField(default=False)
910

    
911
    policy_add = models.NullBooleanField(null=True, default=None)
912
    policy_remove = models.NullBooleanField(null=True, default=None)
913
    policy_create = models.NullBooleanField(null=True, default=None)
914
    policy_login = models.NullBooleanField(null=True, default=None)
915
    policy_limit = models.IntegerField(null=True, default=None)
916
    policy_required = models.NullBooleanField(null=True, default=None)
917
    policy_automoderate = models.NullBooleanField(null=True, default=None)
918
    policy_switch = models.NullBooleanField(null=True, default=None)
919

    
920
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
921
                     'automoderate')
922

    
923
    priority = models.IntegerField(null=False, default=1)
924
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
925
    users = models.ManyToManyField(AstakosUser,
926
                                   related_name='authpolicy_profiles')
927
    active = models.BooleanField(default=True)
928

    
929
    objects = AuthProviderPolicyProfileManager()
930

    
931
    class Meta:
932
        ordering = ['priority']
933

    
934
    @property
935
    def policies(self):
936
        policies = {}
937
        for pkey in self.POLICY_FIELDS:
938
            value = getattr(self, 'policy_%s' % pkey, None)
939
            if value is None:
940
                continue
941
            policies[pkey] = value
942
        return policies
943

    
944
    def set_policies(self, policies_dict):
945
        for key, value in policies_dict.iteritems():
946
            if key in self.POLICY_FIELDS:
947
                setattr(self, 'policy_%s' % key, value)
948
        return self.policies
949

    
950

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

    
969
    objects = AstakosUserAuthProviderManager()
970

    
971
    class Meta:
972
        unique_together = (('identifier', 'module', 'user'), )
973
        ordering = ('module', 'created')
974

    
975
    def __init__(self, *args, **kwargs):
976
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
977
        try:
978
            self.info = json.loads(self.info_data)
979
            if not self.info:
980
                self.info = {}
981
        except Exception, e:
982
            self.info = {}
983

    
984
        for key,value in self.info.iteritems():
985
            setattr(self, 'info_%s' % key, value)
986

    
987
    @property
988
    def settings(self):
989
        extra_data = {}
990

    
991
        info_data = {}
992
        if self.info_data:
993
            info_data = json.loads(self.info_data)
994

    
995
        extra_data['info'] = info_data
996

    
997
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
998
            extra_data[key] = getattr(self, key)
999

    
1000
        extra_data['instance'] = self
1001
        return auth.get_provider(self.module, self.user,
1002
                                           self.identifier, **extra_data)
1003

    
1004
    def __repr__(self):
1005
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
1006

    
1007
    def __unicode__(self):
1008
        if self.identifier:
1009
            return "%s:%s" % (self.module, self.identifier)
1010
        if self.auth_backend:
1011
            return "%s:%s" % (self.module, self.auth_backend)
1012
        return self.module
1013

    
1014
    def save(self, *args, **kwargs):
1015
        self.info_data = json.dumps(self.info)
1016
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1017

    
1018

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

    
1046
    update_or_create = _update_or_create
1047

    
1048

    
1049
class AstakosUserQuota(models.Model):
1050
    objects = ExtendedManager()
1051
    capacity = intDecimalField()
1052
    quantity = intDecimalField(default=0)
1053
    export_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1054
    import_limit = intDecimalField(default=QH_PRACTICALLY_INFINITE)
1055
    resource = models.ForeignKey(Resource)
1056
    user = models.ForeignKey(AstakosUser)
1057

    
1058
    class Meta:
1059
        unique_together = ("resource", "user")
1060

    
1061
    def quota_values(self):
1062
        return QuotaValues(
1063
            quantity = self.quantity,
1064
            capacity = self.capacity,
1065
            )
1066

    
1067

    
1068
class ApprovalTerms(models.Model):
1069
    """
1070
    Model for approval terms
1071
    """
1072

    
1073
    date = models.DateTimeField(
1074
        _('Issue date'), db_index=True, auto_now_add=True)
1075
    location = models.CharField(_('Terms location'), max_length=255)
1076

    
1077

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

    
1091
    def __init__(self, *args, **kwargs):
1092
        super(Invitation, self).__init__(*args, **kwargs)
1093
        if not self.id:
1094
            self.code = _generate_invitation_code()
1095

    
1096
    def consume(self):
1097
        self.is_consumed = True
1098
        self.consumed = datetime.now()
1099
        self.save()
1100

    
1101
    def __unicode__(self):
1102
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1103

    
1104

    
1105
class EmailChangeManager(models.Manager):
1106

    
1107
    @transaction.commit_on_success
1108
    def change_email(self, activation_key):
1109
        """
1110
        Validate an activation key and change the corresponding
1111
        ``User`` if valid.
1112

1113
        If the key is valid and has not expired, return the ``User``
1114
        after activating.
1115

1116
        If the key is not valid or has expired, return ``None``.
1117

1118
        If the key is valid but the ``User`` is already active,
1119
        return ``None``.
1120

1121
        After successful email change the activation record is deleted.
1122

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

    
1152

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

    
1165
    objects = EmailChangeManager()
1166

    
1167
    def get_url(self):
1168
        return reverse('email_change_confirm',
1169
                      kwargs={'activation_key': self.activation_key})
1170

    
1171
    def activation_key_expired(self):
1172
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1173
        return self.requested_at + expiration_date < datetime.now()
1174

    
1175

    
1176
class AdditionalMail(models.Model):
1177
    """
1178
    Model for registring invitations
1179
    """
1180
    owner = models.ForeignKey(AstakosUser)
1181
    email = models.EmailField()
1182

    
1183

    
1184
def _generate_invitation_code():
1185
    while True:
1186
        code = randint(1, 2L ** 63 - 1)
1187
        try:
1188
            Invitation.objects.get(code=code)
1189
            # An invitation with this code already exists, try again
1190
        except Invitation.DoesNotExist:
1191
            return code
1192

    
1193

    
1194
def get_latest_terms():
1195
    try:
1196
        term = ApprovalTerms.objects.order_by('-id')[0]
1197
        return term
1198
    except IndexError:
1199
        pass
1200
    return None
1201

    
1202

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

    
1222
    class Meta:
1223
        unique_together = ("provider", "third_party_identifier")
1224

    
1225
    def get_user_instance(self):
1226
        d = self.__dict__
1227
        d.pop('_state', None)
1228
        d.pop('id', None)
1229
        d.pop('token', None)
1230
        d.pop('created', None)
1231
        d.pop('info', None)
1232
        user = AstakosUser(**d)
1233

    
1234
        return user
1235

    
1236
    @property
1237
    def realname(self):
1238
        return '%s %s' %(self.first_name, self.last_name)
1239

    
1240
    @realname.setter
1241
    def realname(self, value):
1242
        parts = value.split(' ')
1243
        if len(parts) == 2:
1244
            self.first_name = parts[0]
1245
            self.last_name = parts[1]
1246
        else:
1247
            self.last_name = parts[0]
1248

    
1249
    def save(self, **kwargs):
1250
        if not self.id:
1251
            # set username
1252
            while not self.username:
1253
                username =  uuid.uuid4().hex[:30]
1254
                try:
1255
                    AstakosUser.objects.get(username = username)
1256
                except AstakosUser.DoesNotExist, e:
1257
                    self.username = username
1258
        super(PendingThirdPartyUser, self).save(**kwargs)
1259

    
1260
    def generate_token(self):
1261
        self.password = self.third_party_identifier
1262
        self.last_login = datetime.now()
1263
        self.token = default_token_generator.make_token(self)
1264

    
1265
    def existing_user(self):
1266
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1267
                                         auth_providers__identifier=self.third_party_identifier)
1268

    
1269
    def get_provider(self, user):
1270
        params = {
1271
            'info_data': self.info,
1272
            'affiliation': self.affiliation
1273
        }
1274
        return auth.get_provider(self.provider, user,
1275
                                 self.third_party_identifier, **params)
1276

    
1277
class SessionCatalog(models.Model):
1278
    session_key = models.CharField(_('session key'), max_length=40)
1279
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1280

    
1281

    
1282
class UserSetting(models.Model):
1283
    user = models.ForeignKey(AstakosUser)
1284
    setting = models.CharField(max_length=255)
1285
    value = models.IntegerField()
1286

    
1287
    objects = ForUpdateManager()
1288

    
1289
    class Meta:
1290
        unique_together = ("user", "setting")
1291

    
1292

    
1293
### PROJECTS ###
1294
################
1295

    
1296
class ChainManager(ForUpdateManager):
1297

    
1298
    def search_by_name(self, *search_strings):
1299
        projects = Project.objects.search_by_name(*search_strings)
1300
        chains = [p.id for p in projects]
1301
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1302
        apps = (app for app in apps if app.is_latest())
1303
        app_chains = [app.chain for app in apps if app.chain not in chains]
1304
        return chains + app_chains
1305

    
1306
    def all_full_state(self):
1307
        chains = self.all()
1308
        cids = [c.chain for c in chains]
1309
        projects = Project.objects.select_related('application').in_bulk(cids)
1310

    
1311
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1312
        chain_latest = dict(objs.values_list('chain', 'latest'))
1313

    
1314
        objs = ProjectApplication.objects.select_related('applicant')
1315
        apps = objs.in_bulk(chain_latest.values())
1316

    
1317
        d = {}
1318
        for chain in chains:
1319
            pk = chain.pk
1320
            project = projects.get(pk, None)
1321
            app = apps[chain_latest[pk]]
1322
            d[chain.pk] = chain.get_state(project, app)
1323

    
1324
        return d
1325

    
1326
    def of_project(self, project):
1327
        if project is None:
1328
            return None
1329
        try:
1330
            return self.get(chain=project.id)
1331
        except Chain.DoesNotExist:
1332
            raise AssertionError('project with no chain')
1333

    
1334

    
1335
class Chain(models.Model):
1336
    chain  =   models.AutoField(primary_key=True)
1337

    
1338
    def __str__(self):
1339
        return "%s" % (self.chain,)
1340

    
1341
    objects = ChainManager()
1342

    
1343
    PENDING            = 0
1344
    DENIED             = 3
1345
    DISMISSED          = 4
1346
    CANCELLED          = 5
1347

    
1348
    APPROVED           = 10
1349
    APPROVED_PENDING   = 11
1350
    SUSPENDED          = 12
1351
    SUSPENDED_PENDING  = 13
1352
    TERMINATED         = 14
1353
    TERMINATED_PENDING = 15
1354

    
1355
    PENDING_STATES = [PENDING,
1356
                      APPROVED_PENDING,
1357
                      SUSPENDED_PENDING,
1358
                      TERMINATED_PENDING,
1359
                      ]
1360

    
1361
    MODIFICATION_STATES = [APPROVED_PENDING,
1362
                           SUSPENDED_PENDING,
1363
                           TERMINATED_PENDING,
1364
                           ]
1365

    
1366
    RELEVANT_STATES = [PENDING,
1367
                       DENIED,
1368
                       APPROVED,
1369
                       APPROVED_PENDING,
1370
                       SUSPENDED,
1371
                       SUSPENDED_PENDING,
1372
                       TERMINATED_PENDING,
1373
                       ]
1374

    
1375
    SKIP_STATES = [DISMISSED,
1376
                   CANCELLED,
1377
                   TERMINATED]
1378

    
1379
    STATE_DISPLAY = {
1380
        PENDING            : _("Pending"),
1381
        DENIED             : _("Denied"),
1382
        DISMISSED          : _("Dismissed"),
1383
        CANCELLED          : _("Cancelled"),
1384
        APPROVED           : _("Active"),
1385
        APPROVED_PENDING   : _("Active - Pending"),
1386
        SUSPENDED          : _("Suspended"),
1387
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1388
        TERMINATED         : _("Terminated"),
1389
        TERMINATED_PENDING : _("Terminated - Pending"),
1390
        }
1391

    
1392

    
1393
    @classmethod
1394
    def _chain_state(cls, project_state, app_state):
1395
        s = CHAIN_STATE.get((project_state, app_state), None)
1396
        if s is None:
1397
            raise AssertionError('inconsistent chain state')
1398
        return s
1399

    
1400
    @classmethod
1401
    def chain_state(cls, project, app):
1402
        p_state = project.state if project else None
1403
        return cls._chain_state(p_state, app.state)
1404

    
1405
    @classmethod
1406
    def state_display(cls, s):
1407
        if s is None:
1408
            return _("Unknown")
1409
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1410

    
1411
    def last_application(self):
1412
        return self.chained_apps.order_by('-id')[0]
1413

    
1414
    def get_project(self):
1415
        try:
1416
            return self.chained_project
1417
        except Project.DoesNotExist:
1418
            return None
1419

    
1420
    def get_elements(self):
1421
        project = self.get_project()
1422
        app = self.last_application()
1423
        return project, app
1424

    
1425
    def get_state(self, project, app):
1426
        s = self.chain_state(project, app)
1427
        return s, project, app
1428

    
1429
    def full_state(self):
1430
        project, app = self.get_elements()
1431
        return self.get_state(project, app)
1432

    
1433

    
1434
def new_chain():
1435
    c = Chain.objects.create()
1436
    return c
1437

    
1438

    
1439
class ProjectApplicationManager(ForUpdateManager):
1440

    
1441
    def user_visible_projects(self, *filters, **kw_filters):
1442
        model = self.model
1443
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1444

    
1445
    def user_visible_by_chain(self, flt):
1446
        model = self.model
1447
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1448
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1449
        by_chain = dict(pending.annotate(models.Max('id')))
1450
        by_chain.update(approved.annotate(models.Max('id')))
1451
        return self.filter(flt, id__in=by_chain.values())
1452

    
1453
    def user_accessible_projects(self, user):
1454
        """
1455
        Return projects accessed by specified user.
1456
        """
1457
        if user.is_project_admin():
1458
            participates_filters = Q()
1459
        else:
1460
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1461
                                   Q(project__projectmembership__person=user)
1462

    
1463
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1464

    
1465
    def search_by_name(self, *search_strings):
1466
        q = Q()
1467
        for s in search_strings:
1468
            q = q | Q(name__icontains=s)
1469
        return self.filter(q)
1470

    
1471
    def latest_of_chain(self, chain_id):
1472
        try:
1473
            return self.filter(chain=chain_id).order_by('-id')[0]
1474
        except IndexError:
1475
            return None
1476

    
1477

    
1478
class ProjectApplication(models.Model):
1479
    applicant               =   models.ForeignKey(
1480
                                    AstakosUser,
1481
                                    related_name='projects_applied',
1482
                                    db_index=True)
1483

    
1484
    PENDING     =    0
1485
    APPROVED    =    1
1486
    REPLACED    =    2
1487
    DENIED      =    3
1488
    DISMISSED   =    4
1489
    CANCELLED   =    5
1490

    
1491
    state                   =   models.IntegerField(default=PENDING,
1492
                                                    db_index=True)
1493

    
1494
    owner                   =   models.ForeignKey(
1495
                                    AstakosUser,
1496
                                    related_name='projects_owned',
1497
                                    db_index=True)
1498

    
1499
    chain                   =   models.ForeignKey(Chain,
1500
                                                  related_name='chained_apps',
1501
                                                  db_column='chain')
1502
    precursor_application   =   models.ForeignKey('ProjectApplication',
1503
                                                  null=True,
1504
                                                  blank=True)
1505

    
1506
    name                    =   models.CharField(max_length=80)
1507
    homepage                =   models.URLField(max_length=255, null=True,
1508
                                                verify_exists=False)
1509
    description             =   models.TextField(null=True, blank=True)
1510
    start_date              =   models.DateTimeField(null=True, blank=True)
1511
    end_date                =   models.DateTimeField()
1512
    member_join_policy      =   models.IntegerField()
1513
    member_leave_policy     =   models.IntegerField()
1514
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1515
    resource_grants         =   models.ManyToManyField(
1516
                                    Resource,
1517
                                    null=True,
1518
                                    blank=True,
1519
                                    through='ProjectResourceGrant')
1520
    comments                =   models.TextField(null=True, blank=True)
1521
    issue_date              =   models.DateTimeField(auto_now_add=True)
1522
    response_date           =   models.DateTimeField(null=True, blank=True)
1523
    response                =   models.TextField(null=True, blank=True)
1524

    
1525
    objects                 =   ProjectApplicationManager()
1526

    
1527
    # Compiled queries
1528
    Q_PENDING  = Q(state=PENDING)
1529
    Q_APPROVED = Q(state=APPROVED)
1530
    Q_DENIED   = Q(state=DENIED)
1531

    
1532
    class Meta:
1533
        unique_together = ("chain", "id")
1534

    
1535
    def __unicode__(self):
1536
        return "%s applied by %s" % (self.name, self.applicant)
1537

    
1538
    # TODO: Move to a more suitable place
1539
    APPLICATION_STATE_DISPLAY = {
1540
        PENDING  : _('Pending review'),
1541
        APPROVED : _('Approved'),
1542
        REPLACED : _('Replaced'),
1543
        DENIED   : _('Denied'),
1544
        DISMISSED: _('Dismissed'),
1545
        CANCELLED: _('Cancelled')
1546
    }
1547

    
1548
    @property
1549
    def log_display(self):
1550
        return "application %s (%s) for project %s" % (
1551
            self.id, self.name, self.chain)
1552

    
1553
    def get_project(self):
1554
        try:
1555
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1556
            return Project
1557
        except Project.DoesNotExist, e:
1558
            return None
1559

    
1560
    def state_display(self):
1561
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1562

    
1563
    def project_state_display(self):
1564
        try:
1565
            project = self.project
1566
            return project.state_display()
1567
        except Project.DoesNotExist:
1568
            return self.state_display()
1569

    
1570
    def add_resource_policy(self, service, resource, uplimit):
1571
        """Raises ObjectDoesNotExist, IntegrityError"""
1572
        q = self.projectresourcegrant_set
1573
        resource = Resource.objects.get(service__name=service, name=resource)
1574
        q.create(resource=resource, member_capacity=uplimit)
1575

    
1576
    def members_count(self):
1577
        return self.project.approved_memberships.count()
1578

    
1579
    @property
1580
    def grants(self):
1581
        return self.projectresourcegrant_set.values(
1582
            'member_capacity', 'resource__name', 'resource__service__name')
1583

    
1584
    @property
1585
    def resource_policies(self):
1586
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1587

    
1588
    @resource_policies.setter
1589
    def resource_policies(self, policies):
1590
        for p in policies:
1591
            service = p.get('service', None)
1592
            resource = p.get('resource', None)
1593
            uplimit = p.get('uplimit', 0)
1594
            self.add_resource_policy(service, resource, uplimit)
1595

    
1596
    def pending_modifications_incl_me(self):
1597
        q = self.chained_applications()
1598
        q = q.filter(Q(state=self.PENDING))
1599
        return q
1600

    
1601
    def last_pending_incl_me(self):
1602
        try:
1603
            return self.pending_modifications_incl_me().order_by('-id')[0]
1604
        except IndexError:
1605
            return None
1606

    
1607
    def pending_modifications(self):
1608
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1609

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

    
1616
    def is_modification(self):
1617
        # if self.state != self.PENDING:
1618
        #     return False
1619
        parents = self.chained_applications().filter(id__lt=self.id)
1620
        parents = parents.filter(state__in=[self.APPROVED])
1621
        return parents.count() > 0
1622

    
1623
    def chained_applications(self):
1624
        return ProjectApplication.objects.filter(chain=self.chain)
1625

    
1626
    def is_latest(self):
1627
        return self.chained_applications().order_by('-id')[0] == self
1628

    
1629
    def has_pending_modifications(self):
1630
        return bool(self.last_pending())
1631

    
1632
    def denied_modifications(self):
1633
        q = self.chained_applications()
1634
        q = q.filter(Q(state=self.DENIED))
1635
        q = q.filter(~Q(id=self.id))
1636
        return q
1637

    
1638
    def last_denied(self):
1639
        try:
1640
            return self.denied_modifications().order_by('-id')[0]
1641
        except IndexError:
1642
            return None
1643

    
1644
    def has_denied_modifications(self):
1645
        return bool(self.last_denied())
1646

    
1647
    def is_applied(self):
1648
        try:
1649
            self.project
1650
            return True
1651
        except Project.DoesNotExist:
1652
            return False
1653

    
1654
    def get_project(self):
1655
        try:
1656
            return Project.objects.get(id=self.chain)
1657
        except Project.DoesNotExist:
1658
            return None
1659

    
1660
    def project_exists(self):
1661
        return self.get_project() is not None
1662

    
1663
    def _get_project_for_update(self):
1664
        try:
1665
            objects = Project.objects
1666
            project = objects.get_for_update(id=self.chain)
1667
            return project
1668
        except Project.DoesNotExist:
1669
            return None
1670

    
1671
    def can_cancel(self):
1672
        return self.state == self.PENDING
1673

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

    
1680
        self.state = self.CANCELLED
1681
        self.save()
1682

    
1683
    def can_dismiss(self):
1684
        return self.state == self.DENIED
1685

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

    
1692
        self.state = self.DISMISSED
1693
        self.save()
1694

    
1695
    def can_deny(self):
1696
        return self.state == self.PENDING
1697

    
1698
    def deny(self, reason):
1699
        if not self.can_deny():
1700
            m = _("cannot deny: application '%s' in state '%s'") % (
1701
                    self.id, self.state)
1702
            raise AssertionError(m)
1703

    
1704
        self.state = self.DENIED
1705
        self.response_date = datetime.now()
1706
        self.response = reason
1707
        self.save()
1708

    
1709
    def can_approve(self):
1710
        return self.state == self.PENDING
1711

    
1712
    def approve(self, approval_user=None):
1713
        """
1714
        If approval_user then during owner membership acceptance
1715
        it is checked whether the request_user is eligible.
1716

1717
        Raises:
1718
            PermissionDenied
1719
        """
1720

    
1721
        if not transaction.is_managed():
1722
            raise AssertionError("NOPE")
1723

    
1724
        new_project_name = self.name
1725
        if not self.can_approve():
1726
            m = _("cannot approve: project '%s' in state '%s'") % (
1727
                    new_project_name, self.state)
1728
            raise AssertionError(m) # invalid argument
1729

    
1730
        now = datetime.now()
1731
        project = self._get_project_for_update()
1732

    
1733
        try:
1734
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1735
            conflicting_project = Project.objects.get(q)
1736
            if (conflicting_project != project):
1737
                m = (_("cannot approve: project with name '%s' "
1738
                       "already exists (id: %s)") % (
1739
                        new_project_name, conflicting_project.id))
1740
                raise PermissionDenied(m) # invalid argument
1741
        except Project.DoesNotExist:
1742
            pass
1743

    
1744
        new_project = False
1745
        if project is None:
1746
            new_project = True
1747
            project = Project(id=self.chain)
1748

    
1749
        project.name = new_project_name
1750
        project.application = self
1751
        project.last_approval_date = now
1752
        if not new_project:
1753
            project.is_modified = True
1754

    
1755
        project.save()
1756

    
1757
        self.state = self.APPROVED
1758
        self.response_date = now
1759
        self.save()
1760
        return project
1761

    
1762
    @property
1763
    def member_join_policy_display(self):
1764
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1765

    
1766
    @property
1767
    def member_leave_policy_display(self):
1768
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1769

    
1770
class ProjectResourceGrant(models.Model):
1771

    
1772
    resource                =   models.ForeignKey(Resource)
1773
    project_application     =   models.ForeignKey(ProjectApplication,
1774
                                                  null=True)
1775
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1776
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1777
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1778
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1779
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1780
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1781

    
1782
    objects = ExtendedManager()
1783

    
1784
    class Meta:
1785
        unique_together = ("resource", "project_application")
1786

    
1787
    def member_quota_values(self):
1788
        return QuotaValues(
1789
            quantity = 0,
1790
            capacity = self.member_capacity,
1791
            )
1792

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

    
1806
    def __str__(self):
1807
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1808
                                        self.display_member_capacity())
1809

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

    
1834

    
1835
class ProjectManager(ForUpdateManager):
1836

    
1837
    def terminated_projects(self):
1838
        q = self.model.Q_TERMINATED
1839
        return self.filter(q)
1840

    
1841
    def not_terminated_projects(self):
1842
        q = ~self.model.Q_TERMINATED
1843
        return self.filter(q)
1844

    
1845
    def deactivated_projects(self):
1846
        q = self.model.Q_DEACTIVATED
1847
        return self.filter(q)
1848

    
1849
    def modified_projects(self):
1850
        return self.filter(is_modified=True)
1851

    
1852
    def expired_projects(self):
1853
        q = (~Q(state=Project.TERMINATED) &
1854
              Q(application__end_date__lt=datetime.now()))
1855
        return self.filter(q)
1856

    
1857
    def search_by_name(self, *search_strings):
1858
        q = Q()
1859
        for s in search_strings:
1860
            q = q | Q(name__icontains=s)
1861
        return self.filter(q)
1862

    
1863

    
1864
class Project(models.Model):
1865

    
1866
    id                          =   models.OneToOneField(Chain,
1867
                                                      related_name='chained_project',
1868
                                                      db_column='id',
1869
                                                      primary_key=True)
1870

    
1871
    application                 =   models.OneToOneField(
1872
                                            ProjectApplication,
1873
                                            related_name='project')
1874
    last_approval_date          =   models.DateTimeField(null=True)
1875

    
1876
    members                     =   models.ManyToManyField(
1877
                                            AstakosUser,
1878
                                            through='ProjectMembership')
1879

    
1880
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1881
    deactivation_date           =   models.DateTimeField(null=True)
1882

    
1883
    creation_date               =   models.DateTimeField(auto_now_add=True)
1884
    name                        =   models.CharField(
1885
                                            max_length=80,
1886
                                            null=True,
1887
                                            db_index=True,
1888
                                            unique=True)
1889

    
1890
    APPROVED    = 1
1891
    SUSPENDED   = 10
1892
    TERMINATED  = 100
1893

    
1894
    is_modified                 =   models.BooleanField(default=False,
1895
                                                        db_index=True)
1896
    is_active                   =   models.BooleanField(default=True,
1897
                                                        db_index=True)
1898
    state                       =   models.IntegerField(default=APPROVED,
1899
                                                        db_index=True)
1900

    
1901
    objects     =   ProjectManager()
1902

    
1903
    # Compiled queries
1904
    Q_TERMINATED  = Q(state=TERMINATED)
1905
    Q_SUSPENDED   = Q(state=SUSPENDED)
1906
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1907

    
1908
    def __str__(self):
1909
        return uenc(_("<project %s '%s'>") %
1910
                    (self.id, udec(self.application.name)))
1911

    
1912
    __repr__ = __str__
1913

    
1914
    def __unicode__(self):
1915
        return _("<project %s '%s'>") % (self.id, self.application.name)
1916

    
1917
    STATE_DISPLAY = {
1918
        APPROVED   : 'Active',
1919
        SUSPENDED  : 'Suspended',
1920
        TERMINATED : 'Terminated'
1921
        }
1922

    
1923
    def state_display(self):
1924
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1925

    
1926
    def expiration_info(self):
1927
        return (str(self.id), self.name, self.state_display(),
1928
                str(self.application.end_date))
1929

    
1930
    def is_deactivated(self, reason=None):
1931
        if reason is not None:
1932
            return self.state == reason
1933

    
1934
        return self.state != self.APPROVED
1935

    
1936
    ### Deactivation calls
1937

    
1938
    def terminate(self):
1939
        self.deactivation_reason = 'TERMINATED'
1940
        self.deactivation_date = datetime.now()
1941
        self.state = self.TERMINATED
1942
        self.name = None
1943
        self.save()
1944

    
1945
    def suspend(self):
1946
        self.deactivation_reason = 'SUSPENDED'
1947
        self.deactivation_date = datetime.now()
1948
        self.state = self.SUSPENDED
1949
        self.save()
1950

    
1951
    def resume(self):
1952
        self.deactivation_reason = None
1953
        self.deactivation_date = None
1954
        self.state = self.APPROVED
1955
        self.save()
1956

    
1957
    ### Logical checks
1958

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

    
1966
    def is_active_strict(self):
1967
        return self.is_active and self.state == self.APPROVED
1968

    
1969
    def is_approved(self):
1970
        return self.state == self.APPROVED
1971

    
1972
    @property
1973
    def is_alive(self):
1974
        return not self.is_terminated
1975

    
1976
    @property
1977
    def is_terminated(self):
1978
        return self.is_deactivated(self.TERMINATED)
1979

    
1980
    @property
1981
    def is_suspended(self):
1982
        return self.is_deactivated(self.SUSPENDED)
1983

    
1984
    def violates_resource_grants(self):
1985
        return False
1986

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

    
1994

    
1995
    ### Other
1996

    
1997
    def count_pending_memberships(self):
1998
        memb_set = self.projectmembership_set
1999
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
2000
        return memb_count
2001

    
2002
    def members_count(self):
2003
        return self.approved_memberships.count()
2004

    
2005
    @property
2006
    def approved_memberships(self):
2007
        query = ProjectMembership.Q_ACCEPTED_STATES
2008
        return self.projectmembership_set.filter(query)
2009

    
2010
    @property
2011
    def approved_members(self):
2012
        return [m.person for m in self.approved_memberships]
2013

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

    
2023
        m, created = ProjectMembership.objects.get_or_create(
2024
            person=user, project=self
2025
        )
2026
        m.accept()
2027

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

    
2038
        m = ProjectMembership.objects.get(person=user, project=self)
2039
        m.remove()
2040

    
2041

    
2042
CHAIN_STATE = {
2043
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
2044
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
2045
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
2046
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
2047
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
2048

    
2049
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
2050
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
2051
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
2052
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
2053
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
2054

    
2055
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
2056
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
2057
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
2058
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
2059
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
2060

    
2061
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
2062
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
2063
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
2064
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
2065
    }
2066

    
2067

    
2068
class ProjectMembershipManager(ForUpdateManager):
2069

    
2070
    def any_accepted(self):
2071
        q = self.model.Q_ACTUALLY_ACCEPTED
2072
        return self.filter(q)
2073

    
2074
    def actually_accepted(self):
2075
        q = self.model.Q_ACTUALLY_ACCEPTED
2076
        return self.filter(q)
2077

    
2078
    def requested(self):
2079
        return self.filter(state=ProjectMembership.REQUESTED)
2080

    
2081
    def suspended(self):
2082
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
2083

    
2084
class ProjectMembership(models.Model):
2085

    
2086
    person              =   models.ForeignKey(AstakosUser)
2087
    request_date        =   models.DateField(auto_now_add=True)
2088
    project             =   models.ForeignKey(Project)
2089

    
2090
    REQUESTED           =   0
2091
    ACCEPTED            =   1
2092
    LEAVE_REQUESTED     =   5
2093
    # User deactivation
2094
    USER_SUSPENDED      =   10
2095

    
2096
    REMOVED             =   200
2097

    
2098
    ASSOCIATED_STATES   =   set([REQUESTED,
2099
                                 ACCEPTED,
2100
                                 LEAVE_REQUESTED,
2101
                                 USER_SUSPENDED,
2102
                                 ])
2103

    
2104
    ACCEPTED_STATES     =   set([ACCEPTED,
2105
                                 LEAVE_REQUESTED,
2106
                                 USER_SUSPENDED,
2107
                                 ])
2108

    
2109
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2110

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

    
2125
    acceptance_date     =   models.DateField(null=True, db_index=True)
2126
    leave_request_date  =   models.DateField(null=True)
2127

    
2128
    objects     =   ProjectMembershipManager()
2129

    
2130
    # Compiled queries
2131
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2132
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2133

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

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

    
2150
    def state_display(self):
2151
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2152

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

    
2156
    class Meta:
2157
        unique_together = ("person", "project")
2158
        #index_together = [["project", "state"]]
2159

    
2160
    def __str__(self):
2161
        return uenc(_("<'%s' membership in '%s'>") % (
2162
                self.person.username, self.project))
2163

    
2164
    __repr__ = __str__
2165

    
2166
    def __init__(self, *args, **kwargs):
2167
        self.state = self.REQUESTED
2168
        super(ProjectMembership, self).__init__(*args, **kwargs)
2169

    
2170
    def _set_history_item(self, reason, date=None):
2171
        if isinstance(reason, basestring):
2172
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2173

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

    
2183
    def can_accept(self):
2184
        return self.state == self.REQUESTED
2185

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

    
2191
        now = datetime.now()
2192
        self.acceptance_date = now
2193
        self._set_history_item(reason='ACCEPT', date=now)
2194
        self.state = self.ACCEPTED
2195
        self.save()
2196

    
2197
    def can_leave(self):
2198
        return self.state in self.ACCEPTED_STATES
2199

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

    
2206
        self.leave_request_date = datetime.now()
2207
        self.state = self.LEAVE_REQUESTED
2208
        self.save()
2209

    
2210
    def can_deny_leave(self):
2211
        return self.state == self.LEAVE_REQUESTED
2212

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

    
2219
        self.leave_request_date = None
2220
        self.state = self.ACCEPTED
2221
        self.save()
2222

    
2223
    def can_cancel_leave(self):
2224
        return self.state == self.LEAVE_REQUESTED
2225

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

    
2232
        self.leave_request_date = None
2233
        self.state = self.ACCEPTED
2234
        self.save()
2235

    
2236
    def can_remove(self):
2237
        return self.state in self.ACCEPTED_STATES
2238

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

    
2244
        self._set_history_item(reason='REMOVE')
2245
        self.state = self.REMOVED
2246
        self.save()
2247

    
2248
    def can_reject(self):
2249
        return self.state == self.REQUESTED
2250

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

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

    
2261
    def can_cancel(self):
2262
        return self.state == self.REQUESTED
2263

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

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

    
2274
    def get_diff_quotas(self, sub_list=None, add_list=None,
2275
                        pending_application=None):
2276
        if sub_list is None:
2277
            sub_list = []
2278

    
2279
        if add_list is None:
2280
            add_list = []
2281

    
2282
        sub_append = sub_list.append
2283
        add_append = add_list.append
2284
        holder = self.person.uuid
2285

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

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

    
2305
        return (sub_list, add_list)
2306

    
2307
    def get_pending_application(self):
2308
        project = self.project
2309
        if project.is_deactivated():
2310
            return None
2311
        if self.state not in self.ACTUALLY_ACCEPTED:
2312
            return None
2313
        return project.application
2314

    
2315

    
2316
class Serial(models.Model):
2317
    serial  =   models.AutoField(primary_key=True)
2318

    
2319

    
2320
def sync_users(users, sync=True):
2321
    def _sync_users(users, sync):
2322

    
2323
        info = {}
2324
        for user in users:
2325
            info[user.uuid] = user.email
2326

    
2327
        resources = get_resource_names()
2328
        qh_limits, qh_counters = qh_get_quotas(users, resources)
2329
        astakos_initial = initial_quotas(users)
2330
        astakos_quotas = users_quotas(users)
2331

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

    
2338
        if sync:
2339
            r = send_quotas(diff_quotas)
2340

    
2341
        return (qh_limits, qh_counters,
2342
                astakos_initial, diff_quotas, info)
2343

    
2344
    return _sync_users(users, sync)
2345

    
2346

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

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

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

    
2361
### SIGNALS ###
2362
################
2363

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

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

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

    
2388
def astakosuser_post_save(sender, instance, created, **kwargs):
2389
    pass
2390

    
2391
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2392

    
2393
def resource_post_save(sender, instance, created, **kwargs):
2394
    pass
2395

    
2396
post_save.connect(resource_post_save, sender=Resource)
2397

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