Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (68.1 kB)

1
# Copyright 2011-2012 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

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
70
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA)
71
from astakos.im import settings as astakos_settings
72
from astakos.im.endpoints.qh import (
73
    register_resources, qh_add_quota, QuotaLimits,
74
    qh_query_serials, qh_ack_serials)
75
from astakos.im import auth_providers
76

    
77
import astakos.im.messages as astakos_messages
78
from .managers import ForUpdateManager
79

    
80
from synnefo.lib.quotaholder.api import QH_PRACTICALLY_INFINITE
81
from synnefo.lib.db.intdecimalfield import intDecimalField
82

    
83
logger = logging.getLogger(__name__)
84

    
85
DEFAULT_CONTENT_TYPE = None
86
_content_type = None
87

    
88
def get_content_type():
89
    global _content_type
90
    if _content_type is not None:
91
        return _content_type
92

    
93
    try:
94
        content_type = ContentType.objects.get(app_label='im', model='astakosuser')
95
    except:
96
        content_type = DEFAULT_CONTENT_TYPE
97
    _content_type = content_type
98
    return content_type
99

    
100
RESOURCE_SEPARATOR = '.'
101

    
102
inf = float('inf')
103

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

    
115
    class Meta:
116
        ordering = ('order', )
117

    
118
    def renew_token(self, expiration_date=None):
119
        md5 = hashlib.md5()
120
        md5.update(self.name.encode('ascii', 'ignore'))
121
        md5.update(self.url.encode('ascii', 'ignore'))
122
        md5.update(asctime())
123

    
124
        self.auth_token = b64encode(md5.digest())
125
        self.auth_token_created = datetime.now()
126
        if expiration_date:
127
            self.auth_token_expires = expiration_date
128
        else:
129
            self.auth_token_expires = None
130

    
131
    def __str__(self):
132
        return self.name
133

    
134
    @property
135
    def resources(self):
136
        return self.resource_set.all()
137

    
138
    @resources.setter
139
    def resources(self, resources):
140
        for s in resources:
141
            self.resource_set.create(**s)
142

    
143

    
144
class ResourceMetadata(models.Model):
145
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
146
    value = models.CharField(_('Value'), max_length=255)
147

    
148
_presentation_data = {}
149
def get_presentation(resource):
150
    global _presentation_data
151
    presentation = _presentation_data.get(resource, {})
152
    if not presentation:
153
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
154
        presentation = resource_presentation.get(resource, {})
155
        _presentation_data[resource] = presentation
156
    return presentation
157

    
158
class Resource(models.Model):
159
    name = models.CharField(_('Name'), max_length=255)
160
    meta = models.ManyToManyField(ResourceMetadata)
161
    service = models.ForeignKey(Service)
162
    desc = models.TextField(_('Description'), null=True)
163
    unit = models.CharField(_('Name'), null=True, max_length=255)
164
    group = models.CharField(_('Group'), null=True, max_length=255)
165

    
166
    class Meta:
167
        unique_together = ("name", "service")
168

    
169
    def __str__(self):
170
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
171

    
172
    @property
173
    def help_text(self):
174
        return get_presentation(str(self)).get('help_text', '')
175

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

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

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

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

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

    
196

    
197
_default_quota = {}
198
def get_default_quota():
199
    global _default_quota
200
    if _default_quota:
201
        return _default_quota
202
    for s, data in SERVICES.iteritems():
203
        map(
204
            lambda d:_default_quota.update(
205
                {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
206
            ),
207
            data.get('resources', {})
208
        )
209
    return _default_quota
210

    
211

    
212
class AstakosUserManager(UserManager):
213

    
214
    def get_auth_provider_user(self, provider, **kwargs):
215
        """
216
        Retrieve AstakosUser instance associated with the specified third party
217
        id.
218
        """
219
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
220
                          kwargs.iteritems()))
221
        return self.get(auth_providers__module=provider, **kwargs)
222

    
223
    def get_by_email(self, email):
224
        return self.get(email=email)
225

    
226
    def get_by_identifier(self, email_or_username, **kwargs):
227
        try:
228
            return self.get(email__iexact=email_or_username, **kwargs)
229
        except AstakosUser.DoesNotExist:
230
            return self.get(username__iexact=email_or_username, **kwargs)
231

    
232
    def user_exists(self, email_or_username, **kwargs):
233
        qemail = Q(email__iexact=email_or_username)
234
        qusername = Q(username__iexact=email_or_username)
235
        qextra = Q(**kwargs)
236
        return self.filter((qemail | qusername) & qextra).exists()
237

    
238
    def verified_user_exists(self, email_or_username):
239
        return self.user_exists(email_or_username, email_verified=True)
240

    
241
    def verified(self):
242
        return self.filter(email_verified=True)
243

    
244
    def verified(self):
245
        return self.filter(email_verified=True)
246

    
247

    
248
class AstakosUser(User):
249
    """
250
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
251
    """
252
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
253
                                   null=True)
254

    
255
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
256
    #                    AstakosUserProvider model.
257
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
258
                                null=True)
259
    # ex. screen_name for twitter, eppn for shibboleth
260
    third_party_identifier = models.CharField(_('Third-party identifier'),
261
                                              max_length=255, null=True,
262
                                              blank=True)
263

    
264

    
265
    #for invitations
266
    user_level = DEFAULT_USER_LEVEL
267
    level = models.IntegerField(_('Inviter level'), default=user_level)
268
    invitations = models.IntegerField(
269
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
270

    
271
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
272
                                  null=True, blank=True)
273
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
274
    auth_token_expires = models.DateTimeField(
275
        _('Token expiration date'), null=True)
276

    
277
    updated = models.DateTimeField(_('Update date'))
278
    is_verified = models.BooleanField(_('Is verified?'), default=False)
279

    
280
    email_verified = models.BooleanField(_('Email verified?'), default=False)
281

    
282
    has_credits = models.BooleanField(_('Has credits?'), default=False)
283
    has_signed_terms = models.BooleanField(
284
        _('I agree with the terms'), default=False)
285
    date_signed_terms = models.DateTimeField(
286
        _('Signed terms date'), null=True, blank=True)
287

    
288
    activation_sent = models.DateTimeField(
289
        _('Activation sent data'), null=True, blank=True)
290

    
291
    policy = models.ManyToManyField(
292
        Resource, null=True, through='AstakosUserQuota')
293

    
294
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
295

    
296
    __has_signed_terms = False
297
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
298
                                           default=False, db_index=True)
299

    
300
    objects = AstakosUserManager()
301

    
302
    def __init__(self, *args, **kwargs):
303
        super(AstakosUser, self).__init__(*args, **kwargs)
304
        self.__has_signed_terms = self.has_signed_terms
305
        if not self.id:
306
            self.is_active = False
307

    
308
    @property
309
    def realname(self):
310
        return '%s %s' % (self.first_name, self.last_name)
311

    
312
    @realname.setter
313
    def realname(self, value):
314
        parts = value.split(' ')
315
        if len(parts) == 2:
316
            self.first_name = parts[0]
317
            self.last_name = parts[1]
318
        else:
319
            self.last_name = parts[0]
320

    
321
    def add_permission(self, pname):
322
        if self.has_perm(pname):
323
            return
324
        p, created = Permission.objects.get_or_create(
325
                                    codename=pname,
326
                                    name=pname.capitalize(),
327
                                    content_type=get_content_type())
328
        self.user_permissions.add(p)
329

    
330
    def remove_permission(self, pname):
331
        if self.has_perm(pname):
332
            return
333
        p = Permission.objects.get(codename=pname,
334
                                   content_type=get_content_type())
335
        self.user_permissions.remove(p)
336

    
337
    @property
338
    def invitation(self):
339
        try:
340
            return Invitation.objects.get(username=self.email)
341
        except Invitation.DoesNotExist:
342
            return None
343

    
344
    @property
345
    def quota(self):
346
        """Returns a dict with the sum of quota limits per resource"""
