Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 3c7528c9

History | View | Annotate | Download (66.4 kB)

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

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

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

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

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

    
67
from astakos.im.settings import (
68
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
70
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA)
71
from astakos.im import settings as astakos_settings
72
from astakos.im.endpoints.qh import (
73
    register_users, register_resources, qh_add_quota, QuotaLimits,
74
    qh_query_serials, qh_ack_serials)
75
from astakos.im import auth_providers
76

    
77
import astakos.im.messages as astakos_messages
78
from .managers import ForUpdateManager
79

    
80
logger = logging.getLogger(__name__)
81

    
82
DEFAULT_CONTENT_TYPE = None
83
_content_type = None
84

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

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

    
97
RESOURCE_SEPARATOR = '.'
98

    
99
inf = float('inf')
100

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

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

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

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

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

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

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

    
147

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

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

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

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

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

    
176
    @property
177
    def help_text(self):
178
        return get_presentation(str(self)).get('help_text', '')
179

    
180
    @property
181
    def help_text_input_each(self):
182
        return get_presentation(str(self)).get('help_text_input_each', '')
183

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

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

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

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

    
200

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

    
215
class AstakosUserManager(UserManager):
216

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

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

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

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

    
240

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

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

    
257

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

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

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

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

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

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

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

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

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

    
293
    objects = AstakosUserManager()
294

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
408
        self.update_uuid()
409

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

    
414
        self.validate_unique_email_isactive()
415

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
530
        return True
531

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

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

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

    
543
        return True
544

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

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

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

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

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

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

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

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

    
589
        pending.delete()
590
        return provider
591

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

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

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

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

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

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

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

    
627
        return providers
628

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

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

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

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

    
667
    def owns_project(self, project):
668
        return project.user_status(self) == 100
669

    
670
    def is_project_member(self, project):
671
        return project.user_status(self) in [0,1,2,3]
672

    
673
    def is_project_accepted_member(self, project):
674
        return project.user_status(self) == 2
675

    
676

    
677
class AstakosUserAuthProviderManager(models.Manager):
678

    
679
    def active(self, **filters):
680
        return self.filter(active=True, **filters)
681

    
682

    
683
class AstakosUserAuthProvider(models.Model):
684
    """
685
    Available user authentication methods.
686
    """
687
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
688
                                   null=True, default=None)
689
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
690
    module = models.CharField(_('Provider'), max_length=255, blank=False,
691
                                default='local')
692
    identifier = models.CharField(_('Third-party identifier'),
693
                                              max_length=255, null=True,
694
                                              blank=True)
695
    active = models.BooleanField(default=True)
696
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
697
                                   default='astakos')
698
    info_data = models.TextField(default="", null=True, blank=True)
699
    created = models.DateTimeField('Creation date', auto_now_add=True)
700

    
701
    objects = AstakosUserAuthProviderManager()
702

    
703
    class Meta:
704
        unique_together = (('identifier', 'module', 'user'), )
705
        ordering = ('module', 'created')
706

    
707
    def __init__(self, *args, **kwargs):
708
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
709
        try:
710
            self.info = json.loads(self.info_data)
711
            if not self.info:
712
                self.info = {}
713
        except Exception, e:
714
            self.info = {}
715

    
716
        for key,value in self.info.iteritems():
717
            setattr(self, 'info_%s' % key, value)
718

    
719

    
720
    @property
721
    def settings(self):
722
        return auth_providers.get_provider(self.module)
723

    
724
    @property
725
    def details_display(self):
726
        try:
727
          return self.settings.get_details_tpl_display % self.__dict__
728
        except:
729
          return ''
730

    
731
    @property
732
    def title_display(self):
733
        title_tpl = self.settings.get_title_display
734
        try:
735
            if self.settings.get_user_title_display:
736
                title_tpl = self.settings.get_user_title_display
737
        except Exception, e:
738
            pass
739
        try:
740
          return title_tpl % self.__dict__
741
        except:
742
          return self.settings.get_title_display % self.__dict__
743

    
744
    def can_remove(self):
745
        return self.user.can_remove_auth_provider(self.module)
746

    
747
    def delete(self, *args, **kwargs):
748
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
749
        if self.module == 'local':
750
            self.user.set_unusable_password()
751
            self.user.save()
752
        return ret
753

    
754
    def __repr__(self):
755
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
756

    
757
    def __unicode__(self):
758
        if self.identifier:
759
            return "%s:%s" % (self.module, self.identifier)
760
        if self.auth_backend:
761
            return "%s:%s" % (self.module, self.auth_backend)
762
        return self.module
763

    
764
    def save(self, *args, **kwargs):
765
        self.info_data = json.dumps(self.info)
766
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
767

    
768

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

    
796
    update_or_create = _update_or_create
797

    
798

    
799
class AstakosUserQuota(models.Model):
800
    objects = ExtendedManager()
801
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
802
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
803
    resource = models.ForeignKey(Resource)
804
    user = models.ForeignKey(AstakosUser)
805

    
806
    class Meta:
807
        unique_together = ("resource", "user")
808

    
809

    
810
class ApprovalTerms(models.Model):
811
    """
812
    Model for approval terms
813
    """
814

    
815
    date = models.DateTimeField(
816
        _('Issue date'), db_index=True, default=datetime.now())
