Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 362ff471

History | View | Annotate | Download (74.1 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
import copy
40

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

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

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

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

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

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

    
84
logger = logging.getLogger(__name__)
85

    
86
DEFAULT_CONTENT_TYPE = None
87
_content_type = None
88

    
89

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

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

    
103
inf = float('inf')
104

    
105

    
106
def dict_merge(a, b):
107
    """
108
    http://www.xormedia.com/recursively-merge-dictionaries-in-python/
109
    """
110
    if not isinstance(b, dict):
111
        return b
112
    result = copy.deepcopy(a)
113
    for k, v in b.iteritems():
114
        if k in result and isinstance(result[k], dict):
115
                result[k] = dict_merge(result[k], v)
116
        else:
117
            result[k] = copy.deepcopy(v)
118
    return result
119

    
120

    
121
class Service(models.Model):
122
    name = models.CharField(_('Name'), max_length=255, unique=True,
123
                            db_index=True)
124
    url = models.CharField(_('Service url'), max_length=255, null=True,
125
                           help_text=_("URL the service is accessible from"))
126
    api_url = models.CharField(_('Service API url'), max_length=255, null=True)
127
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
128
                                  null=True, blank=True)
129
    auth_token_created = models.DateTimeField(_('Token creation date'),
130
                                              null=True)
131
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
132
                                              null=True)
133

    
134
    def renew_token(self, expiration_date=None):
135
        md5 = hashlib.md5()
136
        md5.update(self.name.encode('ascii', 'ignore'))
137
        md5.update(self.api_url.encode('ascii', 'ignore'))
138
        md5.update(asctime())
139

    
140
        self.auth_token = b64encode(md5.digest())
141
        self.auth_token_created = datetime.now()
142
        if expiration_date:
143
            self.auth_token_expires = expiration_date
144
        else:
145
            self.auth_token_expires = None
146

    
147
    def __str__(self):
148
        return self.name
149

    
150
    @classmethod
151
    def catalog(cls, orderfor=None):
152
        catalog = {}
153
        services = list(cls.objects.all())
154
        metadata = presentation.SERVICES
155
        metadata = dict_merge(presentation.SERVICES,
156
                              astakos_settings.SERVICES_META)
157
        for service in services:
158
            if service.name in metadata:
159
                d = {'api_url': service.api_url, 'name': service.name}
160
                metadata[service.name].update(d)
161

    
162
        def service_by_order(s):
163
            return s[1].get('order')
164

    
165
        def service_by_dashbaord_order(s):
166
            return s[1].get('dashboard').get('order')
167

    
168
        for service, info in metadata.iteritems():
169
            default_meta = presentation.service_defaults(service)
170
            base_meta = metadata.get(service, {})
171
            settings_meta = astakos_settings.SERVICES_META.get(service, {})
172
            service_meta = dict_merge(default_meta, base_meta)
173
            meta = dict_merge(service_meta, settings_meta)
174
            catalog[service] = meta
175

    
176
        order_key = service_by_order
177
        if orderfor == 'dashboard':
178
            order_key = service_by_dashbaord_order
179

    
180
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
181
                                             key=order_key))
182
        return ordered_catalog
183

    
184

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

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

    
203
    objects = ForUpdateManager()
204

    
205
    def __str__(self):
206
        return self.name
207

    
208
    def full_name(self):
209
        return str(self)
210

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

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

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

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

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

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

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

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

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

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

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

    
268

    
269
class AstakosUserManager(UserManager):
270

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

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

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

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

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

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

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

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

    
323

    
324

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

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

    
341

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

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

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

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

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

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

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

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

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

    
385
    objects = AstakosUserManager()
386

    
387
    forupdate = ForUpdateManager()
388

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

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

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

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

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

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

    
432
    def is_project_admin(self, application_id=None):
433
        return self.uuid in PROJECT_ADMINS