347
        d = defaultdict(int)
348
        default_quota = get_default_quota()
349
        d.update(default_quota)
350
        for q in self.policies:
351
            d[q.resource] = q.capacity or inf
352
        for m in self.projectmembership_set.select_related().all():
353
            if not m.acceptance_date:
354
                continue
355
            p = m.project
356
            if not p.is_active_strict():
357
                continue
358
            grants = p.application.projectresourcegrant_set.all()
359
            for g in grants:
360
                d[str(g.resource)] += g.member_capacity or inf
361
        return d
362

    
363
    @property
364
    def policies(self):
365
        return self.astakosuserquota_set.select_related().all()
366

    
367
    @policies.setter
368
    def policies(self, policies):
369
        for p in policies:
370
            p.setdefault('resource', '')
371
            p.setdefault('capacity', 0)
372
            p.setdefault('quantity', 0)
373
            p.setdefault('import_limit', 0)
374
            p.setdefault('export_limit', 0)
375
            p.setdefault('update', True)
376
            self.add_resource_policy(**p)
377

    
378
    def add_resource_policy(
379
            self, resource, capacity, quantity, import_limit,
380
            export_limit, update=True):
381
        """Raises ObjectDoesNotExist, IntegrityError"""
382
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
383
        resource = Resource.objects.get(service__name=s, name=r)
384
        if update:
385
            AstakosUserQuota.objects.update_or_create(
386
                user=self, resource=resource, defaults={
387
                    'capacity':capacity,
388
                    'quantity': quantity,
389
                    'import_limit':import_limit,
390
                    'export_limit':export_limit})
391
        else:
392
            q = self.astakosuserquota_set
393
            q.create(
394
                resource=resource, capacity=capacity, quanity=quantity,
395
                import_limit=import_limit, export_limit=export_limit)
396

    
397
    def remove_resource_policy(self, service, resource):
398
        """Raises ObjectDoesNotExist, IntegrityError"""
399
        resource = Resource.objects.get(service__name=service, name=resource)
400
        q = self.policies.get(resource=resource).delete()
401

    
402
    def update_uuid(self):
403
        while not self.uuid:
404
            uuid_val =  str(uuid.uuid4())
405
            try:
406
                AstakosUser.objects.get(uuid=uuid_val)
407
            except AstakosUser.DoesNotExist, e:
408
                self.uuid = uuid_val
409
        return self.uuid
410

    
411
    def save(self, update_timestamps=True, **kwargs):
412
        if update_timestamps:
413
            if not self.id:
414
                self.date_joined = datetime.now()
415
            self.updated = datetime.now()
416

    
417
        # update date_signed_terms if necessary
418
        if self.__has_signed_terms != self.has_signed_terms:
419
            self.date_signed_terms = datetime.now()
420

    
421
        self.update_uuid()
422

    
423
        if self.username != self.email.lower():
424
            # set username
425
            self.username = self.email.lower()
426

    
427
        super(AstakosUser, self).save(**kwargs)
428

    
429
    def renew_token(self, flush_sessions=False, current_key=None):
430
        md5 = hashlib.md5()
431
        md5.update(settings.SECRET_KEY)
432
        md5.update(self.username)
433
        md5.update(self.realname.encode('ascii', 'ignore'))
434
        md5.update(asctime())
435

    
436
        self.auth_token = b64encode(md5.digest())
437
        self.auth_token_created = datetime.now()
438
        self.auth_token_expires = self.auth_token_created + \
439
                                  timedelta(hours=AUTH_TOKEN_DURATION)
440
        if flush_sessions:
441
            self.flush_sessions(current_key)
442
        msg = 'Token renewed for %s' % self.email
443
        logger.log(LOGGING_LEVEL, msg)
444

    
445
    def flush_sessions(self, current_key=None):
446
        q = self.sessions
447
        if current_key:
448
            q = q.exclude(session_key=current_key)
449

    
450
        keys = q.values_list('session_key', flat=True)
451
        if keys:
452
            msg = 'Flushing sessions: %s' % ','.join(keys)
453
            logger.log(LOGGING_LEVEL, msg, [])
454
        engine = import_module(settings.SESSION_ENGINE)
455
        for k in keys:
456
            s = engine.SessionStore(k)
457
            s.flush()
458

    
459
    def __unicode__(self):
460
        return '%s (%s)' % (self.realname, self.email)
461

    
462
    def conflicting_email(self):
463
        q = AstakosUser.objects.exclude(username=self.username)
464
        q = q.filter(email__iexact=self.email)
465
        if q.count() != 0:
466
            return True
467
        return False
468

    
469
    def email_change_is_pending(self):
470
        return self.emailchanges.count() > 0
471

    
472
    def email_change_is_pending(self):
473
        return self.emailchanges.count() > 0
474

    
475
    @property
476
    def signed_terms(self):
477
        term = get_latest_terms()
478
        if not term:
479
            return True
480
        if not self.has_signed_terms:
481
            return False
482
        if not self.date_signed_terms:
483
            return False
484
        if self.date_signed_terms < term.date:
485
            self.has_signed_terms = False
486
            self.date_signed_terms = None
487
            self.save()
488
            return False
489
        return True
490

    
491
    def set_invitations_level(self):
492
        """
493
        Update user invitation level
494
        """
495
        level = self.invitation.inviter.level + 1
496
        self.level = level
497
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
498

    
499
    def can_login_with_auth_provider(self, provider):
500
        if not self.has_auth_provider(provider):
501
            return False
502
        else:
503
            return auth_providers.get_provider(provider).is_available_for_login()
504

    
505
    def can_add_auth_provider(self, provider, **kwargs):
506
        provider_settings = auth_providers.get_provider(provider)
507

    
508
        if not provider_settings.is_available_for_add():
509
            return False
510

    
511
        if self.has_auth_provider(provider) and \
512
           provider_settings.one_per_user:
513
            return False
514

    
515
        if 'provider_info' in kwargs:
516
            kwargs.pop('provider_info')
517

    
518
        if 'identifier' in kwargs:
519
            try:
520
                # provider with specified params already exist
521
                kwargs['user__email_verified'] = True
522
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
523
                                                                   **kwargs)
524
            except AstakosUser.DoesNotExist:
525
                return True
526
            else:
527
                return False
528

    
529
        return True
530

    
531
    def can_remove_auth_provider(self, module):
532
        provider = auth_providers.get_provider(module)
533
        existing = self.get_active_auth_providers()
534
        existing_for_provider = self.get_active_auth_providers(module=module)
535

    
536
        if len(existing) <= 1:
537
            return False
538

    
539
        if len(existing_for_provider) == 1 and provider.is_required():
540
            return False
541

    
542
        return True
543

    
544
    def can_change_password(self):
545
        return self.has_auth_provider('local', auth_backend='astakos')
546

    
547
    def has_required_auth_providers(self):
548
        required = auth_providers.REQUIRED_PROVIDERS
549
        for provider in required:
550
            if not self.has_auth_provider(provider):
551
                return False
552
        return True
553

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

    
558
    def add_auth_provider(self, provider, **kwargs):
559
        info_data = ''
560
        if 'provider_info' in kwargs:
561
            info_data = kwargs.pop('provider_info')
562
            if isinstance(info_data, dict):
563
                info_data = json.dumps(info_data)
564

    
565
        if self.can_add_auth_provider(provider, **kwargs):
566
            AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
567
                                                                **kwargs)
568
            self.auth_providers.create(module=provider, active=True,
569
                                       info_data=info_data,
570
                                       **kwargs)
571
        else:
572
            raise Exception('Cannot add provider')
573

    
574
    def add_pending_auth_provider(self, pending):
575
        """
576
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
577
        the current user.
578
        """
579
        if not isinstance(pending, PendingThirdPartyUser):
580
            pending = PendingThirdPartyUser.objects.get(token=pending)
