Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 05617ab9

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

    
144
class ResourceMetadata(models.Model):
145
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
146
    value = models.CharField(_('Value'), max_length=255)
147

    
148
_presentation_data = {}
149
def get_presentation(resource):
150
    global _presentation_data
151
    presentation = _presentation_data.get(resource, {})
152
    if not presentation:
153
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
154
        presentation = resource_presentation.get(resource, {})
155
        _presentation_data[resource] = presentation
156
    return presentation
157

    
158
class Resource(models.Model):
159
    name = models.CharField(_('Name'), max_length=255)
160
    meta = models.ManyToManyField(ResourceMetadata)
161
    service = models.ForeignKey(Service)
162
    desc = models.TextField(_('Description'), null=True)
163
    unit = models.CharField(_('Name'), null=True, max_length=255)
164
    group = models.CharField(_('Group'), null=True, max_length=255)
165

    
166
    class Meta:
167
        unique_together = ("name", "service")
168

    
169
    def __str__(self):
170
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
171

    
172
    @property
173
    def help_text(self):
174
        return get_presentation(str(self)).get('help_text', '')
175

    
176
    @property
177
    def help_text_input_each(self):
178
        return get_presentation(str(self)).get('help_text_input_each', '')
179

    
180
    @property
181
    def is_abbreviation(self):
182
        return get_presentation(str(self)).get('is_abbreviation', False)
183

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

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

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

    
196

    
197
_default_quota = {}
198
def get_default_quota():
199
    global _default_quota
200
    if _default_quota:
201
        return _default_quota
202
    for s, data in SERVICES.iteritems():
203
        map(
204
            lambda d:_default_quota.update(
205
                {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
206
            ),
207
            data.get('resources', {})
208
        )
209
    return _default_quota
210

    
211

    
212
class AstakosUserManager(UserManager):
213

    
214
    def get_auth_provider_user(self, provider, **kwargs):
215
        """
216
        Retrieve AstakosUser instance associated with the specified third party
217
        id.
218
        """
219
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
220
                          kwargs.iteritems()))
221
        return self.get(auth_providers__module=provider, **kwargs)
222

    
223
    def get_by_email(self, email):
224
        return self.get(email=email)
225

    
226
    def get_by_identifier(self, email_or_username, **kwargs):
227
        try:
228
            return self.get(email__iexact=email_or_username, **kwargs)
229
        except AstakosUser.DoesNotExist:
230
            return self.get(username__iexact=email_or_username, **kwargs)
231

    
232
    def user_exists(self, email_or_username, **kwargs):
233
        qemail = Q(email__iexact=email_or_username)
234
        qusername = Q(username__iexact=email_or_username)
235
        qextra = Q(**kwargs)
236
        return self.filter((qemail | qusername) & qextra).exists()
237

    
238
    def verified_user_exists(self, email_or_username):
239
        return self.user_exists(email_or_username, email_verified=True)
240

    
241
    def verified(self):
242
        return self.filter(email_verified=True)
243

    
244
    def verified(self):
245
        return self.filter(email_verified=True)
246

    
247

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

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

    
264

    
265
    #for invitations
266
    user_level = DEFAULT_USER_LEVEL
267
    level = models.IntegerField(_('Inviter level'), default=user_level)
268
    invitations = models.IntegerField(
269
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
270

    
271
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
272
                                  null=True, blank=True)
273
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
274
    auth_token_expires = models.DateTimeField(
275
        _('Token expiration date'), null=True)
276

    
277
    updated = models.DateTimeField(_('Update date'))
278
    is_verified = models.BooleanField(_('Is verified?'), default=False)
279

    
280
    email_verified = models.BooleanField(_('Email verified?'), default=False)
281

    
282
    has_credits = models.BooleanField(_('Has credits?'), default=False)
283
    has_signed_terms = models.BooleanField(
284
        _('I agree with the terms'), default=False)
285
    date_signed_terms = models.DateTimeField(
286
        _('Signed terms date'), null=True, blank=True)
287

    
288
    activation_sent = models.DateTimeField(
289
        _('Activation sent data'), null=True, blank=True)
290

    
291
    policy = models.ManyToManyField(
292
        Resource, null=True, through='AstakosUserQuota')
293

    
294
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
295

    
296
    __has_signed_terms = False
297
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
298
                                           default=False, db_index=True)
299

    
300
    objects = AstakosUserManager()
301

    
302
    def __init__(self, *args, **kwargs):
303
        super(AstakosUser, self).__init__(*args, **kwargs)
304
        self.__has_signed_terms = self.has_signed_terms
305
        if not self.id:
306
            self.is_active = False
307

    
308
    @property
309
    def realname(self):
310
        return '%s %s' % (self.first_name, self.last_name)
311

    
312
    @realname.setter
313
    def realname(self, value):
314
        parts = value.split(' ')
315
        if len(parts) == 2:
316
            self.first_name = parts[0]
317
            self.last_name = parts[1]
318
        else:
319
            self.last_name = parts[0]
320

    
321
    def add_permission(self, pname):
322
        if self.has_perm(pname):
323
            return
324
        p, created = Permission.objects.get_or_create(
325
                                    codename=pname,
326
                                    name=pname.capitalize(),
327
                                    content_type=get_content_type())
328
        self.user_permissions.add(p)
329

    
330
    def remove_permission(self, pname):
331
        if self.has_perm(pname):
332
            return
333
        p = Permission.objects.get(codename=pname,
334
                                   content_type=get_content_type())
335
        self.user_permissions.remove(p)
336

    
337
    @property
338
    def invitation(self):
339
        try:
340
            return Invitation.objects.get(username=self.email)
341
        except Invitation.DoesNotExist:
342
            return None
343

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

    
363
    @property
364
    def policies(self):
365
        return self.astakosuserquota_set.select_related().all()
366

    
367
    @policies.setter
368
    def policies(self, policies):
369
        for p in policies:
370
            p.setdefault('resource', '')
371
            p.setdefault('capacity', 0)
372
            p.setdefault('quantity', 0)
373
            p.setdefault('import_limit', 0)
374
            p.setdefault('export_limit', 0)
375
            p.setdefault('update', True)
376
            self.add_resource_policy(**p)
377

    
378
    def add_resource_policy(
379
            self, resource, capacity, quantity, import_limit,
380
            export_limit, update=True):
381
        """Raises ObjectDoesNotExist, IntegrityError"""
382
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
383
        resource = Resource.objects.get(service__name=s, name=r)
384
        if update:
385
            AstakosUserQuota.objects.update_or_create(
386
                user=self, resource=resource, defaults={
387
                    'capacity':capacity,
388
                    'quantity': quantity,
389
                    'import_limit':import_limit,
390
                    'export_limit':export_limit})
391
        else:
392
            q = self.astakosuserquota_set
393
            q.create(
394
                resource=resource, capacity=capacity, quanity=quantity,
395
                import_limit=import_limit, export_limit=export_limit)
396

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

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

    
411
    def save(self, update_timestamps=True, **kwargs):
412
        if update_timestamps:
413
            if not self.id:
414
                self.date_joined = datetime.now()
415
            self.updated = datetime.now()
416

    
417
        # update date_signed_terms if necessary