434

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

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

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

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

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

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

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

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

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

    
503
        self.update_uuid()
504

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
651
        modules = astakos_settings.IM_MODULES
652

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

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

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

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

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

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

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

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

    
689
        msg_extra = ''
690
        message = ''
691

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

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

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

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

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

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

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

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

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

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

    
762

    
763
class AstakosUserAuthProviderManager(models.Manager):
764

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

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

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

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

    
791

    
792
class AuthProviderPolicyProfileManager(models.Manager):
793

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

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

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

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

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

    
827

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

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

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

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

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

    
855
    objects = AuthProviderPolicyProfileManager()
856

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

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

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

    
876

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

    
895
    objects = AstakosUserAuthProviderManager()
896

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

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

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

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

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

    
921
        extra_data['info'] = info_data
922

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

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

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

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

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

    
944

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

    
972
    update_or_create = _update_or_create
973

    
974

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

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

    
984

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

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

    
994

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

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

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

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

    
1021

    
1022
class EmailChangeManager(models.Manager):
1023

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

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

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

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

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

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

    
1069

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

    
1082
    objects = EmailChangeManager()
1083

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

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

    
1092

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

    
1100

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

    
1110

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

    
1119

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

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

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

    
1151
        return user
1152

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

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

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

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

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

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

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

    
1198

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

    
1204
    objects = ForUpdateManager()
1205

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

    
1209

    
1210
### PROJECTS ###
1211
################
1212

    
1213
class ChainManager(ForUpdateManager):
1214

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

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

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

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

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

    
1241
        return d
1242

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

    
1251

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

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

    
1258
    objects = ChainManager()
1259

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

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

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

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

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

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

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

    
1309

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

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

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

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

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

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

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

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

    
1350

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

    
1355

    
1356
class ProjectApplicationManager(ForUpdateManager):
1357

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

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

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

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

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

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

    
1394

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

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

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

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

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

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

    
1442
    objects                 =   ProjectApplicationManager()
1443

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

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

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

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

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

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

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

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

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

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

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

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

    
1505
    @resource_policies.setter
1506
    def resource_policies(self, policies):
1507
        for p in policies:
1508
            service = p.get('service', None)
1509
            resource = p.get('resource', None)
1510
            uplimit = p.get('uplimit', 0)
1511
            self.add_resource_policy(service, resource, uplimit)
1512

    
1513
    def pending_modifications_incl_me(self):
1514
        q = self.chained_applications()
1515
        q = q.filter(Q(state=self.PENDING))
1516
        return q
1517

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

    
1524
    def pending_modifications(self):
1525
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1526

    
1527
    def last_pending(self):
1528
        try:
1529
            return self.pending_modifications().order_by('-id')[0]
1530
        except IndexError:
1531
            return None
1532

    
1533
    def is_modification(self):
1534
        # if self.state != self.PENDING:
1535
        #     return False
1536
        parents = self.chained_applications().filter(id__lt=self.id)
1537
        parents = parents.filter(state__in=[self.APPROVED])
1538
        return parents.count() > 0
1539

    
1540
    def chained_applications(self):
1541
        return ProjectApplication.objects.filter(chain=self.chain)
1542

    
1543
    def is_latest(self):
1544
        return self.chained_applications().order_by('-id')[0] == self
1545

    
1546
    def has_pending_modifications(self):
1547
        return bool(self.last_pending())
1548

    
1549
    def denied_modifications(self):
1550
        q = self.chained_applications()
1551
        q = q.filter(Q(state=self.DENIED))
1552
        q = q.filter(~Q(id=self.id))
1553
        return q
1554

    
1555
    def last_denied(self):
1556
        try:
1557
            return self.denied_modifications().order_by('-id')[0]
1558
        except IndexError:
1559
            return None
1560

    
1561
    def has_denied_modifications(self):
1562
        return bool(self.last_denied())
1563

    
1564
    def is_applied(self):