581

    
582
        provider = self.add_auth_provider(pending.provider,
583
                               identifier=pending.third_party_identifier,
584
                                affiliation=pending.affiliation,
585
                                          provider_info=pending.info)
586

    
587
        if email_re.match(pending.email or '') and pending.email != self.email:
588
            self.additionalmail_set.get_or_create(email=pending.email)
589

    
590
        pending.delete()
591
        return provider
592

    
593
    def remove_auth_provider(self, provider, **kwargs):
594
        self.auth_providers.get(module=provider, **kwargs).delete()
595

    
596
    # user urls
597
    def get_resend_activation_url(self):
598
        return reverse('send_activation', kwargs={'user_id': self.pk})
599

    
600
    def get_provider_remove_url(self, module, **kwargs):
601
        return reverse('remove_auth_provider', kwargs={
602
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
603

    
604
    def get_activation_url(self, nxt=False):
605
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
606
                                 quote(self.auth_token))
607
        if nxt:
608
            url += "&next=%s" % quote(nxt)
609
        return url
610

    
611
    def get_password_reset_url(self, token_generator=default_token_generator):
612
        return reverse('django.contrib.auth.views.password_reset_confirm',
613
                          kwargs={'uidb36':int_to_base36(self.id),
614
                                  'token':token_generator.make_token(self)})
615

    
616
    def get_auth_providers(self):
617
        return self.auth_providers.all()
618

    
619
    def get_available_auth_providers(self):
620
        """
621
        Returns a list of providers available for user to connect to.
622
        """
623
        providers = []
624
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
625
            if self.can_add_auth_provider(module):
626
                providers.append(provider_settings(self))
627

    
628
        return providers
629

    
630
    def get_active_auth_providers(self, **filters):
631
        providers = []
632
        for provider in self.auth_providers.active(**filters):
633
            if auth_providers.get_provider(provider.module).is_available_for_login():
634
                providers.append(provider)
635
        return providers
636

    
637
    @property
638
    def auth_providers_display(self):
639
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
640

    
641
    def get_inactive_message(self):
642
        msg_extra = ''
643
        message = ''
644
        if self.activation_sent:
645
            if self.email_verified:
646
                message = _(astakos_messages.ACCOUNT_INACTIVE)
647
            else:
648
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
649
                if astakos_settings.MODERATION_ENABLED:
650
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
651
                else:
652
                    url = self.get_resend_activation_url()
653
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
654
                                u' ' + \
655
                                _('<a href="%s">%s?</a>') % (url,
656
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
657
        else:
658
            if astakos_settings.MODERATION_ENABLED:
659
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
660
            else:
661
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
662
                url = self.get_resend_activation_url()
663
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
664
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
665

    
666
        return mark_safe(message + u' '+ msg_extra)
667

    
668
    def owns_project(self, project):
669
        return project.owner == self
670

    
671
    def is_project_member(self, project_or_application):
672
        return self.get_status_in_project(project_or_application) in \
673
                                        ProjectMembership.ASSOCIATED_STATES
674

    
675
    def is_project_accepted_member(self, project_or_application):
676
        return self.get_status_in_project(project_or_application) in \
677
                                            ProjectMembership.ACCEPTED_STATES
678

    
679
    def get_status_in_project(self, project_or_application):
680
        application = project_or_application
681
        if isinstance(project_or_application, Project):
682
            application = project_or_application.project
683
        return application.user_status(self)
684

    
685

    
686
class AstakosUserAuthProviderManager(models.Manager):
687

    
688
    def active(self, **filters):
689
        return self.filter(active=True, **filters)
690

    
691
    def remove_unverified_providers(self, provider, **filters):
692
        try:
693
            existing = self.filter(module=provider, user__email_verified=False, **filters)
694
            for p in existing:
695
                p.user.delete()
696
        except:
697
            pass
698

    
699

    
700

    
701
class AstakosUserAuthProvider(models.Model):
702
    """
703
    Available user authentication methods.
704
    """
705
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
706
                                   null=True, default=None)
707
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
708
    module = models.CharField(_('Provider'), max_length=255, blank=False,
709
                                default='local')
710
    identifier = models.CharField(_('Third-party identifier'),
711
                                              max_length=255, null=True,
712
                                              blank=True)
713
    active = models.BooleanField(default=True)
714
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
715
                                   default='astakos')
716
    info_data = models.TextField(default="", null=True, blank=True)
717
    created = models.DateTimeField('Creation date', auto_now_add=True)
718

    
719
    objects = AstakosUserAuthProviderManager()
720

    
721
    class Meta:
722
        unique_together = (('identifier', 'module', 'user'), )
723
        ordering = ('module', 'created')
724

    
725
    def __init__(self, *args, **kwargs):
726
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
727
        try:
728
            self.info = json.loads(self.info_data)
729
            if not self.info:
730
                self.info = {}
731
        except Exception, e:
732
            self.info = {}
733

    
734
        for key,value in self.info.iteritems():
735
            setattr(self, 'info_%s' % key, value)
736

    
737

    
738
    @property
739
    def settings(self):
740
        return auth_providers.get_provider(self.module)
741

    
742
    @property
743
    def details_display(self):
744
        try:
745
          return self.settings.get_details_tpl_display % self.__dict__
746
        except:
747
          return ''
748

    
749
    @property
750
    def title_display(self):
751
        title_tpl = self.settings.get_title_display
752
        try:
753
            if self.settings.get_user_title_display:
754
                title_tpl = self.settings.get_user_title_display
755
        except Exception, e:
756
            pass
757
        try:
758
          return title_tpl % self.__dict__
759
        except:
760
          return self.settings.get_title_display % self.__dict__
761

    
762
    def can_remove(self):
763
        return self.user.can_remove_auth_provider(self.module)
764

    
765
    def delete(self, *args, **kwargs):
766
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
767
        if self.module == 'local':
768
            self.user.set_unusable_password()
769
            self.user.save()
770
        return ret
771

    
772
    def __repr__(self):
773
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
774

    
775
    def __unicode__(self):
776
        if self.identifier:
777
            return "%s:%s" % (self.module, self.identifier)
778
        if self.auth_backend:
779
            return "%s:%s" % (self.module, self.auth_backend)
780
        return self.module
781

    
782
    def save(self, *args, **kwargs):
783
        self.info_data = json.dumps(self.info)
784
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
785

    
786

    
787
class ExtendedManager(models.Manager):
788
    def _update_or_create(self, **kwargs):
789
        assert kwargs, \
790
            'update_or_create() must be passed at least one keyword argument'
791
        obj, created = self.get_or_create(**kwargs)
792
        defaults = kwargs.pop('defaults', {})
793
        if created:
794
            return obj, True, False
795
        else:
796
            try:
797
                params = dict(
798
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
799
                params.update(defaults)
800
                for attr, val in params.items():
801
                    if hasattr(obj, attr):
802
                        setattr(obj, attr, val)
803
                sid = transaction.savepoint()
804
                obj.save(force_update=True)
805
                transaction.savepoint_commit(sid)
806
                return obj, False, True
807
            except IntegrityError, e:
808
                transaction.savepoint_rollback(sid)
809
                try:
810
                    return self.get(**kwargs), False, False
811
                except self.model.DoesNotExist:
812
                    raise e
813

    
814
    update_or_create = _update_or_create
815

    
816

    
817
class AstakosUserQuota(models.Model):
818
    objects = ExtendedManager()
819
    capacity = models.BigIntegerField(_('Capacity'), null=True)
820
    quantity = models.BigIntegerField(_('Quantity'), null=True)
821
    export_limit = models.BigIntegerField(_('Export limit'), null=True)
822
    import_limit = models.BigIntegerField(_('Import limit'), null=True)
823
    resource = models.ForeignKey(Resource)
824
    user = models.ForeignKey(AstakosUser)
825

    
826
    class Meta:
827
        unique_together = ("resource", "user")
828

    
829

    
830
class ApprovalTerms(models.Model):
831
    """
832
    Model for approval terms
833
    """
834

    
835
    date = models.DateTimeField(
836
        _('Issue date'), db_index=True, default=datetime.now())
837
    location = models.CharField(_('Terms location'), max_length=255)
838

    
839

    
840
class Invitation(models.Model):
841
    """
842
    Model for registring invitations
843
    """
844
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
845
                                null=True)
