Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (66.7 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
    def add_resource(self, service, resource, uplimit, update=True):
144
        """Raises ObjectDoesNotExist, IntegrityError"""
145
        resource = Resource.objects.get(service__name=service, name=resource)
146
        if update:
147
            AstakosUserQuota.objects.update_or_create(user=self,
148
                                                      resource=resource,
149
                                                      defaults={'uplimit': uplimit})
150
        else:
151
            q = self.astakosuserquota_set
152
            q.create(resource=resource, uplimit=uplimit)
153

    
154

    
155
class ResourceMetadata(models.Model):
156
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
157
    value = models.CharField(_('Value'), max_length=255)
158

    
159
_presentation_data = {}
160
def get_presentation(resource):
161
    global _presentation_data
162
    presentation = _presentation_data.get(resource, {})
163
    if not presentation:
164
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
165
        presentation = resource_presentation.get(resource, {})
166
        _presentation_data[resource] = presentation
167
    return presentation
168

    
169
class Resource(models.Model):
170
    name = models.CharField(_('Name'), max_length=255)
171
    meta = models.ManyToManyField(ResourceMetadata)
172
    service = models.ForeignKey(Service)
173
    desc = models.TextField(_('Description'), null=True)
174
    unit = models.CharField(_('Name'), null=True, max_length=255)
175
    group = models.CharField(_('Group'), null=True, max_length=255)
176

    
177
    class Meta:
178
        unique_together = ("name", "service")
179

    
180
    def __str__(self):
181
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
182

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

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

    
191
    @property
192
    def is_abbreviation(self):
193
        return get_presentation(str(self)).get('is_abbreviation', False)
194

    
195
    @property
196
    def report_desc(self):
197
        return get_presentation(str(self)).get('report_desc', '')
198

    
199
    @property
200
    def placeholder(self):
201
        return get_presentation(str(self)).get('placeholder', '')
202

    
203
    @property
204
    def verbose_name(self):
205
        return get_presentation(str(self)).get('verbose_name', '')
206

    
207

    
208
_default_quota = {}
209
def get_default_quota():
210
    global _default_quota
211
    if _default_quota:
212
        return _default_quota
213
    for s, data in SERVICES.iteritems():
214
        map(
215
            lambda d:_default_quota.update(
216
                {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
217
            ),
218
            data.get('resources', {})
219
        )
220
    return _default_quota
221

    
222

    
223
class AstakosUserManager(UserManager):
224

    
225
    def get_auth_provider_user(self, provider, **kwargs):
226
        """
227
        Retrieve AstakosUser instance associated with the specified third party
228
        id.
229
        """
230
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
231
                          kwargs.iteritems()))
232
        return self.get(auth_providers__module=provider, **kwargs)
233

    
234
    def get_by_email(self, email):
235
        return self.get(email=email)
236

    
237
    def get_by_identifier(self, email_or_username, **kwargs):
238
        try:
239
            return self.get(email__iexact=email_or_username, **kwargs)
240
        except AstakosUser.DoesNotExist:
241
            return self.get(username__iexact=email_or_username, **kwargs)
242

    
243
    def user_exists(self, email_or_username, **kwargs):
244
        qemail = Q(email__iexact=email_or_username)
245
        qusername = Q(username__iexact=email_or_username)
246
        qextra = Q(**kwargs)
247
        return self.filter((qemail | qusername) & qextra).exists()
248

    
249
    def verified_user_exists(self, email_or_username):
250
        return self.user_exists(email_or_username, email_verified=True)
251

    
252
    def verified(self):
253
        return self.filter(email_verified=True)
254

    
255
    def verified(self):
256
        return self.filter(email_verified=True)
257

    
258

    
259
class AstakosUser(User):
260
    """
261
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
262
    """
263
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
264
                                   null=True)
265

    
266
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
267
    #                    AstakosUserProvider model.
268
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
269
                                null=True)
270
    # ex. screen_name for twitter, eppn for shibboleth
271
    third_party_identifier = models.CharField(_('Third-party identifier'),
272
                                              max_length=255, null=True,
273
                                              blank=True)
274

    
275

    
276
    #for invitations
277
    user_level = DEFAULT_USER_LEVEL
278
    level = models.IntegerField(_('Inviter level'), default=user_level)
279
    invitations = models.IntegerField(
280
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
281

    
282
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
283
                                  null=True, blank=True)
284
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
285
    auth_token_expires = models.DateTimeField(
286
        _('Token expiration date'), null=True)
287

    
288
    updated = models.DateTimeField(_('Update date'))
289
    is_verified = models.BooleanField(_('Is verified?'), default=False)
290

    
291
    email_verified = models.BooleanField(_('Email verified?'), default=False)
292

    
293
    has_credits = models.BooleanField(_('Has credits?'), default=False)
294
    has_signed_terms = models.BooleanField(
295
        _('I agree with the terms'), default=False)
296
    date_signed_terms = models.DateTimeField(
297
        _('Signed terms date'), null=True, blank=True)
298

    
299
    activation_sent = models.DateTimeField(
300
        _('Activation sent data'), null=True, blank=True)
301

    
302
    policy = models.ManyToManyField(
303
        Resource, null=True, through='AstakosUserQuota')
304

    
305
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
306

    
307
    __has_signed_terms = False
308
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
309
                                           default=False, db_index=True)
