Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ eee9ec4d

History | View | Annotate | Download (73.7 kB)

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

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

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

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

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

    
67
from synnefo.lib.utils import dict_merge
68

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

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

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

    
85
logger = logging.getLogger(__name__)
86

    
87
DEFAULT_CONTENT_TYPE = None
88
_content_type = None
89

    
90

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

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

    
104
inf = float('inf')
105

    
106

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

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

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

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

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

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

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

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

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

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

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

    
175

    
176
_presentation_data = {}
177

    
178

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

    
188

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

    
197
    objects = ForUpdateManager()
198

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

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

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

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

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

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

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

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

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

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

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

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

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

    
263

    
264
class AstakosUserManager(UserManager):
265

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

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

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

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

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

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

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

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

    
318

    
319

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

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

    
336

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

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

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

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

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

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

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

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

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

    
380
    objects = AstakosUserManager()
381

    
382
    forupdate = ForUpdateManager()
383

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
498
        self.update_uuid()
499

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
646
        modules = astakos_settings.IM_MODULES
647

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

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

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

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

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

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

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

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

    
684
        msg_extra = ''
685
        message = ''
686

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

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

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

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

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

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

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

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

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

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

    
757

    
758
class AstakosUserAuthProviderManager(models.Manager):
759

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

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

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

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

    
786

    
787
class AuthProviderPolicyProfileManager(models.Manager):
788

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

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

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

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

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

    
822

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

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

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

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

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

    
850
    objects = AuthProviderPolicyProfileManager()
851

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

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

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

    
871

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

    
890
    objects = AstakosUserAuthProviderManager()
891

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

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

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

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

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

    
916
        extra_data['info'] = info_data
917

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

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

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

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

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

    
939

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

    
967
    update_or_create = _update_or_create
968

    
969

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

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

    
979

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

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

    
989

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

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

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

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

    
1016

    
1017
class EmailChangeManager(models.Manager):
1018

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

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

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

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

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

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

    
1064

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

    
1077
    objects = EmailChangeManager()
1078

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

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

    
1087

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

    
1095

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

    
1105

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

    
1114

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

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

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

    
1146
        return user
1147

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

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

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

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

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

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

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

    
1193

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

    
1199
    objects = ForUpdateManager()
1200

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

    
1204

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

    
1208
class ChainManager(ForUpdateManager):
1209

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

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

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

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

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

    
1236
        return d
1237

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

    
1246

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

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

    
1253
    objects = ChainManager()
1254

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

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

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

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

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

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

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

    
1304

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

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

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

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

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

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

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

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

    
1345

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

    
1350

    
1351
class ProjectApplicationManager(ForUpdateManager):
1352

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

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

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

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

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

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

    
1389

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

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

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

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

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

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

    
1437
    objects                 =   ProjectApplicationManager()
1438

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

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

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

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

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

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

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

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

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

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

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

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

    
1500
    def set_resource_policies(self, policies):
1501
        for resource, uplimit in policies:
1502
            self.add_resource_policy(resource, uplimit)
1503

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1620
    def approve(self, approval_user=None):
1621
        """
1622
        If approval_user then during owner membership acceptance
1623
        it is checked whether the request_user is eligible.
1624

1625
        Raises:
1626
            PermissionDenied
1627
        """
1628

    
1629
        if not transaction.is_managed():
1630
            raise AssertionError("NOPE")
1631

    
1632
        new_project_name = self.name
1633
        if not self.can_approve():
1634
            m = _("cannot approve: project '%s' in state '%s'") % (
1635
                    new_project_name, self.state)
1636
            raise AssertionError(m) # invalid argument
1637

    
1638
        now = datetime.now()
1639
        project = self._get_project_for_update()
1640

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

    
1652
        new_project = False
1653
        if project is None:
1654
            new_project = True
1655
            project = Project(id=self.chain)
1656

    
1657
        project.name = new_project_name
1658
        project.application = self
1659
        project.last_approval_date = now
1660

    
1661
        project.save()
1662

    
1663
        self.state = self.APPROVED
1664
        self.response_date = now
1665
        self.save()
1666
        return project
1667

    
1668
    @property
1669
    def member_join_policy_display(self):
1670
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1671

    
1672
    @property
1673
    def member_leave_policy_display(self):
1674
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1675

    
1676
class ProjectResourceGrant(models.Model):
1677

    
1678
    resource                =   models.ForeignKey(Resource)
1679
    project_application     =   models.ForeignKey(ProjectApplication,
1680
                                                  null=True)
1681
    project_capacity        =   intDecimalField(null=True)
1682
    member_capacity         =   intDecimalField(default=0)
1683

    
1684
    objects = ExtendedManager()
