Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (64.8 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

    
114
    def renew_token(self, expiration_date=None):
115
        md5 = hashlib.md5()
116
        md5.update(self.name.encode('ascii', 'ignore'))
117
        md5.update(self.url.encode('ascii', 'ignore'))
118
        md5.update(asctime())
119

    
120
        self.auth_token = b64encode(md5.digest())
121
        self.auth_token_created = datetime.now()
122
        if expiration_date:
123
            self.auth_token_expires = expiration_date
124
        else:
125
            self.auth_token_expires = None
126

    
127
    def __str__(self):
128
        return self.name
129

    
130
    @property
131
    def resources(self):
132
        return self.resource_set.all()
133

    
134
    @resources.setter
135
    def resources(self, resources):
136
        for s in resources:
137
            self.resource_set.create(**s)
138

    
139
    def add_resource(self, service, resource, uplimit, update=True):
140
        """Raises ObjectDoesNotExist, IntegrityError"""
141
        resource = Resource.objects.get(service__name=service, name=resource)
142
        if update:
143
            AstakosUserQuota.objects.update_or_create(user=self,
144
                                                      resource=resource,
145
                                                      defaults={'uplimit': uplimit})
146
        else:
147
            q = self.astakosuserquota_set
148
            q.create(resource=resource, uplimit=uplimit)
149

    
150

    
151
class ResourceMetadata(models.Model):
152
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
153
    value = models.CharField(_('Value'), max_length=255)
154

    
155
_presentation_data = {}
156
def get_presentation(resource):
157
    global _presentation_data
158
    presentation = _presentation_data.get(resource, {})
159
    if not presentation:
160
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
161
        presentation = resource_presentation.get(resource, {})
162
        _presentation_data[resource] = presentation
163
    return presentation
164

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

    
173
    class Meta:
174
        unique_together = ("name", "service")
175

    
176
    def __str__(self):
177
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
178

    
179
    @property
180
    def help_text(self):
181
        return get_presentation(str(self)).get('help_text', '')
182

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

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

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

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

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

    
203

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

    
218
class AstakosUserManager(UserManager):
219

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

    
229
    def get_by_email(self, email):
230
        return self.get(email=email)
231

    
232
    def get_by_identifier(self, email_or_username, **kwargs):
233
        try:
234
            return self.get(email__iexact=email_or_username, **kwargs)
235
        except AstakosUser.DoesNotExist:
236
            return self.get(username__iexact=email_or_username, **kwargs)
237

    
238
    def user_exists(self, email_or_username, **kwargs):
239
        qemail = Q(email__iexact=email_or_username)
240
        qusername = Q(username__iexact=email_or_username)
241
        return self.filter(qemail | qusername).exists()
242

    
243

    
244
class AstakosUser(User):
245
    """
246
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
247
    """
248
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
249
                                   null=True)
250

    
251
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
252
    #                    AstakosUserProvider model.
253
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
254
                                null=True)
255
    # ex. screen_name for twitter, eppn for shibboleth
256
    third_party_identifier = models.CharField(_('Third-party identifier'),
257
                                              max_length=255, null=True,
258
                                              blank=True)
259

    
260

    
261
    #for invitations
262
    user_level = DEFAULT_USER_LEVEL
263
    level = models.IntegerField(_('Inviter level'), default=user_level)
264
    invitations = models.IntegerField(
265
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
266

    
267
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
268
                                  null=True, blank=True)
269
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
270
    auth_token_expires = models.DateTimeField(
271
        _('Token expiration date'), null=True)
272

    
273
    updated = models.DateTimeField(_('Update date'))
274
    is_verified = models.BooleanField(_('Is verified?'), default=False)
275

    
276
    email_verified = models.BooleanField(_('Email verified?'), default=False)
277

    
278
    has_credits = models.BooleanField(_('Has credits?'), default=False)
279
    has_signed_terms = models.BooleanField(
280
        _('I agree with the terms'), default=False)
281
    date_signed_terms = models.DateTimeField(
282
        _('Signed terms date'), null=True, blank=True)
283

    
284
    activation_sent = models.DateTimeField(
285
        _('Activation sent data'), null=True, blank=True)
286

    
287
    policy = models.ManyToManyField(
288
        Resource, null=True, through='AstakosUserQuota')
289

    
290
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
291

    
292
    __has_signed_terms = False
293
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
294
                                           default=False, db_index=True)
295

    
296
    objects = AstakosUserManager()
297

    
298
    def __init__(self, *args, **kwargs):
299
        super(AstakosUser, self).__init__(*args, **kwargs)
300
        self.__has_signed_terms = self.has_signed_terms
301
        if not self.id:
302
            self.is_active = False
303

    
304
    @property
305
    def realname(self):
306
        return '%s %s' % (self.first_name, self.last_name)
307

    
308
    @realname.setter
309
    def realname(self, value):
310
        parts = value.split(' ')
311
        if len(parts) == 2:
312
            self.first_name = parts[0]
313
            self.last_name = parts[1]
314
        else:
315
            self.last_name = parts[0]
316

    
317
    def add_permission(self, pname):
318
        if self.has_perm(pname):
319
            return
320
        p, created = Permission.objects.get_or_create(
321
                                    codename=pname,
322
                                    name=pname.capitalize(),
323
                                    content_type=get_content_type())
324
        self.user_permissions.add(p)
325

    
326
    def remove_permission(self, pname):
327
        if self.has_perm(pname):
328
            return
329
        p = Permission.objects.get(codename=pname,
330
                                   content_type=get_content_type())
331
        self.user_permissions.remove(p)
332

    
333
    @property
334
    def invitation(self):
335
        try:
336
            return Invitation.objects.get(username=self.email)
337
        except Invitation.DoesNotExist:
338
            return None
339

    
340
    @property
341
    def quota(self):
342
        """Returns a dict with the sum of quota limits per resource"""
343
        d = defaultdict(int)
344
        default_quota = get_default_quota()
345
        d.update(default_quota)
346
        for q in self.policies:
347
            d[q.resource] += q.uplimit or inf
348
        for m in self.projectmembership_set.select_related().all():
349
            if not m.acceptance_date:
350
                continue
351
            p = m.project
352
            if not p.is_active():
353
                continue
354
            grants = p.application.projectresourcegrant_set.all()
355
            for g in grants:
356
                d[str(g.resource)] += g.member_capacity or inf
357
        return d
358

    
359
    @property
360
    def policies(self):
361
        return self.astakosuserquota_set.select_related().all()
362

    
363
    @policies.setter
364
    def policies(self, policies):
365
        for p in policies:
366
            service = policies.get('service', None)
367
            resource = policies.get('resource', None)
368
            uplimit = policies.get('uplimit', 0)
369
            update = policies.get('update', True)
370
            self.add_policy(service, resource, uplimit, update)
371

    
372
    def add_policy(self, service, resource, uplimit, update=True):
373
        """Raises ObjectDoesNotExist, IntegrityError"""
374
        resource = Resource.objects.get(service__name=service, name=resource)
375
        if update:
376
            AstakosUserQuota.objects.update_or_create(user=self,
377
                                                      resource=resource,
378
                                                      defaults={'uplimit': uplimit})
379
        else:
380
            q = self.astakosuserquota_set
381
            q.create(resource=resource, uplimit=uplimit)
382

    
383
    def remove_policy(self, service, resource):
384
        """Raises ObjectDoesNotExist, IntegrityError"""
385
        resource = Resource.objects.get(service__name=service, name=resource)
386
        q = self.policies.get(resource=resource).delete()
387

    
388
    def update_uuid(self):
389
        while not self.uuid:
390
            uuid_val =  str(uuid.uuid4())
391
            try:
392
                AstakosUser.objects.get(uuid=uuid_val)
393
            except AstakosUser.DoesNotExist, e:
394
                self.uuid = uuid_val
395
        return self.uuid
396

    
397
    @property
398
    def extended_groups(self):
399
        return self.membership_set.select_related().all()
400

    
401
    def save(self, update_timestamps=True, **kwargs):
402
        if update_timestamps:
403
            if not self.id:
404
                self.date_joined = datetime.now()
405
            self.updated = datetime.now()
406

    
407
        # update date_signed_terms if necessary
408
        if self.__has_signed_terms != self.has_signed_terms:
409
            self.date_signed_terms = datetime.now()
410

    
411
        self.update_uuid()
412

    
413
        if self.username != self.email.lower():
414
            # set username
415
            self.username = self.email.lower()
416

    
417
        self.validate_unique_email_isactive()
418

    
419
        super(AstakosUser, self).save(**kwargs)
420

    
421
    def renew_token(self, flush_sessions=False, current_key=None):
422
        md5 = hashlib.md5()
423
        md5.update(settings.SECRET_KEY)
424
        md5.update(self.username)
425
        md5.update(self.realname.encode('ascii', 'ignore'))
426
        md5.update(asctime())
427

    
428
        self.auth_token = b64encode(md5.digest())
429
        self.auth_token_created = datetime.now()
430
        self.auth_token_expires = self.auth_token_created + \
431
                                  timedelta(hours=AUTH_TOKEN_DURATION)
432
        if flush_sessions:
433
            self.flush_sessions(current_key)
434
        msg = 'Token renewed for %s' % self.email
435
        logger.log(LOGGING_LEVEL, msg)
436

    
437
    def flush_sessions(self, current_key=None):
438
        q = self.sessions
439
        if current_key:
440
            q = q.exclude(session_key=current_key)
441

    
442
        keys = q.values_list('session_key', flat=True)
443
        if keys:
444
            msg = 'Flushing sessions: %s' % ','.join(keys)
445
            logger.log(LOGGING_LEVEL, msg, [])
446
        engine = import_module(settings.SESSION_ENGINE)
447
        for k in keys:
448
            s = engine.SessionStore(k)
449
            s.flush()
450

    
451
    def __unicode__(self):
452
        return '%s (%s)' % (self.realname, self.email)
453

    
454
    def conflicting_email(self):
455
        q = AstakosUser.objects.exclude(username=self.username)
456
        q = q.filter(email__iexact=self.email)
457
        if q.count() != 0:
458
            return True
459
        return False
460

    
461
    def validate_unique_email_isactive(self):
462
        """
463
        Implements a unique_together constraint for email and is_active fields.
464
        """
465
        q = AstakosUser.objects.all()
466
        q = q.filter(email = self.email)
467
        if self.id:
468
            q = q.filter(~Q(id = self.id))
469
        if q.count() != 0:
470
            m = 'Another account with the same email = %(email)s & \
471
                is_active = %(is_active)s found.' % self.__dict__
472
            raise ValidationError(m)
473

    
474
    def email_change_is_pending(self):
475
        return self.emailchanges.count() > 0
476

    
477
    def email_change_is_pending(self):
478
        return self.emailchanges.count() > 0
479

    
480
    @property
481
    def signed_terms(self):
482
        term = get_latest_terms()
483
        if not term:
484
            return True
485
        if not self.has_signed_terms:
486
            return False
487
        if not self.date_signed_terms:
488
            return False
489
        if self.date_signed_terms < term.date:
490
            self.has_signed_terms = False
491
            self.date_signed_terms = None
492
            self.save()
493
            return False
494
        return True
495

    
496
    def set_invitations_level(self):
497
        """
498
        Update user invitation level
499
        """
500
        level = self.invitation.inviter.level + 1
501
        self.level = level
502
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
503

    
504
    def can_login_with_auth_provider(self, provider):
505
        if not self.has_auth_provider(provider):
506
            return False
507
        else:
508
            return auth_providers.get_provider(provider).is_available_for_login()
509

    
510
    def can_add_auth_provider(self, provider, **kwargs):
511
        provider_settings = auth_providers.get_provider(provider)
512

    
513
        if not provider_settings.is_available_for_add():
514
            return False
515

    
516
        if self.has_auth_provider(provider) and \
517
           provider_settings.one_per_user:
518
            return False
519

    
520
        if 'provider_info' in kwargs:
521
            kwargs.pop('provider_info')
522

    
523
        if 'identifier' in kwargs:
524
            try:
525
                # provider with specified params already exist
526
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
527
                                                                   **kwargs)
528
            except AstakosUser.DoesNotExist:
529
                return True
530
            else:
531
                return False
532

    
533
        return True
534

    
535
    def can_remove_auth_provider(self, module):
536
        provider = auth_providers.get_provider(module)
537
        existing = self.get_active_auth_providers()
538
        existing_for_provider = self.get_active_auth_providers(module=module)
539

    
540
        if len(existing) <= 1:
541
            return False
542

    
543
        if len(existing_for_provider) == 1 and provider.is_required():
544
            return False
545

    
546
        return True
547

    
548
    def can_change_password(self):
549
        return self.has_auth_provider('local', auth_backend='astakos')
550

    
551
    def has_required_auth_providers(self):
552
        required = auth_providers.REQUIRED_PROVIDERS
553
        for provider in required:
554
            if not self.has_auth_provider(provider):
555
                return False
556
        return True
557

    
558
    def has_auth_provider(self, provider, **kwargs):
559
        return bool(self.auth_providers.filter(module=provider,
560
                                               **kwargs).count())
561

    
562
    def add_auth_provider(self, provider, **kwargs):
563
        info_data = ''
564
        if 'provider_info' in kwargs:
565
            info_data = kwargs.pop('provider_info')
566
            if isinstance(info_data, dict):
567
                info_data = json.dumps(info_data)
568

    
569
        if self.can_add_auth_provider(provider, **kwargs):
570
            self.auth_providers.create(module=provider, active=True,
571
                                       info_data=info_data,
572
                                       **kwargs)
573
        else:
574
            raise Exception('Cannot add provider')
575

    
576
    def add_pending_auth_provider(self, pending):
577
        """
578
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
579
        the current user.
580
        """
581
        if not isinstance(pending, PendingThirdPartyUser):
582
            pending = PendingThirdPartyUser.objects.get(token=pending)
