Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (66.6 kB)

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

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

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
70
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA)
71
from astakos.im import settings as astakos_settings
72
from astakos.im.endpoints.qh import (
73
    register_users, register_resources, qh_add_quota, QuotaLimits,
74
    qh_query_serials, qh_ack_serials)
75
from astakos.im import auth_providers
76

    
77
import astakos.im.messages as astakos_messages
78
from .managers import ForUpdateManager
79

    
80
from synnefo.lib.quotaholder.api import QH_PRACTICALLY_INFINITE
81
from synnefo.lib.db.intdecimalfield import intDecimalField
82

    
83
logger = logging.getLogger(__name__)
84

    
85
DEFAULT_CONTENT_TYPE = None
86
_content_type = None
87

    
88
def get_content_type():
89
    global _content_type
90
    if _content_type is not None:
91
        return _content_type
92

    
93
    try:
94
        content_type = ContentType.objects.get(app_label='im', model='astakosuser')
95
    except:
96
        content_type = DEFAULT_CONTENT_TYPE
97
    _content_type = content_type
98
    return content_type
99

    
100
RESOURCE_SEPARATOR = '.'
101

    
102
inf = float('inf')
103

    
104
class Service(models.Model):
105
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
106
    url = models.FilePathField()
107
    icon = models.FilePathField(blank=True)
108
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
109
                                  null=True, blank=True)
110
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
111
    auth_token_expires = models.DateTimeField(
112
        _('Token expiration date'), null=True)
113
    order = models.PositiveIntegerField(default=0)
114

    
115
    class Meta:
116
        ordering = ('order', )
117

    
118
    def renew_token(self, expiration_date=None):
119
        md5 = hashlib.md5()
120
        md5.update(self.name.encode('ascii', 'ignore'))
121
        md5.update(self.url.encode('ascii', 'ignore'))
122
        md5.update(asctime())
123

    
124
        self.auth_token = b64encode(md5.digest())
125
        self.auth_token_created = datetime.now()
126
        if expiration_date:
127
            self.auth_token_expires = expiration_date
128
        else:
129
            self.auth_token_expires = None
130

    
131
    def __str__(self):
132
        return self.name
133

    
134
    @property
135
    def resources(self):
136
        return self.resource_set.all()
137

    
138
    @resources.setter
139
    def resources(self, resources):
140
        for s in resources:
141
            self.resource_set.create(**s)
142

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

    
154

    
155
class ResourceMetadata(models.Model):
156
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
157
    value = models.CharField(_('Value'), max_length=255)
158

    
159
_presentation_data = {}
160
def get_presentation(resource):
161
    global _presentation_data
162
    presentation = _presentation_data.get(resource, {})
163
    if not presentation:
164
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
165
        presentation = resource_presentation.get(resource, {})
166
        _presentation_data[resource] = presentation
167
    return presentation
168

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

    
177
    class Meta:
178
        unique_together = ("name", "service")
179

    
180
    def __str__(self):
181
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
182

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

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

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

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

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

    
203
    @property
204
    def verbose_name(self):
205
        return get_presentation(str(self)).get('verbose_name', '')
206

    
207

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

    
222

    
223
class AstakosUserManager(UserManager):
224

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

    
234
    def get_by_email(self, email):
235
        return self.get(email=email)
236

    
237
    def get_by_identifier(self, email_or_username, **kwargs):
238
        try:
239
            return self.get(email__iexact=email_or_username, **kwargs)
240
        except AstakosUser.DoesNotExist:
241
            return self.get(username__iexact=email_or_username, **kwargs)
242

    
243
    def user_exists(self, email_or_username, **kwargs):
244
        qemail = Q(email__iexact=email_or_username)
245
        qusername = Q(username__iexact=email_or_username)
246
        qextra = Q(**kwargs)
247
        return self.filter((qemail | qusername) & qextra).exists()
248

    
249
    def verified_user_exists(self, email_or_username):
250
        return self.user_exists(email_or_username, email_verified=True)
251

    
252
    def verified(self):
253
        return self.filter(email_verified=True)
254

    
255
    def verified(self):
256
        return self.filter(email_verified=True)
257

    
258

    
259
class AstakosUser(User):
260
    """
261
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
262
    """
263
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
264
                                   null=True)
265

    
266
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
267
    #                    AstakosUserProvider model.
268
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
269
                                null=True)
270
    # ex. screen_name for twitter, eppn for shibboleth
271
    third_party_identifier = models.CharField(_('Third-party identifier'),
272
                                              max_length=255, null=True,
273
                                              blank=True)
274

    
275

    
276
    #for invitations
277
    user_level = DEFAULT_USER_LEVEL
278
    level = models.IntegerField(_('Inviter level'), default=user_level)
279
    invitations = models.IntegerField(
280
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
281

    
282
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
283
                                  null=True, blank=True)
284
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
285
    auth_token_expires = models.DateTimeField(
286
        _('Token expiration date'), null=True)
287

    
288
    updated = models.DateTimeField(_('Update date'))
289
    is_verified = models.BooleanField(_('Is verified?'), default=False)
290

    
291
    email_verified = models.BooleanField(_('Email verified?'), default=False)
292

    
293
    has_credits = models.BooleanField(_('Has credits?'), default=False)
294
    has_signed_terms = models.BooleanField(
295
        _('I agree with the terms'), default=False)
296
    date_signed_terms = models.DateTimeField(
297
        _('Signed terms date'), null=True, blank=True)
298

    
299
    activation_sent = models.DateTimeField(
300
        _('Activation sent data'), null=True, blank=True)
301

    
302
    policy = models.ManyToManyField(
303
        Resource, null=True, through='AstakosUserQuota')
304

    
305
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
306

    
307
    __has_signed_terms = False
308
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
309
                                           default=False, db_index=True)
310

    
311
    objects = AstakosUserManager()
312

    
313
    def __init__(self, *args, **kwargs):
314
        super(AstakosUser, self).__init__(*args, **kwargs)
315
        self.__has_signed_terms = self.has_signed_terms
316
        if not self.id:
317
            self.is_active = False
318

    
319
    @property
320
    def realname(self):
321
        return '%s %s' % (self.first_name, self.last_name)
322

    
323
    @realname.setter
324
    def realname(self, value):
325
        parts = value.split(' ')
326
        if len(parts) == 2:
327
            self.first_name = parts[0]
328
            self.last_name = parts[1]
329
        else:
330
            self.last_name = parts[0]
331

    
332
    def add_permission(self, pname):
333
        if self.has_perm(pname):
334
            return
335
        p, created = Permission.objects.get_or_create(
336
                                    codename=pname,
337
                                    name=pname.capitalize(),
338
                                    content_type=get_content_type())
339
        self.user_permissions.add(p)