418
        if self.__has_signed_terms != self.has_signed_terms:
419
            self.date_signed_terms = datetime.now()
420

    
421
        self.update_uuid()
422

    
423
        if self.username != self.email.lower():
424
            # set username
425
            self.username = self.email.lower()
426

    
427
        self.validate_unique_email_isactive()
428

    
429
        super(AstakosUser, self).save(**kwargs)
430

    
431
    def renew_token(self, flush_sessions=False, current_key=None):
432
        md5 = hashlib.md5()
433
        md5.update(settings.SECRET_KEY)
434
        md5.update(self.username)
435
        md5.update(self.realname.encode('ascii', 'ignore'))
436
        md5.update(asctime())
437

    
438
        self.auth_token = b64encode(md5.digest())
439
        self.auth_token_created = datetime.now()
440
        self.auth_token_expires = self.auth_token_created + \
441
                                  timedelta(hours=AUTH_TOKEN_DURATION)
442
        if flush_sessions:
443
            self.flush_sessions(current_key)
444
        msg = 'Token renewed for %s' % self.email
445
        logger.log(LOGGING_LEVEL, msg)
446

    
447
    def flush_sessions(self, current_key=None):
448
        q = self.sessions
449
        if current_key:
450
            q = q.exclude(session_key=current_key)
451

    
452
        keys = q.values_list('session_key', flat=True)
453
        if keys:
454
            msg = 'Flushing sessions: %s' % ','.join(keys)
455
            logger.log(LOGGING_LEVEL, msg, [])
456
        engine = import_module(settings.SESSION_ENGINE)
457
        for k in keys:
458
            s = engine.SessionStore(k)
459
            s.flush()
460

    
461
    def __unicode__(self):
462
        return '%s (%s)' % (self.realname, self.email)
463

    
464
    def conflicting_email(self):
465
        q = AstakosUser.objects.exclude(username=self.username)
466
        q = q.filter(email__iexact=self.email)
467
        if q.count() != 0:
468
            return True
469
        return False
470

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

    
484
    def email_change_is_pending(self):
485
        return self.emailchanges.count() > 0
486

    
487
    def email_change_is_pending(self):
488
        return self.emailchanges.count() > 0
489

    
490
    @property
491
    def signed_terms(self):
492
        term = get_latest_terms()
493
        if not term:
494
            return True
495
        if not self.has_signed_terms:
496
            return False
497
        if not self.date_signed_terms:
498
            return False
499
        if self.date_signed_terms < term.date:
500
            self.has_signed_terms = False
501
            self.date_signed_terms = None
502
            self.save()
503
            return False
504
        return True
505

    
506
    def set_invitations_level(self):
507
        """
508
        Update user invitation level
509
        """
510
        level = self.invitation.inviter.level + 1
511
        self.level = level
512
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
513

    
514
    def can_login_with_auth_provider(self, provider):
515
        if not self.has_auth_provider(provider):
516
            return False
517
        else:
518
            return auth_providers.get_provider(provider).is_available_for_login()
519

    
520
    def can_add_auth_provider(self, provider, **kwargs):
521
        provider_settings = auth_providers.get_provider(provider)
522

    
523
        if not provider_settings.is_available_for_add():
524
            return False
525

    
526
        if self.has_auth_provider(provider) and \
527
           provider_settings.one_per_user:
528
            return False
529

    
530
        if 'provider_info' in kwargs:
531
            kwargs.pop('provider_info')
532

    
533
        if 'identifier' in kwargs:
534
            try:
535
                # provider with specified params already exist
536
                kwargs['user__email_verified'] = True
537
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
538
                                                                   **kwargs)
539
            except AstakosUser.DoesNotExist:
540
                return True
541
            else:
542
                return False
543

    
544
        return True
545

    
546
    def can_remove_auth_provider(self, module):
547
        provider = auth_providers.get_provider(module)
548
        existing = self.get_active_auth_providers()
549
        existing_for_provider = self.get_active_auth_providers(module=module)
550

    
551
        if len(existing) <= 1:
552
            return False
553

    
554
        if len(existing_for_provider) == 1 and provider.is_required():
555
            return False
556

    
557
        return True
558

    
559
    def can_change_password(self):
560
        return self.has_auth_provider('local', auth_backend='astakos')
561

    
562
    def has_required_auth_providers(self):
563
        required = auth_providers.REQUIRED_PROVIDERS
564
        for provider in required:
565
            if not self.has_auth_provider(provider):
566
                return False
567
        return True
568

    
569
    def has_auth_provider(self, provider, **kwargs):
570
        return bool(self.auth_providers.filter(module=provider,
571
                                               **kwargs).count())
572

    
573
    def add_auth_provider(self, provider, **kwargs):
574
        info_data = ''
575
        if 'provider_info' in kwargs:
576
            info_data = kwargs.pop('provider_info')
577
            if isinstance(info_data, dict):
578
                info_data = json.dumps(info_data)
579

    
580
        if self.can_add_auth_provider(provider, **kwargs):
581
            AstakosUserAuthProvider.objects.remove_unverified_providers(provider,
582
                                                                **kwargs)
583
            self.auth_providers.create(module=provider, active=True,
584
                                       info_data=info_data,
585
                                       **kwargs)
586
        else:
587
            raise Exception('Cannot add provider')
588

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

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

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

    
605
        pending.delete()
606
        return provider
607

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

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

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

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

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

    
631
    def get_auth_providers(self):
632
        return self.auth_providers.all()
633

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

    
643
        return providers
644

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

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

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

    
681
        return mark_safe(message + u' '+ msg_extra)
682

    
683
    def owns_project(self, project):
684
        return project.owner == self
685

    
686
    def is_project_member(self, project_or_application):
687
        return self.get_status_in_project(project_or_application) in \
688
                                        ProjectMembership.ASSOCIATED_STATES
689

    
690
    def is_project_accepted_member(self, project_or_application):
691
        return self.get_status_in_project(project_or_application) in \
692
                                            ProjectMembership.ACCEPTED_STATES
693

    
694
    def get_status_in_project(self, project_or_application):
695
        application = project_or_application
696
        if isinstance(project_or_application, Project):
697
            application = project_or_application.project
698
        return application.user_status(self)
699

    
700

    
701
class AstakosUserAuthProviderManager(models.Manager):
702

    
703
    def active(self, **filters):
704
        return self.filter(active=True, **filters)
705

    
706
    def remove_unverified_providers(self, provider, **filters):
707
        try:
708
            existing = self.filter(module=provider, user__email_verified=False, **filters)
709
            for p in existing:
710
                p.user.delete()
711
        except:
712
            pass
713

    
714

    
715

    
716
class AstakosUserAuthProvider(models.Model):
717
    """
718
    Available user authentication methods.
719
    """
720
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
721
                                   null=True, default=None)
722
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
723
    module = models.CharField(_('Provider'), max_length=255, blank=False,
724
                                default='local')
725
    identifier = models.CharField(_('Third-party identifier'),
726
                                              max_length=255, null=True,
727
                                              blank=True)
728
    active = models.BooleanField(default=True)
729
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
730
                                   default='astakos')
731
    info_data = models.TextField(default="", null=True, blank=True)
732
    created = models.DateTimeField('Creation date', auto_now_add=True)