1565
        try:
1566
            self.project
1567
            return True
1568
        except Project.DoesNotExist:
1569
            return False
1570

    
1571
    def get_project(self):
1572
        try:
1573
            return Project.objects.get(id=self.chain)
1574
        except Project.DoesNotExist:
1575
            return None
1576

    
1577
    def project_exists(self):
1578
        return self.get_project() is not None
1579

    
1580
    def _get_project_for_update(self):
1581
        try:
1582
            objects = Project.objects
1583
            project = objects.get_for_update(id=self.chain)
1584
            return project
1585
        except Project.DoesNotExist:
1586
            return None
1587

    
1588
    def can_cancel(self):
1589
        return self.state == self.PENDING
1590

    
1591
    def cancel(self):
1592
        if not self.can_cancel():
1593
            m = _("cannot cancel: application '%s' in state '%s'") % (
1594
                    self.id, self.state)
1595
            raise AssertionError(m)
1596

    
1597
        self.state = self.CANCELLED
1598
        self.save()
1599

    
1600
    def can_dismiss(self):
1601
        return self.state == self.DENIED
1602

    
1603
    def dismiss(self):
1604
        if not self.can_dismiss():
1605
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1606
                    self.id, self.state)
1607
            raise AssertionError(m)
1608

    
1609
        self.state = self.DISMISSED
1610
        self.save()
1611

    
1612
    def can_deny(self):
1613
        return self.state == self.PENDING
1614

    
1615
    def deny(self, reason):
1616
        if not self.can_deny():
1617
            m = _("cannot deny: application '%s' in state '%s'") % (
1618
                    self.id, self.state)
1619
            raise AssertionError(m)
1620

    
1621
        self.state = self.DENIED
1622
        self.response_date = datetime.now()
1623
        self.response = reason
1624
        self.save()
1625

    
1626
    def can_approve(self):
1627
        return self.state == self.PENDING
1628

    
1629
    def approve(self, approval_user=None):
1630
        """
1631
        If approval_user then during owner membership acceptance
1632
        it is checked whether the request_user is eligible.
1633

1634
        Raises:
1635
            PermissionDenied
1636
        """
1637

    
1638
        if not transaction.is_managed():
1639
            raise AssertionError("NOPE")
1640

    
1641
        new_project_name = self.name
1642
        if not self.can_approve():
1643
            m = _("cannot approve: project '%s' in state '%s'") % (
1644
                    new_project_name, self.state)
1645
            raise AssertionError(m) # invalid argument
1646

    
1647
        now = datetime.now()
1648
        project = self._get_project_for_update()
1649

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

    
1661
        new_project = False
1662
        if project is None:
1663
            new_project = True
1664
            project = Project(id=self.chain)
1665

    
1666
        project.name = new_project_name
1667
        project.application = self
1668
        project.last_approval_date = now
1669

    
1670
        project.save()
1671

    
1672
        self.state = self.APPROVED
1673
        self.response_date = now
1674
        self.save()
1675
        return project
1676

    
1677
    @property
1678
    def member_join_policy_display(self):
1679
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1680

    
1681
    @property
1682
    def member_leave_policy_display(self):
1683
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1684

    
1685
class ProjectResourceGrant(models.Model):
1686

    
1687
    resource                =   models.ForeignKey(Resource)
1688
    project_application     =   models.ForeignKey(ProjectApplication,
1689
                                                  null=True)
1690
    project_capacity        =   intDecimalField(null=True)
1691
    member_capacity         =   intDecimalField(default=0)
1692

    
1693
    objects = ExtendedManager()
1694

    
1695
    class Meta:
1696
        unique_together = ("resource", "project_application")
1697

    
1698
    def display_member_capacity(self):
1699
        if self.member_capacity:
1700
            if self.resource.unit:
1701
                return ProjectResourceGrant.display_filesize(
1702
                    self.member_capacity)
