Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (61.6 kB)

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

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

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, 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, 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, expiration_date=None):
115
        md5 = hashlib.md5()
116
        md5.update(self.name.encode('ascii', 'ignore'))
117
        md5.update(self.url.encode('ascii', 'ignore'))
118
        md5.update(asctime())
119

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

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

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

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

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

    
150

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

    
155

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

    
164
    class Meta:
165
        unique_together = ("name", "service")
166

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

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

    
184
class AstakosUserManager(UserManager):
185

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

    
195
    def get_by_email(self, email):
196
        return self.get(email=email)
197

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

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

    
209

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

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

    
226

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

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

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

    
242
    email_verified = models.BooleanField(_('Email verified?'), default=False)
243

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

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

    
253
    policy = models.ManyToManyField(
254
        Resource, null=True, through='AstakosUserQuota')
255

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

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

    
262
    objects = AstakosUserManager()
263

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

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

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

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

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

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

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

    
326
    @property
327
    def policies(self):
328
        return self.astakosuserquota_set.select_related().all()
329

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

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

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

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

    
364
    @property
365
    def extended_groups(self):
366
        return self.membership_set.select_related().all()
367

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

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

    
378
        self.update_uuid()
379

    
380
        if self.username != self.email.lower():
381
            # set username
382
            self.username = self.email.lower()
383

    
384
        self.validate_unique_email_isactive()
385

    
386
        super(AstakosUser, self).save(**kwargs)
387

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

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

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

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

    
418
    def __unicode__(self):
419
        return '%s (%s)' % (self.realname, self.email)
420

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

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

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

    
444
    def email_change_is_pending(self):
445
        return self.emailchanges.count() > 0
446

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

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

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

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

    
480
        if not provider_settings.is_available_for_add():
481
            return False
482

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

    
487
        if 'provider_info' in kwargs:
488
            kwargs.pop('provider_info')
489

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

    
500
        return True
501

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

    
507
        if len(existing) <= 1:
508
            return False
509

    
510
        if len(existing_for_provider) == 1 and provider.is_required():
511
            return False
512

    
513
        return True
514

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

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

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

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

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

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

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

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

    
559
        pending.delete()
560
        return provider
561

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

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

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

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

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

    
585
    def get_auth_providers(self):
586
        return self.auth_providers.all()
587

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

    
597
        return providers
598

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

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

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

    
635
        return mark_safe(message + u' '+ msg_extra)
636

    
637

    
638
class AstakosUserAuthProviderManager(models.Manager):
639

    
640
    def active(self, **filters):
641
        return self.filter(active=True, **filters)
642

    
643

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

    
662
    objects = AstakosUserAuthProviderManager()
663

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

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

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

    
680

    
681
    @property
682
    def settings(self):
683
        return auth_providers.get_provider(self.module)
684

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

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

    
705
    def can_remove(self):
706
        return self.user.can_remove_auth_provider(self.module)
707

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

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

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

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

    
729

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

    
757
    update_or_create = _update_or_create
758

    
759

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

    
767
    class Meta:
768
        unique_together = ("resource", "user")
769

    
770

    
771
class ApprovalTerms(models.Model):
772
    """
773
    Model for approval terms
774
    """
775

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

    
780

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

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

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

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

    
807

    
808
class EmailChangeManager(models.Manager):
809

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

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

819
        If the key is not valid or has expired, return ``None``.
820

821
        If the key is valid but the ``User`` is already active,
822
        return ``None``.
823

824
        After successful email change the activation record is deleted.
825

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

    
854

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

    
865
    objects = EmailChangeManager()
866

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

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

    
875

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

    
883

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

    
893

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

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

    
917
    class Meta:
918
        unique_together = ("provider", "third_party_identifier")
919

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

    
929
        return user
930

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

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

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

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

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

    
964

    
965
### PROJECTS ###
966
################
967

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

    
972
    def __str__(self):
973
        return self.policy
974

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

    
979
    def __str__(self):
980
        return self.policy
981

    
982
def synced_model_metaclass(class_name, class_parents, class_attributes):
983

    
984
    new_attributes = {}