1685

    
1686
    class Meta:
1687
        unique_together = ("resource", "project_application")
1688

    
1689
    def display_member_capacity(self):
1690
        if self.member_capacity:
1691
            if self.resource.unit:
1692
                return ProjectResourceGrant.display_filesize(
1693
                    self.member_capacity)
1694
            else:
1695
                if math.isinf(self.member_capacity):
1696
                    return 'Unlimited'
1697
                else:
1698
                    return self.member_capacity
1699
        else:
1700
            return 'Unlimited'
1701

    
1702
    def __str__(self):
1703
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1704
                                        self.display_member_capacity())
1705

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

    
1730

    
1731
class ProjectManager(ForUpdateManager):
1732

    
1733
    def terminated_projects(self):
1734
        q = self.model.Q_TERMINATED
1735
        return self.filter(q)
1736

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

    
1741
    def deactivated_projects(self):
1742
        q = self.model.Q_DEACTIVATED
1743
        return self.filter(q)
1744

    
1745
    def expired_projects(self):
1746
        q = (~Q(state=Project.TERMINATED) &
1747
              Q(application__end_date__lt=datetime.now()))
1748
        return self.filter(q)
1749

    
1750
    def search_by_name(self, *search_strings):
1751
        q = Q()
1752
        for s in search_strings:
1753
            q = q | Q(name__icontains=s)
1754
        return self.filter(q)
1755

    
1756

    
1757
class Project(models.Model):
1758

    
1759
    id                          =   models.OneToOneField(Chain,
1760
                                                      related_name='chained_project',
1761
                                                      db_column='id',
1762
                                                      primary_key=True)
1763

    
1764
    application                 =   models.OneToOneField(
1765
                                            ProjectApplication,
1766
                                            related_name='project')
1767
    last_approval_date          =   models.DateTimeField(null=True)
1768

    
1769
    members                     =   models.ManyToManyField(
1770
                                            AstakosUser,
1771
                                            through='ProjectMembership')
1772

    
1773
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1774
    deactivation_date           =   models.DateTimeField(null=True)
1775

    
1776
    creation_date               =   models.DateTimeField(auto_now_add=True)
1777
    name                        =   models.CharField(
1778
                                            max_length=80,
1779
                                            null=True,
1780
                                            db_index=True,
1781
                                            unique=True)
1782

    
1783
    APPROVED    = 1
1784
    SUSPENDED   = 10
1785
    TERMINATED  = 100
1786

    
1787
    state                       =   models.IntegerField(default=APPROVED,
1788
                                                        db_index=True)
1789

    
1790
    objects     =   ProjectManager()
1791

    
1792
    # Compiled queries
1793
    Q_TERMINATED  = Q(state=TERMINATED)
1794
    Q_SUSPENDED   = Q(state=SUSPENDED)
1795
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1796

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

    
1801
    __repr__ = __str__
1802

    
1803
    def __unicode__(self):
1804
        return _("<project %s '%s'>") % (self.id, self.application.name)
1805

    
1806
    STATE_DISPLAY = {
1807
        APPROVED   : 'Active',
1808
        SUSPENDED  : 'Suspended',
1809
        TERMINATED : 'Terminated'
1810
        }
1811

    
1812
    def state_display(self):
1813
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1814

    
1815
    def expiration_info(self):
1816
        return (str(self.id), self.name, self.state_display(),
1817
                str(self.application.end_date))
1818

    
1819
    def is_deactivated(self, reason=None):
1820
        if reason is not None:
1821
            return self.state == reason
1822

    
1823
        return self.state != self.APPROVED
1824

    
1825
    ### Deactivation calls
1826

    
1827
    def terminate(self):
1828
        self.deactivation_reason = 'TERMINATED'
1829
        self.deactivation_date = datetime.now()
1830
        self.state = self.TERMINATED
1831
        self.name = None
1832
        self.save()
1833

    
1834
    def suspend(self):
1835
        self.deactivation_reason = 'SUSPENDED'
1836
        self.deactivation_date = datetime.now()
1837
        self.state = self.SUSPENDED
1838
        self.save()
1839

    
1840
    def resume(self):
1841
        self.deactivation_reason = None
1842
        self.deactivation_date = None
1843
        self.state = self.APPROVED
1844
        self.save()
1845

    
1846
    ### Logical checks
1847

    
1848
    def is_inconsistent(self):
1849
        now = datetime.now()
1850
        dates = [self.creation_date,
1851
                 self.last_approval_date,
1852
                 self.deactivation_date]
1853
        return any([date > now for date in dates])
1854

    
1855
    def is_approved(self):
1856
        return self.state == self.APPROVED
1857

    
1858
    @property
1859
    def is_alive(self):
