Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 64d0c13e

History | View | Annotate | Download (73.4 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, 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
    def set_resource_policies(self, policies):
1501
        for resource, uplimit in policies:
1502
            self.add_resource_policy(resource, uplimit)
1503

    
1504
    def pending_modifications_incl_me(self):
1505
        q = self.chained_applications()
1506
        q = q.filter(Q(state=self.PENDING))
1507
        return q
1508

    
1509
    def last_pending_incl_me(self):
1510
        try:
1511
            return self.pending_modifications_incl_me().order_by('-id')[0]
1512
        except IndexError:
1513
            return None
1514

    
1515
    def pending_modifications(self):
1516
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1517

    
1518
    def last_pending(self):
1519
        try:
1520
            return self.pending_modifications().order_by('-id')[0]
1521
        except IndexError:
1522
            return None
1523

    
1524
    def is_modification(self):
1525
        # if self.state != self.PENDING:
1526
        #     return False
1527
        parents = self.chained_applications().filter(id__lt=self.id)
1528
        parents = parents.filter(state__in=[self.APPROVED])
1529
        return parents.count() > 0
1530

    
1531
    def chained_applications(self):
1532
        return ProjectApplication.objects.filter(chain=self.chain)
1533

    
1534
    def is_latest(self):
1535
        return self.chained_applications().order_by('-id')[0] == self
1536

    
1537
    def has_pending_modifications(self):
1538
        return bool(self.last_pending())
1539

    
1540
    def denied_modifications(self):
1541
        q = self.chained_applications()
1542
        q = q.filter(Q(state=self.DENIED))
1543
        q = q.filter(~Q(id=self.id))
1544
        return q
1545

    
1546
    def last_denied(self):
1547
        try:
1548
            return self.denied_modifications().order_by('-id')[0]
1549
        except IndexError:
1550
            return None
1551

    
1552
    def has_denied_modifications(self):
1553
        return bool(self.last_denied())
1554

    
1555
    def is_applied(self):
1556
        try:
1557
            self.project
1558
            return True
1559
        except Project.DoesNotExist:
1560
            return False
1561

    
1562
    def get_project(self):
1563
        try:
1564
            return Project.objects.get(id=self.chain)
1565
        except Project.DoesNotExist:
1566
            return None
1567

    
1568
    def project_exists(self):
1569
        return self.get_project() is not None
1570

    
1571
    def _get_project_for_update(self):
1572
        try:
1573
            objects = Project.objects
1574
            project = objects.get_for_update(id=self.chain)
1575
            return project
1576
        except Project.DoesNotExist:
1577
            return None
1578

    
1579
    def can_cancel(self):
1580
        return self.state == self.PENDING
1581

    
1582
    def cancel(self):
1583
        if not self.can_cancel():
1584
            m = _("cannot cancel: application '%s' in state '%s'") % (
1585
                    self.id, self.state)
1586
            raise AssertionError(m)
1587

    
1588
        self.state = self.CANCELLED
1589
        self.save()
1590

    
1591
    def can_dismiss(self):
1592
        return self.state == self.DENIED
1593

    
1594
    def dismiss(self):
1595
        if not self.can_dismiss():
1596
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1597
                    self.id, self.state)
1598
            raise AssertionError(m)
1599

    
1600
        self.state = self.DISMISSED
1601
        self.save()
1602

    
1603
    def can_deny(self):
1604
        return self.state == self.PENDING
1605

    
1606
    def deny(self, reason):
1607
        if not self.can_deny():
1608
            m = _("cannot deny: application '%s' in state '%s'") % (
1609
                    self.id, self.state)
1610
            raise AssertionError(m)
1611

    
1612
        self.state = self.DENIED
1613
        self.response_date = datetime.now()
1614
        self.response = reason
1615
        self.save()
1616

    
1617
    def can_approve(self):
1618
        return self.state == self.PENDING
1619

    
1620
    def approve(self, reason):
1621
        new_project_name = self.name
1622
        if not self.can_approve():
1623
            m = _("cannot approve: project '%s' in state '%s'") % (
1624
                    new_project_name, self.state)
1625
            raise AssertionError(m) # invalid argument
1626

    
1627
        now = datetime.now()
1628
        project = self._get_project_for_update()
1629

    
1630
        try:
1631
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1632
            conflicting_project = Project.objects.get(q)
1633
            if (conflicting_project != project):
1634
                m = (_("cannot approve: project with name '%s' "
1635
                       "already exists (id: %s)") % (
1636
                        new_project_name, conflicting_project.id))
