Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 251b83be

History | View | Annotate | Download (71.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
    )
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
        default_metadata = presentation.SERVICES
141
        metadata = {}
142

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

    
153

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

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

    
160
        metadata = dict_merge(metadata,
161
                              astakos_settings.SERVICES_META)
162

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

    
171
        order_key = service_by_order
172
        if orderfor == 'dashboard':
173
            order_key = service_by_dashbaord_order
174

    
175
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
176
                                             key=order_key))
177
        return ordered_catalog
178

    
179

    
180
_presentation_data = {}
181

    
182

    
183
def get_presentation(resource):
184
    global _presentation_data
185
    resource_presentation = _presentation_data.get(resource, {})
186
    if not resource_presentation:
187
        resources_presentation = presentation.RESOURCES.get('resources', {})
188
        resource_presentation = resources_presentation.get(resource, {})
189
        _presentation_data[resource] = resource_presentation
190
    return resource_presentation
191

    
192

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

    
201
    objects = ForUpdateManager()
202

    
203
    def __str__(self):
204
        return self.name
205

    
206
    def full_name(self):
207
        return str(self)
208

    
209
    def get_info(self):
210
        return {'service': str(self.service),
211
                'description': self.desc,
212
                'unit': self.unit,
213
                'allow_in_projects': self.allow_in_projects,
214
                }
215

    
216
    @property
217
    def group(self):
218
        default = self.name
219
        return get_presentation(str(self)).get('group', default)
220

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

    
226
    @property
227
    def help_text_input_each(self):
228
        default = "%s resource" % self.name
229
        return get_presentation(str(self)).get('help_text_input_each', default)
230

    
231
    @property
232
    def is_abbreviation(self):
233
        return get_presentation(str(self)).get('is_abbreviation', False)
234

    
235
    @property
236
    def report_desc(self):
237
        default = "%s resource" % self.name
238
        return get_presentation(str(self)).get('report_desc', default)
239

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

    
244
    @property
245
    def verbose_name(self):
246
        return get_presentation(str(self)).get('verbose_name', self.name)
247

    
248
    @property
249
    def display_name(self):
250
        name = self.verbose_name
251
        if self.is_abbreviation:
252
            name = name.upper()
253
        return name
254

    
255
    @property
256
    def pluralized_display_name(self):
257
        if not self.unit:
258
            return '%ss' % self.display_name
259
        return self.display_name
260

    
261
def get_resource_names():
262
    _RESOURCE_NAMES = []
263
    resources = Resource.objects.select_related('service').all()
264
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
265
    return _RESOURCE_NAMES
266

    
267

    
268
class AstakosUserManager(UserManager):
269

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

    
279
    def get_by_email(self, email):
280
        return self.get(email=email)
281

    
282
    def get_by_identifier(self, email_or_username, **kwargs):
283
        try:
284
            return self.get(email__iexact=email_or_username, **kwargs)
285
        except AstakosUser.DoesNotExist:
286
            return self.get(username__iexact=email_or_username, **kwargs)
287

    
288
    def user_exists(self, email_or_username, **kwargs):
289
        qemail = Q(email__iexact=email_or_username)
290
        qusername = Q(username__iexact=email_or_username)
291
        qextra = Q(**kwargs)
292
        return self.filter((qemail | qusername) & qextra).exists()
293

    
294
    def verified_user_exists(self, email_or_username):
295
        return self.user_exists(email_or_username, email_verified=True)
296

    
297
    def verified(self):
298
        return self.filter(email_verified=True)
299

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

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

    
322

    
323

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

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

    
340

    
341
    #for invitations
342
    user_level = DEFAULT_USER_LEVEL
343
    level = models.IntegerField(_('Inviter level'), default=user_level)
344
    invitations = models.IntegerField(
345
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
346

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

    
361
    updated = models.DateTimeField(_('Update date'))
362
    is_verified = models.BooleanField(_('Is verified?'), default=False)
363

    
364
    email_verified = models.BooleanField(_('Email verified?'), default=False)
365

    
366
    has_credits = models.BooleanField(_('Has credits?'), default=False)
367
    has_signed_terms = models.BooleanField(
368
        _('I agree with the terms'), default=False)
369
    date_signed_terms = models.DateTimeField(
370
        _('Signed terms date'), null=True, blank=True)
371

    
372
    activation_sent = models.DateTimeField(
373
        _('Activation sent data'), null=True, blank=True)
374

    
375
    policy = models.ManyToManyField(
376
        Resource, null=True, through='AstakosUserQuota')
377

    
378
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
379

    
380
    __has_signed_terms = False
381
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
382
                                           default=False, db_index=True)
383

    
384
    objects = AstakosUserManager()
385

    
386
    forupdate = ForUpdateManager()
387

    
388
    def __init__(self, *args, **kwargs):
389
        super(AstakosUser, self).__init__(*args, **kwargs)
390
        self.__has_signed_terms = self.has_signed_terms
391
        if not self.id:
392
            self.is_active = False
393

    
394
    @property
395
    def realname(self):
396
        return '%s %s' % (self.first_name, self.last_name)
397

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

    
406
    @realname.setter
407
    def realname(self, value):
408
        parts = value.split(' ')
409
        if len(parts) == 2:
410
            self.first_name = parts[0]
411
            self.last_name = parts[1]
412
        else:
413
            self.last_name = parts[0]
414

    
415
    def add_permission(self, pname):
416
        if self.has_perm(pname):
417
            return
418
        p, created = Permission.objects.get_or_create(
419
                                    codename=pname,
420
                                    name=pname.capitalize(),
421
                                    content_type=get_content_type())
422
        self.user_permissions.add(p)
423

    
424
    def remove_permission(self, pname):
425
        if self.has_perm(pname):
426
            return
427
        p = Permission.objects.get(codename=pname,
428
                                   content_type=get_content_type())
429
        self.user_permissions.remove(p)
430

    
431
    def add_group(self, gname):
432
        group, _ = Group.objects.get_or_create(name=gname)
433
        self.groups.add(group)
434

    
435
    def is_project_admin(self, application_id=None):
436
        return self.uuid in astakos_settings.PROJECT_ADMINS
437

    
438
    @property
439
    def invitation(self):
440
        try:
441
            return Invitation.objects.get(username=self.email)
442
        except Invitation.DoesNotExist:
443
            return None
444

    
445
    @property
446
    def policies(self):
447
        return self.astakosuserquota_set.select_related().all()
448

    
449
    def get_resource_policy(self, resource):
450
        resource = Resource.objects.get(name=resource)
451
        default_capacity = resource.uplimit
452
        try:
453
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
454
            return policy, default_capacity
455
        except AstakosUserQuota.DoesNotExist:
456
            return None, default_capacity
457

    
458
    def update_uuid(self):
459
        while not self.uuid:
460
            uuid_val =  str(uuid.uuid4())
461
            try:
462
                AstakosUser.objects.get(uuid=uuid_val)
463
            except AstakosUser.DoesNotExist, e:
464
                self.uuid = uuid_val
465
        return self.uuid
466

    
467
    def save(self, update_timestamps=True, **kwargs):
468
        if update_timestamps:
469
            if not self.id:
470
                self.date_joined = datetime.now()
471
            self.updated = datetime.now()
472

    
473
        # update date_signed_terms if necessary
474
        if self.__has_signed_terms != self.has_signed_terms:
475
            self.date_signed_terms = datetime.now()
476

    
477
        self.update_uuid()
478

    
479
        if self.username != self.email.lower():
480
            # set username
481
            self.username = self.email.lower()
482

    
483
        super(AstakosUser, self).save(**kwargs)
484

    
485
    def renew_token(self, flush_sessions=False, current_key=None):