733

    
734
    objects = AstakosUserAuthProviderManager()
735

    
736
    class Meta:
737
        unique_together = (('identifier', 'module', 'user'), )
738
        ordering = ('module', 'created')
739

    
740
    def __init__(self, *args, **kwargs):
741
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
742
        try:
743
            self.info = json.loads(self.info_data)
744
            if not self.info:
745
                self.info = {}
746
        except Exception, e:
747
            self.info = {}
748

    
749
        for key,value in self.info.iteritems():
750
            setattr(self, 'info_%s' % key, value)
751

    
752

    
753
    @property
754
    def settings(self):
755
        return auth_providers.get_provider(self.module)
756

    
757
    @property
758
    def details_display(self):
759
        try:
760
          return self.settings.get_details_tpl_display % self.__dict__
761
        except:
762
          return ''
763

    
764
    @property
765
    def title_display(self):
766
        title_tpl = self.settings.get_title_display
767
        try:
768
            if self.settings.get_user_title_display:
769
                title_tpl = self.settings.get_user_title_display
770
        except Exception, e:
771
            pass
772
        try:
773
          return title_tpl % self.__dict__
774
        except:
775
          return self.settings.get_title_display % self.__dict__
776

    
777
    def can_remove(self):
778
        return self.user.can_remove_auth_provider(self.module)
779

    
780
    def delete(self, *args, **kwargs):
781
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
782
        if self.module == 'local':
783
            self.user.set_unusable_password()
784
            self.user.save()
785
        return ret
786

    
787
    def __repr__(self):
788
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
789

    
790
    def __unicode__(self):
791
        if self.identifier:
792
            return "%s:%s" % (self.module, self.identifier)
793
        if self.auth_backend:
794
            return "%s:%s" % (self.module, self.auth_backend)
795
        return self.module
796

    
797
    def save(self, *args, **kwargs):
798
        self.info_data = json.dumps(self.info)
799
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
800

    
801

    
802
class ExtendedManager(models.Manager):
803
    def _update_or_create(self, **kwargs):
804
        assert kwargs, \
805
            'update_or_create() must be passed at least one keyword argument'
806
        obj, created = self.get_or_create(**kwargs)
807
        defaults = kwargs.pop('defaults', {})
808
        if created:
809
            return obj, True, False
810
        else:
811
            try:
812
                params = dict(
813
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
814
                params.update(defaults)
815
                for attr, val in params.items():
816
                    if hasattr(obj, attr):
817
                        setattr(obj, attr, val)
818
                sid = transaction.savepoint()
819
                obj.save(force_update=True)
820
                transaction.savepoint_commit(sid)
821
                return obj, False, True
822
            except IntegrityError, e:
823
                transaction.savepoint_rollback(sid)
824
                try:
825
                    return self.get(**kwargs), False, False
826
                except self.model.DoesNotExist:
827
                    raise e
828

    
829
    update_or_create = _update_or_create
830

    
831

    
832
class AstakosUserQuota(models.Model):
833
    objects = ExtendedManager()
834
    capacity = models.BigIntegerField(_('Capacity'), null=True)
835
    quantity = models.BigIntegerField(_('Quantity'), null=True)
836
    export_limit = models.BigIntegerField(_('Export limit'), null=True)
837
    import_limit = models.BigIntegerField(_('Import limit'), null=True)
838
    resource = models.ForeignKey(Resource)
839
    user = models.ForeignKey(AstakosUser)
840

    
841
    class Meta:
842
        unique_together = ("resource", "user")
843

    
844

    
845
class ApprovalTerms(models.Model):
846
    """
847
    Model for approval terms
848
    """
849

    
850
    date = models.DateTimeField(
851
        _('Issue date'), db_index=True, default=datetime.now())
852
    location = models.CharField(_('Terms location'), max_length=255)
853

    
854

    
855
class Invitation(models.Model):
856
    """
857
    Model for registring invitations
858
    """
859
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
860
                                null=True)
861
    realname = models.CharField(_('Real name'), max_length=255)
862
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
863
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
864
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
865
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
866
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
867

    
868
    def __init__(self, *args, **kwargs):
869
        super(Invitation, self).__init__(*args, **kwargs)
870
        if not self.id:
871
            self.code = _generate_invitation_code()
872

    
873
    def consume(self):
874
        self.is_consumed = True
875
        self.consumed = datetime.now()
876
        self.save()
877

    
878
    def __unicode__(self):
879
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
880

    
881

    
882
class EmailChangeManager(models.Manager):
883

    
884
    @transaction.commit_on_success
885
    def change_email(self, activation_key):
886
        """
887
        Validate an activation key and change the corresponding
888
        ``User`` if valid.
889

890
        If the key is valid and has not expired, return the ``User``
891
        after activating.
892

893
        If the key is not valid or has expired, return ``None``.
894

895
        If the key is valid but the ``User`` is already active,
896
        return ``None``.
897

898
        After successful email change the activation record is deleted.
899

900
        Throws ValueError if there is already
901
        """
902
        try:
903
            email_change = self.model.objects.get(
904
                activation_key=activation_key)
905
            if email_change.activation_key_expired():
906
                email_change.delete()
907
                raise EmailChange.DoesNotExist
908
            # is there an active user with this address?
909
            try:
910
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
911
            except AstakosUser.DoesNotExist:
912
                pass
913
            else:
914
                raise ValueError(_('The new email address is reserved.'))
915
            # update user
916
            user = AstakosUser.objects.get(pk=email_change.user_id)
917
            old_email = user.email
918
            user.email = email_change.new_email_address
919
            user.save()
920
            email_change.delete()
921
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
922
                                                          user.email)
923
            logger.log(LOGGING_LEVEL, msg)
924
            return user
925
        except EmailChange.DoesNotExist:
926
            raise ValueError(_('Invalid activation key.'))
927

    
928

    
929
class EmailChange(models.Model):
930
    new_email_address = models.EmailField(
931
        _(u'new e-mail address'),
932
        help_text=_('Your old email address will be used until you verify your new one.'))
933
    user = models.ForeignKey(
934
        AstakosUser, unique=True, related_name='emailchanges')
935
    requested_at = models.DateTimeField(default=datetime.now())
936
    activation_key = models.CharField(
937
        max_length=40, unique=True, db_index=True)
938

    
939
    objects = EmailChangeManager()
940

    
941
    def get_url(self):
942
        return reverse('email_change_confirm',
943
                      kwargs={'activation_key': self.activation_key})
944

    
945
    def activation_key_expired(self):
946
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
947
        return self.requested_at + expiration_date < datetime.now()
948

    
949

    
950
class AdditionalMail(models.Model):
951
    """
952
    Model for registring invitations
953
    """
954
    owner = models.ForeignKey(AstakosUser)
955
    email = models.EmailField()
956

    
957

    
958
def _generate_invitation_code():
959
    while True:
960
        code = randint(1, 2L ** 63 - 1)
961
        try:
962
            Invitation.objects.get(code=code)
963
            # An invitation with this code already exists, try again
964
        except Invitation.DoesNotExist:
965
            return code
966

    
967

    
968
def get_latest_terms():
969
    try:
970
        term = ApprovalTerms.objects.order_by('-id')[0]