1703
            else:
1704
                if math.isinf(self.member_capacity):
1705
                    return 'Unlimited'
1706
                else:
1707
                    return self.member_capacity
1708
        else:
1709
            return 'Unlimited'
1710

    
1711
    def __str__(self):
1712
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1713
                                        self.display_member_capacity())
1714

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

    
1739

    
1740
class ProjectManager(ForUpdateManager):
1741

    
1742
    def terminated_projects(self):
1743
        q = self.model.Q_TERMINATED
1744
        return self.filter(q)
1745

    
1746
    def not_terminated_projects(self):
1747
        q = ~self.model.Q_TERMINATED
1748
        return self.filter(q)
1749

    
1750
    def deactivated_projects(self):
1751
        q = self.model.Q_DEACTIVATED
1752
        return self.filter(q)
1753

    
1754
    def expired_projects(self):
1755
        q = (~Q(state=Project.TERMINATED) &
1756
              Q(application__end_date__lt=datetime.now()))
1757
        return self.filter(q)
1758

    
1759
    def search_by_name(self, *search_strings):
1760
        q = Q()
1761
        for s in search_strings:
1762
            q = q | Q(name__icontains=s)
1763
        return self.filter(q)
1764

    
1765

    
1766
class Project(models.Model):
1767

    
1768
    id                          =   models.OneToOneField(Chain,
1769
                                                      related_name='chained_project',
1770
                                                      db_column='id',
1771
                                                      primary_key=True)
1772

    
1773
    application                 =   models.OneToOneField(
1774
                                            ProjectApplication,
1775
                                            related_name='project')
1776
    last_approval_date          =   models.DateTimeField(null=True)
1777

    
1778
    members                     =   models.ManyToManyField(
1779
                                            AstakosUser,
1780
                                            through='ProjectMembership')
1781

    
1782
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1783
    deactivation_date           =   models.DateTimeField(null=True)
1784

    
1785
    creation_date               =   models.DateTimeField(auto_now_add=True)
1786
    name                        =   models.CharField(
1787
                                            max_length=80,
1788
                                            null=True,
1789
                                            db_index=True,
1790
                                            unique=True)
1791

    
1792
    APPROVED    = 1
1793
    SUSPENDED   = 10
1794
    TERMINATED  = 100
1795

    
1796
    state                       =   models.IntegerField(default=APPROVED,
1797
                                                        db_index=True)
1798

    
1799
    objects     =   ProjectManager()
1800

    
1801
    # Compiled queries
1802
    Q_TERMINATED  = Q(state=TERMINATED)
1803
    Q_SUSPENDED   = Q(state=SUSPENDED)
1804
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1805

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

    
1810
    __repr__ = __str__
1811

    
1812
    def __unicode__(self):
1813
        return _("<project %s '%s'>") % (self.id, self.application.name)
1814

    
1815
    STATE_DISPLAY = {
1816
        APPROVED   : 'Active',
1817
        SUSPENDED  : 'Suspended',
1818
        TERMINATED : 'Terminated'
1819
        }
1820

    
1821
    def state_display(self):
1822
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1823

    
1824
    def expiration_info(self):
1825
        return (str(self.id), self.name, self.state_display(),
1826
                str(self.application.end_date))
1827

    
1828
    def is_deactivated(self, reason=None):
1829
        if reason is not None:
1830
            return self.state == reason
1831

    
1832
        return self.state != self.APPROVED
1833

    
1834
    ### Deactivation calls
1835

    
1836
    def terminate(self):
1837
        self.deactivation_reason = 'TERMINATED'
1838
        self.deactivation_date = datetime.now()
1839
        self.state = self.TERMINATED
1840
        self.name = None
1841
        self.save()
1842

    
1843
    def suspend(self):
1844
        self.deactivation_reason = 'SUSPENDED'
1845
        self.deactivation_date = datetime.now()