486
        md5 = hashlib.md5()
487
        md5.update(settings.SECRET_KEY)
488
        md5.update(self.username)
489
        md5.update(self.realname.encode('ascii', 'ignore'))
490
        md5.update(asctime())
491

    
492
        self.auth_token = b64encode(md5.digest())
493
        self.auth_token_created = datetime.now()
494
        self.auth_token_expires = self.auth_token_created + \
495
                                  timedelta(hours=AUTH_TOKEN_DURATION)
496
        if flush_sessions:
497
            self.flush_sessions(current_key)
498
        msg = 'Token renewed for %s' % self.email
499
        logger.log(LOGGING_LEVEL, msg)
500

    
501
    def flush_sessions(self, current_key=None):
502
        q = self.sessions
503
        if current_key:
504
            q = q.exclude(session_key=current_key)
505

    
506
        keys = q.values_list('session_key', flat=True)
507
        if keys:
508
            msg = 'Flushing sessions: %s' % ','.join(keys)
509
            logger.log(LOGGING_LEVEL, msg, [])
510
        engine = import_module(settings.SESSION_ENGINE)
511
        for k in keys:
512
            s = engine.SessionStore(k)
513
            s.flush()
514

    
515
    def __unicode__(self):
516
        return '%s (%s)' % (self.realname, self.email)
517

    
518
    def conflicting_email(self):
519
        q = AstakosUser.objects.exclude(username=self.username)
520
        q = q.filter(email__iexact=self.email)
521
        if q.count() != 0:
522
            return True
523
        return False
524

    
525
    def email_change_is_pending(self):
526
        return self.emailchanges.count() > 0
527

    
528
    @property
529
    def signed_terms(self):
530
        term = get_latest_terms()
531
        if not term:
532
            return True
533
        if not self.has_signed_terms:
534
            return False
535
        if not self.date_signed_terms:
536
            return False
537
        if self.date_signed_terms < term.date:
538
            self.has_signed_terms = False
539
            self.date_signed_terms = None
540
            self.save()
541
            return False
542
        return True
543

    
544
    def set_invitations_level(self):
545
        """
546
        Update user invitation level
547
        """
548
        level = self.invitation.inviter.level + 1
549
        self.level = level
550
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
551

    
552
    def can_change_password(self):
553
        return self.has_auth_provider('local', auth_backend='astakos')
554

    
555
    def can_change_email(self):
556
        if not self.has_auth_provider('local'):
557
            return True
558

    
559
        local = self.get_auth_provider('local')._instance
560
        return local.auth_backend == 'astakos'
561

    
562
    # Auth providers related methods
563
    def get_auth_provider(self, module=None, identifier=None, **filters):
564
        if not module:
565
            return self.auth_providers.active()[0].settings
566

    
567
        params = {'module': module}
568
        if identifier:
569
            params['identifier'] = identifier
570
        params.update(filters)
571
        return self.auth_providers.active().get(**params).settings
572

    
573
    def has_auth_provider(self, provider, **kwargs):
574
        return bool(self.auth_providers.active().filter(module=provider,
575
                                                        **kwargs).count())
576

    
577
    def get_required_providers(self, **kwargs):
578
        return auth.REQUIRED_PROVIDERS.keys()
579

    
580
    def missing_required_providers(self):
581
        required = self.get_required_providers()
582
        missing = []
583
        for provider in required:
584
            if not self.has_auth_provider(provider):
585
                missing.append(auth.get_provider(provider, self))
586
        return missing
587

    
588
    def get_available_auth_providers(self, **filters):
589
        """
590
        Returns a list of providers available for add by the user.
591
        """
592
        modules = astakos_settings.IM_MODULES
593
        providers = []
594
        for p in modules:
595
            providers.append(auth.get_provider(p, self))
596
        available = []
597

    
598
        for p in providers:
599
            if p.get_add_policy:
600
                available.append(p)
601
        return available
602

    
603
    def get_disabled_auth_providers(self, **filters):
604
        providers = self.get_auth_providers(**filters)
605
        disabled = []
606
        for p in providers:
607
            if not p.get_login_policy:
608
                disabled.append(p)
609
        return disabled
610

    
611
    def get_enabled_auth_providers(self, **filters):
612
        providers = self.get_auth_providers(**filters)
613
        enabled = []
614
        for p in providers:
615
            if p.get_login_policy:
616
                enabled.append(p)
617
        return enabled
618

    
619
    def get_auth_providers(self, **filters):
620
        providers = []
621
        for provider in self.auth_providers.active(**filters):
622
            if provider.settings.module_enabled:
623
                providers.append(provider.settings)
624

    
625
        modules = astakos_settings.IM_MODULES
626

    
627
        def key(p):
628
            if not p.module in modules:
629
                return 100
630
            return modules.index(p.module)
631

    
632
        providers = sorted(providers, key=key)
633
        return providers
634

    
635
    # URL methods
636
    @property
637
    def auth_providers_display(self):
638
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
639
                         self.get_enabled_auth_providers()])
640

    
641
    def add_auth_provider(self, module='local', identifier=None, **params):
642
        provider = auth.get_provider(module, self, identifier, **params)
643
        provider.add_to_user()
644

    
645
    def get_resend_activation_url(self):
646
        return reverse('send_activation', kwargs={'user_id': self.pk})
647

    
648
    def get_activation_url(self, nxt=False):
649
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
650
                                 quote(self.auth_token))
651
        if nxt:
652
            url += "&next=%s" % quote(nxt)
653
        return url
654

    
655
    def get_password_reset_url(self, token_generator=default_token_generator):
656
        return reverse('django.contrib.auth.views.password_reset_confirm',
657
                          kwargs={'uidb36':int_to_base36(self.id),
658
                                  'token':token_generator.make_token(self)})
659

    
660
    def get_inactive_message(self, provider_module, identifier=None):
661
        provider = self.get_auth_provider(provider_module, identifier)
662

    
663
        msg_extra = ''
664
        message = ''
665

    
666
        msg_inactive = provider.get_account_inactive_msg
667
        msg_pending = provider.get_pending_activation_msg
668
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
669
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
670
        msg_pending_mod = provider.get_pending_moderation_msg
671
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
672

    
673
        if self.activation_sent:
674
            if self.email_verified:
675
                message = msg_inactive
676
            else:
677
                message = msg_pending
678
                url = self.get_resend_activation_url()
679
                msg_extra = msg_pending_help + \
680
                            u' ' + \
681
                            '<a href="%s">%s?</a>' % (url, msg_resend)
682
        else:
683
            if astakos_settings.MODERATION_ENABLED:
684
                message = msg_pending_mod
685
            else:
686
                message = msg_pending
687
                url = self.get_resend_activation_url()
688
                msg_extra = '<a href="%s">%s?</a>' % (url, \
689
                                msg_resend)
690

    
691
        return mark_safe(message + u' '+ msg_extra)
692

    
693
    def owns_application(self, application):
694
        return application.owner == self
695

    
696
    def owns_project(self, project):
697
        return project.application.owner == self
698

    
699
    def is_associated(self, project):
700
        try:
701
            m = ProjectMembership.objects.get(person=self, project=project)
702
            return m.state in ProjectMembership.ASSOCIATED_STATES
703
        except ProjectMembership.DoesNotExist:
704
            return False
705

    
706
    def get_membership(self, project):
707
        try:
708
            return ProjectMembership.objects.get(
709
                project=project,
710
                person=self)
711
        except ProjectMembership.DoesNotExist:
712
            return None
713

    
714
    def membership_display(self, project):
715
        m = self.get_membership(project)
716
        if m is None:
717
            return _('Not a member')
718
        else:
719
            return m.user_friendly_state_display()
720

    
721
    def non_owner_can_view(self, maybe_project):
722
        if self.is_project_admin():
723
            return True
724
        if maybe_project is None:
725
            return False
726
        project = maybe_project
727
        if self.is_associated(project):
728
            return True
729
        if project.is_deactivated():
730
            return False
731
        return True
732

    
733
    def settings(self):
734
        return UserSetting.objects.filter(user=self)
735

    
736

    
737
class AstakosUserAuthProviderManager(models.Manager):
738

    
739
    def active(self, **filters):
740
        return self.filter(active=True, **filters)
741

    
742
    def remove_unverified_providers(self, provider, **filters):
743
        try:
744
            existing = self.filter(module=provider, user__email_verified=False,
745
                                   **filters)
746
            for p in existing:
747
                p.user.delete()
748
        except:
749
            pass
750

    
751
    def unverified(self, provider, **filters):
752
        try:
753
            return self.get(module=provider, user__email_verified=False,
754
                            **filters).settings
755
        except AstakosUserAuthProvider.DoesNotExist:
756
            return None
757

    
758
    def verified(self, provider, **filters):
759
        try:
760
            return self.get(module=provider, user__email_verified=True,
761
                            **filters).settings
762
        except AstakosUserAuthProvider.DoesNotExist:
763
            return None
764

    
765

    
766
class AuthProviderPolicyProfileManager(models.Manager):
767

    
768
    def active(self):
769
        return self.filter(active=True)
770

    
771
    def for_user(self, user, provider):
772
        policies = {}
773
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
774
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
775
        exclusive_q = exclusive_q1 | exclusive_q2
776

    
777
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
778
            policies.update(profile.policies)
779

    
780
        user_groups = user.groups.all().values('pk')
781
        for profile in self.active().filter(groups__in=user_groups).filter(
782
                exclusive_q):
783
            policies.update(profile.policies)
784
        return policies
785

    
786
    def add_policy(self, name, provider, group_or_user, exclusive=False,
787
                   **policies):
788
        is_group = isinstance(group_or_user, Group)
789
        profile, created = self.get_or_create(name=name, provider=provider,
790
                                              is_exclusive=exclusive)
791
        profile.is_exclusive = exclusive
792
        profile.save()
793
        if is_group:
794
            profile.groups.add(group_or_user)
795
        else:
796
            profile.users.add(group_or_user)
797
        profile.set_policies(policies)
798
        profile.save()
799
        return profile
800

    
801

    
802
class AuthProviderPolicyProfile(models.Model):
803
    name = models.CharField(_('Name'), max_length=255, blank=False,
804
                            null=False, db_index=True)
805
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
806
                                null=False)
807

    
808
    # apply policies to all providers excluding the one set in provider field
809
    is_exclusive = models.BooleanField(default=False)
810

    
811
    policy_add = models.NullBooleanField(null=True, default=None)
812
    policy_remove = models.NullBooleanField(null=True, default=None)
813
    policy_create = models.NullBooleanField(null=True, default=None)
814
    policy_login = models.NullBooleanField(null=True, default=None)
815
    policy_limit = models.IntegerField(null=True, default=None)
816
    policy_required = models.NullBooleanField(null=True, default=None)
817
    policy_automoderate = models.NullBooleanField(null=True, default=None)
818
    policy_switch = models.NullBooleanField(null=True, default=None)
819

    
820
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
821
                     'automoderate')
822

    
823
    priority = models.IntegerField(null=False, default=1)
824
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
825
    users = models.ManyToManyField(AstakosUser,
826
                                   related_name='authpolicy_profiles')
827
    active = models.BooleanField(default=True)
828

    
829
    objects = AuthProviderPolicyProfileManager()
830

    
831
    class Meta:
832
        ordering = ['priority']
833

    
834
    @property
835
    def policies(self):
836
        policies = {}
837
        for pkey in self.POLICY_FIELDS:
838
            value = getattr(self, 'policy_%s' % pkey, None)
839
            if value is None:
840
                continue
841
            policies[pkey] = value
842
        return policies
843

    
844
    def set_policies(self, policies_dict):
845
        for key, value in policies_dict.iteritems():
846
            if key in self.POLICY_FIELDS:
847
                setattr(self, 'policy_%s' % key, value)
848
        return self.policies
849

    
850

    
851
class AstakosUserAuthProvider(models.Model):
852
    """
853
    Available user authentication methods.
854
    """
855
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
856
                                   null=True, default=None)
857
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
858
    module = models.CharField(_('Provider'), max_length=255, blank=False,
859
                                default='local')
860
    identifier = models.CharField(_('Third-party identifier'),
861
                                              max_length=255, null=True,
862
                                              blank=True)
863
    active = models.BooleanField(default=True)
864
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
865
                                   default='astakos')
866
    info_data = models.TextField(default="", null=True, blank=True)
867
    created = models.DateTimeField('Creation date', auto_now_add=True)
868

    
869
    objects = AstakosUserAuthProviderManager()
870

    
871
    class Meta:
872
        unique_together = (('identifier', 'module', 'user'), )
873
        ordering = ('module', 'created')
874

    
875
    def __init__(self, *args, **kwargs):
876
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
877
        try:
878
            self.info = json.loads(self.info_data)
879
            if not self.info:
880
                self.info = {}
881
        except Exception, e:
882
            self.info = {}
883

    
884
        for key,value in self.info.iteritems():
885
            setattr(self, 'info_%s' % key, value)
886

    
887
    @property
888
    def settings(self):
889
        extra_data = {}
890

    
891
        info_data = {}
892
        if self.info_data:
893
            info_data = json.loads(self.info_data)
894

    
895
        extra_data['info'] = info_data
896

    
897
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
898
            extra_data[key] = getattr(self, key)
899

    
900
        extra_data['instance'] = self
901
        return auth.get_provider(self.module, self.user,
902
                                           self.identifier, **extra_data)
903

    
904
    def __repr__(self):
905
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
906

    
907
    def __unicode__(self):
908
        if self.identifier:
909
            return "%s:%s" % (self.module, self.identifier)
910
        if self.auth_backend:
911
            return "%s:%s" % (self.module, self.auth_backend)
912
        return self.module
913

    
914
    def save(self, *args, **kwargs):
915
        self.info_data = json.dumps(self.info)
916
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
917

    
918

    
919
class ExtendedManager(models.Manager):
920
    def _update_or_create(self, **kwargs):
921
        assert kwargs, \
922
            'update_or_create() must be passed at least one keyword argument'
923
        obj, created = self.get_or_create(**kwargs)
924
        defaults = kwargs.pop('defaults', {})
925
        if created:
926
            return obj, True, False
927
        else:
928
            try:
929
                params = dict(
930
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
931
                params.update(defaults)
932
                for attr, val in params.items():
933
                    if hasattr(obj, attr):
934
                        setattr(obj, attr, val)
935
                sid = transaction.savepoint()
936
                obj.save(force_update=True)
937
                transaction.savepoint_commit(sid)
938
                return obj, False, True
939
            except IntegrityError, e:
940
                transaction.savepoint_rollback(sid)
941
                try:
942
                    return self.get(**kwargs), False, False
943
                except self.model.DoesNotExist:
944
                    raise e
945

    
946
    update_or_create = _update_or_create
947

    
948

    
949
class AstakosUserQuota(models.Model):
950
    objects = ExtendedManager()
951
    capacity = intDecimalField()
952
    resource = models.ForeignKey(Resource)
953
    user = models.ForeignKey(AstakosUser)
954

    
955
    class Meta:
956
        unique_together = ("resource", "user")
957

    
958

    
959
class ApprovalTerms(models.Model):
960
    """
961
    Model for approval terms
962
    """
963

    
964
    date = models.DateTimeField(
965
        _('Issue date'), db_index=True, auto_now_add=True)
966
    location = models.CharField(_('Terms location'), max_length=255)
967

    
968

    
969
class Invitation(models.Model):
970
    """
971
    Model for registring invitations
972
    """
973
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
974
                                null=True)
