Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (66.2 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
        self.validate_unique_email_isactive()
428

    
429
        super(AstakosUser, self).save(**kwargs)
430

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

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

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

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

    
461
    def __unicode__(self):
462
        return '%s (%s)' % (self.realname, self.email)
463

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

    
471
    def validate_unique_email_isactive(self):
472
        """
473
        Implements a unique_together constraint for email and is_active fields.
474
        """
475
        q = AstakosUser.objects.all()
476
        q = q.filter(email = self.email)
477
        if self.id:
478
            q = q.filter(~Q(id = self.id))
479
        if q.count() != 0:
480
            m = 'Another account with the same email = %(email)s & \
481
                is_active = %(is_active)s found.' % self.__dict__
482
            raise ValidationError(m)
483

    
484
    def email_change_is_pending(self):
485
        return self.emailchanges.count() > 0
486

    
487
    def email_change_is_pending(self):
488
        return self.emailchanges.count() > 0
489

    
490
    @property
491
    def signed_terms(self):
492
        term = get_latest_terms()
493
        if not term:
494
            return True
495
        if not self.has_signed_terms:
496
            return False
497
        if not self.date_signed_terms:
498
            return False
499
        if self.date_signed_terms < term.date:
500
            self.has_signed_terms = False
501
            self.date_signed_terms = None
502
            self.save()
503
            return False
504
        return True
505

    
506
    def set_invitations_level(self):
507
        """
508
        Update user invitation level
509
        """
510
        level = self.invitation.inviter.level + 1
511
        self.level = level
512
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
513

    
514
    def can_login_with_auth_provider(self, provider):
515
        if not self.has_auth_provider(provider):
516
            return False
517
        else:
518
            return auth_providers.get_provider(provider).is_available_for_login()
519

    
520
    def can_add_auth_provider(self, provider, **kwargs):
521
        provider_settings = auth_providers.get_provider(provider)
522

    
523
        if not provider_settings.is_available_for_add():
524
            return False
525

    
526
        if self.has_auth_provider(provider) and \
527
           provider_settings.one_per_user:
528
            return False
529

    
530
        if 'provider_info' in kwargs:
531
            kwargs.pop('provider_info')
532

    
533
        if 'identifier' in kwargs:
534
            try:
535
                # provider with specified params already exist
536
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
537
                                                                   **kwargs)
538
            except AstakosUser.DoesNotExist:
539
                return True
540
            else:
541
                return False
542

    
543
        return True
544

    
545
    def can_remove_auth_provider(self, module):
546
        provider = auth_providers.get_provider(module)
547
        existing = self.get_active_auth_providers()
548
        existing_for_provider = self.get_active_auth_providers(module=module)
549

    
550
        if len(existing) <= 1:
551
            return False
552

    
553
        if len(existing_for_provider) == 1 and provider.is_required():
554
            return False
555

    
556
        return True
557

    
558
    def can_change_password(self):
559
        return self.has_auth_provider('local', auth_backend='astakos')
560

    
561
    def has_required_auth_providers(self):
562
        required = auth_providers.REQUIRED_PROVIDERS
563
        for provider in required:
564
            if not self.has_auth_provider(provider):
565
                return False
566
        return True
567

    
568
    def has_auth_provider(self, provider, **kwargs):
569
        return bool(self.auth_providers.filter(module=provider,
570
                                               **kwargs).count())
571

    
572
    def add_auth_provider(self, provider, **kwargs):
573
        info_data = ''
574
        if 'provider_info' in kwargs:
575
            info_data = kwargs.pop('provider_info')
576
            if isinstance(info_data, dict):
577
                info_data = json.dumps(info_data)
578

    
579
        if self.can_add_auth_provider(provider, **kwargs):
580
            self.auth_providers.create(module=provider, active=True,
581
                                       info_data=info_data,
582
                                       **kwargs)
583
        else:
584
            raise Exception('Cannot add provider')
585

    
586
    def add_pending_auth_provider(self, pending):
587
        """
588
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
589
        the current user.
590
        """
591
        if not isinstance(pending, PendingThirdPartyUser):
592
            pending = PendingThirdPartyUser.objects.get(token=pending)
593

    
594
        provider = self.add_auth_provider(pending.provider,
595
                               identifier=pending.third_party_identifier,
596
                                affiliation=pending.affiliation,
597
                                          provider_info=pending.info)
598

    
599
        if email_re.match(pending.email or '') and pending.email != self.email:
600
            self.additionalmail_set.get_or_create(email=pending.email)
601

    
602
        pending.delete()
603
        return provider
604

    
605
    def remove_auth_provider(self, provider, **kwargs):
606
        self.auth_providers.get(module=provider, **kwargs).delete()
607

    
608
    # user urls
609
    def get_resend_activation_url(self):
610
        return reverse('send_activation', kwargs={'user_id': self.pk})
611

    
612
    def get_provider_remove_url(self, module, **kwargs):