846
    realname = models.CharField(_('Real name'), max_length=255)
847
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
848
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
849
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
850
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
851
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
852

    
853
    def __init__(self, *args, **kwargs):
854
        super(Invitation, self).__init__(*args, **kwargs)
855
        if not self.id:
856
            self.code = _generate_invitation_code()
857

    
858
    def consume(self):
859
        self.is_consumed = True
860
        self.consumed = datetime.now()
861
        self.save()
862

    
863
    def __unicode__(self):
864
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
865

    
866

    
867
class EmailChangeManager(models.Manager):
868

    
869
    @transaction.commit_on_success
870
    def change_email(self, activation_key):
871
        """
872
        Validate an activation key and change the corresponding
873
        ``User`` if valid.
874

875
        If the key is valid and has not expired, return the ``User``
876
        after activating.
877

878
        If the key is not valid or has expired, return ``None``.
879

880
        If the key is valid but the ``User`` is already active,
881
        return ``None``.
882

883
        After successful email change the activation record is deleted.
884

885
        Throws ValueError if there is already
886
        """
887
        try:
888
            email_change = self.model.objects.get(
889
                activation_key=activation_key)
890
            if email_change.activation_key_expired():
891
                email_change.delete()
892
                raise EmailChange.DoesNotExist
893
            # is there an active user with this address?
894
            try:
895
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
896
            except AstakosUser.DoesNotExist:
897
                pass
898
            else:
899
                raise ValueError(_('The new email address is reserved.'))
900
            # update user
901
            user = AstakosUser.objects.get(pk=email_change.user_id)
902
            old_email = user.email
903
            user.email = email_change.new_email_address
904
            user.save()
905
            email_change.delete()
906
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
907
                                                          user.email)
908
            logger.log(LOGGING_LEVEL, msg)
909
            return user
910
        except EmailChange.DoesNotExist:
911
            raise ValueError(_('Invalid activation key.'))
912

    
913

    
914
class EmailChange(models.Model):
915
    new_email_address = models.EmailField(
916
        _(u'new e-mail address'),
917
        help_text=_('Your old email address will be used until you verify your new one.'))
918
    user = models.ForeignKey(
919
        AstakosUser, unique=True, related_name='emailchanges')
920
    requested_at = models.DateTimeField(default=datetime.now())
921
    activation_key = models.CharField(
922
        max_length=40, unique=True, db_index=True)
923

    
924
    objects = EmailChangeManager()
925

    
926
    def get_url(self):
927
        return reverse('email_change_confirm',
928
                      kwargs={'activation_key': self.activation_key})
929

    
930
    def activation_key_expired(self):
931
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
932
        return self.requested_at + expiration_date < datetime.now()
933

    
934

    
935
class AdditionalMail(models.Model):
936
    """
937
    Model for registring invitations
938
    """
939
    owner = models.ForeignKey(AstakosUser)
940
    email = models.EmailField()
941

    
942

    
943
def _generate_invitation_code():
944
    while True:
945
        code = randint(1, 2L ** 63 - 1)
946
        try:
947
            Invitation.objects.get(code=code)
948
            # An invitation with this code already exists, try again
949
        except Invitation.DoesNotExist:
950
            return code
951

    
952

    
953
def get_latest_terms():
954
    try:
955
        term = ApprovalTerms.objects.order_by('-id')[0]
956
        return term
957
    except IndexError:
958
        pass
959
    return None
960

    
961
class PendingThirdPartyUser(models.Model):
962
    """
963
    Model for registring successful third party user authentications
964
    """
965
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
966
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
967
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
968
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
969
                                  null=True)
970
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
971
                                 null=True)
972
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
973
                                   null=True)
974
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
975
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
976
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
977
    info = models.TextField(default="", null=True, blank=True)
978

    
979
    class Meta:
980
        unique_together = ("provider", "third_party_identifier")
981

    
982
    def get_user_instance(self):
983
        d = self.__dict__
984
        d.pop('_state', None)
985
        d.pop('id', None)
986
        d.pop('token', None)
987
        d.pop('created', None)
988
        d.pop('info', None)
989
        user = AstakosUser(**d)
990

    
991
        return user
992

    
993
    @property
994
    def realname(self):
995
        return '%s %s' %(self.first_name, self.last_name)
996

    
997
    @realname.setter
998
    def realname(self, value):
999
        parts = value.split(' ')
1000
        if len(parts) == 2:
1001
            self.first_name = parts[0]
1002
            self.last_name = parts[1]
1003
        else:
1004
            self.last_name = parts[0]
1005

    
1006
    def save(self, **kwargs):
1007
        if not self.id:
1008
            # set username
1009
            while not self.username:
1010
                username =  uuid.uuid4().hex[:30]
1011
                try:
1012
                    AstakosUser.objects.get(username = username)
1013
                except AstakosUser.DoesNotExist, e:
1014
                    self.username = username
1015
        super(PendingThirdPartyUser, self).save(**kwargs)
1016

    
1017
    def generate_token(self):
1018
        self.password = self.third_party_identifier
1019
        self.last_login = datetime.now()
1020
        self.token = default_token_generator.make_token(self)
1021

    
1022
class SessionCatalog(models.Model):
1023
    session_key = models.CharField(_('session key'), max_length=40)
1024
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1025

    
1026

    
1027
### PROJECTS ###
1028
################
1029

    
1030
def synced_model_metaclass(class_name, class_parents, class_attributes):
1031

    
1032
    new_attributes = {}
1033
    sync_attributes = {}
1034

    
1035
    for name, value in class_attributes.iteritems():
1036
        sync, underscore, rest = name.partition('_')
1037
        if sync == 'sync' and underscore == '_':
1038
            sync_attributes[rest] = value
1039
        else:
1040
            new_attributes[name] = value
1041

    
1042
    if 'prefix' not in sync_attributes:
1043
        m = ("you did not specify a 'sync_prefix' attribute "
1044
             "in class '%s'" % (class_name,))
1045
        raise ValueError(m)
1046

    
1047
    prefix = sync_attributes.pop('prefix')
1048
    class_name = sync_attributes.pop('classname', prefix + '_model')
1049

    
1050
    for name, value in sync_attributes.iteritems():
1051
        newname = prefix + '_' + name
1052
        if newname in new_attributes:
1053
            m = ("class '%s' was specified with prefix '%s' "
1054
                 "but it already has an attribute named '%s'"
1055
                 % (class_name, prefix, newname))
1056
            raise ValueError(m)
1057

    
1058
        new_attributes[newname] = value
1059

    
1060
    newclass = type(class_name, class_parents, new_attributes)
1061
    return newclass
1062

    
1063

    
1064
def make_synced(prefix='sync', name='SyncedState'):
1065

    
1066
    the_name = name
1067
    the_prefix = prefix
1068

    
1069
    class SyncedState(models.Model):
1070

    
1071
        sync_classname      = the_name
1072
        sync_prefix         = the_prefix
1073
        __metaclass__       = synced_model_metaclass
1074

    
1075
        sync_new_state      = models.BigIntegerField(null=True)
1076
        sync_synced_state   = models.BigIntegerField(null=True)
1077
        STATUS_SYNCED       = 0
1078
        STATUS_PENDING      = 1
1079
        sync_status         = models.IntegerField(db_index=True)
1080

    
1081
        class Meta:
1082
            abstract = True
1083

    
1084
        class NotSynced(Exception):
1085
            pass
1086

    
1087
        def sync_init_state(self, state):
1088
            self.sync_synced_state = state
1089
            self.sync_new_state = state
1090
            self.sync_status = self.STATUS_SYNCED
