Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 213ba781

History | View | Annotate | Download (62 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
logger = logging.getLogger(__name__)
81

    
82
DEFAULT_CONTENT_TYPE = None
83
_content_type = None
84

    
85
def get_content_type():
86
    global _content_type
87
    if _content_type is not None:
88
        return _content_type
89

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

    
97
RESOURCE_SEPARATOR = '.'
98

    
99
inf = float('inf')
100

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

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

    
117
        self.auth_token = b64encode(md5.digest())
118
        self.auth_token_created = datetime.now()
119
        if expiration_date:
120
            self.auth_token_expires = expiration_date
121
        else:
122
            self.auth_token_expires = None
123

    
124
    def __str__(self):
125
        return self.name
126

    
127
    @property
128
    def resources(self):
129
        return self.resource_set.all()
130

    
131
    @resources.setter
132
    def resources(self, resources):
133
        for s in resources:
134
            self.resource_set.create(**s)
135

    
136
    def add_resource(self, service, resource, uplimit, update=True):
137
        """Raises ObjectDoesNotExist, IntegrityError"""
138
        resource = Resource.objects.get(service__name=service, name=resource)
139
        if update:
140
            AstakosUserQuota.objects.update_or_create(user=self,
141
                                                      resource=resource,
142
                                                      defaults={'uplimit': uplimit})
143
        else:
144
            q = self.astakosuserquota_set
145
            q.create(resource=resource, uplimit=uplimit)
146

    
147

    
148
class ResourceMetadata(models.Model):
149
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
150
    value = models.CharField(_('Value'), max_length=255)
151

    
152
_presentation_data = {}
153
def get_presentation(resource):
154
    global _presentation_data
155
    presentation = _presentation_data.get(resource, {})
156
    if not presentation:
157
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
158
        presentation = resource_presentation.get(resource, {})
159
        _presentation_data[resource] = presentation
160
    return presentation 
161

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

    
170
    class Meta:
171
        unique_together = ("name", "service")
172

    
173
    def __str__(self):
174
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
175

    
176
    @property
177
    def help_text(self):
178
        return get_presentation(str(self)).get('help_text', '')
179
    
180
    @property
181
    def help_text_input_each(self):
182
        return get_presentation(str(self)).get('help_text_input_each', '')
183

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

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

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

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

    
200

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

    
215
class AstakosUserManager(UserManager):
216

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

    
226
    def get_by_email(self, email):
227
        return self.get(email=email)
228

    
229
    def get_by_identifier(self, email_or_username, **kwargs):
230
        try:
231
            return self.get(email__iexact=email_or_username, **kwargs)
232
        except AstakosUser.DoesNotExist:
233
            return self.get(username__iexact=email_or_username, **kwargs)
234

    
235
    def user_exists(self, email_or_username, **kwargs):
236
        qemail = Q(email__iexact=email_or_username)
237
        qusername = Q(username__iexact=email_or_username)
238
        return self.filter(qemail | qusername).exists()
239

    
240

    
241
class AstakosUser(User):
242
    """
243
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
244
    """
245
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
246
                                   null=True)
247

    
248
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
249
    #                    AstakosUserProvider model.
250
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
251
                                null=True)
252
    # ex. screen_name for twitter, eppn for shibboleth
253
    third_party_identifier = models.CharField(_('Third-party identifier'),
254
                                              max_length=255, null=True,
255
                                              blank=True)
256

    
257

    
258
    #for invitations
259
    user_level = DEFAULT_USER_LEVEL
260
    level = models.IntegerField(_('Inviter level'), default=user_level)
261
    invitations = models.IntegerField(
262
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
263

    
264
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
265
                                  null=True, blank=True)
266
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
267
    auth_token_expires = models.DateTimeField(
268
        _('Token expiration date'), null=True)
269

    
270
    updated = models.DateTimeField(_('Update date'))
271
    is_verified = models.BooleanField(_('Is verified?'), default=False)
272

    
273
    email_verified = models.BooleanField(_('Email verified?'), default=False)
274

    
275
    has_credits = models.BooleanField(_('Has credits?'), default=False)
276
    has_signed_terms = models.BooleanField(
277
        _('I agree with the terms'), default=False)
278
    date_signed_terms = models.DateTimeField(
279
        _('Signed terms date'), null=True, blank=True)
280

    
281
    activation_sent = models.DateTimeField(
282
        _('Activation sent data'), null=True, blank=True)
283

    
284
    policy = models.ManyToManyField(
285
        Resource, null=True, through='AstakosUserQuota')
286

    
287
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
288

    
289
    __has_signed_terms = False
290
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
291
                                           default=False, db_index=True)
292

    
293
    objects = AstakosUserManager()
294

    
295
    def __init__(self, *args, **kwargs):
296
        super(AstakosUser, self).__init__(*args, **kwargs)
297
        self.__has_signed_terms = self.has_signed_terms
298
        if not self.id:
299
            self.is_active = False
300

    
301
    @property
302
    def realname(self):
303
        return '%s %s' % (self.first_name, self.last_name)
304

    
305
    @realname.setter
306
    def realname(self, value):
307
        parts = value.split(' ')
308
        if len(parts) == 2:
309
            self.first_name = parts[0]
310
            self.last_name = parts[1]
311
        else:
312
            self.last_name = parts[0]
313

    
314
    def add_permission(self, pname):
315
        if self.has_perm(pname):
316
            return