340

    
341
    def remove_permission(self, pname):
342
        if self.has_perm(pname):
343
            return
344
        p = Permission.objects.get(codename=pname,
345
                                   content_type=get_content_type())
346
        self.user_permissions.remove(p)
347

    
348
    @property
349
    def invitation(self):
350
        try:
351
            return Invitation.objects.get(username=self.email)
352
        except Invitation.DoesNotExist:
353
            return None
354

    
355
    @property
356
    def quota(self):
357
        """Returns a dict with the sum of quota limits per resource"""
358
        d = defaultdict(int)
359
        default_quota = get_default_quota()
360
        d.update(default_quota)
361
        for q in self.policies:
362
            d[q.resource] += q.uplimit or inf
363
        for m in self.projectmembership_set.select_related().all():
364
            if not m.acceptance_date:
365
                continue
366
            p = m.project
367
            if not p.is_active_strict():
368
                continue
369
            grants = p.application.projectresourcegrant_set.all()
370
            for g in grants:
371
                d[str(g.resource)] += g.member_capacity or inf
372
        return d
373

    
374
    @property
375
    def policies(self):
376
        return self.astakosuserquota_set.select_related().all()
377

    
378
    @policies.setter
379
    def policies(self, policies):
380
        for p in policies:
381
            service = policies.get('service', None)
382
            resource = policies.get('resource', None)
383
            uplimit = policies.get('uplimit', 0)
384
            update = policies.get('update', True)
385
            self.add_policy(service, resource, uplimit, update)
386

    
387
    def add_policy(self, service, resource, uplimit, update=True):
388
        """Raises ObjectDoesNotExist, IntegrityError"""
389
        resource = Resource.objects.get(service__name=service, name=resource)
390
        if update:
391
            AstakosUserQuota.objects.update_or_create(user=self,
392
                                                      resource=resource,
393
                                                      defaults={'uplimit': uplimit})
394
        else:
395
            q = self.astakosuserquota_set
396
            q.create(resource=resource, uplimit=uplimit)
397

    
398
    def remove_policy(self, service, resource):
399
        """Raises ObjectDoesNotExist, IntegrityError"""
400
        resource = Resource.objects.get(service__name=service, name=resource)
401
        q = self.policies.get(resource=resource).delete()
402

    
403
    def update_uuid(self):
404
        while not self.uuid:
405
            uuid_val =  str(uuid.uuid4())
406
            try:
407
                AstakosUser.objects.get(uuid=uuid_val)
408
            except AstakosUser.DoesNotExist, e:
409
                self.uuid = uuid_val
410
        return self.uuid
411

    
412
    @property
413
    def extended_groups(self):
414
        return self.membership_set.select_related().all()
415

    
416
    def save(self, update_timestamps=True, **kwargs):
417
        if update_timestamps:
418
            if not self.id:
419
                self.date_joined = datetime.now()
420
            self.updated = datetime.now()
421

    
422
        # update date_signed_terms if necessary
423
        if self.__has_signed_terms != self.has_signed_terms:
424
            self.date_signed_terms = datetime.now()
425

    
426
        self.update_uuid()
427

    
428
        if self.username != self.email.lower():
429
            # set username
430
            self.username = self.email.lower()
431

    
432
        self.validate_unique_email_isactive()
433

    
434
        super(AstakosUser, self).save(**kwargs)
435

    
436
    def renew_token(self, flush_sessions=False, current_key=None):
437
        md5 = hashlib.md5()
438
        md5.update(settings.SECRET_KEY)
439
        md5.update(self.username)
440
        md5.update(self.realname.encode('ascii', 'ignore'))
441
        md5.update(asctime())
442

    
443
        self.auth_token = b64encode(md5.digest())
444
        self.auth_token_created = datetime.now()
445
        self.auth_token_expires = self.auth_token_created + \
446
                                  timedelta(hours=AUTH_TOKEN_DURATION)
447
        if flush_sessions:
448
            self.flush_sessions(current_key)
449
        msg = 'Token renewed for %s' % self.email
450
        logger.log(LOGGING_LEVEL, msg)
451

    
452
    def flush_sessions(self, current_key=None):
453
        q = self.sessions
454
        if current_key:
455
            q = q.exclude(session_key=current_key)
456

    
457
        keys = q.values_list('session_key', flat=True)
458
        if keys:
459
            msg = 'Flushing sessions: %s' % ','.join(keys)
460
            logger.log(LOGGING_LEVEL, msg, [])
461
        engine = import_module(settings.SESSION_ENGINE)
462
        for k in keys:
463
            s = engine.SessionStore(k)
464
            s.flush()
465

    
466
    def __unicode__(self):
467
        return '%s (%s)' % (self.realname, self.email)
468

    
469
    def conflicting_email(self):
470
        q = AstakosUser.objects.exclude(username=self.username)
471
        q = q.filter(email__iexact=self.email)
472
        if q.count() != 0:
473
            return True
474
        return False
475

    
476
    def validate_unique_email_isactive(self):
477
        """
478
        Implements a unique_together constraint for email and is_active fields.
479
        """
480
        q = AstakosUser.objects.all()
481
        q = q.filter(email = self.email)
482
        if self.id:
483
            q = q.filter(~Q(id = self.id))
484
        if q.count() != 0:
485
            m = 'Another account with the same email = %(email)s & \
486
                is_active = %(is_active)s found.' % self.__dict__
487
            raise ValidationError(m)
488

    
489
    def email_change_is_pending(self):
490
        return self.emailchanges.count() > 0
491

    
492
    def email_change_is_pending(self):
493
        return self.emailchanges.count() > 0
494

    
495
    @property
496
    def signed_terms(self):
497
        term = get_latest_terms()
498
        if not term:
499
            return True
500
        if not self.has_signed_terms:
501
            return False
502
        if not self.date_signed_terms:
503
            return False
504
        if self.date_signed_terms < term.date:
505
            self.has_signed_terms = False
506
            self.date_signed_terms = None
507
            self.save()
508
            return False
509
        return True
510

    
511
    def set_invitations_level(self):
512
        """
513
        Update user invitation level
514
        """
515
        level = self.invitation.inviter.level + 1
516
        self.level = level
517
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
518

    
519
    def can_login_with_auth_provider(self, provider):
520
        if not self.has_auth_provider(provider):
521
            return False
522
        else:
523
            return auth_providers.get_provider(provider).is_available_for_login()
524

    
525
    def can_add_auth_provider(self, provider, **kwargs):
526
        provider_settings = auth_providers.get_provider(provider)
527

    
528
        if not provider_settings.is_available_for_add():
529
            return False
530

    
531
        if self.has_auth_provider(provider) and \
532
           provider_settings.one_per_user:
533
            return False
534

    
535
        if 'provider_info' in kwargs:
536
            kwargs.pop('provider_info')
537

    
538
        if 'identifier' in kwargs:
539
            try:
540
                # provider with specified params already exist
541
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
542
                                                                   **kwargs)
543
            except AstakosUser.DoesNotExist:
544
                return True
545
            else:
546
                return False
547

    
548
        return True
549

    
550
    def can_remove_auth_provider(self, module):
551
        provider = auth_providers.get_provider(module)
552
        existing = self.get_active_auth_providers()
553
        existing_for_provider = self.get_active_auth_providers(module=module)
554

    
555
        if len(existing) <= 1:
556
            return False
557

    
558
        if len(existing_for_provider) == 1 and provider.is_required():
559
            return False
560

    
561
        return True
562

    
563
    def can_change_password(self):
564
        return self.has_auth_provider('local', auth_backend='astakos')
565

    
566
    def has_required_auth_providers(self):
567
        required = auth_providers.REQUIRED_PROVIDERS
568
        for provider in required:
569
            if not self.has_auth_provider(provider):
570
                return False
571
        return True
572

    
573
    def has_auth_provider(self, provider, **kwargs):
574
        return bool(self.auth_providers.filter(module=provider,
575
                                               **kwargs).count())
576

    
577
    def add_auth_provider(self, provider, **kwargs):
578
        info_data = ''
579
        if 'provider_info' in kwargs:
580
            info_data = kwargs.pop('provider_info')
581
            if isinstance(info_data, dict):
582
                info_data = json.dumps(info_data)
583

    
584
        if self.can_add_auth_provider(provider, **kwargs):
585
            self.auth_providers.create(module=provider, active=True,
586
                                       info_data=info_data,
587
                                       **kwargs)
588
        else:
589
            raise Exception('Cannot add provider')
590

    
591
    def add_pending_auth_provider(self, pending):
592
        """
593
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
594
        the current user.
595
        """
596
        if not isinstance(pending, PendingThirdPartyUser):
597
            pending = PendingThirdPartyUser.objects.get(token=pending)
598

    
599
        provider = self.add_auth_provider(pending.provider,
600
                               identifier=pending.third_party_identifier,
601
                                affiliation=pending.affiliation,
602
                                          provider_info=pending.info)
603

    
604
        if email_re.match(pending.email or '') and pending.email != self.email:
605
            self.additionalmail_set.get_or_create(email=pending.email)
606

    
607
        pending.delete()
608
        return provider
609

    
610
    def remove_auth_provider(self, provider, **kwargs):
611
        self.auth_providers.get(module=provider, **kwargs).delete()
612

    
613
    # user urls
614
    def get_resend_activation_url(self):
615
        return reverse('send_activation', kwargs={'user_id': self.pk})
616

    
617
    def get_provider_remove_url(self, module, **kwargs):
618
        return reverse('remove_auth_provider', kwargs={
619
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
620

    
621
    def get_activation_url(self, nxt=False):
622
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
623
                                 quote(self.auth_token))
624
        if nxt:
625
            url += "&next=%s" % quote(nxt)
626
        return url
627

    
628
    def get_password_reset_url(self, token_generator=default_token_generator):
629
        return reverse('django.contrib.auth.views.password_reset_confirm',
630
                          kwargs={'uidb36':int_to_base36(self.id),
631
                                  'token':token_generator.make_token(self)})
632

    
633
    def get_auth_providers(self):
634
        return self.auth_providers.all()
635

    
636
    def get_available_auth_providers(self):
637
        """
638
        Returns a list of providers available for user to connect to.
639
        """
640
        providers = []
641
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
642
            if self.can_add_auth_provider(module):
643
                providers.append(provider_settings(self))
644

    
645
        return providers
646

    
647
    def get_active_auth_providers(self, **filters):
648
        providers = []
649
        for provider in self.auth_providers.active(**filters):
650
            if auth_providers.get_provider(provider.module).is_available_for_login():
651
                providers.append(provider)
652
        return providers
653

    
654
    @property
655
    def auth_providers_display(self):
656
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
657

    
658
    def get_inactive_message(self):
659
        msg_extra = ''
660
        message = ''
661
        if self.activation_sent:
662
            if self.email_verified:
663
                message = _(astakos_messages.ACCOUNT_INACTIVE)
664
            else:
665
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
666
                if astakos_settings.MODERATION_ENABLED:
667
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
668
                else:
669
                    url = self.get_resend_activation_url()
670
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
671
                                u' ' + \
672
                                _('<a href="%s">%s?</a>') % (url,
673
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
674
        else:
675
            if astakos_settings.MODERATION_ENABLED:
676
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
677
            else:
678
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
679
                url = self.get_resend_activation_url()
680
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
681
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
682

    
683
        return mark_safe(message + u' '+ msg_extra)
684

    
685
    def owns_project(self, project):
686
        return project.owner == self
687

    
688
    def is_project_member(self, project):
689
        return project.user_status(self) in [0,1,2,3]
690

    
691
    def is_project_accepted_member(self, project):
692
        return project.user_status(self) == 2
693

    
694

    
695
class AstakosUserAuthProviderManager(models.Manager):
696

    
697
    def active(self, **filters):
698
        return self.filter(active=True, **filters)
699

    
700

    
701
class AstakosUserAuthProvider(models.Model):
702
    """
703
    Available user authentication methods.
704
    """
705
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
706
                                   null=True, default=None)
707
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
708
    module = models.CharField(_('Provider'), max_length=255, blank=False,
709
                                default='local')
710
    identifier = models.CharField(_('Third-party identifier'),
711
                                              max_length=255, null=True,
712
                                              blank=True)
713
    active = models.BooleanField(default=True)
714
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
715
                                   default='astakos')
716
    info_data = models.TextField(default="", null=True, blank=True)
717
    created = models.DateTimeField('Creation date', auto_now_add=True)
718

    
719
    objects = AstakosUserAuthProviderManager()
720

    
721
    class Meta:
722
        unique_together = (('identifier', 'module', 'user'), )
723
        ordering = ('module', 'created')
724

    
725
    def __init__(self, *args, **kwargs):
726
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
727
        try:
728
            self.info = json.loads(self.info_data)
729
            if not self.info:
730
                self.info = {}
731
        except Exception, e:
732
            self.info = {}
733

    
734
        for key,value in self.info.iteritems():
735
            setattr(self, 'info_%s' % key, value)
736

    
737

    
738
    @property
739
    def settings(self):
740
        return auth_providers.get_provider(self.module)
741

    
742
    @property
743
    def details_display(self):
744
        try:
745
          return self.settings.get_details_tpl_display % self.__dict__
746
        except:
747
          return ''
748

    
749
    @property
750
    def title_display(self):
751
        title_tpl = self.settings.get_title_display
752
        try:
