Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 2e15f9f6

History | View | Annotate | Download (65.7 kB)

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

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

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
70
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA)
71
from astakos.im import settings as astakos_settings
72
from astakos.im.endpoints.qh import (
73
    register_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 _get_project(self):
1275
        precursor = self
1276
        while precursor:
1277
            try:
1278
                objects = Project.objects.select_for_update()
1279
                project = objects.get(application=precursor)
1280
                return project
1281
            except Project.DoesNotExist:
1282
                pass
1283
            precursor = precursor.precursor_application
1284

    
1285
        return None
1286

    
1287
    def approve(self, approval_user=None):
1288
        """
1289
        If approval_user then during owner membership acceptance
1290
        it is checked whether the request_user is eligible.
1291

1292
        Raises:
1293
            PermissionDenied
1294
        """
1295

    
1296
        if not transaction.is_managed():
1297
            raise AssertionError("NOPE")
1298

    
1299
        new_project_name = self.name
1300
        if self.state != self.PENDING:
1301
            m = _("cannot approve: project '%s' in state '%s'") % (
1302
                    new_project_name, self.state)
1303
            raise PermissionDenied(m) # invalid argument
1304

    
1305
        now = datetime.now()
1306
        project = self._get_project()
1307

    
1308
        try:
1309
            # needs SERIALIZABLE
1310
            conflicting_project = Project.objects.get(name=new_project_name)
1311
            if (conflicting_project.is_alive and
1312
                conflicting_project != project):
1313
                m = (_("cannot approve: project with name '%s' "
1314
                       "already exists (serial: %s)") % (
1315
                        new_project_name, conflicting_project.id))
1316
                raise PermissionDenied(m) # invalid argument
1317
        except Project.DoesNotExist:
1318
            pass
1319

    
1320
        new_project = False
1321
        if project is None:
1322
            new_project = True
1323
            project = Project(creation_date=now)
1324

    
1325
        project.name = new_project_name
1326
        project.application = self
1327
        project.last_approval_date = now
1328
        project.save()
1329

    
1330
        if new_project:
1331
            project.add_member(self.owner)
1332

    
1333
        # This will block while syncing,
1334
        # but unblock before setting the membership state.
1335
        # See ProjectMembership.set_sync()
1336
        project.set_membership_pending_sync()
1337

    
1338
        precursor = self.precursor_application
1339
        while precursor:
1340
            precursor.state = self.REPLACED
1341
            precursor.save()
1342
            precursor = precursor.precursor_application
1343

    
1344
        self.state = self.APPROVED
1345
        self.save()
1346

    
1347
def submit_application(**kw):
1348

    
1349
    resource_policies = kw.pop('resource_policies', None)
1350
    application = ProjectApplication(**kw)
1351

    
1352
    precursor = kw['precursor_application']
1353

    
1354
    if precursor is not None:
1355
        precursor.state = ProjectApplication.REPLACED
1356
        precursor.save()
1357

    
1358
    application.save()
1359
    application.resource_policies = resource_policies
1360
    return application
1361

    
1362
class ProjectResourceGrant(models.Model):
1363

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

    
1374
    objects = ExtendedManager()
1375

    
1376
    class Meta:
1377
        unique_together = ("resource", "project_application")
1378

    
1379

    
1380
class Project(models.Model):
1381

    
1382
    application                 =   models.OneToOneField(
1383
                                            ProjectApplication,
1384
                                            related_name='project')
1385
    last_approval_date          =   models.DateTimeField(null=True)
1386

    
1387
    members                     =   models.ManyToManyField(
1388
                                            AstakosUser,
1389
                                            through='ProjectMembership')
1390

    
1391
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1392
    deactivation_start_date     =   models.DateTimeField(null=True)
1393
    deactivation_date           =   models.DateTimeField(null=True)
1394

    
1395
    creation_date               =   models.DateTimeField()
1396
    name                        =   models.CharField(
1397
                                            max_length=80,
1398
                                            db_index=True,
1399
                                            unique=True)
1400

    
1401
    TERMINATED  =   'TERMINATED'
1402
    SUSPENDED   =   'SUSPENDED'
1403

    
1404
    objects     =   ForUpdateManager()
1405

    
1406
    def __str__(self):
1407
        return _("<project %s '%s'>") % (self.id, self.application.name)
1408

    
1409
    __repr__ = __str__
1410

    
1411
    def is_deactivating(self):
1412
        return bool(self.deactivation_start_date)
1413

    
1414
    def is_deactivated_synced(self):
1415
        return bool(self.deactivation_date)
1416

    
1417
    def is_deactivated(self):
1418
        return self.is_deactivated_synced() or self.is_deactivating()
1419

    
1420
    def is_still_approved(self):
1421
        return bool(self.last_approval_date)
1422

    
1423
    def is_active(self):
1424
        return not(self.is_deactivated())
1425

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

    
1434
    def set_deactivation_start_date(self):
1435
        self.deactivation_start_date = datetime.now()
1436

    
1437
    def set_deactivation_date(self):
1438
        self.deactivation_start_date = None
1439
        self.deactivation_date = datetime.now()
1440

    
1441
    def violates_resource_grants(self):
1442
        return False
1443

    
1444
    def violates_members_limit(self, adding=0):
1445
        application = self.application
1446
        return (len(self.approved_members) + adding >
1447
                application.limit_on_members_number)
1448

    
1449
    @property
1450
    def is_alive(self):
1451
        return self.is_active()
1452

    
1453
    @property
1454
    def approved_memberships(self):
1455
        query = ProjectMembership.query_approved()
1456
        return self.projectmembership_set.filter(query)
1457

    
1458
    @property
1459
    def approved_members(self):
1460
        return [m.person for m in self.approved_memberships]
1461

    
1462
    def set_membership_pending_sync(self):
1463
        query = ProjectMembership.query_approved()
1464
        sfu = self.projectmembership_set.select_for_update()
1465
        members = sfu.filter(query)
1466

    
1467
        for member in members:
1468
            member.state = member.PENDING
1469
            member.save()
1470

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

    
1480
        m, created = ProjectMembership.objects.get_or_create(
1481
            person=user, project=self
1482
        )
1483
        m.accept()
1484

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

    
1495
        m = ProjectMembership.objects.get(person=user, project=self)
1496
        m.remove()
1497

    
1498
    def terminate(self):
1499
        self.set_deactivation_start_date()
1500
        self.deactivation_reason = self.TERMINATED
1501
        self.save()
1502

    
1503
    @property
1504
    def is_terminated(self):
1505
        return (self.is_deactivated() and
1506
                self.deactivation_reason == self.TERMINATED)
1507

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

    
1512
class ProjectMembership(models.Model):
1513

    
1514
    person              =   models.ForeignKey(AstakosUser)
1515
    request_date        =   models.DateField(default=datetime.now())
1516
    project             =   models.ForeignKey(Project)
1517

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

    
1529
    acceptance_date     =   models.DateField(null=True, db_index=True)
1530
    leave_request_date  =   models.DateField(null=True)
1531

    
1532
    objects     =   ForUpdateManager()
1533

    
1534
    REQUESTED   =   0
1535
    PENDING     =   1
1536
    ACCEPTED    =   2
1537
    REMOVING    =   3
1538
    REMOVED     =   4
1539
    INACTIVE    =   5
1540

    
1541
    APPROVED_SET    =   [PENDING, ACCEPTED, INACTIVE]
1542

    
1543
    @classmethod
1544
    def query_approved(cls):
1545
        return (Q(state=cls.PENDING) |
1546
                Q(state=cls.ACCEPTED) |
1547
                Q(state=cls.INACTIVE))
1548

    
1549
    class Meta:
1550
        unique_together = ("person", "project")
1551
        #index_together = [["project", "state"]]
1552

    
1553
    def __str__(self):
1554
        return _("<'%s' membership in '%s'>") % (
1555
                self.person.username, self.project)
1556

    
1557
    __repr__ = __str__
1558

    
1559
    def __init__(self, *args, **kwargs):
1560
        self.state = self.REQUESTED
1561
        super(ProjectMembership, self).__init__(*args, **kwargs)
1562

    
1563
    def _set_history_item(self, reason, date=None):
1564
        if isinstance(reason, basestring):
1565
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1566

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

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

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

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

    
1595
        self._set_history_item(reason='REMOVE')
1596
        self.state = self.REMOVING
1597
        self.save()
1598

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

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

    
1610
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1611
        if sub_list is None:
1612
            sub_list = []
1613

    
1614
        if add_list is None:
1615
            add_list = []
1616

    
1617
        sub_append = sub_list.append
1618
        add_append = add_list.append
1619
        holder = self.person.uuid
1620

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

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

    
1642
        return (sub_list, add_list)
1643

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

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

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

    
1687
class Serial(models.Model):
1688
    serial  =   models.AutoField(primary_key=True)
1689

    
1690
def new_serial():
1691
    s = Serial.objects.create()
1692
    serial = s.serial
1693
    s.delete()
1694
    return serial
1695

    
1696
def sync_finish_serials(serials_to_ack=None):
1697
    if serials_to_ack is None:
1698
        serials_to_ack = qh_query_serials([])
1699

    
1700
    serials_to_ack = set(serials_to_ack)
1701
    sfu = ProjectMembership.objects.select_for_update()
1702
    memberships = list(sfu.filter(pending_serial__isnull=False))
1703

    
1704
    if memberships:
1705
        for membership in memberships:
1706
            serial = membership.pending_serial
1707
            if serial in serials_to_ack:
1708
                membership.set_sync()
1709
            else:
1710
                membership.reset_sync()
1711

    
1712
        transaction.commit()
1713

    
1714
    qh_ack_serials(list(serials_to_ack))
1715
    return len(memberships)
1716

    
1717
def sync_all_projects():
1718
    sync_finish_serials()
1719

    
1720
    PENDING = ProjectMembership.PENDING
1721
    REMOVING = ProjectMembership.REMOVING
1722
    objects = ProjectMembership.objects.select_for_update()
1723

    
1724
    sub_quota, add_quota = [], []
1725

    
1726
    serial = new_serial()
1727

    
1728
    pending = objects.filter(state=PENDING)
1729
    for membership in pending:
1730

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

    
1740
        membership.pending_application = membership.project.application
1741
        membership.pending_serial = serial
1742
        membership.get_diff_quotas(sub_quota, add_quota)
1743
        membership.save()
1744

    
1745
    removing = objects.filter(state=REMOVING)
1746
    for membership in removing:
1747

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

    
1757
        membership.pending_serial = serial
1758
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1759
        membership.save()
1760

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

    
1767
    r = qh_add_quota(serial, sub_quota, add_quota)
1768
    if r:
1769
        m = "cannot sync serial: %d" % serial
1770
        raise RuntimeError(m)
1771

    
1772
    sync_finish_serials([serial])
1773

    
1774
def sync_deactivating_projects():
1775

    
1776
    ACCEPTED = ProjectMembership.ACCEPTED
1777
    PENDING = ProjectMembership.PENDING
1778
    REMOVING = ProjectMembership.REMOVING
1779

    
1780
    psfu = Project.objects.select_for_update()
1781
    projects = psfu.filter(deactivation_start_date__isnull=False)
1782

    
1783
    if not projects:
1784
        return
1785

    
1786
    sub_quota, add_quota = [], []
1787

    
1788
    serial = new_serial()
1789

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

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

    
1809
            membership.pending_serial = serial
1810
            membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1811
            membership.save()
1812

    
1813
    transaction.commit()
1814

    
1815
    r = qh_add_quota(serial, sub_quota, add_quota)
1816
    if r:
1817
        m = "cannot sync serial: %d" % serial
1818
        raise RuntimeError(m)
1819

    
1820
    sync_finish_serials([serial])
1821

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

    
1832
    transaction.commit()
1833

    
1834
def sync_projects():
1835
    sync_all_projects()
1836
    sync_deactivating_projects()
1837

    
1838
def trigger_sync(retries=3, retry_wait=1.0):
1839
    transaction.commit()
1840

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

    
1854
            retries -= 1
1855
            if retries <= 0:
1856
                return False
1857
            sleep(retry_wait)
1858

    
1859
        sync_projects()
1860
        return True
1861

    
1862
    finally:
1863
        if locked:
1864
            cursor.execute("SELECT pg_advisory_unlock(1)")
1865
            cursor.fetchall()
1866

    
1867

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

    
1872
    person  =   models.CharField(max_length=255)
1873
    project =   models.BigIntegerField()
1874
    date    =   models.DateField(default=datetime.now)
1875
    reason  =   models.IntegerField()
1876
    serial  =   models.BigIntegerField()
1877

    
1878
### SIGNALS ###
1879
################
1880

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

    
1893

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

    
1901

    
1902
def user_post_save(sender, instance, created, **kwargs):
1903
    if not created:
1904
        return
1905
    create_astakos_user(instance)
1906
post_save.connect(user_post_save, sender=User)
1907

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

    
1915
def resource_post_save(sender, instance, created, **kwargs):
1916
    if not created:
1917
        return
1918
    register_resources((instance,))
1919
post_save.connect(resource_post_save, sender=Resource)
1920

    
1921
def renew_token(sender, instance, **kwargs):
1922
    if not instance.auth_token:
1923
        instance.renew_token()
1924
pre_save.connect(renew_token, sender=AstakosUser)
1925
pre_save.connect(renew_token, sender=Service)
1926