1860
        return not self.is_terminated
1861

    
1862
    @property
1863
    def is_terminated(self):
1864
        return self.is_deactivated(self.TERMINATED)
1865

    
1866
    @property
1867
    def is_suspended(self):
1868
        return self.is_deactivated(self.SUSPENDED)
1869

    
1870
    def violates_resource_grants(self):
1871
        return False
1872

    
1873
    def violates_members_limit(self, adding=0):
1874
        application = self.application
1875
        limit = application.limit_on_members_number
1876
        if limit is None:
1877
            return False
1878
        return (len(self.approved_members) + adding > limit)
1879

    
1880

    
1881
    ### Other
1882

    
1883
    def count_pending_memberships(self):
1884
        memb_set = self.projectmembership_set
1885
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1886
        return memb_count
1887

    
1888
    def members_count(self):
1889
        return self.approved_memberships.count()
1890

    
1891
    @property
1892
    def approved_memberships(self):
1893
        query = ProjectMembership.Q_ACCEPTED_STATES
1894
        return self.projectmembership_set.filter(query)
1895

    
1896
    @property
1897
    def approved_members(self):
1898
        return [m.person for m in self.approved_memberships]
1899

    
1900
    def add_member(self, user):
1901
        """
1902
        Raises:
1903
            django.exceptions.PermissionDenied
1904
            astakos.im.models.AstakosUser.DoesNotExist
1905
        """
1906
        if isinstance(user, (int, long)):
1907
            user = AstakosUser.objects.get(user=user)
1908

    
1909
        m, created = ProjectMembership.objects.get_or_create(
1910
            person=user, project=self
1911
        )
1912
        m.accept()
1913

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

    
1924
        m = ProjectMembership.objects.get(person=user, project=self)
1925
        m.remove()
1926

    
1927

    
1928
CHAIN_STATE = {
1929
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1930
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1931
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1932
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1933
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1934

    
1935
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1936
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1937
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1938
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1939
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1940

    
1941
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1942
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1943
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1944
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1945
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1946

    
1947
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1948
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1949
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1950
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1951
    }
1952

    
1953

    
1954
class ProjectMembershipManager(ForUpdateManager):
1955

    
1956
    def any_accepted(self):
1957
        q = self.model.Q_ACTUALLY_ACCEPTED
1958
        return self.filter(q)
1959

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

    
1964
    def requested(self):
1965
        return self.filter(state=ProjectMembership.REQUESTED)
1966

    
1967
    def suspended(self):
1968
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1969

    
1970
class ProjectMembership(models.Model):
1971

    
1972
    person              =   models.ForeignKey(AstakosUser)
1973
    request_date        =   models.DateField(auto_now_add=True)
1974
    project             =   models.ForeignKey(Project)
1975

    
1976
    REQUESTED           =   0
1977
    ACCEPTED            =   1
1978
    LEAVE_REQUESTED     =   5
1979
    # User deactivation
1980
    USER_SUSPENDED      =   10
1981

    
1982
    REMOVED             =   200
1983

    
1984
    ASSOCIATED_STATES   =   set([REQUESTED,
1985
                                 ACCEPTED,
1986
                                 LEAVE_REQUESTED,
1987
                                 USER_SUSPENDED,
1988
                                 ])
1989

    
1990
    ACCEPTED_STATES     =   set([ACCEPTED,
1991
                                 LEAVE_REQUESTED,
1992
                                 USER_SUSPENDED,
1993
                                 ])
1994

    
1995
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1996

    
1997
    state               =   models.IntegerField(default=REQUESTED,
1998
                                                db_index=True)
1999
    acceptance_date     =   models.DateField(null=True, db_index=True)
2000
    leave_request_date  =   models.DateField(null=True)
2001

    
2002
    objects     =   ProjectMembershipManager()
2003

    
2004
    # Compiled queries
2005
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2006
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2007

    
2008
    MEMBERSHIP_STATE_DISPLAY = {
2009
        REQUESTED           : _('Requested'),
2010
        ACCEPTED            : _('Accepted'),
2011
        LEAVE_REQUESTED     : _('Leave Requested'),
2012
        USER_SUSPENDED      : _('Suspended'),
2013
        REMOVED             : _('Pending removal'),
2014
        }
2015

    
2016
    USER_FRIENDLY_STATE_DISPLAY = {
2017
        REQUESTED           : _('Join requested'),
2018
        ACCEPTED            : _('Accepted member'),
2019
        LEAVE_REQUESTED     : _('Requested to leave'),
2020
        USER_SUSPENDED      : _('Suspended member'),
2021
        REMOVED             : _('Pending removal'),
2022
        }
2023

    
2024
    def state_display(self):
