Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 943d5554

History | View | Annotate | Download (66.9 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
PROJECT_STATE_DISPLAY = {
1170
    'Pending': _('Pending review'),
1171
    'Approved': _('Active'),
1172
    'Replaced': _('Replaced'),
1173
    'Unknown': _('Unknown')
1174
}
1175

    
1176
USER_STATUS_DISPLAY = {
1177
    100: _('Owner'),
1178
      0: _('Join requested'),
1179
      1: _('Pending'),
1180
      2: _('Accepted member'),
1181
      3: _('Removing'),
1182
      4: _('Removed'),
1183
     -1: _('Not a member'),
1184
}
1185

    
1186
class Chain(models.Model):
1187
    chain  =   models.AutoField(primary_key=True)
1188

    
1189
def new_chain():
1190
    c = Chain.objects.create()
1191
    chain = c.chain
1192
    c.delete()
1193
    return chain
1194

    
1195

    
1196
class ProjectApplication(models.Model):
1197
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1198
    applicant               =   models.ForeignKey(
1199
                                    AstakosUser,
1200
                                    related_name='projects_applied',
1201
                                    db_index=True)
1202

    
1203
    state                   =   models.CharField(max_length=80,
1204
                                                default=PENDING)
1205

    
1206
    owner                   =   models.ForeignKey(
1207
                                    AstakosUser,
1208
                                    related_name='projects_owned',
1209
                                    db_index=True)
1210

    
1211
    chain                   =   models.IntegerField()
1212
    precursor_application   =   models.OneToOneField('ProjectApplication',
1213
                                                     null=True,
1214
                                                     blank=True,
1215
                                                     db_index=True)
1216

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

    
1233

    
1234
    objects                 =   ProjectApplicationManager()
1235

    
1236
    class Meta:
1237
        unique_together = ("chain", "id")
1238

    
1239
    def __unicode__(self):
1240
        return "%s applied by %s" % (self.name, self.applicant)
1241

    
1242
    def state_display(self):
1243
        return PROJECT_STATE_DISPLAY.get(self.state, _('Unknown'))
1244

    
1245
    def add_resource_policy(self, service, resource, uplimit):
1246
        """Raises ObjectDoesNotExist, IntegrityError"""
1247
        q = self.projectresourcegrant_set
1248
        resource = Resource.objects.get(service__name=service, name=resource)
1249
        q.create(resource=resource, member_capacity=uplimit)
1250

    
1251
    def user_status(self, user):
1252
        """
1253
        100 OWNER
1254
        0   REQUESTED
1255
        1   PENDING
1256
        2   ACCEPTED
1257
        3   REMOVING
1258
        4   REMOVED
1259
       -1   User has no association with the project
1260
        """
1261
        try:
1262
            membership = self.project.projectmembership_set.get(person=user)
1263
            status = membership.state
1264
        except Project.DoesNotExist:
1265
            status = -1
1266
        except ProjectMembership.DoesNotExist:
1267
            status = -1
1268

    
1269
        return status
1270

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

    
1274
    def members_count(self):
1275
        return self.project.approved_memberships.count()
1276

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

    
1282
    @property
1283
    def resource_policies(self):
1284
        return self.projectresourcegrant_set.all()
1285

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

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

    
1301
    def followers(self):
1302
        current = self
1303
        try:
1304
            while current.projectapplication:
1305
                yield current.follower
1306
                current = current.follower
1307
        except:
1308
            pass
1309

    
1310
    def last_follower(self):
1311
        try:
1312
            return list(self.followers())[-1]
1313
        except IndexError:
1314
            return None
1315

    
1316
    def _get_project_for_update(self):
1317
        try:
1318
            objects = Project.objects.select_for_update()
1319
            project = objects.get(id=self.chain)
1320
            return project
1321
        except Project.DoesNotExist:
1322
            return None
1323

    
1324
    def approve(self, approval_user=None):
1325
        """
1326
        If approval_user then during owner membership acceptance
1327
        it is checked whether the request_user is eligible.
1328

1329
        Raises:
1330
            PermissionDenied
1331
        """
1332

    
1333
        if not transaction.is_managed():
1334
            raise AssertionError("NOPE")
1335

    
1336
        new_project_name = self.name
1337
        if self.state != self.PENDING:
1338
            m = _("cannot approve: project '%s' in state '%s'") % (
1339
                    new_project_name, self.state)
1340
            raise PermissionDenied(m) # invalid argument
1341

    
1342
        now = datetime.now()
1343
        project = self._get_project_for_update()
1344

    
1345
        try:
1346
            # needs SERIALIZABLE
1347
            conflicting_project = Project.objects.get(name=new_project_name)
1348
            if (conflicting_project.is_alive and
1349
                conflicting_project != project):
1350
                m = (_("cannot approve: project with name '%s' "
1351
                       "already exists (serial: %s)") % (
1352
                        new_project_name, conflicting_project.id))
1353
                raise PermissionDenied(m) # invalid argument
1354
        except Project.DoesNotExist:
1355
            pass
1356

    
1357
        new_project = False
1358
        if project is None:
1359
            new_project = True
1360
            project = Project(id=self.chain, creation_date=now)
1361

    
1362
        project.name = new_project_name
1363
        project.application = self
1364
        project.last_approval_date = now
1365
        if not new_project:
1366
            project.is_modified = True
1367

    
1368
        project.save()
1369

    
1370
        self.state = self.APPROVED
1371
        self.save()
1372

    
1373
def submit_application(**kw):
1374

    
1375
    resource_policies = kw.pop('resource_policies', None)
1376
    application = ProjectApplication(**kw)
1377

    
1378
    precursor = kw['precursor_application']
1379

    
1380
    if precursor is not None:
1381
        precursor.state = ProjectApplication.REPLACED
1382
        precursor.save()
1383
        application.chain = precursor.chain
1384
    else:
1385
        application.chain = new_chain()
1386

    
1387
    application.save()
1388
    application.resource_policies = resource_policies
1389
    return application
1390

    
1391
class ProjectResourceGrant(models.Model):
1392

    
1393
    resource                =   models.ForeignKey(Resource)
1394
    project_application     =   models.ForeignKey(ProjectApplication,
1395
                                                  null=True)
1396
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1397
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1398
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1399
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1400
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1401
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1402

    
1403
    objects = ExtendedManager()
1404

    
1405
    class Meta:
1406
        unique_together = ("resource", "project_application")
1407

    
1408

    
1409
class ProjectManager(ForUpdateManager):
1410

    
1411
    def _q_terminated(self):
1412
        return Q(state=Project.TERMINATED)
1413

    
1414
    def terminated_projects(self):
1415
        q = self._q_terminated()
1416
        return self.filter(q)
1417

    
1418
    def not_terminated_projects(self):
1419
        q = ~self._q_terminated()
1420
        return self.filter(q)
1421

    
1422
    def terminating_projects(self):
1423
        q = self._q_terminated() & Q(is_active=True)
1424
        return self.filter(q)
1425

    
1426
    def modified_projects(self):
1427
        return self.filter(is_modified=True)
1428

    
1429

    
1430
class Project(models.Model):
1431

    
1432
    application                 =   models.OneToOneField(
1433
                                            ProjectApplication,
1434
                                            related_name='project')
1435
    last_approval_date          =   models.DateTimeField(null=True)
1436

    
1437
    members                     =   models.ManyToManyField(
1438
                                            AstakosUser,
1439
                                            through='ProjectMembership')
1440

    
1441
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1442
    deactivation_date           =   models.DateTimeField(null=True)
1443

    
1444
    creation_date               =   models.DateTimeField()
1445
    name                        =   models.CharField(
1446
                                            max_length=80,
1447
                                            db_index=True,
1448
                                            unique=True)
1449

    
1450
    APPROVED    = 1
1451
    SUSPENDED   = 10
1452
    TERMINATED  = 100
1453

    
1454
    is_modified                 =   models.BooleanField(default=False,
1455
                                                        db_index=True)
1456
    is_active                   =   models.BooleanField(default=True,
1457
                                                        db_index=True)
1458
    state                       =   models.IntegerField(default=APPROVED,
1459
                                                        db_index=True)
1460

    
1461
    objects     =   ProjectManager()
1462

    
1463
    def __str__(self):
1464
        return _("<project %s '%s'>") % (self.id, self.application.name)
1465

    
1466
    __repr__ = __str__
1467

    
1468
    def is_deactivated(self, reason=None):
1469
        if reason is not None:
1470
            return self.state == reason
1471

    
1472
        return self.state != self.APPROVED
1473

    
1474
    def is_deactivating(self, reason=None):
1475
        if not self.is_active:
1476
            return False
1477

    
1478
        return self.is_deactivated(reason)
1479

    
1480
    def is_deactivated_strict(self, reason=None):
1481
        if self.is_active:
1482
            return False
1483

    
1484
        return self.is_deactivated(reason)
1485

    
1486
    ### Deactivation calls
1487

    
1488
    def deactivate(self):
1489
        self.deactivation_date = datetime.now()
1490
        self.is_active = False
1491

    
1492
    def terminate(self):
1493
        self.deactivation_reason = 'TERMINATED'
1494
        self.state = self.TERMINATED
1495
        self.save()
1496

    
1497

    
1498
    ### Logical checks
1499

    
1500
    def is_inconsistent(self):
1501
        now = datetime.now()
1502
        dates = [self.creation_date,
1503
                 self.last_approval_date,
1504
                 self.deactivation_date]
1505
        return any([date > now for date in dates])
1506

    
1507
    def is_active_strict(self):
1508
        return self.is_active and self.state == self.APPROVED
1509

    
1510
    @property
1511
    def is_alive(self):
1512
        return self.is_active_strict()
1513

    
1514
    @property
1515
    def is_terminated(self):
1516
        return self.is_deactivated(self.TERMINATED)
1517

    
1518
    @property
1519
    def is_suspended(self):
1520
        return False
1521

    
1522
    def violates_resource_grants(self):
1523
        return False
1524

    
1525
    def violates_members_limit(self, adding=0):
1526
        application = self.application
1527
        limit = application.limit_on_members_number
1528
        if limit is None:
1529
            return False
1530
        return (len(self.approved_members) + adding > limit)
1531

    
1532

    
1533
    ### Other
1534

    
1535
    @property
1536
    def approved_memberships(self):
1537
        query = ProjectMembership.query_approved()
1538
        return self.projectmembership_set.filter(query)
1539

    
1540
    @property
1541
    def approved_members(self):
1542
        return [m.person for m in self.approved_memberships]
1543

    
1544
    def add_member(self, user):
1545
        """
1546
        Raises:
1547
            django.exceptions.PermissionDenied
1548
            astakos.im.models.AstakosUser.DoesNotExist
1549
        """
1550
        if isinstance(user, int):
1551
            user = AstakosUser.objects.get(user=user)
1552

    
1553
        m, created = ProjectMembership.objects.get_or_create(
1554
            person=user, project=self
1555
        )
1556
        m.accept()
1557

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

    
1568
        m = ProjectMembership.objects.get(person=user, project=self)
1569
        m.remove()
1570

    
1571

    
1572
class PendingMembershipError(Exception):
1573
    pass
1574

    
1575

    
1576
class ProjectMembership(models.Model):
1577

    
1578
    person              =   models.ForeignKey(AstakosUser)
1579
    request_date        =   models.DateField(default=datetime.now())
1580
    project             =   models.ForeignKey(Project)
1581

    
1582
    REQUESTED   =   0
1583
    ACCEPTED    =   1
1584
    SUSPENDED   =   10
1585
    TERMINATED  =   100
1586
    REMOVED     =   200
1587

    
1588
    state               =   models.IntegerField(default=REQUESTED,
1589
                                                db_index=True)
1590
    is_pending          =   models.BooleanField(default=False, db_index=True)
1591
    is_active           =   models.BooleanField(default=False, db_index=True)
1592
    application         =   models.ForeignKey(
1593
                                ProjectApplication,
1594
                                null=True,
1595
                                related_name='memberships')
1596
    pending_application =   models.ForeignKey(
1597
                                ProjectApplication,
1598
                                null=True,
1599
                                related_name='pending_memebrships')
1600
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1601

    
1602
    acceptance_date     =   models.DateField(null=True, db_index=True)
1603
    leave_request_date  =   models.DateField(null=True)
1604

    
1605
    objects     =   ForUpdateManager()
1606

    
1607

    
1608
    def get_combined_state(self):
1609
        return self.state, self.is_active, self.is_pending
1610

    
1611
    @classmethod
1612
    def query_approved(cls):
1613
        return (~Q(state=cls.REQUESTED) &
1614
                ~Q(state=cls.REMOVED))
1615

    
1616
    class Meta:
1617
        unique_together = ("person", "project")
1618
        #index_together = [["project", "state"]]
1619

    
1620
    def __str__(self):
1621
        return _("<'%s' membership in '%s'>") % (
1622
                self.person.username, self.project)
1623

    
1624
    __repr__ = __str__
1625

    
1626
    def __init__(self, *args, **kwargs):
1627
        self.state = self.REQUESTED
1628
        super(ProjectMembership, self).__init__(*args, **kwargs)
1629

    
1630
    def _set_history_item(self, reason, date=None):
1631
        if isinstance(reason, basestring):
1632
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1633

    
1634
        history_item = ProjectMembershipHistory(
1635
                            serial=self.id,
1636
                            person=self.person.uuid,
1637
                            project=self.project_id,
1638
                            date=date or datetime.now(),
1639
                            reason=reason)
1640
        history_item.save()
1641
        serial = history_item.id
1642

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

    
1648
        state = self.state
1649
        if state != self.REQUESTED:
1650
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1651
            raise AssertionError(m)
1652

    
1653
        now = datetime.now()
1654
        self.acceptance_date = now
1655
        self._set_history_item(reason='ACCEPT', date=now)
1656
        if self.project.is_active_strict():
1657
            self.state = self.ACCEPTED
1658
            self.is_pending = True
1659
        else:
1660
            self.state = self.TERMINATED
1661

    
1662
        self.save()
1663

    
1664
    def remove(self):
1665
        if self.is_pending:
1666
            m = _("%s: attempt to remove while is pending") % (self,)
1667
            raise AssertionError(m)
1668

    
1669
        state = self.state
1670
        if state not in [self.ACCEPTED, self.TERMINATED]:
1671
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1672
            raise AssertionError(m)
1673

    
1674
        self._set_history_item(reason='REMOVE')
1675
        self.state = self.REMOVED
1676
        self.is_pending = True
1677
        self.save()
1678

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

    
1684
        state = self.state
1685
        if state != self.REQUESTED:
1686
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1687
            raise AssertionError(m)
1688

    
1689
        # rejected requests don't need sync,
1690
        # because they were never effected
1691
        self._set_history_item(reason='REJECT')
1692
        self.delete()
1693

    
1694
    def get_diff_quotas(self, sub_list=None, add_list=None):
1695
        if sub_list is None:
1696
            sub_list = []
1697

    
1698
        if add_list is None:
1699
            add_list = []
1700

    
1701
        sub_append = sub_list.append
1702
        add_append = add_list.append
1703
        holder = self.person.uuid
1704

    
1705
        synced_application = self.application
1706
        if synced_application is not None:
1707
            cur_grants = synced_application.projectresourcegrant_set.all()
1708
            for grant in cur_grants:
1709
                sub_append(QuotaLimits(
1710
                               holder       = holder,
1711
                               resource     = str(grant.resource),
1712
                               capacity     = grant.member_capacity,
1713
                               import_limit = grant.member_import_limit,
1714
                               export_limit = grant.member_export_limit))
1715

    
1716
        pending_application = self.pending_application
1717
        if pending_application is not None:
1718
            new_grants = pending_application.projectresourcegrant_set.all()
1719
            for new_grant in new_grants:
1720
                add_append(QuotaLimits(
1721
                               holder       = holder,
1722
                               resource     = str(new_grant.resource),
1723
                               capacity     = new_grant.member_capacity,
1724
                               import_limit = new_grant.member_import_limit,
1725
                               export_limit = new_grant.member_export_limit))
1726

    
1727
        return (sub_list, add_list)
1728

    
1729
    def set_sync(self):
1730
        if not self.is_pending:
1731
            m = _("%s: attempt to sync a non pending membership") % (self,)
1732
            raise AssertionError(m)
1733

    
1734
        state = self.state
1735
        if state == self.ACCEPTED:
1736
            pending_application = self.pending_application
1737
            if pending_application is None:
1738
                m = _("%s: attempt to sync an empty pending application") % (
1739
                    self,)
1740
                raise AssertionError(m)
1741

    
1742
            self.application = pending_application
1743
            self.is_active = True
1744

    
1745
            self.pending_application = None
1746
            self.pending_serial = None
1747

    
1748
            # project.application may have changed in the meantime,
1749
            # in which case we stay PENDING;
1750
            # we are safe to check due to select_for_update
1751
            if self.application == self.project.application:
1752
                self.is_pending = False
1753
            self.save()
1754

    
1755
        elif state == self.TERMINATED:
1756
            if self.pending_application:
1757
                m = _("%s: attempt to sync in state '%s' "
1758
                      "with a pending application") % (self, state)
1759
                raise AssertionError(m)
1760

    
1761
            self.application = None
1762
            self.pending_serial = None
1763
            self.is_pending = False
1764
            self.save()
1765

    
1766
        elif state == self.REMOVED:
1767
            self.delete()
1768

    
1769
        else:
1770
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1771
            raise AssertionError(m)
1772

    
1773
    def reset_sync(self):
1774
        if not self.is_pending:
1775
            m = _("%s: attempt to reset a non pending membership") % (self,)
1776
            raise AssertionError(m)
1777

    
1778
        state = self.state
1779
        if state in [self.ACCEPTED, self.TERMINATED, self.REMOVED]:
1780
            self.pending_application = None
1781
            self.pending_serial = None
1782
            self.save()
1783
        else:
1784
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1785
            raise AssertionError(m)
1786

    
1787
class Serial(models.Model):
1788
    serial  =   models.AutoField(primary_key=True)
1789

    
1790
def new_serial():
1791
    s = Serial.objects.create()
1792
    serial = s.serial
1793
    s.delete()
1794
    return serial
1795

    
1796
def sync_finish_serials(serials_to_ack=None):
1797
    if serials_to_ack is None:
1798
        serials_to_ack = qh_query_serials([])
1799

    
1800
    serials_to_ack = set(serials_to_ack)
1801
    sfu = ProjectMembership.objects.select_for_update()
1802
    memberships = list(sfu.filter(pending_serial__isnull=False))
1803

    
1804
    if memberships:
1805
        for membership in memberships:
1806
            serial = membership.pending_serial
1807
            if serial in serials_to_ack:
1808
                membership.set_sync()
1809
            else:
1810
                membership.reset_sync()
1811

    
1812
        transaction.commit()
1813

    
1814
    qh_ack_serials(list(serials_to_ack))
1815
    return len(memberships)
1816

    
1817
def pre_sync():
1818
    ACCEPTED = ProjectMembership.ACCEPTED
1819
    TERMINATED = ProjectMembership.TERMINATED
1820
    psfu = Project.objects.select_for_update()
1821

    
1822
    modified = psfu.modified_projects()
1823
    for project in modified:
1824
        objects = project.projectmembership_set.select_for_update()
1825

    
1826
        memberships = objects.filter(state=ACCEPTED)
1827
        for membership in memberships:
1828
            membership.is_pending = True
1829
            membership.save()
1830

    
1831
    terminating = psfu.terminating_projects()
1832
    for project in terminating:
1833
        objects = project.projectmembership_set.select_for_update()
1834

    
1835
        memberships = objects.filter(state=ACCEPTED)
1836
        for membership in memberships:
1837
            membership.is_pending = True
1838
            membership.state = TERMINATED
1839
            membership.save()
1840

    
1841
def do_sync():
1842

    
1843
    ACCEPTED = ProjectMembership.ACCEPTED
1844
    objects = ProjectMembership.objects.select_for_update()
1845

    
1846
    sub_quota, add_quota = [], []
1847

    
1848
    serial = new_serial()
1849

    
1850
    pending = objects.filter(is_pending=True)
1851
    for membership in pending:
1852

    
1853
        if membership.pending_application:
1854
            m = "%s: impossible: pending_application is not None (%s)" % (
1855
                membership, membership.pending_application)
1856
            raise AssertionError(m)
1857
        if membership.pending_serial:
1858
            m = "%s: impossible: pending_serial is not None (%s)" % (
1859
                membership, membership.pending_serial)
1860
            raise AssertionError(m)
1861

    
1862
        if membership.state == ACCEPTED:
1863
            membership.pending_application = membership.project.application
1864

    
1865
        membership.pending_serial = serial
1866
        membership.get_diff_quotas(sub_quota, add_quota)
1867
        membership.save()
1868

    
1869
    transaction.commit()
1870
    # ProjectApplication.approve() unblocks here
1871
    # and can set PENDING an already PENDING membership
1872
    # which has been scheduled to sync with the old project.application
1873
    # Need to check in ProjectMembership.set_sync()
1874

    
1875
    r = qh_add_quota(serial, sub_quota, add_quota)
1876
    if r:
1877
        m = "cannot sync serial: %d" % serial
1878
        raise RuntimeError(m)
1879

    
1880
    return serial
1881

    
1882
def post_sync():
1883
    ACCEPTED = ProjectMembership.ACCEPTED
1884
    psfu = Project.objects.select_for_update()
1885

    
1886
    modified = psfu.modified_projects()
1887
    for project in modified:
1888
        objects = project.projectmembership_set.select_for_update()
1889

    
1890
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
1891
        if not memberships:
1892
            project.is_modified = False
1893
            project.save()
1894

    
1895
    terminating = psfu.terminating_projects()
1896
    for project in terminating:
1897
        objects = project.projectmembership_set.select_for_update()
1898

    
1899
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1900
                                          Q(is_pending=True)))