583

    
584
        provider = self.add_auth_provider(pending.provider,
585
                               identifier=pending.third_party_identifier,
586
                                affiliation=pending.affiliation,
587
                                          provider_info=pending.info)
588

    
589
        if email_re.match(pending.email or '') and pending.email != self.email:
590
            self.additionalmail_set.get_or_create(email=pending.email)
591

    
592
        pending.delete()
593
        return provider
594

    
595
    def remove_auth_provider(self, provider, **kwargs):
596
        self.auth_providers.get(module=provider, **kwargs).delete()
597

    
598
    # user urls
599
    def get_resend_activation_url(self):
600
        return reverse('send_activation', kwargs={'user_id': self.pk})
601

    
602
    def get_provider_remove_url(self, module, **kwargs):
603
        return reverse('remove_auth_provider', kwargs={
604
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
605

    
606
    def get_activation_url(self, nxt=False):
607
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
608
                                 quote(self.auth_token))
609
        if nxt:
610
            url += "&next=%s" % quote(nxt)
611
        return url
612

    
613
    def get_password_reset_url(self, token_generator=default_token_generator):
614
        return reverse('django.contrib.auth.views.password_reset_confirm',
615
                          kwargs={'uidb36':int_to_base36(self.id),
616
                                  'token':token_generator.make_token(self)})
617

    
618
    def get_auth_providers(self):
619
        return self.auth_providers.all()
620

    
621
    def get_available_auth_providers(self):
622
        """
623
        Returns a list of providers available for user to connect to.
624
        """
625
        providers = []
626
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
627
            if self.can_add_auth_provider(module):
628
                providers.append(provider_settings(self))
629

    
630
        return providers
631

    
632
    def get_active_auth_providers(self, **filters):
633
        providers = []
634
        for provider in self.auth_providers.active(**filters):
635
            if auth_providers.get_provider(provider.module).is_available_for_login():
636
                providers.append(provider)
637
        return providers
638

    
639
    @property
640
    def auth_providers_display(self):
641
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
642

    
643
    def get_inactive_message(self):
644
        msg_extra = ''
645
        message = ''
646
        if self.activation_sent:
647
            if self.email_verified:
648
                message = _(astakos_messages.ACCOUNT_INACTIVE)
649
            else:
650
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
651
                if astakos_settings.MODERATION_ENABLED:
652
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
653
                else:
654
                    url = self.get_resend_activation_url()
655
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
656
                                u' ' + \
657
                                _('<a href="%s">%s?</a>') % (url,
658
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
659
        else:
660
            if astakos_settings.MODERATION_ENABLED:
661
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
662
            else:
663
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
664
                url = self.get_resend_activation_url()
665
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
666
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
667

    
668
        return mark_safe(message + u' '+ msg_extra)
669

    
670
    def owns_project(self, project):
671
        return project.user_status(self) == 100
672

    
673
    def is_project_member(self, project):
674
        return project.user_status(self) in [0,1,2,3]
675

    
676
    def is_project_accepted_member(self, project):
677
        return project.user_status(self) == 2
678

    
679

    
680
class AstakosUserAuthProviderManager(models.Manager):
681

    
682
    def active(self, **filters):
683
        return self.filter(active=True, **filters)
684

    
685

    
686
class AstakosUserAuthProvider(models.Model):
687
    """
688
    Available user authentication methods.
689
    """
690
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
691
                                   null=True, default=None)
692
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
693
    module = models.CharField(_('Provider'), max_length=255, blank=False,
694
                                default='local')
695
    identifier = models.CharField(_('Third-party identifier'),
696
                                              max_length=255, null=True,
697
                                              blank=True)
698
    active = models.BooleanField(default=True)
699
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
700
                                   default='astakos')
701
    info_data = models.TextField(default="", null=True, blank=True)
702
    created = models.DateTimeField('Creation date', auto_now_add=True)
703

    
704
    objects = AstakosUserAuthProviderManager()
705

    
706
    class Meta:
707
        unique_together = (('identifier', 'module', 'user'), )
708
        ordering = ('module', 'created')
709

    
710
    def __init__(self, *args, **kwargs):
711
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
712
        try:
713
            self.info = json.loads(self.info_data)
714
            if not self.info:
715
                self.info = {}
716
        except Exception, e:
717
            self.info = {}
718

    
719
        for key,value in self.info.iteritems():
720
            setattr(self, 'info_%s' % key, value)
721

    
722

    
723
    @property
724
    def settings(self):
725
        return auth_providers.get_provider(self.module)
726

    
727
    @property
728
    def details_display(self):
729
        try:
730
          return self.settings.get_details_tpl_display % self.__dict__
731
        except:
732
          return ''
733

    
734
    @property
735
    def title_display(self):
736
        title_tpl = self.settings.get_title_display
737
        try:
738
            if self.settings.get_user_title_display:
739
                title_tpl = self.settings.get_user_title_display
740
        except Exception, e:
741
            pass
742
        try:
743
          return title_tpl % self.__dict__
744
        except:
745
          return self.settings.get_title_display % self.__dict__
746

    
747
    def can_remove(self):
748
        return self.user.can_remove_auth_provider(self.module)
749

    
750
    def delete(self, *args, **kwargs):
751
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
752
        if self.module == 'local':
753
            self.user.set_unusable_password()
754
            self.user.save()
755
        return ret
756

    
757
    def __repr__(self):
758
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
759

    
760
    def __unicode__(self):
761
        if self.identifier:
762
            return "%s:%s" % (self.module, self.identifier)
763
        if self.auth_backend:
764
            return "%s:%s" % (self.module, self.auth_backend)
765
        return self.module
766

    
767
    def save(self, *args, **kwargs):
768
        self.info_data = json.dumps(self.info)
769
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
770

    
771

    
772
class ExtendedManager(models.Manager):
773
    def _update_or_create(self, **kwargs):
774
        assert kwargs, \
775
            'update_or_create() must be passed at least one keyword argument'
776
        obj, created = self.get_or_create(**kwargs)
777
        defaults = kwargs.pop('defaults', {})
778
        if created:
779
            return obj, True, False
780
        else:
781
            try:
782
                params = dict(
783
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
784
                params.update(defaults)
785
                for attr, val in params.items():
786
                    if hasattr(obj, attr):
787
                        setattr(obj, attr, val)
788
                sid = transaction.savepoint()
789
                obj.save(force_update=True)
790
                transaction.savepoint_commit(sid)
791
                return obj, False, True
792
            except IntegrityError, e:
793
                transaction.savepoint_rollback(sid)
794
                try:
795
                    return self.get(**kwargs), False, False
796
                except self.model.DoesNotExist:
797
                    raise e
798

    
799
    update_or_create = _update_or_create
800

    
801

    
802
class AstakosUserQuota(models.Model):
803
    objects = ExtendedManager()
804
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
805
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
806
    resource = models.ForeignKey(Resource)
807
    user = models.ForeignKey(AstakosUser)
808

    
809
    class Meta:
810
        unique_together = ("resource", "user")
811

    
812

    
813
class ApprovalTerms(models.Model):
814
    """
815
    Model for approval terms
816
    """
817

    
818
    date = models.DateTimeField(
819
        _('Issue date'), db_index=True, default=datetime.now())
820
    location = models.CharField(_('Terms location'), max_length=255)
821

    
822

    
823
class Invitation(models.Model):
824
    """
825
    Model for registring invitations
826
    """
827
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
828
                                null=True)
