Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 37d59b27

History | View | Annotate | Download (73.9 kB)

1
# Copyright 2011, 2012, 2013 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
import math
39

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

    
48
from django.db import models, IntegrityError, transaction
49
from django.contrib.auth.models import User, UserManager, Group, Permission
50
from django.utils.translation import ugettext as _
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, Max
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 synnefo.lib.utils import dict_merge
68

    
69
from astakos.im.settings import (
70
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
71
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
72
    SITENAME, MODERATION_ENABLED,
73
    PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES)
74
from astakos.im import settings as astakos_settings
75
from astakos.im import auth_providers as auth
76

    
77
import astakos.im.messages as astakos_messages
78
from snf_django.lib.db.managers import ForUpdateManager
79
from synnefo.lib.ordereddict import OrderedDict
80

    
81
from snf_django.lib.db.fields import intDecimalField
82
from synnefo.util.text import uenc, udec
83
from astakos.im import presentation
84

    
85
logger = logging.getLogger(__name__)
86

    
87
DEFAULT_CONTENT_TYPE = None
88
_content_type = None
89

    
90

    
91
def get_content_type():
92
    global _content_type
93
    if _content_type is not None:
94
        return _content_type
95

    
96
    try:
97
        content_type = ContentType.objects.get(app_label='im',
98
                                               model='astakosuser')
99
    except:
100
        content_type = DEFAULT_CONTENT_TYPE
101
    _content_type = content_type
102
    return content_type
103

    
104
inf = float('inf')
105

    
106

    
107
class Service(models.Model):
108
    name = models.CharField(_('Name'), max_length=255, unique=True,
109
                            db_index=True)
110
    url = models.CharField(_('Service url'), max_length=255, null=True,
111
                           help_text=_("URL the service is accessible from"))
112
    api_url = models.CharField(_('Service API url'), max_length=255, null=True)
113
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
114
                                  null=True, blank=True)
115
    auth_token_created = models.DateTimeField(_('Token creation date'),
116
                                              null=True)
117
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
118
                                              null=True)
119

    
120
    def renew_token(self, expiration_date=None):
121
        md5 = hashlib.md5()
122
        md5.update(self.name.encode('ascii', 'ignore'))
123
        md5.update(self.api_url.encode('ascii', 'ignore'))
124
        md5.update(asctime())
125

    
126
        self.auth_token = b64encode(md5.digest())
127
        self.auth_token_created = datetime.now()
128
        if expiration_date:
129
            self.auth_token_expires = expiration_date
130
        else:
131
            self.auth_token_expires = None
132

    
133
    def __str__(self):
134
        return self.name
135

    
136
    @classmethod
137
    def catalog(cls, orderfor=None):
138
        catalog = {}
139
        services = list(cls.objects.all())
140
        metadata = presentation.SERVICES
141
        metadata = dict_merge(presentation.SERVICES,
142
                              astakos_settings.SERVICES_META)
143

    
144
        for service in services:
145
            d = {'api_url': service.api_url,
146
                 'url': service.url,
147
                 'name': service.name}
148
            if service.name in metadata:
149
                metadata[service.name].update(d)
150
            else:
151
                metadata[service.name] = d
152

    
153
        def service_by_order(s):
154
            return s[1].get('order')
155

    
156
        def service_by_dashbaord_order(s):
157
            return s[1].get('dashboard').get('order')
158

    
159
        for service, info in metadata.iteritems():
160
            default_meta = presentation.service_defaults(service)
161
            base_meta = metadata.get(service, {})
162
            settings_meta = astakos_settings.SERVICES_META.get(service, {})
163
            service_meta = dict_merge(default_meta, base_meta)
164
            meta = dict_merge(service_meta, settings_meta)
165
            catalog[service] = meta
166

    
167
        order_key = service_by_order
168
        if orderfor == 'dashboard':
169
            order_key = service_by_dashbaord_order
170

    
171
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
172
                                             key=order_key))
173
        return ordered_catalog
174

    
175

    
176
_presentation_data = {}
177

    
178

    
179
def get_presentation(resource):
180
    global _presentation_data
181
    resource_presentation = _presentation_data.get(resource, {})
182
    if not resource_presentation:
183
        resources_presentation = presentation.RESOURCES.get('resources', {})
184
        resource_presentation = resources_presentation.get(resource, {})
185
        _presentation_data[resource] = resource_presentation
186
    return resource_presentation
187

    
188

    
189
class Resource(models.Model):
190
    name = models.CharField(_('Name'), max_length=255, unique=True)
191
    desc = models.TextField(_('Description'), null=True)
192
    service = models.ForeignKey(Service)
193
    unit = models.CharField(_('Unit'), null=True, max_length=255)
194
    uplimit = intDecimalField(default=0)
195
    allow_in_projects = models.BooleanField(default=True)
196

    
197
    objects = ForUpdateManager()
198

    
199
    def __str__(self):
200
        return self.name
201

    
202
    def full_name(self):
203
        return str(self)
204

    
205
    def get_info(self):
206
        return {'service': str(self.service),
207
                'description': self.desc,
208
                'unit': self.unit,
209
                'allow_in_projects': self.allow_in_projects,
210
                }
211

    
212
    @property
213
    def group(self):
214
        default = self.name
215
        return get_presentation(str(self)).get('group', default)
216

    
217
    @property
218
    def help_text(self):
219
        default = "%s resource" % self.name
220
        return get_presentation(str(self)).get('help_text', default)
221

    
222
    @property
223
    def help_text_input_each(self):
224
        default = "%s resource" % self.name
225
        return get_presentation(str(self)).get('help_text_input_each', default)
226

    
227
    @property
228
    def is_abbreviation(self):
229
        return get_presentation(str(self)).get('is_abbreviation', False)
230

    
231
    @property
232
    def report_desc(self):
233
        default = "%s resource" % self.name
234
        return get_presentation(str(self)).get('report_desc', default)
235

    
236
    @property
237
    def placeholder(self):
238
        return get_presentation(str(self)).get('placeholder', self.unit)
239

    
240
    @property
241
    def verbose_name(self):
242
        return get_presentation(str(self)).get('verbose_name', self.name)
243

    
244
    @property
245
    def display_name(self):
246
        name = self.verbose_name
247
        if self.is_abbreviation:
248
            name = name.upper()
249
        return name
250

    
251
    @property
252
    def pluralized_display_name(self):
253
        if not self.unit:
254
            return '%ss' % self.display_name
255
        return self.display_name
256

    
257
def get_resource_names():
258
    _RESOURCE_NAMES = []
259
    resources = Resource.objects.select_related('service').all()
260
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
261
    return _RESOURCE_NAMES
262

    
263

    
264
class AstakosUserManager(UserManager):
265

    
266
    def get_auth_provider_user(self, provider, **kwargs):
267
        """
268
        Retrieve AstakosUser instance associated with the specified third party
269
        id.
270
        """
271
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
272
                          kwargs.iteritems()))
273
        return self.get(auth_providers__module=provider, **kwargs)
274

    
275
    def get_by_email(self, email):
276
        return self.get(email=email)
277

    
278
    def get_by_identifier(self, email_or_username, **kwargs):
279
        try:
280
            return self.get(email__iexact=email_or_username, **kwargs)
281
        except AstakosUser.DoesNotExist:
282
            return self.get(username__iexact=email_or_username, **kwargs)
283

    
284
    def user_exists(self, email_or_username, **kwargs):
285
        qemail = Q(email__iexact=email_or_username)
286
        qusername = Q(username__iexact=email_or_username)
287
        qextra = Q(**kwargs)
288
        return self.filter((qemail | qusername) & qextra).exists()
289

    
290
    def verified_user_exists(self, email_or_username):
291
        return self.user_exists(email_or_username, email_verified=True)
292

    
293
    def verified(self):
294
        return self.filter(email_verified=True)
295

    
296
    def uuid_catalog(self, l=None):
297
        """
298
        Returns a uuid to username mapping for the uuids appearing in l.
299
        If l is None returns the mapping for all existing users.
300
        """
301
        q = self.filter(uuid__in=l) if l != None else self
302
        return dict(q.values_list('uuid', 'username'))
303

    
304
    def displayname_catalog(self, l=None):
305
        """
306
        Returns a username to uuid mapping for the usernames appearing in l.
307
        If l is None returns the mapping for all existing users.
308
        """
309
        if l is not None:
310
            lmap = dict((x.lower(), x) for x in l)
311
            q = self.filter(username__in=lmap.keys())
312
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
313
        else:
314
            q = self
315
            values = self.values_list('username', 'uuid')
316
        return dict(values)
317

    
318

    
319

    
320
class AstakosUser(User):
321
    """
322
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
323
    """
324
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
325
                                   null=True)