971
        return term
972
    except IndexError:
973
        pass
974
    return None
975

    
976
class PendingThirdPartyUser(models.Model):
977
    """
978
    Model for registring successful third party user authentications
979
    """
980
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
981
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
982
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
983
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
984
                                  null=True)
985
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
986
                                 null=True)
987
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
988
                                   null=True)
989
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
990
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
991
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
992
    info = models.TextField(default="", null=True, blank=True)
993

    
994
    class Meta:
995
        unique_together = ("provider", "third_party_identifier")
996

    
997
    def get_user_instance(self):
998
        d = self.__dict__
999
        d.pop('_state', None)
1000
        d.pop('id', None)
1001
        d.pop('token', None)
1002
        d.pop('created', None)
1003
        d.pop('info', None)
1004
        user = AstakosUser(**d)
1005

    
1006
        return user
1007

    
1008
    @property
1009
    def realname(self):
1010
        return '%s %s' %(self.first_name, self.last_name)
1011

    
1012
    @realname.setter
1013
    def realname(self, value):
1014
        parts = value.split(' ')
1015
        if len(parts) == 2:
1016
            self.first_name = parts[0]
1017
            self.last_name = parts[1]
1018
        else:
1019
            self.last_name = parts[0]
1020

    
1021
    def save(self, **kwargs):
1022
        if not self.id:
1023
            # set username
1024
            while not self.username:
1025
                username =  uuid.uuid4().hex[:30]
1026
                try:
1027
                    AstakosUser.objects.get(username = username)
1028
                except AstakosUser.DoesNotExist, e:
1029
                    self.username = username
1030
        super(PendingThirdPartyUser, self).save(**kwargs)
1031

    
1032
    def generate_token(self):
1033
        self.password = self.third_party_identifier
1034
        self.last_login = datetime.now()
1035
        self.token = default_token_generator.make_token(self)
1036

    
1037
class SessionCatalog(models.Model):
1038
    session_key = models.CharField(_('session key'), max_length=40)
1039
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1040

    
1041

    
1042
### PROJECTS ###
1043
################
1044

    
1045
def synced_model_metaclass(class_name, class_parents, class_attributes):
1046

    
1047
    new_attributes = {}
1048
    sync_attributes = {}
1049

    
1050
    for name, value in class_attributes.iteritems():
1051
        sync, underscore, rest = name.partition('_')
1052
        if sync == 'sync' and underscore == '_':
1053
            sync_attributes[rest] = value
1054
        else:
1055
            new_attributes[name] = value
1056

    
1057
    if 'prefix' not in sync_attributes:
1058
        m = ("you did not specify a 'sync_prefix' attribute "
1059
             "in class '%s'" % (class_name,))
1060
        raise ValueError(m)
1061

    
1062
    prefix = sync_attributes.pop('prefix')
1063
    class_name = sync_attributes.pop('classname', prefix + '_model')
1064

    
1065
    for name, value in sync_attributes.iteritems():
1066
        newname = prefix + '_' + name
1067
        if newname in new_attributes:
1068
            m = ("class '%s' was specified with prefix '%s' "
1069
                 "but it already has an attribute named '%s'"
1070
                 % (class_name, prefix, newname))
1071
            raise ValueError(m)
1072

    
1073
        new_attributes[newname] = value
1074

    
1075
    newclass = type(class_name, class_parents, new_attributes)
1076
    return newclass
1077

    
1078

    
1079
def make_synced(prefix='sync', name='SyncedState'):
1080

    
1081
    the_name = name
1082
    the_prefix = prefix
1083

    
1084
    class SyncedState(models.Model):
1085

    
1086
        sync_classname      = the_name
1087
        sync_prefix         = the_prefix
1088
        __metaclass__       = synced_model_metaclass
1089

    
1090
        sync_new_state      = models.BigIntegerField(null=True)
1091
        sync_synced_state   = models.BigIntegerField(null=True)
1092
        STATUS_SYNCED       = 0
1093
        STATUS_PENDING      = 1
1094
        sync_status         = models.IntegerField(db_index=True)
1095

    
1096
        class Meta:
1097
            abstract = True
1098

    
1099
        class NotSynced(Exception):
1100
            pass
1101

    
1102
        def sync_init_state(self, state):
1103
            self.sync_synced_state = state
1104
            self.sync_new_state = state
1105
            self.sync_status = self.STATUS_SYNCED
1106

    
1107
        def sync_get_status(self):
1108
            return self.sync_status
1109

    
1110
        def sync_set_status(self):
1111
            if self.sync_new_state != self.sync_synced_state:
1112
                self.sync_status = self.STATUS_PENDING
1113
            else:
1114
                self.sync_status = self.STATUS_SYNCED
1115

    
1116
        def sync_set_synced(self):
1117
            self.sync_synced_state = self.sync_new_state
1118
            self.sync_status = self.STATUS_SYNCED
1119

    
1120
        def sync_get_synced_state(self):
1121
            return self.sync_synced_state
1122

    
1123
        def sync_set_new_state(self, new_state):
1124
            self.sync_new_state = new_state
1125
            self.sync_set_status()
1126

    
1127
        def sync_get_new_state(self):
1128
            return self.sync_new_state
1129

    
1130
        def sync_set_synced_state(self, synced_state):
1131
            self.sync_synced_state = synced_state
1132
            self.sync_set_status()
1133

    
1134
        def sync_get_pending_objects(self):
1135
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1136
            return self.objects.filter(**kw)
1137

    
1138
        def sync_get_synced_objects(self):
1139
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1140
            return self.objects.filter(**kw)
1141

    
1142
        def sync_verify_get_synced_state(self):
1143
            status = self.sync_get_status()
1144
            state = self.sync_get_synced_state()
1145
            verified = (status == self.STATUS_SYNCED)
1146
            return state, verified
1147

    
1148
        def sync_is_synced(self):
1149
            state, verified = self.sync_verify_get_synced_state()
1150
            return verified
1151

    
1152
    return SyncedState
1153

    
1154
SyncedState = make_synced(prefix='sync', name='SyncedState')
1155

    
1156

    
1157
class ProjectApplicationManager(ForUpdateManager):
1158

    
1159
    def user_visible_projects(self, *filters, **kw_filters):
1160
        return self.filter(Q(state=ProjectApplication.PENDING)|\
1161
                           Q(state=ProjectApplication.APPROVED))
1162

    
1163
    def user_visible_by_last_of_chain(self, *filters, **kw_filters):
1164
        by_chain = self.user_visible_projects(*filters, **kw_filters).values('chain')
1165
        by_chain = by_chain.annotate(last_id=models.Min('id')).values_list('last_id', flat=True)
1166
        return self.filter(id__in=by_chain)
1167

    
1168
    def user_accessible_projects(self, user):
1169
        """
1170
        Return projects accessed by specified user.
1171
        """
1172
        participates_filters = Q(owner=user) | Q(applicant=user) | \
1173
                               Q(project__projectmembership__person=user)
1174

    
1175
        return self.user_visible_by_last_of_chain(participates_filters).order_by('issue_date').distinct()
1176

    
1177
    def search_by_name(self, *search_strings):
1178
        q = Q()
1179
        for s in search_strings:
1180
            q = q | Q(name__icontains=s)
