Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 61a1b2d2

History | View | Annotate | Download (66.1 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import hashlib
35
import uuid
36
import logging
37
import json
38

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
70
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA)
71
from astakos.im import settings as astakos_settings
72
from astakos.im.endpoints.qh import (
73
    register_users, 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():
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 ProjectApplication(models.Model):
1175
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1176
    applicant               =   models.ForeignKey(
1177
                                    AstakosUser,
1178
                                    related_name='projects_applied',
1179
                                    db_index=True)
1180

    
1181
    state                   =   models.CharField(max_length=80,
1182
                                                default=PENDING)
1183

    
1184
    owner                   =   models.ForeignKey(
1185
                                    AstakosUser,
1186
                                    related_name='projects_owned',
1187
                                    db_index=True)
1188

    
1189
    precursor_application   =   models.OneToOneField('ProjectApplication',
1190
                                                     null=True,
1191
                                                     blank=True,
1192
                                                     db_index=True)
1193

    
1194
    name                    =   models.CharField(max_length=80)
1195
    homepage                =   models.URLField(max_length=255, null=True)
1196
    description             =   models.TextField(null=True, blank=True)
1197
    start_date              =   models.DateTimeField(null=True, blank=True)
1198
    end_date                =   models.DateTimeField()
1199
    member_join_policy      =   models.IntegerField()
1200
    member_leave_policy     =   models.IntegerField()
1201
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1202
    resource_grants         =   models.ManyToManyField(
1203
                                    Resource,
1204
                                    null=True,
1205
                                    blank=True,
1206
                                    through='ProjectResourceGrant')
1207
    comments                =   models.TextField(null=True, blank=True)
1208
    issue_date              =   models.DateTimeField(default=datetime.now)
1209

    
1210

    
1211
    objects                 =   ProjectApplicationManager()
1212

    
1213
    def __unicode__(self):
1214
        return "%s applied by %s" % (self.name, self.applicant)
1215

    
1216
    def state_display(self):
1217
        return PROJECT_STATE_DISPLAY.get(self.state, _('Unknown'))
1218

    
1219
    def add_resource_policy(self, service, resource, uplimit):
1220
        """Raises ObjectDoesNotExist, IntegrityError"""
1221
        q = self.projectresourcegrant_set
1222
        resource = Resource.objects.get(service__name=service, name=resource)
1223
        q.create(resource=resource, member_capacity=uplimit)
1224

    
1225
    def user_status(self, user):
1226
        """
1227
        100 OWNER
1228
        0   REQUESTED
1229
        1   PENDING
1230
        2   ACCEPTED
1231
        3   REMOVING
1232
        4   REMOVED
1233
       -1   User has no association with the project
1234
        """
1235
        try:
1236
            membership = self.project.projectmembership_set.get(person=user)
1237
            status = membership.state
1238
        except Project.DoesNotExist:
1239
            status = -1
1240
        except ProjectMembership.DoesNotExist:
1241
            status = -1
1242

    
1243
        return status
1244

    
1245
    def user_status_display(self, user):
1246
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1247

    
1248
    def members_count(self):
1249
        return self.project.approved_memberships.count()
1250

    
1251
    @property
1252
    def grants(self):
1253
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1254

    
1255
    @property
1256
    def resource_policies(self):
1257
        return self.projectresourcegrant_set.all()
1258

    
1259
    @resource_policies.setter
1260
    def resource_policies(self, policies):
1261
        for p in policies:
1262
            service = p.get('service', None)
1263
            resource = p.get('resource', None)
1264
            uplimit = p.get('uplimit', 0)
1265
            self.add_resource_policy(service, resource, uplimit)
1266

    
1267
    @property
1268
    def follower(self):
1269
        try:
1270
            return ProjectApplication.objects.get(precursor_application=self)
1271
        except ProjectApplication.DoesNotExist:
1272
            return
1273

    
1274
    def followers(self):
1275
        current = self
1276
        try:
1277
            while current.projectapplication:
1278
                yield current.follower
1279
                current = current.follower
1280
        except:
1281
            pass
1282

    
1283
    def last_follower(self):
1284
        try:
1285
            return list(self.followers())[-1]
1286
        except IndexError:
1287
            return None
1288

    
1289
    def _get_project(self):
1290
        precursor = self
1291
        while precursor:
1292
            try:
1293
                objects = Project.objects.select_for_update()
1294
                project = objects.get(application=precursor)
1295
                return project
1296
            except Project.DoesNotExist:
1297
                pass
1298
            precursor = precursor.precursor_application
1299

    
1300
        return None
1301

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

1307
        Raises:
1308
            PermissionDenied
1309
        """
1310

    
1311
        if not transaction.is_managed():
1312
            raise AssertionError("NOPE")
1313

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

    
1320
        now = datetime.now()
1321
        project = self._get_project()
1322

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

    
1335
        new_project = False
1336
        if project is None:
1337
            new_project = True
1338
            project = Project(creation_date=now)
1339

    
1340
        project.name = new_project_name
1341
        project.application = self
1342
        project.last_approval_date = now
1343
        project.save()
1344

    
1345
        if new_project:
1346
            project.add_member(self.owner)
1347

    
1348
        # This will block while syncing,
1349
        # but unblock before setting the membership state.
1350
        # See ProjectMembership.set_sync()
1351
        project.set_membership_pending_sync()
1352

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

    
1359
        self.state = self.APPROVED
1360
        self.save()
1361

    
1362
def submit_application(**kw):
1363

    
1364
    resource_policies = kw.pop('resource_policies', None)
1365
    application = ProjectApplication(**kw)
1366

    
1367
    precursor = kw['precursor_application']
1368

    
1369
    if precursor is not None:
1370
        precursor.state = ProjectApplication.REPLACED
1371
        precursor.save()
1372

    
1373
    application.save()
1374
    application.resource_policies = resource_policies
1375
    return application
1376

    
1377
class ProjectResourceGrant(models.Model):
1378

    
1379
    resource                =   models.ForeignKey(Resource)
1380
    project_application     =   models.ForeignKey(ProjectApplication,
1381
                                                  null=True)
1382
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1383
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1384
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1385
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1386
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1387
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1388

    
1389
    objects = ExtendedManager()
1390

    
1391
    class Meta:
1392
        unique_together = ("resource", "project_application")
1393

    
1394

    
1395
class Project(models.Model):
1396

    
1397
    application                 =   models.OneToOneField(
1398
                                            ProjectApplication,
1399
                                            related_name='project')
1400
    last_approval_date          =   models.DateTimeField(null=True)
1401

    
1402
    members                     =   models.ManyToManyField(
1403
                                            AstakosUser,
1404
                                            through='ProjectMembership')
1405

    
1406
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1407
    deactivation_start_date     =   models.DateTimeField(null=True)
1408
    deactivation_date           =   models.DateTimeField(null=True)
1409

    
1410
    creation_date               =   models.DateTimeField()
1411
    name                        =   models.CharField(
1412
                                            max_length=80,
1413
                                            db_index=True,
1414
                                            unique=True)
1415

    
1416
    TERMINATED  =   'TERMINATED'
1417
    SUSPENDED   =   'SUSPENDED'
1418

    
1419
    objects     =   ForUpdateManager()
1420

    
1421
    def __str__(self):
1422
        return _("<project %s '%s'>") % (self.id, self.application.name)
1423

    
1424
    __repr__ = __str__
1425

    
1426
    def is_deactivating(self):
1427
        return bool(self.deactivation_start_date)
1428

    
1429
    def is_deactivated_synced(self):
1430
        return bool(self.deactivation_date)
1431

    
1432
    def is_deactivated(self):
1433
        return self.is_deactivated_synced() or self.is_deactivating()
1434

    
1435
    def is_still_approved(self):
1436
        return bool(self.last_approval_date)
1437

    
1438
    def is_active(self):
1439
        return not(self.is_deactivated())
1440

    
1441
    def is_inconsistent(self):
1442
        now = datetime.now()
1443
        dates = [self.creation_date,
1444
                 self.last_approval_date,
1445
                 self.deactivation_start_date,
1446
                 self.deactivation_date]
1447
        return any([date > now for date in dates])
1448

    
1449
    def set_deactivation_start_date(self):
1450
        self.deactivation_start_date = datetime.now()
1451

    
1452
    def set_deactivation_date(self):
1453
        self.deactivation_start_date = None
1454
        self.deactivation_date = datetime.now()
1455

    
1456
    def violates_resource_grants(self):
1457
        return False
1458

    
1459
    def violates_members_limit(self, adding=0):
1460
        application = self.application
1461
        return (len(self.approved_members) + adding >
1462
                application.limit_on_members_number)
1463

    
1464
    @property
1465
    def is_alive(self):
1466
        return self.is_active()
1467

    
1468
    @property
1469
    def approved_memberships(self):
1470
        query = ProjectMembership.query_approved()
1471
        return self.projectmembership_set.filter(query)
1472

    
1473
    @property
1474
    def approved_members(self):
1475
        return [m.person for m in self.approved_memberships]
1476

    
1477
    def set_membership_pending_sync(self):
1478
        query = ProjectMembership.query_approved()
1479
        sfu = self.projectmembership_set.select_for_update()
1480
        members = sfu.filter(query)
1481

    
1482
        for member in members:
1483
            member.state = member.PENDING
1484
            member.save()
1485

    
1486
    def add_member(self, user):
1487
        """
1488
        Raises:
1489
            django.exceptions.PermissionDenied
1490
            astakos.im.models.AstakosUser.DoesNotExist
1491
        """
1492
        if isinstance(user, int):
1493
            user = AstakosUser.objects.get(user=user)
1494

    
1495
        m, created = ProjectMembership.objects.get_or_create(
1496
            person=user, project=self
1497
        )
1498
        m.accept()
1499

    
1500
    def remove_member(self, user):
1501
        """
1502
        Raises:
1503
            django.exceptions.PermissionDenied
1504
            astakos.im.models.AstakosUser.DoesNotExist
1505
            astakos.im.models.ProjectMembership.DoesNotExist
1506
        """
1507
        if isinstance(user, int):
1508
            user = AstakosUser.objects.get(user=user)
1509

    
1510
        m = ProjectMembership.objects.get(person=user, project=self)
1511
        m.remove()
1512

    
1513
    def terminate(self):
1514
        self.set_deactivation_start_date()
1515
        self.deactivation_reason = self.TERMINATED
1516
        self.save()
1517

    
1518
    @property
1519
    def is_terminated(self):
1520
        return (self.is_deactivated() and
1521
                self.deactivation_reason == self.TERMINATED)
1522

    
1523
    @property
1524
    def is_suspended(self):
1525
        return False
1526

    
1527
class ProjectMembership(models.Model):
1528

    
1529
    person              =   models.ForeignKey(AstakosUser)
1530
    request_date        =   models.DateField(default=datetime.now())
1531
    project             =   models.ForeignKey(Project)
1532

    
1533
    state               =   models.IntegerField(default=0)
1534
    application         =   models.ForeignKey(
1535
                                ProjectApplication,
1536
                                null=True,
1537
                                related_name='memberships')
1538
    pending_application =   models.ForeignKey(
1539
                                ProjectApplication,
1540
                                null=True,
1541
                                related_name='pending_memebrships')
1542
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1543

    
1544
    acceptance_date     =   models.DateField(null=True, db_index=True)
1545
    leave_request_date  =   models.DateField(null=True)
1546

    
1547
    objects     =   ForUpdateManager()
1548

    
1549
    REQUESTED   =   0
1550
    PENDING     =   1
1551
    ACCEPTED    =   2
1552
    REMOVING    =   3
1553
    REMOVED     =   4
1554
    INACTIVE    =   5
1555

    
1556
    APPROVED_SET    =   [PENDING, ACCEPTED, INACTIVE]
1557

    
1558
    @classmethod
1559
    def query_approved(cls):
1560
        return (Q(state=cls.PENDING) |
1561
                Q(state=cls.ACCEPTED) |
1562
                Q(state=cls.INACTIVE))
1563

    
1564
    class Meta:
1565
        unique_together = ("person", "project")
1566
        #index_together = [["project", "state"]]
1567

    
1568
    def __str__(self):
1569
        return _("<'%s' membership in '%s'>") % (
1570
                self.person.username, self.project)
1571

    
1572
    __repr__ = __str__
1573

    
1574
    def __init__(self, *args, **kwargs):
1575
        self.state = self.REQUESTED
1576
        super(ProjectMembership, self).__init__(*args, **kwargs)
1577

    
1578
    def _set_history_item(self, reason, date=None):
1579
        if isinstance(reason, basestring):
1580
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1581

    
1582
        history_item = ProjectMembershipHistory(
1583
                            serial=self.id,
1584
                            person=self.person.uuid,
1585
                            project=self.project_id,
1586
                            date=date or datetime.now(),
1587
                            reason=reason)
1588
        history_item.save()
1589
        serial = history_item.id
1590

    
1591
    def accept(self):
1592
        state = self.state
1593
        if state != self.REQUESTED:
1594
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1595
            raise AssertionError(m)
1596

    
1597
        now = datetime.now()
1598
        self.acceptance_date = now
1599
        self._set_history_item(reason='ACCEPT', date=now)
1600
        self.state = (self.PENDING if self.project.is_active()
1601
                      else self.INACTIVE)
1602
        self.save()
1603

    
1604
    def remove(self):
1605
        state = self.state
1606
        if state not in [self.ACCEPTED, self.INACTIVE]:
1607
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1608
            raise AssertionError(m)
1609

    
1610
        self._set_history_item(reason='REMOVE')
1611
        self.state = self.REMOVING
1612
        self.save()
1613

    
1614
    def reject(self):
1615
        state = self.state
1616
        if state != self.REQUESTED:
1617
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1618
            raise AssertionError(m)
1619

    
1620
        # rejected requests don't need sync,
1621
        # because they were never effected
1622
        self._set_history_item(reason='REJECT')
1623
        self.delete()
1624

    
1625
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1626
        if sub_list is None:
1627
            sub_list = []
1628

    
1629
        if add_list is None:
1630
            add_list = []
1631

    
1632
        sub_append = sub_list.append
1633
        add_append = add_list.append
1634
        holder = self.person.uuid
1635

    
1636
        synced_application = self.application
1637
        if synced_application is not None:
1638
            cur_grants = synced_application.projectresourcegrant_set.all()
1639
            for grant in cur_grants:
1640
                sub_append(QuotaLimits(
1641
                               holder       = holder,
1642
                               resource     = str(grant.resource),
1643
                               capacity     = grant.member_capacity,
1644
                               import_limit = grant.member_import_limit,
1645
                               export_limit = grant.member_export_limit))
1646

    
1647
        if not remove:
1648
            new_grants = self.pending_application.projectresourcegrant_set.all()
1649
            for new_grant in new_grants:
1650
                add_append(QuotaLimits(
1651
                               holder       = holder,
1652
                               resource     = str(new_grant.resource),
1653
                               capacity     = new_grant.member_capacity,
1654
                               import_limit = new_grant.member_import_limit,
1655
                               export_limit = new_grant.member_export_limit))
1656

    
1657
        return (sub_list, add_list)
1658

    
1659
    def set_sync(self):
1660
        state = self.state
1661
        if state == self.PENDING:
1662
            pending_application = self.pending_application
1663
            if pending_application is None:
1664
                m = _("%s: attempt to sync an empty pending application") % (
1665
                    self,)
1666
                raise AssertionError(m)
1667
            self.application = pending_application
1668
            self.pending_application = None
1669
            self.pending_serial = None
1670

    
1671
            # project.application may have changed in the meantime,
1672
            # in which case we stay PENDING;
1673
            # we are safe to check due to select_for_update
1674
            if self.application == self.project.application:
1675
                self.state = self.ACCEPTED
1676
            self.save()
1677
        elif state == self.ACCEPTED:
1678
            if self.pending_application:
1679
                m = _("%s: attempt to sync in state '%s' "
1680
                      "with a pending application") % (self, state)
1681
                raise AssertionError(m)
1682
            self.application = None
1683
            self.pending_serial = None
1684
            self.state = self.INACTIVE
1685
            self.save()
1686
        elif state == self.REMOVING:
1687
            self.delete()
1688
        else:
1689
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1690
            raise AssertionError(m)
1691

    
1692
    def reset_sync(self):
1693
        state = self.state
1694
        if state in [self.PENDING, self.ACCEPTED, self.REMOVING]:
1695
            self.pending_application = None
1696
            self.pending_serial = None
1697
            self.save()
1698
        else:
1699
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1700
            raise AssertionError(m)
1701

    
1702
class Serial(models.Model):
1703
    serial  =   models.AutoField(primary_key=True)
1704

    
1705
def new_serial():
1706
    s = Serial.objects.create()
1707
    serial = s.serial
1708
    s.delete()
1709
    return serial
1710

    
1711
def sync_finish_serials(serials_to_ack=None):
1712
    if serials_to_ack is None:
1713
        serials_to_ack = qh_query_serials([])
1714

    
1715
    serials_to_ack = set(serials_to_ack)
1716
    sfu = ProjectMembership.objects.select_for_update()
1717
    memberships = list(sfu.filter(pending_serial__isnull=False))
1718

    
1719
    if memberships:
1720
        for membership in memberships:
1721
            serial = membership.pending_serial
1722
            if serial in serials_to_ack:
1723
                membership.set_sync()
1724
            else:
1725
                membership.reset_sync()
1726

    
1727
        transaction.commit()
1728

    
1729
    qh_ack_serials(list(serials_to_ack))
1730
    return len(memberships)
1731

    
1732
def sync_all_projects():
1733
    sync_finish_serials()
1734

    
1735
    PENDING = ProjectMembership.PENDING
1736
    REMOVING = ProjectMembership.REMOVING
1737
    objects = ProjectMembership.objects.select_for_update()
1738

    
1739
    sub_quota, add_quota = [], []
1740

    
1741
    serial = new_serial()
1742

    
1743
    pending = objects.filter(state=PENDING)
1744
    for membership in pending:
1745

    
1746
        if membership.pending_application:
1747
            m = "%s: impossible: pending_application is not None (%s)" % (
1748
                membership, membership.pending_application)
1749
            raise AssertionError(m)
1750
        if membership.pending_serial:
1751
            m = "%s: impossible: pending_serial is not None (%s)" % (
1752
                membership, membership.pending_serial)
1753
            raise AssertionError(m)
1754

    
1755
        membership.pending_application = membership.project.application
1756
        membership.pending_serial = serial
1757
        membership.get_diff_quotas(sub_quota, add_quota)
1758
        membership.save()
1759

    
1760
    removing = objects.filter(state=REMOVING)
1761
    for membership in removing:
1762

    
1763
        if membership.pending_application:
1764
            m = ("%s: impossible: removing pending_application is not None (%s)"
1765
                % (membership, membership.pending_application))
1766
            raise AssertionError(m)
1767
        if membership.pending_serial:
1768
            m = "%s: impossible: pending_serial is not None (%s)" % (
1769
                membership, membership.pending_serial)
1770
            raise AssertionError(m)
1771

    
1772
        membership.pending_serial = serial
1773
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1774
        membership.save()
1775

    
1776
    transaction.commit()
1777
    # ProjectApplication.approve() unblocks here
1778
    # and can set PENDING an already PENDING membership
1779
    # which has been scheduled to sync with the old project.application
1780
    # Need to check in ProjectMembership.set_sync()
1781

    
1782
    r = qh_add_quota(serial, sub_quota, add_quota)
1783
    if r:
1784
        m = "cannot sync serial: %d" % serial
1785
        raise RuntimeError(m)
1786

    
1787
    sync_finish_serials([serial])
1788

    
1789
def sync_deactivating_projects():
1790

    
1791
    ACCEPTED = ProjectMembership.ACCEPTED
1792
    PENDING = ProjectMembership.PENDING
1793
    REMOVING = ProjectMembership.REMOVING
1794

    
1795
    psfu = Project.objects.select_for_update()
1796
    projects = psfu.filter(deactivation_start_date__isnull=False)
1797

    
1798
    if not projects:
1799
        return
1800

    
1801
    sub_quota, add_quota = [], []
1802

    
1803
    serial = new_serial()
1804

    
1805
    for project in projects:
1806
        objects = project.projectmembership_set.select_for_update()
1807
        memberships = objects.filter(Q(state=ACCEPTED) |
1808
                                     Q(state=PENDING) | Q(state=REMOVING))
1809
        for membership in memberships:
1810
            if membership.state in (PENDING, REMOVING):
1811
                m = "cannot sync deactivating project '%s'" % project
1812
                raise RuntimeError(m)
1813

    
1814
            # state == ACCEPTED
1815
            if membership.pending_application:
1816
                m = "%s: impossible: pending_application is not None (%s)" % (
1817
                    membership, membership.pending_application)
1818
                raise AssertionError(m)
1819
            if membership.pending_serial:
1820
                m = "%s: impossible: pending_serial is not None (%s)" % (
1821
                    membership, membership.pending_serial)
1822
                raise AssertionError(m)
1823

    
1824
            membership.pending_serial = serial
1825
            membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1826
            membership.save()
1827

    
1828
    transaction.commit()
1829

    
1830
    r = qh_add_quota(serial, sub_quota, add_quota)
1831
    if r:
1832
        m = "cannot sync serial: %d" % serial
1833
        raise RuntimeError(m)
1834

    
1835
    sync_finish_serials([serial])
1836

    
1837
    # finalize deactivating projects
1838
    deactivating_projects = psfu.filter(deactivation_start_date__isnull=False)
1839
    for project in deactivating_projects:
1840
        objects = project.projectmembership_set.select_for_update()
1841
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1842
                                          Q(state=PENDING) | Q(state=REMOVING)))
1843
        if not memberships:
1844
            project.set_deactivation_date()
1845
            project.save()
1846

    
1847
    transaction.commit()
1848

    
1849
def sync_projects():
1850
    sync_all_projects()
1851
    sync_deactivating_projects()
1852

    
1853
def trigger_sync(retries=3, retry_wait=1.0):
1854
    transaction.commit()
1855

    
1856
    cursor = connection.cursor()
1857
    locked = True
1858
    try:
1859
        while 1:
1860
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1861
            r = cursor.fetchone()
1862
            if r is None:
1863
                m = "Impossible"
1864
                raise AssertionError(m)
1865
            locked = r[0]
1866
            if locked:
1867
                break
1868

    
1869
            retries -= 1
1870
            if retries <= 0:
1871
                return False
1872
            sleep(retry_wait)
1873

    
1874
        sync_projects()
1875
        return True
1876

    
1877
    finally:
1878
        if locked:
1879
            cursor.execute("SELECT pg_advisory_unlock(1)")
1880
            cursor.fetchall()
1881

    
1882

    
1883
class ProjectMembershipHistory(models.Model):
1884
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1885
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1886

    
1887
    person  =   models.CharField(max_length=255)
1888
    project =   models.BigIntegerField()
1889
    date    =   models.DateField(default=datetime.now)
1890
    reason  =   models.IntegerField()
1891
    serial  =   models.BigIntegerField()
1892

    
1893
### SIGNALS ###
1894
################
1895

    
1896
def create_astakos_user(u):
1897
    try:
1898
        AstakosUser.objects.get(user_ptr=u.pk)
1899
    except AstakosUser.DoesNotExist:
1900
        extended_user = AstakosUser(user_ptr_id=u.pk)
1901
        extended_user.__dict__.update(u.__dict__)
1902
        extended_user.save()
1903
        if not extended_user.has_auth_provider('local'):
1904
            extended_user.add_auth_provider('local')
1905
    except BaseException, e:
1906
        logger.exception(e)
1907

    
1908

    
1909
def fix_superusers(sender, **kwargs):
1910
    # Associate superusers with AstakosUser
1911
    admins = User.objects.filter(is_superuser=True)
1912
    for u in admins:
1913
        create_astakos_user(u)
1914
post_syncdb.connect(fix_superusers)
1915

    
1916

    
1917
def user_post_save(sender, instance, created, **kwargs):
1918
    if not created:
1919
        return
1920
    create_astakos_user(instance)
1921
post_save.connect(user_post_save, sender=User)
1922

    
1923
def astakosuser_post_save(sender, instance, created, **kwargs):
1924
    if not created:
1925
        return
1926
    # TODO handle socket.error & IOError
1927
    register_users((instance,))
1928
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1929

    
1930
def resource_post_save(sender, instance, created, **kwargs):
1931
    if not created:
1932
        return
1933
    register_resources((instance,))
1934
post_save.connect(resource_post_save, sender=Resource)
1935

    
1936
def renew_token(sender, instance, **kwargs):
1937
    if not instance.auth_token:
1938
        instance.renew_token()
1939
pre_save.connect(renew_token, sender=AstakosUser)
1940
pre_save.connect(renew_token, sender=Service)
1941