753
            if self.settings.get_user_title_display:
754
                title_tpl = self.settings.get_user_title_display
755
        except Exception, e:
756
            pass
757
        try:
758
          return title_tpl % self.__dict__
759
        except:
760
          return self.settings.get_title_display % self.__dict__
761

    
762
    def can_remove(self):
763
        return self.user.can_remove_auth_provider(self.module)
764

    
765
    def delete(self, *args, **kwargs):
766
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
767
        if self.module == 'local':
768
            self.user.set_unusable_password()
769
            self.user.save()
770
        return ret
771

    
772
    def __repr__(self):
773
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
774

    
775
    def __unicode__(self):
776
        if self.identifier:
777
            return "%s:%s" % (self.module, self.identifier)
778
        if self.auth_backend:
779
            return "%s:%s" % (self.module, self.auth_backend)
780
        return self.module
781

    
782
    def save(self, *args, **kwargs):
783
        self.info_data = json.dumps(self.info)
784
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
785

    
786

    
787
class ExtendedManager(models.Manager):
788
    def _update_or_create(self, **kwargs):
789
        assert kwargs, \
790
            'update_or_create() must be passed at least one keyword argument'
791
        obj, created = self.get_or_create(**kwargs)
792
        defaults = kwargs.pop('defaults', {})
793
        if created:
794
            return obj, True, False
795
        else:
796
            try:
797
                params = dict(
798
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
799
                params.update(defaults)
800
                for attr, val in params.items():
801
                    if hasattr(obj, attr):
802
                        setattr(obj, attr, val)
803
                sid = transaction.savepoint()
804
                obj.save(force_update=True)
805
                transaction.savepoint_commit(sid)
806
                return obj, False, True
807
            except IntegrityError, e:
808
                transaction.savepoint_rollback(sid)
809
                try:
810
                    return self.get(**kwargs), False, False
811
                except self.model.DoesNotExist:
812
                    raise e
813

    
814
    update_or_create = _update_or_create
815

    
816

    
817
class AstakosUserQuota(models.Model):
818
    objects = ExtendedManager()
819
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
820
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
821
    resource = models.ForeignKey(Resource)
822
    user = models.ForeignKey(AstakosUser)
823

    
824
    class Meta:
825
        unique_together = ("resource", "user")
826

    
827

    
828
class ApprovalTerms(models.Model):
829
    """
830
    Model for approval terms
831
    """
832

    
833
    date = models.DateTimeField(
834
        _('Issue date'), db_index=True, default=datetime.now())
835
    location = models.CharField(_('Terms location'), max_length=255)
836

    
837

    
838
class Invitation(models.Model):
839
    """
840
    Model for registring invitations
841
    """
842
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
843
                                null=True)
844
    realname = models.CharField(_('Real name'), max_length=255)
845
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
846
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
847
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
848
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
849
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
850

    
851
    def __init__(self, *args, **kwargs):
852
        super(Invitation, self).__init__(*args, **kwargs)
853
        if not self.id:
854
            self.code = _generate_invitation_code()
855

    
856
    def consume(self):
857
        self.is_consumed = True
858
        self.consumed = datetime.now()
859
        self.save()
860

    
861
    def __unicode__(self):
862
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
863

    
864

    
865
class EmailChangeManager(models.Manager):
866

    
867
    @transaction.commit_on_success
868
    def change_email(self, activation_key):
869
        """
870
        Validate an activation key and change the corresponding
871
        ``User`` if valid.
872

873
        If the key is valid and has not expired, return the ``User``
874
        after activating.
875

876
        If the key is not valid or has expired, return ``None``.
877

878
        If the key is valid but the ``User`` is already active,
879
        return ``None``.
880

881
        After successful email change the activation record is deleted.
882

883
        Throws ValueError if there is already
884
        """
885
        try:
886
            email_change = self.model.objects.get(
887
                activation_key=activation_key)
888
            if email_change.activation_key_expired():
889
                email_change.delete()
890
                raise EmailChange.DoesNotExist
891
            # is there an active user with this address?
892
            try:
893
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
894
            except AstakosUser.DoesNotExist:
895
                pass
896
            else:
897
                raise ValueError(_('The new email address is reserved.'))
898
            # update user
899
            user = AstakosUser.objects.get(pk=email_change.user_id)
900
            old_email = user.email
901
            user.email = email_change.new_email_address
902
            user.save()
903
            email_change.delete()
904
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
905
                                                          user.email)
906
            logger.log(LOGGING_LEVEL, msg)
907
            return user
908
        except EmailChange.DoesNotExist:
909
            raise ValueError(_('Invalid activation key.'))
910

    
911

    
912
class EmailChange(models.Model):
913
    new_email_address = models.EmailField(
914
        _(u'new e-mail address'),
915
        help_text=_('Your old email address will be used until you verify your new one.'))
916
    user = models.ForeignKey(
917
        AstakosUser, unique=True, related_name='emailchanges')
918
    requested_at = models.DateTimeField(default=datetime.now())
919
    activation_key = models.CharField(
920
        max_length=40, unique=True, db_index=True)
921

    
922
    objects = EmailChangeManager()
923

    
924
    def get_url(self):
925
        return reverse('email_change_confirm',
926
                      kwargs={'activation_key': self.activation_key})
927

    
928
    def activation_key_expired(self):
929
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
930
        return self.requested_at + expiration_date < datetime.now()
931

    
932

    
933
class AdditionalMail(models.Model):
934
    """
935
    Model for registring invitations
936
    """
937
    owner = models.ForeignKey(AstakosUser)
938
    email = models.EmailField()
939

    
940

    
941
def _generate_invitation_code():
942
    while True:
943
        code = randint(1, 2L ** 63 - 1)
944
        try:
945
            Invitation.objects.get(code=code)
946
            # An invitation with this code already exists, try again
947
        except Invitation.DoesNotExist:
948
            return code
949

    
950

    
951
def get_latest_terms():
952
    try:
953
        term = ApprovalTerms.objects.order_by('-id')[0]
954
        return term
955
    except IndexError:
956
        pass
957
    return None
958

    
959
class PendingThirdPartyUser(models.Model):
960
    """
961
    Model for registring successful third party user authentications
962
    """
963
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
964
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
965
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
966
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
967
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
968
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
969
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
970
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
971
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
972
    info = models.TextField(default="", null=True, blank=True)
973

    
974
    class Meta:
975
        unique_together = ("provider", "third_party_identifier")
976

    
977
    def get_user_instance(self):
978
        d = self.__dict__
979
        d.pop('_state', None)
980
        d.pop('id', None)
981
        d.pop('token', None)
982
        d.pop('created', None)
983
        d.pop('info', None)
984
        user = AstakosUser(**d)
985

    
986
        return user
987

    
988
    @property