1901
        if not memberships:
1902
            project.deactivate()
1903
            project.save()
1904

    
1905
    transaction.commit()
1906

    
1907
def sync_projects():
1908
    sync_finish_serials()
1909
    pre_sync()
1910
    serial = do_sync()
1911
    sync_finish_serials([serial])
1912
    post_sync()
1913

    
1914
def trigger_sync(retries=3, retry_wait=1.0):
1915
    transaction.commit()
1916

    
1917
    cursor = connection.cursor()
1918
    locked = True
1919
    try:
1920
        while 1:
1921
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1922
            r = cursor.fetchone()
1923
            if r is None:
1924
                m = "Impossible"
1925
                raise AssertionError(m)
1926
            locked = r[0]
1927
            if locked:
1928
                break
1929

    
1930
            retries -= 1
1931
            if retries <= 0:
1932
                return False
1933
            sleep(retry_wait)
1934

    
1935
        sync_projects()
1936
        return True
1937

    
1938
    finally:
1939
        if locked:
1940
            cursor.execute("SELECT pg_advisory_unlock(1)")
1941
            cursor.fetchall()
1942

    
1943

    
1944
class ProjectMembershipHistory(models.Model):
1945
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1946
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1947

    
1948
    person  =   models.CharField(max_length=255)
1949
    project =   models.BigIntegerField()