975
    realname = models.CharField(_('Real name'), max_length=255)
976
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
977
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
978
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
979
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
980
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
981

    
982
    def __init__(self, *args, **kwargs):
983
        super(Invitation, self).__init__(*args, **kwargs)
984
        if not self.id:
985
            self.code = _generate_invitation_code()
986

    
987
    def consume(self):
988
        self.is_consumed = True
989
        self.consumed = datetime.now()
990
        self.save()
991

    
992
    def __unicode__(self):
993
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
994

    
995

    
996
class EmailChangeManager(models.Manager):
997

    
998
    @transaction.commit_on_success
999
    def change_email(self, activation_key):
1000
        """
1001
        Validate an activation key and change the corresponding
1002
        ``User`` if valid.
1003

1004
        If the key is valid and has not expired, return the ``User``
1005
        after activating.
1006

1007
        If the key is not valid or has expired, return ``None``.
1008

1009
        If the key is valid but the ``User`` is already active,
1010
        return ``None``.
1011

1012
        After successful email change the activation record is deleted.
1013

1014
        Throws ValueError if there is already
1015
        """
1016
        try:
1017
            email_change = self.model.objects.get(
1018
                activation_key=activation_key)
1019
            if email_change.activation_key_expired():
1020
                email_change.delete()
1021
                raise EmailChange.DoesNotExist
1022
            # is there an active user with this address?
1023
            try:
1024
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1025
            except AstakosUser.DoesNotExist:
1026
                pass
1027
            else:
1028
                raise ValueError(_('The new email address is reserved.'))
1029
            # update user
1030
            user = AstakosUser.objects.get(pk=email_change.user_id)
1031
            old_email = user.email
1032
            user.email = email_change.new_email_address
1033
            user.save()
1034
            email_change.delete()
1035
            msg = "User %s changed email from %s to %s" % (user.log_display,
1036
                                                           old_email,
1037
                                                           user.email)
1038
            logger.log(LOGGING_LEVEL, msg)
1039
            return user
1040
        except EmailChange.DoesNotExist:
1041
            raise ValueError(_('Invalid activation key.'))
1042

    
1043

    
1044
class EmailChange(models.Model):
1045
    new_email_address = models.EmailField(
1046
        _(u'new e-mail address'),
1047
        help_text=_('Provide a new email address. Until you verify the new '
1048
                    'address by following the activation link that will be '
1049
                    'sent to it, your old email address will remain active.'))
1050
    user = models.ForeignKey(
1051
        AstakosUser, unique=True, related_name='emailchanges')
1052
    requested_at = models.DateTimeField(auto_now_add=True)
1053
    activation_key = models.CharField(
1054
        max_length=40, unique=True, db_index=True)
1055

    
1056
    objects = EmailChangeManager()
1057

    
1058
    def get_url(self):
1059
        return reverse('email_change_confirm',
1060
                      kwargs={'activation_key': self.activation_key})
1061

    
1062
    def activation_key_expired(self):
1063
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1064
        return self.requested_at + expiration_date < datetime.now()
1065

    
1066

    
1067
class AdditionalMail(models.Model):
1068
    """
1069
    Model for registring invitations
1070
    """
1071
    owner = models.ForeignKey(AstakosUser)
1072
    email = models.EmailField()
1073

    
1074

    
1075
def _generate_invitation_code():
1076
    while True:
1077
        code = randint(1, 2L ** 63 - 1)
1078
        try:
1079
            Invitation.objects.get(code=code)
1080
            # An invitation with this code already exists, try again
1081
        except Invitation.DoesNotExist:
1082
            return code
1083

    
1084

    
1085
def get_latest_terms():
1086
    try:
1087
        term = ApprovalTerms.objects.order_by('-id')[0]
1088
        return term
1089
    except IndexError:
1090
        pass
1091
    return None
1092

    
1093

    
1094
class PendingThirdPartyUser(models.Model):
1095
    """
1096
    Model for registring successful third party user authentications
1097
    """
1098
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1099
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1100
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1101
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1102
                                  null=True)
1103
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1104
                                 null=True)
1105
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1106
                                   null=True)
1107
    username = models.CharField(_('username'), max_length=30, unique=True,
1108
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1109
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1110
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1111
    info = models.TextField(default="", null=True, blank=True)
1112

    
1113
    class Meta:
1114
        unique_together = ("provider", "third_party_identifier")
1115

    
1116
    def get_user_instance(self):
1117
        d = self.__dict__
1118
        d.pop('_state', None)
1119
        d.pop('id', None)
1120
        d.pop('token', None)
1121
        d.pop('created', None)
1122
        d.pop('info', None)
1123
        user = AstakosUser(**d)
1124

    
1125
        return user
1126

    
1127
    @property
1128
    def realname(self):
1129
        return '%s %s' %(self.first_name, self.last_name)
1130

    
1131
    @realname.setter
1132
    def realname(self, value):
1133
        parts = value.split(' ')
1134
        if len(parts) == 2:
1135
            self.first_name = parts[0]
1136
            self.last_name = parts[1]
1137
        else:
1138
            self.last_name = parts[0]
1139

    
1140
    def save(self, **kwargs):
1141
        if not self.id:
1142
            # set username
1143
            while not self.username:
1144
                username =  uuid.uuid4().hex[:30]
1145
                try:
1146
                    AstakosUser.objects.get(username = username)
1147
                except AstakosUser.DoesNotExist, e:
1148
                    self.username = username
1149
        super(PendingThirdPartyUser, self).save(**kwargs)
1150

    
1151
    def generate_token(self):
1152
        self.password = self.third_party_identifier
1153
        self.last_login = datetime.now()
1154
        self.token = default_token_generator.make_token(self)
1155

    
1156
    def existing_user(self):
1157
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1158
                                         auth_providers__identifier=self.third_party_identifier)
1159

    
1160
    def get_provider(self, user):
1161
        params = {
1162
            'info_data': self.info,
1163
            'affiliation': self.affiliation
1164
        }
1165
        return auth.get_provider(self.provider, user,
1166
                                 self.third_party_identifier, **params)
1167

    
1168
class SessionCatalog(models.Model):
1169
    session_key = models.CharField(_('session key'), max_length=40)
1170
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1171

    
1172

    
1173
class UserSetting(models.Model):
1174
    user = models.ForeignKey(AstakosUser)
1175
    setting = models.CharField(max_length=255)
1176
    value = models.IntegerField()
1177

    
1178
    objects = ForUpdateManager()
1179

    
1180
    class Meta:
1181
        unique_together = ("user", "setting")
1182

    
1183

    
1184
### PROJECTS ###
1185
################
1186

    
1187
class ChainManager(ForUpdateManager):
1188

    
1189
    def search_by_name(self, *search_strings):
1190
        projects = Project.objects.search_by_name(*search_strings)
1191
        chains = [p.id for p in projects]
1192
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1193
        apps = (app for app in apps if app.is_latest())
1194
        app_chains = [app.chain for app in apps if app.chain not in chains]
1195
        return chains + app_chains
1196

    
1197
    def all_full_state(self):
1198
        chains = self.all()
1199
        cids = [c.chain for c in chains]
1200
        projects = Project.objects.select_related('application').in_bulk(cids)