817
    location = models.CharField(_('Terms location'), max_length=255)
818

    
819

    
820
class Invitation(models.Model):
821
    """
822
    Model for registring invitations
823
    """
824
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
825
                                null=True)
826
    realname = models.CharField(_('Real name'), max_length=255)
827
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
828
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
829
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
830
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
831
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
832

    
833
    def __init__(self, *args, **kwargs):
834
        super(Invitation, self).__init__(*args, **kwargs)
835
        if not self.id:
836
            self.code = _generate_invitation_code()
837

    
838
    def consume(self):
839
        self.is_consumed = True
840
        self.consumed = datetime.now()
841
        self.save()
842

    
843
    def __unicode__(self):
844
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
845

    
846

    
847
class EmailChangeManager(models.Manager):
848

    
849
    @transaction.commit_on_success
850
    def change_email(self, activation_key):
851
        """
852
        Validate an activation key and change the corresponding
853
        ``User`` if valid.
854

855
        If the key is valid and has not expired, return the ``User``
856
        after activating.
857

858
        If the key is not valid or has expired, return ``None``.
859

860
        If the key is valid but the ``User`` is already active,
861
        return ``None``.
862

863
        After successful email change the activation record is deleted.
864

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

    
893

    
894
class EmailChange(models.Model):
895
    new_email_address = models.EmailField(
896
        _(u'new e-mail address'),
897
        help_text=_('Your old email address will be used until you verify your new one.'))
898
    user = models.ForeignKey(
899
        AstakosUser, unique=True, related_name='emailchanges')
900
    requested_at = models.DateTimeField(default=datetime.now())
901
    activation_key = models.CharField(
902
        max_length=40, unique=True, db_index=True)
903

    
904
    objects = EmailChangeManager()
905

    
906
    def get_url(self):
907
        return reverse('email_change_confirm',
908
                      kwargs={'activation_key': self.activation_key})
909

    
910
    def activation_key_expired(self):
911
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
912
        return self.requested_at + expiration_date < datetime.now()
913

    
914

    
915
class AdditionalMail(models.Model):
916
    """
917
    Model for registring invitations