326

    
327
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
328
    #                    AstakosUserProvider model.
329
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
330
                                null=True)
331
    # ex. screen_name for twitter, eppn for shibboleth
332
    third_party_identifier = models.CharField(_('Third-party identifier'),
333
                                              max_length=255, null=True,
334
                                              blank=True)
335

    
336

    
337
    #for invitations
338
    user_level = DEFAULT_USER_LEVEL
339
    level = models.IntegerField(_('Inviter level'), default=user_level)
340
    invitations = models.IntegerField(
341
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
342

    
343
    auth_token = models.CharField(_('Authentication Token'),
344
                                  max_length=32,
345
                                  null=True,
346
                                  blank=True,
347
                                  help_text = _('Renew your authentication '
348
                                                'token. Make sure to set the new '
349
                                                'token in any client you may be '
350
                                                'using, to preserve its '
351
                                                'functionality.'))
352
    auth_token_created = models.DateTimeField(_('Token creation date'),
353
                                              null=True)
354
    auth_token_expires = models.DateTimeField(
355
        _('Token expiration date'), null=True)
356

    
357
    updated = models.DateTimeField(_('Update date'))
358
    is_verified = models.BooleanField(_('Is verified?'), default=False)
359

    
360
    email_verified = models.BooleanField(_('Email verified?'), default=False)
361

    
362
    has_credits = models.BooleanField(_('Has credits?'), default=False)
363
    has_signed_terms = models.BooleanField(
364
        _('I agree with the terms'), default=False)
365
    date_signed_terms = models.DateTimeField(
366
        _('Signed terms date'), null=True, blank=True)
367

    
368
    activation_sent = models.DateTimeField(
369
        _('Activation sent data'), null=True, blank=True)
370

    
371
    policy = models.ManyToManyField(
372
        Resource, null=True, through='AstakosUserQuota')
373

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

    
376
    __has_signed_terms = False
377
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
378
                                           default=False, db_index=True)
379

    
380
    objects = AstakosUserManager()
381

    
382
    forupdate = ForUpdateManager()
383

    
384
    def __init__(self, *args, **kwargs):
385
        super(AstakosUser, self).__init__(*args, **kwargs)
386
        self.__has_signed_terms = self.has_signed_terms
387
        if not self.id:
388
            self.is_active = False
389

    
390
    @property
391
    def realname(self):
392
        return '%s %s' % (self.first_name, self.last_name)
393

    
394
    @property
395
    def log_display(self):
396
        """
397
        Should be used in all logger.* calls that refer to a user so that
398
        user display is consistent across log entries.
399
        """
400
        return '%s::%s' % (self.uuid, self.email)
401

    
402
    @realname.setter
403
    def realname(self, value):
404
        parts = value.split(' ')
405
        if len(parts) == 2:
406
            self.first_name = parts[0]
407
            self.last_name = parts[1]
408
        else:
409
            self.last_name = parts[0]
410

    
411
    def add_permission(self, pname):
412
        if self.has_perm(pname):
413
            return
414
        p, created = Permission.objects.get_or_create(
415
                                    codename=pname,
416
                                    name=pname.capitalize(),
417
                                    content_type=get_content_type())
418
        self.user_permissions.add(p)
419

    
420
    def remove_permission(self, pname):
421
        if self.has_perm(pname):
422
            return
423
        p = Permission.objects.get(codename=pname,
424
                                   content_type=get_content_type())
425
        self.user_permissions.remove(p)
426

    
427
    def is_project_admin(self, application_id=None):
428
        return self.uuid in astakos_settings.PROJECT_ADMINS
429

    
430
    @property
431
    def invitation(self):
432
        try:
433
            return Invitation.objects.get(username=self.email)
434
        except Invitation.DoesNotExist:
435
            return None
436

    
437
    @property
438
    def policies(self):
439
        return self.astakosuserquota_set.select_related().all()
440

    
441
    @policies.setter
442
    def policies(self, policies):
443
        for p in policies:
444
            p.setdefault('resource', '')
445
            p.setdefault('capacity', 0)
446
            p.setdefault('update', True)
447
            self.add_resource_policy(**p)
448

    
449
    def add_resource_policy(
450
            self, resource, capacity,
451
            update=True):
452
        """Raises ObjectDoesNotExist, IntegrityError"""
453
        resource = Resource.objects.get(name=resource)
454
        if update:
455
            AstakosUserQuota.objects.update_or_create(
456
                user=self, resource=resource, defaults={
457
                    'capacity':capacity,
458
                    })
459
        else:
460
            q = self.astakosuserquota_set
461
            q.create(
462
                resource=resource, capacity=capacity,
463
                )
464

    
465
    def get_resource_policy(self, resource):
466
        resource = Resource.objects.get(name=resource)
467
        default_capacity = resource.uplimit
468
        try:
469
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
470
            return policy, default_capacity
471
        except AstakosUserQuota.DoesNotExist:
472
            return None, default_capacity
473

    
474
    def remove_resource_policy(self, service, resource):
475
        """Raises ObjectDoesNotExist, IntegrityError"""
476
        resource = Resource.objects.get(name=resource)
477
        q = self.policies.get(resource=resource).delete()
478

    
479
    def update_uuid(self):
480
        while not self.uuid:
481
            uuid_val =  str(uuid.uuid4())
482
            try:
483
                AstakosUser.objects.get(uuid=uuid_val)
484
            except AstakosUser.DoesNotExist, e:
485
                self.uuid = uuid_val
486
        return self.uuid
487

    
488
    def save(self, update_timestamps=True, **kwargs):
489
        if update_timestamps:
490
            if not self.id:
491
                self.date_joined = datetime.now()
492
            self.updated = datetime.now()
493

    
494
        # update date_signed_terms if necessary
495
        if self.__has_signed_terms != self.has_signed_terms:
496
            self.date_signed_terms = datetime.now()
497

    
498
        self.update_uuid()
499

    
500
        if self.username != self.email.lower():
501
            # set username
502
            self.username = self.email.lower()
503

    
504
        super(AstakosUser, self).save(**kwargs)
505

    
506
    def renew_token(self, flush_sessions=False, current_key=None):
507
        md5 = hashlib.md5()
508
        md5.update(settings.SECRET_KEY)
509
        md5.update(self.username)
510
        md5.update(self.realname.encode('ascii', 'ignore'))
511
        md5.update(asctime())
512

    
513
        self.auth_token = b64encode(md5.digest())
514
        self.auth_token_created = datetime.now()
515
        self.auth_token_expires = self.auth_token_created + \
516
                                  timedelta(hours=AUTH_TOKEN_DURATION)
517
        if flush_sessions:
518
            self.flush_sessions(current_key)
519
        msg = 'Token renewed for %s' % self.email
520
        logger.log(LOGGING_LEVEL, msg)
521

    
522
    def flush_sessions(self, current_key=None):
523
        q = self.sessions
524
        if current_key:
525
            q = q.exclude(session_key=current_key)
526

    
527
        keys = q.values_list('session_key', flat=True)
528
        if keys:
529
            msg = 'Flushing sessions: %s' % ','.join(keys)
530
            logger.log(LOGGING_LEVEL, msg, [])
531
        engine = import_module(settings.SESSION_ENGINE)
532
        for k in keys:
533
            s = engine.SessionStore(k)
534
            s.flush()
535

    
536
    def __unicode__(self):
537
        return '%s (%s)' % (self.realname, self.email)
538

    
539
    def conflicting_email(self):
540
        q = AstakosUser.objects.exclude(username=self.username)
541
        q = q.filter(email__iexact=self.email)
542
        if q.count() != 0:
543
            return True
544
        return False
545

    
546
    def email_change_is_pending(self):
547
        return self.emailchanges.count() > 0
548

    
549
    @property
550
    def signed_terms(self):
551
        term = get_latest_terms()
552
        if not term:
553
            return True
554
        if not self.has_signed_terms:
555
            return False
556
        if not self.date_signed_terms:
557
            return False
558
        if self.date_signed_terms < term.date:
559
            self.has_signed_terms = False
560
            self.date_signed_terms = None
561
            self.save()
562
            return False
563
        return True
564

    
565
    def set_invitations_level(self):
566
        """
567
        Update user invitation level
568
        """
569
        level = self.invitation.inviter.level + 1
570
        self.level = level
571
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
572

    
573
    def can_change_password(self):
574
        return self.has_auth_provider('local', auth_backend='astakos')
575

    
576
    def can_change_email(self):
577
        if not self.has_auth_provider('local'):
578
            return True
579

    
580
        local = self.get_auth_provider('local')._instance
581
        return local.auth_backend == 'astakos'
582

    
583
    # Auth providers related methods
584
    def get_auth_provider(self, module=None, identifier=None, **filters):