317
        p, created = Permission.objects.get_or_create(
318
                                    codename=pname,
319
                                    name=pname.capitalize(),
320
                                    content_type=get_content_type())
321
        self.user_permissions.add(p)
322

    
323
    def remove_permission(self, pname):
324
        if self.has_perm(pname):
325
            return
326
        p = Permission.objects.get(codename=pname,
327
                                   content_type=get_content_type())
328
        self.user_permissions.remove(p)
329

    
330
    @property
331
    def invitation(self):
332
        try:
333
            return Invitation.objects.get(username=self.email)
334
        except Invitation.DoesNotExist:
335
            return None
336

    
337
    @property
338
    def quota(self):
339
        """Returns a dict with the sum of quota limits per resource"""
340
        d = defaultdict(int)
341
        default_quota = get_default_quota()
342
        d.update(default_quota)
343
        for q in self.policies:
344
            d[q.resource] += q.uplimit or inf
345
        for m in self.projectmembership_set.select_related().all():
346
            if not m.acceptance_date:
347
                continue
348
            p = m.project
349
            if not p.is_active:
350
                continue
351
            grants = p.application.projectresourcegrant_set.all()
352
            for g in grants:
353
                d[str(g.resource)] += g.member_capacity or inf
354
        return d
355

    
356
    @property
357
    def policies(self):
358
        return self.astakosuserquota_set.select_related().all()
359

    
360
    @policies.setter
361
    def policies(self, policies):
362
        for p in policies:
363
            service = policies.get('service', None)
364
            resource = policies.get('resource', None)
365
            uplimit = policies.get('uplimit', 0)
366
            update = policies.get('update', True)
367
            self.add_policy(service, resource, uplimit, update)
368

    
369
    def add_policy(self, service, resource, uplimit, update=True):
370
        """Raises ObjectDoesNotExist, IntegrityError"""
371
        resource = Resource.objects.get(service__name=service, name=resource)
372
        if update:
373
            AstakosUserQuota.objects.update_or_create(user=self,
374
                                                      resource=resource,
375
                                                      defaults={'uplimit': uplimit})
376
        else:
377
            q = self.astakosuserquota_set
378
            q.create(resource=resource, uplimit=uplimit)
379

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

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

    
394
    @property
395
    def extended_groups(self):
396
        return self.membership_set.select_related().all()
397

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

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

    
408
        self.update_uuid()
409

    
410
        if self.username != self.email.lower():
411
            # set username
412
            self.username = self.email.lower()
413

    
414
        self.validate_unique_email_isactive()
415

    
416
        super(AstakosUser, self).save(**kwargs)
417

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

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

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

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

    
448
    def __unicode__(self):
449
        return '%s (%s)' % (self.realname, self.email)
450

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

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

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

    
474
    def email_change_is_pending(self):
475
        return self.emailchanges.count() > 0
476

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

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

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

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

    
510
        if not provider_settings.is_available_for_add():
511
            return False
512

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

    
517
        if 'provider_info' in kwargs:
518
            kwargs.pop('provider_info')
519

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

    
530
        return True
531

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

    
537
        if len(existing) <= 1:
538
            return False
539

    
540
        if len(existing_for_provider) == 1 and provider.is_required():
541
            return False
542

    
543
        return True
544

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

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

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

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

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

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

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

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

    
589
        pending.delete()
590
        return provider
591

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

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

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

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

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

    
615
    def get_auth_providers(self):
616
        return self.auth_providers.all()
617

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

    
627
        return providers
628

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

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

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

    
665
        return mark_safe(message + u' '+ msg_extra)
666

    
667

    
668
class AstakosUserAuthProviderManager(models.Manager):
669

    
670
    def active(self, **filters):
671
        return self.filter(active=True, **filters)
672

    
673

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

    
692
    objects = AstakosUserAuthProviderManager()
693

    
694
    class Meta:
695
        unique_together = (('identifier', 'module', 'user'), )
696
        ordering = ('module', 'created')
697

    
698
    def __init__(self, *args, **kwargs):
699
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
700
        try:
701
            self.info = json.loads(self.info_data)
702
            if not self.info:
703
                self.info = {}
704
        except Exception, e:
705
            self.info = {}
706

    
707
        for key,value in self.info.iteritems():
708
            setattr(self, 'info_%s' % key, value)
709

    
710

    
711
    @property
712
    def settings(self):
713
        return auth_providers.get_provider(self.module)
714

    
715
    @property
716
    def details_display(self):
717
        try:
718
          return self.settings.get_details_tpl_display % self.__dict__
719
        except:
720
          return ''
721

    
722
    @property
723
    def title_display(self):
724
        title_tpl = self.settings.get_title_display
725
        try:
726
            if self.settings.get_user_title_display:
727
                title_tpl = self.settings.get_user_title_display
728
        except Exception, e:
729
            pass
730
        try:
731
          return title_tpl % self.__dict__
732
        except:
733
          return self.settings.get_title_display % self.__dict__
734

    
735
    def can_remove(self):
736
        return self.user.can_remove_auth_provider(self.module)
737

    
738
    def delete(self, *args, **kwargs):
739
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
740
        if self.module == 'local':
741
            self.user.set_unusable_password()
742
            self.user.save()
743
        return ret
744

    
745
    def __repr__(self):
746
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
747

    
748
    def __unicode__(self):
749
        if self.identifier:
750
            return "%s:%s" % (self.module, self.identifier)