1201

    
1202
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1203
        chain_latest = dict(objs.values_list('chain', 'latest'))
1204

    
1205
        objs = ProjectApplication.objects.select_related('applicant')
1206
        apps = objs.in_bulk(chain_latest.values())
1207

    
1208
        d = {}
1209
        for chain in chains:
1210
            pk = chain.pk
1211
            project = projects.get(pk, None)
1212
            app = apps[chain_latest[pk]]
1213
            d[chain.pk] = chain.get_state(project, app)
1214

    
1215
        return d
1216

    
1217
    def of_project(self, project):
1218
        if project is None:
1219
            return None
1220
        try:
1221
            return self.get(chain=project.id)
1222
        except Chain.DoesNotExist:
1223
            raise AssertionError('project with no chain')
1224

    
1225

    
1226
class Chain(models.Model):
1227
    chain  =   models.AutoField(primary_key=True)
1228

    
1229
    def __str__(self):
1230
        return "%s" % (self.chain,)
1231

    
1232
    objects = ChainManager()
1233

    
1234
    PENDING            = 0
1235
    DENIED             = 3
1236
    DISMISSED          = 4
1237
    CANCELLED          = 5
1238

    
1239
    APPROVED           = 10
1240
    APPROVED_PENDING   = 11
1241
    SUSPENDED          = 12
1242
    SUSPENDED_PENDING  = 13
1243
    TERMINATED         = 14
1244
    TERMINATED_PENDING = 15
1245

    
1246
    PENDING_STATES = [PENDING,
1247
                      APPROVED_PENDING,
1248
                      SUSPENDED_PENDING,
1249
                      TERMINATED_PENDING,
1250
                      ]
1251

    
1252
    MODIFICATION_STATES = [APPROVED_PENDING,
1253
                           SUSPENDED_PENDING,
1254
                           TERMINATED_PENDING,
1255
                           ]
1256

    
1257
    RELEVANT_STATES = [PENDING,
1258
                       DENIED,
1259
                       APPROVED,
1260
                       APPROVED_PENDING,
1261
                       SUSPENDED,
1262
                       SUSPENDED_PENDING,
1263
                       TERMINATED_PENDING,
1264
                       ]
1265

    
1266
    SKIP_STATES = [DISMISSED,
1267
                   CANCELLED,
1268
                   TERMINATED]
1269

    
1270
    STATE_DISPLAY = {
1271
        PENDING            : _("Pending"),
1272
        DENIED             : _("Denied"),
1273
        DISMISSED          : _("Dismissed"),
1274
        CANCELLED          : _("Cancelled"),
1275
        APPROVED           : _("Active"),
1276
        APPROVED_PENDING   : _("Active - Pending"),
1277
        SUSPENDED          : _("Suspended"),
1278
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1279
        TERMINATED         : _("Terminated"),
1280
        TERMINATED_PENDING : _("Terminated - Pending"),
1281
        }
1282

    
1283

    
1284
    @classmethod
1285
    def _chain_state(cls, project_state, app_state):
1286
        s = CHAIN_STATE.get((project_state, app_state), None)
1287
        if s is None:
1288
            raise AssertionError('inconsistent chain state')
1289
        return s
1290

    
1291
    @classmethod
1292
    def chain_state(cls, project, app):
1293
        p_state = project.state if project else None
1294
        return cls._chain_state(p_state, app.state)
1295

    
1296
    @classmethod
1297
    def state_display(cls, s):
1298
        if s is None:
1299
            return _("Unknown")
1300
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1301

    
1302
    def last_application(self):
1303
        return self.chained_apps.order_by('-id')[0]
1304

    
1305
    def get_project(self):
1306
        try:
1307
            return self.chained_project
1308
        except Project.DoesNotExist:
1309
            return None
1310

    
1311
    def get_elements(self):
1312
        project = self.get_project()
1313
        app = self.last_application()
1314
        return project, app
1315

    
1316
    def get_state(self, project, app):
1317
        s = self.chain_state(project, app)
1318
        return s, project, app
1319

    
1320
    def full_state(self):
1321
        project, app = self.get_elements()
1322
        return self.get_state(project, app)
1323

    
1324

    
1325
def new_chain():
1326
    c = Chain.objects.create()
1327
    return c
1328

    
1329

    
1330
class ProjectApplicationManager(ForUpdateManager):
1331

    
1332
    def user_visible_projects(self, *filters, **kw_filters):
1333
        model = self.model
1334
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1335

    
1336
    def user_visible_by_chain(self, flt):
1337
        model = self.model
1338
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1339
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1340
        by_chain = dict(pending.annotate(models.Max('id')))
1341
        by_chain.update(approved.annotate(models.Max('id')))
1342
        return self.filter(flt, id__in=by_chain.values())
1343

    
1344
    def user_accessible_projects(self, user):
1345
        """
1346
        Return projects accessed by specified user.
1347
        """
1348
        if user.is_project_admin():
1349
            participates_filters = Q()
1350
        else:
1351
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1352
                                   Q(project__projectmembership__person=user)
1353

    
1354
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1355

    
1356
    def search_by_name(self, *search_strings):
1357
        q = Q()
1358
        for s in search_strings:
1359
            q = q | Q(name__icontains=s)
1360
        return self.filter(q)
1361

    
1362
    def latest_of_chain(self, chain_id):
1363
        try:
1364
            return self.filter(chain=chain_id).order_by('-id')[0]
1365
        except IndexError:
1366
            return None
1367

    
1368

    
1369
class ProjectApplication(models.Model):
1370
    applicant               =   models.ForeignKey(
1371
                                    AstakosUser,
1372
                                    related_name='projects_applied',
1373
                                    db_index=True)
1374

    
1375
    PENDING     =    0
1376
    APPROVED    =    1
1377
    REPLACED    =    2
1378
    DENIED      =    3
1379
    DISMISSED   =    4
1380
    CANCELLED   =    5
1381

    
1382
    state                   =   models.IntegerField(default=PENDING,
1383
                                                    db_index=True)
1384

    
1385
    owner                   =   models.ForeignKey(
1386
                                    AstakosUser,
1387
                                    related_name='projects_owned',
1388
                                    db_index=True)
1389

    
1390
    chain                   =   models.ForeignKey(Chain,
1391
                                                  related_name='chained_apps',
1392
                                                  db_column='chain')
1393
    precursor_application   =   models.ForeignKey('ProjectApplication',
1394
                                                  null=True,
1395
                                                  blank=True)
1396

    
1397
    name                    =   models.CharField(max_length=80)
1398
    homepage                =   models.URLField(max_length=255, null=True,
1399
                                                verify_exists=False)
1400
    description             =   models.TextField(null=True, blank=True)
1401
    start_date              =   models.DateTimeField(null=True, blank=True)
1402
    end_date                =   models.DateTimeField()
1403
    member_join_policy      =   models.IntegerField()
1404
    member_leave_policy     =   models.IntegerField()
1405
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1406
    resource_grants         =   models.ManyToManyField(
1407
                                    Resource,
1408
                                    null=True,
1409
                                    blank=True,
1410
                                    through='ProjectResourceGrant')
1411
    comments                =   models.TextField(null=True, blank=True)
1412
    issue_date              =   models.DateTimeField(auto_now_add=True)
1413
    response_date           =   models.DateTimeField(null=True, blank=True)
1414
    response                =   models.TextField(null=True, blank=True)
1415

    
1416
    objects                 =   ProjectApplicationManager()
1417

    
1418
    # Compiled queries
1419
    Q_PENDING  = Q(state=PENDING)
1420
    Q_APPROVED = Q(state=APPROVED)
