Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 8ee54f74

History | View | Annotate | Download (73.5 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
        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 is_project_admin(self, application_id=None):
432
        return self.uuid in astakos_settings.PROJECT_ADMINS
433

    
434
    @property
435
    def invitation(self):
436
        try:
437
            return Invitation.objects.get(username=self.email)
438
        except Invitation.DoesNotExist:
439
            return None
440

    
441
    @property
442
    def policies(self):
443
        return self.astakosuserquota_set.select_related().all()
444

    
445
    @policies.setter
446
    def policies(self, policies):
447
        for p in policies:
448
            p.setdefault('resource', '')
449
            p.setdefault('capacity', 0)
450
            p.setdefault('update', True)
451
            self.add_resource_policy(**p)
452

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

    
469
    def get_resource_policy(self, resource):
470
        resource = Resource.objects.get(name=resource)
471
        default_capacity = resource.uplimit
472
        try:
473
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
474
            return policy, default_capacity
475
        except AstakosUserQuota.DoesNotExist:
476
            return None, default_capacity
477

    
478
    def remove_resource_policy(self, service, resource):
479
        """Raises ObjectDoesNotExist, IntegrityError"""
480
        resource = Resource.objects.get(name=resource)
481
        q = self.policies.get(resource=resource).delete()
482

    
483
    def update_uuid(self):
484
        while not self.uuid:
485
            uuid_val =  str(uuid.uuid4())
486
            try:
487
                AstakosUser.objects.get(uuid=uuid_val)
488
            except AstakosUser.DoesNotExist, e:
489
                self.uuid = uuid_val
490
        return self.uuid
491

    
492
    def save(self, update_timestamps=True, **kwargs):
493
        if update_timestamps:
494
            if not self.id:
495
                self.date_joined = datetime.now()
496
            self.updated = datetime.now()
497

    
498
        # update date_signed_terms if necessary
499
        if self.__has_signed_terms != self.has_signed_terms:
500
            self.date_signed_terms = datetime.now()
501

    
502
        self.update_uuid()
503

    
504
        if self.username != self.email.lower():
505
            # set username
506
            self.username = self.email.lower()
507

    
508
        super(AstakosUser, self).save(**kwargs)
509

    
510
    def renew_token(self, flush_sessions=False, current_key=None):
511
        md5 = hashlib.md5()
512
        md5.update(settings.SECRET_KEY)
513
        md5.update(self.username)
514
        md5.update(self.realname.encode('ascii', 'ignore'))
515
        md5.update(asctime())
516

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

    
526
    def flush_sessions(self, current_key=None):
527
        q = self.sessions
528
        if current_key:
529
            q = q.exclude(session_key=current_key)
530

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

    
540
    def __unicode__(self):
541
        return '%s (%s)' % (self.realname, self.email)
542

    
543
    def conflicting_email(self):
544
        q = AstakosUser.objects.exclude(username=self.username)
545
        q = q.filter(email__iexact=self.email)
546
        if q.count() != 0:
547
            return True
548
        return False
549

    
550
    def email_change_is_pending(self):
551
        return self.emailchanges.count() > 0
552

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

    
569
    def set_invitations_level(self):
570
        """
571
        Update user invitation level
572
        """
573
        level = self.invitation.inviter.level + 1
574
        self.level = level
575
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
576

    
577
    def can_change_password(self):
578
        return self.has_auth_provider('local', auth_backend='astakos')
579

    
580
    def can_change_email(self):
581
        if not self.has_auth_provider('local'):
582
            return True
583

    
584
        local = self.get_auth_provider('local')._instance
585
        return local.auth_backend == 'astakos'
586

    
587
    # Auth providers related methods
588
    def get_auth_provider(self, module=None, identifier=None, **filters):
589
        if not module:
590
            return self.auth_providers.active()[0].settings
591

    
592
        params = {'module': module}
593
        if identifier:
594
            params['identifier'] = identifier
595
        params.update(filters)
596
        return self.auth_providers.active().get(**params).settings
597

    
598
    def has_auth_provider(self, provider, **kwargs):
599
        return bool(self.auth_providers.active().filter(module=provider,
600
                                                        **kwargs).count())
601

    
602
    def get_required_providers(self, **kwargs):
603
        return auth.REQUIRED_PROVIDERS.keys()
604

    
605
    def missing_required_providers(self):
606
        required = self.get_required_providers()
607
        missing = []
608
        for provider in required:
609
            if not self.has_auth_provider(provider):
610
                missing.append(auth.get_provider(provider, self))
611
        return missing
612

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

    
623
        for p in providers:
624
            if p.get_add_policy:
625
                available.append(p)
626
        return available
627

    
628
    def get_disabled_auth_providers(self, **filters):
629
        providers = self.get_auth_providers(**filters)
630
        disabled = []
631
        for p in providers:
632
            if not p.get_login_policy:
633
                disabled.append(p)
634
        return disabled
635

    
636
    def get_enabled_auth_providers(self, **filters):
637
        providers = self.get_auth_providers(**filters)
638
        enabled = []
639
        for p in providers:
640
            if p.get_login_policy:
641
                enabled.append(p)
642
        return enabled
643

    
644
    def get_auth_providers(self, **filters):
645
        providers = []
646
        for provider in self.auth_providers.active(**filters):
647
            if provider.settings.module_enabled:
648
                providers.append(provider.settings)
649

    
650
        modules = astakos_settings.IM_MODULES
651

    
652
        def key(p):
653
            if not p.module in modules:
654
                return 100
655
            return modules.index(p.module)
656

    
657
        providers = sorted(providers, key=key)
658
        return providers
659

    
660
    # URL methods
661
    @property
662
    def auth_providers_display(self):
663
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
664
                         self.get_enabled_auth_providers()])