1637
                raise PermissionDenied(m) # invalid argument
1638
        except Project.DoesNotExist:
1639
            pass
1640

    
1641
        new_project = False
1642
        if project is None:
1643
            new_project = True
1644
            project = Project(id=self.chain)
1645

    
1646
        project.name = new_project_name
1647
        project.application = self
1648
        project.last_approval_date = now
1649

    
1650
        project.save()
1651

    
1652
        self.state = self.APPROVED
1653
        self.response_date = now
1654
        self.response = reason
1655
        self.save()
1656
        return project
1657

    
1658
    @property
1659
    def member_join_policy_display(self):
1660
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1661

    
1662
    @property
1663
    def member_leave_policy_display(self):
1664
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1665

    
1666
class ProjectResourceGrant(models.Model):
1667

    
1668
    resource                =   models.ForeignKey(Resource)
1669
    project_application     =   models.ForeignKey(ProjectApplication,
1670
                                                  null=True)
1671
    project_capacity        =   intDecimalField(null=True)
1672
    member_capacity         =   intDecimalField(default=0)
1673

    
1674
    objects = ExtendedManager()
1675

    
1676
    class Meta:
1677
        unique_together = ("resource", "project_application")
1678

    
1679
    def display_member_capacity(self):
1680
        if self.member_capacity:
1681
            if self.resource.unit:
1682
                return ProjectResourceGrant.display_filesize(
1683
                    self.member_capacity)
1684
            else:
1685
                if math.isinf(self.member_capacity):
1686
                    return 'Unlimited'
1687
                else:
1688
                    return self.member_capacity
1689
        else:
1690
            return 'Unlimited'
1691

    
1692
    def __str__(self):
1693
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1694
                                        self.display_member_capacity())
1695

    
1696
    @classmethod
1697
    def display_filesize(cls, value):
1698
        try:
1699
            value = float(value)
1700
        except:
1701
            return
1702
        else:
1703
            if math.isinf(value):
1704
                return 'Unlimited'
1705
            if value > 1:
1706
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1707
                                [0, 0, 0, 0, 0, 0])
1708
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1709
                quotient = float(value) / 1024**exponent
1710
                unit, value_decimals = unit_list[exponent]
1711
                format_string = '{0:.%sf} {1}' % (value_decimals)
1712
                return format_string.format(quotient, unit)
1713
            if value == 0:
1714
                return '0 bytes'
1715
            if value == 1:
1716
                return '1 byte'
1717
            else:
1718
               return '0'
1719

    
1720

    
1721
class ProjectManager(ForUpdateManager):
1722

    
1723
    def terminated_projects(self):
1724
        q = self.model.Q_TERMINATED
1725
        return self.filter(q)
1726

    
1727
    def not_terminated_projects(self):
1728
        q = ~self.model.Q_TERMINATED
1729
        return self.filter(q)
1730

    
1731
    def deactivated_projects(self):
1732
        q = self.model.Q_DEACTIVATED
1733
        return self.filter(q)
1734

    
1735
    def expired_projects(self):
1736
        q = (~Q(state=Project.TERMINATED) &
1737
              Q(application__end_date__lt=datetime.now()))
1738
        return self.filter(q)
1739

    
1740
    def search_by_name(self, *search_strings):
1741
        q = Q()
1742
        for s in search_strings:
1743
            q = q | Q(name__icontains=s)
1744
        return self.filter(q)
1745

    
1746

    
1747
class Project(models.Model):
1748

    
1749
    id                          =   models.OneToOneField(Chain,
1750
                                                      related_name='chained_project',
1751
                                                      db_column='id',
1752
                                                      primary_key=True)
1753

    
1754
    application                 =   models.OneToOneField(
1755
                                            ProjectApplication,
1756
                                            related_name='project')
1757
    last_approval_date          =   models.DateTimeField(null=True)
1758

    
1759
    members                     =   models.ManyToManyField(
1760
                                            AstakosUser,
1761
                                            through='ProjectMembership')
1762

    
1763
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1764
    deactivation_date           =   models.DateTimeField(null=True)
1765

    
1766
    creation_date               =   models.DateTimeField(auto_now_add=True)
1767
    name                        =   models.CharField(
1768
                                            max_length=80,
1769
                                            null=True,
1770
                                            db_index=True,
1771
                                            unique=True)
1772

    
1773
    APPROVED    = 1
1774
    SUSPENDED   = 10
1775
    TERMINATED  = 100