1181
        return self.filter(q)
1182

    
1183

    
1184
USER_STATUS_DISPLAY = {
1185
    100: _('Owner'),
1186
      0: _('Join requested'),
1187
      1: _('Pending'),
1188
      2: _('Accepted member'),
1189
      3: _('Removing'),
1190
      4: _('Removed'),
1191
     -1: _('Not a member'),
1192
}
1193

    
1194
class Chain(models.Model):
1195
    chain  =   models.AutoField(primary_key=True)
1196

    
1197
def new_chain():
1198
    c = Chain.objects.create()
1199
    chain = c.chain
1200
    c.delete()
1201
    return chain
1202

    
1203

    
1204
class ProjectApplication(models.Model):
1205
    applicant               =   models.ForeignKey(
1206
                                    AstakosUser,
1207
                                    related_name='projects_applied',
1208
                                    db_index=True)
1209

    
1210
    PENDING     =    0
1211
    APPROVED    =    1
1212
    REPLACED    =    2
1213
    DENIED      =    3
1214

    
1215
    state                   =   models.IntegerField(default=PENDING)
1216

    
1217
    owner                   =   models.ForeignKey(
1218
                                    AstakosUser,
1219
                                    related_name='projects_owned',
1220
                                    db_index=True)
1221

    
1222
    chain                   =   models.IntegerField()
1223
    precursor_application   =   models.OneToOneField('ProjectApplication',
1224
                                                     null=True,
1225
                                                     blank=True,
1226
                                                     db_index=True)
1227

    
1228
    name                    =   models.CharField(max_length=80)
1229
    homepage                =   models.URLField(max_length=255, null=True)
1230
    description             =   models.TextField(null=True, blank=True)
1231
    start_date              =   models.DateTimeField(null=True, blank=True)
1232
    end_date                =   models.DateTimeField()
1233
    member_join_policy      =   models.IntegerField()
1234
    member_leave_policy     =   models.IntegerField()
1235
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1236
    resource_grants         =   models.ManyToManyField(
1237
                                    Resource,
1238
                                    null=True,
1239
                                    blank=True,
1240
                                    through='ProjectResourceGrant')
1241
    comments                =   models.TextField(null=True, blank=True)
1242
    issue_date              =   models.DateTimeField(default=datetime.now)
1243

    
1244

    
1245
    objects                 =   ProjectApplicationManager()
1246

    
1247
    class Meta:
1248
        unique_together = ("chain", "id")
1249

    
1250
    def __unicode__(self):
1251
        return "%s applied by %s" % (self.name, self.applicant)
1252

    
1253
    # TODO: Move to a more suitable place
1254
    PROJECT_STATE_DISPLAY = {
1255
        PENDING : _('Pending review'),
1256
        APPROVED: _('Active'),
1257
        REPLACED: _('Replaced'),
1258
        DENIED  : _('Denied')
1259
        }
1260

    
1261
    def state_display(self):
1262
        return self.PROJECT_STATE_DISPLAY.get(self.state, _('Unknown'))
1263

    
1264
    def add_resource_policy(self, service, resource, uplimit):
1265
        """Raises ObjectDoesNotExist, IntegrityError"""
1266
        q = self.projectresourcegrant_set
1267
        resource = Resource.objects.get(service__name=service, name=resource)
1268
        q.create(resource=resource, member_capacity=uplimit)
1269

    
1270
    def user_status(self, user):
1271
        try:
1272
            project = self.get_project()
1273
            if not project:
1274
                return -1
1275
            membership = project.projectmembership_set.get(person=user)
1276
            membership = membership.exclude(state=ProjectMembership.REMOVED)
1277
            status = membership.state
1278
        except ProjectMembership.DoesNotExist:
1279
            status = -1
1280

    
1281
        return status
1282

    
1283
    def user_status_display(self, user):
1284
        return USER_STATUS_DISPLAY.get(self.user_status(user), _('Unknown'))
1285

    
1286
    def members_count(self):
1287
        return self.project.approved_memberships.count()
1288

    
1289
    @property
1290
    def grants(self):
1291
        return self.projectresourcegrant_set.values(
1292
            'member_capacity', 'resource__name', 'resource__service__name')
1293

    
1294
    @property
1295
    def resource_policies(self):
1296
        return self.projectresourcegrant_set.all()
1297

    
1298
    @resource_policies.setter
1299
    def resource_policies(self, policies):
1300
        for p in policies:
1301
            service = p.get('service', None)
1302
            resource = p.get('resource', None)
1303
            uplimit = p.get('uplimit', 0)
1304
            self.add_resource_policy(service, resource, uplimit)
1305

    
1306
    @property
1307
    def follower(self):
1308
        try:
1309
            return ProjectApplication.objects.get(precursor_application=self)
1310
        except ProjectApplication.DoesNotExist:
1311
            return
1312

    
1313
    def followers(self):
1314
        followers = ProjectApplication.objects.filter(chain=self.chain)
1315
        return followers.exclude(pk=self.pk).order_by('id')
1316

    
1317
    def last_follower(self):
1318
        try:
1319
            return self.followers().filter(
1320
                          state__in=[self.PENDING, self.APPROVED]).order_by('-id')[0]
1321
        except IndexError:
1322
            import traceback; print traceback.print_exc()
1323
            return None
1324

    
1325
    def has_pending_modifications(self):
1326
        return bool(self.last_follower())
1327

    
1328
    def get_project(self):
1329
        try:
1330
            return Project.objects.get(id=self.chain)
1331
        except Project.DoesNotExist:
1332
            return None
1333

    
1334
    def _get_project_for_update(self):
1335
        try:
1336
            objects = Project.objects.select_for_update()
1337
            project = objects.get(id=self.chain)
1338
            return project
1339
        except Project.DoesNotExist:
1340
            return None
1341

    
1342
    def deny(self):
1343
        if self.state != self.PENDING:
1344
            m = _("cannot deny: application '%s' in state '%s'") % (
1345
                    self.id, self.state)
1346
            raise AssertionError(m)
1347

    
1348
        self.state = self.DENIED
1349
        self.save()
1350

    
1351
    def approve(self, approval_user=None):
1352
        """
1353
        If approval_user then during owner membership acceptance
1354
        it is checked whether the request_user is eligible.
1355

1356
        Raises:
1357
            PermissionDenied
1358
        """
1359

    
1360
        if not transaction.is_managed():
1361
            raise AssertionError("NOPE")
1362

    
1363
        new_project_name = self.name
1364
        if self.state != self.PENDING:
1365
            m = _("cannot approve: project '%s' in state '%s'") % (
1366
                    new_project_name, self.state)
1367
            raise PermissionDenied(m) # invalid argument
1368

    
1369
        now = datetime.now()
1370
        project = self._get_project_for_update()
1371

    
1372
        try:
1373
            # needs SERIALIZABLE
1374
            conflicting_project = Project.objects.get(name=new_project_name)
1375
            if (conflicting_project.is_alive and
1376
                conflicting_project != project):
1377
                m = (_("cannot approve: project with name '%s' "
1378
                       "already exists (serial: %s)") % (
1379
                        new_project_name, conflicting_project.id))
1380
                raise PermissionDenied(m) # invalid argument