585
        if not module:
586
            return self.auth_providers.active()[0].settings
587

    
588
        params = {'module': module}
589
        if identifier:
590
            params['identifier'] = identifier
591
        params.update(filters)
592
        return self.auth_providers.active().get(**params).settings
593

    
594
    def has_auth_provider(self, provider, **kwargs):
595
        return bool(self.auth_providers.active().filter(module=provider,
596
                                                        **kwargs).count())
597

    
598
    def get_required_providers(self, **kwargs):
599
        return auth.REQUIRED_PROVIDERS.keys()
600

    
601
    def missing_required_providers(self):
602
        required = self.get_required_providers()
603
        missing = []
604
        for provider in required:
605
            if not self.has_auth_provider(provider):
606
                missing.append(auth.get_provider(provider, self))
607
        return missing
608

    
609
    def get_available_auth_providers(self, **filters):
610
        """
611
        Returns a list of providers available for add by the user.
612
        """
613
        modules = astakos_settings.IM_MODULES
614
        providers = []
615
        for p in modules:
616
            providers.append(auth.get_provider(p, self))
617
        available = []
618

    
619
        for p in providers:
620
            if p.get_add_policy:
621
                available.append(p)
622
        return available
623

    
624
    def get_disabled_auth_providers(self, **filters):
625
        providers = self.get_auth_providers(**filters)
626
        disabled = []
627
        for p in providers:
628
            if not p.get_login_policy:
629
                disabled.append(p)
630
        return disabled
631

    
632
    def get_enabled_auth_providers(self, **filters):
633
        providers = self.get_auth_providers(**filters)
634
        enabled = []
635
        for p in providers:
636
            if p.get_login_policy:
637
                enabled.append(p)
638
        return enabled
639

    
640
    def get_auth_providers(self, **filters):
641
        providers = []
642
        for provider in self.auth_providers.active(**filters):
643
            if provider.settings.module_enabled:
644
                providers.append(provider.settings)
645

    
646
        modules = astakos_settings.IM_MODULES
647

    
648
        def key(p):
649
            if not p.module in modules:
650
                return 100
651
            return modules.index(p.module)
652

    
653
        providers = sorted(providers, key=key)
654
        return providers
655

    
656
    # URL methods
657
    @property
658
    def auth_providers_display(self):
659
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
660
                         self.get_enabled_auth_providers()])
661

    
662
    def add_auth_provider(self, module='local', identifier=None, **params):
663
        provider = auth.get_provider(module, self, identifier, **params)
664
        provider.add_to_user()
665

    
666
    def get_resend_activation_url(self):
667
        return reverse('send_activation', kwargs={'user_id': self.pk})
668

    
669
    def get_activation_url(self, nxt=False):
670
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
671
                                 quote(self.auth_token))
672
        if nxt:
673
            url += "&next=%s" % quote(nxt)
674
        return url
675

    
676
    def get_password_reset_url(self, token_generator=default_token_generator):
677
        return reverse('django.contrib.auth.views.password_reset_confirm',
678
                          kwargs={'uidb36':int_to_base36(self.id),
679
                                  'token':token_generator.make_token(self)})
680

    
681
    def get_inactive_message(self, provider_module, identifier=None):
682
        provider = self.get_auth_provider(provider_module, identifier)
683

    
684
        msg_extra = ''
685
        message = ''
686

    
687
        msg_inactive = provider.get_account_inactive_msg
688
        msg_pending = provider.get_pending_activation_msg
689
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
690
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
691
        msg_pending_mod = provider.get_pending_moderation_msg
692
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
693

    
694
        if self.activation_sent:
695
            if self.email_verified:
696
                message = msg_inactive
697
            else:
698
                message = msg_pending
699
                url = self.get_resend_activation_url()
700
                msg_extra = msg_pending_help + \
701
                            u' ' + \
702
                            '<a href="%s">%s?</a>' % (url, msg_resend)
703
        else:
704
            if astakos_settings.MODERATION_ENABLED:
705
                message = msg_pending_mod
706
            else:
707
                message = msg_pending
708
                url = self.get_resend_activation_url()
709
                msg_extra = '<a href="%s">%s?</a>' % (url, \
710
                                msg_resend)
711

    
712
        return mark_safe(message + u' '+ msg_extra)
713

    
714
    def owns_application(self, application):
715
        return application.owner == self
716

    
717
    def owns_project(self, project):
718
        return project.application.owner == self
719

    
720
    def is_associated(self, project):
721
        try:
722
            m = ProjectMembership.objects.get(person=self, project=project)
723
            return m.state in ProjectMembership.ASSOCIATED_STATES
724
        except ProjectMembership.DoesNotExist:
725
            return False
726

    
727
    def get_membership(self, project):
728
        try:
729
            return ProjectMembership.objects.get(
730
                project=project,
731
                person=self)
732
        except ProjectMembership.DoesNotExist:
733
            return None
734

    
735
    def membership_display(self, project):
736
        m = self.get_membership(project)
737
        if m is None:
738
            return _('Not a member')
739
        else:
740
            return m.user_friendly_state_display()
741

    
742
    def non_owner_can_view(self, maybe_project):
743
        if self.is_project_admin():
744
            return True
745
        if maybe_project is None:
746
            return False
747
        project = maybe_project
748
        if self.is_associated(project):
749
            return True
750
        if project.is_deactivated():
751
            return False
752
        return True
753

    
754
    def settings(self):
755
        return UserSetting.objects.filter(user=self)
756

    
757

    
758
class AstakosUserAuthProviderManager(models.Manager):
759

    
760
    def active(self, **filters):
761
        return self.filter(active=True, **filters)
762

    
763
    def remove_unverified_providers(self, provider, **filters):
764
        try:
765
            existing = self.filter(module=provider, user__email_verified=False,
766
                                   **filters)
767
            for p in existing:
768
                p.user.delete()
769
        except:
770
            pass
771

    
772
    def unverified(self, provider, **filters):
773
        try:
774
            return self.get(module=provider, user__email_verified=False,
775
                            **filters).settings
776
        except AstakosUserAuthProvider.DoesNotExist:
777
            return None
778

    
779
    def verified(self, provider, **filters):
780
        try:
781
            return self.get(module=provider, user__email_verified=True,
782
                            **filters).settings
783
        except AstakosUserAuthProvider.DoesNotExist:
784
            return None
785

    
786

    
787
class AuthProviderPolicyProfileManager(models.Manager):
788

    
789
    def active(self):
790
        return self.filter(active=True)
791

    
792
    def for_user(self, user, provider):
793
        policies = {}
794
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
795
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
796
        exclusive_q = exclusive_q1 | exclusive_q2
797

    
798
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
799
            policies.update(profile.policies)
800

    
801
        user_groups = user.groups.all().values('pk')
802
        for profile in self.active().filter(groups__in=user_groups).filter(
803
                exclusive_q):
804
            policies.update(profile.policies)
805
        return policies
806

    
807
    def add_policy(self, name, provider, group_or_user, exclusive=False,
808
                   **policies):
809
        is_group = isinstance(group_or_user, Group)
810
        profile, created = self.get_or_create(name=name, provider=provider,
811
                                              is_exclusive=exclusive)
812
        profile.is_exclusive = exclusive
813
        profile.save()
814
        if is_group:
815
            profile.groups.add(group_or_user)
816
        else:
817
            profile.users.add(group_or_user)
818
        profile.set_policies(policies)
819
        profile.save()
820
        return profile
821

    
822

    
823
class AuthProviderPolicyProfile(models.Model):
824
    name = models.CharField(_('Name'), max_length=255, blank=False,
825
                            null=False, db_index=True)
826
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
827
                                null=False)
828

    
829
    # apply policies to all providers excluding the one set in provider field
830
    is_exclusive = models.BooleanField(default=False)
831

    
832
    policy_add = models.NullBooleanField(null=True, default=None)
833
    policy_remove = models.NullBooleanField(null=True, default=None)
834
    policy_create = models.NullBooleanField(null=True, default=None)
835
    policy_login = models.NullBooleanField(null=True, default=None)
836
    policy_limit = models.IntegerField(null=True, default=None)
837
    policy_required = models.NullBooleanField(null=True, default=None)
838
    policy_automoderate = models.NullBooleanField(null=True, default=None)
839
    policy_switch = models.NullBooleanField(null=True, default=None)
840

    
841
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
842
                     'automoderate')
843

    
844
    priority = models.IntegerField(null=False, default=1)
845
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
846
    users = models.ManyToManyField(AstakosUser,
847
                                   related_name='authpolicy_profiles')
848
    active = models.BooleanField(default=True)
849

    
850
    objects = AuthProviderPolicyProfileManager()
851

    
852
    class Meta:
853
        ordering = ['priority']
854

    
855
    @property
856
    def policies(self):
857
        policies = {}
858
        for pkey in self.POLICY_FIELDS:
859
            value = getattr(self, 'policy_%s' % pkey, None)
860
            if value is None:
861
                continue
862
            policies[pkey] = value
863
        return policies
864

    
865
    def set_policies(self, policies_dict):
866
        for key, value in policies_dict.iteritems():
867
            if key in self.POLICY_FIELDS:
868
                setattr(self, 'policy_%s' % key, value)
869
        return self.policies
870

    
871

    
872
class AstakosUserAuthProvider(models.Model):
873
    """
874
    Available user authentication methods.
875
    """
876
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
877
                                   null=True, default=None)
878
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
879
    module = models.CharField(_('Provider'), max_length=255, blank=False,
880
                                default='local')
881
    identifier = models.CharField(_('Third-party identifier'),
882
                                              max_length=255, null=True,
883
                                              blank=True)
884
    active = models.BooleanField(default=True)
885
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
886
                                   default='astakos')
887
    info_data = models.TextField(default="", null=True, blank=True)
888
    created = models.DateTimeField('Creation date', auto_now_add=True)
889

    
890
    objects = AstakosUserAuthProviderManager()
891

    
892
    class Meta:
893
        unique_together = (('identifier', 'module', 'user'), )
894
        ordering = ('module', 'created')
895

    
896
    def __init__(self, *args, **kwargs):
897
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
898
        try:
899
            self.info = json.loads(self.info_data)
900
            if not self.info:
901
                self.info = {}
902
        except Exception, e:
903
            self.info = {}
904

    
905
        for key,value in self.info.iteritems():
906
            setattr(self, 'info_%s' % key, value)
907

    
908
    @property
909
    def settings(self):
910
        extra_data = {}
911

    
912
        info_data = {}
913
        if self.info_data:
914
            info_data = json.loads(self.info_data)
915

    
916
        extra_data['info'] = info_data
917

    
918
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
919
            extra_data[key] = getattr(self, key)
920

    
921
        extra_data['instance'] = self
922
        return auth.get_provider(self.module, self.user,
923
                                           self.identifier, **extra_data)
924

    
925
    def __repr__(self):
926
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
927

    
928
    def __unicode__(self):
929
        if self.identifier:
930
            return "%s:%s" % (self.module, self.identifier)
931
        if self.auth_backend:
932
            return "%s:%s" % (self.module, self.auth_backend)
933
        return self.module
934

    
935
    def save(self, *args, **kwargs):
936
        self.info_data = json.dumps(self.info)
937
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
938

    
939

    
940
class ExtendedManager(models.Manager):
941
    def _update_or_create(self, **kwargs):
942
        assert kwargs, \
943
            'update_or_create() must be passed at least one keyword argument'
944
        obj, created = self.get_or_create(**kwargs)
945
        defaults = kwargs.pop('defaults', {})
946
        if created:
947
            return obj, True, False
948
        else:
949
            try:
950
                params = dict(
951
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
952
                params.update(defaults)
953
                for attr, val in params.items():
954
                    if hasattr(obj, attr):
955
                        setattr(obj, attr, val)
956
                sid = transaction.savepoint()
957
                obj.save(force_update=True)
958
                transaction.savepoint_commit(sid)
959
                return obj, False, True
960
            except IntegrityError, e:
961
                transaction.savepoint_rollback(sid)
962
                try:
963
                    return self.get(**kwargs), False, False
964
                except self.model.DoesNotExist:
965
                    raise e
966

    
967
    update_or_create = _update_or_create
968

    
969

    
970
class AstakosUserQuota(models.Model):
971
    objects = ExtendedManager()
972
    capacity = intDecimalField()
973
    resource = models.ForeignKey(Resource)
974
    user = models.ForeignKey(AstakosUser)
975

    
976
    class Meta:
977
        unique_together = ("resource", "user")
978

    
979

    
980
class ApprovalTerms(models.Model):
981
    """
982
    Model for approval terms
983
    """
984

    
985
    date = models.DateTimeField(
986
        _('Issue date'), db_index=True, auto_now_add=True)
987
    location = models.CharField(_('Terms location'), max_length=255)
988

    
989

    
990
class Invitation(models.Model):
991
    """
992
    Model for registring invitations
993
    """
994
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
995
                                null=True)
996
    realname = models.CharField(_('Real name'), max_length=255)
997
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
998
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
999
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1000
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1001
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1002

    
1003
    def __init__(self, *args, **kwargs):
1004
        super(Invitation, self).__init__(*args, **kwargs)
1005
        if not self.id:
1006
            self.code = _generate_invitation_code()
1007

    
1008
    def consume(self):
1009
        self.is_consumed = True
1010
        self.consumed = datetime.now()
1011
        self.save()
1012

    
1013
    def __unicode__(self):
1014
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1015

    
1016

    
1017
class EmailChangeManager(models.Manager):
1018

    
1019
    @transaction.commit_on_success
1020
    def change_email(self, activation_key):
1021
        """
1022
        Validate an activation key and change the corresponding
1023
        ``User`` if valid.
1024

1025
        If the key is valid and has not expired, return the ``User``
1026
        after activating.
1027

1028
        If the key is not valid or has expired, return ``None``.
1029

1030
        If the key is valid but the ``User`` is already active,
1031
        return ``None``.
1032

1033
        After successful email change the activation record is deleted.
1034

1035
        Throws ValueError if there is already
1036
        """
1037
        try:
1038
            email_change = self.model.objects.get(
1039
                activation_key=activation_key)
1040
            if email_change.activation_key_expired():
1041
                email_change.delete()
1042
                raise EmailChange.DoesNotExist
1043
            # is there an active user with this address?
1044
            try:
1045
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1046
            except AstakosUser.DoesNotExist:
1047
                pass
1048
            else:
1049
                raise ValueError(_('The new email address is reserved.'))
1050
            # update user
1051
            user = AstakosUser.objects.get(pk=email_change.user_id)
1052
            old_email = user.email
1053
            user.email = email_change.new_email_address
1054
            user.save()
1055
            email_change.delete()
1056
            msg = "User %s changed email from %s to %s" % (user.log_display,
1057
                                                           old_email,
1058
                                                           user.email)
1059
            logger.log(LOGGING_LEVEL, msg)
1060
            return user
1061
        except EmailChange.DoesNotExist:
1062
            raise ValueError(_('Invalid activation key.'))
1063

    
1064

    
1065
class EmailChange(models.Model):
1066
    new_email_address = models.EmailField(
1067
        _(u'new e-mail address'),
1068
        help_text=_('Provide a new email address. Until you verify the new '
1069
                    'address by following the activation link that will be '
1070
                    'sent to it, your old email address will remain active.'))
1071
    user = models.ForeignKey(
1072
        AstakosUser, unique=True, related_name='emailchanges')
1073
    requested_at = models.DateTimeField(auto_now_add=True)
1074
    activation_key = models.CharField(
1075
        max_length=40, unique=True, db_index=True)
1076

    
1077
    objects = EmailChangeManager()
1078

    
1079
    def get_url(self):
1080
        return reverse('email_change_confirm',
1081
                      kwargs={'activation_key': self.activation_key})
1082

    
1083
    def activation_key_expired(self):
1084
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1085
        return self.requested_at + expiration_date < datetime.now()
1086

    
1087

    
1088
class AdditionalMail(models.Model):
1089
    """
1090
    Model for registring invitations
1091
    """
1092
    owner = models.ForeignKey(AstakosUser)
1093
    email = models.EmailField()
1094

    
1095

    
1096
def _generate_invitation_code():
1097
    while True:
1098
        code = randint(1, 2L ** 63 - 1)
1099
        try:
1100
            Invitation.objects.get(code=code)
1101
            # An invitation with this code already exists, try again
1102
        except Invitation.DoesNotExist:
1103
            return code
1104

    
1105

    
1106
def get_latest_terms():
1107
    try:
1108
        term = ApprovalTerms.objects.order_by('-id')[0]
1109
        return term
1110
    except IndexError:
1111
        pass
1112
    return None
1113

    
1114

    
1115
class PendingThirdPartyUser(models.Model):
1116
    """
1117
    Model for registring successful third party user authentications
1118
    """
1119
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1120
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1121
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1122
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1123
                                  null=True)
1124
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1125
                                 null=True)
1126
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1127
                                   null=True)