985
    sync_attributes = {}
986

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

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

    
999
    prefix = sync_attributes.pop('prefix')
1000
    class_name = sync_attributes.pop('classname', prefix + '_model')
1001

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

    
1010
        new_attributes[newname] = value
1011

    
1012
    newclass = type(class_name, class_parents, new_attributes)
1013
    return newclass
1014

    
1015

    
1016
def make_synced(prefix='sync', name='SyncedState'):
1017

    
1018
    the_name = name
1019
    the_prefix = prefix
1020

    
1021
    class SyncedState(models.Model):
1022

    
1023
        sync_classname      = the_name
1024
        sync_prefix         = the_prefix
1025
        __metaclass__       = synced_model_metaclass
1026

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

    
1033
        class Meta:
1034
            abstract = True
1035

    
1036
        class NotSynced(Exception):
1037
            pass
1038

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

    
1044
        def sync_get_status(self):
1045
            return self.sync_status
1046

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

    
1053
        def sync_set_synced(self):
1054
            self.sync_synced_state = self.sync_new_state
1055
            self.sync_status = self.STATUS_SYNCED
1056

    
1057
        def sync_get_synced_state(self):
1058
            return self.sync_synced_state
1059

    
1060
        def sync_set_new_state(self, new_state):
1061
            self.sync_new_state = new_state
1062
            self.sync_set_status()
1063

    
1064
        def sync_get_new_state(self):
1065
            return self.sync_new_state
1066

    
1067
        def sync_set_synced_state(self, synced_state):
1068
            self.sync_synced_state = synced_state
1069
            self.sync_set_status()
1070

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

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

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

    
1085
        def sync_is_synced(self):
1086
            state, verified = self.sync_verify_get_synced_state()
1087
            return verified
1088

    
1089
    return SyncedState
1090

    
1091
SyncedState = make_synced(prefix='sync', name='SyncedState')
1092

    
1093

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

    
1101
    state                   =   models.CharField(max_length=80,
1102
                                                default=UNKNOWN)
1103

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

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

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

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

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

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

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

    
1162
    def submit(self, resource_policies, applicant, comments,
1163
               precursor_application=None):
1164

    
1165
        if precursor_application:
1166
            self.precursor_application = precursor_application
1167
            self.owner = precursor_application.owner
1168
        else:
1169
            self.owner = applicant
1170

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

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

    
1189
        return None
1190

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

1196
        Raises:
1197
            PermissionDenied
