Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (62.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 ProjectApplicationManager(models.Manager):
1095

    
1096
    def user_projects(self, user):
1097
        """
1098
        Return projects accessed by specified user.
1099
        """
1100
        return self.filter(Q(owner=user) | Q(applicant=user) | \
1101
                        Q(project__in=user.projectmembership_set.filter()))
1102

    
1103
    def search_by_name(self, *search_strings):
1104
        q = Q()
1105
        for s in search_strings:
1106
            q = q | Q(name__icontains=s)
1107
        return self.filter(q)
1108

    
1109

    
1110
class ProjectApplication(models.Model):
1111
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1112
    applicant               =   models.ForeignKey(
1113
                                    AstakosUser,
1114
                                    related_name='projects_applied',
1115
                                    db_index=True)
1116

    
1117
    state                   =   models.CharField(max_length=80,
1118
                                                default=UNKNOWN)
1119

    
1120
    owner                   =   models.ForeignKey(
1121
                                    AstakosUser,
1122
                                    related_name='projects_owned',
1123
                                    db_index=True)
1124

    
1125
    precursor_application   =   models.OneToOneField('ProjectApplication',
1126
                                                     null=True,
1127
                                                     blank=True,
1128
                                                     db_index=True)
1129

    
1130
    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 ",)
1131
    homepage                =   models.URLField(max_length=255, null=True,
1132
                                                blank=True,help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",)
1133
    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. ")
1134
    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.")
1135
    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.  ")
1136
    member_join_policy      =   models.ForeignKey(MemberJoinPolicy)
1137
    member_leave_policy     =   models.ForeignKey(MemberLeavePolicy)
1138
    limit_on_members_number =   models.PositiveIntegerField(null=True,
1139
                                                            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. ")
1140
    resource_grants         =   models.ManyToManyField(
1141
                                    Resource,
1142
                                    null=True,
1143
                                    blank=True,
1144
                                    through='ProjectResourceGrant')
1145
    comments                =   models.TextField(null=True, blank=True)
1146
    issue_date              =   models.DateTimeField()
1147

    
1148
    objects                 =   ProjectApplicationManager()
1149

    
1150
    def add_resource_policy(self, service, resource, uplimit):
1151
        """Raises ObjectDoesNotExist, IntegrityError"""
1152
        q = self.projectresourcegrant_set
1153
        resource = Resource.objects.get(service__name=service, name=resource)
1154
        q.create(resource=resource, member_capacity=uplimit)
1155

    
1156
    def member_status(self, user):
1157
        if user == self.owner:
1158
            status = 100
1159
        else:
1160
            try:
1161
                membership = self.project.projectmembership_set.get(person=user)
1162
                status = membership.state
1163
            except Project.DoesNotExist:
1164
                status = -1
1165
            except ProjectMembership.DoesNotExist:
1166
                status = -1
1167

    
1168
        return status
1169

    
1170
    def members_count(self):
1171
        return self.project.approved_memberships.count()
1172

    
1173
    @property
1174
    def grants(self):
1175
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1176

    
1177
    @property
1178
    def resource_policies(self):
1179
        return self.projectresourcegrant_set.all()
1180

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

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

    
1196
    def submit(self, resource_policies, applicant, comments,
1197
               precursor_application=None):
1198

    
1199
        if precursor_application:
1200
            self.precursor_application = precursor_application
1201
            self.owner = precursor_application.owner
1202
        else:
1203
            self.owner = applicant
1204

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

    
1213
    def _get_project(self):
1214
        precursor = self
1215
        while precursor:
1216
            try:
1217
                project = precursor.project
1218
                return project
1219
            except Project.DoesNotExist:
1220
                pass
1221
            precursor = precursor.precursor_application
1222

    
1223
        return None
1224

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

1230
        Raises:
1231
            PermissionDenied
1232
        """
1233

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

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

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

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

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

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

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

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

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

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

    
1285

    
1286
class ProjectResourceGrant(models.Model):
1287

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

    
1298
    objects = ExtendedManager()
1299

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

    
1303

    
1304
class Project(models.Model):
1305

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

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

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

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

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

    
1328
    @property
1329
    def violated_members_number_limit(self):
1330
        application = self.application
1331
        return len(self.approved_members) > application.limit_on_members_number
1332

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1434

    
1435
class ProjectMembership(models.Model):
1436

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

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

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

    
1455
    objects     =   ForUpdateManager()
1456

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

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

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

    
1471
    __repr__ = __str__
1472

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1555
        return (sub_list, add_list)
1556

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

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

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

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

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

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

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

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

    
1618
        transaction.commit()
1619

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

    
1623
def sync_projects():
1624
    sync_finish_serials()
1625

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

    
1630
    sub_quota, add_quota = [], []
1631

    
1632
    serial = new_serial()
1633

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

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

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

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

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

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

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

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

    
1678
    sync_finish_serials([serial])
1679

    
1680

    
1681
def trigger_sync(retries=3, retry_wait=1.0):
1682
    cursor = connection.cursor()
1683
    locked = True
1684
    try:
1685
        while 1:
1686
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1687
            r = cursor.fetchone()
1688
            if r is None:
1689
                m = "Impossible"
1690
                raise AssertionError(m)
1691
            locked = r[0]
1692
            if locked:
1693
                break
1694

    
1695
            retries -= 1
1696
            if retries <= 0:
1697
                return False
1698
            sleep(retry_wait)
1699

    
1700
        transaction.commit()
1701
        sync_projects()
1702
        return True
1703

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

    
1709

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

    
1714
    person  =   models.ForeignKey(AstakosUser)
1715
    project =   models.ForeignKey(Project)
1716
    date    =   models.DateField(default=datetime.now)
1717
    reason  =   models.IntegerField()
1718
    serial  =   models.BigIntegerField()
1719

    
1720
### SIGNALS ###
1721
################
1722

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

    
1735

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

    
1743

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

    
1750
def astakosuser_pre_save(sender, instance, **kwargs):
1751
    instance.aquarium_report = False
1752
    instance.new = False
1753
    try:
1754
        db_instance = AstakosUser.objects.get(id=instance.id)
1755
    except AstakosUser.DoesNotExist:
1756
        # create event
1757
        instance.aquarium_report = True
1758
        instance.new = True
1759
    else:
1760
        get = AstakosUser.__getattribute__
1761
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1762
                   BILLING_FIELDS)
1763
        instance.aquarium_report = True if l else False
1764
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1765

    
1766
def astakosuser_post_save(sender, instance, created, **kwargs):
1767
    if instance.aquarium_report:
1768
        report_user_event(instance, create=instance.new)
1769
    if not created:
1770
        return
1771
    # TODO handle socket.error & IOError
1772
    register_users((instance,))
1773
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1774

    
1775

    
1776
def resource_post_save(sender, instance, created, **kwargs):
1777
    if not created:
1778
        return
1779
    register_resources((instance,))
1780
post_save.connect(resource_post_save, sender=Resource)
1781

    
1782

    
1783
def renew_token(sender, instance, **kwargs):
1784
    if not instance.auth_token:
1785
        instance.renew_token()
1786
pre_save.connect(renew_token, sender=AstakosUser)
1787
pre_save.connect(renew_token, sender=Service)
1788