1128
    username = models.CharField(_('username'), max_length=30, unique=True,
1129
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1130
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1131
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1132
    info = models.TextField(default="", null=True, blank=True)
1133

    
1134
    class Meta:
1135
        unique_together = ("provider", "third_party_identifier")
1136

    
1137
    def get_user_instance(self):
1138
        d = self.__dict__
1139
        d.pop('_state', None)
1140
        d.pop('id', None)
1141
        d.pop('token', None)
1142
        d.pop('created', None)
1143
        d.pop('info', None)
1144
        user = AstakosUser(**d)
1145

    
1146
        return user
1147

    
1148
    @property
1149
    def realname(self):
1150
        return '%s %s' %(self.first_name, self.last_name)
1151

    
1152
    @realname.setter
1153
    def realname(self, value):
1154
        parts = value.split(' ')
1155
        if len(parts) == 2:
1156
            self.first_name = parts[0]
1157
            self.last_name = parts[1]
1158
        else:
1159
            self.last_name = parts[0]
1160

    
1161
    def save(self, **kwargs):
1162
        if not self.id:
1163
            # set username
1164
            while not self.username:
1165
                username =  uuid.uuid4().hex[:30]
1166
                try:
1167
                    AstakosUser.objects.get(username = username)
1168
                except AstakosUser.DoesNotExist, e:
1169
                    self.username = username
1170
        super(PendingThirdPartyUser, self).save(**kwargs)
1171

    
1172
    def generate_token(self):
1173
        self.password = self.third_party_identifier
1174
        self.last_login = datetime.now()
1175
        self.token = default_token_generator.make_token(self)
1176

    
1177
    def existing_user(self):
1178
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1179
                                         auth_providers__identifier=self.third_party_identifier)
1180

    
1181
    def get_provider(self, user):
1182
        params = {
1183
            'info_data': self.info,
1184
            'affiliation': self.affiliation
1185
        }
1186
        return auth.get_provider(self.provider, user,
1187
                                 self.third_party_identifier, **params)
1188

    
1189
class SessionCatalog(models.Model):
1190
    session_key = models.CharField(_('session key'), max_length=40)
1191
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1192

    
1193

    
1194
class UserSetting(models.Model):
1195
    user = models.ForeignKey(AstakosUser)
1196
    setting = models.CharField(max_length=255)
1197
    value = models.IntegerField()
1198

    
1199
    objects = ForUpdateManager()
1200

    
1201
    class Meta:
1202
        unique_together = ("user", "setting")
1203

    
1204

    
1205
### PROJECTS ###
1206
################
1207

    
1208
class ChainManager(ForUpdateManager):
1209

    
1210
    def search_by_name(self, *search_strings):
1211
        projects = Project.objects.search_by_name(*search_strings)
1212
        chains = [p.id for p in projects]
1213
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1214
        apps = (app for app in apps if app.is_latest())
1215
        app_chains = [app.chain for app in apps if app.chain not in chains]
1216
        return chains + app_chains
1217

    
1218
    def all_full_state(self):
1219
        chains = self.all()
1220
        cids = [c.chain for c in chains]
1221
        projects = Project.objects.select_related('application').in_bulk(cids)
1222

    
1223
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1224
        chain_latest = dict(objs.values_list('chain', 'latest'))
1225

    
1226
        objs = ProjectApplication.objects.select_related('applicant')
1227
        apps = objs.in_bulk(chain_latest.values())
1228

    
1229
        d = {}
1230
        for chain in chains:
1231
            pk = chain.pk
1232
            project = projects.get(pk, None)
1233
            app = apps[chain_latest[pk]]
1234
            d[chain.pk] = chain.get_state(project, app)
1235

    
1236
        return d
1237

    
1238
    def of_project(self, project):
1239
        if project is None:
1240
            return None
1241
        try:
1242
            return self.get(chain=project.id)
1243
        except Chain.DoesNotExist:
1244
            raise AssertionError('project with no chain')
1245

    
1246

    
1247
class Chain(models.Model):
1248
    chain  =   models.AutoField(primary_key=True)
1249

    
1250
    def __str__(self):
1251
        return "%s" % (self.chain,)
1252

    
1253
    objects = ChainManager()
1254

    
1255
    PENDING            = 0
1256
    DENIED             = 3
1257
    DISMISSED          = 4
1258
    CANCELLED          = 5
1259

    
1260
    APPROVED           = 10
1261
    APPROVED_PENDING   = 11
1262
    SUSPENDED          = 12
1263
    SUSPENDED_PENDING  = 13
1264
    TERMINATED         = 14
1265
    TERMINATED_PENDING = 15
1266

    
1267
    PENDING_STATES = [PENDING,
1268
                      APPROVED_PENDING,
1269
                      SUSPENDED_PENDING,
1270
                      TERMINATED_PENDING,
1271
                      ]
1272

    
1273
    MODIFICATION_STATES = [APPROVED_PENDING,
1274
                           SUSPENDED_PENDING,
1275
                           TERMINATED_PENDING,
1276
                           ]
1277

    
1278
    RELEVANT_STATES = [PENDING,
1279
                       DENIED,
1280
                       APPROVED,
1281
                       APPROVED_PENDING,
1282
                       SUSPENDED,
1283
                       SUSPENDED_PENDING,
1284
                       TERMINATED_PENDING,
1285
                       ]
1286

    
1287
    SKIP_STATES = [DISMISSED,
1288
                   CANCELLED,
1289
                   TERMINATED]
1290

    
1291
    STATE_DISPLAY = {
1292
        PENDING            : _("Pending"),
1293
        DENIED             : _("Denied"),
1294
        DISMISSED          : _("Dismissed"),
1295
        CANCELLED          : _("Cancelled"),
1296
        APPROVED           : _("Active"),
1297
        APPROVED_PENDING   : _("Active - Pending"),
1298
        SUSPENDED          : _("Suspended"),
1299
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1300
        TERMINATED         : _("Terminated"),
1301
        TERMINATED_PENDING : _("Terminated - Pending"),
1302
        }
1303

    
1304

    
1305
    @classmethod
1306
    def _chain_state(cls, project_state, app_state):
1307
        s = CHAIN_STATE.get((project_state, app_state), None)
1308
        if s is None:
1309
            raise AssertionError('inconsistent chain state')
1310
        return s
1311

    
1312
    @classmethod
1313
    def chain_state(cls, project, app):
1314
        p_state = project.state if project else None
1315
        return cls._chain_state(p_state, app.state)
1316

    
1317
    @classmethod
1318
    def state_display(cls, s):
1319
        if s is None:
1320
            return _("Unknown")
1321
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1322

    
1323
    def last_application(self):
1324
        return self.chained_apps.order_by('-id')[0]
1325

    
1326
    def get_project(self):
1327
        try:
1328
            return self.chained_project
1329
        except Project.DoesNotExist:
1330
            return None
1331

    
1332
    def get_elements(self):
1333
        project = self.get_project()
1334
        app = self.last_application()
1335
        return project, app
1336

    
1337
    def get_state(self, project, app):
1338
        s = self.chain_state(project, app)
1339
        return s, project, app
1340

    
1341
    def full_state(self):
1342
        project, app = self.get_elements()
1343
        return self.get_state(project, app)
1344

    
1345

    
1346
def new_chain():
1347
    c = Chain.objects.create()
1348
    return c
1349

    
1350

    
1351
class ProjectApplicationManager(ForUpdateManager):
1352

    
1353
    def user_visible_projects(self, *filters, **kw_filters):
1354
        model = self.model
1355
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1356

    
1357
    def user_visible_by_chain(self, flt):
1358
        model = self.model
1359
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1360
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1361
        by_chain = dict(pending.annotate(models.Max('id')))
1362
        by_chain.update(approved.annotate(models.Max('id')))
1363
        return self.filter(flt, id__in=by_chain.values())
1364

    
1365
    def user_accessible_projects(self, user):
1366
        """
1367
        Return projects accessed by specified user.
1368
        """
1369
        if user.is_project_admin():
1370
            participates_filters = Q()
1371
        else:
1372
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1373
                                   Q(project__projectmembership__person=user)
1374

    
1375
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1376

    
1377
    def search_by_name(self, *search_strings):
1378
        q = Q()
1379
        for s in search_strings:
1380
            q = q | Q(name__icontains=s)
1381
        return self.filter(q)
1382

    
1383
    def latest_of_chain(self, chain_id):
1384
        try:
1385
            return self.filter(chain=chain_id).order_by('-id')[0]
1386
        except IndexError:
1387
            return None
1388

    
1389

    
1390
class ProjectApplication(models.Model):
1391
    applicant               =   models.ForeignKey(
1392
                                    AstakosUser,
1393
                                    related_name='projects_applied',
1394
                                    db_index=True)
1395

    
1396
    PENDING     =    0
1397
    APPROVED    =    1
1398
    REPLACED    =    2
1399
    DENIED      =    3
1400
    DISMISSED   =    4
1401
    CANCELLED   =    5
