Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 6c997921

History | View | Annotate | Download (64.6 kB)

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

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

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

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

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

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

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

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

    
83
logger = logging.getLogger(__name__)
84

    
85
DEFAULT_CONTENT_TYPE = None
86
_content_type = None
87

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

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

    
100
RESOURCE_SEPARATOR = '.'
101

    
102
inf = float('inf')
103

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

    
114
    def renew_token(self, expiration_date=None):
115
        md5 = hashlib.md5()
116
        md5.update(self.name.encode('ascii', 'ignore'))
117
        md5.update(self.url.encode('ascii', 'ignore'))
118
        md5.update(asctime())
119

    
120
        self.auth_token = b64encode(md5.digest())
121
        self.auth_token_created = datetime.now()
122
        if expiration_date:
123
            self.auth_token_expires = expiration_date
124
        else:
125
            self.auth_token_expires = None
126

    
127
    def __str__(self):
128
        return self.name
129

    
130
    @property
131
    def resources(self):
132
        return self.resource_set.all()
133

    
134
    @resources.setter
135
    def resources(self, resources):
136
        for s in resources:
137
            self.resource_set.create(**s)
138

    
139

    
140
class ResourceMetadata(models.Model):
141
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
142
    value = models.CharField(_('Value'), max_length=255)
143

    
144
_presentation_data = {}
145
def get_presentation(resource):
146
    global _presentation_data
147
    presentation = _presentation_data.get(resource, {})
148
    if not presentation:
149
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
150
        presentation = resource_presentation.get(resource, {})
151
        _presentation_data[resource] = presentation
152
    return presentation
153

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

    
162
    class Meta:
163
        unique_together = ("name", "service")
164

    
165
    def __str__(self):
166
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
167

    
168
    @property
169
    def help_text(self):
170
        return get_presentation(str(self)).get('help_text', '')
171

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

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

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

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

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

    
192

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

    
207
class AstakosUserManager(UserManager):
208

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

    
218
    def get_by_email(self, email):
219
        return self.get(email=email)
220

    
221
    def get_by_identifier(self, email_or_username, **kwargs):
222
        try:
223
            return self.get(email__iexact=email_or_username, **kwargs)
224
        except AstakosUser.DoesNotExist:
225
            return self.get(username__iexact=email_or_username, **kwargs)
226

    
227
    def user_exists(self, email_or_username, **kwargs):
228
        qemail = Q(email__iexact=email_or_username)
229
        qusername = Q(username__iexact=email_or_username)
230
        return self.filter(qemail | qusername).exists()
231

    
232

    
233
class AstakosUser(User):
234
    """
235
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
236
    """
237
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
238
                                   null=True)
239

    
240
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
241
    #                    AstakosUserProvider model.
242
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
243
                                null=True)
244
    # ex. screen_name for twitter, eppn for shibboleth
245
    third_party_identifier = models.CharField(_('Third-party identifier'),
246
                                              max_length=255, null=True,
247
                                              blank=True)
248

    
249

    
250
    #for invitations
251
    user_level = DEFAULT_USER_LEVEL
252
    level = models.IntegerField(_('Inviter level'), default=user_level)
253
    invitations = models.IntegerField(
254
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
255

    
256
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
257
                                  null=True, blank=True)
258
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
259
    auth_token_expires = models.DateTimeField(
260
        _('Token expiration date'), null=True)
261

    
262
    updated = models.DateTimeField(_('Update date'))
263
    is_verified = models.BooleanField(_('Is verified?'), default=False)
264

    
265
    email_verified = models.BooleanField(_('Email verified?'), default=False)
266

    
267
    has_credits = models.BooleanField(_('Has credits?'), default=False)
268
    has_signed_terms = models.BooleanField(
269
        _('I agree with the terms'), default=False)
270
    date_signed_terms = models.DateTimeField(
271
        _('Signed terms date'), null=True, blank=True)
272

    
273
    activation_sent = models.DateTimeField(
274
        _('Activation sent data'), null=True, blank=True)
275

    
276
    policy = models.ManyToManyField(
277
        Resource, null=True, through='AstakosUserQuota')
278

    
279
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
280

    
281
    __has_signed_terms = False
282
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
283
                                           default=False, db_index=True)
284

    
285
    objects = AstakosUserManager()
286

    
287
    def __init__(self, *args, **kwargs):
288
        super(AstakosUser, self).__init__(*args, **kwargs)
289
        self.__has_signed_terms = self.has_signed_terms
290
        if not self.id:
291
            self.is_active = False
292

    
293
    @property
294
    def realname(self):
295
        return '%s %s' % (self.first_name, self.last_name)
296

    
297
    @realname.setter
298
    def realname(self, value):
299
        parts = value.split(' ')
300
        if len(parts) == 2:
301
            self.first_name = parts[0]
302
            self.last_name = parts[1]
303
        else:
304
            self.last_name = parts[0]
305

    
306
    def add_permission(self, pname):
307
        if self.has_perm(pname):
308
            return
309
        p, created = Permission.objects.get_or_create(
310
                                    codename=pname,
311
                                    name=pname.capitalize(),
312
                                    content_type=get_content_type())
313
        self.user_permissions.add(p)
314

    
315
    def remove_permission(self, pname):
316
        if self.has_perm(pname):
317
            return
318
        p = Permission.objects.get(codename=pname,
319
                                   content_type=get_content_type())
320
        self.user_permissions.remove(p)
321

    
322
    @property
323
    def invitation(self):
324
        try:
325
            return Invitation.objects.get(username=self.email)
326
        except Invitation.DoesNotExist:
327
            return None
328

    
329
    @property
330
    def quota(self):
331
        """Returns a dict with the sum of quota limits per resource"""
332
        d = defaultdict(int)
333
        default_quota = get_default_quota()
334
        d.update(default_quota)
335
        for q in self.policies:
336
            d[q.resource] = q.capacity or inf
337
        for m in self.projectmembership_set.select_related().all():
338
            if not m.acceptance_date:
339
                continue
340
            p = m.project
341
            if not p.is_active():
342
                continue
343
            grants = p.application.projectresourcegrant_set.all()
344
            for g in grants:
345
                d[str(g.resource)] += g.member_capacity or inf
346
        return d
347

    
348
    @property
349
    def policies(self):
350
        return self.astakosuserquota_set.select_related().all()
351

    
352
    @policies.setter
353
    def policies(self, policies):
354
        for p in policies:
355
            p.setdefault('resource', '')
356
            p.setdefault('capacity', 0)
357
            p.setdefault('quantity', 0)
358
            p.setdefault('import_limit', 0)
359
            p.setdefault('export_limit', 0)
360
            p.setdefault('update', True)
361
            self.add_resource_policy(**p)
362

    
363
    def add_resource_policy(
364
            self, resource, capacity, quantity, import_limit,
365
            export_limit, update=True):
366
        """Raises ObjectDoesNotExist, IntegrityError"""
367
        s, sep, r = resource.partition(RESOURCE_SEPARATOR)
368
        resource = Resource.objects.get(service__name=s, name=r)
369
        if update:
370
            AstakosUserQuota.objects.update_or_create(
371
                user=self, resource=resource, defaults={
372
                    'capacity':capacity,
373
                    'quantity': quantity,
374
                    'import_limit':import_limit,
375
                    'export_limit':export_limit})