989
    def realname(self):
990
        return '%s %s' %(self.first_name, self.last_name)
991

    
992
    @realname.setter
993
    def realname(self, value):
994
        parts = value.split(' ')
995
        if len(parts) == 2:
996
            self.first_name = parts[0]
997
            self.last_name = parts[1]
998
        else:
999
            self.last_name = parts[0]
1000

    
1001
    def save(self, **kwargs):
1002
        if not self.id:
1003
            # set username
1004
            while not self.username:
1005
                username =  uuid.uuid4().hex[:30]
1006
                try:
1007
                    AstakosUser.objects.get(username = username)
1008
                except AstakosUser.DoesNotExist, e:
1009
                    self.username = username
1010
        super(PendingThirdPartyUser, self).save(**kwargs)
1011

    
1012
    def generate_token(self):
1013
        self.password = self.third_party_identifier
1014
        self.last_login = datetime.now()
1015
        self.token = default_token_generator.make_token(self)
1016

    
1017
class SessionCatalog(models.Model):
1018
    session_key = models.CharField(_('session key'), max_length=40)
1019
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1020

    
1021

    
1022
### PROJECTS ###
1023
################
1024

    
1025
def synced_model_metaclass(class_name, class_parents, class_attributes):
1026

    
1027
    new_attributes = {}
1028
    sync_attributes = {}
1029

    
1030
    for name, value in class_attributes.iteritems():
1031
        sync, underscore, rest = name.partition('_')
1032
        if sync == 'sync' and underscore == '_':
1033
            sync_attributes[rest] = value
1034
        else:
1035
            new_attributes[name] = value
1036

    
1037
    if 'prefix' not in sync_attributes:
1038
        m = ("you did not specify a 'sync_prefix' attribute "
1039
             "in class '%s'" % (class_name,))
1040
        raise ValueError(m)
1041

    
1042
    prefix = sync_attributes.pop('prefix')
1043
    class_name = sync_attributes.pop('classname', prefix + '_model')
1044

    
1045
    for name, value in sync_attributes.iteritems():
1046
        newname = prefix + '_' + name
1047
        if newname in new_attributes:
1048
            m = ("class '%s' was specified with prefix '%s' "
1049
                 "but it already has an attribute named '%s'"
1050
                 % (class_name, prefix, newname))
1051
            raise ValueError(m)
1052

    
1053
        new_attributes[newname] = value
1054

    
1055
    newclass = type(class_name, class_parents, new_attributes)
1056
    return newclass
1057

    
1058

    
1059
def make_synced(prefix='sync', name='SyncedState'):
1060

    
1061
    the_name = name
1062
    the_prefix = prefix
1063

    
1064
    class SyncedState(models.Model):
1065

    
1066
        sync_classname      = the_name
1067
        sync_prefix         = the_prefix
1068
        __metaclass__       = synced_model_metaclass
1069

    
1070
        sync_new_state      = models.BigIntegerField(null=True)
1071
        sync_synced_state   = models.BigIntegerField(null=True)
1072
        STATUS_SYNCED       = 0
1073
        STATUS_PENDING      = 1
1074
        sync_status         = models.IntegerField(db_index=True)
1075

    
1076
        class Meta:
1077
            abstract = True
1078

    
1079
        class NotSynced(Exception):
1080
            pass
1081

    
1082
        def sync_init_state(self, state):
1083
            self.sync_synced_state = state
1084
            self.sync_new_state = state
1085
            self.sync_status = self.STATUS_SYNCED
1086

    
1087
        def sync_get_status(self):
1088
            return self.sync_status
1089

    
1090
        def sync_set_status(self):
1091
            if self.sync_new_state != self.sync_synced_state:
1092
                self.sync_status = self.STATUS_PENDING
1093
            else:
1094
                self.sync_status = self.STATUS_SYNCED
1095

    
1096
        def sync_set_synced(self):
1097
            self.sync_synced_state = self.sync_new_state
1098
            self.sync_status = self.STATUS_SYNCED
1099

    
1100
        def sync_get_synced_state(self):
1101
            return self.sync_synced_state
1102

    
1103
        def sync_set_new_state(self, new_state):
1104
            self.sync_new_state = new_state
1105
            self.sync_set_status()
1106

    
1107
        def sync_get_new_state(self):
1108
            return self.sync_new_state
1109

    
1110
        def sync_set_synced_state(self, synced_state):
1111
            self.sync_synced_state = synced_state
1112
            self.sync_set_status()
1113

    
1114
        def sync_get_pending_objects(self):
1115
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1116
            return self.objects.filter(**kw)
1117

    
1118
        def sync_get_synced_objects(self):
1119
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1120
            return self.objects.filter(**kw)
1121

    
1122
        def sync_verify_get_synced_state(self):
1123
            status = self.sync_get_status()
1124
            state = self.sync_get_synced_state()
1125
            verified = (status == self.STATUS_SYNCED)
1126
            return state, verified
1127

    
1128
        def sync_is_synced(self):
1129
            state, verified = self.sync_verify_get_synced_state()
1130
            return verified
1131

    
1132
    return SyncedState
1133

    
1134
SyncedState = make_synced(prefix='sync', name='SyncedState')
1135

    
1136

    
1137
class ProjectApplicationManager(ForUpdateManager):
1138

    
1139
    def user_projects(self, user):
1140
        """
1141
        Return projects accessed by specified user.
1142
        """
1143
        participates_fitlers = Q(owner=user) | Q(applicant=user) | \
1144
                               Q(project__projectmembership__person=user)
1145
        state_filters = (Q(state=ProjectApplication.PENDING) & \
1146
                        Q(precursor_application__isnull=True)) | \
1147
                        Q(state=ProjectApplication.APPROVED)
1148
        return self.filter(participates_fitlers & state_filters).order_by('issue_date').distinct()
1149

    
1150
    def search_by_name(self, *search_strings):
1151
        q = Q()
1152
        for s in search_strings:
1153
            q = q | Q(name__icontains=s)
1154
        return self.filter(q)
1155

    
1156

    
1157
PROJECT_STATE_DISPLAY = {
1158
    'Pending': _('Pending review'),
1159
    'Approved': _('Active'),
1160
    'Replaced': _('Replaced'),
1161
    'Unknown': _('Unknown')
1162
}
1163

    
1164
USER_STATUS_DISPLAY = {
1165
    100: _('Owner'),
1166
      0: _('Join requested'),
1167
      1: _('Pending'),
1168
      2: _('Accepted member'),
1169
      3: _('Removing'),
1170
      4: _('Removed'),
1171
     -1: _('Not a member'),
1172
}
1173

    
1174
class ProjectApplication(models.Model):
1175
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1176
    applicant               =   models.ForeignKey(
1177
                                    AstakosUser,
1178
                                    related_name='projects_applied',
1179
                                    db_index=True)