2025
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2026

    
2027
    def user_friendly_state_display(self):
2028
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2029

    
2030
    class Meta:
2031
        unique_together = ("person", "project")
2032
        #index_together = [["project", "state"]]
2033

    
2034
    def __str__(self):
2035
        return uenc(_("<'%s' membership in '%s'>") % (
2036
                self.person.username, self.project))
2037

    
2038
    __repr__ = __str__
2039

    
2040
    def __init__(self, *args, **kwargs):
2041
        self.state = self.REQUESTED
2042
        super(ProjectMembership, self).__init__(*args, **kwargs)
2043

    
2044
    def _set_history_item(self, reason, date=None):
2045
        if isinstance(reason, basestring):
2046
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2047

    
2048
        history_item = ProjectMembershipHistory(
2049
                            serial=self.id,
2050
                            person=self.person_id,
2051
                            project=self.project_id,
2052
                            date=date or datetime.now(),
2053
                            reason=reason)
2054
        history_item.save()
2055
        serial = history_item.id
2056

    
2057
    def can_accept(self):
2058
        return self.state == self.REQUESTED
2059

    
2060
    def accept(self):
2061
        if not self.can_accept():
2062
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2063
            raise AssertionError(m)
2064

    
2065
        now = datetime.now()
2066
        self.acceptance_date = now
2067
        self._set_history_item(reason='ACCEPT', date=now)
2068
        self.state = self.ACCEPTED
2069
        self.save()
2070

    
2071
    def can_leave(self):
2072
        return self.state in self.ACCEPTED_STATES
2073

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

    
2080
        self.leave_request_date = datetime.now()
2081
        self.state = self.LEAVE_REQUESTED
2082
        self.save()
2083

    
2084
    def can_deny_leave(self):
2085
        return self.state == self.LEAVE_REQUESTED
2086

    
2087
    def leave_request_deny(self):
2088
        if not self.can_deny_leave():
2089
            m = _("%s: attempt to deny leave request in state '%s'") % (
2090
                self, self.state)
2091
            raise AssertionError(m)
2092

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

    
2097
    def can_cancel_leave(self):
2098
        return self.state == self.LEAVE_REQUESTED
2099

    
2100
    def leave_request_cancel(self):
2101
        if not self.can_cancel_leave():
2102
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2103
                self, self.state)
2104
            raise AssertionError(m)
2105

    
2106
        self.leave_request_date = None
2107
        self.state = self.ACCEPTED
2108
        self.save()
2109

    
2110
    def can_remove(self):
2111
        return self.state in self.ACCEPTED_STATES
2112

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

    
2118
        self._set_history_item(reason='REMOVE')
2119
        self.delete()
2120

    
2121
    def can_reject(self):
2122
        return self.state == self.REQUESTED
2123

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

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

    
2134
    def can_cancel(self):
2135
        return self.state == self.REQUESTED
2136

    
2137
    def cancel(self):
2138
        if not self.can_cancel():
2139
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2140
            raise AssertionError(m)
2141

    
2142
        # rejected requests don't need sync,
2143
        # because they were never effected
2144
        self._set_history_item(reason='CANCEL')
2145
        self.delete()
2146

    
2147

    
2148
class Serial(models.Model):
2149
    serial  =   models.AutoField(primary_key=True)
2150

    
2151

    
2152
class ProjectMembershipHistory(models.Model):
2153
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2154
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2155

    
2156
    person  =   models.BigIntegerField()
2157
    project =   models.BigIntegerField()
2158
    date    =   models.DateField(auto_now_add=True)
2159
    reason  =   models.IntegerField()
2160
    serial  =   models.BigIntegerField()
2161

    
2162
### SIGNALS ###
2163
################
2164

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

    
2177
def fix_superusers():
2178
    # Associate superusers with AstakosUser
2179
    admins = User.objects.filter(is_superuser=True)
2180
    for u in admins:
2181
        create_astakos_user(u)
2182

    
2183
def user_post_save(sender, instance, created, **kwargs):
2184
    if not created:
2185
        return
2186
    create_astakos_user(instance)
2187
post_save.connect(user_post_save, sender=User)
2188

    
2189
def astakosuser_post_save(sender, instance, created, **kwargs):
2190
    pass
2191

    
2192
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2193

    
2194
def resource_post_save(sender, instance, created, **kwargs):
2195
    pass
2196

    
2197
post_save.connect(resource_post_save, sender=Resource)
2198

    
2199
def renew_token(sender, instance, **kwargs):
2200
    if not instance.auth_token:
2201
        instance.renew_token()
2202
pre_save.connect(renew_token, sender=AstakosUser)
2203
pre_save.connect(renew_token, sender=Service)