1776

    
1777
    state                       =   models.IntegerField(default=APPROVED,
1778
                                                        db_index=True)
1779

    
1780
    objects     =   ProjectManager()
1781

    
1782
    # Compiled queries
1783
    Q_TERMINATED  = Q(state=TERMINATED)
1784
    Q_SUSPENDED   = Q(state=SUSPENDED)
1785
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1786

    
1787
    def __str__(self):
1788
        return uenc(_("<project %s '%s'>") %
1789
                    (self.id, udec(self.application.name)))
1790

    
1791
    __repr__ = __str__
1792

    
1793
    def __unicode__(self):
1794
        return _("<project %s '%s'>") % (self.id, self.application.name)
1795

    
1796
    STATE_DISPLAY = {
1797
        APPROVED   : 'Active',
1798
        SUSPENDED  : 'Suspended',
1799
        TERMINATED : 'Terminated'
1800
        }
1801

    
1802
    def state_display(self):
1803
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1804

    
1805
    def expiration_info(self):
1806
        return (str(self.id), self.name, self.state_display(),
1807
                str(self.application.end_date))
1808

    
1809
    def is_deactivated(self, reason=None):
1810
        if reason is not None:
1811
            return self.state == reason
1812

    
1813
        return self.state != self.APPROVED
1814

    
1815
    ### Deactivation calls
1816

    
1817
    def terminate(self):
1818
        self.deactivation_reason = 'TERMINATED'
1819
        self.deactivation_date = datetime.now()
1820
        self.state = self.TERMINATED
1821
        self.name = None
1822
        self.save()
1823

    
1824
    def suspend(self):
1825
        self.deactivation_reason = 'SUSPENDED'
1826
        self.deactivation_date = datetime.now()
1827
        self.state = self.SUSPENDED
1828
        self.save()
1829

    
1830
    def resume(self):
1831
        self.deactivation_reason = None
1832
        self.deactivation_date = None
1833
        self.state = self.APPROVED
1834
        self.save()
1835

    
1836
    ### Logical checks
1837

    
1838
    def is_inconsistent(self):
1839
        now = datetime.now()
1840
        dates = [self.creation_date,
1841
                 self.last_approval_date,
1842
                 self.deactivation_date]
1843
        return any([date > now for date in dates])
1844

    
1845
    def is_approved(self):
1846
        return self.state == self.APPROVED
1847

    
1848
    @property
1849
    def is_alive(self):
1850
        return not self.is_terminated
1851

    
1852
    @property
1853
    def is_terminated(self):
1854
        return self.is_deactivated(self.TERMINATED)
1855

    
1856
    @property
1857
    def is_suspended(self):
1858
        return self.is_deactivated(self.SUSPENDED)
1859

    
1860
    def violates_resource_grants(self):
1861
        return False
1862

    
1863
    def violates_members_limit(self, adding=0):
1864
        application = self.application
1865
        limit = application.limit_on_members_number
1866
        if limit is None:
1867
            return False
1868
        return (len(self.approved_members) + adding > limit)
1869

    
1870

    
1871
    ### Other
1872

    
1873
    def count_pending_memberships(self):
1874
        memb_set = self.projectmembership_set
1875
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1876
        return memb_count
1877

    
1878
    def members_count(self):
1879
        return self.approved_memberships.count()
1880

    
1881
    @property
1882
    def approved_memberships(self):
1883
        query = ProjectMembership.Q_ACCEPTED_STATES
1884
        return self.projectmembership_set.filter(query)
1885

    
1886
    @property
1887
    def approved_members(self):
1888
        return [m.person for m in self.approved_memberships]
1889

    
1890
    def add_member(self, user):
1891
        """
1892
        Raises:
1893
            astakos.im.models.AstakosUser.DoesNotExist
1894
        """
1895
        if isinstance(user, (int, long)):
1896
            user = AstakosUser.objects.get(user=user)
1897

    
1898
        m, created = ProjectMembership.objects.get_or_create(
1899
            person=user, project=self
1900
        )
1901
        m.accept()
1902

    
1903
    def remove_member(self, user):
1904
        """
1905
        Raises:
1906
            astakos.im.models.AstakosUser.DoesNotExist
1907
            astakos.im.models.ProjectMembership.DoesNotExist
1908
        """
1909
        if isinstance(user, (int, long)):
1910
            user = AstakosUser.objects.get(user=user)
1911

    
1912
        m = ProjectMembership.objects.get(person=user, project=self)
1913
        m.remove()