751
        if self.auth_backend:
752
            return "%s:%s" % (self.module, self.auth_backend)
753
        return self.module
754

    
755
    def save(self, *args, **kwargs):
756
        self.info_data = json.dumps(self.info)
757
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
758

    
759

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

    
787
    update_or_create = _update_or_create
788

    
789

    
790
class AstakosUserQuota(models.Model):
791
    objects = ExtendedManager()
792
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
793
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
794
    resource = models.ForeignKey(Resource)
795
    user = models.ForeignKey(AstakosUser)
796

    
797
    class Meta:
798
        unique_together = ("resource", "user")
799

    
800

    
801
class ApprovalTerms(models.Model):
802
    """
803
    Model for approval terms
804
    """
805

    
806
    date = models.DateTimeField(
807
        _('Issue date'), db_index=True, default=datetime.now())
808
    location = models.CharField(_('Terms location'), max_length=255)
809

    
810

    
811
class Invitation(models.Model):
812
    """
813
    Model for registring invitations
814
    """
815
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
816
                                null=True)
817
    realname = models.CharField(_('Real name'), max_length=255)
818
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
819
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
820
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
821
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
822
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
823

    
824
    def __init__(self, *args, **kwargs):
825
        super(Invitation, self).__init__(*args, **kwargs)
826
        if not self.id:
827
            self.code = _generate_invitation_code()
828

    
829
    def consume(self):
830
        self.is_consumed = True
831
        self.consumed = datetime.now()
832
        self.save()
833

    
834
    def __unicode__(self):
835
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
836

    
837

    
838
class EmailChangeManager(models.Manager):
839

    
840
    @transaction.commit_on_success
841
    def change_email(self, activation_key):
842
        """
843
        Validate an activation key and change the corresponding
844
        ``User`` if valid.
845

846
        If the key is valid and has not expired, return the ``User``
847
        after activating.
848

849
        If the key is not valid or has expired, return ``None``.
850

851
        If the key is valid but the ``User`` is already active,
852
        return ``None``.
853

854
        After successful email change the activation record is deleted.
855

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

    
884

    
885
class EmailChange(models.Model):
886
    new_email_address = models.EmailField(
887
        _(u'new e-mail address'),
888
        help_text=_('Your old email address will be used until you verify your new one.'))
889
    user = models.ForeignKey(
890
        AstakosUser, unique=True, related_name='emailchanges')
891
    requested_at = models.DateTimeField(default=datetime.now())
892
    activation_key = models.CharField(
893
        max_length=40, unique=True, db_index=True)
894

    
895
    objects = EmailChangeManager()
896

    
897
    def get_url(self):
898
        return reverse('email_change_confirm',
899
                      kwargs={'activation_key': self.activation_key})
900

    
901
    def activation_key_expired(self):
902
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
903
        return self.requested_at + expiration_date < datetime.now()
904

    
905

    
906
class AdditionalMail(models.Model):
907
    """
908
    Model for registring invitations