1421
    Q_DENIED   = Q(state=DENIED)
1422

    
1423
    class Meta:
1424
        unique_together = ("chain", "id")
1425

    
1426
    def __unicode__(self):
1427
        return "%s applied by %s" % (self.name, self.applicant)
1428

    
1429
    # TODO: Move to a more suitable place
1430
    APPLICATION_STATE_DISPLAY = {
1431
        PENDING  : _('Pending review'),
1432
        APPROVED : _('Approved'),
1433
        REPLACED : _('Replaced'),
1434
        DENIED   : _('Denied'),
1435
        DISMISSED: _('Dismissed'),
1436
        CANCELLED: _('Cancelled')
1437
    }
1438

    
1439
    @property
1440
    def log_display(self):
1441
        return "application %s (%s) for project %s" % (
1442
            self.id, self.name, self.chain)
1443

    
1444
    def get_project(self):
1445
        try:
1446
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1447
            return Project
1448
        except Project.DoesNotExist, e:
1449
            return None
1450

    
1451
    def state_display(self):
1452
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1453

    
1454
    def project_state_display(self):
1455
        try:
1456
            project = self.project
1457
            return project.state_display()
1458
        except Project.DoesNotExist:
1459
            return self.state_display()
1460

    
1461
    def add_resource_policy(self, resource, uplimit):
1462
        """Raises ObjectDoesNotExist, IntegrityError"""
1463
        q = self.projectresourcegrant_set
1464
        resource = Resource.objects.get(name=resource)
1465
        q.create(resource=resource, member_capacity=uplimit)
1466

    
1467
    def members_count(self):
1468
        return self.project.approved_memberships.count()
1469

    
1470
    @property
1471
    def grants(self):
1472
        return self.projectresourcegrant_set.values('member_capacity',
1473
                                                    'resource__name')
1474

    
1475
    @property
1476
    def resource_policies(self):
1477
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1478

    
1479
    def set_resource_policies(self, policies):
1480
        for resource, uplimit in policies:
1481
            self.add_resource_policy(resource, uplimit)
1482

    
1483
    def pending_modifications_incl_me(self):
1484
        q = self.chained_applications()
1485
        q = q.filter(Q(state=self.PENDING))
1486
        return q
1487

    
1488
    def last_pending_incl_me(self):
1489
        try:
1490
            return self.pending_modifications_incl_me().order_by('-id')[0]
1491
        except IndexError:
1492
            return None
1493

    
1494
    def pending_modifications(self):
1495
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1496

    
1497
    def last_pending(self):
1498
        try:
1499
            return self.pending_modifications().order_by('-id')[0]
1500
        except IndexError:
1501
            return None
1502

    
1503
    def is_modification(self):
1504
        # if self.state != self.PENDING:
1505
        #     return False
1506
        parents = self.chained_applications().filter(id__lt=self.id)
1507
        parents = parents.filter(state__in=[self.APPROVED])
1508
        return parents.count() > 0
1509

    
1510
    def chained_applications(self):
1511
        return ProjectApplication.objects.filter(chain=self.chain)
1512

    
1513
    def is_latest(self):
1514
        return self.chained_applications().order_by('-id')[0] == self
1515

    
1516
    def has_pending_modifications(self):
1517
        return bool(self.last_pending())
1518

    
1519
    def denied_modifications(self):
1520
        q = self.chained_applications()
1521
        q = q.filter(Q(state=self.DENIED))
1522
        q = q.filter(~Q(id=self.id))
1523
        return q
1524

    
1525
    def last_denied(self):
1526
        try:
1527
            return self.denied_modifications().order_by('-id')[0]
1528
        except IndexError:
1529
            return None
1530

    
1531
    def has_denied_modifications(self):
1532
        return bool(self.last_denied())
1533

    
1534
    def is_applied(self):
1535
        try:
1536
            self.project
1537
            return True
1538
        except Project.DoesNotExist:
1539
            return False
1540

    
1541
    def get_project(self):
1542
        try:
1543
            return Project.objects.get(id=self.chain)
1544
        except Project.DoesNotExist:
1545
            return None
1546

    
1547
    def project_exists(self):
1548
        return self.get_project() is not None
1549

    
1550
    def _get_project_for_update(self):
1551
        try:
1552
            objects = Project.objects
1553
            project = objects.get_for_update(id=self.chain)
1554
            return project
1555
        except Project.DoesNotExist:
1556
            return None
1557

    
1558
    def can_cancel(self):
1559
        return self.state == self.PENDING
1560

    
1561
    def cancel(self):
1562
        if not self.can_cancel():
1563
            m = _("cannot cancel: application '%s' in state '%s'") % (
1564
                    self.id, self.state)
1565
            raise AssertionError(m)
1566

    
1567
        self.state = self.CANCELLED
1568
        self.save()
1569

    
1570
    def can_dismiss(self):
1571
        return self.state == self.DENIED
1572

    
1573
    def dismiss(self):
1574
        if not self.can_dismiss():
1575
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1576
                    self.id, self.state)
1577
            raise AssertionError(m)
1578

    
1579
        self.state = self.DISMISSED
1580
        self.save()
1581

    
1582
    def can_deny(self):
1583
        return self.state == self.PENDING
1584

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

    
1591
        self.state = self.DENIED
1592
        self.response_date = datetime.now()
1593
        self.response = reason
1594
        self.save()
1595

    
1596
    def can_approve(self):
1597
        return self.state == self.PENDING
1598

    
1599
    def approve(self, reason):
1600
        new_project_name = self.name
1601
        if not self.can_approve():
1602
            m = _("cannot approve: project '%s' in state '%s'") % (
1603
                    new_project_name, self.state)
1604
            raise AssertionError(m) # invalid argument
1605

    
1606
        now = datetime.now()
1607
        project = self._get_project_for_update()
1608

    
1609
        try:
1610
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1611
            conflicting_project = Project.objects.get(q)
1612
            if (conflicting_project != project):
1613
                m = (_("cannot approve: project with name '%s' "
1614
                       "already exists (id: %s)") % (
1615
                        new_project_name, conflicting_project.id))
1616
                raise PermissionDenied(m) # invalid argument
1617
        except Project.DoesNotExist:
1618
            pass
1619

    
1620
        new_project = False
1621
        if project is None:
1622
            new_project = True
1623
            project = Project(id=self.chain)
1624

    
1625
        project.name = new_project_name
1626
        project.application = self
1627
        project.last_approval_date = now
1628

    
1629
        project.save()
1630

    
1631
        self.state = self.APPROVED
1632
        self.response_date = now
1633
        self.response = reason
1634
        self.save()
1635
        return project
1636

    
1637
    @property
1638
    def member_join_policy_display(self):
1639
        policy = self.member_join_policy
1640
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1641

    
1642
    @property
1643
    def member_leave_policy_display(self):
1644
        policy = self.member_leave_policy
1645
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1646

    
1647
class ProjectResourceGrant(models.Model):
1648

    
1649
    resource                =   models.ForeignKey(Resource)
1650
    project_application     =   models.ForeignKey(ProjectApplication,
1651
                                                  null=True)
1652
    project_capacity        =   intDecimalField(null=True)
1653
    member_capacity         =   intDecimalField(default=0)
1654

    
1655
    objects = ExtendedManager()
1656

    
1657
    class Meta:
1658
        unique_together = ("resource", "project_application")
1659

    
1660
    def display_member_capacity(self):
1661
        if self.member_capacity:
1662
            if self.resource.unit:
1663
                return ProjectResourceGrant.display_filesize(
1664
                    self.member_capacity)
1665
            else:
1666
                if math.isinf(self.member_capacity):
1667
                    return 'Unlimited'