1914

    
1915

    
1916
CHAIN_STATE = {
1917
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1918
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1919
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1920
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1921
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1922

    
1923
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1924
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1925
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1926
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1927
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1928

    
1929
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1930
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1931
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1932
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1933
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1934

    
1935
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1936
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1937
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1938
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1939
    }
1940

    
1941

    
1942
class ProjectMembershipManager(ForUpdateManager):
1943

    
1944
    def any_accepted(self):
1945
        q = self.model.Q_ACTUALLY_ACCEPTED
1946
        return self.filter(q)
1947

    
1948
    def actually_accepted(self):
1949
        q = self.model.Q_ACTUALLY_ACCEPTED
1950
        return self.filter(q)
1951

    
1952
    def requested(self):
1953
        return self.filter(state=ProjectMembership.REQUESTED)
1954

    
1955
    def suspended(self):
1956
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1957

    
1958
class ProjectMembership(models.Model):
1959

    
1960
    person              =   models.ForeignKey(AstakosUser)
1961
    request_date        =   models.DateField(auto_now_add=True)
1962
    project             =   models.ForeignKey(Project)
1963

    
1964
    REQUESTED           =   0
1965
    ACCEPTED            =   1
1966
    LEAVE_REQUESTED     =   5
1967
    # User deactivation
1968
    USER_SUSPENDED      =   10
1969

    
1970
    REMOVED             =   200
1971

    
1972
    ASSOCIATED_STATES   =   set([REQUESTED,
1973
                                 ACCEPTED,
1974
                                 LEAVE_REQUESTED,
1975
                                 USER_SUSPENDED,
1976
                                 ])
1977

    
1978
    ACCEPTED_STATES     =   set([ACCEPTED,
1979
                                 LEAVE_REQUESTED,
1980
                                 USER_SUSPENDED,
1981
                                 ])
1982

    
1983
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1984

    
1985
    state               =   models.IntegerField(default=REQUESTED,
1986
                                                db_index=True)
1987
    acceptance_date     =   models.DateField(null=True, db_index=True)
1988
    leave_request_date  =   models.DateField(null=True)
1989

    
1990
    objects     =   ProjectMembershipManager()
1991

    
1992
    # Compiled queries
1993
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1994
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1995

    
1996
    MEMBERSHIP_STATE_DISPLAY = {
1997
        REQUESTED           : _('Requested'),
1998
        ACCEPTED            : _('Accepted'),
1999
        LEAVE_REQUESTED     : _('Leave Requested'),
2000
        USER_SUSPENDED      : _('Suspended'),
2001
        REMOVED             : _('Pending removal'),
2002
        }
2003

    
2004
    USER_FRIENDLY_STATE_DISPLAY = {
2005
        REQUESTED           : _('Join requested'),
2006
        ACCEPTED            : _('Accepted member'),
2007
        LEAVE_REQUESTED     : _('Requested to leave'),
2008
        USER_SUSPENDED      : _('Suspended member'),
2009
        REMOVED             : _('Pending removal'),
2010
        }
2011

    
2012
    def state_display(self):
2013
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2014

    
2015
    def user_friendly_state_display(self):
2016
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2017

    
2018
    class Meta:
2019
        unique_together = ("person", "project")
2020
        #index_together = [["project", "state"]]
2021

    
2022
    def __str__(self):
2023
        return uenc(_("<'%s' membership in '%s'>") % (
2024
                self.person.username, self.project))
2025

    
2026
    __repr__ = __str__
2027

    
2028
    def __init__(self, *args, **kwargs):
2029
        self.state = self.REQUESTED
2030
        super(ProjectMembership, self).__init__(*args, **kwargs)
2031

    
2032
    def _set_history_item(self, reason, date=None):
2033
        if isinstance(reason, basestring):
2034
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2035

    
2036
        history_item = ProjectMembershipHistory(
2037
                            serial=self.id,
2038
                            person=self.person_id,
2039
                            project=self.project_id,
2040
                            date=date or datetime.now(),
2041
                            reason=reason)
2042
        history_item.save()
2043
        serial = history_item.id
2044

    
2045
    def can_accept(self):
2046
        return self.state == self.REQUESTED
2047

    
2048
    def accept(self):
2049
        if not self.can_accept():
2050
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2051
            raise AssertionError(m)
2052

    
2053
        now = datetime.now()
2054
        self.acceptance_date = now
2055
        self._set_history_item(reason='ACCEPT', date=now)
2056
        self.state = self.ACCEPTED
2057
        self.save()