1091

    
1092
        def sync_get_status(self):
1093
            return self.sync_status
1094

    
1095
        def sync_set_status(self):
1096
            if self.sync_new_state != self.sync_synced_state:
1097
                self.sync_status = self.STATUS_PENDING
1098
            else:
1099
                self.sync_status = self.STATUS_SYNCED
1100

    
1101
        def sync_set_synced(self):
1102
            self.sync_synced_state = self.sync_new_state
1103
            self.sync_status = self.STATUS_SYNCED
1104

    
1105
        def sync_get_synced_state(self):
1106
            return self.sync_synced_state
1107

    
1108
        def sync_set_new_state(self, new_state):
1109
            self.sync_new_state = new_state
1110
            self.sync_set_status()
1111

    
1112
        def sync_get_new_state(self):
1113
            return self.sync_new_state
1114

    
1115
        def sync_set_synced_state(self, synced_state):
1116
            self.sync_synced_state = synced_state
1117
            self.sync_set_status()
1118

    
1119
        def sync_get_pending_objects(self):
1120
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1121
            return self.objects.filter(**kw)
1122

    
1123
        def sync_get_synced_objects(self):
1124
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1125
            return self.objects.filter(**kw)
1126

    
1127
        def sync_verify_get_synced_state(self):
1128
            status = self.sync_get_status()
1129
            state = self.sync_get_synced_state()
1130
            verified = (status == self.STATUS_SYNCED)
1131
            return state, verified
1132

    
1133
        def sync_is_synced(self):
1134
            state, verified = self.sync_verify_get_synced_state()
1135
            return verified
1136

    
1137
    return SyncedState
1138

    
1139
SyncedState = make_synced(prefix='sync', name='SyncedState')
1140

    
1141

    
1142
class ProjectApplicationManager(ForUpdateManager):
1143

    
1144
    def user_visible_projects(self, *filters, **kw_filters):
1145
        return self.filter(Q(state=ProjectApplication.PENDING)|\
1146
                           Q(state=ProjectApplication.APPROVED))
1147

    
1148
    def user_visible_by_last_of_chain(self, *filters, **kw_filters):
1149
        by_chain = self.user_visible_projects(*filters, **kw_filters).values('chain')
1150
        by_chain_min = [x['last_id'] for x in by_chain.annotate(last_id=models.Min('id'))]
1151
        return self.filter(id__in=by_chain_min)
1152

    
1153
    def user_accessible_projects(self, user):
1154
        """
1155
        Return projects accessed by specified user.
1156
        """
1157
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1158
                               Q(project__projectmembership__person=user)
1159

    
1160
        return self.user_visible_by_last_of_chain(participates_filters).order_by('issue_date').distinct()
1161

    
1162
    def search_by_name(self, *search_strings):
1163
        q = Q()
1164
        for s in search_strings:
1165
            q = q | Q(name__icontains=s)
1166
        return self.filter(q)
1167

    
1168

    
1169
USER_STATUS_DISPLAY = {
1170
      0: _('Join requested'),
1171
      1: _('Accepted member'),
1172
     10: _('Suspended'),
1173
    100: _('Terminated'),
1174
    200: _('Removed'),
1175
     -1: _('Not a member'),
1176
}
1177

    
1178

    
1179
class Chain(models.Model):
1180
    chain  =   models.AutoField(primary_key=True)
1181

    
1182
def new_chain():
1183
    c = Chain.objects.create()
1184
    chain = c.chain
1185
    c.delete()
1186
    return chain
1187

    
1188

    
1189
class ProjectApplication(models.Model):
1190
    applicant               =   models.ForeignKey(
1191
                                    AstakosUser,
1192
                                    related_name='projects_applied',
1193
                                    db_index=True)
1194

    
1195
    PENDING     =    0
1196
    APPROVED    =    1
1197
    REPLACED    =    2
1198
    DENIED      =    3
1199

    
1200
    state                   =   models.IntegerField(default=PENDING)
1201

    
1202
    owner                   =   models.ForeignKey(
1203
                                    AstakosUser,
1204
                                    related_name='projects_owned',
1205
                                    db_index=True)
1206

    
1207
    chain                   =   models.IntegerField()
1208
    precursor_application   =   models.OneToOneField('ProjectApplication',
1209
                                                     null=True,
1210
                                                     blank=True,
1211
                                                     db_index=True)
1212

    
1213
    name                    =   models.CharField(max_length=80)
1214
    homepage                =   models.URLField(max_length=255, null=True)
1215
    description             =   models.TextField(null=True, blank=True)
1216
    start_date              =   models.DateTimeField(null=True, blank=True)
1217
    end_date                =   models.DateTimeField()
1218
    member_join_policy      =   models.IntegerField()
1219
    member_leave_policy     =   models.IntegerField()
1220
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1221
    resource_grants         =   models.ManyToManyField(
1222
                                    Resource,
1223
                                    null=True,
1224
                                    blank=True,
1225
                                    through='ProjectResourceGrant')
1226
    comments                =   models.TextField(null=True, blank=True)
1227
    issue_date              =   models.DateTimeField(default=datetime.now)
1228

    
1229

    
1230
    objects                 =   ProjectApplicationManager()
1231

    
1232
    class Meta:
1233
        unique_together = ("chain", "id")
1234

    
1235
    def __unicode__(self):
1236
        return "%s applied by %s" % (self.name, self.applicant)
1237

    
1238
    # TODO: Move to a more suitable place
1239
    PROJECT_STATE_DISPLAY = {
1240
        PENDING : _('Pending review'),
1241
        APPROVED: _('Active'),
1242
        REPLACED: _('Replaced'),
1243
        DENIED  : _('Denied')
1244
    }
1245

    
1246
    def state_display(self):
1247
        return self.PROJECT_STATE_DISPLAY.get(self.state, _('Unknown'))
1248

    
1249
    def add_resource_policy(self, service, resource, uplimit):
1250
        """Raises ObjectDoesNotExist, IntegrityError"""
1251
        q = self.projectresourcegrant_set
1252
        resource = Resource.objects.get(service__name=service, name=resource)
1253
        q.create(resource=resource, member_capacity=uplimit)
1254

    
1255
    def user_status(self, user):
1256
        try:
1257
            project = self.get_project()
1258
            if not project:
1259
                return -1
1260
            membership = project.projectmembership_set
1261
            membership = membership.exclude(state=ProjectMembership.REMOVED)
1262
            membership = membership.get(person=user)
1263
            status = membership.state
1264
        except ProjectMembership.DoesNotExist:
1265
            return -1
1266

    
1267
        return status
1268

    
1269
    def user_status_display(self, user):
1270
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1271

    
1272
    def members_count(self):
1273
        return self.project.approved_memberships.count()
1274

    
1275
    @property
1276
    def grants(self):
1277
        return self.projectresourcegrant_set.values(
1278
            'member_capacity', 'resource__name', 'resource__service__name')
1279

    
1280
    @property
1281
    def resource_policies(self):
1282
        return self.projectresourcegrant_set.all()
1283

    
1284
    @resource_policies.setter
1285
    def resource_policies(self, policies):
1286
        for p in policies:
1287
            service = p.get('service', None)
1288
            resource = p.get('resource', None)
1289
            uplimit = p.get('uplimit', 0)
1290
            self.add_resource_policy(service, resource, uplimit)
1291

    
1292
    @property
1293
    def follower(self):
1294
        try:
1295
            return ProjectApplication.objects.get(precursor_application=self)
1296
        except ProjectApplication.DoesNotExist:
1297
            return
1298

    
1299
    def followers(self):
1300
        followers = self.chained_applications()
1301
        followers = followers.exclude(id=self.pk).filter(state=self.PENDING)
1302
        followers = followers.order_by('id')
1303
        return followers
1304

    
1305
    def last_follower(self):
1306
        try:
1307
            return self.followers().order_by('-id')[0]
1308
        except IndexError:
1309
            return None
1310

    
1311
    def is_modification(self):