665

    
666
    def add_auth_provider(self, module='local', identifier=None, **params):
667
        provider = auth.get_provider(module, self, identifier, **params)
668
        provider.add_to_user()
669

    
670
    def get_resend_activation_url(self):
671
        return reverse('send_activation', kwargs={'user_id': self.pk})
672

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

    
680
    def get_password_reset_url(self, token_generator=default_token_generator):
681
        return reverse('django.contrib.auth.views.password_reset_confirm',
682
                          kwargs={'uidb36':int_to_base36(self.id),
683
                                  'token':token_generator.make_token(self)})
684

    
685
    def get_inactive_message(self, provider_module, identifier=None):
686
        provider = self.get_auth_provider(provider_module, identifier)
687

    
688
        msg_extra = ''
689
        message = ''
690

    
691
        msg_inactive = provider.get_account_inactive_msg
692
        msg_pending = provider.get_pending_activation_msg
693
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
694
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
695
        msg_pending_mod = provider.get_pending_moderation_msg
696
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
697

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

    
716
        return mark_safe(message + u' '+ msg_extra)
717

    
718
    def owns_application(self, application):
719
        return application.owner == self
720

    
721
    def owns_project(self, project):
722
        return project.application.owner == self
723

    
724
    def is_associated(self, project):
725
        try:
726
            m = ProjectMembership.objects.get(person=self, project=project)
727
            return m.state in ProjectMembership.ASSOCIATED_STATES
728
        except ProjectMembership.DoesNotExist:
729
            return False
730

    
731
    def get_membership(self, project):
732
        try:
733
            return ProjectMembership.objects.get(
734
                project=project,
735
                person=self)
736
        except ProjectMembership.DoesNotExist:
737
            return None
738

    
739
    def membership_display(self, project):
740
        m = self.get_membership(project)
741
        if m is None:
742
            return _('Not a member')
743
        else:
744
            return m.user_friendly_state_display()
745

    
746
    def non_owner_can_view(self, maybe_project):
747
        if self.is_project_admin():
748
            return True
749
        if maybe_project is None:
750
            return False
751
        project = maybe_project
752
        if self.is_associated(project):
753
            return True
754
        if project.is_deactivated():
755
            return False
756
        return True
757

    
758
    def settings(self):
759
        return UserSetting.objects.filter(user=self)
760

    
761

    
762
class AstakosUserAuthProviderManager(models.Manager):
763

    
764
    def active(self, **filters):
765
        return self.filter(active=True, **filters)
766

    
767
    def remove_unverified_providers(self, provider, **filters):
768
        try:
769
            existing = self.filter(module=provider, user__email_verified=False,
770
                                   **filters)
771
            for p in existing:
772
                p.user.delete()
773
        except:
774
            pass
775

    
776
    def unverified(self, provider, **filters):
777
        try:
778
            return self.get(module=provider, user__email_verified=False,
779
                            **filters).settings
780
        except AstakosUserAuthProvider.DoesNotExist:
781
            return None
782

    
783
    def verified(self, provider, **filters):
784
        try:
785
            return self.get(module=provider, user__email_verified=True,
786
                            **filters).settings
787
        except AstakosUserAuthProvider.DoesNotExist:
788
            return None
789

    
790

    
791
class AuthProviderPolicyProfileManager(models.Manager):
792

    
793
    def active(self):
794
        return self.filter(active=True)
795

    
796
    def for_user(self, user, provider):
797
        policies = {}
798
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
799
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
800
        exclusive_q = exclusive_q1 | exclusive_q2
801

    
802
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
803
            policies.update(profile.policies)
804

    
805
        user_groups = user.groups.all().values('pk')
806
        for profile in self.active().filter(groups__in=user_groups).filter(
807
                exclusive_q):
808
            policies.update(profile.policies)
809
        return policies
810

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

    
826

    
827
class AuthProviderPolicyProfile(models.Model):
828
    name = models.CharField(_('Name'), max_length=255, blank=False,
829
                            null=False, db_index=True)
830
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
831
                                null=False)
832

    
833
    # apply policies to all providers excluding the one set in provider field
834
    is_exclusive = models.BooleanField(default=False)
835

    
836
    policy_add = models.NullBooleanField(null=True, default=None)