918
    """
919
    owner = models.ForeignKey(AstakosUser)
920
    email = models.EmailField()
921

    
922

    
923
def _generate_invitation_code():
924
    while True:
925
        code = randint(1, 2L ** 63 - 1)
926
        try:
927
            Invitation.objects.get(code=code)
928
            # An invitation with this code already exists, try again
929
        except Invitation.DoesNotExist:
930
            return code
931

    
932

    
933
def get_latest_terms():
934
    try:
935
        term = ApprovalTerms.objects.order_by('-id')[0]
936
        return term
937
    except IndexError:
938
        pass
939
    return None
940

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

    
956
    class Meta:
957
        unique_together = ("provider", "third_party_identifier")
958

    
959
    def get_user_instance(self):
960
        d = self.__dict__
961
        d.pop('_state', None)
962
        d.pop('id', None)
963
        d.pop('token', None)
964
        d.pop('created', None)
965
        d.pop('info', None)
966
        user = AstakosUser(**d)
967

    
968
        return user
969

    
970
    @property
971
    def realname(self):
972
        return '%s %s' %(self.first_name, self.last_name)
973

    
974
    @realname.setter
975
    def realname(self, value):
976
        parts = value.split(' ')
977
        if len(parts) == 2:
978
            self.first_name = parts[0]
979
            self.last_name = parts[1]
980
        else:
981
            self.last_name = parts[0]
982

    
983
    def save(self, **kwargs):
984
        if not self.id:
985
            # set username
986
            while not self.username:
987
                username =  uuid.uuid4().hex[:30]
988
                try:
989
                    AstakosUser.objects.get(username = username)
990
                except AstakosUser.DoesNotExist, e:
991
                    self.username = username
992
        super(PendingThirdPartyUser, self).save(**kwargs)
993

    
994
    def generate_token(self):
995
        self.password = self.third_party_identifier
996
        self.last_login = datetime.now()
997
        self.token = default_token_generator.make_token(self)
998

    
999
class SessionCatalog(models.Model):
1000
    session_key = models.CharField(_('session key'), max_length=40)
1001
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1002

    
1003

    
1004
### PROJECTS ###
1005
################
1006

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

    
1011
    def __str__(self):
1012
        return self.description.capitalize()
1013

    
1014
class MemberLeavePolicy(models.Model):
1015
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1016
    description = models.CharField(_('Description'), max_length=80)
1017

    
1018
    def __str__(self):
1019
        return self.description.capitalize()
1020

    
1021
def synced_model_metaclass(class_name, class_parents, class_attributes):
1022

    
1023
    new_attributes = {}
1024
    sync_attributes = {}
1025

    
1026
    for name, value in class_attributes.iteritems():
1027
        sync, underscore, rest = name.partition('_')
1028
        if sync == 'sync' and underscore == '_':
1029
            sync_attributes[rest] = value
1030
        else:
1031
            new_attributes[name] = value
1032

    
1033
    if 'prefix' not in sync_attributes:
1034
        m = ("you did not specify a 'sync_prefix' attribute "
1035
             "in class '%s'" % (class_name,))
1036
        raise ValueError(m)
1037

    
1038
    prefix = sync_attributes.pop('prefix')
1039
    class_name = sync_attributes.pop('classname', prefix + '_model')
1040

    
1041
    for name, value in sync_attributes.iteritems():
1042
        newname = prefix + '_' + name
1043
        if newname in new_attributes:
1044
            m = ("class '%s' was specified with prefix '%s' "
1045
                 "but it already has an attribute named '%s'"
1046
                 % (class_name, prefix, newname))
1047
            raise ValueError(m)
1048

    
1049
        new_attributes[newname] = value
1050

    
1051
    newclass = type(class_name, class_parents, new_attributes)
1052
    return newclass
1053

    
1054

    
1055
def make_synced(prefix='sync', name='SyncedState'):
1056

    
1057
    the_name = name
1058
    the_prefix = prefix
1059

    
1060
    class SyncedState(models.Model):
1061

    
1062
        sync_classname      = the_name
1063
        sync_prefix         = the_prefix
1064
        __metaclass__       = synced_model_metaclass
1065

    
1066
        sync_new_state      = models.BigIntegerField(null=True)
1067
        sync_synced_state   = models.BigIntegerField(null=True)
1068
        STATUS_SYNCED       = 0
1069
        STATUS_PENDING      = 1
1070
        sync_status         = models.IntegerField(db_index=True)
1071

    
1072
        class Meta:
1073
            abstract = True
1074

    
1075
        class NotSynced(Exception):
1076
            pass
1077

    
1078
        def sync_init_state(self, state):
1079
            self.sync_synced_state = state
1080
            self.sync_new_state = state
1081
            self.sync_status = self.STATUS_SYNCED
1082

    
1083
        def sync_get_status(self):
1084
            return self.sync_status
1085

    
1086
        def sync_set_status(self):
1087
            if self.sync_new_state != self.sync_synced_state:
1088
                self.sync_status = self.STATUS_PENDING
1089
            else:
1090
                self.sync_status = self.STATUS_SYNCED
1091

    
1092
        def sync_set_synced(self):
1093
            self.sync_synced_state = self.sync_new_state
1094
            self.sync_status = self.STATUS_SYNCED
1095

    
1096
        def sync_get_synced_state(self):
1097
            return self.sync_synced_state
1098

    
1099
        def sync_set_new_state(self, new_state):
1100
            self.sync_new_state = new_state
1101
            self.sync_set_status()
1102

    
1103
        def sync_get_new_state(self):
1104
            return self.sync_new_state
1105

    
1106
        def sync_set_synced_state(self, synced_state):
1107
            self.sync_synced_state = synced_state
1108
            self.sync_set_status()
1109

    
1110
        def sync_get_pending_objects(self):
1111
            kw = dict((the_prefix + '_status', self.STATUS_PENDING))
1112
            return self.objects.filter(**kw)
1113

    
1114
        def sync_get_synced_objects(self):
1115
            kw = dict((the_prefix + '_status', self.STATUS_SYNCED))
1116
            return self.objects.filter(**kw)
1117

    
1118
        def sync_verify_get_synced_state(self):
1119
            status = self.sync_get_status()
1120
            state = self.sync_get_synced_state()
1121
            verified = (status == self.STATUS_SYNCED)
1122
            return state, verified
1123

    
1124
        def sync_is_synced(self):
1125
            state, verified = self.sync_verify_get_synced_state()
1126
            return verified
1127

    
1128
    return SyncedState
1129

    
1130
SyncedState = make_synced(prefix='sync', name='SyncedState')
1131

    
1132

    
1133
class ProjectApplicationManager(ForUpdateManager):
1134

    
1135
    def user_projects(self, user):
1136
        """
1137
        Return projects accessed by specified user.
1138
        """
1139
        return self.filter(Q(owner=user) | Q(applicant=user) | \
1140
                        Q(project__projectmembership__person=user))
1141

    
1142
    def search_by_name(self, *search_strings):
1143
        q = Q()
1144
        for s in search_strings:
1145
            q = q | Q(name__icontains=s)
1146
        return self.filter(q)
1147

    
1148

    
1149
class ProjectApplication(models.Model):
1150
    PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
1151
    applicant               =   models.ForeignKey(
1152
                                    AstakosUser,
1153
                                    related_name='projects_applied',
1154
                                    db_index=True)
1155

    
1156
    state                   =   models.CharField(max_length=80,
1157
                                                default=UNKNOWN)
1158

    
1159
    owner                   =   models.ForeignKey(
1160
                                    AstakosUser,
1161
                                    related_name='projects_owned',
1162
                                    db_index=True)
1163

    
1164
    precursor_application   =   models.OneToOneField('ProjectApplication',
1165
                                                     null=True,
1166
                                                     blank=True,
1167
                                                     db_index=True)
1168

    
1169
    name                    =   models.CharField(max_length=80,
1170
                                                 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"))
1171
    homepage                =   models.URLField(max_length=255,
1172
                                                null=True,
1173
                                                blank=True,help_text=_("This should be a URL pointing at your project's site. e.g.: http://myproject.com ",))
1174
    description             =   models.TextField(null=True,
1175
                                                 blank=True,
1176
                                                 help_text=_("Please provide a short but descriptive abstract of your Project, so that anyone searching can quickly understand what this Project is about. "))
1177
    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."))
1178
    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.  "))
1179
    member_join_policy      =   models.ForeignKey(MemberJoinPolicy)
1180
    member_leave_policy     =   models.ForeignKey(MemberLeavePolicy)
1181
    limit_on_members_number =   models.PositiveIntegerField(null=True,
1182
                                                            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. "))
1183
    resource_grants         =   models.ManyToManyField(
1184
                                    Resource,
1185
                                    null=True,
1186
                                    blank=True,
1187
                                    through='ProjectResourceGrant')
1188
    comments                =   models.TextField(null=True, blank=True)
1189
    issue_date              =   models.DateTimeField()
1190

    
1191

    
1192
    objects                 =   ProjectApplicationManager()
1193

    
1194
    def add_resource_policy(self, service, resource, uplimit):
1195
        """Raises ObjectDoesNotExist, IntegrityError"""
1196
        q = self.projectresourcegrant_set
1197
        resource = Resource.objects.get(service__name=service, name=resource)
1198
        q.create(resource=resource, member_capacity=uplimit)
1199

    
1200
    def user_status(self, user):
1201
        """