1198
        """
1199

    
1200
        if not transaction.is_managed():
1201
            raise AssertionError("NOPE")
1202

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

    
1209
        now = datetime.now()
1210
        project = self._get_project()
1211

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

    
1224
        new_project = False
1225
        if project is None:
1226
            new_project = True
1227
            project = Project(creation_date=now)
1228

    
1229
        project.name = new_project_name
1230
        project.application = self
1231
        project.last_approval_date = now
1232
        project.save()
1233

    
1234
        if new_project:
1235
            project.add_member(self.owner)
1236

    
1237
        # This will block while syncing,
1238
        # but unblock before setting the membership state.
1239
        # See ProjectMembership.set_sync()
1240
        project.set_membership_pending_sync()
1241

    
1242
        precursor = self.precursor_application
1243
        while precursor:
1244
            precursor.state = self.REPLACED
1245
            precursor.save()
1246
            precursor = precursor.precursor_application
1247

    
1248
        self.state = self.APPROVED
1249
        self.save()
1250

    
1251

    
1252
class ProjectResourceGrant(models.Model):
1253

    
1254
    resource                =   models.ForeignKey(Resource)
1255
    project_application     =   models.ForeignKey(ProjectApplication,
1256
                                                  null=True)
1257
    project_capacity        =   models.BigIntegerField(null=True)
1258
    project_import_limit    =   models.BigIntegerField(null=True)
1259
    project_export_limit    =   models.BigIntegerField(null=True)
1260
    member_capacity         =   models.BigIntegerField(null=True)
1261
    member_import_limit     =   models.BigIntegerField(null=True)
1262
    member_export_limit     =   models.BigIntegerField(null=True)
1263

    
1264
    objects = ExtendedManager()
1265

    
1266
    class Meta:
1267
        unique_together = ("resource", "project_application")
1268

    
1269

    
1270
class Project(models.Model):
1271

    
1272
    application                 =   models.OneToOneField(
1273
                                            ProjectApplication,
1274
                                            related_name='project')
1275
    last_approval_date          =   models.DateTimeField(null=True)
1276

    
1277
    members                     =   models.ManyToManyField(
1278
                                            AstakosUser,
1279
                                            through='ProjectMembership')
1280

    
1281
    termination_start_date      =   models.DateTimeField(null=True)
1282
    termination_date            =   models.DateTimeField(null=True)
1283

    
1284
    creation_date               =   models.DateTimeField()
1285
    name                        =   models.CharField(
1286
                                            max_length=80,
1287
                                            db_index=True,
1288
                                            unique=True)
1289

    
1290
    @property
1291
    def violated_resource_grants(self):
1292
        return False
1293

    
1294
    @property
1295
    def violated_members_number_limit(self):
1296
        application = self.application
1297
        return len(self.approved_members) > application.limit_on_members_number
1298

    
1299
    @property
1300
    def is_terminated(self):
1301
        return bool(self.termination_date)
1302

    
1303
    @property
1304
    def is_still_approved(self):
1305
        return bool(self.last_approval_date)
1306

    
1307
    @property
1308
    def is_active(self):
1309
        if (self.is_terminated or
1310
            not self.is_still_approved or
1311
            self.violated_resource_grants):
1312
            return False
1313
#         if self.violated_members_number_limit:
1314
#             return False
1315
        return True
1316

    
1317
    @property
1318
    def is_suspended(self):
1319
        if (self.is_terminated or
1320
            self.is_still_approved or
1321
            not self.violated_resource_grants):
1322
            return False
1323
#             if not self.violated_members_number_limit:
1324
#                 return False
1325
        return True
1326

    
1327
    @property
1328
    def is_alive(self):
1329
        return self.is_active or self.is_suspended
1330

    
1331
    @property
1332
    def is_inconsistent(self):
1333
        now = datetime.now()
1334
        if self.creation_date > now:
1335
            return True
1336
        if self.last_approval_date > now:
1337
            return True
1338
        if self.terminaton_date > now:
1339
            return True
1340
        return False
1341

    
1342
    @property
1343
    def approved_memberships(self):
1344
        ACCEPTED = ProjectMembership.ACCEPTED
1345
        PENDING  = ProjectMembership.PENDING
1346
        return self.projectmembership_set.filter(
1347
            Q(state=ACCEPTED) | Q(state=PENDING))
1348

    
1349
    @property
1350
    def approved_members(self):
1351
        return [m.person for m in self.approved_memberships]
1352

    
1353
    def set_membership_pending_sync(self):
1354
        ACCEPTED = ProjectMembership.ACCEPTED
1355
        PENDING  = ProjectMembership.PENDING
1356
        sfu = self.projectmembership_set.select_for_update()
1357
        members = sfu.filter(Q(state=ACCEPTED) | Q(state=PENDING))
1358

    
1359
        for member in members:
1360
            member.state = member.PENDING
1361
            member.save()
1362

    
1363
    def add_member(self, user):
1364
        """
1365
        Raises:
1366
            django.exceptions.PermissionDenied
1367
            astakos.im.models.AstakosUser.DoesNotExist
