Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (62.5 kB)

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

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

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, BILLING_FIELDS,
70
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
71
    SITENAME, SERVICES, MODERATION_ENABLED)
72
from astakos.im import settings as astakos_settings
73
from astakos.im.endpoints.qh import (
74
    register_users, 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 ProjectMembership.DoesNotExist:
1164
                status = -1
1165

    
1166
        return status
1167

    
1168
    def members_count(self):
1169
        return self.project.approved_memberships.count()
1170

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

    
1175
    @property
1176
    def resource_policies(self):
1177
        return self.projectresourcegrant_set.all()
1178

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

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

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

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

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

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

    
1221
        return None
1222

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

1228
        Raises:
1229
            PermissionDenied
1230
        """
1231

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

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

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

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

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

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

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

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

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

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

    
1283

    
1284
class ProjectResourceGrant(models.Model):
1285

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

    
1296
    objects = ExtendedManager()
1297

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

    
1301

    
1302
class Project(models.Model):
1303

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

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

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

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

    
1322
    @property
1323
    def violated_resource_grants(self):
1324
        return False
1325

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

    
1331
    @property
1332
    def is_terminated(self):
1333
        return bool(self.termination_date)
1334

    
1335
    @property
1336
    def is_still_approved(self):
1337
        return bool(self.last_approval_date)
1338

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

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

    
1359
    @property
1360
    def is_alive(self):
1361
        return self.is_active or self.is_suspended
1362

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

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

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

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

    
1391
        for member in members:
1392
            member.state = member.PENDING
1393
            member.save()
1394

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

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

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

    
1419
        m = ProjectMembership.objects.get(person=user, project=self)
1420
        m.remove()
1421

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

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

    
1432

    
1433
class ProjectMembership(models.Model):
1434

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

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

    
1450
    acceptance_date     =   models.DateField(null=True, db_index=True)
1451
    leave_request_date  =   models.DateField(null=True)
1452

    
1453
    objects     =   ForUpdateManager()
1454

    
1455
    REQUESTED   =   0
1456
    PENDING     =   1
1457
    ACCEPTED    =   2
1458
    REMOVING    =   3
1459
    REMOVED     =   4
1460

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

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

    
1469
    __repr__ = __str__
1470

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

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

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

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

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

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

    
1506
        self._set_history_item(reason='REMOVE')
1507
        self.state = self.REMOVING
1508
        self.save()
1509

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

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

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

    
1525
        if add_list is None:
1526
            add_list = []
1527

    
1528
        sub_append = sub_list.append
1529
        add_append = add_list.append
1530
        holder = self.person.uuid
1531

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

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

    
1553
        return (sub_list, add_list)
1554

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

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

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

    
1589
class Serial(models.Model):
1590
    serial  =   models.AutoField(primary_key=True)
1591

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

    
1598
def sync_finish_serials(serials_to_ack=None):
1599
    if serials_to_ack is None:
1600
        serials_to_ack = qh_query_serials([])
1601

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

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

    
1616
        transaction.commit()
1617

    
1618
    qh_ack_serials(list(serials_to_ack))
1619
    return len(memberships)
1620

    
1621
def sync_projects():
1622
    sync_finish_serials()
1623

    
1624
    PENDING = ProjectMembership.PENDING
1625
    REMOVING = ProjectMembership.REMOVING
1626
    objects = ProjectMembership.objects.select_for_update()
1627

    
1628
    sub_quota, add_quota = [], []
1629

    
1630
    serial = new_serial()
1631

    
1632
    pending = objects.filter(state=PENDING)
1633
    for membership in pending:
1634

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

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

    
1649
    removing = objects.filter(state=REMOVING)
1650
    for membership in removing:
1651

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

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

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

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

    
1676
    sync_finish_serials([serial])
1677

    
1678

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

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

    
1698
        transaction.commit()
1699
        sync_projects()
1700
        return True
1701

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

    
1707

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

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

    
1718
### SIGNALS ###
1719
################
1720

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

    
1733

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

    
1741

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

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

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

    
1773

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

    
1780

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