1202
        100 OWNER
1203
        0   REQUESTED
1204
        1   PENDING
1205
        2   ACCEPTED
1206
        3   REMOVING
1207
        4   REMOVED
1208
       -1   User has no association with the project
1209
        """
1210
        if user == self.owner:
1211
            status = 100
1212
        else:
1213
            try:
1214
                membership = self.project.projectmembership_set.get(person=user)
1215
                status = membership.state
1216
            except Project.DoesNotExist:
1217
                status = -1
1218
            except ProjectMembership.DoesNotExist:
1219
                status = -1
1220

    
1221
        return status
1222

    
1223
    def members_count(self):
1224
        return self.project.approved_memberships.count()
1225

    
1226
    @property
1227
    def grants(self):
1228
        return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name')
1229

    
1230
    @property
1231
    def resource_policies(self):
1232
        return self.projectresourcegrant_set.all()
1233

    
1234
    @resource_policies.setter
1235
    def resource_policies(self, policies):
1236
        for p in policies:
1237
            service = p.get('service', None)
1238
            resource = p.get('resource', None)
1239
            uplimit = p.get('uplimit', 0)
1240
            self.add_resource_policy(service, resource, uplimit)
1241

    
1242
    @property
1243
    def follower(self):
1244
        try:
1245
            return ProjectApplication.objects.get(precursor_application=self)
1246
        except ProjectApplication.DoesNotExist:
1247
            return
1248

    
1249
    def submit(self, resource_policies, applicant, comments,
1250
               precursor_application=None):
1251

    
1252
        if precursor_application:
1253
            self.precursor_application = precursor_application
1254
            self.owner = precursor_application.owner
1255
        else:
1256
            self.owner = applicant
1257

    
1258
        self.id = None
1259
        self.applicant = applicant
1260
        self.comments = comments
1261
        self.issue_date = datetime.now()
1262
        self.state = self.PENDING
1263
        self.save()
1264
        self.resource_policies = resource_policies
1265

    
1266
    def _get_project(self):
1267
        precursor = self
1268
        while precursor:
1269
            try:
1270
                objects = Project.objects.select_for_update()
1271
                project = objects.get(application=precursor)
1272
                return project
1273
            except Project.DoesNotExist:
1274
                pass
1275
            precursor = precursor.precursor_application
1276

    
1277
        return None
1278

    
1279
    def approve(self, approval_user=None):
1280
        """
1281
        If approval_user then during owner membership acceptance
1282
        it is checked whether the request_user is eligible.
1283

1284
        Raises:
1285
            PermissionDenied