837
    policy_remove = models.NullBooleanField(null=True, default=None)
838
    policy_create = models.NullBooleanField(null=True, default=None)
839
    policy_login = models.NullBooleanField(null=True, default=None)
840
    policy_limit = models.IntegerField(null=True, default=None)
841
    policy_required = models.NullBooleanField(null=True, default=None)
842
    policy_automoderate = models.NullBooleanField(null=True, default=None)
843
    policy_switch = models.NullBooleanField(null=True, default=None)
844

    
845
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
846
                     'automoderate')
847

    
848
    priority = models.IntegerField(null=False, default=1)
849
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
850
    users = models.ManyToManyField(AstakosUser,
851
                                   related_name='authpolicy_profiles')
852
    active = models.BooleanField(default=True)
853

    
854
    objects = AuthProviderPolicyProfileManager()
855

    
856
    class Meta:
857
        ordering = ['priority']
858

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

    
869
    def set_policies(self, policies_dict):
870
        for key, value in policies_dict.iteritems():
871
            if key in self.POLICY_FIELDS:
872
                setattr(self, 'policy_%s' % key, value)
873
        return self.policies
874

    
875

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

    
894
    objects = AstakosUserAuthProviderManager()
895

    
896
    class Meta:
897
        unique_together = (('identifier', 'module', 'user'), )
898
        ordering = ('module', 'created')
899

    
900
    def __init__(self, *args, **kwargs):
901
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
902
        try:
903
            self.info = json.loads(self.info_data)
904
            if not self.info:
905
                self.info = {}
906
        except Exception, e:
907
            self.info = {}
908

    
909
        for key,value in self.info.iteritems():
910
            setattr(self, 'info_%s' % key, value)
911

    
912
    @property
913
    def settings(self):
914
        extra_data = {}
915

    
916
        info_data = {}
917
        if self.info_data:
918
            info_data = json.loads(self.info_data)
919

    
920
        extra_data['info'] = info_data
921

    
922
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
923
            extra_data[key] = getattr(self, key)
924

    
925
        extra_data['instance'] = self
926
        return auth.get_provider(self.module, self.user,
927
                                           self.identifier, **extra_data)
928

    
929
    def __repr__(self):
930
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
931

    
932
    def __unicode__(self):
933
        if self.identifier:
934
            return "%s:%s" % (self.module, self.identifier)
935
        if self.auth_backend:
936
            return "%s:%s" % (self.module, self.auth_backend)
937
        return self.module
938

    
939
    def save(self, *args, **kwargs):
940
        self.info_data = json.dumps(self.info)
941
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
942

    
943

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

    
971
    update_or_create = _update_or_create
972

    
973

    
974
class AstakosUserQuota(models.Model):
975
    objects = ExtendedManager()
976
    capacity = intDecimalField()
977
    resource = models.ForeignKey(Resource)
978
    user = models.ForeignKey(AstakosUser)
979

    
980
    class Meta:
981
        unique_together = ("resource", "user")
982

    
983

    
984
class ApprovalTerms(models.Model):
985
    """
986
    Model for approval terms
987
    """
988

    
989
    date = models.DateTimeField(
990
        _('Issue date'), db_index=True, auto_now_add=True)
991
    location = models.CharField(_('Terms location'), max_length=255)
992

    
993

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

    
1007
    def __init__(self, *args, **kwargs):
1008
        super(Invitation, self).__init__(*args, **kwargs)
1009
        if not self.id:
1010
            self.code = _generate_invitation_code()
1011

    
1012
    def consume(self):
1013
        self.is_consumed = True
1014
        self.consumed = datetime.now()
1015
        self.save()
1016

    
1017
    def __unicode__(self):
1018
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1019

    
1020

    
1021
class EmailChangeManager(models.Manager):
1022

    
1023
    @transaction.commit_on_success
1024
    def change_email(self, activation_key):
1025
        """
1026
        Validate an activation key and change the corresponding
1027
        ``User`` if valid.
1028

1029
        If the key is valid and has not expired, return the ``User``
1030
        after activating.
1031

1032
        If the key is not valid or has expired, return ``None``.
1033

1034
        If the key is valid but the ``User`` is already active,
1035
        return ``None``.
1036

1037
        After successful email change the activation record is deleted.
1038

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

    
1068

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

    
1081
    objects = EmailChangeManager()
1082

    
1083
    def get_url(self):
1084
        return reverse('email_change_confirm',
1085
                      kwargs={'activation_key': self.activation_key})
1086

    
1087
    def activation_key_expired(self):
1088
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1089
        return self.requested_at + expiration_date < datetime.now()
1090

    
1091

    
1092
class AdditionalMail(models.Model):
1093
    """
1094
    Model for registring invitations