1402

    
1403
    state                   =   models.IntegerField(default=PENDING,
1404
                                                    db_index=True)
1405

    
1406
    owner                   =   models.ForeignKey(
1407
                                    AstakosUser,
1408
                                    related_name='projects_owned',
1409
                                    db_index=True)
1410

    
1411
    chain                   =   models.ForeignKey(Chain,
1412
                                                  related_name='chained_apps',
1413
                                                  db_column='chain')
1414
    precursor_application   =   models.ForeignKey('ProjectApplication',
1415
                                                  null=True,
1416
                                                  blank=True)
1417

    
1418
    name                    =   models.CharField(max_length=80)
1419
    homepage                =   models.URLField(max_length=255, null=True,
1420
                                                verify_exists=False)
1421
    description             =   models.TextField(null=True, blank=True)
1422
    start_date              =   models.DateTimeField(null=True, blank=True)
1423
    end_date                =   models.DateTimeField()
1424
    member_join_policy      =   models.IntegerField()
1425
    member_leave_policy     =   models.IntegerField()
1426
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1427
    resource_grants         =   models.ManyToManyField(
1428
                                    Resource,
1429
                                    null=True,
1430
                                    blank=True,
1431
                                    through='ProjectResourceGrant')
1432
    comments                =   models.TextField(null=True, blank=True)
1433
    issue_date              =   models.DateTimeField(auto_now_add=True)
1434
    response_date           =   models.DateTimeField(null=True, blank=True)
1435
    response                =   models.TextField(null=True, blank=True)
1436

    
1437
    objects                 =   ProjectApplicationManager()
1438

    
1439
    # Compiled queries
1440
    Q_PENDING  = Q(state=PENDING)
1441
    Q_APPROVED = Q(state=APPROVED)
1442
    Q_DENIED   = Q(state=DENIED)
1443

    
1444
    class Meta:
1445
        unique_together = ("chain", "id")
1446

    
1447
    def __unicode__(self):
1448
        return "%s applied by %s" % (self.name, self.applicant)
1449

    
1450
    # TODO: Move to a more suitable place
1451
    APPLICATION_STATE_DISPLAY = {
1452
        PENDING  : _('Pending review'),
1453
        APPROVED : _('Approved'),
1454
        REPLACED : _('Replaced'),
1455
        DENIED   : _('Denied'),
1456
        DISMISSED: _('Dismissed'),
1457
        CANCELLED: _('Cancelled')
1458
    }
1459

    
1460
    @property
1461
    def log_display(self):
1462
        return "application %s (%s) for project %s" % (
1463
            self.id, self.name, self.chain)
1464

    
1465
    def get_project(self):
1466
        try:
1467
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1468
            return Project
1469
        except Project.DoesNotExist, e:
1470
            return None
1471

    
1472
    def state_display(self):
1473
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1474

    
1475
    def project_state_display(self):
1476
        try:
1477
            project = self.project
1478
            return project.state_display()
1479
        except Project.DoesNotExist:
1480
            return self.state_display()
1481

    
1482
    def add_resource_policy(self, service, resource, uplimit):
1483
        """Raises ObjectDoesNotExist, IntegrityError"""
1484
        q = self.projectresourcegrant_set
1485
        resource = Resource.objects.get(name=resource)
1486
        q.create(resource=resource, member_capacity=uplimit)
1487

    
1488
    def members_count(self):
1489
        return self.project.approved_memberships.count()
1490

    
1491
    @property
1492
    def grants(self):
1493
        return self.projectresourcegrant_set.values('member_capacity',
1494
                                                    'resource__name')
1495

    
1496
    @property
1497
    def resource_policies(self):
1498
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1499

    
1500
    @resource_policies.setter
1501
    def resource_policies(self, policies):
1502
        for p in policies:
1503
            service = p.get('service', None)
1504
            resource = p.get('resource', None)
1505
            uplimit = p.get('uplimit', 0)
1506
            self.add_resource_policy(service, resource, uplimit)
1507

    
1508
    def pending_modifications_incl_me(self):
1509
        q = self.chained_applications()
1510
        q = q.filter(Q(state=self.PENDING))
1511
        return q
1512

    
1513
    def last_pending_incl_me(self):
1514
        try:
1515
            return self.pending_modifications_incl_me().order_by('-id')[0]
1516
        except IndexError:
1517
            return None
1518

    
1519
    def pending_modifications(self):
1520
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1521

    
1522
    def last_pending(self):
1523
        try:
1524
            return self.pending_modifications().order_by('-id')[0]
1525
        except IndexError:
1526
            return None
1527

    
1528
    def is_modification(self):
1529
        # if self.state != self.PENDING:
1530
        #     return False
1531
        parents = self.chained_applications().filter(id__lt=self.id)
1532
        parents = parents.filter(state__in=[self.APPROVED])
1533
        return parents.count() > 0
1534

    
1535
    def chained_applications(self):
1536
        return ProjectApplication.objects.filter(chain=self.chain)
1537

    
1538
    def is_latest(self):
1539
        return self.chained_applications().order_by('-id')[0] == self
1540

    
1541
    def has_pending_modifications(self):
1542
        return bool(self.last_pending())
1543

    
1544
    def denied_modifications(self):
1545
        q = self.chained_applications()
1546
        q = q.filter(Q(state=self.DENIED))
1547
        q = q.filter(~Q(id=self.id))
1548
        return q
1549

    
1550
    def last_denied(self):
1551
        try:
1552
            return self.denied_modifications().order_by('-id')[0]
1553
        except IndexError:
1554
            return None
1555

    
1556
    def has_denied_modifications(self):
1557
        return bool(self.last_denied())
1558

    
1559
    def is_applied(self):
1560
        try:
1561
            self.project
1562
            return True
1563
        except Project.DoesNotExist:
1564
            return False
1565

    
1566
    def get_project(self):
1567
        try:
1568
            return Project.objects.get(id=self.chain)
1569
        except Project.DoesNotExist:
1570
            return None
1571

    
1572
    def project_exists(self):
1573
        return self.get_project() is not None
1574

    
1575
    def _get_project_for_update(self):
1576
        try:
1577
            objects = Project.objects
1578
            project = objects.get_for_update(id=self.chain)
1579
            return project
1580
        except Project.DoesNotExist:
1581
            return None
1582

    
1583
    def can_cancel(self):
1584
        return self.state == self.PENDING
1585

    
1586
    def cancel(self):
1587
        if not self.can_cancel():
1588
            m = _("cannot cancel: application '%s' in state '%s'") % (
1589
                    self.id, self.state)
1590
            raise AssertionError(m)
1591

    
1592
        self.state = self.CANCELLED
1593
        self.save()
1594

    
1595
    def can_dismiss(self):
1596
        return self.state == self.DENIED
1597

    
1598
    def dismiss(self):
1599
        if not self.can_dismiss():
1600
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1601
                    self.id, self.state)
1602
            raise AssertionError(m)
1603

    
1604
        self.state = self.DISMISSED
1605
        self.save()
1606

    
1607
    def can_deny(self):
1608
        return self.state == self.PENDING
1609

    
1610
    def deny(self, reason):
1611
        if not self.can_deny():
1612
            m = _("cannot deny: application '%s' in state '%s'") % (
1613
                    self.id, self.state)
1614
            raise AssertionError(m)
1615

    
1616
        self.state = self.DENIED
1617
        self.response_date = datetime.now()
1618
        self.response = reason
1619
        self.save()
1620

    
1621
    def can_approve(self):
1622
        return self.state == self.PENDING
1623

    
1624
    def approve(self, approval_user=None):
1625
        """
1626
        If approval_user then during owner membership acceptance
1627
        it is checked whether the request_user is eligible.
1628

1629
        Raises:
1630
            PermissionDenied
1631
        """
1632

    
1633
        if not transaction.is_managed():
1634
            raise AssertionError("NOPE")
1635

    
1636
        new_project_name = self.name
1637
        if not self.can_approve():
1638
            m = _("cannot approve: project '%s' in state '%s'") % (
1639
                    new_project_name, self.state)
1640
            raise AssertionError(m) # invalid argument
1641

    
1642
        now = datetime.now()
1643
        project = self._get_project_for_update()
1644

    
1645
        try:
1646
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1647
            conflicting_project = Project.objects.get(q)
1648
            if (conflicting_project != project):
1649
                m = (_("cannot approve: project with name '%s' "
1650
                       "already exists (id: %s)") % (
1651
                        new_project_name, conflicting_project.id))
1652
                raise PermissionDenied(m) # invalid argument
1653
        except Project.DoesNotExist:
1654
            pass
1655

    
1656
        new_project = False
1657
        if project is None:
1658
            new_project = True
1659
            project = Project(id=self.chain)
1660

    
1661
        project.name = new_project_name