1368
        """
1369
        if isinstance(user, int):
1370
            user = AstakosUser.objects.get(user=user)
1371

    
1372
        m, created = ProjectMembership.objects.get_or_create(
1373
            person=user, project=self
1374
        )
1375
        m.accept()
1376

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

    
1387
        m = ProjectMembership.objects.get(person=user, project=self)
1388
        m.remove()
1389

    
1390
    def set_termination_start_date(self):
1391
        self.termination_start_date = datetime.now()
1392
        self.terminaton_date = None
1393
        self.save()
1394

    
1395
    def set_termination_date(self):
1396
        self.termination_start_date = None
1397
        self.termination_date = datetime.now()
1398
        self.save()
1399

    
1400

    
1401
class ProjectMembership(models.Model):
1402

    
1403
    person              =   models.ForeignKey(AstakosUser)
1404
    request_date        =   models.DateField(default=datetime.now())
1405
    project             =   models.ForeignKey(Project)
1406

    
1407
    state               =   models.IntegerField(default=0)
1408
    application         =   models.ForeignKey(
1409
                                ProjectApplication,
1410
                                null=True,
1411
                                related_name='memberships')
1412
    pending_application =   models.ForeignKey(
1413
                                ProjectApplication,
1414
                                null=True,
1415
                                related_name='pending_memebrships')
1416
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1417

    
1418
    acceptance_date     =   models.DateField(null=True, db_index=True)
1419
    leave_request_date  =   models.DateField(null=True)
1420

    
1421
    objects     =   ForUpdateManager()
1422

    
1423
    REQUESTED   =   0
1424
    PENDING     =   1
1425
    ACCEPTED    =   2
1426
    REMOVING    =   3
1427
    REMOVED     =   4
1428

    
1429
    class Meta:
1430
        unique_together = ("person", "project")
1431
        #index_together = [["project", "state"]]
1432

    
1433
    def __str__(self):
1434
        return _("<'%s' membership in project '%s'>") % (
1435
                self.person.username, self.project.application)
1436

    
1437
    __repr__ = __str__
1438

    
1439
    def __init__(self, *args, **kwargs):
1440
        self.state = self.REQUESTED
1441
        super(ProjectMembership, self).__init__(*args, **kwargs)
1442

    
1443
    def _set_history_item(self, reason, date=None):
1444
        if isinstance(reason, basestring):
1445
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1446

    
1447
        history_item = ProjectMembershipHistory(
1448
                            serial=self.id,
1449
                            person=self.person,
1450
                            project=self.project,
1451
                            date=date or datetime.now(),
1452
                            reason=reason)
1453
        history_item.save()
1454
        serial = history_item.id
1455

    
1456
    def accept(self):
1457
        state = self.state
1458
        if state != self.REQUESTED:
1459
            m = _("%s: attempt to accept in state [%s]") % (self, state)
1460
            raise AssertionError(m)
1461

    
1462
        now = datetime.now()
1463
        self.acceptance_date = now
1464
        self._set_history_item(reason='ACCEPT', date=now)
1465
        self.state = self.PENDING
1466
        self.save()
1467

    
1468
    def remove(self):
1469
        state = self.state
1470
        if state != self.ACCEPTED:
1471
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1472
            raise AssertionError(m)
1473

    
1474
        self._set_history_item(reason='REMOVE')
1475
        self.state = self.REMOVING
1476
        self.save()
1477

    
1478
    def reject(self):
1479
        state = self.state
1480
        if state != self.REQUESTED:
1481
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1482
            raise AssertionError(m)
1483

    
1484
        # rejected requests don't need sync,
1485
        # because they were never effected
1486
        self._set_history_item(reason='REJECT')
1487
        self.delete()
1488

    
1489
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1490
        if sub_list is None:
1491
            sub_list = []
1492

    
1493
        if add_list is None:
1494
            add_list = []
1495

    
1496
        sub_append = sub_list.append
1497
        add_append = add_list.append
1498
        holder = self.person.uuid
1499

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

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

    
1521
        return (sub_list, add_list)
1522

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

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

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

    
1557
class Serial(models.Model):
1558
    serial  =   models.AutoField(primary_key=True)
1559

    
1560
def new_serial():
1561
    s = Serial.objects.create()
1562
    serial = s.serial
1563
    s.delete()
1564
    return serial
1565

    
1566
def sync_finish_serials(serials_to_ack=None):
1567
    if serials_to_ack is None:
1568
        serials_to_ack = qh_query_serials([])
1569

    
1570
    serials_to_ack = set(serials_to_ack)
1571
    sfu = ProjectMembership.objects.select_for_update()
1572
    memberships = list(sfu.filter(pending_serial__isnull=False))
1573

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

    
1584
        transaction.commit()
1585

    
1586
    qh_ack_serials(list(serials_to_ack))
1587
    return len(memberships)
1588

    
1589
def sync_projects():
1590
    sync_finish_serials()
1591

    
1592
    PENDING = ProjectMembership.PENDING
1593
    REMOVING = ProjectMembership.REMOVING
1594
    objects = ProjectMembership.objects.select_for_update()
1595

    
1596
    sub_quota, add_quota = [], []
1597

    
1598
    serial = new_serial()
1599

    
1600
    pending = objects.filter(state=PENDING)
1601
    for membership in pending:
1602

    
1603
        if membership.pending_application:
1604
            m = "%s: impossible: pending_application is not None (%s)" % (
1605
                membership, membership.pending_application)
1606
            raise AssertionError(m)
1607
        if membership.pending_serial:
1608
            m = "%s: impossible: pending_serial is not None (%s)" % (
1609
                membership, membership.pending_serial)
1610
            raise AssertionError(m)
1611

    
1612
        membership.pending_application = membership.project.application
1613
        membership.pending_serial = serial
1614
        membership.get_diff_quotas(sub_quota, add_quota)
1615
        membership.save()
1616

    
1617
    removing = objects.filter(state=REMOVING)
1618
    for membership in removing:
1619

    
1620
        if membership.pending_application:
1621
            m = ("%s: impossible: removing pending_application is not None (%s)"
1622
                % (membership, membership.pending_application))
1623
            raise AssertionError(m)
1624
        if membership.pending_serial:
1625
            m = "%s: impossible: pending_serial is not None (%s)" % (
1626
                membership, membership.pending_serial)
1627
            raise AssertionError(m)
1628

    
1629
        membership.pending_serial = serial
1630
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1631
        membership.save()
1632

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

    
1639
    r = qh_add_quota(serial, sub_quota, add_quota)
1640
    if r:
1641
        m = "cannot sync serial: %d" % serial
1642
        raise RuntimeError(m)
1643

    
1644
    sync_finish_serials([serial])
1645

    
1646

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

    
1661
            retries -= 1
1662
            if retries <= 0:
1663
                return False
1664
            sleep(retry_wait)
1665

    
1666
        transaction.commit()
1667
        sync_projects()
1668
        return True
1669

    
1670
    finally:
1671
        if locked:
1672
            cursor.execute("SELECT pg_advisory_unlock(1)")
1673
            cursor.fetchall()
1674

    
1675

    
1676
class ProjectMembershipHistory(models.Model):
1677
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1678
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1679

    
1680
    person  =   models.ForeignKey(AstakosUser)
1681
    project =   models.ForeignKey(Project)
1682
    date    =   models.DateField(default=datetime.now)
1683
    reason  =   models.IntegerField()
1684
    serial  =   models.BigIntegerField()
1685

    
1686
### SIGNALS ###
1687
################
1688

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

    
1701

    
1702
def fix_superusers(sender, **kwargs):
1703
    # Associate superusers with AstakosUser
1704
    admins = User.objects.filter(is_superuser=True)
1705
    for u in admins:
1706
        create_astakos_user(u)
1707
post_syncdb.connect(fix_superusers)
1708

    
1709

    
1710
def user_post_save(sender, instance, created, **kwargs):
1711
    if not created:
1712
        return
1713
    create_astakos_user(instance)
1714
post_save.connect(user_post_save, sender=User)
1715

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

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

    
1741

    
1742
def resource_post_save(sender, instance, created, **kwargs):
1743
    if not created:
1744
        return
1745
    register_resources((instance,))
1746
post_save.connect(resource_post_save, sender=Resource)
1747

    
1748

    
1749
def renew_token(sender, instance, **kwargs):
1750
    if not instance.auth_token:
1751
        instance.renew_token()
1752
pre_save.connect(renew_token, sender=AstakosUser)
1753
pre_save.connect(renew_token, sender=Service)
1754