1180

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

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

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

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

    
1210

    
1211
    objects                 =   ProjectApplicationManager()
1212

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

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

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

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

    
1243
        return status
1244

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

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

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

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

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

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

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

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

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

    
1300
        return None
1301

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

1307
        Raises:
1308
            PermissionDenied
1309
        """
1310

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

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

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

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

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

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

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

    
1349
        precursor = self.precursor_application
1350
        while precursor:
1351
            precursor.state = self.REPLACED
1352
            precursor.save()
1353
            precursor = precursor.precursor_application
1354

    
1355
        self.state = self.APPROVED
1356
        self.save()
1357

    
1358
def submit_application(**kw):
1359

    
1360
    resource_policies = kw.pop('resource_policies', None)
1361
    application = ProjectApplication(**kw)
1362

    
1363
    precursor = kw['precursor_application']
1364

    
1365
    if precursor is not None:
1366
        precursor.state = ProjectApplication.REPLACED
1367
        precursor.save()
1368

    
1369
    application.save()
1370
    application.resource_policies = resource_policies
1371
    return application
1372

    
1373
class ProjectResourceGrant(models.Model):
1374

    
1375
    resource                =   models.ForeignKey(Resource)
1376
    project_application     =   models.ForeignKey(ProjectApplication,
1377
                                                  null=True)
1378
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1379
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1380
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1381
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1382
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1383
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1384

    
1385
    objects = ExtendedManager()
1386

    
1387
    class Meta:
1388
        unique_together = ("resource", "project_application")
1389

    
1390

    
1391
class ProjectManager(ForUpdateManager):
1392

    
1393
    def _q_terminated(self):
1394
        return Q(state=Project.TERMINATED)
1395

    
1396
    def terminated_projects(self):
1397
        q = self._q_terminated()
1398
        return self.filter(q)
1399

    
1400
    def not_terminated_projects(self):
1401
        q = ~self._q_terminated()
1402
        return self.filter(q)
1403

    
1404
    def terminating_projects(self):
1405
        q = self._q_terminated() & Q(is_active=True)
1406
        return self.filter(q)
1407

    
1408
    def modified_projects(self):
1409
        return self.filter(is_modified=True)
1410

    
1411

    
1412
class Project(models.Model):
1413

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

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

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

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

    
1432
    APPROVED    = 1
1433
    SUSPENDED   = 10
1434
    TERMINATED  = 100
1435

    
1436
    is_modified                 =   models.BooleanField(default=False,
1437
                                                        db_index=True)
1438
    is_active                   =   models.BooleanField(default=True,
1439
                                                        db_index=True)
1440
    state                       =   models.IntegerField(default=APPROVED,
1441
                                                        db_index=True)
1442

    
1443
    objects     =   ProjectManager()
1444

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

    
1448
    __repr__ = __str__
1449

    
1450
    def is_deactivated(self, reason=None):
1451
        if reason is not None:
1452
            return self.state == reason
1453

    
1454
        return self.state != self.APPROVED
1455

    
1456
    def is_deactivating(self, reason=None):
1457
        if not self.is_active:
1458
            return False
1459

    
1460
        return self.is_deactivated(reason)
1461

    
1462
    def is_deactivated_strict(self, reason=None):
1463
        if self.is_active:
1464
            return False
1465

    
1466
        return self.is_deactivated(reason)
1467

    
1468
    ### Deactivation calls
1469

    
1470
    def deactivate(self):
1471
        self.deactivation_date = datetime.now()
1472
        self.is_active = False
1473

    
1474
    def terminate(self):
1475
        self.deactivation_reason = 'TERMINATED'
1476
        self.state = self.TERMINATED
1477
        self.save()
1478

    
1479

    
1480
    ### Logical checks
1481

    
1482
    def is_inconsistent(self):
1483
        now = datetime.now()
1484
        dates = [self.creation_date,
1485
                 self.last_approval_date,
1486
                 self.deactivation_date]
1487
        return any([date > now for date in dates])
1488

    
1489
    def is_active_strict(self):
1490
        return self.is_active and self.state == self.APPROVED
1491

    
1492
    @property
1493
    def is_alive(self):
1494
        return self.is_active_strict()
1495

    
1496
    @property
1497
    def is_terminated(self):
1498
        return self.is_deactivated(self.TERMINATED)
1499

    
1500
    @property
1501
    def is_suspended(self):
1502
        return False
1503

    
1504
    def violates_resource_grants(self):
1505
        return False
1506

    
1507
    def violates_members_limit(self, adding=0):
1508
        application = self.application
1509
        return (len(self.approved_members) + adding >
1510
                application.limit_on_members_number)
1511

    
1512

    
1513
    ### Other
1514

    
1515
    @property
1516
    def approved_memberships(self):
1517
        query = ProjectMembership.query_approved()
1518
        return self.projectmembership_set.filter(query)
1519

    
1520
    @property
1521
    def approved_members(self):
1522
        return [m.person for m in self.approved_memberships]
1523

    
1524
    def add_member(self, user):
1525
        """
1526
        Raises:
1527
            django.exceptions.PermissionDenied
1528
            astakos.im.models.AstakosUser.DoesNotExist
1529
        """
1530
        if isinstance(user, int):
1531
            user = AstakosUser.objects.get(user=user)
1532

    
1533
        m, created = ProjectMembership.objects.get_or_create(
1534
            person=user, project=self
1535
        )
1536
        m.accept()
1537

    
1538
    def remove_member(self, user):
1539
        """
1540
        Raises:
1541
            django.exceptions.PermissionDenied
1542
            astakos.im.models.AstakosUser.DoesNotExist
1543
            astakos.im.models.ProjectMembership.DoesNotExist
