Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 69c822cc

History | View | Annotate | Download (65.9 kB)

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

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

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
70
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA)
71
from astakos.im import settings as astakos_settings
72
from astakos.im.endpoints.qh import (
73
    register_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.owner == self
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
        participates_fitlers = Q(owner=user) | Q(applicant=user) | \
1129
                               Q(project__projectmembership__person=user)
1130
        state_filters = (Q(state=ProjectApplication.PENDING) & \
1131
                        Q(precursor_application__isnull=True)) | \
1132
                        Q(state=ProjectApplication.APPROVED)
1133
        return self.filter(participates_fitlers & state_filters).order_by('issue_date').distinct()
1134

    
1135
    def search_by_name(self, *search_strings):
1136
        q = Q()
1137
        for s in search_strings:
1138
            q = q | Q(name__icontains=s)
1139
        return self.filter(q)
1140

    
1141

    
1142
PROJECT_STATE_DISPLAY = {
1143
    'Pending': _('Pending review'),
1144
    'Approved': _('Active'),
1145
    'Replaced': _('Replaced'),
1146
    'Unknown': _('Unknown')
1147
}
1148

    
1149
USER_STATUS_DISPLAY = {
1150
    100: _('Owner'),
1151
      0: _('Join requested'),
1152
      1: _('Pending'),
1153
      2: _('Accepted member'),
1154
      3: _('Removing'),
1155
      4: _('Removed'),
1156
     -1: _('Not a member'),
1157
}
1158

    
1159
class ProjectApplication(models.Model):
1160
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1161
    applicant               =   models.ForeignKey(
1162
                                    AstakosUser,
1163
                                    related_name='projects_applied',
1164
                                    db_index=True)
1165

    
1166
    state                   =   models.CharField(max_length=80,
1167
                                                default=PENDING)
1168

    
1169
    owner                   =   models.ForeignKey(
1170
                                    AstakosUser,
1171
                                    related_name='projects_owned',
1172
                                    db_index=True)
1173

    
1174
    precursor_application   =   models.OneToOneField('ProjectApplication',
1175
                                                     null=True,
1176
                                                     blank=True,
1177
                                                     db_index=True)
1178

    
1179
    name                    =   models.CharField(max_length=80)
1180
    homepage                =   models.URLField(max_length=255, null=True)
1181
    description             =   models.TextField(null=True, blank=True)
1182
    start_date              =   models.DateTimeField(null=True, blank=True)
1183
    end_date                =   models.DateTimeField()
1184
    member_join_policy      =   models.IntegerField()
1185
    member_leave_policy     =   models.IntegerField()
1186
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1187
    resource_grants         =   models.ManyToManyField(
1188
                                    Resource,
1189
                                    null=True,
1190
                                    blank=True,
1191
                                    through='ProjectResourceGrant')
1192
    comments                =   models.TextField(null=True, blank=True)
1193
    issue_date              =   models.DateTimeField(default=datetime.now)
1194

    
1195

    
1196
    objects                 =   ProjectApplicationManager()
1197

    
1198
    def __unicode__(self):
1199
        return "%s applied by %s" % (self.name, self.applicant)
1200

    
1201
    def state_display(self):
1202
        return PROJECT_STATE_DISPLAY.get(self.state, _('Unknown'))
1203

    
1204
    def add_resource_policy(self, service, resource, uplimit):
1205
        """Raises ObjectDoesNotExist, IntegrityError"""
1206
        q = self.projectresourcegrant_set
1207
        resource = Resource.objects.get(service__name=service, name=resource)
1208
        q.create(resource=resource, member_capacity=uplimit)
1209

    
1210
    def user_status(self, user):
1211
        """
1212
        100 OWNER
1213
        0   REQUESTED
1214
        1   PENDING
1215
        2   ACCEPTED
1216
        3   REMOVING
1217
        4   REMOVED
1218
       -1   User has no association with the project
1219
        """
1220
        try:
1221
            membership = self.project.projectmembership_set.get(person=user)
1222
            status = membership.state
1223
        except Project.DoesNotExist:
1224
            status = -1
1225
        except ProjectMembership.DoesNotExist:
1226
            status = -1
1227

    
1228
        return status
1229

    
1230
    def user_status_display(self, user):
1231
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1232

    
1233
    def members_count(self):
1234
        return self.project.approved_memberships.count()
1235

    
1236
    @property
1237
    def grants(self):
1238
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1239

    
1240
    @property
1241
    def resource_policies(self):
1242
        return self.projectresourcegrant_set.all()
1243

    
1244
    @resource_policies.setter
1245
    def resource_policies(self, policies):
1246
        for p in policies:
1247
            service = p.get('service', None)
1248
            resource = p.get('resource', None)
1249
            uplimit = p.get('uplimit', 0)
1250
            self.add_resource_policy(service, resource, uplimit)
1251

    
1252
    @property
1253
    def follower(self):
1254
        try:
1255
            return ProjectApplication.objects.get(precursor_application=self)
1256
        except ProjectApplication.DoesNotExist:
1257
            return
1258

    
1259
    def followers(self):
1260
        current = self
1261
        try:
1262
            while current.projectapplication:
1263
                yield current.follower
1264
                current = current.follower
1265
        except:
1266
            pass
1267

    
1268
    def last_follower(self):
1269
        try:
1270
            return list(self.followers())[-1]
1271
        except IndexError:
1272
            return None
1273

    
1274
    def submit(self, resource_policies, applicant, comments,
1275
               precursor_application=None):
1276

    
1277
        if precursor_application:
1278
            self.precursor_application = precursor_application
1279
            self.owner = precursor_application.owner
1280
        else:
1281
            self.owner = applicant
1282

    
1283
        self.id = None
1284
        self.applicant = applicant
1285
        self.comments = comments
1286
        self.issue_date = datetime.now()
1287
        self.state = self.PENDING
1288
        self.save()
1289
        self.resource_policies = resource_policies
1290

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

    
1302
        return None
1303

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

1309
        Raises:
1310
            PermissionDenied
1311
        """
1312

    
1313
        if not transaction.is_managed():
1314
            raise AssertionError("NOPE")
1315

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

    
1322
        now = datetime.now()
1323
        project = self._get_project()
1324

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

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

    
1342
        project.name = new_project_name
1343
        project.application = self
1344
        project.last_approval_date = now
1345
        project.save()
1346

    
1347
        if new_project:
1348
            project.add_member(self.owner)
1349

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

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

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

    
1364

    
1365
class ProjectResourceGrant(models.Model):
1366

    
1367
    resource                =   models.ForeignKey(Resource)
1368
    project_application     =   models.ForeignKey(ProjectApplication,
1369
                                                  null=True)
1370
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1371
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1372
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1373
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1374
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1375
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1376

    
1377
    objects = ExtendedManager()
1378

    
1379
    class Meta:
1380
        unique_together = ("resource", "project_application")
1381

    
1382

    
1383
class Project(models.Model):
1384

    
1385
    application                 =   models.OneToOneField(
1386
                                            ProjectApplication,
1387
                                            related_name='project')
1388
    last_approval_date          =   models.DateTimeField(null=True)
1389

    
1390
    members                     =   models.ManyToManyField(
1391
                                            AstakosUser,
1392
                                            through='ProjectMembership')
1393

    
1394
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1395
    deactivation_start_date     =   models.DateTimeField(null=True)
1396
    deactivation_date           =   models.DateTimeField(null=True)
1397

    
1398
    creation_date               =   models.DateTimeField()
1399
    name                        =   models.CharField(
1400
                                            max_length=80,
1401
                                            db_index=True,
1402
                                            unique=True)
1403

    
1404
    TERMINATED  =   'TERMINATED'
1405
    SUSPENDED   =   'SUSPENDED'
1406

    
1407
    objects     =   ForUpdateManager()
1408

    
1409
    def __str__(self):
1410
        return _("<project %s '%s'>") % (self.id, self.application.name)
1411

    
1412
    __repr__ = __str__
1413

    
1414
    def is_deactivating(self):
1415
        return bool(self.deactivation_start_date)
1416

    
1417
    def is_deactivated_synced(self):
1418
        return bool(self.deactivation_date)
1419

    
1420
    def is_deactivated(self):
1421
        return self.is_deactivated_synced() or self.is_deactivating()
1422

    
1423
    def is_still_approved(self):
1424
        return bool(self.last_approval_date)
1425

    
1426
    def is_active(self):
1427
        return not(self.is_deactivated())
1428

    
1429
    def is_inconsistent(self):
1430
        now = datetime.now()
1431
        dates = [self.creation_date,
1432
                 self.last_approval_date,
1433
                 self.deactivation_start_date,
1434
                 self.deactivation_date]
1435
        return any([date > now for date in dates])
1436

    
1437
    def set_deactivation_start_date(self):
1438
        self.deactivation_start_date = datetime.now()
1439

    
1440
    def set_deactivation_date(self):
1441
        self.deactivation_start_date = None
1442
        self.deactivation_date = datetime.now()
1443

    
1444
    def violates_resource_grants(self):
1445
        return False
1446

    
1447
    def violates_members_limit(self, adding=0):
1448
        application = self.application
1449
        return (len(self.approved_members) + adding >
1450
                application.limit_on_members_number)
1451

    
1452
    @property
1453
    def is_alive(self):
1454
        return self.is_active()
1455

    
1456
    @property
1457
    def approved_memberships(self):
1458
        query = ProjectMembership.query_approved()
1459
        return self.projectmembership_set.filter(query)
1460

    
1461
    @property
1462
    def approved_members(self):
1463
        return [m.person for m in self.approved_memberships]
1464

    
1465
    def set_membership_pending_sync(self):
1466
        query = ProjectMembership.query_approved()
1467
        sfu = self.projectmembership_set.select_for_update()
1468
        members = sfu.filter(query)
1469

    
1470
        for member in members:
1471
            member.state = member.PENDING
1472
            member.save()
1473

    
1474
    def add_member(self, user):
1475
        """
1476
        Raises:
1477
            django.exceptions.PermissionDenied
1478
            astakos.im.models.AstakosUser.DoesNotExist
1479
        """
1480
        if isinstance(user, int):
1481
            user = AstakosUser.objects.get(user=user)
1482

    
1483
        m, created = ProjectMembership.objects.get_or_create(
1484
            person=user, project=self
1485
        )
1486
        m.accept()
1487

    
1488
    def remove_member(self, user):
1489
        """
1490
        Raises:
1491
            django.exceptions.PermissionDenied
1492
            astakos.im.models.AstakosUser.DoesNotExist
1493
            astakos.im.models.ProjectMembership.DoesNotExist
1494
        """
1495
        if isinstance(user, int):
1496
            user = AstakosUser.objects.get(user=user)
1497

    
1498
        m = ProjectMembership.objects.get(person=user, project=self)
1499
        m.remove()
1500

    
1501
    def terminate(self):
1502
        self.set_deactivation_start_date()
1503
        self.deactivation_reason = self.TERMINATED
1504
        self.save()
1505

    
1506
    @property
1507
    def is_terminated(self):
1508
        return (self.is_deactivated() and
1509
                self.deactivation_reason == self.TERMINATED)
1510

    
1511
    @property
1512
    def is_suspended(self):
1513
        return False
1514

    
1515
class ProjectMembership(models.Model):
1516

    
1517
    person              =   models.ForeignKey(AstakosUser)
1518
    request_date        =   models.DateField(default=datetime.now())
1519
    project             =   models.ForeignKey(Project)
1520

    
1521
    state               =   models.IntegerField(default=0)
1522
    application         =   models.ForeignKey(
1523
                                ProjectApplication,
1524
                                null=True,
1525
                                related_name='memberships')
1526
    pending_application =   models.ForeignKey(
1527
                                ProjectApplication,
1528
                                null=True,
1529
                                related_name='pending_memebrships')
1530
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1531

    
1532
    acceptance_date     =   models.DateField(null=True, db_index=True)
1533
    leave_request_date  =   models.DateField(null=True)
1534

    
1535
    objects     =   ForUpdateManager()
1536

    
1537
    REQUESTED   =   0
1538
    PENDING     =   1
1539
    ACCEPTED    =   2
1540
    REMOVING    =   3
1541
    REMOVED     =   4
1542
    INACTIVE    =   5
1543

    
1544
    APPROVED_SET    =   [PENDING, ACCEPTED, INACTIVE]
1545

    
1546
    @classmethod
1547
    def query_approved(cls):
1548
        return (Q(state=cls.PENDING) |
1549
                Q(state=cls.ACCEPTED) |
1550
                Q(state=cls.INACTIVE))
1551

    
1552
    class Meta:
1553
        unique_together = ("person", "project")
1554
        #index_together = [["project", "state"]]
1555

    
1556
    def __str__(self):
1557
        return _("<'%s' membership in '%s'>") % (
1558
                self.person.username, self.project)
1559

    
1560
    __repr__ = __str__
1561

    
1562
    def __init__(self, *args, **kwargs):
1563
        self.state = self.REQUESTED
1564
        super(ProjectMembership, self).__init__(*args, **kwargs)
1565

    
1566
    def _set_history_item(self, reason, date=None):
1567
        if isinstance(reason, basestring):
1568
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1569

    
1570
        history_item = ProjectMembershipHistory(
1571
                            serial=self.id,
1572
                            person=self.person.uuid,
1573
                            project=self.project_id,
1574
                            date=date or datetime.now(),
1575
                            reason=reason)
1576
        history_item.save()
1577
        serial = history_item.id
1578

    
1579
    def accept(self):
1580
        state = self.state
1581
        if state != self.REQUESTED:
1582
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1583
            raise AssertionError(m)
1584

    
1585
        now = datetime.now()
1586
        self.acceptance_date = now
1587
        self._set_history_item(reason='ACCEPT', date=now)
1588
        self.state = (self.PENDING if self.project.is_active()
1589
                      else self.INACTIVE)
1590
        self.save()
1591

    
1592
    def remove(self):
1593
        state = self.state
1594
        if state not in [self.ACCEPTED, self.INACTIVE]:
1595
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1596
            raise AssertionError(m)
1597

    
1598
        self._set_history_item(reason='REMOVE')
1599
        self.state = self.REMOVING
1600
        self.save()
1601

    
1602
    def reject(self):
1603
        state = self.state
1604
        if state != self.REQUESTED:
1605
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1606
            raise AssertionError(m)
1607

    
1608
        # rejected requests don't need sync,
1609
        # because they were never effected
1610
        self._set_history_item(reason='REJECT')
1611
        self.delete()
1612

    
1613
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1614
        if sub_list is None:
1615
            sub_list = []
1616

    
1617
        if add_list is None:
1618
            add_list = []
1619

    
1620
        sub_append = sub_list.append
1621
        add_append = add_list.append
1622
        holder = self.person.uuid
1623

    
1624
        synced_application = self.application
1625
        if synced_application is not None:
1626
            cur_grants = synced_application.projectresourcegrant_set.all()
1627
            for grant in cur_grants:
1628
                sub_append(QuotaLimits(
1629
                               holder       = holder,
1630
                               resource     = str(grant.resource),
1631
                               capacity     = grant.member_capacity,
1632
                               import_limit = grant.member_import_limit,
1633
                               export_limit = grant.member_export_limit))
1634

    
1635
        if not remove:
1636
            new_grants = self.pending_application.projectresourcegrant_set.all()
1637
            for new_grant in new_grants:
1638
                add_append(QuotaLimits(
1639
                               holder       = holder,
1640
                               resource     = str(new_grant.resource),
1641
                               capacity     = new_grant.member_capacity,
1642
                               import_limit = new_grant.member_import_limit,
1643
                               export_limit = new_grant.member_export_limit))
1644

    
1645
        return (sub_list, add_list)
1646

    
1647
    def set_sync(self):
1648
        state = self.state
1649
        if state == self.PENDING:
1650
            pending_application = self.pending_application
1651
            if pending_application is None:
1652
                m = _("%s: attempt to sync an empty pending application") % (
1653
                    self,)
1654
                raise AssertionError(m)
1655
            self.application = pending_application
1656
            self.pending_application = None
1657
            self.pending_serial = None
1658

    
1659
            # project.application may have changed in the meantime,
1660
            # in which case we stay PENDING;
1661
            # we are safe to check due to select_for_update
1662
            if self.application == self.project.application:
1663
                self.state = self.ACCEPTED
1664
            self.save()
1665
        elif state == self.ACCEPTED:
1666
            if self.pending_application:
1667
                m = _("%s: attempt to sync in state '%s' "
1668
                      "with a pending application") % (self, state)
1669
                raise AssertionError(m)
1670
            self.application = None
1671
            self.pending_serial = None
1672
            self.state = self.INACTIVE
1673
            self.save()
1674
        elif state == self.REMOVING:
1675
            self.delete()
1676
        else:
1677
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1678
            raise AssertionError(m)
1679

    
1680
    def reset_sync(self):
1681
        state = self.state
1682
        if state in [self.PENDING, self.ACCEPTED, self.REMOVING]:
1683
            self.pending_application = None
1684
            self.pending_serial = None
1685
            self.save()
1686
        else:
1687
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1688
            raise AssertionError(m)
1689

    
1690
class Serial(models.Model):
1691
    serial  =   models.AutoField(primary_key=True)
1692

    
1693
def new_serial():
1694
    s = Serial.objects.create()
1695
    serial = s.serial
1696
    s.delete()
1697
    return serial
1698

    
1699
def sync_finish_serials(serials_to_ack=None):
1700
    if serials_to_ack is None:
1701
        serials_to_ack = qh_query_serials([])
1702

    
1703
    serials_to_ack = set(serials_to_ack)
1704
    sfu = ProjectMembership.objects.select_for_update()
1705
    memberships = list(sfu.filter(pending_serial__isnull=False))
1706

    
1707
    if memberships:
1708
        for membership in memberships:
1709
            serial = membership.pending_serial
1710
            if serial in serials_to_ack:
1711
                membership.set_sync()
1712
            else:
1713
                membership.reset_sync()
1714

    
1715
        transaction.commit()
1716

    
1717
    qh_ack_serials(list(serials_to_ack))
1718
    return len(memberships)
1719

    
1720
def sync_all_projects():
1721
    sync_finish_serials()
1722

    
1723
    PENDING = ProjectMembership.PENDING
1724
    REMOVING = ProjectMembership.REMOVING
1725
    objects = ProjectMembership.objects.select_for_update()
1726

    
1727
    sub_quota, add_quota = [], []
1728

    
1729
    serial = new_serial()
1730

    
1731
    pending = objects.filter(state=PENDING)
1732
    for membership in pending:
1733

    
1734
        if membership.pending_application:
1735
            m = "%s: impossible: pending_application is not None (%s)" % (
1736
                membership, membership.pending_application)
1737
            raise AssertionError(m)
1738
        if membership.pending_serial:
1739
            m = "%s: impossible: pending_serial is not None (%s)" % (
1740
                membership, membership.pending_serial)
1741
            raise AssertionError(m)
1742

    
1743
        membership.pending_application = membership.project.application
1744
        membership.pending_serial = serial
1745
        membership.get_diff_quotas(sub_quota, add_quota)
1746
        membership.save()
1747

    
1748
    removing = objects.filter(state=REMOVING)
1749
    for membership in removing:
1750

    
1751
        if membership.pending_application:
1752
            m = ("%s: impossible: removing pending_application is not None (%s)"
1753
                % (membership, membership.pending_application))
1754
            raise AssertionError(m)
1755
        if membership.pending_serial:
1756
            m = "%s: impossible: pending_serial is not None (%s)" % (
1757
                membership, membership.pending_serial)
1758
            raise AssertionError(m)
1759

    
1760
        membership.pending_serial = serial
1761
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1762
        membership.save()
1763

    
1764
    transaction.commit()
1765
    # ProjectApplication.approve() unblocks here
1766
    # and can set PENDING an already PENDING membership
1767
    # which has been scheduled to sync with the old project.application
1768
    # Need to check in ProjectMembership.set_sync()
1769

    
1770
    r = qh_add_quota(serial, sub_quota, add_quota)
1771
    if r:
1772
        m = "cannot sync serial: %d" % serial
1773
        raise RuntimeError(m)
1774

    
1775
    sync_finish_serials([serial])
1776

    
1777
def sync_deactivating_projects():
1778

    
1779
    ACCEPTED = ProjectMembership.ACCEPTED
1780
    PENDING = ProjectMembership.PENDING
1781
    REMOVING = ProjectMembership.REMOVING
1782

    
1783
    psfu = Project.objects.select_for_update()
1784
    projects = psfu.filter(deactivation_start_date__isnull=False)
1785

    
1786
    if not projects:
1787
        return
1788

    
1789
    sub_quota, add_quota = [], []
1790

    
1791
    serial = new_serial()
1792

    
1793
    for project in projects:
1794
        objects = project.projectmembership_set.select_for_update()
1795
        memberships = objects.filter(Q(state=ACCEPTED) |
1796
                                     Q(state=PENDING) | Q(state=REMOVING))
1797
        for membership in memberships:
1798
            if membership.state in (PENDING, REMOVING):
1799
                m = "cannot sync deactivating project '%s'" % project
1800
                raise RuntimeError(m)
1801

    
1802
            # state == ACCEPTED
1803
            if membership.pending_application:
1804
                m = "%s: impossible: pending_application is not None (%s)" % (
1805
                    membership, membership.pending_application)
1806
                raise AssertionError(m)
1807
            if membership.pending_serial:
1808
                m = "%s: impossible: pending_serial is not None (%s)" % (
1809
                    membership, membership.pending_serial)
1810
                raise AssertionError(m)
1811

    
1812
            membership.pending_serial = serial
1813
            membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1814
            membership.save()
1815

    
1816
    transaction.commit()
1817

    
1818
    r = qh_add_quota(serial, sub_quota, add_quota)
1819
    if r:
1820
        m = "cannot sync serial: %d" % serial
1821
        raise RuntimeError(m)
1822

    
1823
    sync_finish_serials([serial])
1824

    
1825
    # finalize deactivating projects
1826
    deactivating_projects = psfu.filter(deactivation_start_date__isnull=False)
1827
    for project in deactivating_projects:
1828
        objects = project.projectmembership_set.select_for_update()
1829
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1830
                                          Q(state=PENDING) | Q(state=REMOVING)))
1831
        if not memberships:
1832
            project.set_deactivation_date()
1833
            project.save()
1834

    
1835
    transaction.commit()
1836

    
1837
def sync_projects():
1838
    sync_all_projects()
1839
    sync_deactivating_projects()
1840

    
1841
def trigger_sync(retries=3, retry_wait=1.0):
1842
    transaction.commit()
1843

    
1844
    cursor = connection.cursor()
1845
    locked = True
1846
    try:
1847
        while 1:
1848
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1849
            r = cursor.fetchone()
1850
            if r is None:
1851
                m = "Impossible"
1852
                raise AssertionError(m)
1853
            locked = r[0]
1854
            if locked:
1855
                break
1856

    
1857
            retries -= 1
1858
            if retries <= 0:
1859
                return False
1860
            sleep(retry_wait)
1861

    
1862
        sync_projects()
1863
        return True
1864

    
1865
    finally:
1866
        if locked:
1867
            cursor.execute("SELECT pg_advisory_unlock(1)")
1868
            cursor.fetchall()
1869

    
1870

    
1871
class ProjectMembershipHistory(models.Model):
1872
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1873
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1874

    
1875
    person  =   models.CharField(max_length=255)
1876
    project =   models.BigIntegerField()
1877
    date    =   models.DateField(default=datetime.now)
1878
    reason  =   models.IntegerField()
1879
    serial  =   models.BigIntegerField()
1880

    
1881
### SIGNALS ###
1882
################
1883

    
1884
def create_astakos_user(u):
1885
    try:
1886
        AstakosUser.objects.get(user_ptr=u.pk)
1887
    except AstakosUser.DoesNotExist:
1888
        extended_user = AstakosUser(user_ptr_id=u.pk)
1889
        extended_user.__dict__.update(u.__dict__)
1890
        extended_user.save()
1891
        if not extended_user.has_auth_provider('local'):
1892
            extended_user.add_auth_provider('local')
1893
    except BaseException, e:
1894
        logger.exception(e)
1895

    
1896

    
1897
def fix_superusers(sender, **kwargs):
1898
    # Associate superusers with AstakosUser
1899
    admins = User.objects.filter(is_superuser=True)
1900
    for u in admins:
1901
        create_astakos_user(u)
1902
post_syncdb.connect(fix_superusers)
1903

    
1904

    
1905
def user_post_save(sender, instance, created, **kwargs):
1906
    if not created:
1907
        return
1908
    create_astakos_user(instance)
1909
post_save.connect(user_post_save, sender=User)
1910

    
1911
def astakosuser_post_save(sender, instance, created, **kwargs):
1912
    if not created:
1913
        return
1914
    # TODO handle socket.error & IOError
1915
    register_users((instance,))
1916
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1917

    
1918
def resource_post_save(sender, instance, created, **kwargs):
1919
    if not created:
1920
        return
1921
    register_resources((instance,))
1922
post_save.connect(resource_post_save, sender=Resource)
1923

    
1924
def renew_token(sender, instance, **kwargs):
1925
    if not instance.auth_token:
1926
        instance.renew_token()
1927
pre_save.connect(renew_token, sender=AstakosUser)
1928
pre_save.connect(renew_token, sender=Service)
1929