1950
    date    =   models.DateField(default=datetime.now)
1951
    reason  =   models.IntegerField()
1952
    serial  =   models.BigIntegerField()
1953

    
1954
### SIGNALS ###
1955
################
1956

    
1957
def create_astakos_user(u):
1958
    try:
1959
        AstakosUser.objects.get(user_ptr=u.pk)
1960
    except AstakosUser.DoesNotExist:
1961
        extended_user = AstakosUser(user_ptr_id=u.pk)
1962
        extended_user.__dict__.update(u.__dict__)
1963
        extended_user.save()
1964
        if not extended_user.has_auth_provider('local'):
1965
            extended_user.add_auth_provider('local')
1966
    except BaseException, e:
1967
        logger.exception(e)
1968

    
1969

    
1970
def fix_superusers(sender, **kwargs):
1971
    # Associate superusers with AstakosUser
1972
    admins = User.objects.filter(is_superuser=True)
1973
    for u in admins:
1974
        create_astakos_user(u)
1975
post_syncdb.connect(fix_superusers)
1976

    
1977

    
1978
def user_post_save(sender, instance, created, **kwargs):
1979
    if not created:
1980
        return
1981
    create_astakos_user(instance)
1982
post_save.connect(user_post_save, sender=User)
1983

    
1984
def astakosuser_post_save(sender, instance, created, **kwargs):
1985
    pass
1986

    
1987
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1988

    
1989
def resource_post_save(sender, instance, created, **kwargs):
1990
    if not created:
1991
        return
1992
    register_resources((instance,))
1993
post_save.connect(resource_post_save, sender=Resource)
1994

    
1995
def renew_token(sender, instance, **kwargs):
1996
    if not instance.auth_token:
1997
        instance.renew_token()
1998
pre_save.connect(renew_token, sender=AstakosUser)
1999
pre_save.connect(renew_token, sender=Service)
2000