1312
        parents = self.chained_applications().filter(id__lt=self.id)
1313
        parents = parents.filter(state__in=[self.APPROVED])
1314
        return parents.count() > 0
1315

    
1316
    def chained_applications(self):
1317
        return ProjectApplication.objects.filter(chain=self.chain)
1318

    
1319
    def has_pending_modifications(self):
1320
        return bool(self.last_follower())
1321

    
1322
    def get_project(self):
1323
        try:
1324
            return Project.objects.get(id=self.chain)
1325
        except Project.DoesNotExist:
1326
            return None
1327

    
1328
    def _get_project_for_update(self):
1329
        try:
1330
            objects = Project.objects.select_for_update()
1331
            project = objects.get(id=self.chain)
1332
            return project
1333
        except Project.DoesNotExist:
1334
            return None
1335

    
1336
    def deny(self):
1337
        if self.state != self.PENDING:
1338
            m = _("cannot deny: application '%s' in state '%s'") % (
1339
                    self.id, self.state)
1340
            raise AssertionError(m)
1341

    
1342
        self.state = self.DENIED
1343
        self.save()
1344

    
1345
    def approve(self, approval_user=None):
1346
        """
1347
        If approval_user then during owner membership acceptance
1348
        it is checked whether the request_user is eligible.
1349

1350
        Raises:
1351
            PermissionDenied
1352
        """
1353

    
1354
        if not transaction.is_managed():
1355
            raise AssertionError("NOPE")
1356

    
1357
        new_project_name = self.name
1358
        if self.state != self.PENDING:
1359
            m = _("cannot approve: project '%s' in state '%s'") % (
1360
                    new_project_name, self.state)
1361
            raise PermissionDenied(m) # invalid argument
1362

    
1363
        now = datetime.now()
1364
        project = self._get_project_for_update()
1365

    
1366
        try:
1367
            # needs SERIALIZABLE
1368
            conflicting_project = Project.objects.get(name=new_project_name)
1369
            if (conflicting_project.is_alive and
1370
                conflicting_project != project):
1371
                m = (_("cannot approve: project with name '%s' "
1372
                       "already exists (serial: %s)") % (
1373
                        new_project_name, conflicting_project.id))
1374
                raise PermissionDenied(m) # invalid argument
1375
        except Project.DoesNotExist:
1376
            pass
1377

    
1378
        new_project = False
1379
        if project is None:
1380
            new_project = True
1381
            project = Project(id=self.chain, creation_date=now)
1382

    
1383
        project.name = new_project_name
1384
        project.application = self
1385
        project.last_approval_date = now
1386
        if not new_project:
1387
            project.is_modified = True
1388

    
1389
        project.save()
1390

    
1391
        self.state = self.APPROVED
1392
        self.save()
1393

    
1394
def submit_application(**kw):
1395

    
1396
    resource_policies = kw.pop('resource_policies', None)
1397
    application = ProjectApplication(**kw)
1398

    
1399
    precursor = kw['precursor_application']
1400

    
1401
    if precursor is None:
1402
        application.chain = new_chain()
1403
    else:
1404
        application.chain = precursor.chain
1405
        if precursor.state == ProjectApplication.PENDING:
1406
            precursor.state = ProjectApplication.REPLACED
1407
            precursor.save()
1408

    
1409
    application.save()
1410
    application.resource_policies = resource_policies
1411
    return application
1412

    
1413
class ProjectResourceGrant(models.Model):
1414

    
1415
    resource                =   models.ForeignKey(Resource)
1416
    project_application     =   models.ForeignKey(ProjectApplication,
1417
                                                  null=True)
1418
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1419
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1420
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1421
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1422
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1423
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1424

    
1425
    objects = ExtendedManager()
1426

    
1427
    class Meta:
1428
        unique_together = ("resource", "project_application")
1429

    
1430

    
1431
class ProjectManager(ForUpdateManager):
1432

    
1433
    def _q_terminated(self):
1434
        return Q(state=Project.TERMINATED)
1435

    
1436
    def terminated_projects(self):
1437
        q = self._q_terminated()
1438
        return self.filter(q)
1439

    
1440
    def not_terminated_projects(self):
1441
        q = ~self._q_terminated()
1442
        return self.filter(q)
1443

    
1444
    def terminating_projects(self):
1445
        q = self._q_terminated() & Q(is_active=True)
1446
        return self.filter(q)
1447

    
1448
    def modified_projects(self):
1449
        return self.filter(is_modified=True)
1450

    
1451

    
1452
class Project(models.Model):
1453

    
1454
    application                 =   models.OneToOneField(
1455
                                            ProjectApplication,
1456
                                            related_name='project')
1457
    last_approval_date          =   models.DateTimeField(null=True)
1458

    
1459
    members                     =   models.ManyToManyField(
1460
                                            AstakosUser,
1461
                                            through='ProjectMembership')
1462

    
1463
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1464
    deactivation_date           =   models.DateTimeField(null=True)
1465

    
1466
    creation_date               =   models.DateTimeField()
1467
    name                        =   models.CharField(
1468
                                            max_length=80,
1469
                                            db_index=True,
1470
                                            unique=True)
1471

    
1472
    APPROVED    = 1
1473
    SUSPENDED   = 10
1474
    TERMINATED  = 100
1475

    
1476
    is_modified                 =   models.BooleanField(default=False,
1477
                                                        db_index=True)
1478
    is_active                   =   models.BooleanField(default=True,
1479
                                                        db_index=True)
1480
    state                       =   models.IntegerField(default=APPROVED,
1481
                                                        db_index=True)
1482

    
1483
    objects     =   ProjectManager()
1484

    
1485
    def __str__(self):
1486
        return _("<project %s '%s'>") % (self.id, self.application.name)
1487

    
1488
    __repr__ = __str__
1489

    
1490
    def is_deactivated(self, reason=None):
1491
        if reason is not None:
1492
            return self.state == reason
1493

    
1494
        return self.state != self.APPROVED
1495

    
1496
    def is_deactivating(self, reason=None):
1497
        if not self.is_active:
1498
            return False
1499

    
1500
        return self.is_deactivated(reason)
1501

    
1502
    def is_deactivated_strict(self, reason=None):
1503
        if self.is_active:
1504
            return False
1505

    
1506
        return self.is_deactivated(reason)
1507

    
1508
    ### Deactivation calls
1509

    
1510
    def deactivate(self):
1511
        self.deactivation_date = datetime.now()
1512
        self.is_active = False
1513

    
1514
    def terminate(self):
1515
        self.deactivation_reason = 'TERMINATED'
1516
        self.state = self.TERMINATED
1517
        self.save()
1518

    
1519

    
1520
    ### Logical checks
1521

    
1522
    def is_inconsistent(self):
1523
        now = datetime.now()
1524
        dates = [self.creation_date,
1525
                 self.last_approval_date,
1526
                 self.deactivation_date]
1527
        return any([date > now for date in dates])
1528

    
1529
    def is_active_strict(self):
1530
        return self.is_active and self.state == self.APPROVED
1531

    
1532
    @property
1533
    def is_alive(self):
1534
        return self.is_active_strict()
1535

    
1536
    @property
1537
    def is_terminated(self):
1538
        return self.is_deactivated(self.TERMINATED)
1539

    
1540
    @property
1541
    def is_suspended(self):
1542
        return False
1543

    
1544
    def violates_resource_grants(self):
1545
        return False
1546

    
1547
    def violates_members_limit(self, adding=0):
1548
        application = self.application
1549
        limit = application.limit_on_members_number
1550
        if limit is None:
1551
            return False
1552
        return (len(self.approved_members) + adding > limit)
1553

    
1554

    
1555
    ### Other
1556

    
1557
    @property
1558
    def approved_memberships(self):
1559
        query = ProjectMembership.query_approved()
1560
        return self.projectmembership_set.filter(query)
1561

    
1562
    @property
1563
    def approved_members(self):
