Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 123be68a

History | View | Annotate | Download (67.2 kB)

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

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

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
70
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA)
71
from astakos.im import settings as astakos_settings
72
from astakos.im.endpoints.qh import (
73
    register_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 ProjectManager(ForUpdateManager):
1396

    
1397
    def deactivating_projects(self):
1398
        return self.filter(state__gt=Project.ACTIVE)
1399

    
1400
    def _q_terminated(self):
1401
        return Q(state=Project.TERMINATED) | Q(state=Project.TERMINATING)
1402

    
1403
    def terminated_projects(self):
1404
        q = self._q_terminated()
1405
        return self.filter(q)
1406

    
1407
    def not_terminated_projects(self):
1408
        q = ~self._q_terminated()
1409
        return self.filter(q)
1410

    
1411
class Project(models.Model):
1412

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

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

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

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

    
1431
    ACTIVE      = 1 << 8
1432
    TERMINATED  = 1
1433
    SUSPENDED   = 2
1434

    
1435
    INACTIVE    = 0
1436
    TERMINATING = TERMINATED | ACTIVE
1437
    SUSPENDING  = SUSPENDED | ACTIVE
1438

    
1439
    state                       =   models.IntegerField(default=ACTIVE,
1440
                                                        db_index=True)
1441

    
1442
    objects     =   ProjectManager()
1443

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

    
1447
    __repr__ = __str__
1448

    
1449

    
1450
    ### Internal state manipulation
1451

    
1452
    def _active_bit(self):
1453
        return self.state & self.ACTIVE
1454

    
1455
    def is_active_bit(self):
1456
        return self._active_bit() == self.ACTIVE
1457

    
1458
    def is_active_strict(self):
1459
        return self.state == self.ACTIVE
1460

    
1461
    def is_modulo_active(self, s):
1462
        return self.state & (~self.ACTIVE) == s
1463

    
1464
    def set_modulo_active(self, s):
1465
        self.state = s | self._active_bit()
1466

    
1467
    def set_inactive(self):
1468
        self.state &= (~self.ACTIVE)
1469

    
1470
    def is_deactivating(self, reason=None):
1471
        return (self.is_active_bit() and
1472
                (self.is_modulo_active(reason) if reason
1473
                 else not self.is_active_strict()))
1474

    
1475
    def is_deactivated_synced(self, reason=None):
1476
        if reason:
1477
            return self.state == reason
1478
        return not self.is_active_bit()
1479

    
1480
    def is_deactivated(self, reason=None):
1481
        return (self.is_deactivated_synced(reason) or
1482
                self.is_deactivating(reason))
1483

    
1484

    
1485
    ### Deactivation calls
1486

    
1487
    def set_deactivation_date(self):
1488
        self.deactivation_date = datetime.now()
1489

    
1490
    def deactivate(self):
1491
        self.set_deactivation_date()
1492
        self.set_inactive()
1493

    
1494
    def terminate(self):
1495
        self.deactivation_reason = 'TERMINATED'
1496
        self.set_modulo_active(self.TERMINATED)
1497
        self.save()
1498

    
1499

    
1500
    ### Logical checks
1501

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

    
1509
    def is_active(self):
1510
        return self.is_active_strict()
1511

    
1512
    @property
1513
    def is_alive(self):
1514
        return self.is_active()
1515

    
1516
    @property
1517
    def is_terminated(self):
1518
        return self.is_deactivated(self.TERMINATED)
1519

    
1520
    @property
1521
    def is_suspended(self):
1522
        return False
1523

    
1524
    def violates_resource_grants(self):
1525
        return False
1526

    
1527
    def violates_members_limit(self, adding=0):
1528
        application = self.application
1529
        return (len(self.approved_members) + adding >
1530
                application.limit_on_members_number)
1531

    
1532

    
1533
    ### Other
1534

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

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

    
1544
    def set_membership_pending_sync(self):
1545
        query = ProjectMembership.query_approved()
1546
        sfu = self.projectmembership_set.select_for_update()
1547
        members = sfu.filter(query)
1548

    
1549
        for member in members:
1550
            member.state = member.PENDING
1551
            member.save()
1552

    
1553
    def add_member(self, user):
1554
        """
1555
        Raises:
1556
            django.exceptions.PermissionDenied
1557
            astakos.im.models.AstakosUser.DoesNotExist
1558
        """
1559
        if isinstance(user, int):
1560
            user = AstakosUser.objects.get(user=user)
1561

    
1562
        m, created = ProjectMembership.objects.get_or_create(
1563
            person=user, project=self
1564
        )
1565
        m.accept()
1566

    
1567
    def remove_member(self, user):
1568
        """
1569
        Raises:
1570
            django.exceptions.PermissionDenied
1571
            astakos.im.models.AstakosUser.DoesNotExist
1572
            astakos.im.models.ProjectMembership.DoesNotExist
1573
        """
1574
        if isinstance(user, int):
1575
            user = AstakosUser.objects.get(user=user)
1576

    
1577
        m = ProjectMembership.objects.get(person=user, project=self)
1578
        m.remove()
1579

    
1580
class ProjectMembership(models.Model):
1581

    
1582
    person              =   models.ForeignKey(AstakosUser)
1583
    request_date        =   models.DateField(default=datetime.now())
1584
    project             =   models.ForeignKey(Project)
1585

    
1586
    state               =   models.IntegerField(default=0)
1587
    application         =   models.ForeignKey(
1588
                                ProjectApplication,
1589
                                null=True,
1590
                                related_name='memberships')
1591
    pending_application =   models.ForeignKey(
1592
                                ProjectApplication,
1593
                                null=True,
1594
                                related_name='pending_memebrships')
1595
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1596

    
1597
    acceptance_date     =   models.DateField(null=True, db_index=True)
1598
    leave_request_date  =   models.DateField(null=True)
1599

    
1600
    objects     =   ForUpdateManager()
1601

    
1602
    REQUESTED   =   0
1603
    PENDING     =   1
1604
    ACCEPTED    =   2
1605
    REMOVING    =   3
1606
    REMOVED     =   4
1607
    INACTIVE    =   5
1608

    
1609
    APPROVED_SET    =   [PENDING, ACCEPTED, INACTIVE]
1610

    
1611
    @classmethod
1612
    def query_approved(cls):
1613
        return (Q(state=cls.PENDING) |
1614
                Q(state=cls.ACCEPTED) |
1615
                Q(state=cls.INACTIVE))
1616

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

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

    
1625
    __repr__ = __str__
1626

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

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

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

    
1644
    def accept(self):
1645
        state = self.state
1646
        if state != self.REQUESTED:
1647
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1648
            raise AssertionError(m)
1649

    
1650
        now = datetime.now()
1651
        self.acceptance_date = now
1652
        self._set_history_item(reason='ACCEPT', date=now)
1653
        self.state = (self.PENDING if self.project.is_active()
1654
                      else self.INACTIVE)
1655
        self.save()
1656

    
1657
    def remove(self):
1658
        state = self.state
1659
        if state not in [self.ACCEPTED, self.INACTIVE]:
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.REMOVING
1665
        self.save()
1666

    
1667
    def reject(self):
1668
        state = self.state
1669
        if state != self.REQUESTED:
1670
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1671
            raise AssertionError(m)
1672

    
1673
        # rejected requests don't need sync,
1674
        # because they were never effected
1675
        self._set_history_item(reason='REJECT')
1676
        self.delete()
1677

    
1678
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1679
        if sub_list is None:
1680
            sub_list = []
1681

    
1682
        if add_list is None:
1683
            add_list = []
1684

    
1685
        sub_append = sub_list.append
1686
        add_append = add_list.append
1687
        holder = self.person.uuid
1688

    
1689
        synced_application = self.application
1690
        if synced_application is not None:
1691
            cur_grants = synced_application.projectresourcegrant_set.all()
1692
            for grant in cur_grants:
1693
                sub_append(QuotaLimits(
1694
                               holder       = holder,
1695
                               resource     = str(grant.resource),
1696
                               capacity     = grant.member_capacity,
1697
                               import_limit = grant.member_import_limit,
1698
                               export_limit = grant.member_export_limit))
1699

    
1700
        if not remove:
1701
            new_grants = self.pending_application.projectresourcegrant_set.all()
1702
            for new_grant in new_grants:
1703
                add_append(QuotaLimits(
1704
                               holder       = holder,
1705
                               resource     = str(new_grant.resource),
1706
                               capacity     = new_grant.member_capacity,
1707
                               import_limit = new_grant.member_import_limit,
1708
                               export_limit = new_grant.member_export_limit))
1709

    
1710
        return (sub_list, add_list)
1711

    
1712
    def set_sync(self):
1713
        state = self.state
1714
        if state == self.PENDING:
1715
            pending_application = self.pending_application
1716
            if pending_application is None:
1717
                m = _("%s: attempt to sync an empty pending application") % (
1718
                    self,)
1719
                raise AssertionError(m)
1720
            self.application = pending_application
1721
            self.pending_application = None
1722
            self.pending_serial = None
1723

    
1724
            # project.application may have changed in the meantime,
1725
            # in which case we stay PENDING;
1726
            # we are safe to check due to select_for_update
1727
            if self.application == self.project.application:
1728
                self.state = self.ACCEPTED
1729
            self.save()
1730
        elif state == self.ACCEPTED:
1731
            if self.pending_application:
1732
                m = _("%s: attempt to sync in state '%s' "
1733
                      "with a pending application") % (self, state)
1734
                raise AssertionError(m)
1735
            self.application = None
1736
            self.pending_serial = None
1737
            self.state = self.INACTIVE
1738
            self.save()
1739
        elif state == self.REMOVING:
1740
            self.delete()
1741
        else:
1742
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1743
            raise AssertionError(m)
1744

    
1745
    def reset_sync(self):
1746
        state = self.state
1747
        if state in [self.PENDING, self.ACCEPTED, self.REMOVING]:
1748
            self.pending_application = None
1749
            self.pending_serial = None
1750
            self.save()
1751
        else:
1752
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1753
            raise AssertionError(m)
1754

    
1755
class Serial(models.Model):
1756
    serial  =   models.AutoField(primary_key=True)
1757

    
1758
def new_serial():
1759
    s = Serial.objects.create()
1760
    serial = s.serial
1761
    s.delete()
1762
    return serial
1763

    
1764
def sync_finish_serials(serials_to_ack=None):
1765
    if serials_to_ack is None:
1766
        serials_to_ack = qh_query_serials([])
1767

    
1768
    serials_to_ack = set(serials_to_ack)
1769
    sfu = ProjectMembership.objects.select_for_update()
1770
    memberships = list(sfu.filter(pending_serial__isnull=False))
1771

    
1772
    if memberships:
1773
        for membership in memberships:
1774
            serial = membership.pending_serial
1775
            if serial in serials_to_ack:
1776
                membership.set_sync()
1777
            else:
1778
                membership.reset_sync()
1779

    
1780
        transaction.commit()
1781

    
1782
    qh_ack_serials(list(serials_to_ack))
1783
    return len(memberships)
1784

    
1785
def sync_all_projects():
1786
    sync_finish_serials()
1787

    
1788
    PENDING = ProjectMembership.PENDING
1789
    REMOVING = ProjectMembership.REMOVING
1790
    objects = ProjectMembership.objects.select_for_update()
1791

    
1792
    sub_quota, add_quota = [], []
1793

    
1794
    serial = new_serial()
1795

    
1796
    pending = objects.filter(state=PENDING)
1797
    for membership in pending:
1798

    
1799
        if membership.pending_application:
1800
            m = "%s: impossible: pending_application is not None (%s)" % (
1801
                membership, membership.pending_application)
1802
            raise AssertionError(m)
1803
        if membership.pending_serial:
1804
            m = "%s: impossible: pending_serial is not None (%s)" % (
1805
                membership, membership.pending_serial)
1806
            raise AssertionError(m)
1807

    
1808
        membership.pending_application = membership.project.application
1809
        membership.pending_serial = serial
1810
        membership.get_diff_quotas(sub_quota, add_quota)
1811
        membership.save()
1812

    
1813
    removing = objects.filter(state=REMOVING)
1814
    for membership in removing:
1815

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

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

    
1829
    transaction.commit()
1830
    # ProjectApplication.approve() unblocks here
1831
    # and can set PENDING an already PENDING membership
1832
    # which has been scheduled to sync with the old project.application
1833
    # Need to check in ProjectMembership.set_sync()
1834

    
1835
    r = qh_add_quota(serial, sub_quota, add_quota)
1836
    if r:
1837
        m = "cannot sync serial: %d" % serial
1838
        raise RuntimeError(m)
1839

    
1840
    sync_finish_serials([serial])
1841

    
1842
def sync_deactivating_projects():
1843

    
1844
    ACCEPTED = ProjectMembership.ACCEPTED
1845
    PENDING = ProjectMembership.PENDING
1846
    REMOVING = ProjectMembership.REMOVING
1847

    
1848
    psfu = Project.objects.select_for_update()
1849
    projects = psfu.deactivating_projects()
1850

    
1851
    if not projects:
1852
        return
1853

    
1854
    sub_quota, add_quota = [], []
1855

    
1856
    serial = new_serial()
1857

    
1858
    for project in projects:
1859
        objects = project.projectmembership_set.select_for_update()
1860
        memberships = objects.filter(Q(state=ACCEPTED) |
1861
                                     Q(state=PENDING) | Q(state=REMOVING))
1862
        for membership in memberships:
1863
            if membership.state in (PENDING, REMOVING):
1864
                m = "cannot sync deactivating project '%s'" % project
1865
                raise RuntimeError(m)
1866

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

    
1877
            membership.pending_serial = serial
1878
            membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1879
            membership.save()
1880

    
1881
    transaction.commit()
1882

    
1883
    r = qh_add_quota(serial, sub_quota, add_quota)
1884
    if r:
1885
        m = "cannot sync serial: %d" % serial
1886
        raise RuntimeError(m)
1887

    
1888
    sync_finish_serials([serial])
1889

    
1890
    # finalize deactivating projects
1891
    deactivating_projects = psfu.deactivating_projects()
1892
    for project in deactivating_projects:
1893
        objects = project.projectmembership_set.select_for_update()
1894
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1895
                                          Q(state=PENDING) | Q(state=REMOVING)))