1095
    """
1096
    owner = models.ForeignKey(AstakosUser)
1097
    email = models.EmailField()
1098

    
1099

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

    
1109

    
1110
def get_latest_terms():
1111
    try:
1112
        term = ApprovalTerms.objects.order_by('-id')[0]
1113
        return term
1114
    except IndexError:
1115
        pass
1116
    return None
1117

    
1118

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

    
1138
    class Meta:
1139
        unique_together = ("provider", "third_party_identifier")
1140

    
1141
    def get_user_instance(self):
1142
        d = self.__dict__
1143
        d.pop('_state', None)
1144
        d.pop('id', None)
1145
        d.pop('token', None)
1146
        d.pop('created', None)
1147
        d.pop('info', None)
1148
        user = AstakosUser(**d)
1149

    
1150
        return user
1151

    
1152
    @property
1153
    def realname(self):
1154
        return '%s %s' %(self.first_name, self.last_name)
1155

    
1156
    @realname.setter
1157
    def realname(self, value):
1158
        parts = value.split(' ')
1159
        if len(parts) == 2:
1160
            self.first_name = parts[0]
1161
            self.last_name = parts[1]
1162
        else:
1163
            self.last_name = parts[0]
1164

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

    
1176
    def generate_token(self):
1177
        self.password = self.third_party_identifier
1178
        self.last_login = datetime.now()
1179
        self.token = default_token_generator.make_token(self)
1180

    
1181
    def existing_user(self):
1182
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1183
                                         auth_providers__identifier=self.third_party_identifier)
1184

    
1185
    def get_provider(self, user):
1186
        params = {
1187
            'info_data': self.info,
1188
            'affiliation': self.affiliation
1189
        }
1190
        return auth.get_provider(self.provider, user,
1191
                                 self.third_party_identifier, **params)
1192

    
1193
class SessionCatalog(models.Model):
1194
    session_key = models.CharField(_('session key'), max_length=40)
1195
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1196

    
1197

    
1198
class UserSetting(models.Model):
1199
    user = models.ForeignKey(AstakosUser)
1200
    setting = models.CharField(max_length=255)
1201
    value = models.IntegerField()
1202

    
1203
    objects = ForUpdateManager()
1204

    
1205
    class Meta:
1206
        unique_together = ("user", "setting")
1207

    
1208

    
1209
### PROJECTS ###
1210
################
1211

    
1212
class ChainManager(ForUpdateManager):
1213

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

    
1222
    def all_full_state(self):
1223
        chains = self.all()
1224
        cids = [c.chain for c in chains]
1225
        projects = Project.objects.select_related('application').in_bulk(cids)
1226

    
1227
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1228
        chain_latest = dict(objs.values_list('chain', 'latest'))
1229

    
1230
        objs = ProjectApplication.objects.select_related('applicant')
1231
        apps = objs.in_bulk(chain_latest.values())
1232

    
1233
        d = {}
1234
        for chain in chains:
1235
            pk = chain.pk
1236
            project = projects.get(pk, None)
1237
            app = apps[chain_latest[pk]]
1238
            d[chain.pk] = chain.get_state(project, app)
1239

    
1240
        return d
1241

    
1242
    def of_project(self, project):
1243
        if project is None:
1244
            return None
1245
        try:
1246
            return self.get(chain=project.id)
1247
        except Chain.DoesNotExist:
1248
            raise AssertionError('project with no chain')
1249

    
1250

    
1251
class Chain(models.Model):
1252
    chain  =   models.AutoField(primary_key=True)
1253

    
1254
    def __str__(self):
1255
        return "%s" % (self.chain,)
1256

    
1257
    objects = ChainManager()
1258

    
1259
    PENDING            = 0
1260
    DENIED             = 3
1261
    DISMISSED          = 4
1262
    CANCELLED          = 5
1263

    
1264
    APPROVED           = 10
1265
    APPROVED_PENDING   = 11
1266
    SUSPENDED          = 12
1267
    SUSPENDED_PENDING  = 13
1268
    TERMINATED         = 14
1269
    TERMINATED_PENDING = 15
1270

    
1271
    PENDING_STATES = [PENDING,
1272
                      APPROVED_PENDING,
1273
                      SUSPENDED_PENDING,
1274
                      TERMINATED_PENDING,
1275
                      ]
1276

    
1277
    MODIFICATION_STATES = [APPROVED_PENDING,
1278
                           SUSPENDED_PENDING,
1279
                           TERMINATED_PENDING,
1280
                           ]
1281

    
1282
    RELEVANT_STATES = [PENDING,
1283
                       DENIED,
1284
                       APPROVED,
1285
                       APPROVED_PENDING,
1286
                       SUSPENDED,
1287
                       SUSPENDED_PENDING,
1288
                       TERMINATED_PENDING,
1289
                       ]
1290

    
1291
    SKIP_STATES = [DISMISSED,
1292
                   CANCELLED,
1293
                   TERMINATED]
1294

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

    
1308

    
1309
    @classmethod
1310
    def _chain_state(cls, project_state, app_state):
1311
        s = CHAIN_STATE.get((project_state, app_state), None)
1312
        if s is None:
1313
            raise AssertionError('inconsistent chain state')
1314
        return s
1315

    
1316
    @classmethod
1317
    def chain_state(cls, project, app):
1318
        p_state = project.state if project else None
1319
        return cls._chain_state(p_state, app.state)
1320

    
1321
    @classmethod
1322
    def state_display(cls, s):
1323
        if s is None:
1324
            return _("Unknown")
1325
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1326

    
1327
    def last_application(self):
1328
        return self.chained_apps.order_by('-id')[0]
1329

    
1330
    def get_project(self):
1331
        try:
1332
            return self.chained_project
1333
        except Project.DoesNotExist:
1334
            return None
1335

    
1336
    def get_elements(self):
1337
        project = self.get_project()
1338
        app = self.last_application()
1339
        return project, app
1340

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

    
1345
    def full_state(self):
1346
        project, app = self.get_elements()
1347
        return self.get_state(project, app)
1348

    
1349

    
1350
def new_chain():
1351
    c = Chain.objects.create()
1352
    return c
1353

    
1354

    
1355
class ProjectApplicationManager(ForUpdateManager):
1356

    
1357
    def user_visible_projects(self, *filters, **kw_filters):
1358
        model = self.model
1359
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1360

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

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

    
1379
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1380

    
1381
    def search_by_name(self, *search_strings):
1382
        q = Q()
1383
        for s in search_strings:
1384
            q = q | Q(name__icontains=s)
1385
        return self.filter(q)
1386

    
1387
    def latest_of_chain(self, chain_id):
1388
        try:
1389
            return self.filter(chain=chain_id).order_by('-id')[0]
1390
        except IndexError:
1391
            return None
1392

    
1393

    
1394
class ProjectApplication(models.Model):
1395
    applicant               =   models.ForeignKey(
1396
                                    AstakosUser,
1397
                                    related_name='projects_applied',
1398
                                    db_index=True)
1399

    
1400
    PENDING     =    0
1401
    APPROVED    =    1
1402
    REPLACED    =    2
1403
    DENIED      =    3
1404
    DISMISSED   =    4
1405
    CANCELLED   =    5
1406

    
1407
    state                   =   models.IntegerField(default=PENDING,
1408
                                                    db_index=True)
1409

    
1410
    owner                   =   models.ForeignKey(
1411
                                    AstakosUser,
1412
                                    related_name='projects_owned',
1413
                                    db_index=True)
1414

    
1415
    chain                   =   models.ForeignKey(Chain,
1416
                                                  related_name='chained_apps',
1417
                                                  db_column='chain')
1418
    precursor_application   =   models.ForeignKey('ProjectApplication',
1419
                                                  null=True,
1420
                                                  blank=True)
1421

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

    
1441
    objects                 =   ProjectApplicationManager()
1442

    
1443
    # Compiled queries
1444
    Q_PENDING  = Q(state=PENDING)
1445
    Q_APPROVED = Q(state=APPROVED)
1446
    Q_DENIED   = Q(state=DENIED)
1447

    
1448
    class Meta:
1449
        unique_together = ("chain", "id")
1450

    
1451
    def __unicode__(self):
1452
        return "%s applied by %s" % (self.name, self.applicant)
1453

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

    
1464
    @property
1465
    def log_display(self):
1466
        return "application %s (%s) for project %s" % (
1467
            self.id, self.name, self.chain)
1468

    
1469
    def get_project(self):
1470
        try:
1471
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1472
            return Project
1473
        except Project.DoesNotExist, e:
1474
            return None
1475

    
1476
    def state_display(self):
1477
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1478

    
1479
    def project_state_display(self):
1480
        try:
1481
            project = self.project
1482
            return project.state_display()
1483
        except Project.DoesNotExist:
1484
            return self.state_display()
1485

    
1486
    def add_resource_policy(self, resource, uplimit):
1487
        """Raises ObjectDoesNotExist, IntegrityError"""
1488
        q = self.projectresourcegrant_set
1489
        resource = Resource.objects.get(name=resource)
1490
        q.create(resource=resource, member_capacity=uplimit)
1491

    
1492
    def members_count(self):
1493
        return self.project.approved_memberships.count()
1494

    
1495
    @property
1496
    def grants(self):
1497
        return self.projectresourcegrant_set.values('member_capacity',
1498
                                                    'resource__name')
1499

    
1500
    @property
1501
    def resource_policies(self):
1502
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1503

    
1504
    def set_resource_policies(self, policies):
1505
        for resource, uplimit in policies:
1506
            self.add_resource_policy(resource, uplimit)
1507

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1631
        now = datetime.now()
1632
        project = self._get_project_for_update()
1633

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

    
1645
        new_project = False
1646
        if project is None:
1647
            new_project = True
1648
            project = Project(id=self.chain)
1649

    
1650
        project.name = new_project_name
1651
        project.application = self
1652
        project.last_approval_date = now
1653

    
1654
        project.save()
1655

    
1656
        self.state = self.APPROVED
1657
        self.response_date = now
1658
        self.response = reason
1659
        self.save()
1660
        return project
1661

    
1662
    @property
1663
    def member_join_policy_display(self):
1664
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1665

    
1666
    @property
1667
    def member_leave_policy_display(self):
1668
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1669

    
1670
class ProjectResourceGrant(models.Model):
1671

    
1672
    resource                =   models.ForeignKey(Resource)
1673
    project_application     =   models.ForeignKey(ProjectApplication,
1674
                                                  null=True)
1675
    project_capacity        =   intDecimalField(null=True)
1676
    member_capacity         =   intDecimalField(default=0)
1677

    
1678
    objects = ExtendedManager()
1679

    
1680
    class Meta:
1681
        unique_together = ("resource", "project_application")
1682

    
1683
    def display_member_capacity(self):
1684
        if self.member_capacity:
1685
            if self.resource.unit:
1686
                return ProjectResourceGrant.display_filesize(
1687
                    self.member_capacity)
1688
            else:
1689
                if math.isinf(self.member_capacity):
1690
                    return 'Unlimited'
1691
                else:
1692
                    return self.member_capacity
1693
        else:
1694
            return 'Unlimited'
1695

    
1696
    def __str__(self):
1697
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1698
                                        self.display_member_capacity())
1699

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

    
1724

    
1725
class ProjectManager(ForUpdateManager):
1726

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

    
1731
    def not_terminated_projects(self):
1732
        q = ~self.model.Q_TERMINATED
1733
        return self.filter(q)
1734

    
1735
    def deactivated_projects(self):
1736
        q = self.model.Q_DEACTIVATED
1737
        return self.filter(q)
1738

    
1739
    def expired_projects(self):
1740
        q = (~Q(state=Project.TERMINATED) &
1741
              Q(application__end_date__lt=datetime.now()))
1742
        return self.filter(q)
1743

    
1744
    def search_by_name(self, *search_strings):
1745
        q = Q()
1746
        for s in search_strings:
1747
            q = q | Q(name__icontains=s)
1748
        return self.filter(q)
1749

    
1750

    
1751
class Project(models.Model):
1752

    
1753
    id                          =   models.OneToOneField(Chain,
1754
                                                      related_name='chained_project',
1755
                                                      db_column='id',
1756
                                                      primary_key=True)
1757

    
1758
    application                 =   models.OneToOneField(
1759
                                            ProjectApplication,
1760
                                            related_name='project')
1761
    last_approval_date          =   models.DateTimeField(null=True)
1762

    
1763
    members                     =   models.ManyToManyField(
1764
                                            AstakosUser,
1765
                                            through='ProjectMembership')
1766

    
1767
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1768
    deactivation_date           =   models.DateTimeField(null=True)
1769

    
1770
    creation_date               =   models.DateTimeField(auto_now_add=True)
1771
    name                        =   models.CharField(
1772
                                            max_length=80,
1773
                                            null=True,
1774
                                            db_index=True,
1775
                                            unique=True)
1776

    
1777
    APPROVED    = 1
1778
    SUSPENDED   = 10
1779
    TERMINATED  = 100
1780

    
1781
    state                       =   models.IntegerField(default=APPROVED,
1782
                                                        db_index=True)
1783

    
1784
    objects     =   ProjectManager()
1785

    
1786
    # Compiled queries
1787
    Q_TERMINATED  = Q(state=TERMINATED)
1788
    Q_SUSPENDED   = Q(state=SUSPENDED)
1789
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1790

    
1791
    def __str__(self):
1792
        return uenc(_("<project %s '%s'>") %
1793
                    (self.id, udec(self.application.name)))
1794

    
1795
    __repr__ = __str__
1796

    
1797
    def __unicode__(self):
1798
        return _("<project %s '%s'>") % (self.id, self.application.name)
1799

    
1800
    STATE_DISPLAY = {
1801
        APPROVED   : 'Active',
1802
        SUSPENDED  : 'Suspended',
1803
        TERMINATED : 'Terminated'
1804
        }
1805

    
1806
    def state_display(self):
1807
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1808

    
1809
    def expiration_info(self):
1810
        return (str(self.id), self.name, self.state_display(),
1811
                str(self.application.end_date))
1812

    
1813
    def is_deactivated(self, reason=None):
1814
        if reason is not None:
1815
            return self.state == reason
1816

    
1817
        return self.state != self.APPROVED
1818

    
1819
    ### Deactivation calls
1820

    
1821
    def terminate(self):
1822
        self.deactivation_reason = 'TERMINATED'
1823
        self.deactivation_date = datetime.now()
1824
        self.state = self.TERMINATED
1825
        self.name = None
1826
        self.save()
1827

    
1828
    def suspend(self):
1829
        self.deactivation_reason = 'SUSPENDED'
1830
        self.deactivation_date = datetime.now()
1831
        self.state = self.SUSPENDED
1832
        self.save()
1833

    
1834
    def resume(self):
1835
        self.deactivation_reason = None
1836
        self.deactivation_date = None
1837
        self.state = self.APPROVED
1838
        self.save()
1839

    
1840
    ### Logical checks
1841

    
1842
    def is_inconsistent(self):
1843
        now = datetime.now()
1844
        dates = [self.creation_date,
1845
                 self.last_approval_date,
1846
                 self.deactivation_date]
1847
        return any([date > now for date in dates])
1848

    
1849
    def is_approved(self):
1850
        return self.state == self.APPROVED
1851

    
1852
    @property
1853
    def is_alive(self):
1854
        return not self.is_terminated
1855

    
1856
    @property
1857
    def is_terminated(self):
1858
        return self.is_deactivated(self.TERMINATED)
1859

    
1860
    @property
1861
    def is_suspended(self):
1862
        return self.is_deactivated(self.SUSPENDED)
1863

    
1864
    def violates_resource_grants(self):
1865
        return False
1866

    
1867
    def violates_members_limit(self, adding=0):
1868
        application = self.application
1869
        limit = application.limit_on_members_number
1870
        if limit is None:
1871
            return False
1872
        return (len(self.approved_members) + adding > limit)
1873

    
1874

    
1875
    ### Other
1876

    
1877
    def count_pending_memberships(self):
1878
        memb_set = self.projectmembership_set
1879
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1880
        return memb_count
1881

    
1882
    def members_count(self):
1883
        return self.approved_memberships.count()
1884

    
1885
    @property
1886
    def approved_memberships(self):
1887
        query = ProjectMembership.Q_ACCEPTED_STATES
1888
        return self.projectmembership_set.filter(query)
1889

    
1890
    @property
1891
    def approved_members(self):
1892
        return [m.person for m in self.approved_memberships]
1893

    
1894
    def add_member(self, user):
1895
        """