1662
        project.application = self
1663
        project.last_approval_date = now
1664

    
1665
        project.save()
1666

    
1667
        self.state = self.APPROVED
1668
        self.response_date = now
1669
        self.save()
1670
        return project
1671

    
1672
    @property
1673
    def member_join_policy_display(self):
1674
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1675

    
1676
    @property
1677
    def member_leave_policy_display(self):
1678
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1679

    
1680
class ProjectResourceGrant(models.Model):
1681

    
1682
    resource                =   models.ForeignKey(Resource)
1683
    project_application     =   models.ForeignKey(ProjectApplication,
1684
                                                  null=True)
1685
    project_capacity        =   intDecimalField(null=True)
1686
    member_capacity         =   intDecimalField(default=0)
1687

    
1688
    objects = ExtendedManager()
1689

    
1690
    class Meta:
1691
        unique_together = ("resource", "project_application")
1692

    
1693
    def display_member_capacity(self):
1694
        if self.member_capacity:
1695
            if self.resource.unit:
1696
                return ProjectResourceGrant.display_filesize(
1697
                    self.member_capacity)
1698
            else:
1699
                if math.isinf(self.member_capacity):
1700
                    return 'Unlimited'
1701
                else:
1702
                    return self.member_capacity
1703
        else:
1704
            return 'Unlimited'
1705

    
1706
    def __str__(self):
1707
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1708
                                        self.display_member_capacity())
1709

    
1710
    @classmethod
1711
    def display_filesize(cls, value):
1712
        try:
1713
            value = float(value)
1714
        except:
1715
            return
1716
        else:
1717
            if math.isinf(value):
1718
                return 'Unlimited'
1719
            if value > 1:
1720
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1721
                                [0, 0, 0, 0, 0, 0])
1722
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1723
                quotient = float(value) / 1024**exponent
1724
                unit, value_decimals = unit_list[exponent]
1725
                format_string = '{0:.%sf} {1}' % (value_decimals)
1726
                return format_string.format(quotient, unit)
1727
            if value == 0:
1728
                return '0 bytes'
1729
            if value == 1:
1730
                return '1 byte'
1731
            else:
1732
               return '0'
1733

    
1734

    
1735
class ProjectManager(ForUpdateManager):
1736

    
1737
    def terminated_projects(self):
1738
        q = self.model.Q_TERMINATED
1739
        return self.filter(q)
1740

    
1741
    def not_terminated_projects(self):
1742
        q = ~self.model.Q_TERMINATED
1743
        return self.filter(q)
1744

    
1745
    def deactivated_projects(self):
1746
        q = self.model.Q_DEACTIVATED
1747
        return self.filter(q)
1748

    
1749
    def expired_projects(self):
1750
        q = (~Q(state=Project.TERMINATED) &
1751
              Q(application__end_date__lt=datetime.now()))
1752
        return self.filter(q)
1753

    
1754
    def search_by_name(self, *search_strings):
1755
        q = Q()
1756
        for s in search_strings:
1757
            q = q | Q(name__icontains=s)
1758
        return self.filter(q)
1759

    
1760

    
1761
class Project(models.Model):
1762

    
1763
    id                          =   models.OneToOneField(Chain,
1764
                                                      related_name='chained_project',
1765
                                                      db_column='id',
1766
                                                      primary_key=True)
1767

    
1768
    application                 =   models.OneToOneField(
1769
                                            ProjectApplication,
1770
                                            related_name='project')
1771
    last_approval_date          =   models.DateTimeField(null=True)
1772

    
1773
    members                     =   models.ManyToManyField(
1774
                                            AstakosUser,
1775
                                            through='ProjectMembership')
1776

    
1777
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1778
    deactivation_date           =   models.DateTimeField(null=True)
1779

    
1780
    creation_date               =   models.DateTimeField(auto_now_add=True)
1781
    name                        =   models.CharField(
1782
                                            max_length=80,
1783
                                            null=True,
1784
                                            db_index=True,
1785
                                            unique=True)
1786

    
1787
    APPROVED    = 1
1788
    SUSPENDED   = 10
1789
    TERMINATED  = 100
1790

    
1791
    state                       =   models.IntegerField(default=APPROVED,
1792
                                                        db_index=True)
1793

    
1794
    objects     =   ProjectManager()
1795

    
1796
    # Compiled queries
1797
    Q_TERMINATED  = Q(state=TERMINATED)
1798
    Q_SUSPENDED   = Q(state=SUSPENDED)
1799
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1800

    
1801
    def __str__(self):
1802
        return uenc(_("<project %s '%s'>") %
1803
                    (self.id, udec(self.application.name)))
1804

    
1805
    __repr__ = __str__
1806

    
1807
    def __unicode__(self):
1808
        return _("<project %s '%s'>") % (self.id, self.application.name)
1809

    
1810
    STATE_DISPLAY = {
1811
        APPROVED   : 'Active',
1812
        SUSPENDED  : 'Suspended',
1813
        TERMINATED : 'Terminated'
1814
        }
1815

    
1816
    def state_display(self):
1817
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1818

    
1819
    def expiration_info(self):
1820
        return (str(self.id), self.name, self.state_display(),
1821
                str(self.application.end_date))
1822

    
1823
    def is_deactivated(self, reason=None):
1824
        if reason is not None:
1825
            return self.state == reason
1826

    
1827
        return self.state != self.APPROVED
1828

    
1829
    ### Deactivation calls
1830

    
1831
    def terminate(self):
1832
        self.deactivation_reason = 'TERMINATED'
1833
        self.deactivation_date = datetime.now()
1834
        self.state = self.TERMINATED
1835
        self.name = None
1836
        self.save()
1837

    
1838
    def suspend(self):
1839
        self.deactivation_reason = 'SUSPENDED'
1840
        self.deactivation_date = datetime.now()
1841
        self.state = self.SUSPENDED
1842
        self.save()
1843

    
1844
    def resume(self):
1845
        self.deactivation_reason = None
1846
        self.deactivation_date = None
1847
        self.state = self.APPROVED
1848
        self.save()
1849

    
1850
    ### Logical checks
1851

    
1852
    def is_inconsistent(self):
1853
        now = datetime.now()
1854
        dates = [self.creation_date,
1855
                 self.last_approval_date,
1856
                 self.deactivation_date]
1857
        return any([date > now for date in dates])
1858

    
1859
    def is_approved(self):
1860
        return self.state == self.APPROVED
1861

    
1862
    @property
1863
    def is_alive(self):
1864
        return not self.is_terminated
1865

    
1866
    @property
1867
    def is_terminated(self):
1868
        return self.is_deactivated(self.TERMINATED)
1869

    
1870
    @property
1871
    def is_suspended(self):
1872
        return self.is_deactivated(self.SUSPENDED)
1873

    
1874
    def violates_resource_grants(self):
1875
        return False
1876

    
1877
    def violates_members_limit(self, adding=0):
1878
        application = self.application
1879
        limit = application.limit_on_members_number
1880
        if limit is None:
1881
            return False
1882
        return (len(self.approved_members) + adding > limit)
1883

    
1884

    
1885
    ### Other
1886

    
1887
    def count_pending_memberships(self):
1888
        memb_set = self.projectmembership_set
1889
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1890
        return memb_count
1891

    
1892
    def members_count(self):
1893
        return self.approved_memberships.count()
1894

    
1895
    @property
1896
    def approved_memberships(self):
1897
        query = ProjectMembership.Q_ACCEPTED_STATES
1898
        return self.projectmembership_set.filter(query)
1899

    
1900
    @property
1901
    def approved_members(self):
1902
        return [m.person for m in self.approved_memberships]
1903

    
1904
    def add_member(self, user):
1905
        """
1906
        Raises:
1907
            django.exceptions.PermissionDenied
1908
            astakos.im.models.AstakosUser.DoesNotExist
1909
        """
1910
        if isinstance(user, (int, long)):
1911
            user = AstakosUser.objects.get(user=user)
1912

    
1913
        m, created = ProjectMembership.objects.get_or_create(
1914
            person=user, project=self
1915
        )
1916
        m.accept()
1917

    
1918
    def remove_member(self, user):
1919
        """
1920
        Raises:
1921
            django.exceptions.PermissionDenied
1922
            astakos.im.models.AstakosUser.DoesNotExist
1923
            astakos.im.models.ProjectMembership.DoesNotExist
1924
        """
1925
        if isinstance(user, (int, long)):
1926
            user = AstakosUser.objects.get(user=user)
1927

    
1928
        m = ProjectMembership.objects.get(person=user, project=self)
1929
        m.remove()