376
        else:
377
            q = self.astakosuserquota_set
378
            q.create(
379
                resource=resource, capacity=capacity, quanity=quantity,
380
                import_limit=import_limit, export_limit=export_limit)
381

    
382
    def remove_resource_policy(self, service, resource):
383
        """Raises ObjectDoesNotExist, IntegrityError"""
384
        resource = Resource.objects.get(service__name=service, name=resource)
385
        q = self.policies.get(resource=resource).delete()
386

    
387
    def update_uuid(self):
388
        while not self.uuid:
389
            uuid_val =  str(uuid.uuid4())
390
            try:
391
                AstakosUser.objects.get(uuid=uuid_val)
392
            except AstakosUser.DoesNotExist, e:
393
                self.uuid = uuid_val
394
        return self.uuid
395

    
396
    def save(self, update_timestamps=True, **kwargs):
397
        if update_timestamps:
398
            if not self.id:
399
                self.date_joined = datetime.now()
400
            self.updated = datetime.now()
401

    
402
        # update date_signed_terms if necessary
403
        if self.__has_signed_terms != self.has_signed_terms:
404
            self.date_signed_terms = datetime.now()
405

    
406
        self.update_uuid()
407

    
408
        if self.username != self.email.lower():
409
            # set username
410
            self.username = self.email.lower()
411

    
412
        self.validate_unique_email_isactive()
413

    
414
        super(AstakosUser, self).save(**kwargs)
415

    
416
    def renew_token(self, flush_sessions=False, current_key=None):
417
        md5 = hashlib.md5()
418
        md5.update(settings.SECRET_KEY)
419
        md5.update(self.username)
420
        md5.update(self.realname.encode('ascii', 'ignore'))
421
        md5.update(asctime())
422

    
423
        self.auth_token = b64encode(md5.digest())
424
        self.auth_token_created = datetime.now()
425
        self.auth_token_expires = self.auth_token_created + \
426
                                  timedelta(hours=AUTH_TOKEN_DURATION)
427
        if flush_sessions:
428
            self.flush_sessions(current_key)
429
        msg = 'Token renewed for %s' % self.email
430
        logger.log(LOGGING_LEVEL, msg)
431

    
432
    def flush_sessions(self, current_key=None):
433
        q = self.sessions
434
        if current_key:
435
            q = q.exclude(session_key=current_key)
436

    
437
        keys = q.values_list('session_key', flat=True)
438
        if keys:
439
            msg = 'Flushing sessions: %s' % ','.join(keys)
440
            logger.log(LOGGING_LEVEL, msg, [])
441
        engine = import_module(settings.SESSION_ENGINE)
442
        for k in keys:
443
            s = engine.SessionStore(k)
444
            s.flush()
445

    
446
    def __unicode__(self):
447
        return '%s (%s)' % (self.realname, self.email)
448

    
449
    def conflicting_email(self):
450
        q = AstakosUser.objects.exclude(username=self.username)
451
        q = q.filter(email__iexact=self.email)
452
        if q.count() != 0:
453
            return True
454
        return False
455

    
456
    def validate_unique_email_isactive(self):
457
        """
458
        Implements a unique_together constraint for email and is_active fields.
459
        """
460
        q = AstakosUser.objects.all()
461
        q = q.filter(email = self.email)
462
        if self.id:
463
            q = q.filter(~Q(id = self.id))
464
        if q.count() != 0:
465
            m = 'Another account with the same email = %(email)s & \
466
                is_active = %(is_active)s found.' % self.__dict__
467
            raise ValidationError(m)
468

    
469
    def email_change_is_pending(self):
470
        return self.emailchanges.count() > 0
471

    
472
    def email_change_is_pending(self):
473
        return self.emailchanges.count() > 0
474

    
475
    @property
476
    def signed_terms(self):
477
        term = get_latest_terms()
478
        if not term:
479
            return True
480
        if not self.has_signed_terms:
481
            return False
482
        if not self.date_signed_terms:
483
            return False
484
        if self.date_signed_terms < term.date:
485
            self.has_signed_terms = False
486
            self.date_signed_terms = None
487
            self.save()
488
            return False
489
        return True
490

    
491
    def set_invitations_level(self):
492
        """
493
        Update user invitation level
494
        """
495
        level = self.invitation.inviter.level + 1
496
        self.level = level
497
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
498

    
499
    def can_login_with_auth_provider(self, provider):
500
        if not self.has_auth_provider(provider):
501
            return False
502
        else:
503
            return auth_providers.get_provider(provider).is_available_for_login()
504

    
505
    def can_add_auth_provider(self, provider, **kwargs):
506
        provider_settings = auth_providers.get_provider(provider)
507

    
508
        if not provider_settings.is_available_for_add():
509
            return False
510

    
511
        if self.has_auth_provider(provider) and \
512
           provider_settings.one_per_user:
513
            return False
514

    
515
        if 'provider_info' in kwargs:
516
            kwargs.pop('provider_info')
517

    
518
        if 'identifier' in kwargs:
519
            try:
520
                # provider with specified params already exist
521
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
522
                                                                   **kwargs)
523
            except AstakosUser.DoesNotExist:
524
                return True
525
            else:
526
                return False
527

    
528
        return True
529

    
530
    def can_remove_auth_provider(self, module):
531
        provider = auth_providers.get_provider(module)
532
        existing = self.get_active_auth_providers()
533
        existing_for_provider = self.get_active_auth_providers(module=module)
534

    
535
        if len(existing) <= 1:
536
            return False
537

    
538
        if len(existing_for_provider) == 1 and provider.is_required():
539
            return False
540

    
541
        return True
542

    
543
    def can_change_password(self):
544
        return self.has_auth_provider('local', auth_backend='astakos')
545

    
546
    def has_required_auth_providers(self):
547
        required = auth_providers.REQUIRED_PROVIDERS
548
        for provider in required:
549
            if not self.has_auth_provider(provider):
550
                return False
551
        return True
552

    
553
    def has_auth_provider(self, provider, **kwargs):
554
        return bool(self.auth_providers.filter(module=provider,
555
                                               **kwargs).count())
556

    
557
    def add_auth_provider(self, provider, **kwargs):
558
        info_data = ''
559
        if 'provider_info' in kwargs:
560
            info_data = kwargs.pop('provider_info')
561
            if isinstance(info_data, dict):
562
                info_data = json.dumps(info_data)
563

    
564
        if self.can_add_auth_provider(provider, **kwargs):
565
            self.auth_providers.create(module=provider, active=True,
566
                                       info_data=info_data,
567
                                       **kwargs)
568
        else:
569
            raise Exception('Cannot add provider')
570

    
571
    def add_pending_auth_provider(self, pending):
572
        """
573
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
574
        the current user.
575
        """
576
        if not isinstance(pending, PendingThirdPartyUser):
577
            pending = PendingThirdPartyUser.objects.get(token=pending)
578

    
579
        provider = self.add_auth_provider(pending.provider,
580
                               identifier=pending.third_party_identifier,
581
                                affiliation=pending.affiliation,
582
                                          provider_info=pending.info)