1896
        Raises:
1897
            astakos.im.models.AstakosUser.DoesNotExist
1898
        """
1899
        if isinstance(user, (int, long)):
1900
            user = AstakosUser.objects.get(user=user)
1901

    
1902
        m, created = ProjectMembership.objects.get_or_create(
1903
            person=user, project=self
1904
        )
1905
        m.accept()
1906

    
1907
    def remove_member(self, user):
1908
        """
1909
        Raises:
1910
            astakos.im.models.AstakosUser.DoesNotExist
1911
            astakos.im.models.ProjectMembership.DoesNotExist
1912
        """
1913
        if isinstance(user, (int, long)):
1914
            user = AstakosUser.objects.get(user=user)
1915

    
1916
        m = ProjectMembership.objects.get(person=user, project=self)
1917
        m.remove()
1918

    
1919

    
1920
CHAIN_STATE = {
1921
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1922
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1923
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1924
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1925
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1926

    
1927
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1928
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1929
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1930
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1931
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1932

    
1933
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1934
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1935
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1936
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1937
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1938

    
1939
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1940
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1941
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1942
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1943
    }
1944

    
1945

    
1946
class ProjectMembershipManager(ForUpdateManager):
1947

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

    
1952
    def actually_accepted(self):
1953
        q = self.model.Q_ACTUALLY_ACCEPTED
1954
        return self.filter(q)
1955

    
1956
    def requested(self):
1957
        return self.filter(state=ProjectMembership.REQUESTED)
1958

    
1959
    def suspended(self):
1960
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1961

    
1962
class ProjectMembership(models.Model):
1963

    
1964
    person              =   models.ForeignKey(AstakosUser)
1965
    request_date        =   models.DateField(auto_now_add=True)
1966
    project             =   models.ForeignKey(Project)
1967

    
1968
    REQUESTED           =   0
1969
    ACCEPTED            =   1
1970
    LEAVE_REQUESTED     =   5
1971
    # User deactivation
1972
    USER_SUSPENDED      =   10
1973

    
1974
    REMOVED             =   200
1975

    
1976
    ASSOCIATED_STATES   =   set([REQUESTED,
1977
                                 ACCEPTED,
1978
                                 LEAVE_REQUESTED,
1979
                                 USER_SUSPENDED,
1980
                                 ])
1981

    
1982
    ACCEPTED_STATES     =   set([ACCEPTED,
1983
                                 LEAVE_REQUESTED,
1984
                                 USER_SUSPENDED,
1985
                                 ])
1986

    
1987
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1988

    
1989
    state               =   models.IntegerField(default=REQUESTED,
1990
                                                db_index=True)
1991
    acceptance_date     =   models.DateField(null=True, db_index=True)
1992
    leave_request_date  =   models.DateField(null=True)
1993

    
1994
    objects     =   ProjectMembershipManager()
1995

    
1996
    # Compiled queries
1997
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1998
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1999

    
2000
    MEMBERSHIP_STATE_DISPLAY = {
2001
        REQUESTED           : _('Requested'),
2002
        ACCEPTED            : _('Accepted'),
2003
        LEAVE_REQUESTED     : _('Leave Requested'),
2004
        USER_SUSPENDED      : _('Suspended'),
2005
        REMOVED             : _('Pending removal'),
2006
        }
2007

    
2008
    USER_FRIENDLY_STATE_DISPLAY = {
2009
        REQUESTED           : _('Join requested'),
2010
        ACCEPTED            : _('Accepted member'),
2011
        LEAVE_REQUESTED     : _('Requested to leave'),
2012
        USER_SUSPENDED      : _('Suspended member'),
2013
        REMOVED             : _('Pending removal'),
2014
        }
2015

    
2016
    def state_display(self):
2017
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2018

    
2019
    def user_friendly_state_display(self):
2020
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2021

    
2022
    class Meta:
2023
        unique_together = ("person", "project")
2024
        #index_together = [["project", "state"]]
2025

    
2026
    def __str__(self):
2027
        return uenc(_("<'%s' membership in '%s'>") % (
2028
                self.person.username, self.project))
2029

    
2030
    __repr__ = __str__
2031

    
2032
    def __init__(self, *args, **kwargs):
2033
        self.state = self.REQUESTED
2034
        super(ProjectMembership, self).__init__(*args, **kwargs)
2035

    
2036
    def _set_history_item(self, reason, date=None):
2037
        if isinstance(reason, basestring):
2038
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2039

    
2040
        history_item = ProjectMembershipHistory(
2041
                            serial=self.id,
2042
                            person=self.person_id,
2043
                            project=self.project_id,
2044
                            date=date or datetime.now(),
2045
                            reason=reason)
2046
        history_item.save()
2047
        serial = history_item.id
2048

    
2049
    def can_accept(self):
2050
        return self.state == self.REQUESTED
2051

    
2052
    def accept(self):
2053
        if not self.can_accept():
2054
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2055
            raise AssertionError(m)
2056

    
2057
        now = datetime.now()
2058
        self.acceptance_date = now
2059
        self._set_history_item(reason='ACCEPT', date=now)
2060
        self.state = self.ACCEPTED
2061
        self.save()
2062

    
2063
    def can_leave(self):
2064
        return self.state in self.ACCEPTED_STATES
2065

    
2066
    def leave_request(self):
2067
        if not self.can_leave():
2068
            m = _("%s: attempt to request to leave in state '%s'") % (
2069
                self, self.state)
2070
            raise AssertionError(m)
2071

    
2072
        self.leave_request_date = datetime.now()
2073
        self.state = self.LEAVE_REQUESTED
2074
        self.save()
2075

    
2076
    def can_deny_leave(self):
2077
        return self.state == self.LEAVE_REQUESTED
2078

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

    
2085
        self.leave_request_date = None
2086
        self.state = self.ACCEPTED
2087
        self.save()
2088

    
2089
    def can_cancel_leave(self):
2090
        return self.state == self.LEAVE_REQUESTED
2091

    
2092
    def leave_request_cancel(self):
2093
        if not self.can_cancel_leave():
2094
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2095
                self, self.state)
2096
            raise AssertionError(m)
2097

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

    
2102
    def can_remove(self):
2103
        return self.state in self.ACCEPTED_STATES
2104

    
2105
    def remove(self):
2106
        if not self.can_remove():
2107
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2108
            raise AssertionError(m)
2109

    
2110
        self._set_history_item(reason='REMOVE')
2111
        self.delete()
2112

    
2113
    def can_reject(self):
2114
        return self.state == self.REQUESTED
2115

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

    
2121
        # rejected requests don't need sync,
2122
        # because they were never effected
2123
        self._set_history_item(reason='REJECT')
2124
        self.delete()
2125

    
2126
    def can_cancel(self):
2127
        return self.state == self.REQUESTED
2128

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

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

    
2139

    
2140
class Serial(models.Model):
2141
    serial  =   models.AutoField(primary_key=True)
2142

    
2143

    
2144
class ProjectMembershipHistory(models.Model):
2145
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2146
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2147

    
2148
    person  =   models.BigIntegerField()
2149
    project =   models.BigIntegerField()
2150
    date    =   models.DateField(auto_now_add=True)
2151
    reason  =   models.IntegerField()
2152
    serial  =   models.BigIntegerField()
2153

    
2154
### SIGNALS ###
2155
################
2156

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

    
2169
def fix_superusers():
2170
    # Associate superusers with AstakosUser
2171
    admins = User.objects.filter(is_superuser=True)
2172
    for u in admins:
2173
        create_astakos_user(u)
2174

    
2175
def user_post_save(sender, instance, created, **kwargs):
2176
    if not created:
2177
        return
2178
    create_astakos_user(instance)
2179
post_save.connect(user_post_save, sender=User)
2180

    
2181
def astakosuser_post_save(sender, instance, created, **kwargs):
2182
    pass
2183

    
2184
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2185

    
2186
def resource_post_save(sender, instance, created, **kwargs):
2187
    pass
2188

    
2189
post_save.connect(resource_post_save, sender=Resource)
2190

    
2191
def renew_token(sender, instance, **kwargs):
2192
    if not instance.auth_token:
2193
        instance.renew_token()
2194
pre_save.connect(renew_token, sender=AstakosUser)
2195
pre_save.connect(renew_token, sender=Service)