1930

    
1931

    
1932
CHAIN_STATE = {
1933
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1934
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1935
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1936
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1937
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1938

    
1939
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1940
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1941
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1942
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1943
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1944

    
1945
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1946
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1947
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1948
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1949
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1950

    
1951
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1952
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1953
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1954
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1955
    }
1956

    
1957

    
1958
class ProjectMembershipManager(ForUpdateManager):
1959

    
1960
    def any_accepted(self):
1961
        q = self.model.Q_ACTUALLY_ACCEPTED
1962
        return self.filter(q)
1963

    
1964
    def actually_accepted(self):
1965
        q = self.model.Q_ACTUALLY_ACCEPTED
1966
        return self.filter(q)
1967

    
1968
    def requested(self):
1969
        return self.filter(state=ProjectMembership.REQUESTED)
1970

    
1971
    def suspended(self):
1972
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1973

    
1974
class ProjectMembership(models.Model):
1975

    
1976
    person              =   models.ForeignKey(AstakosUser)
1977
    request_date        =   models.DateField(auto_now_add=True)
1978
    project             =   models.ForeignKey(Project)
1979

    
1980
    REQUESTED           =   0
1981
    ACCEPTED            =   1
1982
    LEAVE_REQUESTED     =   5
1983
    # User deactivation
1984
    USER_SUSPENDED      =   10
1985

    
1986
    REMOVED             =   200
1987

    
1988
    ASSOCIATED_STATES   =   set([REQUESTED,
1989
                                 ACCEPTED,
1990
                                 LEAVE_REQUESTED,
1991
                                 USER_SUSPENDED,
1992
                                 ])
1993

    
1994
    ACCEPTED_STATES     =   set([ACCEPTED,
1995
                                 LEAVE_REQUESTED,
1996
                                 USER_SUSPENDED,
1997
                                 ])
1998

    
1999
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2000

    
2001
    state               =   models.IntegerField(default=REQUESTED,
2002
                                                db_index=True)
2003
    acceptance_date     =   models.DateField(null=True, db_index=True)
2004
    leave_request_date  =   models.DateField(null=True)
2005

    
2006
    objects     =   ProjectMembershipManager()
2007

    
2008
    # Compiled queries
2009
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2010
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2011

    
2012
    MEMBERSHIP_STATE_DISPLAY = {
2013
        REQUESTED           : _('Requested'),
2014
        ACCEPTED            : _('Accepted'),
2015
        LEAVE_REQUESTED     : _('Leave Requested'),
2016
        USER_SUSPENDED      : _('Suspended'),
2017
        REMOVED             : _('Pending removal'),
2018
        }
2019

    
2020
    USER_FRIENDLY_STATE_DISPLAY = {
2021
        REQUESTED           : _('Join requested'),
2022
        ACCEPTED            : _('Accepted member'),
2023
        LEAVE_REQUESTED     : _('Requested to leave'),
2024
        USER_SUSPENDED      : _('Suspended member'),
2025
        REMOVED             : _('Pending removal'),
2026
        }
2027

    
2028
    def state_display(self):
2029
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2030

    
2031
    def user_friendly_state_display(self):
2032
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2033

    
2034
    class Meta:
2035
        unique_together = ("person", "project")
2036
        #index_together = [["project", "state"]]
2037

    
2038
    def __str__(self):
2039
        return uenc(_("<'%s' membership in '%s'>") % (
2040
                self.person.username, self.project))
2041

    
2042
    __repr__ = __str__
2043

    
2044
    def __init__(self, *args, **kwargs):
2045
        self.state = self.REQUESTED
2046
        super(ProjectMembership, self).__init__(*args, **kwargs)
2047

    
2048
    def _set_history_item(self, reason, date=None):
2049
        if isinstance(reason, basestring):
2050
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2051

    
2052
        history_item = ProjectMembershipHistory(
2053
                            serial=self.id,
2054
                            person=self.person_id,
2055
                            project=self.project_id,
2056
                            date=date or datetime.now(),
2057
                            reason=reason)
2058
        history_item.save()
2059
        serial = history_item.id
2060

    
2061
    def can_accept(self):
2062
        return self.state == self.REQUESTED
2063

    
2064
    def accept(self):
2065
        if not self.can_accept():
2066
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2067
            raise AssertionError(m)
2068

    
2069
        now = datetime.now()
2070
        self.acceptance_date = now
2071
        self._set_history_item(reason='ACCEPT', date=now)
2072
        self.state = self.ACCEPTED
2073
        self.save()
2074

    
2075
    def can_leave(self):
2076
        return self.state in self.ACCEPTED_STATES
2077

    
2078
    def leave_request(self):
2079
        if not self.can_leave():
2080
            m = _("%s: attempt to request to leave in state '%s'") % (
2081
                self, self.state)
2082
            raise AssertionError(m)
2083

    
2084
        self.leave_request_date = datetime.now()
2085
        self.state = self.LEAVE_REQUESTED
2086
        self.save()
2087

    
2088
    def can_deny_leave(self):
2089
        return self.state == self.LEAVE_REQUESTED
2090

    
2091
    def leave_request_deny(self):
2092
        if not self.can_deny_leave():
2093
            m = _("%s: attempt to deny leave request in state '%s'") % (
2094
                self, self.state)
2095
            raise AssertionError(m)
2096

    
2097
        self.leave_request_date = None
2098
        self.state = self.ACCEPTED
2099
        self.save()
2100

    
2101
    def can_cancel_leave(self):
2102
        return self.state == self.LEAVE_REQUESTED
2103

    
2104
    def leave_request_cancel(self):
2105
        if not self.can_cancel_leave():
2106
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2107
                self, self.state)
2108
            raise AssertionError(m)
2109

    
2110
        self.leave_request_date = None
2111
        self.state = self.ACCEPTED
2112
        self.save()
2113

    
2114
    def can_remove(self):
2115
        return self.state in self.ACCEPTED_STATES
2116

    
2117
    def remove(self):
2118
        if not self.can_remove():
2119
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2120
            raise AssertionError(m)
2121

    
2122
        self._set_history_item(reason='REMOVE')
2123
        self.delete()
2124

    
2125
    def can_reject(self):
2126
        return self.state == self.REQUESTED
2127

    
2128
    def reject(self):
2129
        if not self.can_reject():
2130
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2131
            raise AssertionError(m)
2132

    
2133
        # rejected requests don't need sync,
2134
        # because they were never effected
2135
        self._set_history_item(reason='REJECT')
2136
        self.delete()
2137

    
2138
    def can_cancel(self):
2139
        return self.state == self.REQUESTED
2140

    
2141
    def cancel(self):
2142
        if not self.can_cancel():
2143
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2144
            raise AssertionError(m)
2145

    
2146
        # rejected requests don't need sync,
2147
        # because they were never effected
2148
        self._set_history_item(reason='CANCEL')
2149
        self.delete()
2150

    
2151

    
2152
class Serial(models.Model):
2153
    serial  =   models.AutoField(primary_key=True)
2154

    
2155

    
2156
class ProjectMembershipHistory(models.Model):
2157
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2158
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2159

    
2160
    person  =   models.BigIntegerField()
2161
    project =   models.BigIntegerField()
2162
    date    =   models.DateField(auto_now_add=True)
2163
    reason  =   models.IntegerField()
2164
    serial  =   models.BigIntegerField()
2165

    
2166
### SIGNALS ###
2167
################
2168

    
2169
def create_astakos_user(u):
2170
    try:
2171
        AstakosUser.objects.get(user_ptr=u.pk)
2172
    except AstakosUser.DoesNotExist:
2173
        extended_user = AstakosUser(user_ptr_id=u.pk)
2174
        extended_user.__dict__.update(u.__dict__)
2175
        extended_user.save()
2176
        if not extended_user.has_auth_provider('local'):
2177
            extended_user.add_auth_provider('local')
2178
    except BaseException, e:
2179
        logger.exception(e)
2180

    
2181
def fix_superusers():
2182
    # Associate superusers with AstakosUser
2183
    admins = User.objects.filter(is_superuser=True)
2184
    for u in admins:
2185
        create_astakos_user(u)
2186

    
2187
def user_post_save(sender, instance, created, **kwargs):
2188
    if not created:
2189
        return
2190
    create_astakos_user(instance)
2191
post_save.connect(user_post_save, sender=User)
2192

    
2193
def astakosuser_post_save(sender, instance, created, **kwargs):
2194
    pass
2195

    
2196
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2197

    
2198
def resource_post_save(sender, instance, created, **kwargs):
2199
    pass
2200

    
2201
post_save.connect(resource_post_save, sender=Resource)
2202

    
2203
def renew_token(sender, instance, **kwargs):
2204
    if not instance.auth_token:
2205
        instance.renew_token()
2206
pre_save.connect(renew_token, sender=AstakosUser)
2207
pre_save.connect(renew_token, sender=Service)