909
    """
910
    owner = models.ForeignKey(AstakosUser)
911
    email = models.EmailField()
912

    
913

    
914
def _generate_invitation_code():
915
    while True:
916
        code = randint(1, 2L ** 63 - 1)
917
        try:
918
            Invitation.objects.get(code=code)
919
            # An invitation with this code already exists, try again
920
        except Invitation.DoesNotExist:
921
            return code
922

    
923

    
924
def get_latest_terms():
925
    try:
926
        term = ApprovalTerms.objects.order_by('-id')[0]
927
        return term
928
    except IndexError:
929
        pass
930
    return None
931

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

    
947
    class Meta:
948
        unique_together = ("provider", "third_party_identifier")
949

    
950
    def get_user_instance(self):
951
        d = self.__dict__
952
        d.pop('_state', None)
953
        d.pop('id', None)
954
        d.pop('token', None)
955
        d.pop('created', None)
956
        d.pop('info', None)
957
        user = AstakosUser(**d)
958

    
959
        return user
960

    
961
    @property
962
    def realname(self):
963
        return '%s %s' %(self.first_name, self.last_name)
964

    
965
    @realname.setter
966
    def realname(self, value):
967
        parts = value.split(' ')
968
        if len(parts) == 2:
969
            self.first_name = parts[0]
970
            self.last_name = parts[1]
971
        else:
972
            self.last_name = parts[0]
973

    
974
    def save(self, **kwargs):
975
        if not self.id:
976
            # set username
977
            while not self.username:
978
                username =  uuid.uuid4().hex[:30]
979
                try:
980
                    AstakosUser.objects.get(username = username)
981
                except AstakosUser.DoesNotExist, e:
982
                    self.username = username
983
        super(PendingThirdPartyUser, self).save(**kwargs)
984

    
985
    def generate_token(self):
986
        self.password = self.third_party_identifier
987
        self.last_login = datetime.now()
988
        self.token = default_token_generator.make_token(self)
989

    
990
class SessionCatalog(models.Model):
991
    session_key = models.CharField(_('session key'), max_length=40)
992
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
993

    
994

    
995
### PROJECTS ###
996
################
997

    
998
class MemberJoinPolicy(models.Model):
999
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1000
    description = models.CharField(_('Description'), max_length=80)
1001

    
1002
    def __str__(self):
1003
        return self.description.capitalize()
1004

    
1005
class MemberLeavePolicy(models.Model):
1006
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1007
    description = models.CharField(_('Description'), max_length=80)
1008

    
1009
    def __str__(self):
1010
        return self.description.capitalize()
1011

    
1012
def synced_model_metaclass(class_name, class_parents, class_attributes):
1013

    
1014
    new_attributes = {}
1015
    sync_attributes = {}
1016

    
1017
    for name, value in class_attributes.iteritems():
1018
        sync, underscore, rest = name.partition('_')
1019
        if sync == 'sync' and underscore == '_':
1020
            sync_attributes[rest] = value
1021
        else:
1022
            new_attributes[name] = value
1023

    
1024
    if 'prefix' not in sync_attributes:
1025
        m = ("you did not specify a 'sync_prefix' attribute "
1026
             "in class '%s'" % (class_name,))
1027
        raise ValueError(m)
1028

    
1029
    prefix = sync_attributes.pop('prefix')
1030
    class_name = sync_attributes.pop('classname', prefix + '_model')
1031

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

    
1040
        new_attributes[newname] = value
1041

    
1042
    newclass = type(class_name, class_parents, new_attributes)
1043
    return newclass
1044

    
1045

    
1046
def make_synced(prefix='sync', name='SyncedState'):
1047

    
1048
    the_name = name
1049
    the_prefix = prefix
1050

    
1051
    class SyncedState(models.Model):
1052

    
1053
        sync_classname      = the_name
1054
        sync_prefix         = the_prefix
1055
        __metaclass__       = synced_model_metaclass
1056

    
1057
        sync_new_state      = models.BigIntegerField(null=True)
1058
        sync_synced_state   = models.BigIntegerField(null=True)
1059
        STATUS_SYNCED       = 0
1060
        STATUS_PENDING      = 1
1061
        sync_status         = models.IntegerField(db_index=True)
1062

    
1063
        class Meta:
1064
            abstract = True
1065

    
1066
        class NotSynced(Exception):
1067
            pass
1068

    
1069
        def sync_init_state(self, state):
1070
            self.sync_synced_state = state
1071
            self.sync_new_state = state
1072
            self.sync_status = self.STATUS_SYNCED
1073

    
1074
        def sync_get_status(self):
1075
            return self.sync_status
1076

    
1077
        def sync_set_status(self):
1078
            if self.sync_new_state != self.sync_synced_state:
1079
                self.sync_status = self.STATUS_PENDING
1080
            else:
1081
                self.sync_status = self.STATUS_SYNCED
1082

    
1083
        def sync_set_synced(self):
1084
            self.sync_synced_state = self.sync_new_state
1085
            self.sync_status = self.STATUS_SYNCED
1086

    
1087
        def sync_get_synced_state(self):
1088
            return self.sync_synced_state
1089

    
1090
        def sync_set_new_state(self, new_state):
1091
            self.sync_new_state = new_state
1092
            self.sync_set_status()
1093

    
1094
        def sync_get_new_state(self):
1095
            return self.sync_new_state
1096

    
1097
        def sync_set_synced_state(self, synced_state):
1098
            self.sync_synced_state = synced_state
1099
            self.sync_set_status()
1100

    
1101
        def sync_get_pending_objects(self):
1102
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1103
            return self.objects.filter(**kw)
1104

    
1105
        def sync_get_synced_objects(self):
1106
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1107
            return self.objects.filter(**kw)
1108

    
1109
        def sync_verify_get_synced_state(self):
1110
            status = self.sync_get_status()
1111
            state = self.sync_get_synced_state()
1112
            verified = (status == self.STATUS_SYNCED)
1113
            return state, verified
1114

    
1115
        def sync_is_synced(self):
1116
            state, verified = self.sync_verify_get_synced_state()
1117
            return verified
1118

    
1119
    return SyncedState
1120

    
1121
SyncedState = make_synced(prefix='sync', name='SyncedState')
1122

    
1123

    
1124
class ProjectApplication(models.Model):
1125
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1126
    applicant               =   models.ForeignKey(
1127
                                    AstakosUser,
1128
                                    related_name='projects_applied',
1129
                                    db_index=True)
1130

    
1131
    state                   =   models.CharField(max_length=80,
1132
                                                default=UNKNOWN)
1133

    
1134
    owner                   =   models.ForeignKey(
1135
                                    AstakosUser,
1136
                                    related_name='projects_owned',
1137
                                    db_index=True)
1138

    
1139
    precursor_application   =   models.OneToOneField('ProjectApplication',
1140
                                                     null=True,
1141
                                                     blank=True,
1142
                                                     db_index=True)
1143

    
1144
    name                    =   models.CharField(max_length=80, help_text="The Project's name should be in a domain format. The domain shouldn't neccessarily exist in the real world but is helpful to imply a structure. e.g.: myproject.mylab.ntua.gr or myservice.myteam.myorganization ",)
1145
    homepage                =   models.URLField(max_length=255, null=True,
1146
                                                blank=True,help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",)
1147
    description             =   models.TextField(null=True, blank=True,help_text= "Please provide a short but descriptive abstract of your Project, so that anyone searching can quickly understand what this Project is about. ")
1148
    start_date              =   models.DateTimeField(help_text= "Here you specify the date you want your Project to start granting its resources. Its members will get the resources coming from this Project on this exact date.")
1149
    end_date                =   models.DateTimeField(help_text= "Here you specify the date you want your Project to cease. This means that after this date all members will no longer be able to allocate resources from this Project.  ")
1150
    member_join_policy      =   models.ForeignKey(MemberJoinPolicy)
1151
    member_leave_policy     =   models.ForeignKey(MemberLeavePolicy)
1152
    limit_on_members_number =   models.PositiveIntegerField(null=True,
1153
                                                            blank=True,help_text= "Here you specify the number of members this Project is going to have. This means that this number of people will be granted the resources you will specify in the next step. This can be '1' if you are the only one wanting to get resources. ")
1154
    resource_grants         =   models.ManyToManyField(
1155
                                    Resource,
1156
                                    null=True,
1157
                                    blank=True,
1158
                                    through='ProjectResourceGrant')
1159
    comments                =   models.TextField(null=True, blank=True)
1160
    issue_date              =   models.DateTimeField()
1161

    
1162
    objects     =   ForUpdateManager()
1163

    
1164
    def add_resource_policy(self, service, resource, uplimit):
1165
        """Raises ObjectDoesNotExist, IntegrityError"""
1166
        q = self.projectresourcegrant_set
1167
        resource = Resource.objects.get(service__name=service, name=resource)
1168
        q.create(resource=resource, member_capacity=uplimit)
1169

    
1170
    
1171
    @property
1172
    def grants(self):
1173
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1174
            
1175
    @property
1176
    def resource_policies(self):
1177
        return self.projectresourcegrant_set.all()
1178

    
1179
    @resource_policies.setter
1180
    def resource_policies(self, policies):
1181
        for p in policies:
1182
            service = p.get('service', None)
1183
            resource = p.get('resource', None)
1184
            uplimit = p.get('uplimit', 0)
1185
            self.add_resource_policy(service, resource, uplimit)
1186

    
1187
    @property
1188
    def follower(self):
1189
        try:
1190
            return ProjectApplication.objects.get(precursor_application=self)
1191
        except ProjectApplication.DoesNotExist:
1192
            return
1193

    
1194
    def submit(self, resource_policies, applicant, comments,
1195
               precursor_application=None):
1196

    
1197
        if precursor_application:
1198
            self.precursor_application = precursor_application
1199
            self.owner = precursor_application.owner
1200
        else:
1201
            self.owner = applicant
1202

    
1203
        self.id = None
1204
        self.applicant = applicant
1205
        self.comments = comments
1206
        self.issue_date = datetime.now()
1207
        self.state = self.PENDING
1208
        self.save()
1209
        self.resource_policies = resource_policies
1210

    
1211
    def _get_project(self):
1212
        precursor = self
1213
        while precursor:
1214
            try:
1215
                objects = Project.objects.select_for_update()
1216
                project = objects.get(application=precursor)
1217
                return project
1218
            except Project.DoesNotExist:
1219
                pass
1220
            precursor = precursor.precursor_application
1221

    
1222
        return None
1223

    
1224
    def approve(self, approval_user=None):
1225
        """