583

    
584
        if email_re.match(pending.email or '') and pending.email != self.email:
585
            self.additionalmail_set.get_or_create(email=pending.email)
586

    
587
        pending.delete()
588
        return provider
589

    
590
    def remove_auth_provider(self, provider, **kwargs):
591
        self.auth_providers.get(module=provider, **kwargs).delete()
592

    
593
    # user urls
594
    def get_resend_activation_url(self):
595
        return reverse('send_activation', kwargs={'user_id': self.pk})
596

    
597
    def get_provider_remove_url(self, module, **kwargs):
598
        return reverse('remove_auth_provider', kwargs={
599
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
600

    
601
    def get_activation_url(self, nxt=False):
602
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
603
                                 quote(self.auth_token))
604
        if nxt:
605
            url += "&next=%s" % quote(nxt)
606
        return url
607

    
608
    def get_password_reset_url(self, token_generator=default_token_generator):
609
        return reverse('django.contrib.auth.views.password_reset_confirm',
610
                          kwargs={'uidb36':int_to_base36(self.id),
611
                                  'token':token_generator.make_token(self)})
612

    
613
    def get_auth_providers(self):
614
        return self.auth_providers.all()
615

    
616
    def get_available_auth_providers(self):
617
        """
618
        Returns a list of providers available for user to connect to.
619
        """
620
        providers = []
621
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
622
            if self.can_add_auth_provider(module):
623
                providers.append(provider_settings(self))
624

    
625
        return providers
626

    
627
    def get_active_auth_providers(self, **filters):
628
        providers = []
629
        for provider in self.auth_providers.active(**filters):
630
            if auth_providers.get_provider(provider.module).is_available_for_login():
631
                providers.append(provider)
632
        return providers
633

    
634
    @property
635
    def auth_providers_display(self):
636
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
637

    
638
    def get_inactive_message(self):
639
        msg_extra = ''
640
        message = ''
641
        if self.activation_sent:
642
            if self.email_verified:
643
                message = _(astakos_messages.ACCOUNT_INACTIVE)
644
            else:
645
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
646
                if astakos_settings.MODERATION_ENABLED:
647
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
648
                else:
649
                    url = self.get_resend_activation_url()
650
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
651
                                u' ' + \
652
                                _('<a href="%s">%s?</a>') % (url,
653
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
654
        else:
655
            if astakos_settings.MODERATION_ENABLED:
656
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
657
            else:
658
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
659
                url = self.get_resend_activation_url()
660
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
661
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
662

    
663
        return mark_safe(message + u' '+ msg_extra)
664

    
665
    def owns_project(self, project):
666
        return project.user_status(self) == 100
667

    
668
    def is_project_member(self, project):
669
        return project.user_status(self) in [0,1,2,3]
670

    
671
    def is_project_accepted_member(self, project):
672
        return project.user_status(self) == 2
673

    
674

    
675
class AstakosUserAuthProviderManager(models.Manager):
676

    
677
    def active(self, **filters):
678
        return self.filter(active=True, **filters)
679

    
680

    
681
class AstakosUserAuthProvider(models.Model):
682
    """
683
    Available user authentication methods.
684
    """
685
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
686
                                   null=True, default=None)
687
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
688
    module = models.CharField(_('Provider'), max_length=255, blank=False,
689
                                default='local')
690
    identifier = models.CharField(_('Third-party identifier'),
691
                                              max_length=255, null=True,
692
                                              blank=True)
693
    active = models.BooleanField(default=True)
694
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
695
                                   default='astakos')
696
    info_data = models.TextField(default="", null=True, blank=True)
697
    created = models.DateTimeField('Creation date', auto_now_add=True)
698

    
699
    objects = AstakosUserAuthProviderManager()
700

    
701
    class Meta:
702
        unique_together = (('identifier', 'module', 'user'), )
703
        ordering = ('module', 'created')
704

    
705
    def __init__(self, *args, **kwargs):
706
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
707
        try:
708
            self.info = json.loads(self.info_data)
709
            if not self.info:
710
                self.info = {}
711
        except Exception, e:
712
            self.info = {}
713

    
714
        for key,value in self.info.iteritems():
715
            setattr(self, 'info_%s' % key, value)
716

    
717

    
718
    @property
719
    def settings(self):
720
        return auth_providers.get_provider(self.module)
721

    
722
    @property
723
    def details_display(self):
724
        try:
725
          return self.settings.get_details_tpl_display % self.__dict__
726
        except:
727
          return ''
728

    
729
    @property
730
    def title_display(self):
731
        title_tpl = self.settings.get_title_display
732
        try:
733
            if self.settings.get_user_title_display:
734
                title_tpl = self.settings.get_user_title_display
735
        except Exception, e:
736
            pass
737
        try:
738
          return title_tpl % self.__dict__
739
        except:
740
          return self.settings.get_title_display % self.__dict__
741

    
742
    def can_remove(self):
743
        return self.user.can_remove_auth_provider(self.module)
744

    
745
    def delete(self, *args, **kwargs):
746
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
747
        if self.module == 'local':
748
            self.user.set_unusable_password()
749
            self.user.save()
750
        return ret
751

    
752
    def __repr__(self):
753
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
754

    
755
    def __unicode__(self):
756
        if self.identifier:
757
            return "%s:%s" % (self.module, self.identifier)
758
        if self.auth_backend:
759
            return "%s:%s" % (self.module, self.auth_backend)
760
        return self.module
761

    
762
    def save(self, *args, **kwargs):
763
        self.info_data = json.dumps(self.info)
764
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
765

    
766

    
767
class ExtendedManager(models.Manager):
768
    def _update_or_create(self, **kwargs):
769
        assert kwargs, \
770
            'update_or_create() must be passed at least one keyword argument'
771
        obj, created = self.get_or_create(**kwargs)
772
        defaults = kwargs.pop('defaults', {})
773
        if created:
774
            return obj, True, False
775
        else:
776
            try:
777
                params = dict(
778
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
779
                params.update(defaults)
780
                for attr, val in params.items():
781
                    if hasattr(obj, attr):
782
                        setattr(obj, attr, val)
783
                sid = transaction.savepoint()
784
                obj.save(force_update=True)
785
                transaction.savepoint_commit(sid)
786
                return obj, False, True
787
            except IntegrityError, e:
788
                transaction.savepoint_rollback(sid)
789
                try:
790
                    return self.get(**kwargs), False, False
791
                except self.model.DoesNotExist:
792
                    raise e
793

    
794
    update_or_create = _update_or_create
795

    
796

    
797
class AstakosUserQuota(models.Model):
798
    objects = ExtendedManager()
799
    capacity = models.BigIntegerField(_('Capacity'), null=True)
800
    quantity = models.BigIntegerField(_('Quantity'), null=True)
801
    export_limit = models.BigIntegerField(_('Export limit'), null=True)
802
    import_limit = models.BigIntegerField(_('Import limit'), null=True)
803
    resource = models.ForeignKey(Resource)
804
    user = models.ForeignKey(AstakosUser)
805

    
806
    class Meta:
807
        unique_together = ("resource", "user")
808

    
809

    
810
class ApprovalTerms(models.Model):
811
    """
812
    Model for approval terms
813
    """
814

    
815
    date = models.DateTimeField(
816
        _('Issue date'), db_index=True, default=datetime.now())
817
    location = models.CharField(_('Terms location'), max_length=255)
818

    
819

    
820
class Invitation(models.Model):
821
    """
822
    Model for registring invitations
823
    """
824
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
825
                                null=True)