310

    
311
    objects = AstakosUserManager()
312

    
313
    def __init__(self, *args, **kwargs):
314
        super(AstakosUser, self).__init__(*args, **kwargs)
315
        self.__has_signed_terms = self.has_signed_terms
316
        if not self.id:
317
            self.is_active = False
318

    
319
    @property
320
    def realname(self):
321
        return '%s %s' % (self.first_name, self.last_name)
322

    
323
    @realname.setter
324
    def realname(self, value):
325
        parts = value.split(' ')
326
        if len(parts) == 2:
327
            self.first_name = parts[0]
328
            self.last_name = parts[1]
329
        else:
330
            self.last_name = parts[0]
331

    
332
    def add_permission(self, pname):
333
        if self.has_perm(pname):
334
            return
335
        p, created = Permission.objects.get_or_create(
336
                                    codename=pname,
337
                                    name=pname.capitalize(),
338
                                    content_type=get_content_type())
339
        self.user_permissions.add(p)
340

    
341
    def remove_permission(self, pname):
342
        if self.has_perm(pname):
343
            return
344
        p = Permission.objects.get(codename=pname,
345
                                   content_type=get_content_type())
346
        self.user_permissions.remove(p)
347

    
348
    @property
349
    def invitation(self):
350
        try:
351
            return Invitation.objects.get(username=self.email)
352
        except Invitation.DoesNotExist:
353
            return None
354

    
355
    @property
356
    def quota(self):
357
        """Returns a dict with the sum of quota limits per resource"""
358
        d = defaultdict(int)
359
        default_quota = get_default_quota()
360
        d.update(default_quota)
361
        for q in self.policies:
362
            d[q.resource] += q.uplimit or inf
363
        for m in self.projectmembership_set.select_related().all():
364
            if not m.acceptance_date:
365
                continue
366
            p = m.project
367
            if not p.is_active_strict():
368
                continue
369
            grants = p.application.projectresourcegrant_set.all()
370
            for g in grants:
371
                d[str(g.resource)] += g.member_capacity or inf
372
        return d
373

    
374
    @property
375
    def policies(self):
376
        return self.astakosuserquota_set.select_related().all()
377

    
378
    @policies.setter
379
    def policies(self, policies):
380
        for p in policies:
381
            service = policies.get('service', None)
382
            resource = policies.get('resource', None)
383
            uplimit = policies.get('uplimit', 0)
384
            update = policies.get('update', True)
385
            self.add_policy(service, resource, uplimit, update)
386

    
387
    def add_policy(self, service, resource, uplimit, update=True):
388
        """Raises ObjectDoesNotExist, IntegrityError"""
389
        resource = Resource.objects.get(service__name=service, name=resource)
390
        if update:
391
            AstakosUserQuota.objects.update_or_create(user=self,
392
                                                      resource=resource,
393
                                                      defaults={'uplimit': uplimit})
394
        else:
395
            q = self.astakosuserquota_set
396
            q.create(resource=resource, uplimit=uplimit)
397

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

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

    
412
    @property
413
    def extended_groups(self):
414
        return self.membership_set.select_related().all()
415

    
416
    def save(self, update_timestamps=True, **kwargs):
417
        if update_timestamps:
418
            if not self.id:
419
                self.date_joined = datetime.now()
420
            self.updated = datetime.now()
421

    
422
        # update date_signed_terms if necessary
423
        if self.__has_signed_terms != self.has_signed_terms:
424
            self.date_signed_terms = datetime.now()
425

    
426
        self.update_uuid()
427

    
428
        if self.username != self.email.lower():
429
            # set username
430
            self.username = self.email.lower()
431

    
432
        self.validate_unique_email_isactive()
433

    
434
        super(AstakosUser, self).save(**kwargs)
435

    
436
    def renew_token(self, flush_sessions=False, current_key=None):
437
        md5 = hashlib.md5()
438
        md5.update(settings.SECRET_KEY)
439
        md5.update(self.username)
440
        md5.update(self.realname.encode('ascii', 'ignore'))
441
        md5.update(asctime())
442

    
443
        self.auth_token = b64encode(md5.digest())
444
        self.auth_token_created = datetime.now()
445
        self.auth_token_expires = self.auth_token_created + \
446
                                  timedelta(hours=AUTH_TOKEN_DURATION)
447
        if flush_sessions:
448
            self.flush_sessions(current_key)
449
        msg = 'Token renewed for %s' % self.email
450
        logger.log(LOGGING_LEVEL, msg)
451

    
452
    def flush_sessions(self, current_key=None):
453
        q = self.sessions
454
        if current_key:
455
            q = q.exclude(session_key=current_key)
456

    
457
        keys = q.values_list('session_key', flat=True)
458
        if keys:
459
            msg = 'Flushing sessions: %s' % ','.join(keys)
460
            logger.log(LOGGING_LEVEL, msg, [])
461
        engine = import_module(settings.SESSION_ENGINE)
462
        for k in keys:
463
            s = engine.SessionStore(k)
464
            s.flush()
465

    
466
    def __unicode__(self):
467
        return '%s (%s)' % (self.realname, self.email)
468

    
469
    def conflicting_email(self):
470
        q = AstakosUser.objects.exclude(username=self.username)
471
        q = q.filter(email__iexact=self.email)
472
        if q.count() != 0:
473
            return True
474
        return False
475

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

    
489
    def email_change_is_pending(self):
490
        return self.emailchanges.count() > 0
491

    
492
    def email_change_is_pending(self):
493
        return self.emailchanges.count() > 0
494

    
495
    @property
496
    def signed_terms(self):
497
        term = get_latest_terms()
498
        if not term:
499
            return True
500
        if not self.has_signed_terms:
501
            return False
502
        if not self.date_signed_terms:
503
            return False
504
        if self.date_signed_terms < term.date:
505
            self.has_signed_terms = False
506
            self.date_signed_terms = None
507
            self.save()
508
            return False
509
        return True
510

    
511
    def set_invitations_level(self):
512
        """
513
        Update user invitation level
514
        """
515
        level = self.invitation.inviter.level + 1
516
        self.level = level
517
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
518

    
519
    def can_login_with_auth_provider(self, provider):
520
        if not self.has_auth_provider(provider):
521
            return False
522
        else:
523
            return auth_providers.get_provider(provider).is_available_for_login()
524

    
525
    def can_add_auth_provider(self, provider, **kwargs):
526
        provider_settings = auth_providers.get_provider(provider)
527

    
528
        if not provider_settings.is_available_for_add():
529
            return False
530

    
531
        if self.has_auth_provider(provider) and \
532
           provider_settings.one_per_user:
533
            return False
534

    
535
        if 'provider_info' in kwargs:
536
            kwargs.pop('provider_info')
537

    
538
        if 'identifier' in kwargs:
539
            try:
540
                # provider with specified params already exist
541
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
542
                                                                   **kwargs)
543
            except AstakosUser.DoesNotExist:
544
                return True
545
            else:
546
                return False
547

    
548
        return True
549

    
550
    def can_remove_auth_provider(self, module):
551
        provider = auth_providers.get_provider(module)
552
        existing = self.get_active_auth_providers()
553
        existing_for_provider = self.get_active_auth_providers(module=module)
554

    
555
        if len(existing) <= 1:
556
            return False
557

    
558
        if len(existing_for_provider) == 1 and provider.is_required():
559
            return False
560

    
561
        return True
562

    
563
    def can_change_password(self):
564
        return self.has_auth_provider('local', auth_backend='astakos')
565

    
566
    def has_required_auth_providers(self):
567
        required = auth_providers.REQUIRED_PROVIDERS
568
        for provider in required:
569
            if not self.has_auth_provider(provider):
570
                return False
571
        return True
572

    
573
    def has_auth_provider(self, provider, **kwargs):
574
        return bool(self.auth_providers.filter(module=provider,
575
                                               **kwargs).count())
576

    
577
    def add_auth_provider(self, provider, **kwargs):
578
        info_data = ''
579
        if 'provider_info' in kwargs:
580
            info_data = kwargs.pop('provider_info')
581
            if isinstance(info_data, dict):
582
                info_data = json.dumps(info_data)
583

    
584
        if self.can_add_auth_provider(provider, **kwargs):
585
            self.auth_providers.create(module=provider, active=True,
586
                                       info_data=info_data,
587
                                       **kwargs)
588
        else:
589
            raise Exception('Cannot add provider')
590

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

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

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

    
607
        pending.delete()
608
        return provider
609

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

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

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

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

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

    
633
    def get_auth_providers(self):
634
        return self.auth_providers.all()
635

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

    
645
        return providers
646

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

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

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

    
683
        return mark_safe(message + u' '+ msg_extra)
684

    
685
    def owns_project(self, project):
686
        return project.owner == self
687

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

    
691
    def is_project_accepted_member(self, project):
692
        return project.user_status(self) == 2
693

    
694

    
695
class AstakosUserAuthProviderManager(models.Manager):
696

    
697
    def active(self, **filters):
698
        return self.filter(active=True, **filters)
699

    
700

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

    
719
    objects = AstakosUserAuthProviderManager()
720

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

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

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

    
737

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

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

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

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

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

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

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

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

    
786

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

    
814
    update_or_create = _update_or_create
815

    
816

    
817
class AstakosUserQuota(models.Model):
818
    objects = ExtendedManager()
819
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
820
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
821
    resource = models.ForeignKey(Resource)
822
    user = models.ForeignKey(AstakosUser)
823

    
824
    class Meta:
825
        unique_together = ("resource", "user")
826

    
827

    
828
class ApprovalTerms(models.Model):
829
    """
830
    Model for approval terms
831
    """
832

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

    
837

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

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

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

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

    
864

    
865
class EmailChangeManager(models.Manager):
866

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

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

876
        If the key is not valid or has expired, return ``None``.
877

878
        If the key is valid but the ``User`` is already active,
879
        return ``None``.
880

881
        After successful email change the activation record is deleted.
882

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

    
911

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

    
922
    objects = EmailChangeManager()
923

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

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

    
932

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

    
940

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

    
950

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

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

    
974
    class Meta:
975
        unique_together = ("provider", "third_party_identifier")
976

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

    
986
        return user
987

    
988
    @property
989
    def realname(self):
990
        return '%s %s' %(self.first_name, self.last_name)
991

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

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

    
1012
    def generate_token(self):
1013
        self.password = self.third_party_identifier
1014
        self.last_login = datetime.now()
1015
        self.token = default_token_generator.make_token(self)
