Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 19eb3ee6

History | View | Annotate | Download (67.3 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
                kwargs['user__email_verified'] = True
537
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
538
                                                                   **kwargs)
539
            except AstakosUser.DoesNotExist:
540
                return True
541
            else:
542
                return False
543

    
544
        return True
545

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

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

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

    
557
        return True
558

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

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

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

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

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

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

    
597
        provider = self.add_auth_provider(pending.provider,
598
                               identifier=pending.third_party_identifier,
599
                                affiliation=pending.affiliation,
600
                                          provider_info=pending.info)
601

    
602
        if email_re.match(pending.email or '') and pending.email != self.email:
603
            self.additionalmail_set.get_or_create(email=pending.email)
604

    
605
        pending.delete()
606
        return provider
607

    
608
    def remove_auth_provider(self, provider, **kwargs):
609
        self.auth_providers.get(module=provider, **kwargs).delete()
610

    
611
    # user urls
612
    def get_resend_activation_url(self):
613
        return reverse('send_activation', kwargs={'user_id': self.pk})
614

    
615
    def get_provider_remove_url(self, module, **kwargs):
616
        return reverse('remove_auth_provider', kwargs={
617
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
618

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

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

    
631
    def get_auth_providers(self):
632
        return self.auth_providers.all()
633

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

    
643
        return providers
644

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

    
652
    @property
653
    def auth_providers_display(self):
654
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
655

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

    
681
        return mark_safe(message + u' '+ msg_extra)
682

    
683
    def owns_project(self, project):
684
        return project.owner == self
685

    
686
    def is_project_member(self, project):
687
        return project.user_status(self) in [0,1,2,3]
688

    
689
    def is_project_accepted_member(self, project):
690
        return project.user_status(self) == 2
691

    
692

    
693
class AstakosUserAuthProviderManager(models.Manager):
694

    
695
    def active(self, **filters):
696
        return self.filter(active=True, **filters)
697

    
698
    def remove_unverified_providers(self, provider, **filters):
699
        try:
700
            existing = self.filter(module=provider, user__email_verified=False, **filters)
701
            for p in existing:
702
                p.user.delete()
703
        except:
704
            pass
705

    
706

    
707

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

    
726
    objects = AstakosUserAuthProviderManager()
727

    
728
    class Meta:
729
        unique_together = (('identifier', 'module', 'user'), )
730
        ordering = ('module', 'created')
731

    
732
    def __init__(self, *args, **kwargs):
733
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
734
        try:
735
            self.info = json.loads(self.info_data)
736
            if not self.info:
737
                self.info = {}
738
        except Exception, e:
739
            self.info = {}
740

    
741
        for key,value in self.info.iteritems():
742
            setattr(self, 'info_%s' % key, value)
743

    
744

    
745
    @property
746
    def settings(self):
747
        return auth_providers.get_provider(self.module)
748

    
749
    @property
750
    def details_display(self):
751
        try:
752
          return self.settings.get_details_tpl_display % self.__dict__
753
        except:
754
          return ''
755

    
756
    @property
757
    def title_display(self):
758
        title_tpl = self.settings.get_title_display
759
        try:
760
            if self.settings.get_user_title_display:
761
                title_tpl = self.settings.get_user_title_display
762
        except Exception, e:
763
            pass
764
        try:
765
          return title_tpl % self.__dict__
766
        except:
767
          return self.settings.get_title_display % self.__dict__
768

    
769
    def can_remove(self):
770
        return self.user.can_remove_auth_provider(self.module)
771

    
772
    def delete(self, *args, **kwargs):
773
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
774
        if self.module == 'local':
775
            self.user.set_unusable_password()
776
            self.user.save()
777
        return ret
778

    
779
    def __repr__(self):
780
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
781

    
782
    def __unicode__(self):
783
        if self.identifier:
784
            return "%s:%s" % (self.module, self.identifier)
785
        if self.auth_backend:
786
            return "%s:%s" % (self.module, self.auth_backend)
787
        return self.module
788

    
789
    def save(self, *args, **kwargs):
790
        self.info_data = json.dumps(self.info)
791
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
792

    
793

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

    
821
    update_or_create = _update_or_create
822

    
823

    
824
class AstakosUserQuota(models.Model):
825
    objects = ExtendedManager()
826
    capacity = models.BigIntegerField(_('Capacity'), null=True)
827
    quantity = models.BigIntegerField(_('Quantity'), null=True)
828
    export_limit = models.BigIntegerField(_('Export limit'), null=True)
829
    import_limit = models.BigIntegerField(_('Import limit'), null=True)
830
    resource = models.ForeignKey(Resource)
831
    user = models.ForeignKey(AstakosUser)
832

    
833
    class Meta:
834
        unique_together = ("resource", "user")
835

    
836

    
837
class ApprovalTerms(models.Model):
838
    """
839
    Model for approval terms
840
    """
841

    
842
    date = models.DateTimeField(
843
        _('Issue date'), db_index=True, default=datetime.now())
844
    location = models.CharField(_('Terms location'), max_length=255)
845

    
846

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

    
860
    def __init__(self, *args, **kwargs):
861
        super(Invitation, self).__init__(*args, **kwargs)
862
        if not self.id:
863
            self.code = _generate_invitation_code()
864

    
865
    def consume(self):
866
        self.is_consumed = True
867
        self.consumed = datetime.now()
868
        self.save()
869

    
870
    def __unicode__(self):
871
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
872

    
873

    
874
class EmailChangeManager(models.Manager):
875

    
876
    @transaction.commit_on_success
877
    def change_email(self, activation_key):
878
        """
879
        Validate an activation key and change the corresponding
880
        ``User`` if valid.
881

882
        If the key is valid and has not expired, return the ``User``
883
        after activating.
884

885
        If the key is not valid or has expired, return ``None``.
886

887
        If the key is valid but the ``User`` is already active,
888
        return ``None``.
889

890
        After successful email change the activation record is deleted.
891

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

    
920

    
921
class EmailChange(models.Model):
922
    new_email_address = models.EmailField(
923
        _(u'new e-mail address'),
924
        help_text=_('Your old email address will be used until you verify your new one.'))
925
    user = models.ForeignKey(
926
        AstakosUser, unique=True, related_name='emailchanges')
927
    requested_at = models.DateTimeField(default=datetime.now())
928
    activation_key = models.CharField(
929
        max_length=40, unique=True, db_index=True)
930

    
931
    objects = EmailChangeManager()
932

    
933
    def get_url(self):
934
        return reverse('email_change_confirm',
935
                      kwargs={'activation_key': self.activation_key})
936

    
937
    def activation_key_expired(self):
938
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
939
        return self.requested_at + expiration_date < datetime.now()
940

    
941

    
942
class AdditionalMail(models.Model):
943
    """
944
    Model for registring invitations
945
    """
946
    owner = models.ForeignKey(AstakosUser)
947
    email = models.EmailField()
948

    
949

    
950
def _generate_invitation_code():
951
    while True:
952
        code = randint(1, 2L ** 63 - 1)
953
        try:
954
            Invitation.objects.get(code=code)
955
            # An invitation with this code already exists, try again
956
        except Invitation.DoesNotExist:
957
            return code
958

    
959

    
960
def get_latest_terms():
961
    try:
962
        term = ApprovalTerms.objects.order_by('-id')[0]
963
        return term
964
    except IndexError:
965
        pass
966
    return None
967

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

    
986
    class Meta:
987
        unique_together = ("provider", "third_party_identifier")
988

    
989
    def get_user_instance(self):
990
        d = self.__dict__
991
        d.pop('_state', None)
992
        d.pop('id', None)
993
        d.pop('token', None)
994
        d.pop('created', None)
995
        d.pop('info', None)
996
        user = AstakosUser(**d)
997

    
998
        return user
999

    
1000
    @property
1001
    def realname(self):
1002
        return '%s %s' %(self.first_name, self.last_name)
1003

    
1004
    @realname.setter
1005
    def realname(self, value):
1006
        parts = value.split(' ')
1007
        if len(parts) == 2:
1008
            self.first_name = parts[0]
1009
            self.last_name = parts[1]
1010
        else:
1011
            self.last_name = parts[0]
1012

    
1013
    def save(self, **kwargs):
1014
        if not self.id:
1015
            # set username
1016
            while not self.username:
1017
                username =  uuid.uuid4().hex[:30]
1018
                try:
1019
                    AstakosUser.objects.get(username = username)
1020
                except AstakosUser.DoesNotExist, e:
1021
                    self.username = username
1022
        super(PendingThirdPartyUser, self).save(**kwargs)
1023

    
1024
    def generate_token(self):
1025
        self.password = self.third_party_identifier
1026
        self.last_login = datetime.now()
1027
        self.token = default_token_generator.make_token(self)
1028

    
1029
class SessionCatalog(models.Model):
1030
    session_key = models.CharField(_('session key'), max_length=40)
1031
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1032

    
1033

    
1034
### PROJECTS ###
1035
################
1036

    
1037
def synced_model_metaclass(class_name, class_parents, class_attributes):
1038

    
1039
    new_attributes = {}
1040
    sync_attributes = {}
1041

    
1042
    for name, value in class_attributes.iteritems():
1043
        sync, underscore, rest = name.partition('_')
1044
        if sync == 'sync' and underscore == '_':
1045
            sync_attributes[rest] = value
1046
        else:
1047
            new_attributes[name] = value
1048

    
1049
    if 'prefix' not in sync_attributes:
1050
        m = ("you did not specify a 'sync_prefix' attribute "
1051
             "in class '%s'" % (class_name,))
1052
        raise ValueError(m)
1053

    
1054
    prefix = sync_attributes.pop('prefix')
1055
    class_name = sync_attributes.pop('classname', prefix + '_model')
1056

    
1057
    for name, value in sync_attributes.iteritems():
1058
        newname = prefix + '_' + name
1059
        if newname in new_attributes:
1060
            m = ("class '%s' was specified with prefix '%s' "
1061
                 "but it already has an attribute named '%s'"
1062
                 % (class_name, prefix, newname))
1063
            raise ValueError(m)
1064

    
1065
        new_attributes[newname] = value
1066

    
1067
    newclass = type(class_name, class_parents, new_attributes)
1068
    return newclass
1069

    
1070

    
1071
def make_synced(prefix='sync', name='SyncedState'):
1072

    
1073
    the_name = name
1074
    the_prefix = prefix
1075

    
1076
    class SyncedState(models.Model):
1077

    
1078
        sync_classname      = the_name
1079
        sync_prefix         = the_prefix
1080
        __metaclass__       = synced_model_metaclass
1081

    
1082
        sync_new_state      = models.BigIntegerField(null=True)
1083
        sync_synced_state   = models.BigIntegerField(null=True)
1084
        STATUS_SYNCED       = 0
1085
        STATUS_PENDING      = 1
1086
        sync_status         = models.IntegerField(db_index=True)
1087

    
1088
        class Meta:
1089
            abstract = True
1090

    
1091
        class NotSynced(Exception):
1092
            pass
1093

    
1094
        def sync_init_state(self, state):
1095
            self.sync_synced_state = state
1096
            self.sync_new_state = state
1097
            self.sync_status = self.STATUS_SYNCED
1098

    
1099
        def sync_get_status(self):
1100
            return self.sync_status
1101

    
1102
        def sync_set_status(self):
1103
            if self.sync_new_state != self.sync_synced_state:
1104
                self.sync_status = self.STATUS_PENDING
1105
            else:
1106
                self.sync_status = self.STATUS_SYNCED
1107

    
1108
        def sync_set_synced(self):
1109
            self.sync_synced_state = self.sync_new_state
1110
            self.sync_status = self.STATUS_SYNCED
1111

    
1112
        def sync_get_synced_state(self):
1113
            return self.sync_synced_state
1114

    
1115
        def sync_set_new_state(self, new_state):
1116
            self.sync_new_state = new_state
1117
            self.sync_set_status()
1118

    
1119
        def sync_get_new_state(self):
1120
            return self.sync_new_state
1121

    
1122
        def sync_set_synced_state(self, synced_state):
1123
            self.sync_synced_state = synced_state
1124
            self.sync_set_status()
1125

    
1126
        def sync_get_pending_objects(self):
1127
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1128
            return self.objects.filter(**kw)
1129

    
1130
        def sync_get_synced_objects(self):
1131
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1132
            return self.objects.filter(**kw)
1133

    
1134
        def sync_verify_get_synced_state(self):
1135
            status = self.sync_get_status()
1136
            state = self.sync_get_synced_state()
1137
            verified = (status == self.STATUS_SYNCED)
1138
            return state, verified
1139

    
1140
        def sync_is_synced(self):
1141
            state, verified = self.sync_verify_get_synced_state()
1142
            return verified
1143

    
1144
    return SyncedState
1145

    
1146
SyncedState = make_synced(prefix='sync', name='SyncedState')
1147

    
1148

    
1149
class ProjectApplicationManager(ForUpdateManager):
1150

    
1151
    def user_projects(self, user):
1152
        """
1153
        Return projects accessed by specified user.
1154
        """
1155
        participates_fitlers = Q(owner=user) | Q(applicant=user) | \
1156
                               Q(project__projectmembership__person=user)
1157
        state_filters = (Q(state=ProjectApplication.PENDING) & \
1158
                        Q(precursor_application__isnull=True)) | \
1159
                        Q(state=ProjectApplication.APPROVED)
1160
        return self.filter(participates_fitlers & state_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
    100: _('Owner'),
1171
      0: _('Join requested'),
1172
      1: _('Pending'),
1173
      2: _('Accepted member'),
1174
      3: _('Removing'),
1175
      4: _('Removed'),
1176
     -1: _('Not a member'),
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
        """
1257
        100 OWNER
1258
        0   REQUESTED
1259
        1   PENDING
1260
        2   ACCEPTED
1261
        3   REMOVING
1262
        4   REMOVED
1263
       -1   User has no association with the project
1264
        """
1265
        try:
1266
            membership = self.project.projectmembership_set.get(person=user)
1267
            status = membership.state
1268
        except Project.DoesNotExist:
1269
            status = -1
1270
        except ProjectMembership.DoesNotExist:
1271
            status = -1
1272

    
1273
        return status
1274

    
1275
    def user_status_display(self, user):
1276
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1277

    
1278
    def members_count(self):
1279
        return self.project.approved_memberships.count()
1280

    
1281
    @property
1282
    def grants(self):
1283
        return self.projectresourcegrant_set.values(
1284
            'member_capacity', 'resource__name', 'resource__service__name')
1285

    
1286
    @property
1287
    def resource_policies(self):
1288
        return self.projectresourcegrant_set.all()
1289

    
1290
    @resource_policies.setter
1291
    def resource_policies(self, policies):
1292
        for p in policies:
1293
            service = p.get('service', None)
1294
            resource = p.get('resource', None)
1295
            uplimit = p.get('uplimit', 0)
1296
            self.add_resource_policy(service, resource, uplimit)
1297

    
1298
    @property
1299
    def follower(self):
1300
        try:
1301
            return ProjectApplication.objects.get(precursor_application=self)
1302
        except ProjectApplication.DoesNotExist:
1303
            return
1304

    
1305
    def followers(self):
1306
        current = self
1307
        try:
1308
            while current.projectapplication:
1309
                yield current.follower
1310
                current = current.follower
1311
        except:
1312
            pass
1313

    
1314
    def last_follower(self):
1315
        try:
1316
            return list(self.followers())[-1]
1317
        except IndexError:
1318
            return None
1319

    
1320
    def _get_project_for_update(self):
1321
        try:
1322
            objects = Project.objects.select_for_update()
1323
            project = objects.get(id=self.chain)
1324
            return project
1325
        except Project.DoesNotExist:
1326
            return None
1327

    
1328
    def deny(self):
1329
        if self.state != self.PENDING:
1330
            m = _("cannot deny: application '%s' in state '%s'") % (
1331
                    self.id, self.state)
1332
            raise AssertionError(m)
1333

    
1334
        self.state = self.DENIED
1335
        self.save()
1336

    
1337
    def approve(self, approval_user=None):
1338
        """
1339
        If approval_user then during owner membership acceptance
1340
        it is checked whether the request_user is eligible.
1341

1342
        Raises:
1343
            PermissionDenied
1344
        """
1345

    
1346
        if not transaction.is_managed():
1347
            raise AssertionError("NOPE")
1348

    
1349
        new_project_name = self.name
1350
        if self.state != self.PENDING:
1351
            m = _("cannot approve: project '%s' in state '%s'") % (
1352
                    new_project_name, self.state)
1353
            raise PermissionDenied(m) # invalid argument
1354

    
1355
        now = datetime.now()
1356
        project = self._get_project_for_update()
1357

    
1358
        try:
1359
            # needs SERIALIZABLE
1360
            conflicting_project = Project.objects.get(name=new_project_name)
1361
            if (conflicting_project.is_alive and
1362
                conflicting_project != project):
1363
                m = (_("cannot approve: project with name '%s' "
1364
                       "already exists (serial: %s)") % (
1365
                        new_project_name, conflicting_project.id))
1366
                raise PermissionDenied(m) # invalid argument
1367
        except Project.DoesNotExist:
1368
            pass
1369

    
1370
        new_project = False
1371
        if project is None:
1372
            new_project = True
1373
            project = Project(id=self.chain, creation_date=now)
1374

    
1375
        project.name = new_project_name
1376
        project.application = self
1377
        project.last_approval_date = now
1378
        if not new_project:
1379
            project.is_modified = True
1380

    
1381
        project.save()
1382

    
1383
        self.state = self.APPROVED
1384
        self.save()
1385

    
1386
def submit_application(**kw):
1387

    
1388
    resource_policies = kw.pop('resource_policies', None)
1389
    application = ProjectApplication(**kw)
1390

    
1391
    precursor = kw['precursor_application']
1392

    
1393
    if precursor is None:
1394
        application.chain = new_chain()
1395
    else:
1396
        application.chain = precursor.chain
1397
        if precursor.state == ProjectApplication.PENDING:
1398
            precursor.state = ProjectApplication.REPLACED
1399
            precursor.save()
1400

    
1401
    application.save()
1402
    application.resource_policies = resource_policies
1403
    return application
1404

    
1405
class ProjectResourceGrant(models.Model):
1406

    
1407
    resource                =   models.ForeignKey(Resource)
1408
    project_application     =   models.ForeignKey(ProjectApplication,
1409
                                                  null=True)
1410
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1411
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1412
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1413
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1414
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1415
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1416

    
1417
    objects = ExtendedManager()
1418

    
1419
    class Meta:
1420
        unique_together = ("resource", "project_application")
1421

    
1422

    
1423
class ProjectManager(ForUpdateManager):
1424

    
1425
    def _q_terminated(self):
1426
        return Q(state=Project.TERMINATED)
1427

    
1428
    def terminated_projects(self):
1429
        q = self._q_terminated()
1430
        return self.filter(q)
1431

    
1432
    def not_terminated_projects(self):
1433
        q = ~self._q_terminated()
1434
        return self.filter(q)
1435

    
1436
    def terminating_projects(self):
1437
        q = self._q_terminated() & Q(is_active=True)
1438
        return self.filter(q)
1439

    
1440
    def modified_projects(self):
1441
        return self.filter(is_modified=True)
1442

    
1443

    
1444
class Project(models.Model):
1445

    
1446
    application                 =   models.OneToOneField(
1447
                                            ProjectApplication,
1448
                                            related_name='project')
1449
    last_approval_date          =   models.DateTimeField(null=True)
1450

    
1451
    members                     =   models.ManyToManyField(
1452
                                            AstakosUser,
1453
                                            through='ProjectMembership')
1454

    
1455
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1456
    deactivation_date           =   models.DateTimeField(null=True)
1457

    
1458
    creation_date               =   models.DateTimeField()
1459
    name                        =   models.CharField(
1460
                                            max_length=80,
1461
                                            db_index=True,
1462
                                            unique=True)
1463

    
1464
    APPROVED    = 1
1465
    SUSPENDED   = 10
1466
    TERMINATED  = 100
1467

    
1468
    is_modified                 =   models.BooleanField(default=False,
1469
                                                        db_index=True)
1470
    is_active                   =   models.BooleanField(default=True,
1471
                                                        db_index=True)
1472
    state                       =   models.IntegerField(default=APPROVED,
1473
                                                        db_index=True)
1474

    
1475
    objects     =   ProjectManager()
1476

    
1477
    def __str__(self):
1478
        return _("<project %s '%s'>") % (self.id, self.application.name)
1479

    
1480
    __repr__ = __str__
1481

    
1482
    def is_deactivated(self, reason=None):
1483
        if reason is not None:
1484
            return self.state == reason
1485

    
1486
        return self.state != self.APPROVED
1487

    
1488
    def is_deactivating(self, reason=None):
1489
        if not self.is_active:
1490
            return False
1491

    
1492
        return self.is_deactivated(reason)
1493

    
1494
    def is_deactivated_strict(self, reason=None):
1495
        if self.is_active:
1496
            return False
1497

    
1498
        return self.is_deactivated(reason)
1499

    
1500
    ### Deactivation calls
1501

    
1502
    def deactivate(self):
1503
        self.deactivation_date = datetime.now()
1504
        self.is_active = False
1505

    
1506
    def terminate(self):
1507
        self.deactivation_reason = 'TERMINATED'
1508
        self.state = self.TERMINATED
1509
        self.save()
1510

    
1511

    
1512
    ### Logical checks
1513

    
1514
    def is_inconsistent(self):
1515
        now = datetime.now()
1516
        dates = [self.creation_date,
1517
                 self.last_approval_date,
1518
                 self.deactivation_date]
1519
        return any([date > now for date in dates])
1520

    
1521
    def is_active_strict(self):
1522
        return self.is_active and self.state == self.APPROVED
1523

    
1524
    @property
1525
    def is_alive(self):
1526
        return self.is_active_strict()
1527

    
1528
    @property
1529
    def is_terminated(self):
1530
        return self.is_deactivated(self.TERMINATED)
1531

    
1532
    @property
1533
    def is_suspended(self):
1534
        return False
1535

    
1536
    def violates_resource_grants(self):
1537
        return False
1538

    
1539
    def violates_members_limit(self, adding=0):
1540
        application = self.application
1541
        limit = application.limit_on_members_number
1542
        if limit is None:
1543
            return False
1544
        return (len(self.approved_members) + adding > limit)
1545

    
1546

    
1547
    ### Other
1548

    
1549
    @property
1550
    def approved_memberships(self):
1551
        query = ProjectMembership.query_approved()
1552
        return self.projectmembership_set.filter(query)
1553

    
1554
    @property
1555
    def approved_members(self):
1556
        return [m.person for m in self.approved_memberships]
1557

    
1558
    def add_member(self, user):
1559
        """
1560
        Raises:
1561
            django.exceptions.PermissionDenied
1562
            astakos.im.models.AstakosUser.DoesNotExist
1563
        """
1564
        if isinstance(user, int):
1565
            user = AstakosUser.objects.get(user=user)
1566

    
1567
        m, created = ProjectMembership.objects.get_or_create(
1568
            person=user, project=self
1569
        )
1570
        m.accept()
1571

    
1572
    def remove_member(self, user):
1573
        """
1574
        Raises:
1575
            django.exceptions.PermissionDenied
1576
            astakos.im.models.AstakosUser.DoesNotExist
1577
            astakos.im.models.ProjectMembership.DoesNotExist
1578
        """
1579
        if isinstance(user, int):
1580
            user = AstakosUser.objects.get(user=user)
1581

    
1582
        m = ProjectMembership.objects.get(person=user, project=self)
1583
        m.remove()
1584

    
1585

    
1586
class PendingMembershipError(Exception):
1587
    pass
1588

    
1589

    
1590
class ProjectMembership(models.Model):
1591

    
1592
    person              =   models.ForeignKey(AstakosUser)
1593
    request_date        =   models.DateField(default=datetime.now())
1594
    project             =   models.ForeignKey(Project)
1595

    
1596
    REQUESTED   =   0
1597
    ACCEPTED    =   1
1598
    SUSPENDED   =   10
1599
    TERMINATED  =   100
1600
    REMOVED     =   200
1601

    
1602
    state               =   models.IntegerField(default=REQUESTED,
1603
                                                db_index=True)
1604
    is_pending          =   models.BooleanField(default=False, db_index=True)
1605
    is_active           =   models.BooleanField(default=False, db_index=True)
1606
    application         =   models.ForeignKey(
1607
                                ProjectApplication,
1608
                                null=True,
1609
                                related_name='memberships')
1610
    pending_application =   models.ForeignKey(
1611
                                ProjectApplication,
1612
                                null=True,
1613
                                related_name='pending_memebrships')
1614
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1615

    
1616
    acceptance_date     =   models.DateField(null=True, db_index=True)
1617
    leave_request_date  =   models.DateField(null=True)
1618

    
1619
    objects     =   ForUpdateManager()
1620

    
1621

    
1622
    def get_combined_state(self):
1623
        return self.state, self.is_active, self.is_pending
1624

    
1625
    @classmethod
1626
    def query_approved(cls):
1627
        return (~Q(state=cls.REQUESTED) &
1628
                ~Q(state=cls.REMOVED))
1629

    
1630
    class Meta:
1631
        unique_together = ("person", "project")
1632
        #index_together = [["project", "state"]]
1633

    
1634
    def __str__(self):
1635
        return _("<'%s' membership in '%s'>") % (
1636
                self.person.username, self.project)
1637

    
1638
    __repr__ = __str__
1639

    
1640
    def __init__(self, *args, **kwargs):
1641
        self.state = self.REQUESTED
1642
        super(ProjectMembership, self).__init__(*args, **kwargs)
1643

    
1644
    def _set_history_item(self, reason, date=None):
1645
        if isinstance(reason, basestring):
1646
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1647

    
1648
        history_item = ProjectMembershipHistory(
1649
                            serial=self.id,
1650
                            person=self.person_id,
1651
                            project=self.project_id,
1652
                            date=date or datetime.now(),
1653
                            reason=reason)
1654
        history_item.save()
1655
        serial = history_item.id
1656

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

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

    
1667
        now = datetime.now()
1668
        self.acceptance_date = now
1669
        self._set_history_item(reason='ACCEPT', date=now)
1670
        if self.project.is_active_strict():
1671
            self.state = self.ACCEPTED
1672
            self.is_pending = True
1673
        else:
1674
            self.state = self.TERMINATED
1675

    
1676
        self.save()
1677

    
1678
    def remove(self):
1679
        if self.is_pending:
1680
            m = _("%s: attempt to remove while is pending") % (self,)
1681
            raise AssertionError(m)
1682

    
1683
        state = self.state
1684
        if state not in [self.ACCEPTED, self.TERMINATED]:
1685
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1686
            raise AssertionError(m)
1687

    
1688
        self._set_history_item(reason='REMOVE')
1689
        self.state = self.REMOVED
1690
        self.is_pending = True
1691
        self.save()
1692

    
1693
    def reject(self):
1694
        if self.is_pending:
1695
            m = _("%s: attempt to reject while is pending") % (self,)
1696
            raise AssertionError(m)
1697

    
1698
        state = self.state
1699
        if state != self.REQUESTED:
1700
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1701
            raise AssertionError(m)
1702

    
1703
        # rejected requests don't need sync,
1704
        # because they were never effected
1705
        self._set_history_item(reason='REJECT')
1706
        self.delete()
1707

    
1708
    def get_diff_quotas(self, sub_list=None, add_list=None):
1709
        if sub_list is None:
1710
            sub_list = []
1711

    
1712
        if add_list is None:
1713
            add_list = []
1714

    
1715
        sub_append = sub_list.append
1716
        add_append = add_list.append
1717
        holder = self.person.uuid
1718

    
1719
        synced_application = self.application
1720
        if synced_application is not None:
1721
            cur_grants = synced_application.projectresourcegrant_set.all()
1722
            for grant in cur_grants:
1723
                sub_append(QuotaLimits(
1724
                               holder       = holder,
1725
                               resource     = str(grant.resource),
1726
                               capacity     = grant.member_capacity,
1727
                               import_limit = grant.member_import_limit,
1728
                               export_limit = grant.member_export_limit))
1729

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

    
1741
        return (sub_list, add_list)
1742

    
1743
    def set_sync(self):
1744
        if not self.is_pending:
1745
            m = _("%s: attempt to sync a non pending membership") % (self,)
1746
            raise AssertionError(m)
1747

    
1748
        state = self.state
1749
        if state == self.ACCEPTED:
1750
            pending_application = self.pending_application
1751
            if pending_application is None:
1752
                m = _("%s: attempt to sync an empty pending application") % (
1753
                    self,)
1754
                raise AssertionError(m)
1755

    
1756
            self.application = pending_application
1757
            self.is_active = True
1758

    
1759
            self.pending_application = None
1760
            self.pending_serial = None
1761

    
1762
            # project.application may have changed in the meantime,
1763
            # in which case we stay PENDING;
1764
            # we are safe to check due to select_for_update
1765
            if self.application == self.project.application:
1766
                self.is_pending = False
1767
            self.save()
1768

    
1769
        elif state == self.TERMINATED:
1770
            if self.pending_application:
1771
                m = _("%s: attempt to sync in state '%s' "
1772
                      "with a pending application") % (self, state)
1773
                raise AssertionError(m)
1774

    
1775
            self.application = None
1776
            self.pending_serial = None
1777
            self.is_pending = False
1778
            self.save()
1779

    
1780
        elif state == self.REMOVED:
1781
            self.delete()
1782

    
1783
        else:
1784
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1785
            raise AssertionError(m)
1786

    
1787
    def reset_sync(self):
1788
        if not self.is_pending:
1789
            m = _("%s: attempt to reset a non pending membership") % (self,)
1790
            raise AssertionError(m)
1791

    
1792
        state = self.state
1793
        if state in [self.ACCEPTED, self.TERMINATED, self.REMOVED]:
1794
            self.pending_application = None
1795
            self.pending_serial = None
1796
            self.save()
1797
        else:
1798
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1799
            raise AssertionError(m)
1800

    
1801
class Serial(models.Model):
1802
    serial  =   models.AutoField(primary_key=True)
1803

    
1804
def new_serial():
1805
    s = Serial.objects.create()
1806
    serial = s.serial
1807
    s.delete()
1808
    return serial
1809

    
1810
def sync_finish_serials(serials_to_ack=None):
1811
    if serials_to_ack is None:
1812
        serials_to_ack = qh_query_serials([])
1813

    
1814
    serials_to_ack = set(serials_to_ack)
1815
    sfu = ProjectMembership.objects.select_for_update()
1816
    memberships = list(sfu.filter(pending_serial__isnull=False))
1817

    
1818
    if memberships:
1819
        for membership in memberships:
1820
            serial = membership.pending_serial
1821
            if serial in serials_to_ack:
1822
                membership.set_sync()
1823
            else:
1824
                membership.reset_sync()
1825

    
1826
        transaction.commit()
1827

    
1828
    qh_ack_serials(list(serials_to_ack))
1829
    return len(memberships)
1830

    
1831
def pre_sync():
1832
    ACCEPTED = ProjectMembership.ACCEPTED
1833
    TERMINATED = ProjectMembership.TERMINATED
1834
    psfu = Project.objects.select_for_update()
1835

    
1836
    modified = psfu.modified_projects()
1837
    for project in modified:
1838
        objects = project.projectmembership_set.select_for_update()
1839

    
1840
        memberships = objects.filter(state=ACCEPTED)
1841
        for membership in memberships:
1842
            membership.is_pending = True
1843
            membership.save()
1844

    
1845
    terminating = psfu.terminating_projects()
1846
    for project in terminating:
1847
        objects = project.projectmembership_set.select_for_update()
1848

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

    
1855
def do_sync():
1856

    
1857
    ACCEPTED = ProjectMembership.ACCEPTED
1858
    objects = ProjectMembership.objects.select_for_update()
1859

    
1860
    sub_quota, add_quota = [], []
1861

    
1862
    serial = new_serial()
1863

    
1864
    pending = objects.filter(is_pending=True)
1865
    for membership in pending:
1866

    
1867
        if membership.pending_application:
1868
            m = "%s: impossible: pending_application is not None (%s)" % (
1869
                membership, membership.pending_application)
1870
            raise AssertionError(m)
1871
        if membership.pending_serial:
1872
            m = "%s: impossible: pending_serial is not None (%s)" % (
1873
                membership, membership.pending_serial)
1874
            raise AssertionError(m)
1875

    
1876
        if membership.state == ACCEPTED:
1877
            membership.pending_application = membership.project.application
1878

    
1879
        membership.pending_serial = serial
1880
        membership.get_diff_quotas(sub_quota, add_quota)
1881
        membership.save()
1882

    
1883
    transaction.commit()
1884
    # ProjectApplication.approve() unblocks here
1885
    # and can set PENDING an already PENDING membership
1886
    # which has been scheduled to sync with the old project.application
1887
    # Need to check in ProjectMembership.set_sync()
1888

    
1889
    r = qh_add_quota(serial, sub_quota, add_quota)
1890
    if r:
1891
        m = "cannot sync serial: %d" % serial
1892
        raise RuntimeError(m)
1893

    
1894
    return serial
1895

    
1896
def post_sync():
1897
    ACCEPTED = ProjectMembership.ACCEPTED
1898
    psfu = Project.objects.select_for_update()
1899

    
1900
    modified = psfu.modified_projects()
1901
    for project in modified:
1902
        objects = project.projectmembership_set.select_for_update()
1903

    
1904
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
1905
        if not memberships:
1906
            project.is_modified = False
1907
            project.save()
1908

    
1909
    terminating = psfu.terminating_projects()
1910
    for project in terminating:
1911
        objects = project.projectmembership_set.select_for_update()
1912

    
1913
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1914
                                          Q(is_pending=True)))
1915
        if not memberships:
1916
            project.deactivate()
1917
            project.save()
1918

    
1919
    transaction.commit()
1920

    
1921
def sync_projects():
1922
    sync_finish_serials()
1923
    pre_sync()
1924
    serial = do_sync()
1925
    sync_finish_serials([serial])
1926
    post_sync()
1927

    
1928
def trigger_sync(retries=3, retry_wait=1.0):
1929
    transaction.commit()
1930

    
1931
    cursor = connection.cursor()
1932
    locked = True
1933
    try:
1934
        while 1:
1935
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1936
            r = cursor.fetchone()
1937
            if r is None:
1938
                m = "Impossible"
1939
                raise AssertionError(m)
1940
            locked = r[0]
1941
            if locked:
1942
                break
1943

    
1944
            retries -= 1
1945
            if retries <= 0:
1946
                return False
1947
            sleep(retry_wait)
1948

    
1949
        sync_projects()
1950
        return True
1951

    
1952
    finally:
1953
        if locked:
1954
            cursor.execute("SELECT pg_advisory_unlock(1)")
1955
            cursor.fetchall()
1956

    
1957

    
1958
class ProjectMembershipHistory(models.Model):
1959
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1960
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1961

    
1962
    person  =   models.BigIntegerField()
1963
    project =   models.BigIntegerField()
1964
    date    =   models.DateField(default=datetime.now)
1965
    reason  =   models.IntegerField()
1966
    serial  =   models.BigIntegerField()
1967

    
1968
### SIGNALS ###
1969
################
1970

    
1971
def create_astakos_user(u):
1972
    try:
1973
        AstakosUser.objects.get(user_ptr=u.pk)
1974
    except AstakosUser.DoesNotExist:
1975
        extended_user = AstakosUser(user_ptr_id=u.pk)
1976
        extended_user.__dict__.update(u.__dict__)
1977
        extended_user.save()
1978
        if not extended_user.has_auth_provider('local'):
1979
            extended_user.add_auth_provider('local')
1980
    except BaseException, e:
1981
        logger.exception(e)
1982

    
1983

    
1984
def fix_superusers(sender, **kwargs):
1985
    # Associate superusers with AstakosUser
1986
    admins = User.objects.filter(is_superuser=True)
1987
    for u in admins:
1988
        create_astakos_user(u)
1989
post_syncdb.connect(fix_superusers)
1990

    
1991

    
1992
def user_post_save(sender, instance, created, **kwargs):
1993
    if not created:
1994
        return
1995
    create_astakos_user(instance)
1996
post_save.connect(user_post_save, sender=User)
1997

    
1998
def astakosuser_post_save(sender, instance, created, **kwargs):
1999
    pass
2000

    
2001
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2002

    
2003
def resource_post_save(sender, instance, created, **kwargs):
2004
    if not created:
2005
        return
2006
    register_resources((instance,))
2007
post_save.connect(resource_post_save, sender=Resource)
2008

    
2009
def renew_token(sender, instance, **kwargs):
2010
    if not instance.auth_token:
2011
        instance.renew_token()
2012
pre_save.connect(renew_token, sender=AstakosUser)
2013
pre_save.connect(renew_token, sender=Service)
2014