829
    realname = models.CharField(_('Real name'), max_length=255)
830
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
831
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
832
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
833
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
834
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
835

    
836
    def __init__(self, *args, **kwargs):
837
        super(Invitation, self).__init__(*args, **kwargs)
838
        if not self.id:
839
            self.code = _generate_invitation_code()
840

    
841
    def consume(self):
842
        self.is_consumed = True
843
        self.consumed = datetime.now()
844
        self.save()
845

    
846
    def __unicode__(self):
847
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
848

    
849

    
850
class EmailChangeManager(models.Manager):
851

    
852
    @transaction.commit_on_success
853
    def change_email(self, activation_key):
854
        """
855
        Validate an activation key and change the corresponding
856
        ``User`` if valid.
857

858
        If the key is valid and has not expired, return the ``User``
859
        after activating.
860

861
        If the key is not valid or has expired, return ``None``.
862

863
        If the key is valid but the ``User`` is already active,
864
        return ``None``.
865

866
        After successful email change the activation record is deleted.
867

868
        Throws ValueError if there is already
869
        """
870
        try:
871
            email_change = self.model.objects.get(
872
                activation_key=activation_key)
873
            if email_change.activation_key_expired():
874
                email_change.delete()
875
                raise EmailChange.DoesNotExist
876
            # is there an active user with this address?
877
            try:
878
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
879
            except AstakosUser.DoesNotExist:
880
                pass
881
            else:
882
                raise ValueError(_('The new email address is reserved.'))
883
            # update user
884
            user = AstakosUser.objects.get(pk=email_change.user_id)
885
            old_email = user.email
886
            user.email = email_change.new_email_address
887
            user.save()
888
            email_change.delete()
889
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
890
                                                          user.email)
891
            logger.log(LOGGING_LEVEL, msg)
892
            return user
893
        except EmailChange.DoesNotExist:
894
            raise ValueError(_('Invalid activation key.'))
895

    
896

    
897
class EmailChange(models.Model):
898
    new_email_address = models.EmailField(
899
        _(u'new e-mail address'),
900
        help_text=_('Your old email address will be used until you verify your new one.'))
901
    user = models.ForeignKey(
902
        AstakosUser, unique=True, related_name='emailchanges')
903
    requested_at = models.DateTimeField(default=datetime.now())
904
    activation_key = models.CharField(
905
        max_length=40, unique=True, db_index=True)
906

    
907
    objects = EmailChangeManager()
908

    
909
    def get_url(self):
910
        return reverse('email_change_confirm',
911
                      kwargs={'activation_key': self.activation_key})
912

    
913
    def activation_key_expired(self):
914
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
915
        return self.requested_at + expiration_date < datetime.now()
916

    
917

    
918
class AdditionalMail(models.Model):
919
    """
920
    Model for registring invitations
921
    """
922
    owner = models.ForeignKey(AstakosUser)
923
    email = models.EmailField()
924

    
925

    
926
def _generate_invitation_code():
927
    while True:
928
        code = randint(1, 2L ** 63 - 1)
929
        try:
930
            Invitation.objects.get(code=code)
931
            # An invitation with this code already exists, try again
932
        except Invitation.DoesNotExist:
933
            return code
934

    
935

    
936
def get_latest_terms():
937
    try:
938
        term = ApprovalTerms.objects.order_by('-id')[0]
939
        return term
940
    except IndexError:
941
        pass
942
    return None
943

    
944
class PendingThirdPartyUser(models.Model):
945
    """
946
    Model for registring successful third party user authentications
947
    """
948
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
949
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
950
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
951
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
952
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
953
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
954
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
955
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
956
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
957
    info = models.TextField(default="", null=True, blank=True)
958

    
959
    class Meta:
960
        unique_together = ("provider", "third_party_identifier")
961

    
962
    def get_user_instance(self):
963
        d = self.__dict__
964
        d.pop('_state', None)
965
        d.pop('id', None)
966
        d.pop('token', None)
967
        d.pop('created', None)
968
        d.pop('info', None)
969
        user = AstakosUser(**d)
970

    
971
        return user
972

    
973
    @property
974
    def realname(self):
975
        return '%s %s' %(self.first_name, self.last_name)
976

    
977
    @realname.setter
978
    def realname(self, value):
979
        parts = value.split(' ')
980
        if len(parts) == 2:
981
            self.first_name = parts[0]
982
            self.last_name = parts[1]
983
        else:
984
            self.last_name = parts[0]
985

    
986
    def save(self, **kwargs):
987
        if not self.id:
988
            # set username
989
            while not self.username:
990
                username =  uuid.uuid4().hex[:30]
991
                try:
992
                    AstakosUser.objects.get(username = username)
993
                except AstakosUser.DoesNotExist, e:
994
                    self.username = username
995
        super(PendingThirdPartyUser, self).save(**kwargs)
996

    
997
    def generate_token(self):
998
        self.password = self.third_party_identifier
999
        self.last_login = datetime.now()
1000
        self.token = default_token_generator.make_token(self)
1001

    
1002
class SessionCatalog(models.Model):
1003
    session_key = models.CharField(_('session key'), max_length=40)
1004
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1005

    
1006

    
1007
### PROJECTS ###
1008
################
1009

    
1010
def synced_model_metaclass(class_name, class_parents, class_attributes):
1011

    
1012
    new_attributes = {}
1013
    sync_attributes = {}
1014

    
1015
    for name, value in class_attributes.iteritems():
1016
        sync, underscore, rest = name.partition('_')
1017
        if sync == 'sync' and underscore == '_':
1018
            sync_attributes[rest] = value
1019
        else:
1020
            new_attributes[name] = value
1021

    
1022
    if 'prefix' not in sync_attributes:
1023
        m = ("you did not specify a 'sync_prefix' attribute "
1024
             "in class '%s'" % (class_name,))
1025
        raise ValueError(m)
1026

    
1027
    prefix = sync_attributes.pop('prefix')
1028
    class_name = sync_attributes.pop('classname', prefix + '_model')
1029

    
1030
    for name, value in sync_attributes.iteritems():
1031
        newname = prefix + '_' + name
1032
        if newname in new_attributes:
1033
            m = ("class '%s' was specified with prefix '%s' "
1034
                 "but it already has an attribute named '%s'"
1035
                 % (class_name, prefix, newname))
1036
            raise ValueError(m)
1037

    
1038
        new_attributes[newname] = value
1039

    
1040
    newclass = type(class_name, class_parents, new_attributes)
1041
    return newclass
1042

    
1043

    
1044
def make_synced(prefix='sync', name='SyncedState'):
1045

    
1046
    the_name = name
1047
    the_prefix = prefix
1048

    
1049
    class SyncedState(models.Model):
1050

    
1051
        sync_classname      = the_name