1564
        return [m.person for m in self.approved_memberships]
1565

    
1566
    def add_member(self, user):
1567
        """
1568
        Raises:
1569
            django.exceptions.PermissionDenied
1570
            astakos.im.models.AstakosUser.DoesNotExist
1571
        """
1572
        if isinstance(user, int):
1573
            user = AstakosUser.objects.get(user=user)
1574

    
1575
        m, created = ProjectMembership.objects.get_or_create(
1576
            person=user, project=self
1577
        )
1578
        m.accept()
1579

    
1580
    def remove_member(self, user):
1581
        """
1582
        Raises:
1583
            django.exceptions.PermissionDenied
1584
            astakos.im.models.AstakosUser.DoesNotExist
1585
            astakos.im.models.ProjectMembership.DoesNotExist
1586
        """
1587
        if isinstance(user, int):
1588
            user = AstakosUser.objects.get(user=user)
1589

    
1590
        m = ProjectMembership.objects.get(person=user, project=self)
1591
        m.remove()
1592

    
1593

    
1594
class PendingMembershipError(Exception):
1595
    pass
1596

    
1597

    
1598
class ProjectMembership(models.Model):
1599

    
1600
    person              =   models.ForeignKey(AstakosUser)
1601
    request_date        =   models.DateField(default=datetime.now())
1602
    project             =   models.ForeignKey(Project)
1603

    
1604
    REQUESTED   =   0
1605
    ACCEPTED    =   1
1606
    SUSPENDED   =   10
1607
    TERMINATED  =   100
1608
    REMOVED     =   200
1609

    
1610
    ASSOCIATED_STATES   =   set([REQUESTED, ACCEPTED, SUSPENDED, TERMINATED])
1611
    ACCEPTED_STATES     =   set([ACCEPTED, SUSPENDED, TERMINATED])
1612

    
1613
    state               =   models.IntegerField(default=REQUESTED,
1614
                                                db_index=True)
1615
    is_pending          =   models.BooleanField(default=False, db_index=True)
1616
    is_active           =   models.BooleanField(default=False, db_index=True)
1617
    application         =   models.ForeignKey(
1618
                                ProjectApplication,
1619
                                null=True,
1620
                                related_name='memberships')
1621
    pending_application =   models.ForeignKey(
1622
                                ProjectApplication,
1623
                                null=True,
1624
                                related_name='pending_memebrships')
1625
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1626

    
1627
    acceptance_date     =   models.DateField(null=True, db_index=True)
1628
    leave_request_date  =   models.DateField(null=True)
1629

    
1630
    objects     =   ForUpdateManager()
1631

    
1632

    
1633
    def get_combined_state(self):
1634
        return self.state, self.is_active, self.is_pending
1635

    
1636
    @classmethod
1637
    def query_approved(cls):
1638
        return (~Q(state=cls.REQUESTED) &
1639
                ~Q(state=cls.REMOVED))
1640

    
1641
    class Meta:
1642
        unique_together = ("person", "project")
1643
        #index_together = [["project", "state"]]
1644

    
1645
    def __str__(self):
1646
        return _("<'%s' membership in '%s'>") % (
1647
                self.person.username, self.project)
1648

    
1649
    __repr__ = __str__
1650

    
1651
    def __init__(self, *args, **kwargs):
1652
        self.state = self.REQUESTED
1653
        super(ProjectMembership, self).__init__(*args, **kwargs)
1654

    
1655
    def _set_history_item(self, reason, date=None):
1656
        if isinstance(reason, basestring):
1657
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1658

    
1659
        history_item = ProjectMembershipHistory(
1660
                            serial=self.id,
1661
                            person=self.person_id,
1662
                            project=self.project_id,
1663
                            date=date or datetime.now(),
1664
                            reason=reason)
1665
        history_item.save()
1666
        serial = history_item.id
1667

    
1668
    def accept(self):
1669
        if self.is_pending:
1670
            m = _("%s: attempt to accept while is pending") % (self,)
1671
            raise AssertionError(m)
1672

    
1673
        state = self.state
1674
        if state != self.REQUESTED:
1675
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1676
            raise AssertionError(m)
1677

    
1678
        now = datetime.now()
1679
        self.acceptance_date = now
1680
        self._set_history_item(reason='ACCEPT', date=now)
1681
        if self.project.is_active_strict():
1682
            self.state = self.ACCEPTED
1683
            self.is_pending = True
1684
        else:
1685
            self.state = self.TERMINATED
1686

    
1687
        self.save()
1688

    
1689
    def remove(self):
1690
        if self.is_pending:
1691
            m = _("%s: attempt to remove while is pending") % (self,)
1692
            raise AssertionError(m)
1693

    
1694
        state = self.state
1695
        if state not in [self.ACCEPTED, self.TERMINATED]:
1696
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1697
            raise AssertionError(m)
1698

    
1699
        self._set_history_item(reason='REMOVE')
1700
        self.state = self.REMOVED
1701
        self.is_pending = True
1702
        self.save()
1703

    
1704
    def reject(self):
1705
        if self.is_pending:
1706
            m = _("%s: attempt to reject while is pending") % (self,)
1707
            raise AssertionError(m)
1708

    
1709
        state = self.state
1710
        if state != self.REQUESTED:
1711
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1712
            raise AssertionError(m)
1713

    
1714
        # rejected requests don't need sync,
1715
        # because they were never effected
1716
        self._set_history_item(reason='REJECT')
1717
        self.delete()
1718

    
1719
    def get_diff_quotas(self, sub_list=None, add_list=None):
1720
        if sub_list is None:
1721
            sub_list = []
1722

    
1723
        if add_list is None:
1724
            add_list = []
1725

    
1726
        sub_append = sub_list.append
1727
        add_append = add_list.append
1728
        holder = self.person.uuid
1729

    
1730
        synced_application = self.application
1731
        if synced_application is not None:
1732
            cur_grants = synced_application.projectresourcegrant_set.all()
1733
            for grant in cur_grants:
1734
                sub_append(QuotaLimits(
1735
                               holder       = holder,
1736
                               resource     = str(grant.resource),
1737
                               capacity     = grant.member_capacity,
1738
                               import_limit = grant.member_import_limit,
1739
                               export_limit = grant.member_export_limit))
1740

    
1741
        pending_application = self.pending_application
1742
        if pending_application is not None:
1743
            new_grants = pending_application.projectresourcegrant_set.all()
1744
            for new_grant in new_grants:
1745
                add_append(QuotaLimits(
1746
                               holder       = holder,
1747
                               resource     = str(new_grant.resource),
1748
                               capacity     = new_grant.member_capacity,
1749
                               import_limit = new_grant.member_import_limit,
1750
                               export_limit = new_grant.member_export_limit))
1751

    
1752
        return (sub_list, add_list)
1753

    
1754
    def set_sync(self):
1755
        if not self.is_pending:
1756
            m = _("%s: attempt to sync a non pending membership") % (self,)
1757
            raise AssertionError(m)
1758

    
1759
        state = self.state
1760
        if state == self.ACCEPTED:
1761
            pending_application = self.pending_application
1762
            if pending_application is None:
1763
                m = _("%s: attempt to sync an empty pending application") % (
1764
                    self,)
1765
                raise AssertionError(m)
1766

    
1767
            self.application = pending_application
1768
            self.is_active = True
1769

    
1770
            self.pending_application = None
1771
            self.pending_serial = None
1772

    
1773
            # project.application may have changed in the meantime,
1774
            # in which case we stay PENDING;
1775
            # we are safe to check due to select_for_update
1776
            if self.application == self.project.application:
1777
                self.is_pending = False
1778
            self.save()
1779

    
1780
        elif state == self.TERMINATED:
1781
            if self.pending_application:
1782
                m = _("%s: attempt to sync in state '%s' "
1783
                      "with a pending application") % (self, state)
1784
                raise AssertionError(m)
1785

    
1786
            self.application = None
1787
            self.pending_serial = None