826
    realname = models.CharField(_('Real name'), max_length=255)
827
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
828
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
829
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
830
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
831
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
832

    
833
    def __init__(self, *args, **kwargs):
834
        super(Invitation, self).__init__(*args, **kwargs)
835
        if not self.id:
836
            self.code = _generate_invitation_code()
837

    
838
    def consume(self):
839
        self.is_consumed = True
840
        self.consumed = datetime.now()
841
        self.save()
842

    
843
    def __unicode__(self):
844
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
845

    
846

    
847
class EmailChangeManager(models.Manager):
848

    
849
    @transaction.commit_on_success
850
    def change_email(self, activation_key):
851
        """
852
        Validate an activation key and change the corresponding
853
        ``User`` if valid.
854

855
        If the key is valid and has not expired, return the ``User``
856
        after activating.
857

858
        If the key is not valid or has expired, return ``None``.
859

860
        If the key is valid but the ``User`` is already active,
861
        return ``None``.
862

863
        After successful email change the activation record is deleted.
864

865
        Throws ValueError if there is already
866
        """
867
        try:
868
            email_change = self.model.objects.get(
869
                activation_key=activation_key)
870
            if email_change.activation_key_expired():
871
                email_change.delete()
872
                raise EmailChange.DoesNotExist
873
            # is there an active user with this address?
874
            try:
875
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
876
            except AstakosUser.DoesNotExist:
877
                pass
878
            else:
879
                raise ValueError(_('The new email address is reserved.'))
880
            # update user
881
            user = AstakosUser.objects.get(pk=email_change.user_id)
882
            old_email = user.email
883
            user.email = email_change.new_email_address
884
            user.save()
885
            email_change.delete()
886
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
887
                                                          user.email)
888
            logger.log(LOGGING_LEVEL, msg)
889
            return user
890
        except EmailChange.DoesNotExist:
891
            raise ValueError(_('Invalid activation key.'))
892

    
893

    
894
class EmailChange(models.Model):
895
    new_email_address = models.EmailField(
896
        _(u'new e-mail address'),
897
        help_text=_('Your old email address will be used until you verify your new one.'))
898
    user = models.ForeignKey(
899
        AstakosUser, unique=True, related_name='emailchanges')
900
    requested_at = models.DateTimeField(default=datetime.now())
901
    activation_key = models.CharField(
902
        max_length=40, unique=True, db_index=True)
903

    
904
    objects = EmailChangeManager()
905

    
906
    def get_url(self):
907
        return reverse('email_change_confirm',
908
                      kwargs={'activation_key': self.activation_key})
909

    
910
    def activation_key_expired(self):
911
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
912
        return self.requested_at + expiration_date < datetime.now()
913

    
914

    
915
class AdditionalMail(models.Model):
916
    """
917
    Model for registring invitations
918
    """
919
    owner = models.ForeignKey(AstakosUser)
920
    email = models.EmailField()
921

    
922

    
923
def _generate_invitation_code():
924
    while True:
925
        code = randint(1, 2L ** 63 - 1)
926
        try:
927
            Invitation.objects.get(code=code)
928
            # An invitation with this code already exists, try again
929
        except Invitation.DoesNotExist:
930
            return code
931

    
932

    
933
def get_latest_terms():
934
    try:
935
        term = ApprovalTerms.objects.order_by('-id')[0]
936
        return term
937
    except IndexError:
938
        pass
939
    return None
940

    
941
class PendingThirdPartyUser(models.Model):
942
    """
943
    Model for registring successful third party user authentications
944
    """
945
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
946
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
947
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
948
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
949
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
950
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
951
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
952
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
953
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
954
    info = models.TextField(default="", null=True, blank=True)
955

    
956
    class Meta:
957
        unique_together = ("provider", "third_party_identifier")
958

    
959
    def get_user_instance(self):
960
        d = self.__dict__
961
        d.pop('_state', None)
962
        d.pop('id', None)
963
        d.pop('token', None)
964
        d.pop('created', None)
965
        d.pop('info', None)
966
        user = AstakosUser(**d)
967

    
968
        return user
969

    
970
    @property
971
    def realname(self):
972
        return '%s %s' %(self.first_name, self.last_name)
973

    
974
    @realname.setter
975
    def realname(self, value):
976
        parts = value.split(' ')
977
        if len(parts) == 2:
978
            self.first_name = parts[0]
979
            self.last_name = parts[1]
980
        else:
981
            self.last_name = parts[0]
982

    
983
    def save(self, **kwargs):
984
        if not self.id:
985
            # set username
986
            while not self.username:
987
                username =  uuid.uuid4().hex[:30]
988
                try:
989
                    AstakosUser.objects.get(username = username)
990
                except AstakosUser.DoesNotExist, e:
991
                    self.username = username
992
        super(PendingThirdPartyUser, self).save(**kwargs)
993

    
994
    def generate_token(self):
995
        self.password = self.third_party_identifier
996
        self.last_login = datetime.now()
997
        self.token = default_token_generator.make_token(self)
998

    
999
class SessionCatalog(models.Model):
1000
    session_key = models.CharField(_('session key'), max_length=40)
1001
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1002

    
1003

    
1004
### PROJECTS ###
1005
################
1006

    
1007
def synced_model_metaclass(class_name, class_parents, class_attributes):
1008

    
1009
    new_attributes = {}
1010
    sync_attributes = {}
1011

    
1012
    for name, value in class_attributes.iteritems():
1013
        sync, underscore, rest = name.partition('_')
1014
        if sync == 'sync' and underscore == '_':
1015
            sync_attributes[rest] = value
1016
        else:
1017
            new_attributes[name] = value
1018

    
1019
    if 'prefix' not in sync_attributes:
1020
        m = ("you did not specify a 'sync_prefix' attribute "
1021
             "in class '%s'" % (class_name,))
1022
        raise ValueError(m)
1023

    
1024
    prefix = sync_attributes.pop('prefix')
1025
    class_name = sync_attributes.pop('classname', prefix + '_model')
1026

    
1027
    for name, value in sync_attributes.iteritems():
1028
        newname = prefix + '_' + name
1029
        if newname in new_attributes:
1030
            m = ("class '%s' was specified with prefix '%s' "
1031
                 "but it already has an attribute named '%s'"
1032
                 % (class_name, prefix, newname))
1033
            raise ValueError(m)
1034

    
1035
        new_attributes[newname] = value
1036

    
1037
    newclass = type(class_name, class_parents, new_attributes)
1038
    return newclass
1039

    
1040

    
1041
def make_synced(prefix='sync', name='SyncedState'):
1042

    
1043
    the_name = name
1044
    the_prefix = prefix
1045

    
1046
    class SyncedState(models.Model):
1047

    
1048
        sync_classname      = the_name
1049
        sync_prefix         = the_prefix
1050
        __metaclass__       = synced_model_metaclass