1846
        self.state = self.SUSPENDED
1847
        self.save()
1848

    
1849
    def resume(self):
1850
        self.deactivation_reason = None
1851
        self.deactivation_date = None
1852
        self.state = self.APPROVED
1853
        self.save()
1854

    
1855
    ### Logical checks
1856

    
1857
    def is_inconsistent(self):
1858
        now = datetime.now()
1859
        dates = [self.creation_date,
1860
                 self.last_approval_date,
1861
                 self.deactivation_date]
1862
        return any([date > now for date in dates])
1863

    
1864
    def is_approved(self):
1865
        return self.state == self.APPROVED
1866

    
1867
    @property
1868
    def is_alive(self):
1869
        return not self.is_terminated
1870

    
1871
    @property
1872
    def is_terminated(self):
1873
        return self.is_deactivated(self.TERMINATED)
1874

    
1875
    @property
1876
    def is_suspended(self):
1877
        return self.is_deactivated(self.SUSPENDED)
1878

    
1879
    def violates_resource_grants(self):
1880
        return False
1881

    
1882
    def violates_members_limit(self, adding=0):
1883
        application = self.application
1884
        limit = application.limit_on_members_number
1885
        if limit is None:
1886
            return False
1887
        return (len(self.approved_members) + adding > limit)
1888

    
1889

    
1890
    ### Other
1891

    
1892
    def count_pending_memberships(self):
1893
        memb_set = self.projectmembership_set
1894
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1895
        return memb_count
1896

    
1897
    def members_count(self):
1898
        return self.approved_memberships.count()
1899

    
1900
    @property
1901
    def approved_memberships(self):
1902
        query = ProjectMembership.Q_ACCEPTED_STATES
1903
        return self.projectmembership_set.filter(query)
1904

    
1905
    @property
1906
    def approved_members(self):
1907
        return [m.person for m in self.approved_memberships]
1908

    
1909
    def add_member(self, user):
1910
        """
1911
        Raises:
1912
            django.exceptions.PermissionDenied
1913
            astakos.im.models.AstakosUser.DoesNotExist
1914
        """
1915
        if isinstance(user, (int, long)):
1916
            user = AstakosUser.objects.get(user=user)
1917

    
1918
        m, created = ProjectMembership.objects.get_or_create(
1919
            person=user, project=self
1920
        )
1921
        m.accept()
1922

    
1923
    def remove_member(self, user):
1924
        """
1925
        Raises:
1926
            django.exceptions.PermissionDenied
1927
            astakos.im.models.AstakosUser.DoesNotExist
1928
            astakos.im.models.ProjectMembership.DoesNotExist
1929
        """
1930
        if isinstance(user, (int, long)):
1931
            user = AstakosUser.objects.get(user=user)
1932

    
1933
        m = ProjectMembership.objects.get(person=user, project=self)
1934
        m.remove()
1935

    
1936

    
1937
CHAIN_STATE = {
1938
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1939
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1940
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1941
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1942
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1943

    
1944
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1945
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1946
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1947
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1948
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1949

    
1950
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1951
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1952
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1953
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1954
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1955

    
1956
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1957
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1958
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1959
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1960
    }
1961

    
1962

    
1963
class ProjectMembershipManager(ForUpdateManager):
1964

    
1965
    def any_accepted(self):
1966
        q = self.model.Q_ACTUALLY_ACCEPTED
1967
        return self.filter(q)
1968

    
1969
    def actually_accepted(self):
1970
        q = self.model.Q_ACTUALLY_ACCEPTED
1971
        return self.filter(q)
1972

    
1973
    def requested(self):
1974
        return self.filter(state=ProjectMembership.REQUESTED)
1975

    
1976
    def suspended(self):
1977
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1978

    
1979
class ProjectMembership(models.Model):
1980

    
1981
    person              =   models.ForeignKey(AstakosUser)
1982
    request_date        =   models.DateField(auto_now_add=True)