1016

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

    
1021

    
1022
### PROJECTS ###
1023
################
1024

    
1025
def synced_model_metaclass(class_name, class_parents, class_attributes):
1026

    
1027
    new_attributes = {}
1028
    sync_attributes = {}
1029

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

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

    
1042
    prefix = sync_attributes.pop('prefix')
1043
    class_name = sync_attributes.pop('classname', prefix + '_model')
1044

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

    
1053
        new_attributes[newname] = value
1054

    
1055
    newclass = type(class_name, class_parents, new_attributes)
1056
    return newclass
1057

    
1058

    
1059
def make_synced(prefix='sync', name='SyncedState'):
1060

    
1061
    the_name = name
1062
    the_prefix = prefix
1063

    
1064
    class SyncedState(models.Model):
1065

    
1066
        sync_classname      = the_name
1067
        sync_prefix         = the_prefix
1068
        __metaclass__       = synced_model_metaclass
1069

    
1070
        sync_new_state      = models.BigIntegerField(null=True)
1071
        sync_synced_state   = models.BigIntegerField(null=True)
1072
        STATUS_SYNCED       = 0
1073
        STATUS_PENDING      = 1
1074
        sync_status         = models.IntegerField(db_index=True)
1075

    
1076
        class Meta:
1077
            abstract = True
1078

    
1079
        class NotSynced(Exception):
1080
            pass
1081

    
1082
        def sync_init_state(self, state):
1083
            self.sync_synced_state = state
1084
            self.sync_new_state = state
1085
            self.sync_status = self.STATUS_SYNCED
1086

    
1087
        def sync_get_status(self):
1088
            return self.sync_status
1089

    
1090
        def sync_set_status(self):
1091
            if self.sync_new_state != self.sync_synced_state:
1092
                self.sync_status = self.STATUS_PENDING
1093
            else:
1094
                self.sync_status = self.STATUS_SYNCED
1095

    
1096
        def sync_set_synced(self):
1097
            self.sync_synced_state = self.sync_new_state
1098
            self.sync_status = self.STATUS_SYNCED
1099

    
1100
        def sync_get_synced_state(self):
1101
            return self.sync_synced_state
1102

    
1103
        def sync_set_new_state(self, new_state):
1104
            self.sync_new_state = new_state
1105
            self.sync_set_status()
1106

    
1107
        def sync_get_new_state(self):
1108
            return self.sync_new_state
1109

    
1110
        def sync_set_synced_state(self, synced_state):
1111
            self.sync_synced_state = synced_state
1112
            self.sync_set_status()
1113

    
1114
        def sync_get_pending_objects(self):
1115
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1116
            return self.objects.filter(**kw)
1117

    
1118
        def sync_get_synced_objects(self):
1119
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1120
            return self.objects.filter(**kw)
1121

    
1122
        def sync_verify_get_synced_state(self):
1123
            status = self.sync_get_status()
1124
            state = self.sync_get_synced_state()
1125
            verified = (status == self.STATUS_SYNCED)
1126
            return state, verified
1127

    
1128
        def sync_is_synced(self):
1129
            state, verified = self.sync_verify_get_synced_state()
1130
            return verified
1131

    
1132
    return SyncedState
1133

    
1134
SyncedState = make_synced(prefix='sync', name='SyncedState')
1135

    
1136

    
1137
class ProjectApplicationManager(ForUpdateManager):
1138

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

    
1150
    def search_by_name(self, *search_strings):
1151
        q = Q()
1152
        for s in search_strings:
1153
            q = q | Q(name__icontains=s)
1154
        return self.filter(q)
1155

    
1156

    
1157
PROJECT_STATE_DISPLAY = {
1158
    'Pending': _('Pending review'),
1159
    'Approved': _('Active'),
1160
    'Replaced': _('Replaced'),
1161
    'Unknown': _('Unknown')
1162
}
1163

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

    
1174
class Chain(models.Model):
1175
    chain  =   models.AutoField(primary_key=True)
1176

    
1177
def new_chain():
1178
    c = Chain.objects.create()
1179
    chain = c.chain
1180
    c.delete()
1181
    return chain
1182

    
1183

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

    
1191
    state                   =   models.CharField(max_length=80,
1192
                                                default=PENDING)
1193

    
1194
    owner                   =   models.ForeignKey(
1195
                                    AstakosUser,
1196
                                    related_name='projects_owned',
1197
                                    db_index=True)
1198

    
1199
    chain                   =   models.IntegerField(db_index=True)
1200
    precursor_application   =   models.OneToOneField('ProjectApplication',
1201
                                                     null=True,
1202
                                                     blank=True,
1203
                                                     db_index=True)
1204

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

    
1221

    
1222
    objects                 =   ProjectApplicationManager()
1223

    
1224
    def __unicode__(self):
1225
        return "%s applied by %s" % (self.name, self.applicant)
1226

    
1227
    def state_display(self):
1228
        return PROJECT_STATE_DISPLAY.get(self.state, _('Unknown'))
1229

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

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

    
1254
        return status
1255

    
1256
    def user_status_display(self, user):
1257
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1258

    
1259
    def members_count(self):
1260
        return self.project.approved_memberships.count()
1261

    
1262
    @property
1263
    def grants(self):
1264
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1265

    
1266
    @property
1267
    def resource_policies(self):