1668
                else:
1669
                    return self.member_capacity
1670
        else:
1671
            return 'Unlimited'
1672

    
1673
    def __str__(self):
1674
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1675
                                        self.display_member_capacity())
1676

    
1677
    @classmethod
1678
    def display_filesize(cls, value):
1679
        try:
1680
            value = float(value)
1681
        except:
1682
            return
1683
        else:
1684
            if math.isinf(value):
1685
                return 'Unlimited'
1686
            if value > 1:
1687
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1688
                                [0, 0, 0, 0, 0, 0])
1689
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1690
                quotient = float(value) / 1024**exponent
1691
                unit, value_decimals = unit_list[exponent]
1692
                format_string = '{0:.%sf} {1}' % (value_decimals)
1693
                return format_string.format(quotient, unit)
1694
            if value == 0:
1695
                return '0 bytes'
1696
            if value == 1:
1697
                return '1 byte'
1698
            else:
1699
               return '0'
1700

    
1701

    
1702
class ProjectManager(ForUpdateManager):
1703

    
1704
    def terminated_projects(self):
1705
        q = self.model.Q_TERMINATED
1706
        return self.filter(q)
1707

    
1708
    def not_terminated_projects(self):
1709
        q = ~self.model.Q_TERMINATED
1710
        return self.filter(q)
1711

    
1712
    def deactivated_projects(self):
1713
        q = self.model.Q_DEACTIVATED
1714
        return self.filter(q)
1715

    
1716
    def expired_projects(self):
1717
        q = (~Q(state=Project.TERMINATED) &
1718
              Q(application__end_date__lt=datetime.now()))
1719
        return self.filter(q)
1720

    
1721
    def search_by_name(self, *search_strings):
1722
        q = Q()
1723
        for s in search_strings:
1724
            q = q | Q(name__icontains=s)
1725
        return self.filter(q)
1726

    
1727

    
1728
class Project(models.Model):
1729

    
1730
    id                          =   models.OneToOneField(Chain,
1731
                                                      related_name='chained_project',
1732
                                                      db_column='id',
1733
                                                      primary_key=True)
1734

    
1735
    application                 =   models.OneToOneField(
1736
                                            ProjectApplication,
1737
                                            related_name='project')
1738
    last_approval_date          =   models.DateTimeField(null=True)
1739

    
1740
    members                     =   models.ManyToManyField(
1741
                                            AstakosUser,
1742
                                            through='ProjectMembership')
1743

    
1744
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1745
    deactivation_date           =   models.DateTimeField(null=True)
1746

    
1747
    creation_date               =   models.DateTimeField(auto_now_add=True)
1748
    name                        =   models.CharField(
1749
                                            max_length=80,
1750
                                            null=True,
1751
                                            db_index=True,
1752
                                            unique=True)
1753

    
1754
    APPROVED    = 1
1755
    SUSPENDED   = 10
1756
    TERMINATED  = 100
1757

    
1758
    state                       =   models.IntegerField(default=APPROVED,
1759
                                                        db_index=True)
1760

    
1761
    objects     =   ProjectManager()
1762

    
1763
    # Compiled queries
1764
    Q_TERMINATED  = Q(state=TERMINATED)
1765
    Q_SUSPENDED   = Q(state=SUSPENDED)
1766
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1767

    
1768
    def __str__(self):
1769
        return uenc(_("<project %s '%s'>") %
1770
                    (self.id, udec(self.application.name)))
1771

    
1772
    __repr__ = __str__
1773

    
1774
    def __unicode__(self):
1775
        return _("<project %s '%s'>") % (self.id, self.application.name)
1776

    
1777
    STATE_DISPLAY = {
1778
        APPROVED   : 'Active',
1779
        SUSPENDED  : 'Suspended',
1780
        TERMINATED : 'Terminated'
1781
        }
1782

    
1783
    def state_display(self):
1784
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1785

    
1786
    def expiration_info(self):
1787
        return (str(self.id), self.name, self.state_display(),
1788
                str(self.application.end_date))
1789

    
1790
    def is_deactivated(self, reason=None):
1791
        if reason is not None:
1792
            return self.state == reason
1793

    
1794
        return self.state != self.APPROVED
1795

    
1796
    ### Deactivation calls
1797

    
1798
    def terminate(self):
1799
        self.deactivation_reason = 'TERMINATED'
1800
        self.deactivation_date = datetime.now()
1801
        self.state = self.TERMINATED
1802
        self.name = None
1803
        self.save()
1804

    
1805
    def suspend(self):
1806
        self.deactivation_reason = 'SUSPENDED'
1807
        self.deactivation_date = datetime.now()
1808
        self.state = self.SUSPENDED
1809
        self.save()
1810

    
1811
    def resume(self):
1812
        self.deactivation_reason = None
1813
        self.deactivation_date = None
1814
        self.state = self.APPROVED
1815
        self.save()
1816

    
1817
    ### Logical checks
1818

    
1819
    def is_inconsistent(self):
1820
        now = datetime.now()
1821
        dates = [self.creation_date,
1822
                 self.last_approval_date,
1823
                 self.deactivation_date]
1824
        return any([date > now for date in dates])
1825

    
1826
    def is_approved(self):
1827
        return self.state == self.APPROVED
1828

    
1829
    @property
1830
    def is_alive(self):
1831
        return not self.is_terminated
1832

    
1833
    @property
1834
    def is_terminated(self):
1835
        return self.is_deactivated(self.TERMINATED)
1836

    
1837
    @property
1838
    def is_suspended(self):
1839
        return self.is_deactivated(self.SUSPENDED)
1840

    
1841
    def violates_resource_grants(self):
1842
        return False
1843

    
1844
    def violates_members_limit(self, adding=0):
1845
        application = self.application
1846
        limit = application.limit_on_members_number
1847
        if limit is None:
1848
            return False
1849
        return (len(self.approved_members) + adding > limit)
1850

    
1851

    
1852
    ### Other
1853

    
1854
    def count_pending_memberships(self):
1855
        memb_set = self.projectmembership_set
1856
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1857
        return memb_count
1858

    
1859
    def members_count(self):
1860
        return self.approved_memberships.count()
1861

    
1862
    @property
1863
    def approved_memberships(self):
1864
        query = ProjectMembership.Q_ACCEPTED_STATES
1865
        return self.projectmembership_set.filter(query)
1866

    
1867
    @property
1868
    def approved_members(self):
1869
        return [m.person for m in self.approved_memberships]
1870

    
1871

    
1872
CHAIN_STATE = {
1873
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1874
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1875
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1876
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1877
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1878

    
1879
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1880
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1881
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1882
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1883
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1884

    
1885
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1886
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1887
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1888
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1889
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1890

    
1891
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1892
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1893
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1894
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1895
    }
1896

    
1897

    
1898
class ProjectMembershipManager(ForUpdateManager):
1899

    
1900
    def any_accepted(self):
1901
        q = self.model.Q_ACTUALLY_ACCEPTED
1902
        return self.filter(q)
1903

    
1904
    def actually_accepted(self):
1905
        q = self.model.Q_ACTUALLY_ACCEPTED
1906
        return self.filter(q)
1907

    
1908
    def requested(self):
1909
        return self.filter(state=ProjectMembership.REQUESTED)
1910

    
1911
    def suspended(self):
1912
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1913

    
1914
class ProjectMembership(models.Model):
1915

    
1916
    person              =   models.ForeignKey(AstakosUser)
1917
    request_date        =   models.DateTimeField(auto_now_add=True)
1918
    project             =   models.ForeignKey(Project)
1919

    
1920
    REQUESTED           =   0
1921
    ACCEPTED            =   1