1286
        """
1287

    
1288
        if not transaction.is_managed():
1289
            raise AssertionError("NOPE")
1290

    
1291
        new_project_name = self.name
1292
        if self.state != self.PENDING:
1293
            m = _("cannot approve: project '%s' in state '%s'") % (
1294
                    new_project_name, self.state)
1295
            raise PermissionDenied(m) # invalid argument
1296

    
1297
        now = datetime.now()
1298
        project = self._get_project()
1299

    
1300
        try:
1301
            # needs SERIALIZABLE
1302
            conflicting_project = Project.objects.get(name=new_project_name)
1303
            if (conflicting_project.is_alive and
1304
                conflicting_project != project):
1305
                m = (_("cannot approve: project with name '%s' "
1306
                       "already exists (serial: %s)") % (
1307
                        new_project_name, conflicting_project.id))
1308
                raise PermissionDenied(m) # invalid argument
1309
        except Project.DoesNotExist:
1310
            pass
1311

    
1312
        new_project = False
1313
        if project is None:
1314
            new_project = True
1315
            project = Project(creation_date=now)
1316

    
1317
        project.name = new_project_name
1318
        project.application = self
1319
        project.last_approval_date = now
1320
        project.save()
1321

    
1322
        if new_project:
1323
            project.add_member(self.owner)
1324

    
1325
        # This will block while syncing,
1326
        # but unblock before setting the membership state.
1327
        # See ProjectMembership.set_sync()
1328
        project.set_membership_pending_sync()
1329

    
1330
        precursor = self.precursor_application
1331
        while precursor:
1332
            precursor.state = self.REPLACED
1333
            precursor.save()
1334
            precursor = precursor.precursor_application
1335

    
1336
        self.state = self.APPROVED
1337
        self.save()
1338

    
1339

    
1340
class ProjectResourceGrant(models.Model):
1341

    
1342
    resource                =   models.ForeignKey(Resource)
1343
    project_application     =   models.ForeignKey(ProjectApplication,
1344
                                                  null=True)
1345
    project_capacity        =   models.BigIntegerField(null=True)
1346
    project_import_limit    =   models.BigIntegerField(null=True)
1347
    project_export_limit    =   models.BigIntegerField(null=True)
1348
    member_capacity         =   models.BigIntegerField(null=True)
1349
    member_import_limit     =   models.BigIntegerField(null=True)
1350
    member_export_limit     =   models.BigIntegerField(null=True)
1351

    
1352
    objects = ExtendedManager()
1353

    
1354
    class Meta:
1355
        unique_together = ("resource", "project_application")
1356

    
1357

    
1358
class Project(models.Model):
1359

    
1360
    application                 =   models.OneToOneField(
1361
                                            ProjectApplication,
1362
                                            related_name='project')
1363
    last_approval_date          =   models.DateTimeField(null=True)
1364

    
1365
    members                     =   models.ManyToManyField(
1366
                                            AstakosUser,
1367
                                            through='ProjectMembership')
1368

    
1369
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1370
    deactivation_start_date     =   models.DateTimeField(null=True)
1371
    deactivation_date           =   models.DateTimeField(null=True)
1372

    
1373
    creation_date               =   models.DateTimeField()
1374
    name                        =   models.CharField(
1375
                                            max_length=80,
1376
                                            db_index=True,
1377
                                            unique=True)
1378

    
1379
    TERMINATED  =   'TERMINATED'
1380
    SUSPENDED   =   'SUSPENDED'
1381

    
1382
    objects     =   ForUpdateManager()
1383

    
1384
    def __str__(self):
1385
        return _("<project %s '%s'>") % (self.id, self.application.name)
1386

    
1387
    __repr__ = __str__
1388

    
1389
    def is_deactivating(self):
1390
        return bool(self.deactivation_start_date)
1391

    
1392
    def is_deactivated_synced(self):
1393
        return bool(self.deactivation_date)
1394

    
1395
    def is_deactivated(self):
1396
        return self.is_deactivated_synced() or self.is_deactivating()
1397

    
1398
    def is_still_approved(self):
1399
        return bool(self.last_approval_date)
1400

    
1401
    def is_active(self):
1402
        return not(self.is_deactivated())
1403

    
1404
    def is_inconsistent(self):
1405
        now = datetime.now()
1406
        dates = [self.creation_date,
1407
                 self.last_approval_date,
1408
                 self.deactivation_start_date,
1409
                 self.deactivation_date]
1410
        return any([date > now for date in dates])
1411

    
1412
    def set_deactivation_start_date(self):
1413
        self.deactivation_start_date = datetime.now()
1414

    
1415
    def set_deactivation_date(self):
1416
        self.deactivation_start_date = None
1417
        self.deactivation_date = datetime.now()
1418

    
1419
    def violates_resource_grants(self):
1420
        return False
1421

    
1422
    def violates_members_limit(self, adding=0):
1423
        application = self.application
1424
        return (len(self.approved_members) + adding >
1425
                application.limit_on_members_number)
1426

    
1427
    @property
1428
    def is_alive(self):
1429
        return self.is_active()
1430

    
1431
    @property
1432
    def approved_memberships(self):
1433
        query = ProjectMembership.query_approved()
1434
        return self.projectmembership_set.filter(query)
1435

    
1436
    @property
1437
    def approved_members(self):
1438
        return [m.person for m in self.approved_memberships]
1439

    
1440
    def set_membership_pending_sync(self):
1441
        query = ProjectMembership.query_approved()
1442
        sfu = self.projectmembership_set.select_for_update()
1443
        members = sfu.filter(query)
1444

    
1445
        for member in members:
1446
            member.state = member.PENDING
1447
            member.save()
1448

    
1449
    def add_member(self, user):
1450
        """
1451
        Raises:
1452
            django.exceptions.PermissionDenied
1453
            astakos.im.models.AstakosUser.DoesNotExist
1454
        """
1455
        if isinstance(user, int):
1456
            user = AstakosUser.objects.get(user=user)
1457

    
1458
        m, created = ProjectMembership.objects.get_or_create(
1459
            person=user, project=self
1460
        )
1461
        m.accept()
1462

    
1463
    def remove_member(self, user):
1464
        """
1465
        Raises:
1466
            django.exceptions.PermissionDenied
1467
            astakos.im.models.AstakosUser.DoesNotExist
1468
            astakos.im.models.ProjectMembership.DoesNotExist