1896
        if not memberships:
1897
            project.set_deactivation_date()
1898
            project.save()
1899

    
1900
    transaction.commit()
1901

    
1902
def sync_projects():
1903
    sync_all_projects()
1904
    sync_deactivating_projects()
1905

    
1906
def trigger_sync(retries=3, retry_wait=1.0):
1907
    transaction.commit()
1908

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

    
1922
            retries -= 1
1923
            if retries <= 0:
1924
                return False
1925
            sleep(retry_wait)
1926

    
1927
        sync_projects()
1928
        return True
1929

    
1930
    finally:
1931
        if locked:
1932
            cursor.execute("SELECT pg_advisory_unlock(1)")
1933
            cursor.fetchall()
1934

    
1935

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

    
1940
    person  =   models.CharField(max_length=255)
1941
    project =   models.BigIntegerField()
1942
    date    =   models.DateField(default=datetime.now)
1943
    reason  =   models.IntegerField()
1944
    serial  =   models.BigIntegerField()
1945

    
1946
### SIGNALS ###
1947
################
1948

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

    
1961

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

    
1969

    
1970
def user_post_save(sender, instance, created, **kwargs):
1971
    if not created:
1972
        return
1973
    create_astakos_user(instance)
1974
post_save.connect(user_post_save, sender=User)
1975

    
1976
def astakosuser_post_save(sender, instance, created, **kwargs):
1977
    if not created:
1978
        return
1979
    # TODO handle socket.error & IOError
1980
    register_users((instance,))
1981
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1982

    
1983
def resource_post_save(sender, instance, created, **kwargs):
1984
    if not created:
1985
        return
1986
    register_resources((instance,))
1987
post_save.connect(resource_post_save, sender=Resource)
1988

    
1989
def renew_token(sender, instance, **kwargs):
1990
    if not instance.auth_token:
1991
        instance.renew_token()
1992
pre_save.connect(renew_token, sender=AstakosUser)
1993
pre_save.connect(renew_token, sender=Service)
1994