Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (61.5 kB)

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

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

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, BILLING_FIELDS,
70
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
71
    SITENAME, SERVICES, MODERATION_ENABLED)
72
from astakos.im import settings as astakos_settings
73
from astakos.im.endpoints.qh import (
74
    register_users, send_quota, register_resources, qh_add_quota, QuotaLimits,
75
    qh_query_serials, qh_ack_serials)
76
from astakos.im import auth_providers
77
#from astakos.im.endpoints.aquarium.producer import report_user_event
78
#from astakos.im.tasks import propagate_groupmembers_quota
79

    
80
import astakos.im.messages as astakos_messages
81
from .managers import ForUpdateManager
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):
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
        self.auth_token_expires = self.auth_token_created + \
123
            timedelta(hours=AUTH_TOKEN_DURATION)
124

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

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

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

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

    
148

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

    
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
_default_quota = {}
169
def get_default_quota():
170
    global _default_quota
171
    if _default_quota:
172
        return _default_quota
173
    for s, data in SERVICES.iteritems():
174
        map(
175
            lambda d:_default_quota.update(
176
                {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
177
            ),
178
            data.get('resources', {})
179
        )
180
    return _default_quota
181

    
182
class AstakosUserManager(UserManager):
183

    
184
    def get_auth_provider_user(self, provider, **kwargs):
185
        """
186
        Retrieve AstakosUser instance associated with the specified third party
187
        id.
188
        """
189
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
190
                          kwargs.iteritems()))
191
        return self.get(auth_providers__module=provider, **kwargs)
192

    
193
    def get_by_email(self, email):
194
        return self.get(email=email)
195

    
196
    def get_by_identifier(self, email_or_username, **kwargs):
197
        try:
198
            return self.get(email__iexact=email_or_username, **kwargs)
199
        except AstakosUser.DoesNotExist:
200
            return self.get(username__iexact=email_or_username, **kwargs)
201

    
202
    def user_exists(self, email_or_username, **kwargs):
203
        qemail = Q(email__iexact=email_or_username)
204
        qusername = Q(username__iexact=email_or_username)
205
        return self.filter(qemail | qusername).exists()
206

    
207

    
208
class AstakosUser(User):
209
    """
210
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
211
    """
212
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
213
                                   null=True)
214

    
215
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
216
    #                    AstakosUserProvider model.
217
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
218
                                null=True)
219
    # ex. screen_name for twitter, eppn for shibboleth
220
    third_party_identifier = models.CharField(_('Third-party identifier'),
221
                                              max_length=255, null=True,
222
                                              blank=True)
223

    
224

    
225
    #for invitations
226
    user_level = DEFAULT_USER_LEVEL
227
    level = models.IntegerField(_('Inviter level'), default=user_level)
228
    invitations = models.IntegerField(
229
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
230

    
231
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
232
                                  null=True, blank=True)
233
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
234
    auth_token_expires = models.DateTimeField(
235
        _('Token expiration date'), null=True)
236

    
237
    updated = models.DateTimeField(_('Update date'))
238
    is_verified = models.BooleanField(_('Is verified?'), default=False)
239

    
240
    email_verified = models.BooleanField(_('Email verified?'), default=False)
241

    
242
    has_credits = models.BooleanField(_('Has credits?'), default=False)
243
    has_signed_terms = models.BooleanField(
244
        _('I agree with the terms'), default=False)
245
    date_signed_terms = models.DateTimeField(
246
        _('Signed terms date'), null=True, blank=True)
247

    
248
    activation_sent = models.DateTimeField(
249
        _('Activation sent data'), null=True, blank=True)
250

    
251
    policy = models.ManyToManyField(
252
        Resource, null=True, through='AstakosUserQuota')
253

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

    
256
    __has_signed_terms = False
257
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
258
                                           default=False, db_index=True)
259

    
260
    objects = AstakosUserManager()
261

    
262
    def __init__(self, *args, **kwargs):
263
        super(AstakosUser, self).__init__(*args, **kwargs)
264
        self.__has_signed_terms = self.has_signed_terms
265
        if not self.id:
266
            self.is_active = False
267

    
268
    @property
269
    def realname(self):
270
        return '%s %s' % (self.first_name, self.last_name)
271

    
272
    @realname.setter
273
    def realname(self, value):
274
        parts = value.split(' ')
275
        if len(parts) == 2:
276
            self.first_name = parts[0]
277
            self.last_name = parts[1]
278
        else:
279
            self.last_name = parts[0]
280

    
281
    def add_permission(self, pname):
282
        if self.has_perm(pname):
283
            return
284
        p, created = Permission.objects.get_or_create(
285
                                    codename=pname,
286
                                    name=pname.capitalize(),
287
                                    content_type=get_content_type())
288
        self.user_permissions.add(p)
289

    
290
    def remove_permission(self, pname):
291
        if self.has_perm(pname):
292
            return
293
        p = Permission.objects.get(codename=pname,
294
                                   content_type=get_content_type())
295
        self.user_permissions.remove(p)
296

    
297
    @property
298
    def invitation(self):
299
        try:
300
            return Invitation.objects.get(username=self.email)
301
        except Invitation.DoesNotExist:
302
            return None
303

    
304
    @property
305
    def quota(self):
306
        """Returns a dict with the sum of quota limits per resource"""
307
        d = defaultdict(int)
308
        default_quota = get_default_quota()
309
        d.update(default_quota)
310
        for q in self.policies:
311
            d[q.resource] += q.uplimit or inf
312
        for m in self.projectmembership_set.select_related().all():
313
            if not m.acceptance_date:
314
                continue
315
            p = m.project
316
            if not p.is_active:
317
                continue
318
            grants = p.application.projectresourcegrant_set.all()
319
            for g in grants:
320
                d[str(g.resource)] += g.member_capacity or inf
321
        # TODO set default for remaining
322
        return d
323

    
324
    @property
325
    def policies(self):
326
        return self.astakosuserquota_set.select_related().all()
327

    
328
    @policies.setter
329
    def policies(self, policies):
330
        for p in policies:
331
            service = policies.get('service', None)
332
            resource = policies.get('resource', None)
333
            uplimit = policies.get('uplimit', 0)
334
            update = policies.get('update', True)
335
            self.add_policy(service, resource, uplimit, update)
336

    
337
    def add_policy(self, service, resource, uplimit, update=True):
338
        """Raises ObjectDoesNotExist, IntegrityError"""
339
        resource = Resource.objects.get(service__name=service, name=resource)
340
        if update:
341
            AstakosUserQuota.objects.update_or_create(user=self,
342
                                                      resource=resource,
343
                                                      defaults={'uplimit': uplimit})