1051

    
1052
        sync_new_state      = models.BigIntegerField(null=True)
1053
        sync_synced_state   = models.BigIntegerField(null=True)
1054
        STATUS_SYNCED       = 0
1055
        STATUS_PENDING      = 1
1056
        sync_status         = models.IntegerField(db_index=True)
1057

    
1058
        class Meta:
1059
            abstract = True
1060

    
1061
        class NotSynced(Exception):
1062
            pass
1063

    
1064
        def sync_init_state(self, state):
1065
            self.sync_synced_state = state
1066
            self.sync_new_state = state
1067
            self.sync_status = self.STATUS_SYNCED
1068

    
1069
        def sync_get_status(self):
1070
            return self.sync_status
1071

    
1072
        def sync_set_status(self):
1073
            if self.sync_new_state != self.sync_synced_state:
1074
                self.sync_status = self.STATUS_PENDING
1075
            else:
1076
                self.sync_status = self.STATUS_SYNCED
1077

    
1078
        def sync_set_synced(self):
1079
            self.sync_synced_state = self.sync_new_state
1080
            self.sync_status = self.STATUS_SYNCED
1081

    
1082
        def sync_get_synced_state(self):
1083
            return self.sync_synced_state
1084

    
1085
        def sync_set_new_state(self, new_state):
1086
            self.sync_new_state = new_state
1087
            self.sync_set_status()
1088

    
1089
        def sync_get_new_state(self):
1090
            return self.sync_new_state
1091

    
1092
        def sync_set_synced_state(self, synced_state):
1093
            self.sync_synced_state = synced_state
1094
            self.sync_set_status()
1095

    
1096
        def sync_get_pending_objects(self):
1097
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1098
            return self.objects.filter(**kw)
1099

    
1100
        def sync_get_synced_objects(self):
1101
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1102
            return self.objects.filter(**kw)
1103

    
1104
        def sync_verify_get_synced_state(self):
1105
            status = self.sync_get_status()
1106
            state = self.sync_get_synced_state()
1107
            verified = (status == self.STATUS_SYNCED)
1108
            return state, verified
1109

    
1110
        def sync_is_synced(self):
1111
            state, verified = self.sync_verify_get_synced_state()
1112
            return verified
1113

    
1114
    return SyncedState
1115

    
1116
SyncedState = make_synced(prefix='sync', name='SyncedState')
1117

    
1118

    
1119
class ProjectApplicationManager(ForUpdateManager):
1120

    
1121
    def user_projects(self, user):
1122
        """
1123
        Return projects accessed by specified user.
1124
        """
1125
        return self.filter(Q(owner=user) | Q(applicant=user) | \
1126
                        Q(project__projectmembership__person=user)).order_by('pk').distinct()
1127

    
1128
    def search_by_name(self, *search_strings):
1129
        q = Q()
1130
        for s in search_strings:
1131
            q = q | Q(name__icontains=s)
1132
        return self.filter(q)
1133

    
1134

    
1135
class ProjectApplication(models.Model):
1136
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1137
    applicant               =   models.ForeignKey(
1138
                                    AstakosUser,
1139
                                    related_name='projects_applied',
1140
                                    db_index=True)
1141

    
1142
    state                   =   models.CharField(max_length=80,
1143
                                                default=UNKNOWN)
1144

    
1145
    owner                   =   models.ForeignKey(
1146
                                    AstakosUser,
1147
                                    related_name='projects_owned',
1148
                                    db_index=True)
1149

    
1150
    precursor_application   =   models.OneToOneField('ProjectApplication',
1151
                                                     null=True,
1152
                                                     blank=True,
1153
                                                     db_index=True)
1154

    
1155
    name                    =   models.CharField(max_length=80)
1156
    homepage                =   models.URLField(max_length=255, null=True)
1157
    description             =   models.TextField(null=True, blank=True)
1158
    start_date              =   models.DateTimeField(null=True, blank=True)
1159
    end_date                =   models.DateTimeField()
1160
    member_join_policy      =   models.IntegerField()
1161
    member_leave_policy     =   models.IntegerField()
1162
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1163
    resource_grants         =   models.ManyToManyField(
1164
                                    Resource,
1165
                                    null=True,
1166
                                    blank=True,
1167
                                    through='ProjectResourceGrant')
1168
    comments                =   models.TextField(null=True, blank=True)
1169
    issue_date              =   models.DateTimeField()
1170

    
1171

    
1172
    objects                 =   ProjectApplicationManager()
1173

    
1174
    def __unicode__(self):
1175
        return "%s applied by %s" % (self.name, self.applicant)
1176

    
1177
    def add_resource_policy(self, service, resource, uplimit):
1178
        """Raises ObjectDoesNotExist, IntegrityError"""
1179
        q = self.projectresourcegrant_set
1180
        resource = Resource.objects.get(service__name=service, name=resource)
1181
        q.create(resource=resource, member_capacity=uplimit)
1182

    
1183
    def user_status(self, user):
1184
        """
1185
        100 OWNER
1186
        0   REQUESTED
1187
        1   PENDING
1188
        2   ACCEPTED
1189
        3   REMOVING
1190
        4   REMOVED
1191
       -1   User has no association with the project
1192
        """
1193
        if user == self.owner:
1194
            status = 100
1195
        else:
1196
            try:
1197
                membership = self.project.projectmembership_set.get(person=user)
1198
                status = membership.state
1199
            except Project.DoesNotExist:
1200
                status = -1
1201
            except ProjectMembership.DoesNotExist:
1202
                status = -1
1203

    
1204
        return status
1205

    
1206
    def members_count(self):
1207
        return self.project.approved_memberships.count()
1208

    
1209
    @property
1210
    def grants(self):
1211
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1212

    
1213
    @property
1214
    def resource_policies(self):
1215
        return self.projectresourcegrant_set.all()
1216

    
1217
    @resource_policies.setter
1218
    def resource_policies(self, policies):
1219
        for p in policies:
1220
            service = p.get('service', None)
1221
            resource = p.get('resource', None)
1222
            uplimit = p.get('uplimit', 0)
1223
            self.add_resource_policy(service, resource, uplimit)
1224

    
1225
    @property
1226
    def follower(self):
1227
        try:
1228
            return ProjectApplication.objects.get(precursor_application=self)
1229
        except ProjectApplication.DoesNotExist:
1230
            return
1231

    
1232
    def submit(self, resource_policies, applicant, comments,
1233
               precursor_application=None):
1234

    
1235
        if precursor_application:
1236
            self.precursor_application = precursor_application
1237
            self.owner = precursor_application.owner
1238
        else:
1239
            self.owner = applicant
1240

    
1241
        self.id = None
1242
        self.applicant = applicant
1243
        self.comments = comments
1244
        self.issue_date = datetime.now()
1245
        self.state = self.PENDING
1246
        self.save()
1247
        self.resource_policies = resource_policies
1248

    
1249
    def _get_project(self):
1250
        precursor = self
1251
        while precursor:
1252
            try:
1253
                objects = Project.objects.select_for_update()
1254
                project = objects.get(application=precursor)
1255
                return project
1256
            except Project.DoesNotExist:
1257
                pass
1258
            precursor = precursor.precursor_application
1259

    
1260
        return None