1052
        sync_prefix         = the_prefix
1053
        __metaclass__       = synced_model_metaclass
1054

    
1055
        sync_new_state      = models.BigIntegerField(null=True)
1056
        sync_synced_state   = models.BigIntegerField(null=True)
1057
        STATUS_SYNCED       = 0
1058
        STATUS_PENDING      = 1
1059
        sync_status         = models.IntegerField(db_index=True)
1060

    
1061
        class Meta:
1062
            abstract = True
1063

    
1064
        class NotSynced(Exception):
1065
            pass
1066

    
1067
        def sync_init_state(self, state):
1068
            self.sync_synced_state = state
1069
            self.sync_new_state = state
1070
            self.sync_status = self.STATUS_SYNCED
1071

    
1072
        def sync_get_status(self):
1073
            return self.sync_status
1074

    
1075
        def sync_set_status(self):
1076
            if self.sync_new_state != self.sync_synced_state:
1077
                self.sync_status = self.STATUS_PENDING
1078
            else:
1079
                self.sync_status = self.STATUS_SYNCED
1080

    
1081
        def sync_set_synced(self):
1082
            self.sync_synced_state = self.sync_new_state
1083
            self.sync_status = self.STATUS_SYNCED
1084

    
1085
        def sync_get_synced_state(self):
1086
            return self.sync_synced_state
1087

    
1088
        def sync_set_new_state(self, new_state):
1089
            self.sync_new_state = new_state
1090
            self.sync_set_status()
1091

    
1092
        def sync_get_new_state(self):
1093
            return self.sync_new_state
1094

    
1095
        def sync_set_synced_state(self, synced_state):
1096
            self.sync_synced_state = synced_state
1097
            self.sync_set_status()
1098

    
1099
        def sync_get_pending_objects(self):
1100
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1101
            return self.objects.filter(**kw)
1102

    
1103
        def sync_get_synced_objects(self):
1104
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1105
            return self.objects.filter(**kw)
1106

    
1107
        def sync_verify_get_synced_state(self):
1108
            status = self.sync_get_status()
1109
            state = self.sync_get_synced_state()
1110
            verified = (status == self.STATUS_SYNCED)
1111
            return state, verified
1112

    
1113
        def sync_is_synced(self):
1114
            state, verified = self.sync_verify_get_synced_state()
1115
            return verified
1116

    
1117
    return SyncedState
1118

    
1119
SyncedState = make_synced(prefix='sync', name='SyncedState')
1120

    
1121

    
1122
class ProjectApplicationManager(ForUpdateManager):
1123

    
1124
    def user_projects(self, user):
1125
        """
1126
        Return projects accessed by specified user.
1127
        """
1128
        return self.filter(Q(owner=user) | Q(applicant=user) | \
1129
                        Q(project__projectmembership__person=user)).order_by('pk').distinct()
1130

    
1131
    def search_by_name(self, *search_strings):
1132
        q = Q()
1133
        for s in search_strings:
1134
            q = q | Q(name__icontains=s)
1135
        return self.filter(q)
1136

    
1137

    
1138
class ProjectApplication(models.Model):
1139
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1140
    applicant               =   models.ForeignKey(
1141
                                    AstakosUser,
1142
                                    related_name='projects_applied',
1143
                                    db_index=True)
1144

    
1145
    state                   =   models.CharField(max_length=80,
1146
                                                default=UNKNOWN)
1147

    
1148
    owner                   =   models.ForeignKey(
1149
                                    AstakosUser,
1150
                                    related_name='projects_owned',
1151
                                    db_index=True)
1152

    
1153
    precursor_application   =   models.OneToOneField('ProjectApplication',
1154
                                                     null=True,
1155
                                                     blank=True,
1156
                                                     db_index=True)
1157

    
1158
    name                    =   models.CharField(max_length=80)
1159
    homepage                =   models.URLField(max_length=255, null=True)
1160
    description             =   models.TextField(null=True, blank=True)
1161
    start_date              =   models.DateTimeField()
1162
    end_date                =   models.DateTimeField()
1163
    member_join_policy      =   models.IntegerField()
1164
    member_leave_policy     =   models.IntegerField()
1165
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1166
    resource_grants         =   models.ManyToManyField(
1167
                                    Resource,
1168
                                    null=True,
1169
                                    blank=True,
1170
                                    through='ProjectResourceGrant')
1171
    comments                =   models.TextField(null=True, blank=True)
1172
    issue_date              =   models.DateTimeField()
1173

    
1174

    
1175
    objects                 =   ProjectApplicationManager()
1176

    
1177
    def __unicode__(self):
1178
        return "%s applied by %s" % (self.name, self.applicant)
1179

    
1180
    def add_resource_policy(self, service, resource, uplimit):
1181
        """Raises ObjectDoesNotExist, IntegrityError"""
1182
        q = self.projectresourcegrant_set
1183
        resource = Resource.objects.get(service__name=service, name=resource)
1184
        q.create(resource=resource, member_capacity=uplimit)
1185

    
1186
    def user_status(self, user):
1187
        """
1188
        100 OWNER
1189
        0   REQUESTED
1190
        1   PENDING
1191
        2   ACCEPTED
1192
        3   REMOVING
1193
        4   REMOVED
1194
       -1   User has no association with the project
1195
        """
1196
        if user == self.owner:
1197
            status = 100
1198
        else:
1199
            try:
1200
                membership = self.project.projectmembership_set.get(person=user)
1201
                status = membership.state
1202
            except Project.DoesNotExist:
1203
                status = -1
1204
            except ProjectMembership.DoesNotExist:
1205
                status = -1
1206

    
1207
        return status
1208

    
1209
    def members_count(self):
1210
        return self.project.approved_memberships.count()
1211

    
1212
    @property
1213
    def grants(self):
1214
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1215

    
1216
    @property
1217
    def resource_policies(self):
1218
        return self.projectresourcegrant_set.all()
1219

    
1220
    @resource_policies.setter
1221
    def resource_policies(self, policies):
1222
        for p in policies:
1223
            service = p.get('service', None)
1224
            resource = p.get('resource', None)
1225
            uplimit = p.get('uplimit', 0)
1226
            self.add_resource_policy(service, resource, uplimit)
1227

    
1228
    @property
1229
    def follower(self):
1230
        try:
1231
            return ProjectApplication.objects.get(precursor_application=self)
1232
        except ProjectApplication.DoesNotExist:
1233
            return
1234

    
1235
    def submit(self, resource_policies, applicant, comments,
1236
               precursor_application=None):
1237

    
1238
        if precursor_application:
1239
            self.precursor_application = precursor_application
1240
            self.owner = precursor_application.owner
1241
        else:
1242
            self.owner = applicant
1243

    
1244
        self.id = None
1245
        self.applicant = applicant
1246
        self.comments = comments
1247
        self.issue_date = datetime.now()
1248
        self.state = self.PENDING
1249
        self.save()
1250
        self.resource_policies = resource_policies
1251

    
1252
    def _get_project(self):
1253
        precursor = self
1254
        while precursor:
1255
            try:
1256
                objects = Project.objects.select_for_update()
1257
                project = objects.get(application=precursor)
1258
                return project
1259
            except Project.DoesNotExist:
1260
                pass
1261
            precursor = precursor.precursor_application
1262

    
1263
        return None
1264

    
1265
    def approve(self, approval_user=None):
1266
        """
1267
        If approval_user then during owner membership acceptance
1268
        it is checked whether the request_user is eligible.
1269

1270
        Raises:
1271
            PermissionDenied
1272
        """
1273

    
1274
        if not transaction.is_managed():
1275
            raise AssertionError("NOPE")
1276

    
1277
        new_project_name = self.name
1278
        if self.state != self.PENDING:
1279
            m = _("cannot approve: project '%s' in state '%s'") % (
1280
                    new_project_name, self.state)
1281
            raise PermissionDenied(m) # invalid argument
1282

    
1283
        now = datetime.now()
1284
        project = self._get_project()
1285

    
1286
        try:
1287
            # needs SERIALIZABLE
1288
            conflicting_project = Project.objects.get(name=new_project_name)
1289
            if (conflicting_project.is_alive and
1290
                conflicting_project != project):
1291
                m = (_("cannot approve: project with name '%s' "
1292
                       "already exists (serial: %s)") % (
1293
                        new_project_name, conflicting_project.id))
1294
                raise PermissionDenied(m) # invalid argument
1295
        except Project.DoesNotExist:
1296
            pass
1297

    
1298
        new_project = False
1299
        if project is None:
1300
            new_project = True
1301
            project = Project(creation_date=now)
1302

    
1303
        project.name = new_project_name
1304
        project.application = self
1305
        project.last_approval_date = now
1306
        project.save()
1307

    
1308
        if new_project:
1309
            project.add_member(self.owner)
1310

    
1311
        # This will block while syncing,
1312
        # but unblock before setting the membership state.
1313
        # See ProjectMembership.set_sync()
1314
        project.set_membership_pending_sync()
1315

    
1316
        precursor = self.precursor_application
1317
        while precursor:
1318
            precursor.state = self.REPLACED
1319
            precursor.save()
1320
            precursor = precursor.precursor_application
1321

    
1322
        self.state = self.APPROVED
1323
        self.save()
1324

    
1325

    
1326
class ProjectResourceGrant(models.Model):
1327

    
1328
    resource                =   models.ForeignKey(Resource)
1329
    project_application     =   models.ForeignKey(ProjectApplication,
1330
                                                  null=True)
1331
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1332
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1333
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1334
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1335
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1336
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1337

    
1338
    objects = ExtendedManager()
1339

    
1340
    class Meta:
1341
        unique_together = ("resource", "project_application")
1342

    
1343

    
1344
class Project(models.Model):
1345

    
1346
    application                 =   models.OneToOneField(
1347
                                            ProjectApplication,
1348
                                            related_name='project')
1349
    last_approval_date          =   models.DateTimeField(null=True)
1350

    
1351
    members                     =   models.ManyToManyField(
1352
                                            AstakosUser,
1353
                                            through='ProjectMembership')
1354

    
1355
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1356
    deactivation_start_date     =   models.DateTimeField(null=True)
1357
    deactivation_date           =   models.DateTimeField(null=True)
1358

    
1359
    creation_date               =   models.DateTimeField()
1360
    name                        =   models.CharField(
1361
                                            max_length=80,
1362
                                            db_index=True,
1363
                                            unique=True)
1364

    
1365
    TERMINATED  =   'TERMINATED'
1366
    SUSPENDED   =   'SUSPENDED'
1367

    
1368
    objects     =   ForUpdateManager()
1369

    
1370
    def __str__(self):
1371
        return _("<project %s '%s'>") % (self.id, self.application.name)
1372

    
1373
    __repr__ = __str__
1374

    
1375
    def is_deactivating(self):
1376
        return bool(self.deactivation_start_date)
1377

    
1378
    def is_deactivated_synced(self):
1379
        return bool(self.deactivation_date)
1380

    
1381
    def is_deactivated(self):
1382
        return self.is_deactivated_synced() or self.is_deactivating()
1383

    
1384
    def is_still_approved(self):
1385
        return bool(self.last_approval_date)
1386

    
1387
    def is_active(self):
1388
        return not(self.is_deactivated())
1389

    
1390
    def is_inconsistent(self):
1391
        now = datetime.now()
1392
        dates = [self.creation_date,
1393
                 self.last_approval_date,
1394
                 self.deactivation_start_date,
1395
                 self.deactivation_date]
1396
        return any([date > now for date in dates])
1397

    
1398
    def set_deactivation_start_date(self):
1399
        self.deactivation_start_date = datetime.now()
1400

    
1401
    def set_deactivation_date(self):
1402
        self.deactivation_start_date = None
1403
        self.deactivation_date = datetime.now()
1404

    
1405
    def violates_resource_grants(self):
1406
        return False
1407

    
1408
    def violates_members_limit(self, adding=0):
1409
        application = self.application
1410
        return (len(self.approved_members) + adding >
1411
                application.limit_on_members_number)
1412

    
1413
    @property
1414
    def is_alive(self):
1415
        return self.is_active()
1416

    
1417
    @property
1418
    def approved_memberships(self):
1419
        query = ProjectMembership.query_approved()
1420
        return self.projectmembership_set.filter(query)
1421

    
1422
    @property
1423
    def approved_members(self):
1424
        return [m.person for m in self.approved_memberships]
1425

    
1426
    def set_membership_pending_sync(self):
1427
        query = ProjectMembership.query_approved()
1428
        sfu = self.projectmembership_set.select_for_update()
1429
        members = sfu.filter(query)
1430

    
1431
        for member in members:
1432
            member.state = member.PENDING
1433
            member.save()
1434

    
1435
    def add_member(self, user):
1436
        """
1437
        Raises:
1438
            django.exceptions.PermissionDenied
1439
            astakos.im.models.AstakosUser.DoesNotExist
1440
        """
1441
        if isinstance(user, int):
1442
            user = AstakosUser.objects.get(user=user)
1443

    
1444
        m, created = ProjectMembership.objects.get_or_create(
1445
            person=user, project=self
1446
        )
1447
        m.accept()
1448

    
1449
    def remove_member(self, user):
1450
        """
1451
        Raises:
1452
            django.exceptions.PermissionDenied
1453
            astakos.im.models.AstakosUser.DoesNotExist
1454
            astakos.im.models.ProjectMembership.DoesNotExist
1455
        """
1456
        if isinstance(user, int):
1457
            user = AstakosUser.objects.get(user=user)
1458

    
1459
        m = ProjectMembership.objects.get(person=user, project=self)
1460
        m.remove()
1461

    
1462
    def terminate(self):
1463
        self.set_deactivation_start_date()
1464
        self.deactivation_reason = self.TERMINATED
1465
        self.save()
1466

    
1467
    @property
1468
    def is_terminated(self):
1469
        return (self.is_deactivated() and
1470
                self.deactivation_reason == self.TERMINATED)