1381
        except Project.DoesNotExist:
1382
            pass
1383

    
1384
        new_project = False
1385
        if project is None:
1386
            new_project = True
1387
            project = Project(id=self.chain, creation_date=now)
1388

    
1389
        project.name = new_project_name
1390
        project.application = self
1391
        project.last_approval_date = now
1392
        if not new_project:
1393
            project.is_modified = True
1394

    
1395
        project.save()
1396

    
1397
        self.state = self.APPROVED
1398
        self.save()
1399

    
1400
def submit_application(**kw):
1401

    
1402
    resource_policies = kw.pop('resource_policies', None)
1403
    application = ProjectApplication(**kw)
1404

    
1405
    precursor = kw['precursor_application']
1406

    
1407
    if precursor is None:
1408
        application.chain = new_chain()
1409
    else:
1410
        application.chain = precursor.chain
1411
        if precursor.state == ProjectApplication.PENDING:
1412
            precursor.state = ProjectApplication.REPLACED
1413
            precursor.save()
1414

    
1415
    application.save()
1416
    application.resource_policies = resource_policies
1417
    return application
1418

    
1419
class ProjectResourceGrant(models.Model):
1420

    
1421
    resource                =   models.ForeignKey(Resource)
1422
    project_application     =   models.ForeignKey(ProjectApplication,
1423
                                                  null=True)
1424
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1425
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1426
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1427
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1428
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1429
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1430

    
1431
    objects = ExtendedManager()
1432

    
1433
    class Meta:
1434
        unique_together = ("resource", "project_application")
1435

    
1436

    
1437
class ProjectManager(ForUpdateManager):
1438

    
1439
    def _q_terminated(self):
1440
        return Q(state=Project.TERMINATED)
1441

    
1442
    def terminated_projects(self):
1443
        q = self._q_terminated()
1444
        return self.filter(q)
1445

    
1446
    def not_terminated_projects(self):
1447
        q = ~self._q_terminated()
1448
        return self.filter(q)
1449

    
1450
    def terminating_projects(self):
1451
        q = self._q_terminated() & Q(is_active=True)
1452
        return self.filter(q)
1453

    
1454
    def modified_projects(self):
1455
        return self.filter(is_modified=True)
1456

    
1457

    
1458
class Project(models.Model):
1459

    
1460
    application                 =   models.OneToOneField(
1461
                                            ProjectApplication,
1462
                                            related_name='project')
1463
    last_approval_date          =   models.DateTimeField(null=True)
1464

    
1465
    members                     =   models.ManyToManyField(
1466
                                            AstakosUser,
1467
                                            through='ProjectMembership')
1468

    
1469
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1470
    deactivation_date           =   models.DateTimeField(null=True)
1471

    
1472
    creation_date               =   models.DateTimeField()
1473
    name                        =   models.CharField(
1474
                                            max_length=80,
1475
                                            db_index=True,
1476
                                            unique=True)
1477

    
1478
    APPROVED    = 1
1479
    SUSPENDED   = 10
1480
    TERMINATED  = 100
1481

    
1482
    is_modified                 =   models.BooleanField(default=False,
1483
                                                        db_index=True)
1484
    is_active                   =   models.BooleanField(default=True,
1485
                                                        db_index=True)
1486
    state                       =   models.IntegerField(default=APPROVED,
1487
                                                        db_index=True)
1488

    
1489
    objects     =   ProjectManager()
1490

    
1491
    def __str__(self):
1492
        return _("<project %s '%s'>") % (self.id, self.application.name)
1493

    
1494
    __repr__ = __str__
1495

    
1496
    def is_deactivated(self, reason=None):
1497
        if reason is not None:
1498
            return self.state == reason
1499

    
1500
        return self.state != self.APPROVED
1501

    
1502
    def is_deactivating(self, reason=None):
1503
        if not self.is_active:
1504
            return False
1505

    
1506
        return self.is_deactivated(reason)
1507

    
1508
    def is_deactivated_strict(self, reason=None):
1509
        if self.is_active:
1510
            return False
1511

    
1512
        return self.is_deactivated(reason)
1513

    
1514
    ### Deactivation calls
1515

    
1516
    def deactivate(self):
1517
        self.deactivation_date = datetime.now()
1518
        self.is_active = False
1519

    
1520
    def terminate(self):
1521
        self.deactivation_reason = 'TERMINATED'
1522
        self.state = self.TERMINATED
1523
        self.save()
1524

    
1525

    
1526
    ### Logical checks
1527

    
1528
    def is_inconsistent(self):
1529
        now = datetime.now()
1530
        dates = [self.creation_date,
1531
                 self.last_approval_date,
1532
                 self.deactivation_date]
1533
        return any([date > now for date in dates])
1534

    
1535
    def is_active_strict(self):
1536
        return self.is_active and self.state == self.APPROVED
1537

    
1538
    @property
1539
    def is_alive(self):
1540
        return self.is_active_strict()
1541

    
1542
    @property
1543
    def is_terminated(self):
1544
        return self.is_deactivated(self.TERMINATED)
1545

    
1546
    @property
1547
    def is_suspended(self):
1548
        return False
1549

    
1550
    def violates_resource_grants(self):
1551
        return False
1552

    
1553
    def violates_members_limit(self, adding=0):
1554
        application = self.application
1555
        limit = application.limit_on_members_number
1556
        if limit is None:
1557
            return False
1558
        return (len(self.approved_members) + adding > limit)
1559

    
1560

    
1561
    ### Other
1562

    
1563
    @property
1564
    def approved_memberships(self):
1565
        query = ProjectMembership.query_approved()
1566
        return self.projectmembership_set.filter(query)
1567

    
1568
    @property
1569
    def approved_members(self):
1570
        return [m.person for m in self.approved_memberships]
1571

    
1572
    def add_member(self, user):
1573
        """
1574
        Raises:
1575
            django.exceptions.PermissionDenied
1576
            astakos.im.models.AstakosUser.DoesNotExist
1577
        """
1578
        if isinstance(user, int):
1579
            user = AstakosUser.objects.get(user=user)
1580

    
1581
        m, created = ProjectMembership.objects.get_or_create(
1582
            person=user, project=self
1583
        )
1584
        m.accept()
1585

    
1586
    def remove_member(self, user):
1587
        """
1588
        Raises:
1589
            django.exceptions.PermissionDenied
1590
            astakos.im.models.AstakosUser.DoesNotExist
1591
            astakos.im.models.ProjectMembership.DoesNotExist
1592
        """
1593
        if isinstance(user, int):
1594
            user = AstakosUser.objects.get(user=user)
1595

    
1596
        m = ProjectMembership.objects.get(person=user, project=self)
1597
        m.remove()
1598

    
1599

    
1600
class PendingMembershipError(Exception):
1601
    pass
1602

    
1603

    
1604
class ProjectMembership(models.Model):
1605

    
1606
    person              =   models.ForeignKey(AstakosUser)
1607
    request_date        =   models.DateField(default=datetime.now())
1608
    project             =   models.ForeignKey(Project)
1609

    
1610
    REQUESTED   =   0
1611
    ACCEPTED    =   1
1612
    SUSPENDED   =   10
1613
    TERMINATED  =   100