1922
    LEAVE_REQUESTED     =   5
1923
    # User deactivation
1924
    USER_SUSPENDED      =   10
1925

    
1926
    REMOVED             =   200
1927

    
1928
    ASSOCIATED_STATES   =   set([REQUESTED,
1929
                                 ACCEPTED,
1930
                                 LEAVE_REQUESTED,
1931
                                 USER_SUSPENDED,
1932
                                 ])
1933

    
1934
    ACCEPTED_STATES     =   set([ACCEPTED,
1935
                                 LEAVE_REQUESTED,
1936
                                 USER_SUSPENDED,
1937
                                 ])
1938

    
1939
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1940

    
1941
    state               =   models.IntegerField(default=REQUESTED,
1942
                                                db_index=True)
1943
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
1944
    leave_request_date  =   models.DateTimeField(null=True)
1945

    
1946
    objects     =   ProjectMembershipManager()
1947

    
1948
    # Compiled queries
1949
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1950
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1951

    
1952
    MEMBERSHIP_STATE_DISPLAY = {
1953
        REQUESTED           : _('Requested'),
1954
        ACCEPTED            : _('Accepted'),
1955
        LEAVE_REQUESTED     : _('Leave Requested'),
1956
        USER_SUSPENDED      : _('Suspended'),
1957
        REMOVED             : _('Pending removal'),
1958
        }
1959

    
1960
    USER_FRIENDLY_STATE_DISPLAY = {
1961
        REQUESTED           : _('Join requested'),
1962
        ACCEPTED            : _('Accepted member'),
1963
        LEAVE_REQUESTED     : _('Requested to leave'),
1964
        USER_SUSPENDED      : _('Suspended member'),
1965
        REMOVED             : _('Pending removal'),
1966
        }
1967

    
1968
    def state_display(self):
1969
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1970

    
1971
    def user_friendly_state_display(self):
1972
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1973

    
1974
    class Meta:
1975
        unique_together = ("person", "project")
1976
        #index_together = [["project", "state"]]
1977

    
1978
    def __str__(self):
1979
        return uenc(_("<'%s' membership in '%s'>") % (
1980
                self.person.username, self.project))
1981

    
1982
    __repr__ = __str__
1983

    
1984
    def __init__(self, *args, **kwargs):
1985
        self.state = self.REQUESTED
1986
        super(ProjectMembership, self).__init__(*args, **kwargs)
1987

    
1988
    def _set_history_item(self, reason, date=None):
1989
        if isinstance(reason, basestring):
1990
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1991

    
1992
        history_item = ProjectMembershipHistory(
1993
                            serial=self.id,
1994
                            person=self.person_id,
1995
                            project=self.project_id,
1996
                            date=date or datetime.now(),
1997
                            reason=reason)
1998
        history_item.save()
1999
        serial = history_item.id
2000

    
2001
    def can_accept(self):
2002
        return self.state == self.REQUESTED
2003

    
2004
    def accept(self):
2005
        if not self.can_accept():
2006
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2007
            raise AssertionError(m)
2008

    
2009
        now = datetime.now()
2010
        self.acceptance_date = now
2011
        self._set_history_item(reason='ACCEPT', date=now)
2012
        self.state = self.ACCEPTED
2013
        self.save()
2014

    
2015
    def can_leave(self):
2016
        return self.state in self.ACCEPTED_STATES
2017

    
2018
    def leave_request(self):
2019
        if not self.can_leave():
2020
            m = _("%s: attempt to request to leave in state '%s'") % (
2021
                self, self.state)
2022
            raise AssertionError(m)
2023

    
2024
        self.leave_request_date = datetime.now()
2025
        self.state = self.LEAVE_REQUESTED
2026
        self.save()
2027

    
2028
    def can_deny_leave(self):
2029
        return self.state == self.LEAVE_REQUESTED
2030

    
2031
    def leave_request_deny(self):
2032
        if not self.can_deny_leave():
2033
            m = _("%s: attempt to deny leave request in state '%s'") % (
2034
                self, self.state)
2035
            raise AssertionError(m)
2036

    
2037
        self.leave_request_date = None
2038
        self.state = self.ACCEPTED
2039
        self.save()
2040

    
2041
    def can_cancel_leave(self):
2042
        return self.state == self.LEAVE_REQUESTED
2043

    
2044
    def leave_request_cancel(self):
2045
        if not self.can_cancel_leave():
2046
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2047
                self, self.state)
2048
            raise AssertionError(m)
2049

    
2050
        self.leave_request_date = None
2051
        self.state = self.ACCEPTED
2052
        self.save()
2053

    
2054
    def can_remove(self):
2055
        return self.state in self.ACCEPTED_STATES
2056

    
2057
    def remove(self):
2058
        if not self.can_remove():
2059
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2060
            raise AssertionError(m)
2061

    
2062
        self._set_history_item(reason='REMOVE')
2063
        self.delete()
2064

    
2065
    def can_reject(self):
2066
        return self.state == self.REQUESTED
2067

    
2068
    def reject(self):
2069
        if not self.can_reject():
2070
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2071
            raise AssertionError(m)
2072

    
2073
        # rejected requests don't need sync,
2074
        # because they were never effected
2075
        self._set_history_item(reason='REJECT')
2076
        self.delete()
2077

    
2078
    def can_cancel(self):
2079
        return self.state == self.REQUESTED
2080

    
2081
    def cancel(self):
2082
        if not self.can_cancel():
2083
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2084
            raise AssertionError(m)
2085

    
2086
        # rejected requests don't need sync,
2087
        # because they were never effected
2088
        self._set_history_item(reason='CANCEL')
2089
        self.delete()
2090

    
2091

    
2092
class Serial(models.Model):
2093
    serial  =   models.AutoField(primary_key=True)
2094

    
2095

    
2096
class ProjectMembershipHistory(models.Model):
2097
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2098
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2099

    
2100
    person  =   models.BigIntegerField()
2101
    project =   models.BigIntegerField()
2102
    date    =   models.DateTimeField(auto_now_add=True)
2103
    reason  =   models.IntegerField()
2104
    serial  =   models.BigIntegerField()
2105

    
2106
### SIGNALS ###
2107
################
2108

    
2109
def create_astakos_user(u):
2110
    try:
2111
        AstakosUser.objects.get(user_ptr=u.pk)
2112
    except AstakosUser.DoesNotExist:
2113
        extended_user = AstakosUser(user_ptr_id=u.pk)
2114
        extended_user.__dict__.update(u.__dict__)
2115
        extended_user.save()
2116
        if not extended_user.has_auth_provider('local'):
2117
            extended_user.add_auth_provider('local')
2118
    except BaseException, e:
2119
        logger.exception(e)
2120

    
2121
def fix_superusers():
2122
    # Associate superusers with AstakosUser
2123
    admins = User.objects.filter(is_superuser=True)
2124
    for u in admins:
2125
        create_astakos_user(u)
2126

    
2127
def user_post_save(sender, instance, created, **kwargs):
2128
    if not created:
2129
        return
2130
    create_astakos_user(instance)
2131
post_save.connect(user_post_save, sender=User)
2132

    
2133
def astakosuser_post_save(sender, instance, created, **kwargs):
2134
    pass
2135

    
2136
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2137

    
2138
def resource_post_save(sender, instance, created, **kwargs):
2139
    pass
2140

    
2141
post_save.connect(resource_post_save, sender=Resource)
2142

    
2143
def renew_token(sender, instance, **kwargs):
2144
    if not instance.auth_token:
2145
        instance.renew_token()
2146
pre_save.connect(renew_token, sender=AstakosUser)
2147
pre_save.connect(renew_token, sender=Service)