2058

    
2059
    def can_leave(self):
2060
        return self.state in self.ACCEPTED_STATES
2061

    
2062
    def leave_request(self):
2063
        if not self.can_leave():
2064
            m = _("%s: attempt to request to leave in state '%s'") % (
2065
                self, self.state)
2066
            raise AssertionError(m)
2067

    
2068
        self.leave_request_date = datetime.now()
2069
        self.state = self.LEAVE_REQUESTED
2070
        self.save()
2071

    
2072
    def can_deny_leave(self):
2073
        return self.state == self.LEAVE_REQUESTED
2074

    
2075
    def leave_request_deny(self):
2076
        if not self.can_deny_leave():
2077
            m = _("%s: attempt to deny leave request in state '%s'") % (
2078
                self, self.state)
2079
            raise AssertionError(m)
2080

    
2081
        self.leave_request_date = None
2082
        self.state = self.ACCEPTED
2083
        self.save()
2084

    
2085
    def can_cancel_leave(self):
2086
        return self.state == self.LEAVE_REQUESTED
2087

    
2088
    def leave_request_cancel(self):
2089
        if not self.can_cancel_leave():
2090
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2091
                self, self.state)
2092
            raise AssertionError(m)
2093

    
2094
        self.leave_request_date = None
2095
        self.state = self.ACCEPTED
2096
        self.save()
2097

    
2098
    def can_remove(self):
2099
        return self.state in self.ACCEPTED_STATES
2100

    
2101
    def remove(self):
2102
        if not self.can_remove():
2103
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2104
            raise AssertionError(m)
2105

    
2106
        self._set_history_item(reason='REMOVE')
2107
        self.delete()
2108

    
2109
    def can_reject(self):
2110
        return self.state == self.REQUESTED
2111

    
2112
    def reject(self):
2113
        if not self.can_reject():
2114
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2115
            raise AssertionError(m)
2116

    
2117
        # rejected requests don't need sync,
2118
        # because they were never effected
2119
        self._set_history_item(reason='REJECT')
2120
        self.delete()
2121

    
2122
    def can_cancel(self):
2123
        return self.state == self.REQUESTED
2124

    
2125
    def cancel(self):
2126
        if not self.can_cancel():
2127
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2128
            raise AssertionError(m)
2129

    
2130
        # rejected requests don't need sync,
2131
        # because they were never effected
2132
        self._set_history_item(reason='CANCEL')
2133
        self.delete()
2134

    
2135

    
2136
class Serial(models.Model):
2137
    serial  =   models.AutoField(primary_key=True)
2138

    
2139

    
2140
class ProjectMembershipHistory(models.Model):
2141
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2142
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2143

    
2144
    person  =   models.BigIntegerField()
2145
    project =   models.BigIntegerField()
2146
    date    =   models.DateField(auto_now_add=True)
2147
    reason  =   models.IntegerField()
2148
    serial  =   models.BigIntegerField()
2149

    
2150
### SIGNALS ###
2151
################
2152

    
2153
def create_astakos_user(u):
2154
    try:
2155
        AstakosUser.objects.get(user_ptr=u.pk)
2156
    except AstakosUser.DoesNotExist:
2157
        extended_user = AstakosUser(user_ptr_id=u.pk)
2158
        extended_user.__dict__.update(u.__dict__)
2159
        extended_user.save()
2160
        if not extended_user.has_auth_provider('local'):
2161
            extended_user.add_auth_provider('local')
2162
    except BaseException, e:
2163
        logger.exception(e)
2164

    
2165
def fix_superusers():
2166
    # Associate superusers with AstakosUser
2167
    admins = User.objects.filter(is_superuser=True)
2168
    for u in admins:
2169
        create_astakos_user(u)
2170

    
2171
def user_post_save(sender, instance, created, **kwargs):
2172
    if not created:
2173
        return
2174
    create_astakos_user(instance)
2175
post_save.connect(user_post_save, sender=User)
2176

    
2177
def astakosuser_post_save(sender, instance, created, **kwargs):
2178
    pass
2179

    
2180
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2181

    
2182
def resource_post_save(sender, instance, created, **kwargs):
2183
    pass
2184

    
2185
post_save.connect(resource_post_save, sender=Resource)
2186

    
2187
def renew_token(sender, instance, **kwargs):
2188
    if not instance.auth_token:
2189
        instance.renew_token()
2190
pre_save.connect(renew_token, sender=AstakosUser)
2191
pre_save.connect(renew_token, sender=Service)