1614
    REMOVED     =   200
1615

    
1616
    ASSOCIATED_STATES   =   set([REQUESTED, ACCEPTED, SUSPENDED, TERMINATED])
1617
    ACCEPTED_STATES     =   set([ACCEPTED, SUSPENDED, TERMINATED])
1618

    
1619
    state               =   models.IntegerField(default=REQUESTED,
1620
                                                db_index=True)
1621
    is_pending          =   models.BooleanField(default=False, db_index=True)
1622
    is_active           =   models.BooleanField(default=False, db_index=True)
1623
    application         =   models.ForeignKey(
1624
                                ProjectApplication,
1625
                                null=True,
1626
                                related_name='memberships')
1627
    pending_application =   models.ForeignKey(
1628
                                ProjectApplication,
1629
                                null=True,
1630
                                related_name='pending_memebrships')
1631
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1632

    
1633
    acceptance_date     =   models.DateField(null=True, db_index=True)
1634
    leave_request_date  =   models.DateField(null=True)
1635

    
1636
    objects     =   ForUpdateManager()
1637

    
1638

    
1639
    def get_combined_state(self):
1640
        return self.state, self.is_active, self.is_pending
1641

    
1642
    @classmethod
1643
    def query_approved(cls):
1644
        return (~Q(state=cls.REQUESTED) &
1645
                ~Q(state=cls.REMOVED))
1646

    
1647
    class Meta:
1648
        unique_together = ("person", "project")
1649
        #index_together = [["project", "state"]]
1650

    
1651
    def __str__(self):
1652
        return _("<'%s' membership in '%s'>") % (
1653
                self.person.username, self.project)
1654

    
1655
    __repr__ = __str__
1656

    
1657
    def __init__(self, *args, **kwargs):
1658
        self.state = self.REQUESTED
1659
        super(ProjectMembership, self).__init__(*args, **kwargs)
1660

    
1661
    def _set_history_item(self, reason, date=None):
1662
        if isinstance(reason, basestring):
1663
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1664

    
1665
        history_item = ProjectMembershipHistory(
1666
                            serial=self.id,
1667
                            person=self.person_id,
1668
                            project=self.project_id,
1669
                            date=date or datetime.now(),
1670
                            reason=reason)
1671
        history_item.save()
1672
        serial = history_item.id
1673

    
1674
    def accept(self):
1675
        if self.is_pending:
1676
            m = _("%s: attempt to accept while is pending") % (self,)
1677
            raise AssertionError(m)
1678

    
1679
        state = self.state
1680
        if state != self.REQUESTED:
1681
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1682
            raise AssertionError(m)
1683

    
1684
        now = datetime.now()
1685
        self.acceptance_date = now
1686
        self._set_history_item(reason='ACCEPT', date=now)
1687
        if self.project.is_active_strict():
1688
            self.state = self.ACCEPTED
1689
            self.is_pending = True
1690
        else:
1691
            self.state = self.TERMINATED
1692

    
1693
        self.save()
1694

    
1695
    def remove(self):
1696
        if self.is_pending:
1697
            m = _("%s: attempt to remove while is pending") % (self,)
1698
            raise AssertionError(m)
1699

    
1700
        state = self.state
1701
        if state not in [self.ACCEPTED, self.TERMINATED]:
1702
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1703
            raise AssertionError(m)
1704

    
1705
        self._set_history_item(reason='REMOVE')
1706
        self.state = self.REMOVED
1707
        self.is_pending = True
1708
        self.save()
1709

    
1710
    def reject(self):
1711
        if self.is_pending:
1712
            m = _("%s: attempt to reject while is pending") % (self,)
1713
            raise AssertionError(m)
1714

    
1715
        state = self.state
1716
        if state != self.REQUESTED:
1717
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1718
            raise AssertionError(m)
1719

    
1720
        # rejected requests don't need sync,
1721
        # because they were never effected
1722
        self._set_history_item(reason='REJECT')
1723
        self.delete()
1724

    
1725
    def get_diff_quotas(self, sub_list=None, add_list=None):
1726
        if sub_list is None:
1727
            sub_list = []
1728

    
1729
        if add_list is None:
1730
            add_list = []
1731

    
1732
        sub_append = sub_list.append
1733
        add_append = add_list.append
1734
        holder = self.person.uuid
1735

    
1736
        synced_application = self.application
1737
        if synced_application is not None:
1738
            cur_grants = synced_application.projectresourcegrant_set.all()
1739
            for grant in cur_grants:
1740
                sub_append(QuotaLimits(
1741
                               holder       = holder,
1742
                               resource     = str(grant.resource),
1743
                               capacity     = grant.member_capacity,
1744
                               import_limit = grant.member_import_limit,
1745
                               export_limit = grant.member_export_limit))
1746

    
1747
        pending_application = self.pending_application
1748
        if pending_application is not None:
1749
            new_grants = pending_application.projectresourcegrant_set.all()
1750
            for new_grant in new_grants:
1751
                add_append(QuotaLimits(
1752
                               holder       = holder,
1753
                               resource     = str(new_grant.resource),
1754
                               capacity     = new_grant.member_capacity,
1755
                               import_limit = new_grant.member_import_limit,
1756
                               export_limit = new_grant.member_export_limit))
1757

    
1758
        return (sub_list, add_list)
1759

    
1760
    def set_sync(self):
1761
        if not self.is_pending:
1762
            m = _("%s: attempt to sync a non pending membership") % (self,)
1763
            raise AssertionError(m)
1764

    
1765
        state = self.state
1766
        if state == self.ACCEPTED:
1767
            pending_application = self.pending_application
1768
            if pending_application is None:
1769
                m = _("%s: attempt to sync an empty pending application") % (
1770
                    self,)
1771
                raise AssertionError(m)
1772

    
1773
            self.application = pending_application
1774
            self.is_active = True
1775

    
1776
            self.pending_application = None
1777
            self.pending_serial = None
1778

    
1779
            # project.application may have changed in the meantime,
1780
            # in which case we stay PENDING;
1781
            # we are safe to check due to select_for_update
1782
            if self.application == self.project.application:
1783
                self.is_pending = False
1784
            self.save()
1785

    
1786
        elif state == self.TERMINATED:
1787
            if self.pending_application:
1788
                m = _("%s: attempt to sync in state '%s' "
1789
                      "with a pending application") % (self, state)
1790
                raise AssertionError(m)
1791

    
1792
            self.application = None
1793
            self.pending_serial = None
1794
            self.is_pending = False
1795
            self.save()
1796

    
1797
        elif state == self.REMOVED:
1798
            self.delete()
1799

    
1800
        else:
1801
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1802
            raise AssertionError(m)
1803

    
1804
    def reset_sync(self):
1805
        if not self.is_pending:
1806
            m = _("%s: attempt to reset a non pending membership") % (self,)
1807
            raise AssertionError(m)
1808

    
1809
        state = self.state
1810
        if state in [self.ACCEPTED, self.TERMINATED, self.REMOVED]:
1811
            self.pending_application = None
1812
            self.pending_serial = None
1813
            self.save()
1814
        else:
1815
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1816
            raise AssertionError(m)
1817

    
1818
class Serial(models.Model):
1819
    serial  =   models.AutoField(primary_key=True)