1226
        If approval_user then during owner membership acceptance
1227
        it is checked whether the request_user is eligible.
1228

1229
        Raises:
1230
            PermissionDenied
1231
        """
1232

    
1233
        if not transaction.is_managed():
1234
            raise AssertionError("NOPE")
1235

    
1236
        new_project_name = self.name
1237
        if self.state != self.PENDING:
1238
            m = _("cannot approve: project '%s' in state '%s'") % (
1239
                    new_project_name, self.state)
1240
            raise PermissionDenied(m) # invalid argument
1241

    
1242
        now = datetime.now()
1243
        project = self._get_project()
1244

    
1245
        try:
1246
            # needs SERIALIZABLE
1247
            conflicting_project = Project.objects.get(name=new_project_name)
1248
            if (conflicting_project.is_alive and
1249
                conflicting_project != project):
1250
                m = (_("cannot approve: project with name '%s' "
1251
                       "already exists (serial: %s)") % (
1252
                        new_project_name, conflicting_project.id))
1253
                raise PermissionDenied(m) # invalid argument
1254
        except Project.DoesNotExist:
1255
            pass
1256

    
1257
        new_project = False
1258
        if project is None:
1259
            new_project = True
1260
            project = Project(creation_date=now)
1261

    
1262
        project.name = new_project_name
1263
        project.application = self
1264
        project.last_approval_date = now
1265
        project.save()
1266

    
1267
        if new_project:
1268
            project.add_member(self.owner)
1269

    
1270
        # This will block while syncing,
1271
        # but unblock before setting the membership state.
1272
        # See ProjectMembership.set_sync()
1273
        project.set_membership_pending_sync()
1274

    
1275
        precursor = self.precursor_application
1276
        while precursor:
1277
            precursor.state = self.REPLACED
1278
            precursor.save()
1279
            precursor = precursor.precursor_application
1280

    
1281
        self.state = self.APPROVED
1282
        self.save()
1283

    
1284

    
1285
class ProjectResourceGrant(models.Model):
1286

    
1287
    resource                =   models.ForeignKey(Resource)
1288
    project_application     =   models.ForeignKey(ProjectApplication,
1289
                                                  null=True)
1290
    project_capacity        =   models.BigIntegerField(null=True)
1291
    project_import_limit    =   models.BigIntegerField(null=True)
1292
    project_export_limit    =   models.BigIntegerField(null=True)
1293
    member_capacity         =   models.BigIntegerField(null=True)
1294
    member_import_limit     =   models.BigIntegerField(null=True)
1295
    member_export_limit     =   models.BigIntegerField(null=True)
1296

    
1297
    objects = ExtendedManager()
1298

    
1299
    class Meta:
1300
        unique_together = ("resource", "project_application")
1301

    
1302

    
1303
class Project(models.Model):
1304

    
1305
    application                 =   models.OneToOneField(
1306
                                            ProjectApplication,
1307
                                            related_name='project')
1308
    last_approval_date          =   models.DateTimeField(null=True)
1309

    
1310
    members                     =   models.ManyToManyField(
1311
                                            AstakosUser,
1312
                                            through='ProjectMembership')
1313

    
1314
    termination_start_date      =   models.DateTimeField(null=True)
1315
    termination_date            =   models.DateTimeField(null=True)
1316

    
1317
    creation_date               =   models.DateTimeField()
1318
    name                        =   models.CharField(
1319
                                            max_length=80,
1320
                                            db_index=True,
1321
                                            unique=True)
1322

    
1323
    objects     =   ForUpdateManager()
1324

    
1325
    @property
1326
    def violated_resource_grants(self):
1327
        return False
1328

    
1329
    def violates_members_limit(self, adding=0):
1330
        application = self.application
1331
        return (len(self.approved_members) + adding >
1332
                application.limit_on_members_number)
1333

    
1334
    @property
1335
    def is_terminated(self):
1336
        return bool(self.termination_date)
1337

    
1338
    @property
1339
    def is_still_approved(self):
1340
        return bool(self.last_approval_date)
1341

    
1342
    @property
1343
    def is_active(self):
1344
        if (self.is_terminated or
1345
            not self.is_still_approved or
1346
            self.violated_resource_grants):
1347
            return False
1348
#         if self.violated_members_number_limit:
1349
#             return False
1350
        return True
1351

    
1352
    @property
1353
    def is_suspended(self):
1354
        if (self.is_terminated or
1355
            self.is_still_approved or
1356
            not self.violated_resource_grants):
1357
            return False
1358
#             if not self.violated_members_number_limit:
1359
#                 return False
1360
        return True
1361

    
1362
    @property
1363
    def is_alive(self):
1364
        return self.is_active or self.is_suspended
1365

    
1366
    @property
1367
    def is_inconsistent(self):
1368
        now = datetime.now()
1369
        if self.creation_date > now:
1370
            return True
1371
        if self.last_approval_date > now:
1372
            return True
1373
        if self.terminaton_date > now:
1374
            return True
1375
        return False
1376

    
1377
    @property
1378
    def approved_memberships(self):
1379
        ACCEPTED = ProjectMembership.ACCEPTED
1380
        PENDING  = ProjectMembership.PENDING
1381
        return self.projectmembership_set.filter(
1382
            Q(state=ACCEPTED) | Q(state=PENDING))
1383

    
1384
    @property
1385
    def approved_members(self):
1386
        return [m.person for m in self.approved_memberships]
1387

    
1388
    def set_membership_pending_sync(self):
1389
        ACCEPTED = ProjectMembership.ACCEPTED
1390
        PENDING  = ProjectMembership.PENDING
1391
        sfu = self.projectmembership_set.select_for_update()
1392
        members = sfu.filter(Q(state=ACCEPTED) | Q(state=PENDING))
1393

    
1394
        for member in members:
1395
            member.state = member.PENDING
1396
            member.save()
1397

    
1398
    def add_member(self, user):
1399
        """