1983
    project             =   models.ForeignKey(Project)
1984

    
1985
    REQUESTED           =   0
1986
    ACCEPTED            =   1
1987
    LEAVE_REQUESTED     =   5
1988
    # User deactivation
1989
    USER_SUSPENDED      =   10
1990

    
1991
    REMOVED             =   200
1992

    
1993
    ASSOCIATED_STATES   =   set([REQUESTED,
1994
                                 ACCEPTED,
1995
                                 LEAVE_REQUESTED,
1996
                                 USER_SUSPENDED,
1997
                                 ])
1998

    
1999
    ACCEPTED_STATES     =   set([ACCEPTED,
2000
                                 LEAVE_REQUESTED,
2001
                                 USER_SUSPENDED,
2002
                                 ])
2003

    
2004
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2005

    
2006
    state               =   models.IntegerField(default=REQUESTED,
2007
                                                db_index=True)
2008
    acceptance_date     =   models.DateField(null=True, db_index=True)
2009
    leave_request_date  =   models.DateField(null=True)
2010

    
2011
    objects     =   ProjectMembershipManager()
2012

    
2013
    # Compiled queries
2014
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2015
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2016

    
2017
    MEMBERSHIP_STATE_DISPLAY = {
2018
        REQUESTED           : _('Requested'),
2019
        ACCEPTED            : _('Accepted'),
2020
        LEAVE_REQUESTED     : _('Leave Requested'),
2021
        USER_SUSPENDED      : _('Suspended'),
2022
        REMOVED             : _('Pending removal'),
2023
        }
2024

    
2025
    USER_FRIENDLY_STATE_DISPLAY = {
2026
        REQUESTED           : _('Join requested'),
2027
        ACCEPTED            : _('Accepted member'),
2028
        LEAVE_REQUESTED     : _('Requested to leave'),
2029
        USER_SUSPENDED      : _('Suspended member'),
2030
        REMOVED             : _('Pending removal'),
2031
        }
2032

    
2033
    def state_display(self):
2034
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2035

    
2036
    def user_friendly_state_display(self):
2037
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2038

    
2039
    class Meta:
2040
        unique_together = ("person", "project")
2041
        #index_together = [["project", "state"]]
2042

    
2043
    def __str__(self):
2044
        return uenc(_("<'%s' membership in '%s'>") % (
2045
                self.person.username, self.project))
2046

    
2047
    __repr__ = __str__
2048

    
2049
    def __init__(self, *args, **kwargs):
2050
        self.state = self.REQUESTED
2051
        super(ProjectMembership, self).__init__(*args, **kwargs)
2052

    
2053
    def _set_history_item(self, reason, date=None):
2054
        if isinstance(reason, basestring):
2055
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2056

    
2057
        history_item = ProjectMembershipHistory(
2058
                            serial=self.id,
2059
                            person=self.person_id,
2060
                            project=self.project_id,
2061
                            date=date or datetime.now(),
2062
                            reason=reason)
2063
        history_item.save()
2064
        serial = history_item.id
2065

    
2066
    def can_accept(self):
2067
        return self.state == self.REQUESTED
2068

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

    
2074
        now = datetime.now()
2075
        self.acceptance_date = now
2076
        self._set_history_item(reason='ACCEPT', date=now)
2077
        self.state = self.ACCEPTED
2078
        self.save()
2079

    
2080
    def can_leave(self):
2081
        return self.state in self.ACCEPTED_STATES
2082

    
2083
    def leave_request(self):
2084
        if not self.can_leave():
2085
            m = _("%s: attempt to request to leave in state '%s'") % (
2086
                self, self.state)
2087
            raise AssertionError(m)
2088

    
2089
        self.leave_request_date = datetime.now()
2090
        self.state = self.LEAVE_REQUESTED
2091
        self.save()
2092

    
2093
    def can_deny_leave(self):