1820

    
1821
def new_serial():
1822
    s = Serial.objects.create()
1823
    serial = s.serial
1824
    s.delete()
1825
    return serial
1826

    
1827
def sync_finish_serials(serials_to_ack=None):
1828
    if serials_to_ack is None:
1829
        serials_to_ack = qh_query_serials([])
1830

    
1831
    serials_to_ack = set(serials_to_ack)
1832
    sfu = ProjectMembership.objects.select_for_update()
1833
    memberships = list(sfu.filter(pending_serial__isnull=False))
1834

    
1835
    if memberships:
1836
        for membership in memberships:
1837
            serial = membership.pending_serial
1838
            if serial in serials_to_ack:
1839
                membership.set_sync()
1840
            else:
1841
                membership.reset_sync()
1842

    
1843
        transaction.commit()
1844

    
1845
    qh_ack_serials(list(serials_to_ack))
1846
    return len(memberships)
1847

    
1848
def pre_sync():
1849
    ACCEPTED = ProjectMembership.ACCEPTED
1850
    TERMINATED = ProjectMembership.TERMINATED
1851
    psfu = Project.objects.select_for_update()
1852

    
1853
    modified = psfu.modified_projects()
1854
    for project in modified:
1855
        objects = project.projectmembership_set.select_for_update()
1856

    
1857
        memberships = objects.filter(state=ACCEPTED)
1858
        for membership in memberships:
1859
            membership.is_pending = True
1860
            membership.save()
1861

    
1862
    terminating = psfu.terminating_projects()
1863
    for project in terminating:
1864
        objects = project.projectmembership_set.select_for_update()
1865

    
1866
        memberships = objects.filter(state=ACCEPTED)
1867
        for membership in memberships:
1868
            membership.is_pending = True
1869
            membership.state = TERMINATED
1870
            membership.save()
1871

    
1872
def do_sync():
1873

    
1874
    ACCEPTED = ProjectMembership.ACCEPTED
1875
    objects = ProjectMembership.objects.select_for_update()
1876

    
1877
    sub_quota, add_quota = [], []
1878

    
1879
    serial = new_serial()
1880

    
1881
    pending = objects.filter(is_pending=True)
1882
    for membership in pending:
1883

    
1884
        if membership.pending_application:
1885
            m = "%s: impossible: pending_application is not None (%s)" % (
1886
                membership, membership.pending_application)
1887
            raise AssertionError(m)
1888
        if membership.pending_serial:
1889
            m = "%s: impossible: pending_serial is not None (%s)" % (
1890
                membership, membership.pending_serial)
1891
            raise AssertionError(m)
1892

    
1893
        if membership.state == ACCEPTED:
1894
            membership.pending_application = membership.project.application
1895

    
1896
        membership.pending_serial = serial
1897
        membership.get_diff_quotas(sub_quota, add_quota)
1898
        membership.save()
1899

    
1900
    transaction.commit()
1901
    # ProjectApplication.approve() unblocks here
1902
    # and can set PENDING an already PENDING membership
1903
    # which has been scheduled to sync with the old project.application
1904
    # Need to check in ProjectMembership.set_sync()
1905

    
1906
    r = qh_add_quota(serial, sub_quota, add_quota)
1907
    if r:
1908
        m = "cannot sync serial: %d" % serial
1909
        raise RuntimeError(m)
1910

    
1911
    return serial
1912

    
1913
def post_sync():
1914
    ACCEPTED = ProjectMembership.ACCEPTED
1915
    psfu = Project.objects.select_for_update()
1916

    
1917
    modified = psfu.modified_projects()
1918
    for project in modified:
1919
        objects = project.projectmembership_set.select_for_update()
1920

    
1921
        memberships = list(objects.filter(state=ACCEPTED, is_pending=True))
1922
        if not memberships:
1923
            project.is_modified = False
1924
            project.save()
1925

    
1926
    terminating = psfu.terminating_projects()
1927
    for project in terminating:
1928
        objects = project.projectmembership_set.select_for_update()
1929

    
1930
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1931
                                          Q(is_pending=True)))
1932
        if not memberships:
1933
            project.deactivate()
1934
            project.save()
1935

    
1936
    transaction.commit()
1937

    
1938
def sync_projects():
1939
    sync_finish_serials()
1940
    pre_sync()
1941
    serial = do_sync()
1942
    sync_finish_serials([serial])
1943
    post_sync()
1944

    
1945
def trigger_sync(retries=3, retry_wait=1.0):
1946
    transaction.commit()
1947

    
1948
    cursor = connection.cursor()
1949
    locked = True
1950
    try:
1951
        while 1:
1952
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1953
            r = cursor.fetchone()
1954
            if r is None:
1955
                m = "Impossible"
1956
                raise AssertionError(m)
1957
            locked = r[0]
1958
            if locked:
1959
                break
1960

    
1961
            retries -= 1
1962
            if retries <= 0:
1963
                return False
1964
            sleep(retry_wait)
1965

    
1966
        sync_projects()
1967
        return True
1968

    
1969
    finally:
1970
        if locked:
1971
            cursor.execute("SELECT pg_advisory_unlock(1)")
1972
            cursor.fetchall()
1973

    
1974

    
1975
class ProjectMembershipHistory(models.Model):
1976
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1977
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1978

    
1979
    person  =   models.BigIntegerField()
1980
    project =   models.BigIntegerField()
1981
    date    =   models.DateField(default=datetime.now)
1982
    reason  =   models.IntegerField()
1983
    serial  =   models.BigIntegerField()
1984

    
1985
### SIGNALS ###
1986
################
1987

    
1988
def create_astakos_user(u):
1989
    try:
1990
        AstakosUser.objects.get(user_ptr=u.pk)
1991
    except AstakosUser.DoesNotExist:
1992
        extended_user = AstakosUser(user_ptr_id=u.pk)
1993
        extended_user.__dict__.update(u.__dict__)
1994
        extended_user.save()
1995
        if not extended_user.has_auth_provider('local'):
1996
            extended_user.add_auth_provider('local')
1997
    except BaseException, e:
1998
        logger.exception(e)
1999

    
2000

    
2001
def fix_superusers(sender, **kwargs):
2002
    # Associate superusers with AstakosUser
2003
    admins = User.objects.filter(is_superuser=True)
2004
    for u in admins:
2005
        create_astakos_user(u)
2006
post_syncdb.connect(fix_superusers)
2007

    
2008

    
2009
def user_post_save(sender, instance, created, **kwargs):
2010
    if not created:
2011
        return
2012
    create_astakos_user(instance)
2013
post_save.connect(user_post_save, sender=User)
2014

    
2015
def astakosuser_post_save(sender, instance, created, **kwargs):
2016
    pass
2017

    
2018
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2019

    
2020
def resource_post_save(sender, instance, created, **kwargs):
2021
    if not created:
2022
        return
2023
    register_resources((instance,))
2024
post_save.connect(resource_post_save, sender=Resource)
2025

    
2026
def renew_token(sender, instance, **kwargs):
2027
    if not instance.auth_token:
2028
        instance.renew_token()
2029
pre_save.connect(renew_token, sender=AstakosUser)
2030
pre_save.connect(renew_token, sender=Service)
2031