1261

    
1262
    def approve(self, approval_user=None):
1263
        """
1264
        If approval_user then during owner membership acceptance
1265
        it is checked whether the request_user is eligible.
1266

1267
        Raises:
1268
            PermissionDenied
1269
        """
1270

    
1271
        if not transaction.is_managed():
1272
            raise AssertionError("NOPE")
1273

    
1274
        new_project_name = self.name
1275
        if self.state != self.PENDING:
1276
            m = _("cannot approve: project '%s' in state '%s'") % (
1277
                    new_project_name, self.state)
1278
            raise PermissionDenied(m) # invalid argument
1279

    
1280
        now = datetime.now()
1281
        project = self._get_project()
1282

    
1283
        try:
1284
            # needs SERIALIZABLE
1285
            conflicting_project = Project.objects.get(name=new_project_name)
1286
            if (conflicting_project.is_alive and
1287
                conflicting_project != project):
1288
                m = (_("cannot approve: project with name '%s' "
1289
                       "already exists (serial: %s)") % (
1290
                        new_project_name, conflicting_project.id))
1291
                raise PermissionDenied(m) # invalid argument
1292
        except Project.DoesNotExist:
1293
            pass
1294

    
1295
        new_project = False
1296
        if project is None:
1297
            new_project = True
1298
            project = Project(creation_date=now)
1299

    
1300
        project.name = new_project_name
1301
        project.application = self
1302
        project.last_approval_date = now
1303
        project.save()
1304

    
1305
        if new_project:
1306
            project.add_member(self.owner)
1307

    
1308
        # This will block while syncing,
1309
        # but unblock before setting the membership state.
1310
        # See ProjectMembership.set_sync()
1311
        project.set_membership_pending_sync()
1312

    
1313
        precursor = self.precursor_application
1314
        while precursor:
1315
            precursor.state = self.REPLACED
1316
            precursor.save()
1317
            precursor = precursor.precursor_application
1318

    
1319
        self.state = self.APPROVED
1320
        self.save()
1321

    
1322

    
1323
class ProjectResourceGrant(models.Model):
1324

    
1325
    resource                =   models.ForeignKey(Resource)
1326
    project_application     =   models.ForeignKey(ProjectApplication,
1327
                                                  null=True)
1328
    project_capacity        =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1329
    project_import_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1330
    project_export_limit    =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1331
    member_capacity         =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1332
    member_import_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1333
    member_export_limit     =   intDecimalField(default=QH_PRACTICALLY_INFINITE)
1334

    
1335
    objects = ExtendedManager()
1336

    
1337
    class Meta:
1338
        unique_together = ("resource", "project_application")
1339

    
1340

    
1341
class Project(models.Model):
1342

    
1343
    application                 =   models.OneToOneField(
1344
                                            ProjectApplication,
1345
                                            related_name='project')
1346
    last_approval_date          =   models.DateTimeField(null=True)
1347

    
1348
    members                     =   models.ManyToManyField(
1349
                                            AstakosUser,
1350
                                            through='ProjectMembership')
1351

    
1352
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1353
    deactivation_start_date     =   models.DateTimeField(null=True)
1354
    deactivation_date           =   models.DateTimeField(null=True)
1355

    
1356
    creation_date               =   models.DateTimeField()
1357
    name                        =   models.CharField(
1358
                                            max_length=80,
1359
                                            db_index=True,
1360
                                            unique=True)
1361

    
1362
    TERMINATED  =   'TERMINATED'
1363
    SUSPENDED   =   'SUSPENDED'
1364

    
1365
    objects     =   ForUpdateManager()
1366

    
1367
    def __str__(self):
1368
        return _("<project %s '%s'>") % (self.id, self.application.name)
1369

    
1370
    __repr__ = __str__
1371

    
1372
    def is_deactivating(self):
1373
        return bool(self.deactivation_start_date)
1374

    
1375
    def is_deactivated_synced(self):
1376
        return bool(self.deactivation_date)
1377

    
1378
    def is_deactivated(self):
1379
        return self.is_deactivated_synced() or self.is_deactivating()
1380

    
1381
    def is_still_approved(self):
1382
        return bool(self.last_approval_date)
1383

    
1384
    def is_active(self):
1385
        return not(self.is_deactivated())
1386

    
1387
    def is_inconsistent(self):
1388
        now = datetime.now()
1389
        dates = [self.creation_date,
1390
                 self.last_approval_date,
1391
                 self.deactivation_start_date,
1392
                 self.deactivation_date]
1393
        return any([date > now for date in dates])
1394

    
1395
    def set_deactivation_start_date(self):
1396
        self.deactivation_start_date = datetime.now()
1397

    
1398
    def set_deactivation_date(self):
1399
        self.deactivation_start_date = None
1400
        self.deactivation_date = datetime.now()
1401

    
1402
    def violates_resource_grants(self):
1403
        return False
1404

    
1405
    def violates_members_limit(self, adding=0):
1406
        application = self.application
1407
        return (len(self.approved_members) + adding >
1408
                application.limit_on_members_number)
1409

    
1410
    @property
1411
    def is_alive(self):
1412
        return self.is_active()
1413

    
1414
    @property
1415
    def approved_memberships(self):
1416
        query = ProjectMembership.query_approved()
1417
        return self.projectmembership_set.filter(query)
1418

    
1419
    @property
1420
    def approved_members(self):
1421
        return [m.person for m in self.approved_memberships]
1422

    
1423
    def set_membership_pending_sync(self):
1424
        query = ProjectMembership.query_approved()
1425
        sfu = self.projectmembership_set.select_for_update()
1426
        members = sfu.filter(query)
1427

    
1428
        for member in members:
1429
            member.state = member.PENDING
1430
            member.save()
1431

    
1432
    def add_member(self, user):
1433
        """
1434
        Raises:
1435
            django.exceptions.PermissionDenied
1436
            astakos.im.models.AstakosUser.DoesNotExist
1437
        """
1438
        if isinstance(user, int):
1439
            user = AstakosUser.objects.get(user=user)
1440

    
1441
        m, created = ProjectMembership.objects.get_or_create(
1442
            person=user, project=self
1443
        )
1444
        m.accept()
1445

    
1446
    def remove_member(self, user):
1447
        """
1448
        Raises:
1449
            django.exceptions.PermissionDenied
1450
            astakos.im.models.AstakosUser.DoesNotExist
1451
            astakos.im.models.ProjectMembership.DoesNotExist
1452
        """
1453
        if isinstance(user, int):
1454
            user = AstakosUser.objects.get(user=user)
1455

    
1456
        m = ProjectMembership.objects.get(person=user, project=self)
1457
        m.remove()
1458

    
1459
    def terminate(self):
1460
        self.set_deactivation_start_date()
1461
        self.deactivation_reason = self.TERMINATED
1462
        self.save()
1463

    
1464
    @property
1465
    def is_terminated(self):
1466
        return (self.is_deactivated() and
1467
                self.deactivation_reason == self.TERMINATED)
1468

    
1469
    @property
1470
    def is_suspended(self):
1471
        return False
1472

    
1473
class ProjectMembership(models.Model):
1474

    
1475
    person              =   models.ForeignKey(AstakosUser)