1471

    
1472
    @property
1473
    def is_suspended(self):
1474
        return False
1475

    
1476
class ProjectMembership(models.Model):
1477

    
1478
    person              =   models.ForeignKey(AstakosUser)
1479
    request_date        =   models.DateField(default=datetime.now())
1480
    project             =   models.ForeignKey(Project)
1481

    
1482
    state               =   models.IntegerField(default=0)
1483
    application         =   models.ForeignKey(
1484
                                ProjectApplication,
1485
                                null=True,
1486
                                related_name='memberships')
1487
    pending_application =   models.ForeignKey(
1488
                                ProjectApplication,
1489
                                null=True,
1490
                                related_name='pending_memebrships')
1491
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1492

    
1493
    acceptance_date     =   models.DateField(null=True, db_index=True)
1494
    leave_request_date  =   models.DateField(null=True)
1495

    
1496
    objects     =   ForUpdateManager()
1497

    
1498
    REQUESTED   =   0
1499
    PENDING     =   1
1500
    ACCEPTED    =   2
1501
    REMOVING    =   3
1502
    REMOVED     =   4
1503
    INACTIVE    =   5
1504

    
1505
    APPROVED_SET    =   [PENDING, ACCEPTED, INACTIVE]
1506

    
1507
    @classmethod
1508
    def query_approved(cls):
1509
        return (Q(state=cls.PENDING) |
1510
                Q(state=cls.ACCEPTED) |
1511
                Q(state=cls.INACTIVE))
1512

    
1513
    class Meta:
1514
        unique_together = ("person", "project")
1515
        #index_together = [["project", "state"]]
1516

    
1517
    def __str__(self):
1518
        return _("<'%s' membership in '%s'>") % (
1519
                self.person.username, self.project)
1520

    
1521
    __repr__ = __str__
1522

    
1523
    def __init__(self, *args, **kwargs):
1524
        self.state = self.REQUESTED
1525
        super(ProjectMembership, self).__init__(*args, **kwargs)
1526

    
1527
    def _set_history_item(self, reason, date=None):
1528
        if isinstance(reason, basestring):
1529
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1530

    
1531
        history_item = ProjectMembershipHistory(
1532
                            serial=self.id,
1533
                            person=self.person.uuid,
1534
                            project=self.project_id,
1535
                            date=date or datetime.now(),
1536
                            reason=reason)
1537
        history_item.save()
1538
        serial = history_item.id
1539

    
1540
    def accept(self):
1541
        state = self.state
1542
        if state != self.REQUESTED:
1543
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1544
            raise AssertionError(m)
1545

    
1546
        now = datetime.now()
1547
        self.acceptance_date = now
1548
        self._set_history_item(reason='ACCEPT', date=now)
1549
        self.state = (self.PENDING if self.project.is_active()
1550
                      else self.INACTIVE)
1551
        self.save()
1552

    
1553
    def remove(self):
1554
        state = self.state
1555
        if state not in [self.ACCEPTED, self.INACTIVE]:
1556
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1557
            raise AssertionError(m)
1558

    
1559
        self._set_history_item(reason='REMOVE')
1560
        self.state = self.REMOVING
1561
        self.save()
1562

    
1563
    def reject(self):
1564
        state = self.state
1565
        if state != self.REQUESTED:
1566
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1567
            raise AssertionError(m)
1568

    
1569
        # rejected requests don't need sync,
1570
        # because they were never effected
1571
        self._set_history_item(reason='REJECT')
1572
        self.delete()
1573

    
1574
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1575
        if sub_list is None:
1576
            sub_list = []
1577

    
1578
        if add_list is None:
1579
            add_list = []
1580

    
1581
        sub_append = sub_list.append
1582
        add_append = add_list.append
1583
        holder = self.person.uuid
1584

    
1585
        synced_application = self.application
1586
        if synced_application is not None:
1587
            cur_grants = synced_application.projectresourcegrant_set.all()
1588
            for grant in cur_grants:
1589
                sub_append(QuotaLimits(
1590
                               holder       = holder,
1591
                               resource     = str(grant.resource),
1592
                               capacity     = grant.member_capacity,
1593
                               import_limit = grant.member_import_limit,
1594
                               export_limit = grant.member_export_limit))
1595

    
1596
        if not remove:
1597
            new_grants = self.pending_application.projectresourcegrant_set.all()
1598
            for new_grant in new_grants:
1599
                add_append(QuotaLimits(
1600
                               holder       = holder,
1601
                               resource     = str(new_grant.resource),
1602
                               capacity     = new_grant.member_capacity,
1603
                               import_limit = new_grant.member_import_limit,
1604
                               export_limit = new_grant.member_export_limit))
1605

    
1606
        return (sub_list, add_list)
1607

    
1608
    def set_sync(self):
1609
        state = self.state
1610
        if state == self.PENDING:
1611
            pending_application = self.pending_application
1612
            if pending_application is None:
1613
                m = _("%s: attempt to sync an empty pending application") % (
1614
                    self,)
1615
                raise AssertionError(m)
1616
            self.application = pending_application
1617
            self.pending_application = None
1618
            self.pending_serial = None
1619

    
1620
            # project.application may have changed in the meantime,
1621
            # in which case we stay PENDING;
1622
            # we are safe to check due to select_for_update
1623
            if self.application == self.project.application:
1624
                self.state = self.ACCEPTED
1625
            self.save()
1626
        elif state == self.ACCEPTED:
1627
            if self.pending_application:
1628
                m = _("%s: attempt to sync in state '%s' "
1629
                      "with a pending application") % (self, state)
1630
                raise AssertionError(m)
1631
            self.application = None
1632
            self.pending_serial = None
1633
            self.state = self.INACTIVE
1634
            self.save()
1635
        elif state == self.REMOVING:
1636
            self.delete()
1637
        else:
1638
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1639
            raise AssertionError(m)
1640

    
1641
    def reset_sync(self):
1642
        state = self.state
1643
        if state in [self.PENDING, self.ACCEPTED, self.REMOVING]:
1644
            self.pending_application = None
1645
            self.pending_serial = None
1646
            self.save()
1647
        else:
1648
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1649
            raise AssertionError(m)
1650

    
1651
class Serial(models.Model):
1652
    serial  =   models.AutoField(primary_key=True)
1653

    
1654
def new_serial():
1655
    s = Serial.objects.create()
1656
    serial = s.serial
1657
    s.delete()
1658
    return serial
1659

    
1660
def sync_finish_serials(serials_to_ack=None):
1661
    if serials_to_ack is None:
1662
        serials_to_ack = qh_query_serials([])
1663

    
1664
    serials_to_ack = set(serials_to_ack)
1665
    sfu = ProjectMembership.objects.select_for_update()
1666
    memberships = list(sfu.filter(pending_serial__isnull=False))
1667

    
1668
    if memberships:
1669
        for membership in memberships:
1670
            serial = membership.pending_serial
1671
            if serial in serials_to_ack:
1672
                membership.set_sync()
1673
            else:
1674
                membership.reset_sync()
1675

    
1676
        transaction.commit()
