Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 9e5eecab

History | View | Annotate | Download (66.5 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_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 Chain(models.Model):
1175
    chain  =   models.AutoField(primary_key=True)
1176

    
1177
def new_chain():
1178
    c = Chain.objects.create()
1179
    chain = c.chain
1180
    c.delete()
1181
    return chain
1182

    
1183

    
1184
class ProjectApplication(models.Model):
1185
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1186
    applicant               =   models.ForeignKey(
1187
                                    AstakosUser,
1188
                                    related_name='projects_applied',
1189
                                    db_index=True)
1190

    
1191
    state                   =   models.CharField(max_length=80,
1192
                                                default=PENDING)
1193

    
1194
    owner                   =   models.ForeignKey(
1195
                                    AstakosUser,
1196
                                    related_name='projects_owned',
1197
                                    db_index=True)
1198

    
1199
    chain                   =   models.IntegerField(db_index=True)
1200
    precursor_application   =   models.OneToOneField('ProjectApplication',
1201
                                                     null=True,
1202
                                                     blank=True,
1203
                                                     db_index=True)
1204

    
1205
    name                    =   models.CharField(max_length=80)
1206
    homepage                =   models.URLField(max_length=255, null=True)
1207
    description             =   models.TextField(null=True, blank=True)
1208
    start_date              =   models.DateTimeField(null=True, blank=True)
1209
    end_date                =   models.DateTimeField()
1210
    member_join_policy      =   models.IntegerField()
1211
    member_leave_policy     =   models.IntegerField()
1212
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1213
    resource_grants         =   models.ManyToManyField(
1214
                                    Resource,
1215
                                    null=True,
1216
                                    blank=True,
1217
                                    through='ProjectResourceGrant')
1218
    comments                =   models.TextField(null=True, blank=True)
1219
    issue_date              =   models.DateTimeField(default=datetime.now)
1220

    
1221

    
1222
    objects                 =   ProjectApplicationManager()
1223

    
1224
    def __unicode__(self):
1225
        return "%s applied by %s" % (self.name, self.applicant)
1226

    
1227
    def state_display(self):
1228
        return PROJECT_STATE_DISPLAY.get(self.state, _('Unknown'))
1229

    
1230
    def add_resource_policy(self, service, resource, uplimit):
1231
        """Raises ObjectDoesNotExist, IntegrityError"""
1232
        q = self.projectresourcegrant_set
1233
        resource = Resource.objects.get(service__name=service, name=resource)
1234
        q.create(resource=resource, member_capacity=uplimit)
1235

    
1236
    def user_status(self, user):
1237
        """
1238
        100 OWNER
1239
        0   REQUESTED
1240
        1   PENDING
1241
        2   ACCEPTED
1242
        3   REMOVING
1243
        4   REMOVED
1244
       -1   User has no association with the project
1245
        """
1246
        try:
1247
            membership = self.project.projectmembership_set.get(person=user)
1248
            status = membership.state
1249
        except Project.DoesNotExist:
1250
            status = -1
1251
        except ProjectMembership.DoesNotExist:
1252
            status = -1
1253

    
1254
        return status
1255

    
1256
    def user_status_display(self, user):
1257
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1258

    
1259
    def members_count(self):
1260
        return self.project.approved_memberships.count()
1261

    
1262
    @property
1263
    def grants(self):
1264
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1265

    
1266
    @property
1267
    def resource_policies(self):
1268
        return self.projectresourcegrant_set.all()
1269

    
1270
    @resource_policies.setter
1271
    def resource_policies(self, policies):
1272
        for p in policies:
1273
            service = p.get('service', None)
1274
            resource = p.get('resource', None)
1275
            uplimit = p.get('uplimit', 0)
1276
            self.add_resource_policy(service, resource, uplimit)
1277

    
1278
    @property
1279
    def follower(self):
1280
        try:
1281
            return ProjectApplication.objects.get(precursor_application=self)
1282
        except ProjectApplication.DoesNotExist:
1283
            return
1284

    
1285
    def followers(self):
1286
        current = self
1287
        try:
1288
            while current.projectapplication:
1289
                yield current.follower
1290
                current = current.follower
1291
        except:
1292
            pass
1293

    
1294
    def last_follower(self):
1295
        try:
1296
            return list(self.followers())[-1]
1297
        except IndexError:
1298
            return None
1299

    
1300
    def _get_project_for_update(self):
1301
        try:
1302
            objects = Project.objects.select_for_update()
1303
            project = objects.get(id=self.chain)
1304
            return project
1305
        except Project.DoesNotExist:
1306
            return None
1307

    
1308
    def approve(self, approval_user=None):
1309
        """
1310
        If approval_user then during owner membership acceptance
1311
        it is checked whether the request_user is eligible.
1312

1313
        Raises:
1314
            PermissionDenied
1315
        """
1316

    
1317
        if not transaction.is_managed():
1318
            raise AssertionError("NOPE")
1319

    
1320
        new_project_name = self.name
1321
        if self.state != self.PENDING:
1322
            m = _("cannot approve: project '%s' in state '%s'") % (
1323
                    new_project_name, self.state)
1324
            raise PermissionDenied(m) # invalid argument
1325

    
1326
        now = datetime.now()
1327
        project = self._get_project_for_update()
1328

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

    
1341
        new_project = False
1342
        if project is None:
1343
            new_project = True
1344
            project = Project(id=self.chain, creation_date=now)
1345

    
1346
        project.name = new_project_name
1347
        project.application = self
1348
        project.last_approval_date = now
1349
        project.is_modified = True
1350
        project.save()
1351

    
1352
        if new_project:
1353
            project.add_member(self.owner)
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
        application.chain = precursor.chain
1369
    else:
1370
        application.chain = new_chain()
1371

    
1372
    application.save()
1373
    application.resource_policies = resource_policies
1374
    return application
1375

    
1376
class ProjectResourceGrant(models.Model):
1377

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

    
1388
    objects = ExtendedManager()
1389

    
1390
    class Meta:
1391
        unique_together = ("resource", "project_application")
1392

    
1393

    
1394
class ProjectManager(ForUpdateManager):
1395

    
1396
    def _q_terminated(self):
1397
        return Q(state=Project.TERMINATED)
1398

    
1399
    def terminated_projects(self):
1400
        q = self._q_terminated()
1401
        return self.filter(q)
1402

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

    
1407
    def terminating_projects(self):
1408
        q = self._q_terminated() & Q(is_active=True)
1409
        return self.filter(q)
1410

    
1411
    def modified_projects(self):
1412
        return self.filter(is_modified=True)
1413

    
1414

    
1415
class Project(models.Model):
1416

    
1417
    application                 =   models.OneToOneField(
1418
                                            ProjectApplication,
1419
                                            related_name='project')
1420
    last_approval_date          =   models.DateTimeField(null=True)
1421

    
1422
    members                     =   models.ManyToManyField(
1423
                                            AstakosUser,
1424
                                            through='ProjectMembership')
1425

    
1426
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1427
    deactivation_date           =   models.DateTimeField(null=True)
1428

    
1429
    creation_date               =   models.DateTimeField()
1430
    name                        =   models.CharField(
1431
                                            max_length=80,
1432
                                            db_index=True,
1433
                                            unique=True)
1434

    
1435
    APPROVED    = 1
1436
    SUSPENDED   = 10
1437
    TERMINATED  = 100
1438

    
1439
    is_modified                 =   models.BooleanField(default=False,
1440
                                                        db_index=True)
1441
    is_active                   =   models.BooleanField(default=True,
1442
                                                        db_index=True)
1443
    state                       =   models.IntegerField(default=APPROVED,
1444
                                                        db_index=True)
1445

    
1446
    objects     =   ProjectManager()
1447

    
1448
    def __str__(self):
1449
        return _("<project %s '%s'>") % (self.id, self.application.name)
1450

    
1451
    __repr__ = __str__
1452

    
1453
    def is_deactivated(self, reason=None):
1454
        if reason is not None:
1455
            return self.state == reason
1456

    
1457
        return self.state != self.APPROVED
1458

    
1459
    def is_deactivating(self, reason=None):
1460
        if not self.is_active:
1461
            return False
1462

    
1463
        return self.is_deactivated(reason)
1464

    
1465
    def is_deactivated_strict(self, reason=None):
1466
        if self.is_active:
1467
            return False
1468

    
1469
        return self.is_deactivated(reason)
1470

    
1471
    ### Deactivation calls
1472

    
1473
    def deactivate(self):
1474
        self.deactivation_date = datetime.now()
1475
        self.is_active = False
1476

    
1477
    def terminate(self):
1478
        self.deactivation_reason = 'TERMINATED'
1479
        self.state = self.TERMINATED
1480
        self.save()
1481

    
1482

    
1483
    ### Logical checks
1484

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

    
1492
    def is_active_strict(self):
1493
        return self.is_active and self.state == self.APPROVED
1494

    
1495
    @property
1496
    def is_alive(self):
1497
        return self.is_active_strict()
1498

    
1499
    @property
1500
    def is_terminated(self):
1501
        return self.is_deactivated(self.TERMINATED)
1502

    
1503
    @property
1504
    def is_suspended(self):
1505
        return False
1506

    
1507
    def violates_resource_grants(self):
1508
        return False
1509

    
1510
    def violates_members_limit(self, adding=0):
1511
        application = self.application
1512
        return (len(self.approved_members) + adding >
1513
                application.limit_on_members_number)
1514

    
1515

    
1516
    ### Other
1517

    
1518
    @property
1519
    def approved_memberships(self):
1520
        query = ProjectMembership.query_approved()
1521
        return self.projectmembership_set.filter(query)
1522

    
1523
    @property
1524
    def approved_members(self):
1525
        return [m.person for m in self.approved_memberships]
1526

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

    
1536
        m, created = ProjectMembership.objects.get_or_create(
1537
            person=user, project=self
1538
        )
1539
        m.accept()
1540

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

    
1551
        m = ProjectMembership.objects.get(person=user, project=self)
1552
        m.remove()
1553

    
1554

    
1555
class PendingMembershipError(Exception):
1556
    pass
1557

    
1558

    
1559
class ProjectMembership(models.Model):
1560

    
1561
    person              =   models.ForeignKey(AstakosUser)
1562
    request_date        =   models.DateField(default=datetime.now())
1563
    project             =   models.ForeignKey(Project)
1564

    
1565
    REQUESTED   =   0
1566
    ACCEPTED    =   1
1567
    SUSPENDED   =   10
1568
    TERMINATED  =   100
1569
    REMOVED     =   200
1570

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

    
1585
    acceptance_date     =   models.DateField(null=True, db_index=True)
1586
    leave_request_date  =   models.DateField(null=True)
1587

    
1588
    objects     =   ForUpdateManager()
1589

    
1590

    
1591
    def get_combined_state(self):
1592
        return self.state, self.is_active, self.is_pending
1593

    
1594
    @classmethod
1595
    def query_approved(cls):
1596
        return (~Q(state=cls.REQUESTED) &
1597
                ~Q(state=cls.REMOVED))
1598

    
1599
    class Meta:
1600
        unique_together = ("person", "project")
1601
        #index_together = [["project", "state"]]
1602

    
1603
    def __str__(self):
1604
        return _("<'%s' membership in '%s'>") % (
1605
                self.person.username, self.project)
1606

    
1607
    __repr__ = __str__
1608

    
1609
    def __init__(self, *args, **kwargs):
1610
        self.state = self.REQUESTED
1611
        super(ProjectMembership, self).__init__(*args, **kwargs)
1612

    
1613
    def _set_history_item(self, reason, date=None):
1614
        if isinstance(reason, basestring):
1615
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1616

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

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

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

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

    
1645
        self.save()
1646

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

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

    
1657
        self._set_history_item(reason='REMOVE')
1658
        self.state = self.REMOVED
1659
        self.is_pending = True
1660
        self.save()
1661

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

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

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

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

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

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

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

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

    
1710
        return (sub_list, add_list)
1711

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

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

    
1725
            self.application = pending_application
1726
            self.is_active = True
1727

    
1728
            self.pending_application = None
1729
            self.pending_serial = None
1730

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

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

    
1744
            self.application = None
1745
            self.pending_serial = None
1746
            self.is_pending = False
1747
            self.save()
1748

    
1749
        elif state == self.REMOVED:
1750
            self.delete()
1751

    
1752
        else:
1753
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1754
            raise AssertionError(m)
1755

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

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

    
1770
class Serial(models.Model):
1771
    serial  =   models.AutoField(primary_key=True)
1772

    
1773
def new_serial():
1774
    s = Serial.objects.create()
1775
    serial = s.serial
1776
    s.delete()
1777
    return serial
1778

    
1779
def sync_finish_serials(serials_to_ack=None):
1780
    if serials_to_ack is None:
1781
        serials_to_ack = qh_query_serials([])
1782

    
1783
    serials_to_ack = set(serials_to_ack)
1784
    sfu = ProjectMembership.objects.select_for_update()
1785
    memberships = list(sfu.filter(pending_serial__isnull=False))
1786

    
1787
    if memberships:
1788
        for membership in memberships:
1789
            serial = membership.pending_serial
1790
            if serial in serials_to_ack:
1791
                membership.set_sync()
1792
            else:
1793
                membership.reset_sync()
1794

    
1795
        transaction.commit()
1796

    
1797
    qh_ack_serials(list(serials_to_ack))
1798
    return len(memberships)
1799

    
1800
def pre_sync():
1801
    ACCEPTED = ProjectMembership.ACCEPTED
1802
    TERMINATED = ProjectMembership.TERMINATED
1803
    psfu = Project.objects.select_for_update()
1804

    
1805
    modified = psfu.modified_projects()
1806
    for project in modified:
1807
        objects = project.projectmembership_set.select_for_update()
1808

    
1809
        memberships = objects.filter(state=ACCEPTED)
1810
        for membership in memberships:
1811
            membership.is_pending = True
1812
            membership.save()
1813

    
1814
    terminating = psfu.terminating_projects()
1815
    for project in terminating:
1816
        objects = project.projectmembership_set.select_for_update()
1817

    
1818
        memberships = objects.filter(state=ACCEPTED)
1819
        for membership in memberships:
1820
            membership.is_pending = True
1821
            membership.state = TERMINATED
1822
            membership.save()
1823

    
1824
def do_sync():
1825

    
1826
    ACCEPTED = ProjectMembership.ACCEPTED
1827
    objects = ProjectMembership.objects.select_for_update()
1828

    
1829
    sub_quota, add_quota = [], []
1830

    
1831
    serial = new_serial()
1832

    
1833
    pending = objects.filter(is_pending=True)
1834
    for membership in pending:
1835

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

    
1845
        if membership.state == ACCEPTED:
1846
            membership.pending_application = membership.project.application
1847

    
1848
        membership.pending_serial = serial
1849
        membership.get_diff_quotas(sub_quota, add_quota)
1850
        membership.save()
1851

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

    
1858
    r = qh_add_quota(serial, sub_quota, add_quota)
1859
    if r:
1860
        m = "cannot sync serial: %d" % serial
1861
        raise RuntimeError(m)
1862

    
1863
    return serial
1864

    
1865
def post_sync():
1866
    ACCEPTED = ProjectMembership.ACCEPTED
1867
    psfu = Project.objects.select_for_update()
1868

    
1869
    modified = psfu.modified_projects()
1870
    for project in modified:
1871
        objects = project.projectmembership_set.select_for_update()
1872

    
1873
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
1874
        if not memberships:
1875
            project.is_modified = False
1876
            project.save()
1877

    
1878
    terminating = psfu.terminating_projects()
1879
    for project in terminating:
1880
        objects = project.projectmembership_set.select_for_update()
1881

    
1882
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1883
                                          Q(is_pending=True)))