344
        else:
345
            q = self.astakosuserquota_set
346
            q.create(resource=resource, uplimit=uplimit)
347

    
348
    def remove_policy(self, service, resource):
349
        """Raises ObjectDoesNotExist, IntegrityError"""
350
        resource = Resource.objects.get(service__name=service, name=resource)
351
        q = self.policies.get(resource=resource).delete()
352

    
353
    def update_uuid(self):
354
        while not self.uuid:
355
            uuid_val =  str(uuid.uuid4())
356
            try:
357
                AstakosUser.objects.get(uuid=uuid_val)
358
            except AstakosUser.DoesNotExist, e:
359
                self.uuid = uuid_val
360
        return self.uuid
361

    
362
    @property
363
    def extended_groups(self):
364
        return self.membership_set.select_related().all()
365

    
366
    def save(self, update_timestamps=True, **kwargs):
367
        if update_timestamps:
368
            if not self.id:
369
                self.date_joined = datetime.now()
370
            self.updated = datetime.now()
371

    
372
        # update date_signed_terms if necessary
373
        if self.__has_signed_terms != self.has_signed_terms:
374
            self.date_signed_terms = datetime.now()
375

    
376
        self.update_uuid()
377

    
378
        if self.username != self.email.lower():
379
            # set username
380
            self.username = self.email.lower()
381

    
382
        self.validate_unique_email_isactive()
383

    
384
        super(AstakosUser, self).save(**kwargs)
385

    
386
    def renew_token(self, flush_sessions=False, current_key=None):
387
        md5 = hashlib.md5()
388
        md5.update(settings.SECRET_KEY)
389
        md5.update(self.username)
390
        md5.update(self.realname.encode('ascii', 'ignore'))
391
        md5.update(asctime())
392

    
393
        self.auth_token = b64encode(md5.digest())
394
        self.auth_token_created = datetime.now()
395
        self.auth_token_expires = self.auth_token_created + \
396
                                  timedelta(hours=AUTH_TOKEN_DURATION)
397
        if flush_sessions:
398
            self.flush_sessions(current_key)
399
        msg = 'Token renewed for %s' % self.email
400
        logger.log(LOGGING_LEVEL, msg)
401

    
402
    def flush_sessions(self, current_key=None):
403
        q = self.sessions
404
        if current_key:
405
            q = q.exclude(session_key=current_key)
406

    
407
        keys = q.values_list('session_key', flat=True)
408
        if keys:
409
            msg = 'Flushing sessions: %s' % ','.join(keys)
410
            logger.log(LOGGING_LEVEL, msg, [])
411
        engine = import_module(settings.SESSION_ENGINE)
412
        for k in keys:
413
            s = engine.SessionStore(k)
414
            s.flush()
415

    
416
    def __unicode__(self):
417
        return '%s (%s)' % (self.realname, self.email)
418

    
419
    def conflicting_email(self):
420
        q = AstakosUser.objects.exclude(username=self.username)
421
        q = q.filter(email__iexact=self.email)
422
        if q.count() != 0:
423
            return True
424
        return False
425

    
426
    def validate_unique_email_isactive(self):
427
        """
428
        Implements a unique_together constraint for email and is_active fields.
429
        """
430
        q = AstakosUser.objects.all()
431
        q = q.filter(email = self.email)
432
        if self.id:
433
            q = q.filter(~Q(id = self.id))
434
        if q.count() != 0:
435
            m = 'Another account with the same email = %(email)s & \
436
                is_active = %(is_active)s found.' % self.__dict__
437
            raise ValidationError(m)
438

    
439
    def email_change_is_pending(self):
440
        return self.emailchanges.count() > 0
441

    
442
    def email_change_is_pending(self):
443
        return self.emailchanges.count() > 0
444

    
445
    @property
446
    def signed_terms(self):
447
        term = get_latest_terms()
448
        if not term:
449
            return True
450
        if not self.has_signed_terms:
451
            return False
452
        if not self.date_signed_terms:
453
            return False
454
        if self.date_signed_terms < term.date:
455
            self.has_signed_terms = False
456
            self.date_signed_terms = None
457
            self.save()
458
            return False
459
        return True
460

    
461
    def set_invitations_level(self):
462
        """
463
        Update user invitation level
464
        """
465
        level = self.invitation.inviter.level + 1
466
        self.level = level
467
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
468

    
469
    def can_login_with_auth_provider(self, provider):
470
        if not self.has_auth_provider(provider):
471
            return False
472
        else:
473
            return auth_providers.get_provider(provider).is_available_for_login()
474

    
475
    def can_add_auth_provider(self, provider, **kwargs):
476
        provider_settings = auth_providers.get_provider(provider)
477

    
478
        if not provider_settings.is_available_for_add():
479
            return False
480

    
481
        if self.has_auth_provider(provider) and \
482
           provider_settings.one_per_user:
483
            return False
484

    
485
        if 'provider_info' in kwargs:
486
            kwargs.pop('provider_info')
487

    
488
        if 'identifier' in kwargs:
489
            try:
490
                # provider with specified params already exist
491
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
492
                                                                   **kwargs)
493
            except AstakosUser.DoesNotExist:
494
                return True
495
            else:
496
                return False
497

    
498
        return True
499

    
500
    def can_remove_auth_provider(self, module):
501
        provider = auth_providers.get_provider(module)
502
        existing = self.get_active_auth_providers()
503
        existing_for_provider = self.get_active_auth_providers(module=module)
504

    
505
        if len(existing) <= 1:
506
            return False
507

    
508
        if len(existing_for_provider) == 1 and provider.is_required():
509
            return False
510

    
511
        return True
512

    
513
    def can_change_password(self):
514
        return self.has_auth_provider('local', auth_backend='astakos')
515

    
516
    def has_required_auth_providers(self):
517
        required = auth_providers.REQUIRED_PROVIDERS
518
        for provider in required:
519
            if not self.has_auth_provider(provider):
520
                return False
521
        return True
522

    
523
    def has_auth_provider(self, provider, **kwargs):
524
        return bool(self.auth_providers.filter(module=provider,
525
                                               **kwargs).count())
526

    
527
    def add_auth_provider(self, provider, **kwargs):
528
        info_data = ''
529
        if 'provider_info' in kwargs:
530
            info_data = kwargs.pop('provider_info')
531
            if isinstance(info_data, dict):
532
                info_data = json.dumps(info_data)
533

    
534
        if self.can_add_auth_provider(provider, **kwargs):
535
            self.auth_providers.create(module=provider, active=True,
536
                                       info_data=info_data,
537
                                       **kwargs)