1677

    
1678
    qh_ack_serials(list(serials_to_ack))
1679
    return len(memberships)
1680

    
1681
def sync_all_projects():
1682
    sync_finish_serials()
1683

    
1684
    PENDING = ProjectMembership.PENDING
1685
    REMOVING = ProjectMembership.REMOVING
1686
    objects = ProjectMembership.objects.select_for_update()
1687

    
1688
    sub_quota, add_quota = [], []
1689

    
1690
    serial = new_serial()
1691

    
1692
    pending = objects.filter(state=PENDING)
1693
    for membership in pending:
1694

    
1695
        if membership.pending_application:
1696
            m = "%s: impossible: pending_application is not None (%s)" % (
1697
                membership, membership.pending_application)
1698
            raise AssertionError(m)
1699
        if membership.pending_serial:
1700
            m = "%s: impossible: pending_serial is not None (%s)" % (
1701
                membership, membership.pending_serial)
1702
            raise AssertionError(m)
1703

    
1704
        membership.pending_application = membership.project.application
1705
        membership.pending_serial = serial
1706
        membership.get_diff_quotas(sub_quota, add_quota)
1707
        membership.save()
1708

    
1709
    removing = objects.filter(state=REMOVING)
1710
    for membership in removing:
1711

    
1712
        if membership.pending_application:
1713
            m = ("%s: impossible: removing pending_application is not None (%s)"
1714
                % (membership, membership.pending_application))
1715
            raise AssertionError(m)
1716
        if membership.pending_serial:
1717
            m = "%s: impossible: pending_serial is not None (%s)" % (
1718
                membership, membership.pending_serial)
1719
            raise AssertionError(m)
1720

    
1721
        membership.pending_serial = serial
1722
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1723
        membership.save()
1724

    
1725
    transaction.commit()
1726
    # ProjectApplication.approve() unblocks here
1727
    # and can set PENDING an already PENDING membership
1728
    # which has been scheduled to sync with the old project.application
1729
    # Need to check in ProjectMembership.set_sync()
1730

    
1731
    r = qh_add_quota(serial, sub_quota, add_quota)
1732
    if r:
1733
        m = "cannot sync serial: %d" % serial
1734
        raise RuntimeError(m)
1735

    
1736
    sync_finish_serials([serial])
1737

    
1738
def sync_deactivating_projects():
1739

    
1740
    ACCEPTED = ProjectMembership.ACCEPTED
1741
    PENDING = ProjectMembership.PENDING
1742
    REMOVING = ProjectMembership.REMOVING
1743

    
1744
    psfu = Project.objects.select_for_update()
1745
    projects = psfu.filter(deactivation_start_date__isnull=False)
1746

    
1747
    if not projects:
1748
        return
1749

    
1750
    sub_quota, add_quota = [], []
1751

    
1752
    serial = new_serial()
1753

    
1754
    for project in projects:
1755
        objects = project.projectmembership_set.select_for_update()
1756
        memberships = objects.filter(Q(state=ACCEPTED) |
1757
                                     Q(state=PENDING) | Q(state=REMOVING))
1758
        for membership in memberships:
1759
            if membership.state in (PENDING, REMOVING):
1760
                m = "cannot sync deactivating project '%s'" % project
1761
                raise RuntimeError(m)
1762

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

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

    
1777
    transaction.commit()
1778

    
1779
    r = qh_add_quota(serial, sub_quota, add_quota)
1780
    if r:
1781
        m = "cannot sync serial: %d" % serial
1782
        raise RuntimeError(m)
1783

    
1784
    sync_finish_serials([serial])
1785

    
1786
    # finalize deactivating projects
1787
    deactivating_projects = psfu.filter(deactivation_start_date__isnull=False)
1788
    for project in deactivating_projects:
1789
        objects = project.projectmembership_set.select_for_update()
1790
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1791
                                          Q(state=PENDING) | Q(state=REMOVING)))
1792
        if not memberships:
1793
            project.set_deactivation_date()
1794
            project.save()
1795

    
1796
    transaction.commit()
1797

    
1798
def sync_projects():
1799
    sync_all_projects()
1800
    sync_deactivating_projects()
1801

    
1802
def trigger_sync(retries=3, retry_wait=1.0):
1803
    transaction.commit()
1804

    
1805
    cursor = connection.cursor()
1806
    locked = True
1807
    try:
1808
        while 1:
1809
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1810
            r = cursor.fetchone()
1811
            if r is None:
1812
                m = "Impossible"
1813
                raise AssertionError(m)
1814
            locked = r[0]
1815
            if locked:
1816
                break
1817

    
1818
            retries -= 1
1819
            if retries <= 0:
1820
                return False
1821
            sleep(retry_wait)
1822

    
1823
        sync_projects()
1824
        return True
1825

    
1826
    finally:
1827
        if locked:
1828
            cursor.execute("SELECT pg_advisory_unlock(1)")
1829
            cursor.fetchall()
1830

    
1831

    
1832
class ProjectMembershipHistory(models.Model):
1833
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1834
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1835

    
1836
    person  =   models.CharField(max_length=255)
1837
    project =   models.BigIntegerField()
1838
    date    =   models.DateField(default=datetime.now)
1839
    reason  =   models.IntegerField()
1840
    serial  =   models.BigIntegerField()
1841

    
1842
### SIGNALS ###
1843
################
1844

    
1845
def create_astakos_user(u):
1846
    try:
1847
        AstakosUser.objects.get(user_ptr=u.pk)
1848
    except AstakosUser.DoesNotExist:
1849
        extended_user = AstakosUser(user_ptr_id=u.pk)
1850
        extended_user.__dict__.update(u.__dict__)
1851
        extended_user.save()
1852
        if not extended_user.has_auth_provider('local'):
1853
            extended_user.add_auth_provider('local')
1854
    except BaseException, e:
1855
        logger.exception(e)
1856

    
1857

    
1858
def fix_superusers(sender, **kwargs):
1859
    # Associate superusers with AstakosUser
1860
    admins = User.objects.filter(is_superuser=True)
1861
    for u in admins:
1862
        create_astakos_user(u)
1863
post_syncdb.connect(fix_superusers)
1864

    
1865

    
1866
def user_post_save(sender, instance, created, **kwargs):
1867
    if not created:
1868
        return
1869
    create_astakos_user(instance)
1870
post_save.connect(user_post_save, sender=User)
1871

    
1872
def astakosuser_post_save(sender, instance, created, **kwargs):
1873
    if not created:
1874
        return
1875
    # TODO handle socket.error & IOError
1876
    register_users((instance,))
1877
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1878

    
1879
def resource_post_save(sender, instance, created, **kwargs):
1880
    if not created:
1881
        return
1882
    register_resources((instance,))
1883
post_save.connect(resource_post_save, sender=Resource)
1884

    
1885
def renew_token(sender, instance, **kwargs):
1886
    if not instance.auth_token:
1887
        instance.renew_token()
1888
pre_save.connect(renew_token, sender=AstakosUser)
1889
pre_save.connect(renew_token, sender=Service)
1890