1476
    request_date        =   models.DateField(default=datetime.now())
1477
    project             =   models.ForeignKey(Project)
1478

    
1479
    state               =   models.IntegerField(default=0)
1480
    application         =   models.ForeignKey(
1481
                                ProjectApplication,
1482
                                null=True,
1483
                                related_name='memberships')
1484
    pending_application =   models.ForeignKey(
1485
                                ProjectApplication,
1486
                                null=True,
1487
                                related_name='pending_memebrships')
1488
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1489

    
1490
    acceptance_date     =   models.DateField(null=True, db_index=True)
1491
    leave_request_date  =   models.DateField(null=True)
1492

    
1493
    objects     =   ForUpdateManager()
1494

    
1495
    REQUESTED   =   0
1496
    PENDING     =   1
1497
    ACCEPTED    =   2
1498
    REMOVING    =   3
1499
    REMOVED     =   4
1500
    INACTIVE    =   5
1501

    
1502
    APPROVED_SET    =   [PENDING, ACCEPTED, INACTIVE]
1503

    
1504
    @classmethod
1505
    def query_approved(cls):
1506
        return (Q(state=cls.PENDING) |
1507
                Q(state=cls.ACCEPTED) |
1508
                Q(state=cls.INACTIVE))
1509

    
1510
    class Meta:
1511
        unique_together = ("person", "project")
1512
        #index_together = [["project", "state"]]
1513

    
1514
    def __str__(self):
1515
        return _("<'%s' membership in '%s'>") % (
1516
                self.person.username, self.project)
1517

    
1518
    __repr__ = __str__
1519

    
1520
    def __init__(self, *args, **kwargs):
1521
        self.state = self.REQUESTED
1522
        super(ProjectMembership, self).__init__(*args, **kwargs)
1523

    
1524
    def _set_history_item(self, reason, date=None):
1525
        if isinstance(reason, basestring):
1526
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1527

    
1528
        history_item = ProjectMembershipHistory(
1529
                            serial=self.id,
1530
                            person=self.person.uuid,
1531
                            project=self.project_id,
1532
                            date=date or datetime.now(),
1533
                            reason=reason)
1534
        history_item.save()
1535
        serial = history_item.id
1536

    
1537
    def accept(self):
1538
        state = self.state
1539
        if state != self.REQUESTED:
1540
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1541
            raise AssertionError(m)
1542

    
1543
        now = datetime.now()
1544
        self.acceptance_date = now
1545
        self._set_history_item(reason='ACCEPT', date=now)
1546
        self.state = (self.PENDING if self.project.is_active()
1547
                      else self.INACTIVE)
1548
        self.save()
1549

    
1550
    def remove(self):
1551
        state = self.state
1552
        if state not in [self.ACCEPTED, self.INACTIVE]:
1553
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1554
            raise AssertionError(m)
1555

    
1556
        self._set_history_item(reason='REMOVE')
1557
        self.state = self.REMOVING
1558
        self.save()
1559

    
1560
    def reject(self):
1561
        state = self.state
1562
        if state != self.REQUESTED:
1563
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1564
            raise AssertionError(m)
1565

    
1566
        # rejected requests don't need sync,
1567
        # because they were never effected
1568
        self._set_history_item(reason='REJECT')
1569
        self.delete()
1570

    
1571
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1572
        if sub_list is None:
1573
            sub_list = []
1574

    
1575
        if add_list is None:
1576
            add_list = []
1577

    
1578
        sub_append = sub_list.append
1579
        add_append = add_list.append
1580
        holder = self.person.uuid
1581

    
1582
        synced_application = self.application
1583
        if synced_application is not None:
1584
            cur_grants = synced_application.projectresourcegrant_set.all()
1585
            for grant in cur_grants:
1586
                sub_append(QuotaLimits(
1587
                               holder       = holder,
1588
                               resource     = str(grant.resource),
1589
                               capacity     = grant.member_capacity,
1590
                               import_limit = grant.member_import_limit,
1591
                               export_limit = grant.member_export_limit))
1592

    
1593
        if not remove:
1594
            new_grants = self.pending_application.projectresourcegrant_set.all()
1595
            for new_grant in new_grants:
1596
                add_append(QuotaLimits(
1597
                               holder       = holder,
1598
                               resource     = str(new_grant.resource),
1599
                               capacity     = new_grant.member_capacity,
1600
                               import_limit = new_grant.member_import_limit,
1601
                               export_limit = new_grant.member_export_limit))
1602

    
1603
        return (sub_list, add_list)
1604

    
1605
    def set_sync(self):
1606
        state = self.state
1607
        if state == self.PENDING:
1608
            pending_application = self.pending_application
1609
            if pending_application is None:
1610
                m = _("%s: attempt to sync an empty pending application") % (
1611
                    self,)
1612
                raise AssertionError(m)
1613
            self.application = pending_application
1614
            self.pending_application = None
1615
            self.pending_serial = None
1616

    
1617
            # project.application may have changed in the meantime,
1618
            # in which case we stay PENDING;
1619
            # we are safe to check due to select_for_update
1620
            if self.application == self.project.application:
1621
                self.state = self.ACCEPTED
1622
            self.save()
1623
        elif state == self.ACCEPTED:
1624
            if self.pending_application:
1625
                m = _("%s: attempt to sync in state '%s' "
1626
                      "with a pending application") % (self, state)
1627
                raise AssertionError(m)
1628
            self.application = None
1629
            self.pending_serial = None
1630
            self.state = self.INACTIVE
1631
            self.save()
1632
        elif state == self.REMOVING:
1633
            self.delete()
1634
        else:
1635
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1636
            raise AssertionError(m)
1637

    
1638
    def reset_sync(self):
1639
        state = self.state
1640
        if state in [self.PENDING, self.ACCEPTED, self.REMOVING]:
1641
            self.pending_application = None
1642
            self.pending_serial = None
1643
            self.save()
1644
        else:
1645
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1646
            raise AssertionError(m)
1647

    
1648
class Serial(models.Model):
1649
    serial  =   models.AutoField(primary_key=True)
1650

    
1651
def new_serial():
1652
    s = Serial.objects.create()
1653
    serial = s.serial
1654
    s.delete()
1655
    return serial
1656

    
1657
def sync_finish_serials(serials_to_ack=None):
1658
    if serials_to_ack is None:
1659
        serials_to_ack = qh_query_serials([])
1660

    
1661
    serials_to_ack = set(serials_to_ack)
1662
    sfu = ProjectMembership.objects.select_for_update()
1663
    memberships = list(sfu.filter(pending_serial__isnull=False))
1664

    
1665
    if memberships:
1666
        for membership in memberships:
1667
            serial = membership.pending_serial
1668
            if serial in serials_to_ack:
1669
                membership.set_sync()
1670
            else:
1671
                membership.reset_sync()
1672

    
1673
        transaction.commit()
1674

    
1675
    qh_ack_serials(list(serials_to_ack))
1676
    return len(memberships)
1677

    
1678
def sync_all_projects():
1679
    sync_finish_serials()
1680

    
1681
    PENDING = ProjectMembership.PENDING
1682
    REMOVING = ProjectMembership.REMOVING
1683
    objects = ProjectMembership.objects.select_for_update()