1544
        """
1545
        if isinstance(user, int):
1546
            user = AstakosUser.objects.get(user=user)
1547

    
1548
        m = ProjectMembership.objects.get(person=user, project=self)
1549
        m.remove()
1550

    
1551

    
1552
class PendingMembershipError(Exception):
1553
    pass
1554

    
1555

    
1556
class ProjectMembership(models.Model):
1557

    
1558
    person              =   models.ForeignKey(AstakosUser)
1559
    request_date        =   models.DateField(default=datetime.now())
1560
    project             =   models.ForeignKey(Project)
1561

    
1562
    REQUESTED   =   0
1563
    ACCEPTED    =   1
1564
    SUSPENDED   =   10
1565
    TERMINATED  =   100
1566
    REMOVED     =   200
1567

    
1568
    state               =   models.IntegerField(default=REQUESTED,
1569
                                                db_index=True)
1570
    is_pending          =   models.BooleanField(default=False, db_index=True)
1571
    is_active           =   models.BooleanField(default=False, db_index=True)
1572
    application         =   models.ForeignKey(
1573
                                ProjectApplication,
1574
                                null=True,
1575
                                related_name='memberships')
1576
    pending_application =   models.ForeignKey(
1577
                                ProjectApplication,
1578
                                null=True,
1579
                                related_name='pending_memebrships')
1580
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1581

    
1582
    acceptance_date     =   models.DateField(null=True, db_index=True)
1583
    leave_request_date  =   models.DateField(null=True)
1584

    
1585
    objects     =   ForUpdateManager()
1586

    
1587

    
1588
    def get_combined_state(self):
1589
        return self.state, self.is_active, self.is_pending
1590

    
1591
    @classmethod
1592
    def query_approved(cls):
1593
        return (~Q(state=cls.REQUESTED) &
1594
                ~Q(state=cls.REMOVED))
1595

    
1596
    class Meta:
1597
        unique_together = ("person", "project")
1598
        #index_together = [["project", "state"]]
1599

    
1600
    def __str__(self):
1601
        return _("<'%s' membership in '%s'>") % (
1602
                self.person.username, self.project)
1603

    
1604
    __repr__ = __str__
1605

    
1606
    def __init__(self, *args, **kwargs):
1607
        self.state = self.REQUESTED
1608
        super(ProjectMembership, self).__init__(*args, **kwargs)
1609

    
1610
    def _set_history_item(self, reason, date=None):
1611
        if isinstance(reason, basestring):
1612
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1613

    
1614
        history_item = ProjectMembershipHistory(
1615
                            serial=self.id,
1616
                            person=self.person.uuid,
1617
                            project=self.project_id,
1618
                            date=date or datetime.now(),
1619
                            reason=reason)
1620
        history_item.save()
1621
        serial = history_item.id
1622

    
1623
    def accept(self):
1624
        if self.is_pending:
1625
            m = _("%s: attempt to accept while is pending") % (self,)
1626
            raise AssertionError(m)
1627

    
1628
        state = self.state
1629
        if state != self.REQUESTED:
1630
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1631
            raise AssertionError(m)
1632

    
1633
        now = datetime.now()
1634
        self.acceptance_date = now
1635
        self._set_history_item(reason='ACCEPT', date=now)
1636
        if self.project.is_active_strict():
1637
            self.state = self.ACCEPTED
1638
            self.is_pending = True
1639
        else:
1640
            self.state = self.TERMINATED
1641

    
1642
        self.save()
1643

    
1644
    def remove(self):
1645
        if self.is_pending:
1646
            m = _("%s: attempt to remove while is pending") % (self,)
1647
            raise AssertionError(m)
1648

    
1649
        state = self.state
1650
        if state not in [self.ACCEPTED, self.TERMINATED]:
1651
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1652
            raise AssertionError(m)
1653

    
1654
        self._set_history_item(reason='REMOVE')
1655
        self.state = self.REMOVED
1656
        self.is_pending = True
1657
        self.save()
1658

    
1659
    def reject(self):
1660
        if self.is_pending:
1661
            m = _("%s: attempt to reject while is pending") % (self,)
1662
            raise AssertionError(m)
1663

    
1664
        state = self.state
1665
        if state != self.REQUESTED:
1666
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1667
            raise AssertionError(m)
1668

    
1669
        # rejected requests don't need sync,
1670
        # because they were never effected
1671
        self._set_history_item(reason='REJECT')
1672
        self.delete()
1673

    
1674
    def get_diff_quotas(self, sub_list=None, add_list=None):
1675
        if sub_list is None:
1676
            sub_list = []
1677

    
1678
        if add_list is None:
1679
            add_list = []
1680

    
1681
        sub_append = sub_list.append
1682
        add_append = add_list.append
1683
        holder = self.person.uuid
1684

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

    
1696
        pending_application = self.pending_application
1697
        if pending_application is not None:
1698
            new_grants = pending_application.projectresourcegrant_set.all()
1699
            for new_grant in new_grants:
1700
                add_append(QuotaLimits(
1701
                               holder       = holder,
1702
                               resource     = str(new_grant.resource),
1703
                               capacity     = new_grant.member_capacity,
1704
                               import_limit = new_grant.member_import_limit,
1705
                               export_limit = new_grant.member_export_limit))
1706

    
1707
        return (sub_list, add_list)
1708

    
1709
    def set_sync(self):
1710
        if not self.is_pending:
1711
            m = _("%s: attempt to sync a non pending membership") % (self,)
1712
            raise AssertionError(m)
1713

    
1714
        state = self.state
1715
        if state == self.ACCEPTED:
1716
            pending_application = self.pending_application
1717
            if pending_application is None:
1718
                m = _("%s: attempt to sync an empty pending application") % (
1719
                    self,)
1720
                raise AssertionError(m)
1721

    
1722
            self.application = pending_application
1723
            self.is_active = True
1724

    
1725
            self.pending_application = None
1726
            self.pending_serial = None
1727

    
1728
            # project.application may have changed in the meantime,
1729
            # in which case we stay PENDING;
1730
            # we are safe to check due to select_for_update
1731
            if self.application == self.project.application:
1732
                self.is_pending = False
1733
            self.save()
1734

    
1735
        elif state == self.TERMINATED:
1736
            if self.pending_application:
1737
                m = _("%s: attempt to sync in state '%s' "
1738
                      "with a pending application") % (self, state)
1739
                raise AssertionError(m)
1740

    
1741
            self.application = None
1742
            self.pending_serial = None
1743
            self.is_pending = False
1744
            self.save()
1745

    
1746
        elif state == self.REMOVED:
1747
            self.delete()
1748

    
1749
        else:
1750
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1751
            raise AssertionError(m)
1752

    
1753
    def reset_sync(self):
1754
        if not self.is_pending:
1755
            m = _("%s: attempt to reset a non pending membership") % (self,)
1756
            raise AssertionError(m)
1757

    
1758
        state = self.state
1759
        if state in [self.ACCEPTED, self.TERMINATED, self.REMOVED]:
1760
            self.pending_application = None
1761
            self.pending_serial = None
1762
            self.save()
1763
        else:
1764
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1765
            raise AssertionError(m)
1766

    
1767
class Serial(models.Model):
1768
    serial  =   models.AutoField(primary_key=True)
1769

    
1770
def new_serial():
1771
    s = Serial.objects.create()
1772
    serial = s.serial
1773
    s.delete()
1774
    return serial
1775

    
1776
def sync_finish_serials(serials_to_ack=None):
1777
    if serials_to_ack is None:
1778
        serials_to_ack = qh_query_serials([])
1779

    
1780
    serials_to_ack = set(serials_to_ack)
1781
    sfu = ProjectMembership.objects.select_for_update()
1782
    memberships = list(sfu.filter(pending_serial__isnull=False))
1783

    
1784
    if memberships:
1785
        for membership in memberships:
1786
            serial = membership.pending_serial
1787
            if serial in serials_to_ack:
1788
                membership.set_sync()
1789
            else:
1790
                membership.reset_sync()
1791

    
1792
        transaction.commit()
1793

    
1794
    qh_ack_serials(list(serials_to_ack))
1795
    return len(memberships)
1796

    
1797
def pre_sync():
1798
    ACCEPTED = ProjectMembership.ACCEPTED
1799
    TERMINATED = ProjectMembership.TERMINATED
1800
    psfu = Project.objects.select_for_update()
1801

    
1802
    modified = psfu.modified_projects()
1803
    for project in modified:
1804
        objects = project.projectmembership_set.select_for_update()
1805

    
1806
        memberships = objects.filter(state=ACCEPTED)
1807
        for membership in memberships:
1808
            membership.is_pending = True
1809
            membership.save()
1810

    
1811
    terminating = psfu.terminating_projects()
1812
    for project in terminating:
1813
        objects = project.projectmembership_set.select_for_update()
1814

    
1815
        memberships = objects.filter(state=ACCEPTED)
1816
        for membership in memberships:
1817
            membership.is_pending = True
1818
            membership.state = TERMINATED
1819
            membership.save()
1820

    
1821
def do_sync():
1822

    
1823
    ACCEPTED = ProjectMembership.ACCEPTED
1824
    objects = ProjectMembership.objects.select_for_update()
1825

    
1826
    sub_quota, add_quota = [], []
1827

    
1828
    serial = new_serial()
1829

    
1830
    pending = objects.filter(is_pending=True)
1831
    for membership in pending:
1832

    
1833
        if membership.pending_application:
1834
            m = "%s: impossible: pending_application is not None (%s)" % (
1835
                membership, membership.pending_application)
1836
            raise AssertionError(m)
1837
        if membership.pending_serial:
1838
            m = "%s: impossible: pending_serial is not None (%s)" % (
1839
                membership, membership.pending_serial)
1840
            raise AssertionError(m)
1841

    
1842
        if membership.state == ACCEPTED:
1843
            membership.pending_application = membership.project.application
1844

    
1845
        membership.pending_serial = serial
1846
        membership.get_diff_quotas(sub_quota, add_quota)
1847
        membership.save()
1848

    
1849
    transaction.commit()
1850
    # ProjectApplication.approve() unblocks here
1851
    # and can set PENDING an already PENDING membership
1852
    # which has been scheduled to sync with the old project.application
1853
    # Need to check in ProjectMembership.set_sync()
1854

    
1855
    r = qh_add_quota(serial, sub_quota, add_quota)
1856
    if r:
1857
        m = "cannot sync serial: %d" % serial
1858
        raise RuntimeError(m)
1859

    
1860
    return serial
1861

    
1862
def post_sync():
1863
    ACCEPTED = ProjectMembership.ACCEPTED
1864
    psfu = Project.objects.select_for_update()
1865

    
1866
    modified = psfu.modified_projects()
1867
    for project in modified:
1868
        objects = project.projectmembership_set.select_for_update()
1869

    
1870
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
1871
        if not memberships:
1872
            project.is_modified = False
1873
            project.save()
1874

    
1875
    terminating = psfu.terminating_projects()
1876
    for project in terminating:
1877
        objects = project.projectmembership_set.select_for_update()
1878

    
1879
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1880
                                          Q(is_pending=True)))
1881
        if not memberships:
1882
            project.deactivate()
1883
            project.save()
1884

    
1885
    transaction.commit()
1886

    
1887
def sync_projects():
1888
    sync_finish_serials()
1889
    pre_sync()
1890
    serial = do_sync()
1891
    sync_finish_serials([serial])
1892
    post_sync()
1893

    
1894
def trigger_sync(retries=3, retry_wait=1.0):
1895
    transaction.commit()
1896

    
1897
    cursor = connection.cursor()
1898
    locked = True
1899
    try:
1900
        while 1:
1901
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1902
            r = cursor.fetchone()
1903
            if r is None:
1904
                m = "Impossible"
1905
                raise AssertionError(m)
1906
            locked = r[0]
1907
            if locked:
1908
                break
1909

    
1910
            retries -= 1
1911
            if retries <= 0:
1912
                return False
1913
            sleep(retry_wait)
1914

    
1915
        sync_projects()
1916
        return True
1917

    
1918
    finally:
1919
        if locked:
1920
            cursor.execute("SELECT pg_advisory_unlock(1)")
1921
            cursor.fetchall()
1922

    
1923

    
1924
class ProjectMembershipHistory(models.Model):
1925
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1926
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1927

    
1928
    person  =   models.CharField(max_length=255)
1929
    project =   models.BigIntegerField()
1930
    date    =   models.DateField(default=datetime.now)
1931
    reason  =   models.IntegerField()
1932
    serial  =   models.BigIntegerField()
1933

    
1934
### SIGNALS ###
1935
################
1936

    
1937
def create_astakos_user(u):
1938
    try:
1939
        AstakosUser.objects.get(user_ptr=u.pk)
1940
    except AstakosUser.DoesNotExist:
1941
        extended_user = AstakosUser(user_ptr_id=u.pk)
1942
        extended_user.__dict__.update(u.__dict__)
1943
        extended_user.save()
1944
        if not extended_user.has_auth_provider('local'):
1945
            extended_user.add_auth_provider('local')
1946
    except BaseException, e:
1947
        logger.exception(e)
1948

    
1949

    
1950
def fix_superusers(sender, **kwargs):
1951
    # Associate superusers with AstakosUser
1952
    admins = User.objects.filter(is_superuser=True)
1953
    for u in admins:
1954
        create_astakos_user(u)
1955
post_syncdb.connect(fix_superusers)
1956

    
1957

    
1958
def user_post_save(sender, instance, created, **kwargs):
1959
    if not created:
1960
        return
1961
    create_astakos_user(instance)
1962
post_save.connect(user_post_save, sender=User)
1963

    
1964
def astakosuser_post_save(sender, instance, created, **kwargs):
1965
    if not created:
1966
        return
1967
    # TODO handle socket.error & IOError
1968
    register_users((instance,))
1969
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1970

    
1971
def resource_post_save(sender, instance, created, **kwargs):
1972
    if not created:
1973
        return
1974
    register_resources((instance,))
1975
post_save.connect(resource_post_save, sender=Resource)
1976

    
1977
def renew_token(sender, instance, **kwargs):
1978
    if not instance.auth_token:
1979
        instance.renew_token()
1980
pre_save.connect(renew_token, sender=AstakosUser)
1981
pre_save.connect(renew_token, sender=Service)
1982