1469
        """
1470
        if isinstance(user, int):
1471
            user = AstakosUser.objects.get(user=user)
1472

    
1473
        m = ProjectMembership.objects.get(person=user, project=self)
1474
        m.remove()
1475

    
1476
    def terminate(self):
1477
        self.set_deactivation_start_date()
1478
        self.deactivation_reason = self.TERMINATED
1479
        self.save()
1480

    
1481
    @property
1482
    def is_terminated(self):
1483
        return (self.is_deactivated() and
1484
                self.deactivation_reason == self.TERMINATED)
1485

    
1486
    @property
1487
    def is_suspended(self):
1488
        return False
1489

    
1490
class ProjectMembership(models.Model):
1491

    
1492
    person              =   models.ForeignKey(AstakosUser)
1493
    request_date        =   models.DateField(default=datetime.now())
1494
    project             =   models.ForeignKey(Project)
1495

    
1496
    state               =   models.IntegerField(default=0)
1497
    application         =   models.ForeignKey(
1498
                                ProjectApplication,
1499
                                null=True,
1500
                                related_name='memberships')
1501
    pending_application =   models.ForeignKey(
1502
                                ProjectApplication,
1503
                                null=True,
1504
                                related_name='pending_memebrships')
1505
    pending_serial      =   models.BigIntegerField(null=True, db_index=True)
1506

    
1507
    acceptance_date     =   models.DateField(null=True, db_index=True)
1508
    leave_request_date  =   models.DateField(null=True)
1509

    
1510
    objects     =   ForUpdateManager()
1511

    
1512
    REQUESTED   =   0
1513
    PENDING     =   1
1514
    ACCEPTED    =   2
1515
    REMOVING    =   3
1516
    REMOVED     =   4
1517
    INACTIVE    =   5
1518

    
1519
    APPROVED_SET    =   [PENDING, ACCEPTED, INACTIVE]
1520

    
1521
    @classmethod
1522
    def query_approved(cls):
1523
        return (Q(state=cls.PENDING) |
1524
                Q(state=cls.ACCEPTED) |
1525
                Q(state=cls.INACTIVE))
1526

    
1527
    class Meta:
1528
        unique_together = ("person", "project")
1529
        #index_together = [["project", "state"]]
1530

    
1531
    def __str__(self):
1532
        return _("<'%s' membership in '%s'>") % (
1533
                self.person.username, self.project)
1534

    
1535
    __repr__ = __str__
1536

    
1537
    def __init__(self, *args, **kwargs):
1538
        self.state = self.REQUESTED
1539
        super(ProjectMembership, self).__init__(*args, **kwargs)
1540

    
1541
    def _set_history_item(self, reason, date=None):
1542
        if isinstance(reason, basestring):
1543
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1544

    
1545
        history_item = ProjectMembershipHistory(
1546
                            serial=self.id,
1547
                            person=self.person.uuid,
1548
                            project=self.project_id,
1549
                            date=date or datetime.now(),
1550
                            reason=reason)
1551
        history_item.save()
1552
        serial = history_item.id
1553

    
1554
    def accept(self):
1555
        state = self.state
1556
        if state != self.REQUESTED:
1557
            m = _("%s: attempt to accept in state '%s'") % (self, state)
1558
            raise AssertionError(m)
1559

    
1560
        now = datetime.now()
1561
        self.acceptance_date = now
1562
        self._set_history_item(reason='ACCEPT', date=now)
1563
        self.state = (self.PENDING if self.project.is_active()
1564
                      else self.INACTIVE)
1565
        self.save()
1566

    
1567
    def remove(self):
1568
        state = self.state
1569
        if state not in [self.ACCEPTED, self.INACTIVE]:
1570
            m = _("%s: attempt to remove in state '%s'") % (self, state)
1571
            raise AssertionError(m)
1572

    
1573
        self._set_history_item(reason='REMOVE')
1574
        self.state = self.REMOVING
1575
        self.save()
1576

    
1577
    def reject(self):
1578
        state = self.state
1579
        if state != self.REQUESTED:
1580
            m = _("%s: attempt to reject in state '%s'") % (self, state)
1581
            raise AssertionError(m)
1582

    
1583
        # rejected requests don't need sync,
1584
        # because they were never effected
1585
        self._set_history_item(reason='REJECT')
1586
        self.delete()
1587

    
1588
    def get_diff_quotas(self, sub_list=None, add_list=None, remove=False):
1589
        if sub_list is None:
1590
            sub_list = []
1591

    
1592
        if add_list is None:
1593
            add_list = []
1594

    
1595
        sub_append = sub_list.append
1596
        add_append = add_list.append
1597
        holder = self.person.uuid
1598

    
1599
        synced_application = self.application
1600
        if synced_application is not None:
1601
            cur_grants = synced_application.projectresourcegrant_set.all()
1602
            for grant in cur_grants:
1603
                sub_append(QuotaLimits(
1604
                               holder       = holder,
1605
                               resource     = str(grant.resource),
1606
                               capacity     = grant.member_capacity,
1607
                               import_limit = grant.member_import_limit,
1608
                               export_limit = grant.member_export_limit))
1609

    
1610
        if not remove:
1611
            new_grants = self.pending_application.projectresourcegrant_set.all()
1612
            for new_grant in new_grants:
1613
                add_append(QuotaLimits(
1614
                               holder       = holder,
1615
                               resource     = str(new_grant.resource),
1616
                               capacity     = new_grant.member_capacity,
1617
                               import_limit = new_grant.member_import_limit,
1618
                               export_limit = new_grant.member_export_limit))
1619

    
1620
        return (sub_list, add_list)
1621

    
1622
    def set_sync(self):
1623
        state = self.state
1624
        if state == self.PENDING:
1625
            pending_application = self.pending_application
1626
            if pending_application is None:
1627
                m = _("%s: attempt to sync an empty pending application") % (
1628
                    self,)
1629
                raise AssertionError(m)
1630
            self.application = pending_application
1631
            self.pending_application = None
1632
            self.pending_serial = None
1633

    
1634
            # project.application may have changed in the meantime,
1635
            # in which case we stay PENDING;
1636
            # we are safe to check due to select_for_update
1637
            if self.application == self.project.application:
1638
                self.state = self.ACCEPTED
1639
            self.save()
1640
        elif state == self.ACCEPTED:
1641
            if self.pending_application:
1642
                m = _("%s: attempt to sync in state '%s' "
1643
                      "with a pending application") % (self, state)
1644
                raise AssertionError(m)
1645
            self.application = None
1646
            self.pending_serial = None
1647
            self.state = self.INACTIVE
1648
            self.save()
1649
        elif state == self.REMOVING:
1650
            self.delete()
1651
        else:
1652
            m = _("%s: attempt to sync in state '%s'") % (self, state)
1653
            raise AssertionError(m)
1654

    
1655
    def reset_sync(self):
1656
        state = self.state
1657
        if state in [self.PENDING, self.ACCEPTED, self.REMOVING]:
1658
            self.pending_application = None
1659
            self.pending_serial = None
1660
            self.save()
1661
        else:
1662
            m = _("%s: attempt to reset sync in state '%s'") % (self, state)
1663
            raise AssertionError(m)
1664

    
1665
class Serial(models.Model):
1666
    serial  =   models.AutoField(primary_key=True)
1667

    
1668
def new_serial():
1669
    s = Serial.objects.create()
1670
    serial = s.serial
1671
    s.delete()
1672
    return serial
1673

    
1674
def sync_finish_serials(serials_to_ack=None):
1675
    if serials_to_ack is None:
1676
        serials_to_ack = qh_query_serials([])
1677

    
1678
    serials_to_ack = set(serials_to_ack)
1679
    sfu = ProjectMembership.objects.select_for_update()
1680
    memberships = list(sfu.filter(pending_serial__isnull=False))
1681

    
1682
    if memberships:
1683
        for membership in memberships:
1684
            serial = membership.pending_serial
1685
            if serial in serials_to_ack:
1686
                membership.set_sync()
1687
            else:
1688
                membership.reset_sync()
1689

    
1690
        transaction.commit()
1691

    
1692
    qh_ack_serials(list(serials_to_ack))
1693
    return len(memberships)
1694

    
1695
def sync_all_projects():
1696
    sync_finish_serials()
1697

    
1698
    PENDING = ProjectMembership.PENDING
1699
    REMOVING = ProjectMembership.REMOVING
1700
    objects = ProjectMembership.objects.select_for_update()
1701

    
1702
    sub_quota, add_quota = [], []
1703

    
1704
    serial = new_serial()
1705

    
1706
    pending = objects.filter(state=PENDING)
1707
    for membership in pending:
1708

    
1709
        if membership.pending_application:
1710
            m = "%s: impossible: pending_application is not None (%s)" % (
1711
                membership, membership.pending_application)
1712
            raise AssertionError(m)
1713
        if membership.pending_serial:
1714
            m = "%s: impossible: pending_serial is not None (%s)" % (
1715
                membership, membership.pending_serial)
1716
            raise AssertionError(m)
1717

    
1718
        membership.pending_application = membership.project.application
1719
        membership.pending_serial = serial
1720
        membership.get_diff_quotas(sub_quota, add_quota)
1721
        membership.save()
1722

    
1723
    removing = objects.filter(state=REMOVING)
1724
    for membership in removing:
1725

    
1726
        if membership.pending_application:
1727
            m = ("%s: impossible: removing pending_application is not None (%s)"
1728
                % (membership, membership.pending_application))
1729
            raise AssertionError(m)
1730
        if membership.pending_serial:
1731
            m = "%s: impossible: pending_serial is not None (%s)" % (
1732
                membership, membership.pending_serial)
1733
            raise AssertionError(m)
1734

    
1735
        membership.pending_serial = serial
1736
        membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1737
        membership.save()
1738

    
1739
    transaction.commit()
1740
    # ProjectApplication.approve() unblocks here
1741
    # and can set PENDING an already PENDING membership
1742
    # which has been scheduled to sync with the old project.application
1743
    # Need to check in ProjectMembership.set_sync()
1744

    
1745
    r = qh_add_quota(serial, sub_quota, add_quota)
1746
    if r:
1747
        m = "cannot sync serial: %d" % serial
1748
        raise RuntimeError(m)
1749

    
1750
    sync_finish_serials([serial])
1751

    
1752
def sync_deactivating_projects():
1753

    
1754
    ACCEPTED = ProjectMembership.ACCEPTED
1755
    PENDING = ProjectMembership.PENDING
1756
    REMOVING = ProjectMembership.REMOVING
1757

    
1758
    psfu = Project.objects.select_for_update()
1759
    projects = psfu.filter(deactivation_start_date__isnull=False)
1760

    
1761
    if not projects:
1762
        return
1763

    
1764
    sub_quota, add_quota = [], []
1765

    
1766
    serial = new_serial()
1767

    
1768
    for project in projects:
1769
        objects = project.projectmembership_set.select_for_update()
1770
        memberships = objects.filter(Q(state=ACCEPTED) |
1771
                                     Q(state=PENDING) | Q(state=REMOVING))
1772
        for membership in memberships:
1773
            if membership.state in (PENDING, REMOVING):
1774
                m = "cannot sync deactivating project '%s'" % project
1775
                raise RuntimeError(m)
1776

    
1777
            # state == ACCEPTED
1778
            if membership.pending_application:
1779
                m = "%s: impossible: pending_application is not None (%s)" % (
1780
                    membership, membership.pending_application)
1781
                raise AssertionError(m)
1782
            if membership.pending_serial:
1783
                m = "%s: impossible: pending_serial is not None (%s)" % (
1784
                    membership, membership.pending_serial)
1785
                raise AssertionError(m)
1786

    
1787
            membership.pending_serial = serial
1788
            membership.get_diff_quotas(sub_quota, add_quota, remove=True)
1789
            membership.save()
1790

    
1791
    transaction.commit()
1792

    
1793
    r = qh_add_quota(serial, sub_quota, add_quota)
1794
    if r:
1795
        m = "cannot sync serial: %d" % serial
1796
        raise RuntimeError(m)
1797

    
1798
    sync_finish_serials([serial])
1799

    
1800
    # finalize deactivating projects
1801
    deactivating_projects = psfu.filter(deactivation_start_date__isnull=False)
1802
    for project in deactivating_projects:
1803
        objects = project.projectmembership_set.select_for_update()
1804
        memberships = list(objects.filter(Q(state=ACCEPTED) |
1805
                                          Q(state=PENDING) | Q(state=REMOVING)))
1806
        if not memberships:
1807
            project.set_deactivation_date()
1808
            project.save()
1809

    
1810
    transaction.commit()
1811

    
1812
def sync_projects():
1813
    sync_all_projects()
1814
    sync_deactivating_projects()
1815

    
1816
def trigger_sync(retries=3, retry_wait=1.0):
1817
    transaction.commit()
1818

    
1819
    cursor = connection.cursor()
1820
    locked = True
1821
    try:
1822
        while 1:
1823
            cursor.execute("SELECT pg_try_advisory_lock(1)")
1824
            r = cursor.fetchone()
1825
            if r is None:
1826
                m = "Impossible"
1827
                raise AssertionError(m)
1828
            locked = r[0]
1829
            if locked:
1830
                break
1831

    
1832
            retries -= 1
1833
            if retries <= 0:
1834
                return False
1835
            sleep(retry_wait)
1836

    
1837
        sync_projects()
1838
        return True
1839

    
1840
    finally:
1841
        if locked:
1842
            cursor.execute("SELECT pg_advisory_unlock(1)")
1843
            cursor.fetchall()
1844

    
1845

    
1846
class ProjectMembershipHistory(models.Model):
1847
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
1848
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
1849

    
1850
    person  =   models.CharField(max_length=255)
1851
    project =   models.BigIntegerField()
1852
    date    =   models.DateField(default=datetime.now)
1853
    reason  =   models.IntegerField()
1854
    serial  =   models.BigIntegerField()
1855

    
1856
### SIGNALS ###
1857
################
1858

    
1859
def create_astakos_user(u):
1860
    try:
1861
        AstakosUser.objects.get(user_ptr=u.pk)
1862
    except AstakosUser.DoesNotExist:
1863
        extended_user = AstakosUser(user_ptr_id=u.pk)
1864
        extended_user.__dict__.update(u.__dict__)
1865
        extended_user.save()
1866
        if not extended_user.has_auth_provider('local'):
1867
            extended_user.add_auth_provider('local')
1868
    except BaseException, e:
1869
        logger.exception(e)
1870

    
1871

    
1872
def fix_superusers(sender, **kwargs):
1873
    # Associate superusers with AstakosUser
1874
    admins = User.objects.filter(is_superuser=True)
1875
    for u in admins:
1876
        create_astakos_user(u)
1877
post_syncdb.connect(fix_superusers)
1878

    
1879

    
1880
def user_post_save(sender, instance, created, **kwargs):
1881
    if not created:
1882
        return
1883
    create_astakos_user(instance)
1884
post_save.connect(user_post_save, sender=User)
1885

    
1886
def astakosuser_post_save(sender, instance, created, **kwargs):
1887
    if not created:
1888
        return
1889
    # TODO handle socket.error & IOError
1890
    register_users((instance,))
1891
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1892

    
1893
def resource_post_save(sender, instance, created, **kwargs):
1894
    if not created:
1895
        return
1896
    register_resources((instance,))
1897
post_save.connect(resource_post_save, sender=Resource)
1898

    
1899
def renew_token(sender, instance, **kwargs):
1900
    if not instance.auth_token:
1901
        instance.renew_token()
1902
pre_save.connect(renew_token, sender=AstakosUser)
1903
pre_save.connect(renew_token, sender=Service)
1904