1400
        Raises:
1401
            django.exceptions.PermissionDenied
1402
            astakos.im.models.AstakosUser.DoesNotExist
1403
        """
1404
        if isinstance(user, int):
1405
            user = AstakosUser.objects.get(user=user)
1406

    
1407
        m, created = ProjectMembership.objects.get_or_create(
1408
            person=user, project=self
1409
        )
1410
        m.accept()
1411

    
1412
    def remove_member(self, user):
1413
        """
1414
        Raises:
1415
            django.exceptions.PermissionDenied
1416
            astakos.im.models.AstakosUser.DoesNotExist
1417
            astakos.im.models.ProjectMembership.DoesNotExist
1418
        """
1419
        if isinstance(user, int):
1420
            user = AstakosUser.objects.get(user=user)
1421

    
1422
        m = ProjectMembership.objects.get(person=user, project=self)
1423
        m.remove()
1424

    
1425
    def set_termination_start_date(self):
1426
        self.termination_start_date = datetime.now()
1427
        self.terminaton_date = None
1428
        self.save()
1429

    
1430
    def set_termination_date(self):
1431
        self.termination_start_date = None
1432
        self.termination_date = datetime.now()
1433
        self.save()
1434

    
1435

    
1436
class ProjectMembership(models.Model):
1437

    
1438
    person              =   models.ForeignKey(AstakosUser)
1439
    request_date        =   models.DateField(default=datetime.now())
1440
    project             =   models.ForeignKey(Project)
1441

    
1442
    state               =   models.IntegerField(default=0)
1443
    application         =   models.ForeignKey(
1444
                                ProjectApplication,
1445
                                null=True,
1446
                                related_name='memberships')
1447
    pending_application =   models.ForeignKey(
1448
                                ProjectApplication,
1449
                                null=True,
1450
                                related_name='pending_memebrships')
1451
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1452

    
1453
    acceptance_date     =   models.DateField(null=True, db_index=True)
1454
    leave_request_date  =   models.DateField(null=True)
1455

    
1456
    objects     =   ForUpdateManager()
1457

    
1458
    REQUESTED   =   0
1459
    PENDING     =   1
1460
    ACCEPTED    =   2
1461
    REMOVING    =   3
1462
    REMOVED     =   4
1463

    
1464
    class Meta:
1465
        unique_together = ("person", "project")
1466
        #index_together = [["project", "state"]]
1467

    
1468
    def __str__(self):
1469
        return _("<'%s' membership in project '%s'>") % (
1470
                self.person.username, self.project.application)
1471

    
1472
    __repr__ = __str__
1473

    
1474
    def __init__(self, *args, **kwargs):
1475
        self.state = self.REQUESTED
1476
        super(ProjectMembership, self).__init__(*args, **kwargs)
1477

    
1478
    def _set_history_item(self, reason, date=None):
1479
        if isinstance(reason, basestring):
1480
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1481

    
1482
        history_item = ProjectMembershipHistory(
1483
                            serial=self.id,
1484
                            person=self.person.uuid,
1485
                            project=self.project_id,
1486
                            date=date or datetime.now(),
1487
                            reason=reason)
1488
        history_item.save()
1489
        serial = history_item.id
1490

    
1491
    def accept(self):
1492
        state = self.state
1493
        if state != self.REQUESTED:
1494
            m = _("%s: attempt to accept in state [%s]") % (self, state)
1495
            raise AssertionError(m)
1496

    
1497
        now = datetime.now()
1498
        self.acceptance_date = now
1499
        self._set_history_item(reason='ACCEPT', date=now)
1500
        self.state = self.PENDING
1501
        self.save()
1502

    
1503
    def remove(self):
1504
        state = self.state
1505
        if state != self.ACCEPTED:
1506
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1507
            raise AssertionError(m)
1508

    
1509
        self._set_history_item(reason='REMOVE')
1510
        self.state = self.REMOVING
1511
        self.save()
1512

    
1513
    def reject(self):
1514
        state = self.state
1515
        if state != self.REQUESTED:
1516
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1517
            raise AssertionError(m)
1518

    
1519
        # rejected requests don't need sync,
1520
        # because they were never effected
1521
        self._set_history_item(reason='REJECT')
1522
        self.delete()
1523

    
1524
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1525
        if sub_list is None:
1526
            sub_list = []
1527

    
1528
        if add_list is None:
1529
            add_list = []
1530

    
1531
        sub_append = sub_list.append
1532
        add_append = add_list.append
1533
        holder = self.person.uuid
1534

    
1535
        synced_application = self.application
1536
        if synced_application is not None:
1537
            cur_grants = synced_application.projectresourcegrant_set.all()
1538
            for grant in cur_grants:
1539
                sub_append(QuotaLimits(
1540
                               holder       = holder,
1541
                               resource     = str(grant.resource),
1542
                               capacity     = grant.member_capacity,
1543
                               import_limit = grant.member_import_limit,
1544
                               export_limit = grant.member_export_limit))
1545

    
1546
        if not remove:
1547
            new_grants = self.pending_application.projectresourcegrant_set.all()
1548
            for new_grant in new_grants:
1549
                add_append(QuotaLimits(
1550
                               holder       = holder,
1551
                               resource     = str(new_grant.resource),
1552
                               capacity     = new_grant.member_capacity,
1553
                               import_limit = new_grant.member_import_limit,
1554
                               export_limit = new_grant.member_export_limit))
1555

    
1556
        return (sub_list, add_list)
1557

    
1558
    def set_sync(self):
1559
        state = self.state
1560
        if state == self.PENDING:
1561
            pending_application = self.pending_application
1562
            if pending_application is None:
1563
                m = _("%s: attempt to sync an empty pending application") % (
1564
                    self, state)
1565
                raise AssertionError(m)
1566
            self.application = pending_application
1567
            self.pending_application = None
1568
            self.pending_serial = None
1569

    
1570
            # project.application may have changed in the meantime,
1571
            # in which case we stay PENDING;
1572
            # we are safe to check due to select_for_update
1573
            if self.application == self.project.application:
1574
                self.state = self.ACCEPTED
1575
            self.save()
1576
        elif state == self.REMOVING:
1577
            self.delete()
1578
        else:
1579
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1580
            raise AssertionError(m)
1581

    
1582
    def reset_sync(self):
1583
        state = self.state
1584
        if state in [self.PENDING, self.REMOVING]:
1585
            self.pending_application = None
1586
            self.pending_serial = None
1587
            self.save()
1588
        else:
1589
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1590
            raise AssertionError(m)
1591

    
1592
class Serial(models.Model):
1593
    serial  =   models.AutoField(primary_key=True)
1594

    
1595
def new_serial():
1596
    s = Serial.objects.create()
1597
    serial = s.serial
1598
    s.delete()
1599
    return serial
1600

    
1601
def sync_finish_serials(serials_to_ack=None):
1602
    if serials_to_ack is None:
1603
        serials_to_ack = qh_query_serials([])
1604

    
1605
    serials_to_ack = set(serials_to_ack)
1606
    sfu = ProjectMembership.objects.select_for_update()
1607
    memberships = list(sfu.filter(pending_serial__isnull=False))
1608

    
1609
    if memberships:
1610
        for membership in memberships:
1611
            serial = membership.pending_serial
1612
            # just make sure the project row is selected for update
1613
            project = membership.project
1614
            if serial in serials_to_ack:
1615
                membership.set_sync()
1616
            else:
1617
                membership.reset_sync()
1618

    
1619
        transaction.commit()
1620

    
1621
    qh_ack_serials(list(serials_to_ack))
1622
    return len(memberships)
1623

    
1624
def sync_projects():
1625
    sync_finish_serials()
1626

    
1627
    PENDING = ProjectMembership.PENDING
1628
    REMOVING = ProjectMembership.REMOVING
1629
    objects = ProjectMembership.objects.select_for_update()
1630

    
1631
    sub_quota, add_quota = [], []
1632

    
1633
    serial = new_serial()
1634

    
1635
    pending = objects.filter(state=PENDING)
1636
    for membership in pending:
1637

    
1638
        if membership.pending_application:
1639
            m = "%s: impossible: pending_application is not None (%s)" % (
1640
                membership, membership.pending_application)
1641
            raise AssertionError(m)
1642
        if membership.pending_serial:
1643
            m = "%s: impossible: pending_serial is not None (%s)" % (
1644
                membership, membership.pending_serial)
1645
            raise AssertionError(m)
1646

    
1647
        membership.pending_application = membership.project.application
1648
        membership.pending_serial = serial
1649
        membership.get_diff_quotas(sub_quota, add_quota)
1650
        membership.save()
1651

    
1652
    removing = objects.filter(state=REMOVING)
1653
    for membership in removing:
1654

    
1655
        if membership.pending_application:
1656
            m = ("%s: impossible: removing pending_application is not None (%s)"
1657
                % (membership, membership.pending_application))
1658
            raise AssertionError(m)
1659
        if membership.pending_serial:
1660
            m = "%s: impossible: pending_serial is not None (%s)" % (
1661
                membership, membership.pending_serial)
1662
            raise AssertionError(m)
1663

    
1664
        membership.pending_serial = serial
1665
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1666
        membership.save()
1667

    
1668
    transaction.commit()
1669
    # ProjectApplication.approve() unblocks here
1670
    # and can set PENDING an already PENDING membership
1671
    # which has been scheduled to sync with the old project.application
1672
    # Need to check in ProjectMembership.set_sync()
1673

    
1674
    r = qh_add_quota(serial, sub_quota, add_quota)
1675
    if r:
1676
        m = "cannot sync serial: %d" % serial
1677
        raise RuntimeError(m)
1678

    
1679
    sync_finish_serials([serial])
1680

    
1681

    
1682
def trigger_sync(retries=3, retry_wait=1.0):
1683
    transaction.commit()
1684

    
1685
    cursor = connection.cursor()
1686
    locked = True
1687
    try:
1688
        while 1:
1689
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1690
            r = cursor.fetchone()
1691
            if r is None:
1692
                m = "Impossible"
1693
                raise AssertionError(m)
1694
            locked = r[0]
1695
            if locked:
1696
                break
1697

    
1698
            retries -= 1
1699
            if retries <= 0:
1700
                return False
1701
            sleep(retry_wait)
1702

    
1703
        sync_projects()
1704
        return True
1705

    
1706
    finally:
1707
        if locked:
1708
            cursor.execute("SELECT pg_advisory_unlock(1)")
1709
            cursor.fetchall()
1710

    
1711

    
1712
class ProjectMembershipHistory(models.Model):
1713
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1714
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1715

    
1716
    person  =   models.CharField(max_length=255)
1717
    project =   models.BigIntegerField()
1718
    date    =   models.DateField(default=datetime.now)
1719
    reason  =   models.IntegerField()
1720
    serial  =   models.BigIntegerField()
1721

    
1722
### SIGNALS ###
1723
################
1724

    
1725
def create_astakos_user(u):
1726
    try:
1727
        AstakosUser.objects.get(user_ptr=u.pk)
1728
    except AstakosUser.DoesNotExist:
1729
        extended_user = AstakosUser(user_ptr_id=u.pk)
1730
        extended_user.__dict__.update(u.__dict__)
1731
        extended_user.save()
1732
        if not extended_user.has_auth_provider('local'):
1733
            extended_user.add_auth_provider('local')
1734
    except BaseException, e:
1735
        logger.exception(e)
1736

    
1737

    
1738
def fix_superusers(sender, **kwargs):
1739
    # Associate superusers with AstakosUser
1740
    admins = User.objects.filter(is_superuser=True)
1741
    for u in admins:
1742
        create_astakos_user(u)
1743
post_syncdb.connect(fix_superusers)
1744

    
1745

    
1746
def user_post_save(sender, instance, created, **kwargs):
1747
    if not created:
1748
        return
1749
    create_astakos_user(instance)
1750
post_save.connect(user_post_save, sender=User)
1751

    
1752
def astakosuser_post_save(sender, instance, created, **kwargs):
1753
    if not created:
1754
        return
1755
    # TODO handle socket.error & IOError
1756
    register_users((instance,))
1757
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1758

    
1759
def resource_post_save(sender, instance, created, **kwargs):
1760
    if not created:
1761
        return
1762
    register_resources((instance,))
1763
post_save.connect(resource_post_save, sender=Resource)
1764

    
1765
def renew_token(sender, instance, **kwargs):
1766
    if not instance.auth_token:
1767
        instance.renew_token()
1768
pre_save.connect(renew_token, sender=AstakosUser)
1769
pre_save.connect(renew_token, sender=Service)
1770