1268
        return self.projectresourcegrant_set.all()
1269

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

    
1278
    @property
1279
    def follower(self):
1280
        try:
1281
            return ProjectApplication.objects.get(precursor_application=self)
1282
        except ProjectApplication.DoesNotExist:
1283
            return
1284

    
1285
    def followers(self):
1286
        current = self
1287
        try:
1288
            while current.projectapplication:
1289
                yield current.follower
1290
                current = current.follower
1291
        except:
1292
            pass
1293

    
1294
    def last_follower(self):
1295
        try:
1296
            return list(self.followers())[-1]
1297
        except IndexError:
1298
            return None
1299

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

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

1313
        Raises:
1314
            PermissionDenied
1315
        """
1316

    
1317
        if not transaction.is_managed():
1318
            raise AssertionError("NOPE")
1319

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

    
1326
        now = datetime.now()
1327
        project = self._get_project_for_update()
1328

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

    
1341
        new_project = False
1342
        if project is None:
1343
            new_project = True
1344
            project = Project(id=self.chain, creation_date=now)
1345

    
1346
        project.name = new_project_name
1347
        project.application = self
1348
        project.last_approval_date = now
1349
        project.is_modified = True
1350
        project.save()
1351

    
1352
        if new_project:
1353
            project.add_member(self.owner)
1354

    
1355
        precursor = self.precursor_application
1356
        while precursor:
1357
            precursor.state = self.REPLACED
1358
            precursor.save()
1359
            precursor = precursor.precursor_application
1360

    
1361
        self.state = self.APPROVED
1362
        self.save()
1363

    
1364
def submit_application(**kw):
1365

    
1366
    resource_policies = kw.pop('resource_policies', None)
1367
    application = ProjectApplication(**kw)
1368

    
1369
    precursor = kw['precursor_application']
1370

    
1371
    if precursor is not None:
1372
        precursor.state = ProjectApplication.REPLACED
1373
        precursor.save()
1374
        application.chain = precursor.chain
1375
    else:
1376
        application.chain = new_chain()
1377

    
1378
    application.save()
1379
    application.resource_policies = resource_policies
1380
    return application
1381

    
1382
class ProjectResourceGrant(models.Model):
1383

    
1384
    resource                =   models.ForeignKey(Resource)
1385
    project_application     =   models.ForeignKey(ProjectApplication,
1386
                                                  null=True)
1387
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1388
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1389
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1390
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1391
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1392
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1393

    
1394
    objects = ExtendedManager()
1395

    
1396
    class Meta:
1397
        unique_together = ("resource", "project_application")
1398

    
1399

    
1400
class ProjectManager(ForUpdateManager):
1401

    
1402
    def _q_terminated(self):
1403
        return Q(state=Project.TERMINATED)
1404

    
1405
    def terminated_projects(self):
1406
        q = self._q_terminated()
1407
        return self.filter(q)
1408

    
1409
    def not_terminated_projects(self):
1410
        q = ~self._q_terminated()
1411
        return self.filter(q)
1412

    
1413
    def terminating_projects(self):
1414
        q = self._q_terminated() & Q(is_active=True)
1415
        return self.filter(q)
1416

    
1417
    def modified_projects(self):
1418
        return self.filter(is_modified=True)
1419

    
1420

    
1421
class Project(models.Model):
1422

    
1423
    application                 =   models.OneToOneField(
1424
                                            ProjectApplication,
1425
                                            related_name='project')
1426
    last_approval_date          =   models.DateTimeField(null=True)
1427

    
1428
    members                     =   models.ManyToManyField(
1429
                                            AstakosUser,
1430
                                            through='ProjectMembership')
1431

    
1432
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1433
    deactivation_date           =   models.DateTimeField(null=True)
1434

    
1435
    creation_date               =   models.DateTimeField()
1436
    name                        =   models.CharField(
1437
                                            max_length=80,
1438
                                            db_index=True,
1439
                                            unique=True)
1440

    
1441
    APPROVED    = 1
1442
    SUSPENDED   = 10
1443
    TERMINATED  = 100
1444

    
1445
    is_modified                 =   models.BooleanField(default=False,
1446
                                                        db_index=True)
1447
    is_active                   =   models.BooleanField(default=True,
1448
                                                        db_index=True)
1449
    state                       =   models.IntegerField(default=APPROVED,
1450
                                                        db_index=True)
1451

    
1452
    objects     =   ProjectManager()
1453

    
1454
    def __str__(self):
1455
        return _("<project %s '%s'>") % (self.id, self.application.name)
1456

    
1457
    __repr__ = __str__
1458

    
1459
    def is_deactivated(self, reason=None):
1460
        if reason is not None:
1461
            return self.state == reason
1462

    
1463
        return self.state != self.APPROVED
1464

    
1465
    def is_deactivating(self, reason=None):
1466
        if not self.is_active:
1467
            return False
1468

    
1469
        return self.is_deactivated(reason)
1470

    
1471
    def is_deactivated_strict(self, reason=None):
1472
        if self.is_active:
1473
            return False
1474

    
1475
        return self.is_deactivated(reason)
1476

    
1477
    ### Deactivation calls
1478

    
1479
    def deactivate(self):
1480
        self.deactivation_date = datetime.now()
1481
        self.is_active = False
1482

    
1483
    def terminate(self):
1484
        self.deactivation_reason = 'TERMINATED'
1485
        self.state = self.TERMINATED
1486
        self.save()
1487

    
1488

    
1489
    ### Logical checks
1490

    
1491
    def is_inconsistent(self):
1492
        now = datetime.now()
1493
        dates = [self.creation_date,
1494
                 self.last_approval_date,
1495
                 self.deactivation_date]
1496
        return any([date > now for date in dates])
1497

    
1498
    def is_active_strict(self):
1499
        return self.is_active and self.state == self.APPROVED
1500

    
1501
    @property
1502
    def is_alive(self):
1503
        return self.is_active_strict()
1504

    
1505
    @property
1506
    def is_terminated(self):
1507
        return self.is_deactivated(self.TERMINATED)
1508

    
1509
    @property
1510
    def is_suspended(self):
1511
        return False
1512

    
1513
    def violates_resource_grants(self):
1514
        return False
1515

    
1516
    def violates_members_limit(self, adding=0):
1517
        application = self.application
1518
        return (len(self.approved_members) + adding >
1519
                application.limit_on_members_number)
1520

    
1521

    
1522
    ### Other
1523

    
1524
    @property
1525
    def approved_memberships(self):
1526
        query = ProjectMembership.query_approved()
1527
        return self.projectmembership_set.filter(query)
1528

    
1529
    @property
1530
    def approved_members(self):
1531
        return [m.person for m in self.approved_memberships]
1532

    
1533
    def add_member(self, user):
1534
        """