538
        else:
539
            raise Exception('Cannot add provider')
540

    
541
    def add_pending_auth_provider(self, pending):
542
        """
543
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
544
        the current user.
545
        """
546
        if not isinstance(pending, PendingThirdPartyUser):
547
            pending = PendingThirdPartyUser.objects.get(token=pending)
548

    
549
        provider = self.add_auth_provider(pending.provider,
550
                               identifier=pending.third_party_identifier,
551
                                affiliation=pending.affiliation,
552
                                          provider_info=pending.info)
553

    
554
        if email_re.match(pending.email or '') and pending.email != self.email:
555
            self.additionalmail_set.get_or_create(email=pending.email)
556

    
557
        pending.delete()
558
        return provider
559

    
560
    def remove_auth_provider(self, provider, **kwargs):
561
        self.auth_providers.get(module=provider, **kwargs).delete()
562

    
563
    # user urls
564
    def get_resend_activation_url(self):
565
        return reverse('send_activation', kwargs={'user_id': self.pk})
566

    
567
    def get_provider_remove_url(self, module, **kwargs):
568
        return reverse('remove_auth_provider', kwargs={
569
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
570

    
571
    def get_activation_url(self, nxt=False):
572
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
573
                                 quote(self.auth_token))
574
        if nxt:
575
            url += "&next=%s" % quote(nxt)
576
        return url
577

    
578
    def get_password_reset_url(self, token_generator=default_token_generator):
579
        return reverse('django.contrib.auth.views.password_reset_confirm',
580
                          kwargs={'uidb36':int_to_base36(self.id),
581
                                  'token':token_generator.make_token(self)})
582

    
583
    def get_auth_providers(self):
584
        return self.auth_providers.all()
585

    
586
    def get_available_auth_providers(self):
587
        """
588
        Returns a list of providers available for user to connect to.
589
        """
590
        providers = []
591
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
592
            if self.can_add_auth_provider(module):
593
                providers.append(provider_settings(self))
594

    
595
        return providers
596

    
597
    def get_active_auth_providers(self, **filters):
598
        providers = []
599
        for provider in self.auth_providers.active(**filters):
600
            if auth_providers.get_provider(provider.module).is_available_for_login():
601
                providers.append(provider)
602
        return providers
603

    
604
    @property
605
    def auth_providers_display(self):
606
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
607

    
608
    def get_inactive_message(self):
609
        msg_extra = ''
610
        message = ''
611
        if self.activation_sent:
612
            if self.email_verified:
613
                message = _(astakos_messages.ACCOUNT_INACTIVE)
614
            else:
615
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
616
                if astakos_settings.MODERATION_ENABLED:
617
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
618
                else:
619
                    url = self.get_resend_activation_url()
620
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
621
                                u' ' + \
622
                                _('<a href="%s">%s?</a>') % (url,
623
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
624
        else:
625
            if astakos_settings.MODERATION_ENABLED:
626
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
627
            else:
628
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
629
                url = self.get_resend_activation_url()
630
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
631
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
632

    
633
        return mark_safe(message + u' '+ msg_extra)
634

    
635

    
636
class AstakosUserAuthProviderManager(models.Manager):
637

    
638
    def active(self, **filters):
639
        return self.filter(active=True, **filters)
640

    
641

    
642
class AstakosUserAuthProvider(models.Model):
643
    """
644
    Available user authentication methods.
645
    """
646
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
647
                                   null=True, default=None)
648
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
649
    module = models.CharField(_('Provider'), max_length=255, blank=False,
650
                                default='local')
651
    identifier = models.CharField(_('Third-party identifier'),
652
                                              max_length=255, null=True,
653
                                              blank=True)
654
    active = models.BooleanField(default=True)
655
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
656
                                   default='astakos')
657
    info_data = models.TextField(default="", null=True, blank=True)
658
    created = models.DateTimeField('Creation date', auto_now_add=True)
659

    
660
    objects = AstakosUserAuthProviderManager()
661

    
662
    class Meta:
663
        unique_together = (('identifier', 'module', 'user'), )
664
        ordering = ('module', 'created')
665

    
666
    def __init__(self, *args, **kwargs):
667
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
668
        try:
669
            self.info = json.loads(self.info_data)
670
            if not self.info:
671
                self.info = {}
672
        except Exception, e:
673
            self.info = {}
674

    
675
        for key,value in self.info.iteritems():
676
            setattr(self, 'info_%s' % key, value)
677

    
678

    
679
    @property
680
    def settings(self):
681
        return auth_providers.get_provider(self.module)
682

    
683
    @property
684
    def details_display(self):
685
        try:
686
          return self.settings.get_details_tpl_display % self.__dict__
687
        except:
688
          return ''
689

    
690
    @property
691
    def title_display(self):
692
        title_tpl = self.settings.get_title_display
693
        try:
694
            if self.settings.get_user_title_display:
695
                title_tpl = self.settings.get_user_title_display
696
        except Exception, e:
697
            pass
698
        try:
699
          return title_tpl % self.__dict__
700
        except:
701
          return self.settings.get_title_display % self.__dict__
702

    
703
    def can_remove(self):
704
        return self.user.can_remove_auth_provider(self.module)
705

    
706
    def delete(self, *args, **kwargs):
707
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
708
        if self.module == 'local':
709
            self.user.set_unusable_password()
710
            self.user.save()
711
        return ret
712

    
713
    def __repr__(self):
714
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
715

    
716
    def __unicode__(self):
717
        if self.identifier:
718
            return "%s:%s" % (self.module, self.identifier)
719
        if self.auth_backend:
720
            return "%s:%s" % (self.module, self.auth_backend)
721
        return self.module
722

    
723
    def save(self, *args, **kwargs):
724
        self.info_data = json.dumps(self.info)
725
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
726

    
727

    
728
class ExtendedManager(models.Manager):
729
    def _update_or_create(self, **kwargs):
730
        assert kwargs, \
731
            'update_or_create() must be passed at least one keyword argument'
732
        obj, created = self.get_or_create(**kwargs)
733
        defaults = kwargs.pop('defaults', {})
734
        if created:
735
            return obj, True, False
736
        else:
737
            try:
738
                params = dict(
739
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
740
                params.update(defaults)
741
                for attr, val in params.items():
742
                    if hasattr(obj, attr):
743
                        setattr(obj, attr, val)
744
                sid = transaction.savepoint()
745
                obj.save(force_update=True)
746
                transaction.savepoint_commit(sid)
747
                return obj, False, True
748
            except IntegrityError, e:
749
                transaction.savepoint_rollback(sid)
750
                try:
751
                    return self.get(**kwargs), False, False
752
                except self.model.DoesNotExist:
753
                    raise e
754

    
755
    update_or_create = _update_or_create
756

    
757

    
758
class AstakosUserQuota(models.Model):
759
    objects = ExtendedManager()
760
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
761
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
762
    resource = models.ForeignKey(Resource)
763
    user = models.ForeignKey(AstakosUser)
764

    
765
    class Meta:
766
        unique_together = ("resource", "user")
767

    
768

    
769
class ApprovalTerms(models.Model):
770
    """
771
    Model for approval terms
772
    """
773

    
774
    date = models.DateTimeField(
775
        _('Issue date'), db_index=True, default=datetime.now())
776
    location = models.CharField(_('Terms location'), max_length=255)
777

    
778

    
779
class Invitation(models.Model):
780
    """
781
    Model for registring invitations
782
    """
783
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
784
                                null=True)
785
    realname = models.CharField(_('Real name'), max_length=255)
786
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
787
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
788
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
789
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
790
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
791

    
792
    def __init__(self, *args, **kwargs):
793
        super(Invitation, self).__init__(*args, **kwargs)
794
        if not self.id:
795
            self.code = _generate_invitation_code()
796

    
797
    def consume(self):
798
        self.is_consumed = True
799
        self.consumed = datetime.now()
800
        self.save()
801

    
802
    def __unicode__(self):
803
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
804

    
805

    
806
class EmailChangeManager(models.Manager):
807

    
808
    @transaction.commit_on_success
809
    def change_email(self, activation_key):
810
        """
811
        Validate an activation key and change the corresponding
812
        ``User`` if valid.
813

814
        If the key is valid and has not expired, return the ``User``
815
        after activating.
816

817
        If the key is not valid or has expired, return ``None``.
818

819
        If the key is valid but the ``User`` is already active,
820
        return ``None``.
821

822
        After successful email change the activation record is deleted.
823

824
        Throws ValueError if there is already
825
        """
826
        try:
827
            email_change = self.model.objects.get(
828
                activation_key=activation_key)
829
            if email_change.activation_key_expired():
830
                email_change.delete()
831
                raise EmailChange.DoesNotExist
832
            # is there an active user with this address?
833
            try:
834
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
835
            except AstakosUser.DoesNotExist:
836
                pass
837
            else:
838
                raise ValueError(_('The new email address is reserved.'))
839
            # update user
840
            user = AstakosUser.objects.get(pk=email_change.user_id)
841
            old_email = user.email
842
            user.email = email_change.new_email_address
843
            user.save()
844
            email_change.delete()
845
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
846
                                                          user.email)
847
            logger.log(LOGGING_LEVEL, msg)
848
            return user
849
        except EmailChange.DoesNotExist:
850
            raise ValueError(_('Invalid activation key.'))
851

    
852

    
853
class EmailChange(models.Model):
854
    new_email_address = models.EmailField(
855
        _(u'new e-mail address'),
856
        help_text=_('Your old email address will be used until you verify your new one.'))
857
    user = models.ForeignKey(
858
        AstakosUser, unique=True, related_name='emailchanges')
859
    requested_at = models.DateTimeField(default=datetime.now())
860
    activation_key = models.CharField(
861
        max_length=40, unique=True, db_index=True)
862

    
863
    objects = EmailChangeManager()
864

    
865
    def get_url(self):
866
        return reverse('email_change_confirm',
867
                      kwargs={'activation_key': self.activation_key})
868

    
869
    def activation_key_expired(self):
870
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
871
        return self.requested_at + expiration_date < datetime.now()
872

    
873

    
874
class AdditionalMail(models.Model):
875
    """
876
    Model for registring invitations
877
    """
878
    owner = models.ForeignKey(AstakosUser)
879
    email = models.EmailField()
880

    
881

    
882
def _generate_invitation_code():
883
    while True:
884
        code = randint(1, 2L ** 63 - 1)
885
        try:
886
            Invitation.objects.get(code=code)
887
            # An invitation with this code already exists, try again
888
        except Invitation.DoesNotExist:
889
            return code
890

    
891

    
892
def get_latest_terms():
893
    try:
894
        term = ApprovalTerms.objects.order_by('-id')[0]
895
        return term
896
    except IndexError:
897
        pass
898
    return None
899

    
900
class PendingThirdPartyUser(models.Model):
901
    """
902
    Model for registring successful third party user authentications
903
    """
904
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
905
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
906
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
907
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
908
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
909
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
910
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
911
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
912
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
913
    info = models.TextField(default="", null=True, blank=True)
914

    
915
    class Meta:
916
        unique_together = ("provider", "third_party_identifier")
917

    
918
    def get_user_instance(self):
919
        d = self.__dict__
920
        d.pop('_state', None)
921
        d.pop('id', None)
922
        d.pop('token', None)
923
        d.pop('created', None)
924
        d.pop('info', None)
925
        user = AstakosUser(**d)
926

    
927
        return user
928

    
929
    @property
930
    def realname(self):
931
        return '%s %s' %(self.first_name, self.last_name)
932

    
933
    @realname.setter
934
    def realname(self, value):
935
        parts = value.split(' ')
936
        if len(parts) == 2:
937
            self.first_name = parts[0]
938
            self.last_name = parts[1]
939
        else:
940
            self.last_name = parts[0]
941

    
942
    def save(self, **kwargs):
943
        if not self.id:
944
            # set username
945
            while not self.username:
946
                username =  uuid.uuid4().hex[:30]
947
                try:
948
                    AstakosUser.objects.get(username = username)
949
                except AstakosUser.DoesNotExist, e:
950
                    self.username = username
951
        super(PendingThirdPartyUser, self).save(**kwargs)
952

    
953
    def generate_token(self):
954
        self.password = self.third_party_identifier
955
        self.last_login = datetime.now()
956
        self.token = default_token_generator.make_token(self)
957

    
958
class SessionCatalog(models.Model):
959
    session_key = models.CharField(_('session key'), max_length=40)
960
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
961

    
962

    
963
### PROJECTS ###
964
################
965

    
966
class MemberJoinPolicy(models.Model):
967
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
968
    description = models.CharField(_('Description'), max_length=80)
969

    
970
    def __str__(self):
971
        return self.policy
972

    
973
class MemberLeavePolicy(models.Model):
974
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
975
    description = models.CharField(_('Description'), max_length=80)
976

    
977
    def __str__(self):
978
        return self.policy
979

    
980
def synced_model_metaclass(class_name, class_parents, class_attributes):
981

    
982
    new_attributes = {}
983
    sync_attributes = {}