2094
        return self.state == self.LEAVE_REQUESTED
2095

    
2096
    def leave_request_deny(self):
2097
        if not self.can_deny_leave():
2098
            m = _("%s: attempt to deny leave request in state '%s'") % (
2099
                self, self.state)
2100
            raise AssertionError(m)
2101

    
2102
        self.leave_request_date = None
2103
        self.state = self.ACCEPTED
2104
        self.save()
2105

    
2106
    def can_cancel_leave(self):
2107
        return self.state == self.LEAVE_REQUESTED
2108

    
2109
    def leave_request_cancel(self):
2110
        if not self.can_cancel_leave():
2111
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2112
                self, self.state)
2113
            raise AssertionError(m)
2114

    
2115
        self.leave_request_date = None
2116
        self.state = self.ACCEPTED
2117
        self.save()
2118

    
2119
    def can_remove(self):
2120
        return self.state in self.ACCEPTED_STATES
2121

    
2122
    def remove(self):
2123
        if not self.can_remove():
2124
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2125
            raise AssertionError(m)
2126

    
2127
        self._set_history_item(reason='REMOVE')
2128
        self.delete()
2129

    
2130
    def can_reject(self):
2131
        return self.state == self.REQUESTED
2132

    
2133
    def reject(self):
2134
        if not self.can_reject():
2135
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2136
            raise AssertionError(m)
2137

    
2138
        # rejected requests don't need sync,
2139
        # because they were never effected
2140
        self._set_history_item(reason='REJECT')
2141
        self.delete()
2142

    
2143
    def can_cancel(self):
2144
        return self.state == self.REQUESTED
2145

    
2146
    def cancel(self):
2147
        if not self.can_cancel():
2148
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2149
            raise AssertionError(m)
2150

    
2151
        # rejected requests don't need sync,
2152
        # because they were never effected
2153
        self._set_history_item(reason='CANCEL')
2154
        self.delete()
2155

    
2156

    
2157
class Serial(models.Model):
2158
    serial  =   models.AutoField(primary_key=True)
2159

    
2160

    
2161
class ProjectMembershipHistory(models.Model):
2162
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2163
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2164

    
2165
    person  =   models.BigIntegerField()
2166
    project =   models.BigIntegerField()
2167
    date    =   models.DateField(auto_now_add=True)
2168
    reason  =   models.IntegerField()
2169
    serial  =   models.BigIntegerField()
2170

    
2171
### SIGNALS ###
2172
################
2173

    
2174
def create_astakos_user(u):
2175
    try:
2176
        AstakosUser.objects.get(user_ptr=u.pk)
2177
    except AstakosUser.DoesNotExist:
2178
        extended_user = AstakosUser(user_ptr_id=u.pk)
2179
        extended_user.__dict__.update(u.__dict__)
2180
        extended_user.save()
2181
        if not extended_user.has_auth_provider('local'):
2182
            extended_user.add_auth_provider('local')
2183
    except BaseException, e:
2184
        logger.exception(e)
2185

    
2186
def fix_superusers():
2187
    # Associate superusers with AstakosUser
2188
    admins = User.objects.filter(is_superuser=True)
2189
    for u in admins:
2190
        create_astakos_user(u)
2191

    
2192
def user_post_save(sender, instance, created, **kwargs):
2193
    if not created:
2194
        return
2195
    create_astakos_user(instance)
2196
post_save.connect(user_post_save, sender=User)
2197

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

    
2201
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2202

    
2203
def resource_post_save(sender, instance, created, **kwargs):
2204
    pass
2205

    
2206
post_save.connect(resource_post_save, sender=Resource)
2207

    
2208
def renew_token(sender, instance, **kwargs):
2209
    if not instance.auth_token:
2210
        instance.renew_token()
2211
pre_save.connect(renew_token, sender=AstakosUser)
2212
pre_save.connect(renew_token, sender=Service)