1684

    
1685
    sub_quota, add_quota = [], []
1686

    
1687
    serial = new_serial()
1688

    
1689
    pending = objects.filter(state=PENDING)
1690
    for membership in pending:
1691

    
1692
        if membership.pending_application:
1693
            m = "%s: impossible: pending_application is not None (%s)" % (
1694
                membership, membership.pending_application)
1695
            raise AssertionError(m)
1696
        if membership.pending_serial:
1697
            m = "%s: impossible: pending_serial is not None (%s)" % (
1698
                membership, membership.pending_serial)
1699
            raise AssertionError(m)
1700

    
1701
        membership.pending_application = membership.project.application
1702
        membership.pending_serial = serial
1703
        membership.get_diff_quotas(sub_quota, add_quota)
1704
        membership.save()
1705

    
1706
    removing = objects.filter(state=REMOVING)
1707
    for membership in removing:
1708

    
1709
        if membership.pending_application:
1710
            m = ("%s: impossible: removing pending_application is not None (%s)"
1711
                % (membership, membership.pending_application))
1712
            raise AssertionError(m)
1713
        if membership.pending_serial:
1714
            m = "%s: impossible: pending_serial is not None (%s)" % (
1715
                membership, membership.pending_serial)
1716
            raise AssertionError(m)
1717

    
1718
        membership.pending_serial = serial
1719
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1720
        membership.save()
1721

    
1722
    transaction.commit()
1723
    # ProjectApplication.approve() unblocks here
1724
    # and can set PENDING an already PENDING membership
1725
    # which has been scheduled to sync with the old project.application
1726
    # Need to check in ProjectMembership.set_sync()
1727

    
1728
    r = qh_add_quota(serial, sub_quota, add_quota)
1729
    if r:
1730
        m = "cannot sync serial: %d" % serial
1731
        raise RuntimeError(m)
1732

    
1733
    sync_finish_serials([serial])
1734

    
1735
def sync_deactivating_projects():
1736

    
1737
    ACCEPTED = ProjectMembership.ACCEPTED
1738
    PENDING = ProjectMembership.PENDING
1739
    REMOVING = ProjectMembership.REMOVING
1740

    
1741
    psfu = Project.objects.select_for_update()
1742
    projects = psfu.filter(deactivation_start_date__isnull=False)
1743

    
1744
    if not projects:
1745
        return
1746

    
1747
    sub_quota, add_quota = [], []
1748

    
1749
    serial = new_serial()
1750

    
1751
    for project in projects:
1752
        objects = project.projectmembership_set.select_for_update()
1753
        memberships = objects.filter(Q(state=ACCEPTED) |
1754
                                     Q(state=PENDING) | Q(state=REMOVING))
1755
        for membership in memberships:
1756
            if membership.state in (PENDING, REMOVING):
1757
                m = "cannot sync deactivating project '%s'" % project
1758
                raise RuntimeError(m)
1759

    
1760
            # state == ACCEPTED
1761
            if membership.pending_application:
1762
                m = "%s: impossible: pending_application is not None (%s)" % (
1763
                    membership, membership.pending_application)
1764
                raise AssertionError(m)
1765
            if membership.pending_serial:
1766
                m = "%s: impossible: pending_serial is not None (%s)" % (
1767
                    membership, membership.pending_serial)
1768
                raise AssertionError(m)
1769

    
1770
            membership.pending_serial = serial
1771
            membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1772
            membership.save()
1773

    
1774
    transaction.commit()
1775

    
1776
    r = qh_add_quota(serial, sub_quota, add_quota)
1777
    if r:
1778
        m = "cannot sync serial: %d" % serial
1779
        raise RuntimeError(m)
1780

    
1781
    sync_finish_serials([serial])
1782

    
1783
    # finalize deactivating projects
1784
    deactivating_projects = psfu.filter(deactivation_start_date__isnull=False)
1785
    for project in deactivating_projects:
1786
        objects = project.projectmembership_set.select_for_update()
1787
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1788
                                          Q(state=PENDING) | Q(state=REMOVING)))
1789
        if not memberships:
1790
            project.set_deactivation_date()
1791
            project.save()
1792

    
1793
    transaction.commit()
1794

    
1795
def sync_projects():
1796
    sync_all_projects()
1797
    sync_deactivating_projects()
1798

    
1799
def trigger_sync(retries=3, retry_wait=1.0):
1800
    transaction.commit()
1801

    
1802
    cursor = connection.cursor()
1803
    locked = True
1804
    try:
1805
        while 1:
1806
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1807
            r = cursor.fetchone()
1808
            if r is None:
1809
                m = "Impossible"
1810
                raise AssertionError(m)
1811
            locked = r[0]
1812
            if locked:
1813
                break
1814

    
1815
            retries -= 1
1816
            if retries <= 0:
1817
                return False
1818
            sleep(retry_wait)
1819

    
1820
        sync_projects()
1821
        return True
1822

    
1823
    finally:
1824
        if locked:
1825
            cursor.execute("SELECT pg_advisory_unlock(1)")
1826
            cursor.fetchall()
1827

    
1828

    
1829
class ProjectMembershipHistory(models.Model):
1830
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1831
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1832

    
1833
    person  =   models.CharField(max_length=255)
1834
    project =   models.BigIntegerField()
1835
    date    =   models.DateField(default=datetime.now)
1836
    reason  =   models.IntegerField()
1837
    serial  =   models.BigIntegerField()
1838

    
1839
### SIGNALS ###
1840
################
1841

    
1842
def create_astakos_user(u):
1843
    try:
1844
        AstakosUser.objects.get(user_ptr=u.pk)
1845
    except AstakosUser.DoesNotExist:
1846
        extended_user = AstakosUser(user_ptr_id=u.pk)
1847
        extended_user.__dict__.update(u.__dict__)
1848
        extended_user.save()
1849
        if not extended_user.has_auth_provider('local'):
1850
            extended_user.add_auth_provider('local')
1851
    except BaseException, e:
1852
        logger.exception(e)
1853

    
1854

    
1855
def fix_superusers(sender, **kwargs):
1856
    # Associate superusers with AstakosUser
1857
    admins = User.objects.filter(is_superuser=True)
1858
    for u in admins:
1859
        create_astakos_user(u)
1860
post_syncdb.connect(fix_superusers)
1861

    
1862

    
1863
def user_post_save(sender, instance, created, **kwargs):
1864
    if not created:
1865
        return
1866
    create_astakos_user(instance)
1867
post_save.connect(user_post_save, sender=User)
1868

    
1869
def astakosuser_post_save(sender, instance, created, **kwargs):
1870
    if not created:
1871
        return
1872
    # TODO handle socket.error & IOError
1873
    register_users((instance,))
1874
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1875

    
1876
def resource_post_save(sender, instance, created, **kwargs):
1877
    if not created:
1878
        return
1879
    register_resources((instance,))
1880
post_save.connect(resource_post_save, sender=Resource)
1881

    
1882
def renew_token(sender, instance, **kwargs):
1883
    if not instance.auth_token:
1884
        instance.renew_token()
1885
pre_save.connect(renew_token, sender=AstakosUser)
1886
pre_save.connect(renew_token, sender=Service)
1887