984

    
985
    for name, value in class_attributes.iteritems():
986
        sync, underscore, rest = name.partition('_')
987
        if sync == 'sync' and underscore == '_':
988
            sync_attributes[rest] = value
989
        else:
990
            new_attributes[name] = value
991

    
992
    if 'prefix' not in sync_attributes:
993
        m = ("you did not specify a 'sync_prefix' attribute "
994
             "in class '%s'" % (class_name,))
995
        raise ValueError(m)
996

    
997
    prefix = sync_attributes.pop('prefix')
998
    class_name = sync_attributes.pop('classname', prefix + '_model')
999

    
1000
    for name, value in sync_attributes.iteritems():
1001
        newname = prefix + '_' + name
1002
        if newname in new_attributes:
1003
            m = ("class '%s' was specified with prefix '%s' "
1004
                 "but it already has an attribute named '%s'"
1005
                 % (class_name, prefix, newname))
1006
            raise ValueError(m)
1007

    
1008
        new_attributes[newname] = value
1009

    
1010
    newclass = type(class_name, class_parents, new_attributes)
1011
    return newclass
1012

    
1013

    
1014
def make_synced(prefix='sync', name='SyncedState'):
1015

    
1016
    the_name = name
1017
    the_prefix = prefix
1018

    
1019
    class SyncedState(models.Model):
1020

    
1021
        sync_classname      = the_name
1022
        sync_prefix         = the_prefix
1023
        __metaclass__       = synced_model_metaclass
1024

    
1025
        sync_new_state      = models.BigIntegerField(null=True)
1026
        sync_synced_state   = models.BigIntegerField(null=True)
1027
        STATUS_SYNCED       = 0
1028
        STATUS_PENDING      = 1
1029
        sync_status         = models.IntegerField(db_index=True)
1030

    
1031
        class Meta:
1032
            abstract = True
1033

    
1034
        class NotSynced(Exception):
1035
            pass
1036

    
1037
        def sync_init_state(self, state):
1038
            self.sync_synced_state = state
1039
            self.sync_new_state = state
1040
            self.sync_status = self.STATUS_SYNCED
1041

    
1042
        def sync_get_status(self):
1043
            return self.sync_status
1044

    
1045
        def sync_set_status(self):
1046
            if self.sync_new_state != self.sync_synced_state:
1047
                self.sync_status = self.STATUS_PENDING
1048
            else:
1049
                self.sync_status = self.STATUS_SYNCED
1050

    
1051
        def sync_set_synced(self):
1052
            self.sync_synced_state = self.sync_new_state
1053
            self.sync_status = self.STATUS_SYNCED
1054

    
1055
        def sync_get_synced_state(self):
1056
            return self.sync_synced_state
1057

    
1058
        def sync_set_new_state(self, new_state):
1059
            self.sync_new_state = new_state
1060
            self.sync_set_status()
1061

    
1062
        def sync_get_new_state(self):
1063
            return self.sync_new_state
1064

    
1065
        def sync_set_synced_state(self, synced_state):
1066
            self.sync_synced_state = synced_state
1067
            self.sync_set_status()
1068

    
1069
        def sync_get_pending_objects(self):
1070
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1071
            return self.objects.filter(**kw)
1072

    
1073
        def sync_get_synced_objects(self):
1074
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1075
            return self.objects.filter(**kw)
1076

    
1077
        def sync_verify_get_synced_state(self):
1078
            status = self.sync_get_status()
1079
            state = self.sync_get_synced_state()
1080
            verified = (status == self.STATUS_SYNCED)
1081
            return state, verified
1082

    
1083
        def sync_is_synced(self):
1084
            state, verified = self.sync_verify_get_synced_state()
1085
            return verified
1086

    
1087
    return SyncedState
1088

    
1089
SyncedState = make_synced(prefix='sync', name='SyncedState')
1090

    
1091

    
1092
class ProjectApplication(models.Model):
1093
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1094
    applicant               =   models.ForeignKey(
1095
                                    AstakosUser,
1096
                                    related_name='projects_applied',
1097
                                    db_index=True)
1098

    
1099
    state                   =   models.CharField(max_length=80,
1100
                                                default=UNKNOWN)
1101

    
1102
    owner                   =   models.ForeignKey(
1103
                                    AstakosUser,
1104
                                    related_name='projects_owned',
1105
                                    db_index=True)
1106

    
1107
    precursor_application   =   models.OneToOneField('ProjectApplication',
1108
                                                     null=True,
1109
                                                     blank=True,
1110
                                                     db_index=True)
1111

    
1112
    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 ",)