1788
            self.is_pending = False
1789
            self.save()
1790

    
1791
        elif state == self.REMOVED:
1792
            self.delete()
1793

    
1794
        else:
1795
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1796
            raise AssertionError(m)
1797

    
1798
    def reset_sync(self):
1799
        if not self.is_pending:
1800
            m = _("%s: attempt to reset a non pending membership") % (self,)
1801
            raise AssertionError(m)
1802

    
1803
        state = self.state
1804
        if state in [self.ACCEPTED, self.TERMINATED, self.REMOVED]:
1805
            self.pending_application = None
1806
            self.pending_serial = None
1807
            self.save()
1808
        else:
1809
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1810
            raise AssertionError(m)
1811

    
1812
class Serial(models.Model):
1813
    serial  =   models.AutoField(primary_key=True)
1814

    
1815
def new_serial():
1816
    s = Serial.objects.create()
1817
    serial = s.serial
1818
    s.delete()
1819
    return serial
1820

    
1821
def sync_finish_serials(serials_to_ack=None):
1822
    if serials_to_ack is None:
1823
        serials_to_ack = qh_query_serials([])
1824

    
1825
    serials_to_ack = set(serials_to_ack)
1826
    sfu = ProjectMembership.objects.select_for_update()
1827
    memberships = list(sfu.filter(pending_serial__isnull=False))
1828

    
1829
    if memberships:
1830
        for membership in memberships:
1831
            serial = membership.pending_serial
1832
            if serial in serials_to_ack:
1833
                membership.set_sync()
1834
            else:
1835
                membership.reset_sync()
1836

    
1837
        transaction.commit()
1838

    
1839
    qh_ack_serials(list(serials_to_ack))
1840
    return len(memberships)
1841

    
1842
def pre_sync():
1843
    ACCEPTED = ProjectMembership.ACCEPTED
1844
    TERMINATED = ProjectMembership.TERMINATED
1845
    psfu = Project.objects.select_for_update()
1846

    
1847
    modified = psfu.modified_projects()
1848
    for project in modified:
1849
        objects = project.projectmembership_set.select_for_update()
1850

    
1851
        memberships = objects.filter(state=ACCEPTED)
1852
        for membership in memberships:
1853
            membership.is_pending = True
1854
            membership.save()
1855

    
1856
    terminating = psfu.terminating_projects()
1857
    for project in terminating:
1858
        objects = project.projectmembership_set.select_for_update()
1859

    
1860
        memberships = objects.filter(state=ACCEPTED)
1861
        for membership in memberships:
1862
            membership.is_pending = True
1863
            membership.state = TERMINATED
1864
            membership.save()
1865

    
1866
def do_sync():
1867

    
1868
    ACCEPTED = ProjectMembership.ACCEPTED
1869
    objects = ProjectMembership.objects.select_for_update()
1870

    
1871
    sub_quota, add_quota = [], []
1872

    
1873
    serial = new_serial()
1874

    
1875
    pending = objects.filter(is_pending=True)
1876
    for membership in pending:
1877

    
1878
        if membership.pending_application:
1879
            m = "%s: impossible: pending_application is not None (%s)" % (
1880
                membership, membership.pending_application)
1881
            raise AssertionError(m)
1882
        if membership.pending_serial:
1883
            m = "%s: impossible: pending_serial is not None (%s)" % (
1884
                membership, membership.pending_serial)
1885
            raise AssertionError(m)
1886

    
1887
        if membership.state == ACCEPTED:
1888
            membership.pending_application = membership.project.application
1889

    
1890
        membership.pending_serial = serial
1891
        membership.get_diff_quotas(sub_quota, add_quota)
1892
        membership.save()
1893

    
1894
    transaction.commit()
1895
    # ProjectApplication.approve() unblocks here
1896
    # and can set PENDING an already PENDING membership
1897
    # which has been scheduled to sync with the old project.application
1898
    # Need to check in ProjectMembership.set_sync()
1899

    
1900
    r = qh_add_quota(serial, sub_quota, add_quota)
1901
    if r:
1902
        m = "cannot sync serial: %d" % serial
1903
        raise RuntimeError(m)
1904

    
1905
    return serial
1906

    
1907
def post_sync():
1908
    ACCEPTED = ProjectMembership.ACCEPTED
1909
    psfu = Project.objects.select_for_update()
1910

    
1911
    modified = psfu.modified_projects()
1912
    for project in modified:
1913
        objects = project.projectmembership_set.select_for_update()
1914

    
1915
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
1916
        if not memberships:
1917
            project.is_modified = False
1918
            project.save()
1919

    
1920
    terminating = psfu.terminating_projects()
1921
    for project in terminating:
1922
        objects = project.projectmembership_set.select_for_update()
1923

    
1924
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1925
                                          Q(is_pending=True)))
1926
        if not memberships:
1927
            project.deactivate()
1928
            project.save()
1929

    
1930
    transaction.commit()
1931

    
1932
def sync_projects():
1933
    sync_finish_serials()
1934
    pre_sync()
1935
    serial = do_sync()
1936
    sync_finish_serials([serial])
1937
    post_sync()
1938

    
1939
def trigger_sync(retries=3, retry_wait=1.0):
1940
    transaction.commit()
1941

    
1942
    cursor = connection.cursor()
1943
    locked = True
1944
    try:
1945
        while 1:
1946
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1947
            r = cursor.fetchone()
1948
            if r is None:
1949
                m = "Impossible"
1950
                raise AssertionError(m)
1951
            locked = r[0]
1952
            if locked:
1953
                break
1954

    
1955
            retries -= 1
1956
            if retries <= 0:
1957
                return False
1958
            sleep(retry_wait)
1959

    
1960
        sync_projects()
1961
        return True
1962

    
1963
    finally:
1964
        if locked:
1965
            cursor.execute("SELECT pg_advisory_unlock(1)")
1966
            cursor.fetchall()
1967

    
1968

    
1969
class ProjectMembershipHistory(models.Model):
1970
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1971
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1972

    
1973
    person  =   models.BigIntegerField()
1974
    project =   models.BigIntegerField()
1975
    date    =   models.DateField(default=datetime.now)
1976
    reason  =   models.IntegerField()
1977
    serial  =   models.BigIntegerField()
1978

    
1979
### SIGNALS ###
1980
################
1981

    
1982
def create_astakos_user(u):
1983
    try:
1984
        AstakosUser.objects.get(user_ptr=u.pk)
1985
    except AstakosUser.DoesNotExist:
1986
        extended_user = AstakosUser(user_ptr_id=u.pk)
1987
        extended_user.__dict__.update(u.__dict__)
1988
        extended_user.save()
1989
        if not extended_user.has_auth_provider('local'):
1990
            extended_user.add_auth_provider('local')
1991
    except BaseException, e:
1992
        logger.exception(e)
1993

    
1994

    
1995
def fix_superusers(sender, **kwargs):
1996
    # Associate superusers with AstakosUser
1997
    admins = User.objects.filter(is_superuser=True)
1998
    for u in admins:
1999
        create_astakos_user(u)
2000
post_syncdb.connect(fix_superusers)
2001

    
2002

    
2003
def user_post_save(sender, instance, created, **kwargs):
2004
    if not created:
2005
        return
2006
    create_astakos_user(instance)
2007
post_save.connect(user_post_save, sender=User)
2008

    
2009
def astakosuser_post_save(sender, instance, created, **kwargs):
2010
    pass
2011

    
2012
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2013

    
2014
def resource_post_save(sender, instance, created, **kwargs):
2015
    if not created:
2016
        return
2017
    register_resources((instance,))
2018
post_save.connect(resource_post_save, sender=Resource)
2019

    
2020
def renew_token(sender, instance, **kwargs):
2021
    if not instance.auth_token:
2022
        instance.renew_token()
2023
pre_save.connect(renew_token, sender=AstakosUser)
2024
pre_save.connect(renew_token, sender=Service)
2025