613
        return reverse('remove_auth_provider', kwargs={
614
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
615

    
616
    def get_activation_url(self, nxt=False):
617
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
618
                                 quote(self.auth_token))
619
        if nxt:
620
            url += "&next=%s" % quote(nxt)
621
        return url
622

    
623
    def get_password_reset_url(self, token_generator=default_token_generator):
624
        return reverse('django.contrib.auth.views.password_reset_confirm',
625
                          kwargs={'uidb36':int_to_base36(self.id),
626
                                  'token':token_generator.make_token(self)})
627

    
628
    def get_auth_providers(self):
629
        return self.auth_providers.all()
630

    
631
    def get_available_auth_providers(self):
632
        """
633
        Returns a list of providers available for user to connect to.
634
        """
635
        providers = []
636
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
637
            if self.can_add_auth_provider(module):
638
                providers.append(provider_settings(self))
639

    
640
        return providers
641

    
642
    def get_active_auth_providers(self, **filters):
643
        providers = []
644
        for provider in self.auth_providers.active(**filters):
645
            if auth_providers.get_provider(provider.module).is_available_for_login():
646
                providers.append(provider)
647
        return providers
648

    
649
    @property
650
    def auth_providers_display(self):
651
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
652

    
653
    def get_inactive_message(self):
654
        msg_extra = ''
655
        message = ''
656
        if self.activation_sent:
657
            if self.email_verified:
658
                message = _(astakos_messages.ACCOUNT_INACTIVE)
659
            else:
660
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
661
                if astakos_settings.MODERATION_ENABLED:
662
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
663
                else:
664
                    url = self.get_resend_activation_url()
665
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
666
                                u' ' + \
667
                                _('<a href="%s">%s?</a>') % (url,
668
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
669
        else:
670
            if astakos_settings.MODERATION_ENABLED:
671
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
672
            else:
673
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
674
                url = self.get_resend_activation_url()
675
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
676
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
677

    
678
        return mark_safe(message + u' '+ msg_extra)
679

    
680
    def owns_project(self, project):
681
        return project.owner == self
682

    
683
    def is_project_member(self, project):
684
        return project.user_status(self) in [0,1,2,3]
685

    
686
    def is_project_accepted_member(self, project):
687
        return project.user_status(self) == 2
688

    
689

    
690
class AstakosUserAuthProviderManager(models.Manager):
691

    
692
    def active(self, **filters):
693
        return self.filter(active=True, **filters)
694

    
695

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

    
714
    objects = AstakosUserAuthProviderManager()
715

    
716
    class Meta:
717
        unique_together = (('identifier', 'module', 'user'), )
718
        ordering = ('module', 'created')
719

    
720
    def __init__(self, *args, **kwargs):
721
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
722
        try:
723
            self.info = json.loads(self.info_data)
724
            if not self.info:
725
                self.info = {}
726
        except Exception, e:
727
            self.info = {}
728

    
729
        for key,value in self.info.iteritems():
730
            setattr(self, 'info_%s' % key, value)
731

    
732

    
733
    @property
734
    def settings(self):
735
        return auth_providers.get_provider(self.module)
736

    
737
    @property
738
    def details_display(self):
739
        try:
740
          return self.settings.get_details_tpl_display % self.__dict__
741
        except:
742
          return ''
743

    
744
    @property
745
    def title_display(self):
746
        title_tpl = self.settings.get_title_display
747
        try:
748
            if self.settings.get_user_title_display:
749
                title_tpl = self.settings.get_user_title_display
750
        except Exception, e:
751
            pass
752
        try:
753
          return title_tpl % self.__dict__
754
        except:
755
          return self.settings.get_title_display % self.__dict__
756

    
757
    def can_remove(self):
758
        return self.user.can_remove_auth_provider(self.module)
759

    
760
    def delete(self, *args, **kwargs):
761
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
762
        if self.module == 'local':
763
            self.user.set_unusable_password()
764
            self.user.save()
765
        return ret
766

    
767
    def __repr__(self):
768
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
769

    
770
    def __unicode__(self):
771
        if self.identifier:
772
            return "%s:%s" % (self.module, self.identifier)
773
        if self.auth_backend:
774
            return "%s:%s" % (self.module, self.auth_backend)
775
        return self.module
776

    
777
    def save(self, *args, **kwargs):
778
        self.info_data = json.dumps(self.info)
779
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
780

    
781

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

    
809
    update_or_create = _update_or_create
810

    
811

    
812
class AstakosUserQuota(models.Model):
813
    objects = ExtendedManager()
814
    capacity = models.BigIntegerField(_('Capacity'), null=True)
815
    quantity = models.BigIntegerField(_('Quantity'), null=True)
816
    export_limit = models.BigIntegerField(_('Export limit'), null=True)
817
    import_limit = models.BigIntegerField(_('Import limit'), null=True)
818
    resource = models.ForeignKey(Resource)
819
    user = models.ForeignKey(AstakosUser)
820

    
821
    class Meta:
822
        unique_together = ("resource", "user")
823

    
824

    
825
class ApprovalTerms(models.Model):
826
    """
827
    Model for approval terms
828
    """
829

    
830
    date = models.DateTimeField(
831
        _('Issue date'), db_index=True, default=datetime.now())
832
    location = models.CharField(_('Terms location'), max_length=255)
833

    
834

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

    
848
    def __init__(self, *args, **kwargs):
849
        super(Invitation, self).__init__(*args, **kwargs)
850
        if not self.id:
851
            self.code = _generate_invitation_code()
852

    
853
    def consume(self):
854
        self.is_consumed = True
855
        self.consumed = datetime.now()
856
        self.save()
857

    
858
    def __unicode__(self):
859
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
860

    
861

    
862
class EmailChangeManager(models.Manager):
863

    
864
    @transaction.commit_on_success
865
    def change_email(self, activation_key):
866
        """
867
        Validate an activation key and change the corresponding
868
        ``User`` if valid.
869

870
        If the key is valid and has not expired, return the ``User``
871
        after activating.
872

873
        If the key is not valid or has expired, return ``None``.
874

875
        If the key is valid but the ``User`` is already active,
876
        return ``None``.
877

878
        After successful email change the activation record is deleted.
879

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

    
908

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

    
919
    objects = EmailChangeManager()
920

    
921
    def get_url(self):
922
        return reverse('email_change_confirm',
923
                      kwargs={'activation_key': self.activation_key})
924

    
925
    def activation_key_expired(self):
926
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
927
        return self.requested_at + expiration_date < datetime.now()
928

    
929

    
930
class AdditionalMail(models.Model):
931
    """
932
    Model for registring invitations
933
    """
934
    owner = models.ForeignKey(AstakosUser)
935
    email = models.EmailField()
936

    
937

    
938
def _generate_invitation_code():
939
    while True:
940
        code = randint(1, 2L ** 63 - 1)
941
        try:
942
            Invitation.objects.get(code=code)
943
            # An invitation with this code already exists, try again
944
        except Invitation.DoesNotExist:
945
            return code
946

    
947

    
948
def get_latest_terms():
949
    try:
950
        term = ApprovalTerms.objects.order_by('-id')[0]
951
        return term
952
    except IndexError:
953
        pass
954
    return None
955

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

    
971
    class Meta:
972
        unique_together = ("provider", "third_party_identifier")
973

    
974
    def get_user_instance(self):
975
        d = self.__dict__
976
        d.pop('_state', None)
977
        d.pop('id', None)
978
        d.pop('token', None)
979
        d.pop('created', None)
980
        d.pop('info', None)
981
        user = AstakosUser(**d)
982

    
983
        return user
984

    
985
    @property
986
    def realname(self):
987
        return '%s %s' %(self.first_name, self.last_name)
988

    
989
    @realname.setter
990
    def realname(self, value):
991
        parts = value.split(' ')
992
        if len(parts) == 2:
993
            self.first_name = parts[0]
994
            self.last_name = parts[1]
995
        else:
996
            self.last_name = parts[0]
997

    
998
    def save(self, **kwargs):
999
        if not self.id:
1000
            # set username
1001
            while not self.username:
1002
                username =  uuid.uuid4().hex[:30]
1003
                try:
1004
                    AstakosUser.objects.get(username = username)
1005
                except AstakosUser.DoesNotExist, e:
1006
                    self.username = username
1007
        super(PendingThirdPartyUser, self).save(**kwargs)
1008

    
1009
    def generate_token(self):
1010
        self.password = self.third_party_identifier
1011
        self.last_login = datetime.now()
1012
        self.token = default_token_generator.make_token(self)
1013

    
1014
class SessionCatalog(models.Model):
1015
    session_key = models.CharField(_('session key'), max_length=40)
1016
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1017

    
1018

    
1019
### PROJECTS ###
1020
################
1021

    
1022
def synced_model_metaclass(class_name, class_parents, class_attributes):
1023

    
1024
    new_attributes = {}
1025
    sync_attributes = {}
1026

    
1027
    for name, value in class_attributes.iteritems():
1028
        sync, underscore, rest = name.partition('_')
1029
        if sync == 'sync' and underscore == '_':
1030
            sync_attributes[rest] = value
1031
        else:
1032
            new_attributes[name] = value
1033

    
1034
    if 'prefix' not in sync_attributes:
1035
        m = ("you did not specify a 'sync_prefix' attribute "
1036
             "in class '%s'" % (class_name,))
1037
        raise ValueError(m)
1038

    
1039
    prefix = sync_attributes.pop('prefix')
1040
    class_name = sync_attributes.pop('classname', prefix + '_model')
1041

    
1042
    for name, value in sync_attributes.iteritems():
1043
        newname = prefix + '_' + name
1044
        if newname in new_attributes:
1045
            m = ("class '%s' was specified with prefix '%s' "
1046
                 "but it already has an attribute named '%s'"
1047
                 % (class_name, prefix, newname))
1048
            raise ValueError(m)
1049

    
1050
        new_attributes[newname] = value
1051

    
1052
    newclass = type(class_name, class_parents, new_attributes)
1053
    return newclass
1054

    
1055

    
1056
def make_synced(prefix='sync', name='SyncedState'):
1057

    
1058
    the_name = name
1059
    the_prefix = prefix
1060

    
1061
    class SyncedState(models.Model):
1062

    
1063
        sync_classname      = the_name
1064
        sync_prefix         = the_prefix
1065
        __metaclass__       = synced_model_metaclass
1066

    
1067
        sync_new_state      = models.BigIntegerField(null=True)
1068
        sync_synced_state   = models.BigIntegerField(null=True)
1069
        STATUS_SYNCED       = 0
1070
        STATUS_PENDING      = 1
1071
        sync_status         = models.IntegerField(db_index=True)
1072

    
1073
        class Meta:
1074
            abstract = True
1075

    
1076
        class NotSynced(Exception):
1077
            pass
1078

    
1079
        def sync_init_state(self, state):
1080
            self.sync_synced_state = state
1081
            self.sync_new_state = state
1082
            self.sync_status = self.STATUS_SYNCED
1083

    
1084
        def sync_get_status(self):
1085
            return self.sync_status
1086

    
1087
        def sync_set_status(self):
1088
            if self.sync_new_state != self.sync_synced_state:
1089
                self.sync_status = self.STATUS_PENDING
1090
            else:
1091
                self.sync_status = self.STATUS_SYNCED
1092

    
1093
        def sync_set_synced(self):
1094
            self.sync_synced_state = self.sync_new_state
1095
            self.sync_status = self.STATUS_SYNCED
1096

    
1097
        def sync_get_synced_state(self):
1098
            return self.sync_synced_state
1099

    
1100
        def sync_set_new_state(self, new_state):
1101
            self.sync_new_state = new_state
1102
            self.sync_set_status()
1103

    
1104
        def sync_get_new_state(self):
1105
            return self.sync_new_state
1106

    
1107
        def sync_set_synced_state(self, synced_state):
1108
            self.sync_synced_state = synced_state
1109
            self.sync_set_status()
1110

    
1111
        def sync_get_pending_objects(self):
1112
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1113
            return self.objects.filter(**kw)
1114

    
1115
        def sync_get_synced_objects(self):
1116
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1117
            return self.objects.filter(**kw)
1118

    
1119
        def sync_verify_get_synced_state(self):
1120
            status = self.sync_get_status()
1121
            state = self.sync_get_synced_state()
1122
            verified = (status == self.STATUS_SYNCED)
1123
            return state, verified
1124

    
1125
        def sync_is_synced(self):
1126
            state, verified = self.sync_verify_get_synced_state()
1127
            return verified
1128

    
1129
    return SyncedState
1130

    
1131
SyncedState = make_synced(prefix='sync', name='SyncedState')
1132

    
1133

    
1134
class ProjectApplicationManager(ForUpdateManager):
1135

    
1136
    def user_projects(self, user):
1137
        """
1138
        Return projects accessed by specified user.
1139
        """
1140
        participates_fitlers = Q(owner=user) | Q(applicant=user) | \
1141
                               Q(project__projectmembership__person=user)
1142
        state_filters = (Q(state=ProjectApplication.PENDING) & \
1143
                        Q(precursor_application__isnull=True)) | \
1144
                        Q(state=ProjectApplication.APPROVED)
1145
        return self.filter(participates_fitlers & state_filters).order_by('issue_date').distinct()
1146

    
1147
    def search_by_name(self, *search_strings):
1148
        q = Q()
1149
        for s in search_strings:
1150
            q = q | Q(name__icontains=s)
1151
        return self.filter(q)
1152

    
1153

    
1154
PROJECT_STATE_DISPLAY = {
1155
    'Pending': _('Pending review'),
1156
    'Approved': _('Active'),
1157
    'Replaced': _('Replaced'),
1158
    'Unknown': _('Unknown')
1159
}
1160

    
1161
USER_STATUS_DISPLAY = {
1162
    100: _('Owner'),
1163
      0: _('Join requested'),
1164
      1: _('Pending'),
1165
      2: _('Accepted member'),
1166
      3: _('Removing'),
1167
      4: _('Removed'),
1168
     -1: _('Not a member'),
1169
}
1170

    
1171
class Chain(models.Model):
1172
    chain  =   models.AutoField(primary_key=True)
1173

    
1174
def new_chain():
1175
    c = Chain.objects.create()
1176
    chain = c.chain
1177
    c.delete()
1178
    return chain
1179

    
1180

    
1181
class ProjectApplication(models.Model):
1182
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1183
    applicant               =   models.ForeignKey(
1184
                                    AstakosUser,
1185
                                    related_name='projects_applied',
1186
                                    db_index=True)
1187

    
1188
    state                   =   models.CharField(max_length=80,
1189
                                                default=PENDING)
1190

    
1191
    owner                   =   models.ForeignKey(
1192
                                    AstakosUser,
1193
                                    related_name='projects_owned',
1194
                                    db_index=True)
1195

    
1196
    chain                   =   models.IntegerField(db_index=True)
1197
    precursor_application   =   models.OneToOneField('ProjectApplication',
1198
                                                     null=True,
1199
                                                     blank=True,
1200
                                                     db_index=True)
1201

    
1202
    name                    =   models.CharField(max_length=80)
1203
    homepage                =   models.URLField(max_length=255, null=True)
1204
    description             =   models.TextField(null=True, blank=True)
1205
    start_date              =   models.DateTimeField(null=True, blank=True)
1206
    end_date                =   models.DateTimeField()
1207
    member_join_policy      =   models.IntegerField()
1208
    member_leave_policy     =   models.IntegerField()
1209
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1210
    resource_grants         =   models.ManyToManyField(
1211
                                    Resource,
1212
                                    null=True,
1213
                                    blank=True,
1214
                                    through='ProjectResourceGrant')
1215
    comments                =   models.TextField(null=True, blank=True)
1216
    issue_date              =   models.DateTimeField(default=datetime.now)
1217

    
1218

    
1219
    objects                 =   ProjectApplicationManager()
1220

    
1221
    def __unicode__(self):
1222
        return "%s applied by %s" % (self.name, self.applicant)
1223

    
1224
    def state_display(self):
1225
        return PROJECT_STATE_DISPLAY.get(self.state, _('Unknown'))
1226

    
1227
    def add_resource_policy(self, service, resource, uplimit):
1228
        """Raises ObjectDoesNotExist, IntegrityError"""
1229
        q = self.projectresourcegrant_set
1230
        resource = Resource.objects.get(service__name=service, name=resource)
1231
        q.create(resource=resource, member_capacity=uplimit)
1232

    
1233
    def user_status(self, user):
1234
        """
1235
        100 OWNER
1236
        0   REQUESTED
1237
        1   PENDING
1238
        2   ACCEPTED
1239
        3   REMOVING
1240
        4   REMOVED
1241
       -1   User has no association with the project
1242
        """
1243
        try:
1244
            membership = self.project.projectmembership_set.get(person=user)
1245
            status = membership.state
1246
        except Project.DoesNotExist:
1247
            status = -1
1248
        except ProjectMembership.DoesNotExist:
1249
            status = -1
1250

    
1251
        return status
1252

    
1253
    def user_status_display(self, user):
1254
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1255

    
1256
    def members_count(self):
1257
        return self.project.approved_memberships.count()
1258

    
1259
    @property
1260
    def grants(self):
1261
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1262

    
1263
    @property
1264
    def resource_policies(self):
1265
        return self.projectresourcegrant_set.all()
1266

    
1267
    @resource_policies.setter
1268
    def resource_policies(self, policies):
1269
        for p in policies:
1270
            service = p.get('service', None)
1271
            resource = p.get('resource', None)
1272
            uplimit = p.get('uplimit', 0)
1273
            self.add_resource_policy(service, resource, uplimit)
1274

    
1275
    @property
1276
    def follower(self):
1277
        try:
1278
            return ProjectApplication.objects.get(precursor_application=self)
1279
        except ProjectApplication.DoesNotExist:
1280
            return
1281

    
1282
    def followers(self):
1283
        current = self
1284
        try:
1285
            while current.projectapplication:
1286
                yield current.follower
1287
                current = current.follower
1288
        except:
1289
            pass
1290

    
1291
    def last_follower(self):
1292
        try:
1293
            return list(self.followers())[-1]
1294
        except IndexError:
1295
            return None
1296

    
1297
    def _get_project_for_update(self):
1298
        try:
1299
            objects = Project.objects.select_for_update()
1300
            project = objects.get(id=self.chain)
1301
            return project
1302
        except Project.DoesNotExist:
1303
            return None
1304

    
1305
    def approve(self, approval_user=None):
1306
        """
1307
        If approval_user then during owner membership acceptance
1308
        it is checked whether the request_user is eligible.
1309

1310
        Raises:
1311
            PermissionDenied
1312
        """
1313

    
1314
        if not transaction.is_managed():
1315
            raise AssertionError("NOPE")
1316

    
1317
        new_project_name = self.name
1318
        if self.state != self.PENDING:
1319
            m = _("cannot approve: project '%s' in state '%s'") % (
1320
                    new_project_name, self.state)
1321
            raise PermissionDenied(m) # invalid argument
1322

    
1323
        now = datetime.now()
1324
        project = self._get_project_for_update()
1325

    
1326
        try:
1327
            # needs SERIALIZABLE
1328
            conflicting_project = Project.objects.get(name=new_project_name)
1329
            if (conflicting_project.is_alive and
1330
                conflicting_project != project):
1331
                m = (_("cannot approve: project with name '%s' "
1332
                       "already exists (serial: %s)") % (
1333
                        new_project_name, conflicting_project.id))
1334
                raise PermissionDenied(m) # invalid argument
1335
        except Project.DoesNotExist:
1336
            pass
1337

    
1338
        new_project = False
1339
        if project is None:
1340
            new_project = True
1341
            project = Project(id=self.chain, creation_date=now)
1342

    
1343
        project.name = new_project_name
1344
        project.application = self
1345
        project.last_approval_date = now
1346
        if not new_project:
1347
            project.is_modified = True
1348

    
1349
        project.save()
1350

    
1351
        self.state = self.APPROVED
1352
        self.save()
1353

    
1354
def submit_application(**kw):
1355

    
1356
    resource_policies = kw.pop('resource_policies', None)
1357
    application = ProjectApplication(**kw)
1358

    
1359
    precursor = kw['precursor_application']
1360

    
1361
    if precursor is not None:
1362
        precursor.state = ProjectApplication.REPLACED
1363
        precursor.save()
1364
        application.chain = precursor.chain
1365
    else:
1366
        application.chain = new_chain()
1367

    
1368
    application.save()
1369
    application.resource_policies = resource_policies
1370
    return application
1371

    
1372
class ProjectResourceGrant(models.Model):
1373

    
1374
    resource                =   models.ForeignKey(Resource)
1375
    project_application     =   models.ForeignKey(ProjectApplication,
1376
                                                  null=True)
1377
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1378
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1379
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1380
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1381
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1382
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1383

    
1384
    objects = ExtendedManager()
1385

    
1386
    class Meta:
1387
        unique_together = ("resource", "project_application")
1388

    
1389

    
1390
class ProjectManager(ForUpdateManager):
1391

    
1392
    def _q_terminated(self):
1393
        return Q(state=Project.TERMINATED)
1394

    
1395
    def terminated_projects(self):
1396
        q = self._q_terminated()
1397
        return self.filter(q)
1398

    
1399
    def not_terminated_projects(self):
1400
        q = ~self._q_terminated()
1401
        return self.filter(q)
1402

    
1403
    def terminating_projects(self):
1404
        q = self._q_terminated() & Q(is_active=True)
1405
        return self.filter(q)
1406

    
1407
    def modified_projects(self):
1408
        return self.filter(is_modified=True)
1409

    
1410

    
1411
class Project(models.Model):
1412

    
1413
    application                 =   models.OneToOneField(
1414
                                            ProjectApplication,
1415
                                            related_name='project')
1416
    last_approval_date          =   models.DateTimeField(null=True)
1417

    
1418
    members                     =   models.ManyToManyField(
1419
                                            AstakosUser,
1420
                                            through='ProjectMembership')
1421

    
1422
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1423
    deactivation_date           =   models.DateTimeField(null=True)
1424

    
1425
    creation_date               =   models.DateTimeField()
1426
    name                        =   models.CharField(
1427
                                            max_length=80,
1428
                                            db_index=True,
1429
                                            unique=True)
1430

    
1431
    APPROVED    = 1
1432
    SUSPENDED   = 10
1433
    TERMINATED  = 100
1434

    
1435
    is_modified                 =   models.BooleanField(default=False,
1436
                                                        db_index=True)
1437
    is_active                   =   models.BooleanField(default=True,
1438
                                                        db_index=True)
1439
    state                       =   models.IntegerField(default=APPROVED,
1440
                                                        db_index=True)
1441

    
1442
    objects     =   ProjectManager()
1443

    
1444
    def __str__(self):
1445
        return _("<project %s '%s'>") % (self.id, self.application.name)
1446

    
1447
    __repr__ = __str__
1448

    
1449
    def is_deactivated(self, reason=None):
1450
        if reason is not None:
1451
            return self.state == reason
1452

    
1453
        return self.state != self.APPROVED
1454

    
1455
    def is_deactivating(self, reason=None):
1456
        if not self.is_active:
1457
            return False
1458

    
1459
        return self.is_deactivated(reason)
1460

    
1461
    def is_deactivated_strict(self, reason=None):
1462
        if self.is_active:
1463
            return False
1464

    
1465
        return self.is_deactivated(reason)
1466

    
1467
    ### Deactivation calls
1468

    
1469
    def deactivate(self):
1470
        self.deactivation_date = datetime.now()
1471
        self.is_active = False
1472

    
1473
    def terminate(self):
1474
        self.deactivation_reason = 'TERMINATED'
1475
        self.state = self.TERMINATED
1476
        self.save()
1477

    
1478

    
1479
    ### Logical checks
1480

    
1481
    def is_inconsistent(self):
1482
        now = datetime.now()
1483
        dates = [self.creation_date,
1484
                 self.last_approval_date,
1485
                 self.deactivation_date]
1486
        return any([date > now for date in dates])
1487

    
1488
    def is_active_strict(self):
1489
        return self.is_active and self.state == self.APPROVED
1490

    
1491
    @property
1492
    def is_alive(self):
1493
        return self.is_active_strict()
1494

    
1495
    @property
1496
    def is_terminated(self):
1497
        return self.is_deactivated(self.TERMINATED)
1498

    
1499
    @property
1500
    def is_suspended(self):
1501
        return False
1502

    
1503
    def violates_resource_grants(self):
1504
        return False
1505

    
1506
    def violates_members_limit(self, adding=0):
1507
        application = self.application
1508
        return (len(self.approved_members) + adding >
1509
                application.limit_on_members_number)
1510

    
1511

    
1512
    ### Other
1513

    
1514
    @property
1515
    def approved_memberships(self):
1516
        query = ProjectMembership.query_approved()
1517
        return self.projectmembership_set.filter(query)
1518

    
1519
    @property
1520
    def approved_members(self):
1521
        return [m.person for m in self.approved_memberships]
1522

    
1523
    def add_member(self, user):
1524
        """
1525
        Raises:
1526
            django.exceptions.PermissionDenied
1527
            astakos.im.models.AstakosUser.DoesNotExist
1528
        """
1529
        if isinstance(user, int):
1530
            user = AstakosUser.objects.get(user=user)
1531

    
1532
        m, created = ProjectMembership.objects.get_or_create(
1533
            person=user, project=self
1534
        )
1535
        m.accept()
1536

    
1537
    def remove_member(self, user):
1538
        """
1539
        Raises:
1540
            django.exceptions.PermissionDenied
1541
            astakos.im.models.AstakosUser.DoesNotExist
1542
            astakos.im.models.ProjectMembership.DoesNotExist
1543
        """
1544
        if isinstance(user, int):
1545
            user = AstakosUser.objects.get(user=user)
1546

    
1547
        m = ProjectMembership.objects.get(person=user, project=self)
1548
        m.remove()
1549

    
1550

    
1551
class PendingMembershipError(Exception):
1552
    pass
1553

    
1554

    
1555
class ProjectMembership(models.Model):
1556

    
1557
    person              =   models.ForeignKey(AstakosUser)
1558
    request_date        =   models.DateField(default=datetime.now())
1559
    project             =   models.ForeignKey(Project)
1560

    
1561
    REQUESTED   =   0
1562
    ACCEPTED    =   1
1563
    SUSPENDED   =   10
1564
    TERMINATED  =   100
1565
    REMOVED     =   200
1566

    
1567
    state               =   models.IntegerField(default=REQUESTED,
1568
                                                db_index=True)
1569
    is_pending          =   models.BooleanField(default=False, db_index=True)
1570
    is_active           =   models.BooleanField(default=False, db_index=True)
1571
    application         =   models.ForeignKey(
1572
                                ProjectApplication,
1573
                                null=True,
1574
                                related_name='memberships')
1575
    pending_application =   models.ForeignKey(
1576
                                ProjectApplication,
1577
                                null=True,
1578
                                related_name='pending_memebrships')
1579
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1580

    
1581
    acceptance_date     =   models.DateField(null=True, db_index=True)
1582
    leave_request_date  =   models.DateField(null=True)
1583

    
1584
    objects     =   ForUpdateManager()
1585

    
1586

    
1587
    def get_combined_state(self):
1588
        return self.state, self.is_active, self.is_pending
1589

    
1590
    @classmethod
1591
    def query_approved(cls):
1592
        return (~Q(state=cls.REQUESTED) &
1593
                ~Q(state=cls.REMOVED))
1594

    
1595
    class Meta:
1596
        unique_together = ("person", "project")
1597
        #index_together = [["project", "state"]]
1598

    
1599
    def __str__(self):
1600
        return _("<'%s' membership in '%s'>") % (
1601
                self.person.username, self.project)
1602

    
1603
    __repr__ = __str__
1604

    
1605
    def __init__(self, *args, **kwargs):
1606
        self.state = self.REQUESTED
1607
        super(ProjectMembership, self).__init__(*args, **kwargs)
1608

    
1609
    def _set_history_item(self, reason, date=None):
1610
        if isinstance(reason, basestring):
1611
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1612

    
1613
        history_item = ProjectMembershipHistory(
1614
                            serial=self.id,
1615
                            person=self.person.uuid,
1616
                            project=self.project_id,
1617
                            date=date or datetime.now(),
1618
                            reason=reason)
1619
        history_item.save()
1620
        serial = history_item.id
1621

    
1622
    def accept(self):
1623
        if self.is_pending:
1624
            m = _("%s: attempt to accept while is pending") % (self,)
1625
            raise AssertionError(m)
1626

    
1627
        state = self.state
1628
        if state != self.REQUESTED:
1629
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1630
            raise AssertionError(m)
1631

    
1632
        now = datetime.now()
1633
        self.acceptance_date = now
1634
        self._set_history_item(reason='ACCEPT', date=now)
1635
        if self.project.is_active_strict():
1636
            self.state = self.ACCEPTED
1637
            self.is_pending = True
1638
        else:
1639
            self.state = self.TERMINATED
1640

    
1641
        self.save()
1642

    
1643
    def remove(self):
1644
        if self.is_pending:
1645
            m = _("%s: attempt to remove while is pending") % (self,)
1646
            raise AssertionError(m)
1647

    
1648
        state = self.state
1649
        if state not in [self.ACCEPTED, self.TERMINATED]:
1650
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1651
            raise AssertionError(m)
1652

    
1653
        self._set_history_item(reason='REMOVE')
1654
        self.state = self.REMOVED
1655
        self.is_pending = True
1656
        self.save()
1657

    
1658
    def reject(self):
1659
        if self.is_pending:
1660
            m = _("%s: attempt to reject while is pending") % (self,)
1661
            raise AssertionError(m)
1662

    
1663
        state = self.state
1664
        if state != self.REQUESTED:
1665
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1666
            raise AssertionError(m)
1667

    
1668
        # rejected requests don't need sync,
1669
        # because they were never effected
1670
        self._set_history_item(reason='REJECT')
1671
        self.delete()
1672

    
1673
    def get_diff_quotas(self, sub_list=None, add_list=None):
1674
        if sub_list is None:
1675
            sub_list = []
1676

    
1677
        if add_list is None:
1678
            add_list = []
1679

    
1680
        sub_append = sub_list.append
1681
        add_append = add_list.append
1682
        holder = self.person.uuid
1683

    
1684
        synced_application = self.application
1685
        if synced_application is not None:
1686
            cur_grants = synced_application.projectresourcegrant_set.all()
1687
            for grant in cur_grants:
1688
                sub_append(QuotaLimits(
1689
                               holder       = holder,
1690
                               resource     = str(grant.resource),
1691
                               capacity     = grant.member_capacity,
1692
                               import_limit = grant.member_import_limit,
1693
                               export_limit = grant.member_export_limit))
1694

    
1695
        pending_application = self.pending_application
1696
        if pending_application is not None:
1697
            new_grants = pending_application.projectresourcegrant_set.all()
1698
            for new_grant in new_grants:
1699
                add_append(QuotaLimits(
1700
                               holder       = holder,
1701
                               resource     = str(new_grant.resource),
1702
                               capacity     = new_grant.member_capacity,
1703
                               import_limit = new_grant.member_import_limit,
1704
                               export_limit = new_grant.member_export_limit))
1705

    
1706
        return (sub_list, add_list)
1707

    
1708
    def set_sync(self):
1709
        if not self.is_pending:
1710
            m = _("%s: attempt to sync a non pending membership") % (self,)
1711
            raise AssertionError(m)
1712

    
1713
        state = self.state
1714
        if state == self.ACCEPTED:
1715
            pending_application = self.pending_application
1716
            if pending_application is None:
1717
                m = _("%s: attempt to sync an empty pending application") % (
1718
                    self,)
1719
                raise AssertionError(m)
1720

    
1721
            self.application = pending_application
1722
            self.is_active = True
1723

    
1724
            self.pending_application = None
1725
            self.pending_serial = None
1726

    
1727
            # project.application may have changed in the meantime,
1728
            # in which case we stay PENDING;
1729
            # we are safe to check due to select_for_update
1730
            if self.application == self.project.application:
1731
                self.is_pending = False
1732
            self.save()
1733

    
1734
        elif state == self.TERMINATED:
1735
            if self.pending_application:
1736
                m = _("%s: attempt to sync in state '%s' "
1737
                      "with a pending application") % (self, state)
1738
                raise AssertionError(m)
1739

    
1740
            self.application = None
1741
            self.pending_serial = None
1742
            self.is_pending = False
1743
            self.save()
1744

    
1745
        elif state == self.REMOVED:
1746
            self.delete()
1747

    
1748
        else:
1749
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1750
            raise AssertionError(m)
1751

    
1752
    def reset_sync(self):
1753
        if not self.is_pending:
1754
            m = _("%s: attempt to reset a non pending membership") % (self,)
1755
            raise AssertionError(m)
1756

    
1757
        state = self.state
1758
        if state in [self.ACCEPTED, self.TERMINATED, self.REMOVED]:
1759
            self.pending_application = None
1760
            self.pending_serial = None
1761
            self.save()
1762
        else:
1763
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1764
            raise AssertionError(m)
1765

    
1766
class Serial(models.Model):
1767
    serial  =   models.AutoField(primary_key=True)
1768

    
1769
def new_serial():
1770
    s = Serial.objects.create()
1771
    serial = s.serial
1772
    s.delete()
1773
    return serial
1774

    
1775
def sync_finish_serials(serials_to_ack=None):
1776
    if serials_to_ack is None:
1777
        serials_to_ack = qh_query_serials([])
1778

    
1779
    serials_to_ack = set(serials_to_ack)
1780
    sfu = ProjectMembership.objects.select_for_update()
1781
    memberships = list(sfu.filter(pending_serial__isnull=False))
1782

    
1783
    if memberships:
1784
        for membership in memberships:
1785
            serial = membership.pending_serial
1786
            if serial in serials_to_ack:
1787
                membership.set_sync()
1788
            else:
1789
                membership.reset_sync()
1790

    
1791
        transaction.commit()
1792

    
1793
    qh_ack_serials(list(serials_to_ack))
1794
    return len(memberships)
1795

    
1796
def pre_sync():
1797
    ACCEPTED = ProjectMembership.ACCEPTED
1798
    TERMINATED = ProjectMembership.TERMINATED
1799
    psfu = Project.objects.select_for_update()
1800

    
1801
    modified = psfu.modified_projects()
1802
    for project in modified:
1803
        objects = project.projectmembership_set.select_for_update()
1804

    
1805
        memberships = objects.filter(state=ACCEPTED)
1806
        for membership in memberships:
1807
            membership.is_pending = True
1808
            membership.save()
1809

    
1810
    terminating = psfu.terminating_projects()
1811
    for project in terminating:
1812
        objects = project.projectmembership_set.select_for_update()
1813

    
1814
        memberships = objects.filter(state=ACCEPTED)
1815
        for membership in memberships:
1816
            membership.is_pending = True
1817
            membership.state = TERMINATED
1818
            membership.save()
1819

    
1820
def do_sync():
1821

    
1822
    ACCEPTED = ProjectMembership.ACCEPTED
1823
    objects = ProjectMembership.objects.select_for_update()
1824

    
1825
    sub_quota, add_quota = [], []
1826

    
1827
    serial = new_serial()
1828

    
1829
    pending = objects.filter(is_pending=True)
1830
    for membership in pending:
1831

    
1832
        if membership.pending_application:
1833
            m = "%s: impossible: pending_application is not None (%s)" % (
1834
                membership, membership.pending_application)
1835
            raise AssertionError(m)
1836
        if membership.pending_serial:
1837
            m = "%s: impossible: pending_serial is not None (%s)" % (
1838
                membership, membership.pending_serial)
1839
            raise AssertionError(m)
1840

    
1841
        if membership.state == ACCEPTED:
1842
            membership.pending_application = membership.project.application
1843

    
1844
        membership.pending_serial = serial
1845
        membership.get_diff_quotas(sub_quota, add_quota)
1846
        membership.save()
1847

    
1848
    transaction.commit()
1849
    # ProjectApplication.approve() unblocks here
1850
    # and can set PENDING an already PENDING membership
1851
    # which has been scheduled to sync with the old project.application
1852
    # Need to check in ProjectMembership.set_sync()
1853

    
1854
    r = qh_add_quota(serial, sub_quota, add_quota)
1855
    if r:
1856
        m = "cannot sync serial: %d" % serial
1857
        raise RuntimeError(m)
1858

    
1859
    return serial
1860

    
1861
def post_sync():
1862
    ACCEPTED = ProjectMembership.ACCEPTED
1863
    psfu = Project.objects.select_for_update()
1864

    
1865
    modified = psfu.modified_projects()
1866
    for project in modified:
1867
        objects = project.projectmembership_set.select_for_update()
1868

    
1869
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
1870
        if not memberships:
1871
            project.is_modified = False
1872
            project.save()
1873

    
1874
    terminating = psfu.terminating_projects()
1875
    for project in terminating:
1876
        objects = project.projectmembership_set.select_for_update()
1877

    
1878
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1879
                                          Q(is_pending=True)))
1880
        if not memberships:
1881
            project.deactivate()
1882
            project.save()
1883

    
1884
    transaction.commit()
1885

    
1886
def sync_projects():
1887
    sync_finish_serials()
1888
    pre_sync()
1889
    serial = do_sync()
1890
    sync_finish_serials([serial])
1891
    post_sync()
1892

    
1893
def trigger_sync(retries=3, retry_wait=1.0):
1894
    transaction.commit()
1895

    
1896
    cursor = connection.cursor()
1897
    locked = True
1898
    try:
1899
        while 1:
1900
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1901
            r = cursor.fetchone()
1902
            if r is None:
1903
                m = "Impossible"
1904
                raise AssertionError(m)
1905
            locked = r[0]
1906
            if locked:
1907
                break
1908

    
1909
            retries -= 1
1910
            if retries <= 0:
1911
                return False
1912
            sleep(retry_wait)
1913

    
1914
        sync_projects()
1915
        return True
1916

    
1917
    finally:
1918
        if locked:
1919
            cursor.execute("SELECT pg_advisory_unlock(1)")
1920
            cursor.fetchall()
1921

    
1922

    
1923
class ProjectMembershipHistory(models.Model):
1924
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1925
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1926

    
1927
    person  =   models.CharField(max_length=255)
1928
    project =   models.BigIntegerField()
1929
    date    =   models.DateField(default=datetime.now)
1930
    reason  =   models.IntegerField()
1931
    serial  =   models.BigIntegerField()
1932

    
1933
### SIGNALS ###
1934
################
1935

    
1936
def create_astakos_user(u):
1937
    try:
1938
        AstakosUser.objects.get(user_ptr=u.pk)
1939
    except AstakosUser.DoesNotExist:
1940
        extended_user = AstakosUser(user_ptr_id=u.pk)
1941
        extended_user.__dict__.update(u.__dict__)
1942
        extended_user.save()
1943
        if not extended_user.has_auth_provider('local'):
1944
            extended_user.add_auth_provider('local')
1945
    except BaseException, e:
1946
        logger.exception(e)
1947

    
1948

    
1949
def fix_superusers(sender, **kwargs):
1950
    # Associate superusers with AstakosUser
1951
    admins = User.objects.filter(is_superuser=True)
1952
    for u in admins:
1953
        create_astakos_user(u)
1954
post_syncdb.connect(fix_superusers)
1955

    
1956

    
1957
def user_post_save(sender, instance, created, **kwargs):
1958
    if not created:
1959
        return
1960
    create_astakos_user(instance)
1961
post_save.connect(user_post_save, sender=User)
1962

    
1963
def astakosuser_post_save(sender, instance, created, **kwargs):
1964
    pass
1965

    
1966
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1967

    
1968
def resource_post_save(sender, instance, created, **kwargs):
1969
    if not created:
1970
        return
1971
    register_resources((instance,))
1972
post_save.connect(resource_post_save, sender=Resource)
1973

    
1974
def renew_token(sender, instance, **kwargs):
1975
    if not instance.auth_token:
1976
        instance.renew_token()
1977
pre_save.connect(renew_token, sender=AstakosUser)
1978
pre_save.connect(renew_token, sender=Service)
1979