1113
    homepage                =   models.URLField(max_length=255, null=True,
1114
                                                blank=True,help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",)
1115
    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. ")
1116
    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.")
1117
    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.  ")
1118
    member_join_policy      =   models.ForeignKey(MemberJoinPolicy)
1119
    member_leave_policy     =   models.ForeignKey(MemberLeavePolicy)
1120
    limit_on_members_number =   models.PositiveIntegerField(null=True,
1121
                                                            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. ")
1122
    resource_grants         =   models.ManyToManyField(
1123
                                    Resource,
1124
                                    null=True,
1125
                                    blank=True,
1126
                                    through='ProjectResourceGrant')
1127
    comments                =   models.TextField(null=True, blank=True)
1128
    issue_date              =   models.DateTimeField()
1129

    
1130
    def add_resource_policy(self, service, resource, uplimit):
1131
        """Raises ObjectDoesNotExist, IntegrityError"""
1132
        q = self.projectresourcegrant_set
1133
        resource = Resource.objects.get(service__name=service, name=resource)
1134
        q.create(resource=resource, member_capacity=uplimit)
1135

    
1136
    
1137
    @property
1138
    def grants(self):
1139
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1140
            
1141
    @property
1142
    def resource_policies(self):
1143
        return self.projectresourcegrant_set.all()
1144

    
1145
    @resource_policies.setter
1146
    def resource_policies(self, policies):
1147
        for p in policies:
1148
            service = p.get('service', None)
1149
            resource = p.get('resource', None)
1150
            uplimit = p.get('uplimit', 0)
1151
            self.add_resource_policy(service, resource, uplimit)
1152

    
1153
    @property
1154
    def follower(self):
1155
        try:
1156
            return ProjectApplication.objects.get(precursor_application=self)
1157
        except ProjectApplication.DoesNotExist:
1158
            return
1159

    
1160
    def submit(self, resource_policies, applicant, comments,
1161
               precursor_application=None):
1162

    
1163
        if precursor_application:
1164
            self.precursor_application = precursor_application
1165
            self.owner = precursor_application.owner
1166
        else:
1167
            self.owner = applicant
1168

    
1169
        self.id = None
1170
        self.applicant = applicant
1171
        self.comments = comments
1172
        self.issue_date = datetime.now()
1173
        self.state = self.PENDING
1174
        self.save()
1175
        self.resource_policies = resource_policies
1176

    
1177
    def _get_project(self):
1178
        precursor = self
1179
        while precursor:
1180
            try:
1181
                project = precursor.project
1182
                return project
1183
            except Project.DoesNotExist:
1184
                pass
1185
            precursor = precursor.precursor_application
1186

    
1187
        return None
1188

    
1189
    def approve(self, approval_user=None):
1190
        """
1191
        If approval_user then during owner membership acceptance
1192
        it is checked whether the request_user is eligible.
1193

1194
        Raises:
1195
            PermissionDenied
1196
        """
1197

    
1198
        if not transaction.is_managed():
1199
            raise AssertionError("NOPE")
1200

    
1201
        new_project_name = self.name
1202
        if self.state != self.PENDING:
1203
            m = _("cannot approve: project '%s' in state '%s'") % (
1204
                    new_project_name, self.state)
1205
            raise PermissionDenied(m) # invalid argument
1206

    
1207
        now = datetime.now()
1208
        project = self._get_project()
1209

    
1210
        try:
1211
            # needs SERIALIZABLE
1212
            conflicting_project = Project.objects.get(name=new_project_name)
1213
            if (conflicting_project.is_alive and
1214
                conflicting_project != project):
1215
                m = (_("cannot approve: project with name '%s' "
1216
                       "already exists (serial: %s)") % (
1217
                        new_project_name, conflicting_project.id))
1218
                raise PermissionDenied(m) # invalid argument
1219
        except Project.DoesNotExist:
1220
            pass
1221

    
1222
        if project is None:
1223
            project = Project(creation_date=now)
1224

    
1225
        project.name = new_project_name
1226
        project.application = self
1227

    
1228
        # This will block while syncing,
1229
        # but unblock before setting the membership state.
1230
        # See ProjectMembership.set_sync()
1231
        project.set_membership_pending_sync()
1232

    
1233
        project.last_approval_date = now
1234
        project.save()
1235
        #ProjectMembership.add_to_project(self)
1236
        project.add_member(self.owner)
1237

    
1238
        precursor = self.precursor_application
1239
        while precursor:
1240
            precursor.state = self.REPLACED
1241
            precursor.save()
1242
            precursor = precursor.precursor_application
1243

    
1244
        self.state = self.APPROVED
1245
        self.save()
1246

    
1247

    
1248
class ProjectResourceGrant(models.Model):
1249

    
1250
    resource                =   models.ForeignKey(Resource)
1251
    project_application     =   models.ForeignKey(ProjectApplication,
1252
                                                  null=True)
1253
    project_capacity        =   models.BigIntegerField(null=True)
1254
    project_import_limit    =   models.BigIntegerField(null=True)
1255
    project_export_limit    =   models.BigIntegerField(null=True)
1256
    member_capacity         =   models.BigIntegerField(null=True)
1257
    member_import_limit     =   models.BigIntegerField(null=True)
1258
    member_export_limit     =   models.BigIntegerField(null=True)
1259

    
1260
    objects = ExtendedManager()
1261

    
1262
    class Meta:
1263
        unique_together = ("resource", "project_application")
1264

    
1265

    
1266
class Project(models.Model):
1267

    
1268
    application                 =   models.OneToOneField(
1269
                                            ProjectApplication,
1270
                                            related_name='project')
1271
    last_approval_date          =   models.DateTimeField(null=True)
1272

    
1273
    members                     =   models.ManyToManyField(
1274
                                            AstakosUser,
1275
                                            through='ProjectMembership')
1276

    
1277
    termination_start_date      =   models.DateTimeField(null=True)
1278
    termination_date            =   models.DateTimeField(null=True)
1279

    
1280
    creation_date               =   models.DateTimeField()
1281
    name                        =   models.CharField(
1282
                                            max_length=80,
1283
                                            db_index=True,
1284
                                            unique=True)
1285

    
1286
    @property
1287
    def violated_resource_grants(self):
1288
        return False
1289

    
1290
    @property
1291
    def violated_members_number_limit(self):
1292
        application = self.application
1293
        return len(self.approved_members) > application.limit_on_members_number
1294

    
1295
    @property
1296
    def is_terminated(self):
1297
        return bool(self.termination_date)
1298

    
1299
    @property
1300
    def is_still_approved(self):
1301
        return bool(self.last_approval_date)
1302

    
1303
    @property
1304
    def is_active(self):
1305
        if (self.is_terminated or
1306
            not self.is_still_approved or
1307
            self.violated_resource_grants):
1308
            return False
1309
#         if self.violated_members_number_limit:
1310
#             return False
1311
        return True
1312

    
1313
    @property
1314
    def is_suspended(self):
1315
        if (self.is_terminated or
1316
            self.is_still_approved or
1317
            not self.violated_resource_grants):
1318
            return False
1319
#             if not self.violated_members_number_limit:
1320
#                 return False
1321
        return True
1322

    
1323
    @property
1324
    def is_alive(self):
1325
        return self.is_active or self.is_suspended
1326

    
1327
    @property
1328
    def is_inconsistent(self):
1329
        now = datetime.now()
1330
        if self.creation_date > now:
1331
            return True
1332
        if self.last_approval_date > now:
1333
            return True
1334
        if self.terminaton_date > now:
1335
            return True
1336
        return False
1337

    
1338
    @property
1339
    def approved_memberships(self):
1340
        ACCEPTED = ProjectMembership.ACCEPTED
1341
        PENDING  = ProjectMembership.PENDING
1342
        return self.projectmembership_set.filter(
1343
            Q(state=ACCEPTED) | Q(state=PENDING))
1344

    
1345
    @property
1346
    def approved_members(self):
1347
        return [m.person for m in self.approved_memberships]
1348

    
1349
    def set_membership_pending_sync(self):
1350
        ACCEPTED = ProjectMembership.ACCEPTED
1351
        PENDING  = ProjectMembership.PENDING
1352
        sfu = self.projectmembership_set.select_for_update()
1353
        members = sfu.filter(Q(state=ACCEPTED) | Q(state=PENDING))
1354

    
1355
        for member in members:
1356
            member.state = member.PENDING
1357
            member.save()
1358

    
1359
    def add_member(self, user):
1360
        """
1361
        Raises:
1362
            django.exceptions.PermissionDenied
1363
            astakos.im.models.AstakosUser.DoesNotExist
1364
        """
1365
        if isinstance(user, int):
1366
            user = AstakosUser.objects.get(user=user)
1367

    
1368
        m, created = ProjectMembership.objects.get_or_create(
1369
            person=user, project=self
1370
        )
1371
        m.accept()
1372

    
1373
    def remove_member(self, user):
1374
        """
1375
        Raises:
1376
            django.exceptions.PermissionDenied
1377
            astakos.im.models.AstakosUser.DoesNotExist
1378
            astakos.im.models.ProjectMembership.DoesNotExist
1379
        """
1380
        if isinstance(user, int):
1381
            user = AstakosUser.objects.get(user=user)
1382

    
1383
        m = ProjectMembership.objects.get(person=user, project=self)
1384
        m.remove()
1385

    
1386
    def set_termination_start_date(self):
1387
        self.termination_start_date = datetime.now()
1388
        self.terminaton_date = None
1389
        self.save()
1390

    
1391
    def set_termination_date(self):
1392
        self.termination_start_date = None
1393
        self.termination_date = datetime.now()
1394
        self.save()
1395

    
1396

    
1397
class ProjectMembership(models.Model):
1398

    
1399
    person              =   models.ForeignKey(AstakosUser)
1400
    request_date        =   models.DateField(default=datetime.now())
1401
    project             =   models.ForeignKey(Project)
1402

    
1403
    state               =   models.IntegerField(default=0)
1404
    application         =   models.ForeignKey(
1405
                                ProjectApplication,
1406
                                null=True,
1407
                                related_name='memberships')
1408
    pending_application =   models.ForeignKey(
1409
                                ProjectApplication,
1410
                                null=True,
1411
                                related_name='pending_memebrships')
1412
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1413

    
1414
    acceptance_date     =   models.DateField(null=True, db_index=True)
1415
    leave_request_date  =   models.DateField(null=True)
1416

    
1417
    objects     =   ForUpdateManager()
1418

    
1419
    REQUESTED   =   0
1420
    PENDING     =   1
1421
    ACCEPTED    =   2
1422
    REMOVING    =   3
1423
    REMOVED     =   4
1424

    
1425
    class Meta:
1426
        unique_together = ("person", "project")
1427
        #index_together = [["project", "state"]]
1428

    
1429
    def __str__(self):
1430
        return _("<'%s' membership in project '%s'>") % (
1431
                self.person.username, self.project.application)
1432

    
1433
    __repr__ = __str__
1434

    
1435
    def __init__(self, *args, **kwargs):
1436
        self.state = self.REQUESTED
1437
        super(ProjectMembership, self).__init__(*args, **kwargs)
1438

    
1439
    def _set_history_item(self, reason, date=None):
1440
        if isinstance(reason, basestring):
1441
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1442

    
1443
        history_item = ProjectMembershipHistory(
1444
                            serial=self.id,
1445
                            person=self.person,
1446
                            project=self.project,
1447
                            date=date or datetime.now(),
1448
                            reason=reason)
1449
        history_item.save()
1450
        serial = history_item.id
1451

    
1452
    def accept(self):
1453
        state = self.state
1454
        if state != self.REQUESTED:
1455
            m = _("%s: attempt to accept in state [%s]") % (self, state)
1456
            raise AssertionError(m)
1457

    
1458
        now = datetime.now()
1459
        self.acceptance_date = now
1460
        self._set_history_item(reason='ACCEPT', date=now)
1461
        self.state = self.PENDING
1462
        self.save()
1463

    
1464
    def remove(self):
1465
        state = self.state
1466
        if state != self.ACCEPTED:
1467
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1468
            raise AssertionError(m)
1469

    
1470
        self._set_history_item(reason='REMOVE')
1471
        self.state = self.REMOVING
1472
        self.save()
1473

    
1474
    def reject(self):
1475
        state = self.state
1476
        if state != self.REQUESTED:
1477
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1478
            raise AssertionError(m)
1479

    
1480
        # rejected requests don't need sync,
1481
        # because they were never effected
1482
        self._set_history_item(reason='REJECT')
1483
        self.delete()
1484

    
1485
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1486
        if sub_list is None:
1487
            sub_list = []
1488

    
1489
        if add_list is None:
1490
            add_list = []
1491

    
1492
        sub_append = sub_list.append
1493
        add_append = add_list.append
1494
        holder = self.person.uuid
1495

    
1496
        synced_application = self.application
1497
        if synced_application is not None:
1498
            cur_grants = synced_application.projectresourcegrant_set.all()
1499
            for grant in cur_grants:
1500
                sub_append(QuotaLimits(
1501
                               holder       = holder,
1502
                               resource     = str(grant.resource),
1503
                               capacity     = grant.member_capacity,
1504
                               import_limit = grant.member_import_limit,
1505
                               export_limit = grant.member_export_limit))
1506

    
1507
        if not remove:
1508
            new_grants = self.pending_application.projectresourcegrant_set.all()
1509
            for new_grant in new_grants:
1510
                add_append(QuotaLimits(
1511
                               holder       = holder,
1512
                               resource     = str(new_grant.resource),
1513
                               capacity     = new_grant.member_capacity,
1514
                               import_limit = new_grant.member_import_limit,
1515
                               export_limit = new_grant.member_export_limit))
1516

    
1517
        return (sub_list, add_list)
1518

    
1519
    def set_sync(self):
1520
        state = self.state
1521
        if state == self.PENDING:
1522
            pending_application = self.pending_application
1523
            if pending_application is None:
1524
                m = _("%s: attempt to sync an empty pending application") % (
1525
                    self, state)
1526
                raise AssertionError(m)
1527
            self.application = pending_application
1528
            self.pending_application = None
1529
            self.pending_serial = None
1530

    
1531
            # project.application may have changed in the meantime,
1532
            # in which case we stay PENDING;
1533
            # we are safe to check due to select_for_update
1534
            if self.application == self.project.application:
1535
                self.state = self.ACCEPTED
1536
            self.save()
1537
        elif state == self.REMOVING:
1538
            self.delete()
1539
        else:
1540
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1541
            raise AssertionError(m)
1542

    
1543
    def reset_sync(self):
1544
        state = self.state
1545
        if state in [self.PENDING, self.REMOVING]:
1546
            self.pending_application = None
1547
            self.pending_serial = None
1548
            self.save()
1549
        else:
1550
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1551
            raise AssertionError(m)
1552

    
1553
class Serial(models.Model):
1554
    serial  =   models.AutoField(primary_key=True)
1555

    
1556
def new_serial():
1557
    s = Serial.objects.create()
1558
    serial = s.serial
1559
    s.delete()
1560
    return serial
1561

    
1562
def sync_finish_serials(serials_to_ack=None):
1563
    if serials_to_ack is None:
1564
        serials_to_ack = qh_query_serials([])
1565

    
1566
    serials_to_ack = set(serials_to_ack)
1567
    sfu = ProjectMembership.objects.select_for_update()
1568
    memberships = list(sfu.filter(pending_serial__isnull=False))
1569

    
1570
    if memberships:
1571
        for membership in memberships:
1572
            serial = membership.pending_serial
1573
            # just make sure the project row is selected for update
1574
            project = membership.project
1575
            if serial in serials_to_ack:
1576
                membership.set_sync()
1577
            else:
1578
                membership.reset_sync()
1579

    
1580
        transaction.commit()
1581

    
1582
    qh_ack_serials(list(serials_to_ack))
1583
    return len(memberships)
1584

    
1585
def sync_projects():
1586
    sync_finish_serials()
1587

    
1588
    PENDING = ProjectMembership.PENDING
1589
    REMOVING = ProjectMembership.REMOVING
1590
    objects = ProjectMembership.objects.select_for_update()
1591

    
1592
    sub_quota, add_quota = [], []
1593

    
1594
    serial = new_serial()
1595

    
1596
    pending = objects.filter(state=PENDING)
1597
    for membership in pending:
1598

    
1599
        if membership.pending_application:
1600
            m = "%s: impossible: pending_application is not None (%s)" % (
1601
                membership, membership.pending_application)
1602
            raise AssertionError(m)
1603
        if membership.pending_serial:
1604
            m = "%s: impossible: pending_serial is not None (%s)" % (
1605
                membership, membership.pending_serial)
1606
            raise AssertionError(m)
1607

    
1608
        membership.pending_application = membership.project.application
1609
        membership.pending_serial = serial
1610
        membership.get_diff_quotas(sub_quota, add_quota)
1611
        membership.save()
1612

    
1613
    removing = objects.filter(state=REMOVING)
1614
    for membership in removing:
1615

    
1616
        if membership.pending_application:
1617
            m = ("%s: impossible: removing pending_application is not None (%s)"
1618
                % (membership, membership.pending_application))
1619
            raise AssertionError(m)
1620
        if membership.pending_serial:
1621
            m = "%s: impossible: pending_serial is not None (%s)" % (
1622
                membership, membership.pending_serial)
1623
            raise AssertionError(m)
1624

    
1625
        membership.pending_serial = serial
1626
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1627
        membership.save()
1628

    
1629
    transaction.commit()
1630
    # ProjectApplication.approve() unblocks here
1631
    # and can set PENDING an already PENDING membership
1632
    # which has been scheduled to sync with the old project.application
1633
    # Need to check in ProjectMembership.set_sync()
1634

    
1635
    r = qh_add_quota(serial, sub_quota, add_quota)
1636
    if r:
1637
        m = "cannot sync serial: %d" % serial
1638
        raise RuntimeError(m)
1639

    
1640
    sync_finish_serials([serial])
1641

    
1642

    
1643
def trigger_sync(retries=3, retry_wait=1.0):
1644
    cursor = connection.cursor()
1645
    locked = True
1646
    try:
1647
        while 1:
1648
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1649
            r = cursor.fetchone()
1650
            if r is None:
1651
                m = "Impossible"
1652
                raise AssertionError(m)
1653
            locked = r[0]
1654
            if locked:
1655
                break
1656

    
1657
            retries -= 1
1658
            if retries <= 0:
1659
                return False
1660
            sleep(retry_wait)
1661

    
1662
        transaction.commit()
1663
        sync_projects()
1664
        return True
1665

    
1666
    finally:
1667
        if locked:
1668
            cursor.execute("SELECT pg_advisory_unlock(1)")
1669
            cursor.fetchall()
1670

    
1671

    
1672
class ProjectMembershipHistory(models.Model):
1673
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1674
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1675

    
1676
    person  =   models.ForeignKey(AstakosUser)
1677
    project =   models.ForeignKey(Project)
1678
    date    =   models.DateField(default=datetime.now)
1679
    reason  =   models.IntegerField()
1680
    serial  =   models.BigIntegerField()
1681

    
1682
### SIGNALS ###
1683
################
1684

    
1685
def create_astakos_user(u):
1686
    try:
1687
        AstakosUser.objects.get(user_ptr=u.pk)
1688
    except AstakosUser.DoesNotExist:
1689
        extended_user = AstakosUser(user_ptr_id=u.pk)
1690
        extended_user.__dict__.update(u.__dict__)
1691
        extended_user.save()
1692
        if not extended_user.has_auth_provider('local'):
1693
            extended_user.add_auth_provider('local')
1694
    except BaseException, e:
1695
        logger.exception(e)
1696

    
1697

    
1698
def fix_superusers(sender, **kwargs):
1699
    # Associate superusers with AstakosUser
1700
    admins = User.objects.filter(is_superuser=True)
1701
    for u in admins:
1702
        create_astakos_user(u)
1703
post_syncdb.connect(fix_superusers)
1704

    
1705

    
1706
def user_post_save(sender, instance, created, **kwargs):
1707
    if not created:
1708
        return
1709
    create_astakos_user(instance)
1710
post_save.connect(user_post_save, sender=User)
1711

    
1712
def astakosuser_pre_save(sender, instance, **kwargs):
1713
    instance.aquarium_report = False
1714
    instance.new = False
1715
    try:
1716
        db_instance = AstakosUser.objects.get(id=instance.id)
1717
    except AstakosUser.DoesNotExist:
1718
        # create event
1719
        instance.aquarium_report = True
1720
        instance.new = True
1721
    else:
1722
        get = AstakosUser.__getattribute__
1723
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1724
                   BILLING_FIELDS)
1725
        instance.aquarium_report = True if l else False
1726
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1727

    
1728
def astakosuser_post_save(sender, instance, created, **kwargs):
1729
    if instance.aquarium_report:
1730
        report_user_event(instance, create=instance.new)
1731
    if not created:
1732
        return
1733
    # TODO handle socket.error & IOError
1734
    register_users((instance,))
1735
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1736

    
1737

    
1738
def resource_post_save(sender, instance, created, **kwargs):
1739
    if not created:
1740
        return
1741
    register_resources((instance,))
1742
post_save.connect(resource_post_save, sender=Resource)
1743

    
1744

    
1745
def renew_token(sender, instance, **kwargs):
1746
    if not instance.auth_token:
1747
        instance.renew_token()
1748
pre_save.connect(renew_token, sender=AstakosUser)
1749
pre_save.connect(renew_token, sender=Service)
1750