1535
        Raises:
1536
            django.exceptions.PermissionDenied
1537
            astakos.im.models.AstakosUser.DoesNotExist
1538
        """
1539
        if isinstance(user, int):
1540
            user = AstakosUser.objects.get(user=user)
1541

    
1542
        m, created = ProjectMembership.objects.get_or_create(
1543
            person=user, project=self
1544
        )
1545
        m.accept()
1546

    
1547
    def remove_member(self, user):
1548
        """
1549
        Raises:
1550
            django.exceptions.PermissionDenied
1551
            astakos.im.models.AstakosUser.DoesNotExist
1552
            astakos.im.models.ProjectMembership.DoesNotExist
1553
        """
1554
        if isinstance(user, int):
1555
            user = AstakosUser.objects.get(user=user)
1556

    
1557
        m = ProjectMembership.objects.get(person=user, project=self)
1558
        m.remove()
1559

    
1560

    
1561
class PendingMembershipError(Exception):
1562
    pass
1563

    
1564

    
1565
class ProjectMembership(models.Model):
1566

    
1567
    person              =   models.ForeignKey(AstakosUser)
1568
    request_date        =   models.DateField(default=datetime.now())
1569
    project             =   models.ForeignKey(Project)
1570

    
1571
    REQUESTED   =   0
1572
    ACCEPTED    =   1
1573
    SUSPENDED   =   10
1574
    TERMINATED  =   100
1575
    REMOVED     =   200
1576

    
1577
    state               =   models.IntegerField(default=REQUESTED,
1578
                                                db_index=True)
1579
    is_pending          =   models.BooleanField(default=False, db_index=True)
1580
    is_active           =   models.BooleanField(default=False, db_index=True)
1581
    application         =   models.ForeignKey(
1582
                                ProjectApplication,
1583
                                null=True,
1584
                                related_name='memberships')
1585
    pending_application =   models.ForeignKey(
1586
                                ProjectApplication,
1587
                                null=True,
1588
                                related_name='pending_memebrships')
1589
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1590

    
1591
    acceptance_date     =   models.DateField(null=True, db_index=True)
1592
    leave_request_date  =   models.DateField(null=True)
1593

    
1594
    objects     =   ForUpdateManager()
1595

    
1596

    
1597
    def get_combined_state(self):
1598
        return self.state, self.is_active, self.is_pending
1599

    
1600
    @classmethod
1601
    def query_approved(cls):
1602
        return (~Q(state=cls.REQUESTED) &
1603
                ~Q(state=cls.REMOVED))
1604

    
1605
    class Meta:
1606
        unique_together = ("person", "project")
1607
        #index_together = [["project", "state"]]
1608

    
1609
    def __str__(self):
1610
        return _("<'%s' membership in '%s'>") % (
1611
                self.person.username, self.project)
1612

    
1613
    __repr__ = __str__
1614

    
1615
    def __init__(self, *args, **kwargs):
1616
        self.state = self.REQUESTED
1617
        super(ProjectMembership, self).__init__(*args, **kwargs)
1618

    
1619
    def _set_history_item(self, reason, date=None):
1620
        if isinstance(reason, basestring):
1621
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1622

    
1623
        history_item = ProjectMembershipHistory(
1624
                            serial=self.id,
1625
                            person=self.person.uuid,
1626
                            project=self.project_id,
1627
                            date=date or datetime.now(),
1628
                            reason=reason)
1629
        history_item.save()
1630
        serial = history_item.id
1631

    
1632
    def accept(self):
1633
        if self.is_pending:
1634
            m = _("%s: attempt to accept while is pending") % (self,)
1635
            raise AssertionError(m)
1636

    
1637
        state = self.state
1638
        if state != self.REQUESTED:
1639
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1640
            raise AssertionError(m)
1641

    
1642
        now = datetime.now()
1643
        self.acceptance_date = now
1644
        self._set_history_item(reason='ACCEPT', date=now)
1645
        if self.project.is_active_strict():
1646
            self.state = self.ACCEPTED
1647
            self.is_pending = True
1648
        else:
1649
            self.state = self.TERMINATED
1650

    
1651
        self.save()
1652

    
1653
    def remove(self):
1654
        if self.is_pending:
1655
            m = _("%s: attempt to remove while is pending") % (self,)
1656
            raise AssertionError(m)
1657

    
1658
        state = self.state
1659
        if state not in [self.ACCEPTED, self.TERMINATED]:
1660
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1661
            raise AssertionError(m)
1662

    
1663
        self._set_history_item(reason='REMOVE')
1664
        self.state = self.REMOVED
1665
        self.is_pending = True
1666
        self.save()
1667

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

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

    
1678
        # rejected requests don't need sync,
1679
        # because they were never effected
1680
        self._set_history_item(reason='REJECT')
1681
        self.delete()
1682

    
1683
    def get_diff_quotas(self, sub_list=None, add_list=None):
1684
        if sub_list is None:
1685
            sub_list = []
1686

    
1687
        if add_list is None:
1688
            add_list = []
1689

    
1690
        sub_append = sub_list.append
1691
        add_append = add_list.append
1692
        holder = self.person.uuid
1693

    
1694
        synced_application = self.application
1695
        if synced_application is not None:
1696
            cur_grants = synced_application.projectresourcegrant_set.all()
1697
            for grant in cur_grants:
1698
                sub_append(QuotaLimits(
1699
                               holder       = holder,
1700
                               resource     = str(grant.resource),
1701
                               capacity     = grant.member_capacity,
1702
                               import_limit = grant.member_import_limit,
1703
                               export_limit = grant.member_export_limit))
1704

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

    
1716
        return (sub_list, add_list)
1717

    
1718
    def set_sync(self):
1719
        if not self.is_pending:
1720
            m = _("%s: attempt to sync a non pending membership") % (self,)
1721
            raise AssertionError(m)
1722

    
1723
        state = self.state
1724
        if state == self.ACCEPTED:
1725
            pending_application = self.pending_application
1726
            if pending_application is None:
1727
                m = _("%s: attempt to sync an empty pending application") % (
1728
                    self,)
1729
                raise AssertionError(m)
1730

    
1731
            self.application = pending_application
1732
            self.is_active = True
1733

    
1734
            self.pending_application = None
1735
            self.pending_serial = None
1736

    
1737
            # project.application may have changed in the meantime,
1738
            # in which case we stay PENDING;
1739
            # we are safe to check due to select_for_update
1740
            if self.application == self.project.application:
1741
                self.is_pending = False
1742
            self.save()
1743

    
1744
        elif state == self.TERMINATED:
1745
            if self.pending_application:
1746
                m = _("%s: attempt to sync in state '%s' "
1747
                      "with a pending application") % (self, state)
1748
                raise AssertionError(m)
1749

    
1750
            self.application = None
1751
            self.pending_serial = None
1752
            self.is_pending = False
1753
            self.save()
1754

    
1755
        elif state == self.REMOVED:
1756
            self.delete()
1757

    
1758
        else:
1759
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1760
            raise AssertionError(m)
1761

    
1762
    def reset_sync(self):
1763
        if not self.is_pending:
1764
            m = _("%s: attempt to reset a non pending membership") % (self,)
1765
            raise AssertionError(m)
1766

    
1767
        state = self.state
1768
        if state in [self.ACCEPTED, self.TERMINATED, self.REMOVED]:
1769
            self.pending_application = None
1770
            self.pending_serial = None
1771
            self.save()
1772
        else:
1773
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1774
            raise AssertionError(m)
1775

    
1776
class Serial(models.Model):
1777
    serial  =   models.AutoField(primary_key=True)
1778

    
1779
def new_serial():
1780
    s = Serial.objects.create()
1781
    serial = s.serial
1782
    s.delete()
1783
    return serial
1784

    
1785
def sync_finish_serials(serials_to_ack=None):
1786
    if serials_to_ack is None:
1787
        serials_to_ack = qh_query_serials([])
1788

    
1789
    serials_to_ack = set(serials_to_ack)
1790
    sfu = ProjectMembership.objects.select_for_update()
1791
    memberships = list(sfu.filter(pending_serial__isnull=False))
1792

    
1793
    if memberships:
1794
        for membership in memberships:
1795
            serial = membership.pending_serial
1796
            if serial in serials_to_ack:
1797
                membership.set_sync()
1798
            else:
1799
                membership.reset_sync()
1800

    
1801
        transaction.commit()
1802

    
1803
    qh_ack_serials(list(serials_to_ack))
1804
    return len(memberships)
1805

    
1806
def pre_sync():
1807
    ACCEPTED = ProjectMembership.ACCEPTED
1808
    TERMINATED = ProjectMembership.TERMINATED
1809
    psfu = Project.objects.select_for_update()
1810

    
1811
    modified = psfu.modified_projects()
1812
    for project in modified:
1813
        objects = project.projectmembership_set.select_for_update()
1814

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

    
1820
    terminating = psfu.terminating_projects()
1821
    for project in terminating:
1822
        objects = project.projectmembership_set.select_for_update()
1823

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

    
1830
def do_sync():
1831

    
1832
    ACCEPTED = ProjectMembership.ACCEPTED
1833
    objects = ProjectMembership.objects.select_for_update()
1834

    
1835
    sub_quota, add_quota = [], []
1836

    
1837
    serial = new_serial()
1838

    
1839
    pending = objects.filter(is_pending=True)
1840
    for membership in pending:
1841

    
1842
        if membership.pending_application:
1843
            m = "%s: impossible: pending_application is not None (%s)" % (
1844
                membership, membership.pending_application)
1845
            raise AssertionError(m)
1846
        if membership.pending_serial:
1847
            m = "%s: impossible: pending_serial is not None (%s)" % (
1848
                membership, membership.pending_serial)
1849
            raise AssertionError(m)
1850

    
1851
        if membership.state == ACCEPTED:
1852
            membership.pending_application = membership.project.application
1853

    
1854
        membership.pending_serial = serial
1855
        membership.get_diff_quotas(sub_quota, add_quota)
1856
        membership.save()
1857

    
1858
    transaction.commit()
1859
    # ProjectApplication.approve() unblocks here
1860
    # and can set PENDING an already PENDING membership
1861
    # which has been scheduled to sync with the old project.application
1862
    # Need to check in ProjectMembership.set_sync()
1863

    
1864
    r = qh_add_quota(serial, sub_quota, add_quota)
1865
    if r:
1866
        m = "cannot sync serial: %d" % serial
1867
        raise RuntimeError(m)
1868

    
1869
    return serial
1870

    
1871
def post_sync():
1872
    ACCEPTED = ProjectMembership.ACCEPTED
1873
    psfu = Project.objects.select_for_update()
1874

    
1875
    modified = psfu.modified_projects()
1876
    for project in modified:
1877
        objects = project.projectmembership_set.select_for_update()
1878

    
1879
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
1880
        if not memberships:
1881
            project.is_modified = False
1882
            project.save()
1883

    
1884
    terminating = psfu.terminating_projects()
1885
    for project in terminating:
1886
        objects = project.projectmembership_set.select_for_update()
1887

    
1888
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1889
                                          Q(is_pending=True)))
1890
        if not memberships:
1891
            project.deactivate()
1892
            project.save()
1893

    
1894
    transaction.commit()
1895

    
1896
def sync_projects():
1897
    sync_finish_serials()
1898
    pre_sync()
1899
    serial = do_sync()
1900
    sync_finish_serials([serial])
1901
    post_sync()
1902

    
1903
def trigger_sync(retries=3, retry_wait=1.0):
1904
    transaction.commit()
1905

    
1906
    cursor = connection.cursor()
1907
    locked = True
1908
    try:
1909
        while 1:
1910
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1911
            r = cursor.fetchone()
1912
            if r is None:
1913
                m = "Impossible"
1914
                raise AssertionError(m)
1915
            locked = r[0]
1916
            if locked:
1917
                break
1918

    
1919
            retries -= 1
1920
            if retries <= 0:
1921
                return False
1922
            sleep(retry_wait)
1923

    
1924
        sync_projects()
1925
        return True
1926

    
1927
    finally:
1928
        if locked:
1929
            cursor.execute("SELECT pg_advisory_unlock(1)")
1930
            cursor.fetchall()
1931

    
1932

    
1933
class ProjectMembershipHistory(models.Model):
1934
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1935
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1936

    
1937
    person  =   models.CharField(max_length=255)
1938
    project =   models.BigIntegerField()
1939
    date    =   models.DateField(default=datetime.now)
1940
    reason  =   models.IntegerField()
1941
    serial  =   models.BigIntegerField()
1942

    
1943
### SIGNALS ###
1944
################
1945

    
1946
def create_astakos_user(u):
1947
    try:
1948
        AstakosUser.objects.get(user_ptr=u.pk)
1949
    except AstakosUser.DoesNotExist:
1950
        extended_user = AstakosUser(user_ptr_id=u.pk)
1951
        extended_user.__dict__.update(u.__dict__)
1952
        extended_user.save()
1953
        if not extended_user.has_auth_provider('local'):
1954
            extended_user.add_auth_provider('local')
1955
    except BaseException, e:
1956
        logger.exception(e)
1957

    
1958

    
1959
def fix_superusers(sender, **kwargs):
1960
    # Associate superusers with AstakosUser
1961
    admins = User.objects.filter(is_superuser=True)
1962
    for u in admins:
1963
        create_astakos_user(u)
1964
post_syncdb.connect(fix_superusers)
1965

    
1966

    
1967
def user_post_save(sender, instance, created, **kwargs):
1968
    if not created:
1969
        return
1970
    create_astakos_user(instance)
1971
post_save.connect(user_post_save, sender=User)
1972

    
1973
def astakosuser_post_save(sender, instance, created, **kwargs):
1974
    pass
1975

    
1976
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1977

    
1978
def resource_post_save(sender, instance, created, **kwargs):
1979
    if not created:
1980
        return
1981
    register_resources((instance,))
1982
post_save.connect(resource_post_save, sender=Resource)
1983

    
1984
def renew_token(sender, instance, **kwargs):
1985
    if not instance.auth_token:
1986
        instance.renew_token()
1987
pre_save.connect(renew_token, sender=AstakosUser)
1988
pre_save.connect(renew_token, sender=Service)
1989