1884
        if not memberships:
1885
            project.deactivate()
1886
            project.save()
1887

    
1888
    transaction.commit()
1889

    
1890
def sync_projects():
1891
    sync_finish_serials()
1892
    pre_sync()
1893
    serial = do_sync()
1894
    sync_finish_serials([serial])
1895
    post_sync()
1896

    
1897
def trigger_sync(retries=3, retry_wait=1.0):
1898
    transaction.commit()
1899

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

    
1913
            retries -= 1
1914
            if retries <= 0:
1915
                return False
1916
            sleep(retry_wait)
1917

    
1918
        sync_projects()
1919
        return True
1920

    
1921
    finally:
1922
        if locked:
1923
            cursor.execute("SELECT pg_advisory_unlock(1)")
1924
            cursor.fetchall()
1925

    
1926

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

    
1931
    person  =   models.CharField(max_length=255)
1932
    project =   models.BigIntegerField()
1933
    date    =   models.DateField(default=datetime.now)
1934
    reason  =   models.IntegerField()
1935
    serial  =   models.BigIntegerField()
1936

    
1937
### SIGNALS ###
1938
################
1939

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

    
1952

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

    
1960

    
1961
def user_post_save(sender, instance, created, **kwargs):
1962
    if not created:
1963
        return
1964
    create_astakos_user(instance)
1965
post_save.connect(user_post_save, sender=User)
1966

    
1967
def astakosuser_post_save(sender, instance, created, **kwargs):
1968
    pass
1969

    
1970
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1971

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

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