Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (74.3 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)
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

    
158
        for service in services:
159
            d = {'api_url': service.api_url,
160
                 'url': service.url,
161
                 'name': service.name}
162
            if service.name in metadata:
163
                metadata[service.name].update(d)
164
            else:
165
                metadata[service.name] = d
166

    
167
        def service_by_order(s):
168
            return s[1].get('order')
169

    
170
        def service_by_dashbaord_order(s):
171
            return s[1].get('dashboard').get('order')
172

    
173
        for service, info in metadata.iteritems():
174
            default_meta = presentation.service_defaults(service)
175
            base_meta = metadata.get(service, {})
176
            settings_meta = astakos_settings.SERVICES_META.get(service, {})
177
            service_meta = dict_merge(default_meta, base_meta)
178
            meta = dict_merge(service_meta, settings_meta)
179
            catalog[service] = meta
180

    
181
        order_key = service_by_order
182
        if orderfor == 'dashboard':
183
            order_key = service_by_dashbaord_order
184

    
185
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
186
                                             key=order_key))
187
        return ordered_catalog
188

    
189

    
190
_presentation_data = {}
191
def get_presentation(resource):
192
    global _presentation_data
193
    resource_presentation = _presentation_data.get(resource, {})
194
    if not resource_presentation:
195
        resources_presentation = presentation.RESOURCES.get('resources', {})
196
        resource_presentation = resources_presentation.get(resource, {})
197
        _presentation_data[resource] = resource_presentation
198
    return resource_presentation
199

    
200
class Resource(models.Model):
201
    name = models.CharField(_('Name'), max_length=255, unique=True)
202
    desc = models.TextField(_('Description'), null=True)
203
    service = models.ForeignKey(Service)
204
    unit = models.CharField(_('Unit'), null=True, max_length=255)
205
    uplimit = intDecimalField(default=0)
206
    allow_in_projects = models.BooleanField(default=True)
207

    
208
    objects = ForUpdateManager()
209

    
210
    def __str__(self):
211
        return self.name
212

    
213
    def full_name(self):
214
        return str(self)
215

    
216
    def get_info(self):
217
        return {'service': str(self.service),
218
                'description': self.desc,
219
                'unit': self.unit,
220
                'allow_in_projects': self.allow_in_projects,
221
                }
222

    
223
    @property
224
    def group(self):
225
        default = self.name
226
        return get_presentation(str(self)).get('group', default)
227

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

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

    
238
    @property
239
    def is_abbreviation(self):
240
        return get_presentation(str(self)).get('is_abbreviation', False)
241

    
242
    @property
243
    def report_desc(self):
244
        default = "%s resource" % self.name
245
        return get_presentation(str(self)).get('report_desc', default)
246

    
247
    @property
248
    def placeholder(self):
249
        return get_presentation(str(self)).get('placeholder', self.unit)
250

    
251
    @property
252
    def verbose_name(self):
253
        return get_presentation(str(self)).get('verbose_name', self.name)
254

    
255
    @property
256
    def display_name(self):
257
        name = self.verbose_name
258
        if self.is_abbreviation:
259
            name = name.upper()
260
        return name
261

    
262
    @property
263
    def pluralized_display_name(self):
264
        if not self.unit:
265
            return '%ss' % self.display_name
266
        return self.display_name
267

    
268
def get_resource_names():
269
    _RESOURCE_NAMES = []
270
    resources = Resource.objects.select_related('service').all()
271
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
272
    return _RESOURCE_NAMES
273

    
274

    
275
class AstakosUserManager(UserManager):
276

    
277
    def get_auth_provider_user(self, provider, **kwargs):
278
        """
279
        Retrieve AstakosUser instance associated with the specified third party
280
        id.
281
        """
282
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
283
                          kwargs.iteritems()))
284
        return self.get(auth_providers__module=provider, **kwargs)
285

    
286
    def get_by_email(self, email):
287
        return self.get(email=email)
288

    
289
    def get_by_identifier(self, email_or_username, **kwargs):
290
        try:
291
            return self.get(email__iexact=email_or_username, **kwargs)
292
        except AstakosUser.DoesNotExist:
293
            return self.get(username__iexact=email_or_username, **kwargs)
294

    
295
    def user_exists(self, email_or_username, **kwargs):
296
        qemail = Q(email__iexact=email_or_username)
297
        qusername = Q(username__iexact=email_or_username)
298
        qextra = Q(**kwargs)
299
        return self.filter((qemail | qusername) & qextra).exists()
300

    
301
    def verified_user_exists(self, email_or_username):
302
        return self.user_exists(email_or_username, email_verified=True)
303

    
304
    def verified(self):
305
        return self.filter(email_verified=True)
306

    
307
    def uuid_catalog(self, l=None):
308
        """
309
        Returns a uuid to username mapping for the uuids appearing in l.
310
        If l is None returns the mapping for all existing users.
311
        """
312
        q = self.filter(uuid__in=l) if l != None else self
313
        return dict(q.values_list('uuid', 'username'))
314

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

    
329

    
330

    
331
class AstakosUser(User):
332
    """
333
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
334
    """
335
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
336
                                   null=True)
337

    
338
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
339
    #                    AstakosUserProvider model.
340
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
341
                                null=True)
342
    # ex. screen_name for twitter, eppn for shibboleth
343
    third_party_identifier = models.CharField(_('Third-party identifier'),
344
                                              max_length=255, null=True,
345
                                              blank=True)
346

    
347

    
348
    #for invitations
349
    user_level = DEFAULT_USER_LEVEL
350
    level = models.IntegerField(_('Inviter level'), default=user_level)
351
    invitations = models.IntegerField(
352
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
353

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

    
368
    updated = models.DateTimeField(_('Update date'))
369
    is_verified = models.BooleanField(_('Is verified?'), default=False)
370

    
371
    email_verified = models.BooleanField(_('Email verified?'), default=False)
372

    
373
    has_credits = models.BooleanField(_('Has credits?'), default=False)
374
    has_signed_terms = models.BooleanField(
375
        _('I agree with the terms'), default=False)
376
    date_signed_terms = models.DateTimeField(
377
        _('Signed terms date'), null=True, blank=True)
378

    
379
    activation_sent = models.DateTimeField(
380
        _('Activation sent data'), null=True, blank=True)
381

    
382
    policy = models.ManyToManyField(
383
        Resource, null=True, through='AstakosUserQuota')
384

    
385
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
386

    
387
    __has_signed_terms = False
388
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
389
                                           default=False, db_index=True)
390

    
391
    objects = AstakosUserManager()
392

    
393
    forupdate = ForUpdateManager()
394

    
395
    def __init__(self, *args, **kwargs):
396
        super(AstakosUser, self).__init__(*args, **kwargs)
397
        self.__has_signed_terms = self.has_signed_terms
398
        if not self.id:
399
            self.is_active = False
400

    
401
    @property
402
    def realname(self):
403
        return '%s %s' % (self.first_name, self.last_name)
404

    
405
    @property
406
    def log_display(self):
407
        """
408
        Should be used in all logger.* calls that refer to a user so that
409
        user display is consistent across log entries.
410
        """
411
        return '%s::%s' % (self.uuid, self.email)
412

    
413
    @realname.setter
414
    def realname(self, value):
415
        parts = value.split(' ')
416
        if len(parts) == 2:
417
            self.first_name = parts[0]
418
            self.last_name = parts[1]
419
        else:
420
            self.last_name = parts[0]
421

    
422
    def add_permission(self, pname):
423
        if self.has_perm(pname):
424
            return
425
        p, created = Permission.objects.get_or_create(
426
                                    codename=pname,
427
                                    name=pname.capitalize(),
428
                                    content_type=get_content_type())
429
        self.user_permissions.add(p)
430

    
431
    def remove_permission(self, pname):
432
        if self.has_perm(pname):
433
            return
434
        p = Permission.objects.get(codename=pname,
435
                                   content_type=get_content_type())
436
        self.user_permissions.remove(p)
437

    
438
    def is_project_admin(self, application_id=None):
439
        return self.uuid in astakos_settings.PROJECT_ADMINS
440

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

    
448
    @property
449
    def policies(self):
450
        return self.astakosuserquota_set.select_related().all()
451

    
452
    @policies.setter
453
    def policies(self, policies):
454
        for p in policies:
455
            p.setdefault('resource', '')
456
            p.setdefault('capacity', 0)
457
            p.setdefault('update', True)
458
            self.add_resource_policy(**p)
459

    
460
    def add_resource_policy(
461
            self, resource, capacity,
462
            update=True):
463
        """Raises ObjectDoesNotExist, IntegrityError"""
464
        resource = Resource.objects.get(name=resource)
465
        if update:
466
            AstakosUserQuota.objects.update_or_create(
467
                user=self, resource=resource, defaults={
468
                    'capacity':capacity,
469
                    })
470
        else:
471
            q = self.astakosuserquota_set
472
            q.create(
473
                resource=resource, capacity=capacity,
474
                )
475

    
476
    def get_resource_policy(self, resource):
477
        resource = Resource.objects.get(name=resource)
478
        default_capacity = resource.uplimit
479
        try:
480
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
481
            return policy, default_capacity
482
        except AstakosUserQuota.DoesNotExist:
483
            return None, default_capacity
484

    
485
    def remove_resource_policy(self, service, resource):
486
        """Raises ObjectDoesNotExist, IntegrityError"""
487
        resource = Resource.objects.get(name=resource)
488
        q = self.policies.get(resource=resource).delete()
489

    
490
    def update_uuid(self):
491
        while not self.uuid:
492
            uuid_val =  str(uuid.uuid4())
493
            try:
494
                AstakosUser.objects.get(uuid=uuid_val)
495
            except AstakosUser.DoesNotExist, e:
496
                self.uuid = uuid_val
497
        return self.uuid
498

    
499
    def save(self, update_timestamps=True, **kwargs):
500
        if update_timestamps:
501
            if not self.id:
502
                self.date_joined = datetime.now()
503
            self.updated = datetime.now()
504

    
505
        # update date_signed_terms if necessary
506
        if self.__has_signed_terms != self.has_signed_terms:
507
            self.date_signed_terms = datetime.now()
508

    
509
        self.update_uuid()
510

    
511
        if self.username != self.email.lower():
512
            # set username
513
            self.username = self.email.lower()
514

    
515
        super(AstakosUser, self).save(**kwargs)
516

    
517
    def renew_token(self, flush_sessions=False, current_key=None):
518
        md5 = hashlib.md5()
519
        md5.update(settings.SECRET_KEY)
520
        md5.update(self.username)
521
        md5.update(self.realname.encode('ascii', 'ignore'))
522
        md5.update(asctime())
523

    
524
        self.auth_token = b64encode(md5.digest())
525
        self.auth_token_created = datetime.now()
526
        self.auth_token_expires = self.auth_token_created + \
527
                                  timedelta(hours=AUTH_TOKEN_DURATION)
528
        if flush_sessions:
529
            self.flush_sessions(current_key)
530
        msg = 'Token renewed for %s' % self.email
531
        logger.log(LOGGING_LEVEL, msg)
532

    
533
    def flush_sessions(self, current_key=None):
534
        q = self.sessions
535
        if current_key:
536
            q = q.exclude(session_key=current_key)
537

    
538
        keys = q.values_list('session_key', flat=True)
539
        if keys:
540
            msg = 'Flushing sessions: %s' % ','.join(keys)
541
            logger.log(LOGGING_LEVEL, msg, [])
542
        engine = import_module(settings.SESSION_ENGINE)
543
        for k in keys:
544
            s = engine.SessionStore(k)
545
            s.flush()
546

    
547
    def __unicode__(self):
548
        return '%s (%s)' % (self.realname, self.email)
549

    
550
    def conflicting_email(self):
551
        q = AstakosUser.objects.exclude(username=self.username)
552
        q = q.filter(email__iexact=self.email)
553
        if q.count() != 0:
554
            return True
555
        return False
556

    
557
    def email_change_is_pending(self):
558
        return self.emailchanges.count() > 0
559

    
560
    @property
561
    def signed_terms(self):
562
        term = get_latest_terms()
563
        if not term:
564
            return True
565
        if not self.has_signed_terms:
566
            return False
567
        if not self.date_signed_terms:
568
            return False
569
        if self.date_signed_terms < term.date:
570
            self.has_signed_terms = False
571
            self.date_signed_terms = None
572
            self.save()
573
            return False
574
        return True
575

    
576
    def set_invitations_level(self):
577
        """
578
        Update user invitation level
579
        """
580
        level = self.invitation.inviter.level + 1
581
        self.level = level
582
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
583

    
584
    def can_change_password(self):
585
        return self.has_auth_provider('local', auth_backend='astakos')
586

    
587
    def can_change_email(self):
588
        if not self.has_auth_provider('local'):
589
            return True
590

    
591
        local = self.get_auth_provider('local')._instance
592
        return local.auth_backend == 'astakos'
593

    
594
    # Auth providers related methods
595
    def get_auth_provider(self, module=None, identifier=None, **filters):
596
        if not module:
597
            return self.auth_providers.active()[0].settings
598

    
599
        params = {'module': module}
600
        if identifier:
601
            params['identifier'] = identifier
602
        params.update(filters)
603
        return self.auth_providers.active().get(**params).settings
604

    
605
    def has_auth_provider(self, provider, **kwargs):
606
        return bool(self.auth_providers.active().filter(module=provider,
607
                                                        **kwargs).count())
608

    
609
    def get_required_providers(self, **kwargs):
610
        return auth.REQUIRED_PROVIDERS.keys()
611

    
612
    def missing_required_providers(self):
613
        required = self.get_required_providers()
614
        missing = []
615
        for provider in required:
616
            if not self.has_auth_provider(provider):
617
                missing.append(auth.get_provider(provider, self))
618
        return missing
619

    
620
    def get_available_auth_providers(self, **filters):
621
        """
622
        Returns a list of providers available for add by the user.
623
        """
624
        modules = astakos_settings.IM_MODULES
625
        providers = []
626
        for p in modules:
627
            providers.append(auth.get_provider(p, self))
628
        available = []
629

    
630
        for p in providers:
631
            if p.get_add_policy:
632
                available.append(p)
633
        return available
634

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

    
643
    def get_enabled_auth_providers(self, **filters):
644
        providers = self.get_auth_providers(**filters)
645
        enabled = []
646
        for p in providers:
647
            if p.get_login_policy:
648
                enabled.append(p)
649
        return enabled
650

    
651
    def get_auth_providers(self, **filters):
652
        providers = []
653
        for provider in self.auth_providers.active(**filters):
654
            if provider.settings.module_enabled:
655
                providers.append(provider.settings)
656

    
657
        modules = astakos_settings.IM_MODULES
658

    
659
        def key(p):
660
            if not p.module in modules:
661
                return 100
662
            return modules.index(p.module)
663

    
664
        providers = sorted(providers, key=key)
665
        return providers
666

    
667
    # URL methods
668
    @property
669
    def auth_providers_display(self):
670
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
671
                         self.get_enabled_auth_providers()])
672

    
673
    def add_auth_provider(self, module='local', identifier=None, **params):
674
        provider = auth.get_provider(module, self, identifier, **params)
675
        provider.add_to_user()
676

    
677
    def get_resend_activation_url(self):
678
        return reverse('send_activation', kwargs={'user_id': self.pk})
679

    
680
    def get_activation_url(self, nxt=False):
681
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
682
                                 quote(self.auth_token))
683
        if nxt:
684
            url += "&next=%s" % quote(nxt)
685
        return url
686

    
687
    def get_password_reset_url(self, token_generator=default_token_generator):
688
        return reverse('django.contrib.auth.views.password_reset_confirm',
689
                          kwargs={'uidb36':int_to_base36(self.id),
690
                                  'token':token_generator.make_token(self)})
691

    
692
    def get_inactive_message(self, provider_module, identifier=None):
693
        provider = self.get_auth_provider(provider_module, identifier)
694

    
695
        msg_extra = ''
696
        message = ''
697

    
698
        msg_inactive = provider.get_account_inactive_msg
699
        msg_pending = provider.get_pending_activation_msg
700
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
701
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
702
        msg_pending_mod = provider.get_pending_moderation_msg
703
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
704

    
705
        if self.activation_sent:
706
            if self.email_verified:
707
                message = msg_inactive
708
            else:
709
                message = msg_pending
710
                url = self.get_resend_activation_url()
711
                msg_extra = msg_pending_help + \
712
                            u' ' + \
713
                            '<a href="%s">%s?</a>' % (url, msg_resend)
714
        else:
715
            if astakos_settings.MODERATION_ENABLED:
716
                message = msg_pending_mod
717
            else:
718
                message = msg_pending
719
                url = self.get_resend_activation_url()
720
                msg_extra = '<a href="%s">%s?</a>' % (url, \
721
                                msg_resend)
722

    
723
        return mark_safe(message + u' '+ msg_extra)
724

    
725
    def owns_application(self, application):
726
        return application.owner == self
727

    
728
    def owns_project(self, project):
729
        return project.application.owner == self
730

    
731
    def is_associated(self, project):
732
        try:
733
            m = ProjectMembership.objects.get(person=self, project=project)
734
            return m.state in ProjectMembership.ASSOCIATED_STATES
735
        except ProjectMembership.DoesNotExist:
736
            return False
737

    
738
    def get_membership(self, project):
739
        try:
740
            return ProjectMembership.objects.get(
741
                project=project,
742
                person=self)
743
        except ProjectMembership.DoesNotExist:
744
            return None
745

    
746
    def membership_display(self, project):
747
        m = self.get_membership(project)
748
        if m is None:
749
            return _('Not a member')
750
        else:
751
            return m.user_friendly_state_display()
752

    
753
    def non_owner_can_view(self, maybe_project):
754
        if self.is_project_admin():
755
            return True
756
        if maybe_project is None:
757
            return False
758
        project = maybe_project
759
        if self.is_associated(project):
760
            return True
761
        if project.is_deactivated():
762
            return False
763
        return True
764

    
765
    def settings(self):
766
        return UserSetting.objects.filter(user=self)
767

    
768

    
769
class AstakosUserAuthProviderManager(models.Manager):
770

    
771
    def active(self, **filters):
772
        return self.filter(active=True, **filters)
773

    
774
    def remove_unverified_providers(self, provider, **filters):
775
        try:
776
            existing = self.filter(module=provider, user__email_verified=False,
777
                                   **filters)
778
            for p in existing:
779
                p.user.delete()
780
        except:
781
            pass
782

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

    
790
    def verified(self, provider, **filters):
791
        try:
792
            return self.get(module=provider, user__email_verified=True,
793
                            **filters).settings
794
        except AstakosUserAuthProvider.DoesNotExist:
795
            return None
796

    
797

    
798
class AuthProviderPolicyProfileManager(models.Manager):
799

    
800
    def active(self):
801
        return self.filter(active=True)
802

    
803
    def for_user(self, user, provider):
804
        policies = {}
805
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
806
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
807
        exclusive_q = exclusive_q1 | exclusive_q2
808

    
809
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
810
            policies.update(profile.policies)
811

    
812
        user_groups = user.groups.all().values('pk')
813
        for profile in self.active().filter(groups__in=user_groups).filter(
814
                exclusive_q):
815
            policies.update(profile.policies)
816
        return policies
817

    
818
    def add_policy(self, name, provider, group_or_user, exclusive=False,
819
                   **policies):
820
        is_group = isinstance(group_or_user, Group)
821
        profile, created = self.get_or_create(name=name, provider=provider,
822
                                              is_exclusive=exclusive)
823
        profile.is_exclusive = exclusive
824
        profile.save()
825
        if is_group:
826
            profile.groups.add(group_or_user)
827
        else:
828
            profile.users.add(group_or_user)
829
        profile.set_policies(policies)
830
        profile.save()
831
        return profile
832

    
833

    
834
class AuthProviderPolicyProfile(models.Model):
835
    name = models.CharField(_('Name'), max_length=255, blank=False,
836
                            null=False, db_index=True)
837
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
838
                                null=False)
839

    
840
    # apply policies to all providers excluding the one set in provider field
841
    is_exclusive = models.BooleanField(default=False)
842

    
843
    policy_add = models.NullBooleanField(null=True, default=None)
844
    policy_remove = models.NullBooleanField(null=True, default=None)
845
    policy_create = models.NullBooleanField(null=True, default=None)
846
    policy_login = models.NullBooleanField(null=True, default=None)
847
    policy_limit = models.IntegerField(null=True, default=None)
848
    policy_required = models.NullBooleanField(null=True, default=None)
849
    policy_automoderate = models.NullBooleanField(null=True, default=None)
850
    policy_switch = models.NullBooleanField(null=True, default=None)
851

    
852
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
853
                     'automoderate')
854

    
855
    priority = models.IntegerField(null=False, default=1)
856
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
857
    users = models.ManyToManyField(AstakosUser,
858
                                   related_name='authpolicy_profiles')
859
    active = models.BooleanField(default=True)
860

    
861
    objects = AuthProviderPolicyProfileManager()
862

    
863
    class Meta:
864
        ordering = ['priority']
865

    
866
    @property
867
    def policies(self):
868
        policies = {}
869
        for pkey in self.POLICY_FIELDS:
870
            value = getattr(self, 'policy_%s' % pkey, None)
871
            if value is None:
872
                continue
873
            policies[pkey] = value
874
        return policies
875

    
876
    def set_policies(self, policies_dict):
877
        for key, value in policies_dict.iteritems():
878
            if key in self.POLICY_FIELDS:
879
                setattr(self, 'policy_%s' % key, value)
880
        return self.policies
881

    
882

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

    
901
    objects = AstakosUserAuthProviderManager()
902

    
903
    class Meta:
904
        unique_together = (('identifier', 'module', 'user'), )
905
        ordering = ('module', 'created')
906

    
907
    def __init__(self, *args, **kwargs):
908
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
909
        try:
910
            self.info = json.loads(self.info_data)
911
            if not self.info:
912
                self.info = {}
913
        except Exception, e:
914
            self.info = {}
915

    
916
        for key,value in self.info.iteritems():
917
            setattr(self, 'info_%s' % key, value)
918

    
919
    @property
920
    def settings(self):
921
        extra_data = {}
922

    
923
        info_data = {}
924
        if self.info_data:
925
            info_data = json.loads(self.info_data)
926

    
927
        extra_data['info'] = info_data
928

    
929
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
930
            extra_data[key] = getattr(self, key)
931

    
932
        extra_data['instance'] = self
933
        return auth.get_provider(self.module, self.user,
934
                                           self.identifier, **extra_data)
935

    
936
    def __repr__(self):
937
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
938

    
939
    def __unicode__(self):
940
        if self.identifier:
941
            return "%s:%s" % (self.module, self.identifier)
942
        if self.auth_backend:
943
            return "%s:%s" % (self.module, self.auth_backend)
944
        return self.module
945

    
946
    def save(self, *args, **kwargs):
947
        self.info_data = json.dumps(self.info)
948
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
949

    
950

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

    
978
    update_or_create = _update_or_create
979

    
980

    
981
class AstakosUserQuota(models.Model):
982
    objects = ExtendedManager()
983
    capacity = intDecimalField()
984
    resource = models.ForeignKey(Resource)
985
    user = models.ForeignKey(AstakosUser)
986

    
987
    class Meta:
988
        unique_together = ("resource", "user")
989

    
990

    
991
class ApprovalTerms(models.Model):
992
    """
993
    Model for approval terms
994
    """
995

    
996
    date = models.DateTimeField(
997
        _('Issue date'), db_index=True, auto_now_add=True)
998
    location = models.CharField(_('Terms location'), max_length=255)
999

    
1000

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

    
1014
    def __init__(self, *args, **kwargs):
1015
        super(Invitation, self).__init__(*args, **kwargs)
1016
        if not self.id:
1017
            self.code = _generate_invitation_code()
1018

    
1019
    def consume(self):
1020
        self.is_consumed = True
1021
        self.consumed = datetime.now()
1022
        self.save()
1023

    
1024
    def __unicode__(self):
1025
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1026

    
1027

    
1028
class EmailChangeManager(models.Manager):
1029

    
1030
    @transaction.commit_on_success
1031
    def change_email(self, activation_key):
1032
        """
1033
        Validate an activation key and change the corresponding
1034
        ``User`` if valid.
1035

1036
        If the key is valid and has not expired, return the ``User``
1037
        after activating.
1038

1039
        If the key is not valid or has expired, return ``None``.
1040

1041
        If the key is valid but the ``User`` is already active,
1042
        return ``None``.
1043

1044
        After successful email change the activation record is deleted.
1045

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

    
1075

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

    
1088
    objects = EmailChangeManager()
1089

    
1090
    def get_url(self):
1091
        return reverse('email_change_confirm',
1092
                      kwargs={'activation_key': self.activation_key})
1093

    
1094
    def activation_key_expired(self):
1095
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1096
        return self.requested_at + expiration_date < datetime.now()
1097

    
1098

    
1099
class AdditionalMail(models.Model):
1100
    """
1101
    Model for registring invitations
1102
    """
1103
    owner = models.ForeignKey(AstakosUser)
1104
    email = models.EmailField()
1105

    
1106

    
1107
def _generate_invitation_code():
1108
    while True:
1109
        code = randint(1, 2L ** 63 - 1)
1110
        try:
1111
            Invitation.objects.get(code=code)
1112
            # An invitation with this code already exists, try again
1113
        except Invitation.DoesNotExist:
1114
            return code
1115

    
1116

    
1117
def get_latest_terms():
1118
    try:
1119
        term = ApprovalTerms.objects.order_by('-id')[0]
1120
        return term
1121
    except IndexError:
1122
        pass
1123
    return None
1124

    
1125

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

    
1145
    class Meta:
1146
        unique_together = ("provider", "third_party_identifier")
1147

    
1148
    def get_user_instance(self):
1149
        d = self.__dict__
1150
        d.pop('_state', None)
1151
        d.pop('id', None)
1152
        d.pop('token', None)
1153
        d.pop('created', None)
1154
        d.pop('info', None)
1155
        user = AstakosUser(**d)
1156

    
1157
        return user
1158

    
1159
    @property
1160
    def realname(self):
1161
        return '%s %s' %(self.first_name, self.last_name)
1162

    
1163
    @realname.setter
1164
    def realname(self, value):
1165
        parts = value.split(' ')
1166
        if len(parts) == 2:
1167
            self.first_name = parts[0]
1168
            self.last_name = parts[1]
1169
        else:
1170
            self.last_name = parts[0]
1171

    
1172
    def save(self, **kwargs):
1173
        if not self.id:
1174
            # set username
1175
            while not self.username:
1176
                username =  uuid.uuid4().hex[:30]
1177
                try:
1178
                    AstakosUser.objects.get(username = username)
1179
                except AstakosUser.DoesNotExist, e:
1180
                    self.username = username
1181
        super(PendingThirdPartyUser, self).save(**kwargs)
1182

    
1183
    def generate_token(self):
1184
        self.password = self.third_party_identifier
1185
        self.last_login = datetime.now()
1186
        self.token = default_token_generator.make_token(self)
1187

    
1188
    def existing_user(self):
1189
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1190
                                         auth_providers__identifier=self.third_party_identifier)
1191

    
1192
    def get_provider(self, user):
1193
        params = {
1194
            'info_data': self.info,
1195
            'affiliation': self.affiliation
1196
        }
1197
        return auth.get_provider(self.provider, user,
1198
                                 self.third_party_identifier, **params)
1199

    
1200
class SessionCatalog(models.Model):
1201
    session_key = models.CharField(_('session key'), max_length=40)
1202
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1203

    
1204

    
1205
class UserSetting(models.Model):
1206
    user = models.ForeignKey(AstakosUser)
1207
    setting = models.CharField(max_length=255)
1208
    value = models.IntegerField()
1209

    
1210
    objects = ForUpdateManager()
1211

    
1212
    class Meta:
1213
        unique_together = ("user", "setting")
1214

    
1215

    
1216
### PROJECTS ###
1217
################
1218

    
1219
class ChainManager(ForUpdateManager):
1220

    
1221
    def search_by_name(self, *search_strings):
1222
        projects = Project.objects.search_by_name(*search_strings)
1223
        chains = [p.id for p in projects]
1224
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1225
        apps = (app for app in apps if app.is_latest())
1226
        app_chains = [app.chain for app in apps if app.chain not in chains]
1227
        return chains + app_chains
1228

    
1229
    def all_full_state(self):
1230
        chains = self.all()
1231
        cids = [c.chain for c in chains]
1232
        projects = Project.objects.select_related('application').in_bulk(cids)
1233

    
1234
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1235
        chain_latest = dict(objs.values_list('chain', 'latest'))
1236

    
1237
        objs = ProjectApplication.objects.select_related('applicant')
1238
        apps = objs.in_bulk(chain_latest.values())
1239

    
1240
        d = {}
1241
        for chain in chains:
1242
            pk = chain.pk
1243
            project = projects.get(pk, None)
1244
            app = apps[chain_latest[pk]]
1245
            d[chain.pk] = chain.get_state(project, app)
1246

    
1247
        return d
1248

    
1249
    def of_project(self, project):
1250
        if project is None:
1251
            return None
1252
        try:
1253
            return self.get(chain=project.id)
1254
        except Chain.DoesNotExist:
1255
            raise AssertionError('project with no chain')
1256

    
1257

    
1258
class Chain(models.Model):
1259
    chain  =   models.AutoField(primary_key=True)
1260

    
1261
    def __str__(self):
1262
        return "%s" % (self.chain,)
1263

    
1264
    objects = ChainManager()
1265

    
1266
    PENDING            = 0
1267
    DENIED             = 3
1268
    DISMISSED          = 4
1269
    CANCELLED          = 5
1270

    
1271
    APPROVED           = 10
1272
    APPROVED_PENDING   = 11
1273
    SUSPENDED          = 12
1274
    SUSPENDED_PENDING  = 13
1275
    TERMINATED         = 14
1276
    TERMINATED_PENDING = 15
1277

    
1278
    PENDING_STATES = [PENDING,
1279
                      APPROVED_PENDING,
1280
                      SUSPENDED_PENDING,
1281
                      TERMINATED_PENDING,
1282
                      ]
1283

    
1284
    MODIFICATION_STATES = [APPROVED_PENDING,
1285
                           SUSPENDED_PENDING,
1286
                           TERMINATED_PENDING,
1287
                           ]
1288

    
1289
    RELEVANT_STATES = [PENDING,
1290
                       DENIED,
1291
                       APPROVED,
1292
                       APPROVED_PENDING,
1293
                       SUSPENDED,
1294
                       SUSPENDED_PENDING,
1295
                       TERMINATED_PENDING,
1296
                       ]
1297

    
1298
    SKIP_STATES = [DISMISSED,
1299
                   CANCELLED,
1300
                   TERMINATED]
1301

    
1302
    STATE_DISPLAY = {
1303
        PENDING            : _("Pending"),
1304
        DENIED             : _("Denied"),
1305
        DISMISSED          : _("Dismissed"),
1306
        CANCELLED          : _("Cancelled"),
1307
        APPROVED           : _("Active"),
1308
        APPROVED_PENDING   : _("Active - Pending"),
1309
        SUSPENDED          : _("Suspended"),
1310
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1311
        TERMINATED         : _("Terminated"),
1312
        TERMINATED_PENDING : _("Terminated - Pending"),
1313
        }
1314

    
1315

    
1316
    @classmethod
1317
    def _chain_state(cls, project_state, app_state):
1318
        s = CHAIN_STATE.get((project_state, app_state), None)
1319
        if s is None:
1320
            raise AssertionError('inconsistent chain state')
1321
        return s
1322

    
1323
    @classmethod
1324
    def chain_state(cls, project, app):
1325
        p_state = project.state if project else None
1326
        return cls._chain_state(p_state, app.state)
1327

    
1328
    @classmethod
1329
    def state_display(cls, s):
1330
        if s is None:
1331
            return _("Unknown")
1332
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1333

    
1334
    def last_application(self):
1335
        return self.chained_apps.order_by('-id')[0]
1336

    
1337
    def get_project(self):
1338
        try:
1339
            return self.chained_project
1340
        except Project.DoesNotExist:
1341
            return None
1342

    
1343
    def get_elements(self):
1344
        project = self.get_project()
1345
        app = self.last_application()
1346
        return project, app
1347

    
1348
    def get_state(self, project, app):
1349
        s = self.chain_state(project, app)
1350
        return s, project, app
1351

    
1352
    def full_state(self):
1353
        project, app = self.get_elements()
1354
        return self.get_state(project, app)
1355

    
1356

    
1357
def new_chain():
1358
    c = Chain.objects.create()
1359
    return c
1360

    
1361

    
1362
class ProjectApplicationManager(ForUpdateManager):
1363

    
1364
    def user_visible_projects(self, *filters, **kw_filters):
1365
        model = self.model
1366
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1367

    
1368
    def user_visible_by_chain(self, flt):
1369
        model = self.model
1370
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1371
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1372
        by_chain = dict(pending.annotate(models.Max('id')))
1373
        by_chain.update(approved.annotate(models.Max('id')))
1374
        return self.filter(flt, id__in=by_chain.values())
1375

    
1376
    def user_accessible_projects(self, user):
1377
        """
1378
        Return projects accessed by specified user.
1379
        """
1380
        if user.is_project_admin():
1381
            participates_filters = Q()
1382
        else:
1383
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1384
                                   Q(project__projectmembership__person=user)
1385

    
1386
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1387

    
1388
    def search_by_name(self, *search_strings):
1389
        q = Q()
1390
        for s in search_strings:
1391
            q = q | Q(name__icontains=s)
1392
        return self.filter(q)
1393

    
1394
    def latest_of_chain(self, chain_id):
1395
        try:
1396
            return self.filter(chain=chain_id).order_by('-id')[0]
1397
        except IndexError:
1398
            return None
1399

    
1400

    
1401
class ProjectApplication(models.Model):
1402
    applicant               =   models.ForeignKey(
1403
                                    AstakosUser,
1404
                                    related_name='projects_applied',
1405
                                    db_index=True)
1406

    
1407
    PENDING     =    0
1408
    APPROVED    =    1
1409
    REPLACED    =    2
1410
    DENIED      =    3
1411
    DISMISSED   =    4
1412
    CANCELLED   =    5
1413

    
1414
    state                   =   models.IntegerField(default=PENDING,
1415
                                                    db_index=True)
1416

    
1417
    owner                   =   models.ForeignKey(
1418
                                    AstakosUser,
1419
                                    related_name='projects_owned',
1420
                                    db_index=True)
1421

    
1422
    chain                   =   models.ForeignKey(Chain,
1423
                                                  related_name='chained_apps',
1424
                                                  db_column='chain')
1425
    precursor_application   =   models.ForeignKey('ProjectApplication',
1426
                                                  null=True,
1427
                                                  blank=True)
1428

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

    
1448
    objects                 =   ProjectApplicationManager()
1449

    
1450
    # Compiled queries
1451
    Q_PENDING  = Q(state=PENDING)
1452
    Q_APPROVED = Q(state=APPROVED)
1453
    Q_DENIED   = Q(state=DENIED)
1454

    
1455
    class Meta:
1456
        unique_together = ("chain", "id")
1457

    
1458
    def __unicode__(self):
1459
        return "%s applied by %s" % (self.name, self.applicant)
1460

    
1461
    # TODO: Move to a more suitable place
1462
    APPLICATION_STATE_DISPLAY = {
1463
        PENDING  : _('Pending review'),
1464
        APPROVED : _('Approved'),
1465
        REPLACED : _('Replaced'),
1466
        DENIED   : _('Denied'),
1467
        DISMISSED: _('Dismissed'),
1468
        CANCELLED: _('Cancelled')
1469
    }
1470

    
1471
    @property
1472
    def log_display(self):
1473
        return "application %s (%s) for project %s" % (
1474
            self.id, self.name, self.chain)
1475

    
1476
    def get_project(self):
1477
        try:
1478
            project = Project.objects.get(id=self.chain, state=Project.APPROVED)
1479
            return Project
1480
        except Project.DoesNotExist, e:
1481
            return None
1482

    
1483
    def state_display(self):
1484
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1485

    
1486
    def project_state_display(self):
1487
        try:
1488
            project = self.project
1489
            return project.state_display()
1490
        except Project.DoesNotExist:
1491
            return self.state_display()
1492

    
1493
    def add_resource_policy(self, service, resource, uplimit):
1494
        """Raises ObjectDoesNotExist, IntegrityError"""
1495
        q = self.projectresourcegrant_set
1496
        resource = Resource.objects.get(name=resource)
1497
        q.create(resource=resource, member_capacity=uplimit)
1498

    
1499
    def members_count(self):
1500
        return self.project.approved_memberships.count()
1501

    
1502
    @property
1503
    def grants(self):
1504
        return self.projectresourcegrant_set.values('member_capacity',
1505
                                                    'resource__name')
1506

    
1507
    @property
1508
    def resource_policies(self):
1509
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1510

    
1511
    @resource_policies.setter
1512
    def resource_policies(self, policies):
1513
        for p in policies:
1514
            service = p.get('service', None)
1515
            resource = p.get('resource', None)
1516
            uplimit = p.get('uplimit', 0)
1517
            self.add_resource_policy(service, resource, uplimit)
1518

    
1519
    def pending_modifications_incl_me(self):
1520
        q = self.chained_applications()
1521
        q = q.filter(Q(state=self.PENDING))
1522
        return q
1523

    
1524
    def last_pending_incl_me(self):
1525
        try:
1526
            return self.pending_modifications_incl_me().order_by('-id')[0]
1527
        except IndexError:
1528
            return None
1529

    
1530
    def pending_modifications(self):
1531
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1532

    
1533
    def last_pending(self):
1534
        try:
1535
            return self.pending_modifications().order_by('-id')[0]
1536
        except IndexError:
1537
            return None
1538

    
1539
    def is_modification(self):
1540
        # if self.state != self.PENDING:
1541
        #     return False
1542
        parents = self.chained_applications().filter(id__lt=self.id)
1543
        parents = parents.filter(state__in=[self.APPROVED])
1544
        return parents.count() > 0
1545

    
1546
    def chained_applications(self):
1547
        return ProjectApplication.objects.filter(chain=self.chain)
1548

    
1549
    def is_latest(self):
1550
        return self.chained_applications().order_by('-id')[0] == self
1551

    
1552
    def has_pending_modifications(self):
1553
        return bool(self.last_pending())
1554

    
1555
    def denied_modifications(self):
1556
        q = self.chained_applications()
1557
        q = q.filter(Q(state=self.DENIED))
1558
        q = q.filter(~Q(id=self.id))
1559
        return q
1560

    
1561
    def last_denied(self):
1562
        try:
1563
            return self.denied_modifications().order_by('-id')[0]
1564
        except IndexError:
1565
            return None
1566

    
1567
    def has_denied_modifications(self):
1568
        return bool(self.last_denied())
1569

    
1570
    def is_applied(self):
1571
        try:
1572
            self.project
1573
            return True
1574
        except Project.DoesNotExist:
1575
            return False
1576

    
1577
    def get_project(self):
1578
        try:
1579
            return Project.objects.get(id=self.chain)
1580
        except Project.DoesNotExist:
1581
            return None
1582

    
1583
    def project_exists(self):
1584
        return self.get_project() is not None
1585

    
1586
    def _get_project_for_update(self):
1587
        try:
1588
            objects = Project.objects
1589
            project = objects.get_for_update(id=self.chain)
1590
            return project
1591
        except Project.DoesNotExist:
1592
            return None
1593

    
1594
    def can_cancel(self):
1595
        return self.state == self.PENDING
1596

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

    
1603
        self.state = self.CANCELLED
1604
        self.save()
1605

    
1606
    def can_dismiss(self):
1607
        return self.state == self.DENIED
1608

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

    
1615
        self.state = self.DISMISSED
1616
        self.save()
1617

    
1618
    def can_deny(self):
1619
        return self.state == self.PENDING
1620

    
1621
    def deny(self, reason):
1622
        if not self.can_deny():
1623
            m = _("cannot deny: application '%s' in state '%s'") % (
1624
                    self.id, self.state)
1625
            raise AssertionError(m)
1626

    
1627
        self.state = self.DENIED
1628
        self.response_date = datetime.now()
1629
        self.response = reason
1630
        self.save()
1631

    
1632
    def can_approve(self):
1633
        return self.state == self.PENDING
1634

    
1635
    def approve(self, approval_user=None):
1636
        """
1637
        If approval_user then during owner membership acceptance
1638
        it is checked whether the request_user is eligible.
1639

1640
        Raises:
1641
            PermissionDenied
1642
        """
1643

    
1644
        if not transaction.is_managed():
1645
            raise AssertionError("NOPE")
1646

    
1647
        new_project_name = self.name
1648
        if not self.can_approve():
1649
            m = _("cannot approve: project '%s' in state '%s'") % (
1650
                    new_project_name, self.state)
1651
            raise AssertionError(m) # invalid argument
1652

    
1653
        now = datetime.now()
1654
        project = self._get_project_for_update()
1655

    
1656
        try:
1657
            q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
1658
            conflicting_project = Project.objects.get(q)
1659
            if (conflicting_project != project):
1660
                m = (_("cannot approve: project with name '%s' "
1661
                       "already exists (id: %s)") % (
1662
                        new_project_name, conflicting_project.id))
1663
                raise PermissionDenied(m) # invalid argument
1664
        except Project.DoesNotExist:
1665
            pass
1666

    
1667
        new_project = False
1668
        if project is None:
1669
            new_project = True
1670
            project = Project(id=self.chain)
1671

    
1672
        project.name = new_project_name
1673
        project.application = self
1674
        project.last_approval_date = now
1675

    
1676
        project.save()
1677

    
1678
        self.state = self.APPROVED
1679
        self.response_date = now
1680
        self.save()
1681
        return project
1682

    
1683
    @property
1684
    def member_join_policy_display(self):
1685
        return PROJECT_MEMBER_JOIN_POLICIES.get(str(self.member_join_policy))
1686

    
1687
    @property
1688
    def member_leave_policy_display(self):
1689
        return PROJECT_MEMBER_LEAVE_POLICIES.get(str(self.member_leave_policy))
1690

    
1691
class ProjectResourceGrant(models.Model):
1692

    
1693
    resource                =   models.ForeignKey(Resource)
1694
    project_application     =   models.ForeignKey(ProjectApplication,
1695
                                                  null=True)
1696
    project_capacity        =   intDecimalField(null=True)
1697
    member_capacity         =   intDecimalField(default=0)
1698

    
1699
    objects = ExtendedManager()
1700

    
1701
    class Meta:
1702
        unique_together = ("resource", "project_application")
1703

    
1704
    def display_member_capacity(self):
1705
        if self.member_capacity:
1706
            if self.resource.unit:
1707
                return ProjectResourceGrant.display_filesize(
1708
                    self.member_capacity)
1709
            else:
1710
                if math.isinf(self.member_capacity):
1711
                    return 'Unlimited'
1712
                else:
1713
                    return self.member_capacity
1714
        else:
1715
            return 'Unlimited'
1716

    
1717
    def __str__(self):
1718
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1719
                                        self.display_member_capacity())
1720

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

    
1745

    
1746
class ProjectManager(ForUpdateManager):
1747

    
1748
    def terminated_projects(self):
1749
        q = self.model.Q_TERMINATED
1750
        return self.filter(q)
1751

    
1752
    def not_terminated_projects(self):
1753
        q = ~self.model.Q_TERMINATED
1754
        return self.filter(q)
1755

    
1756
    def deactivated_projects(self):
1757
        q = self.model.Q_DEACTIVATED
1758
        return self.filter(q)
1759

    
1760
    def expired_projects(self):
1761
        q = (~Q(state=Project.TERMINATED) &
1762
              Q(application__end_date__lt=datetime.now()))
1763
        return self.filter(q)
1764

    
1765
    def search_by_name(self, *search_strings):
1766
        q = Q()
1767
        for s in search_strings:
1768
            q = q | Q(name__icontains=s)
1769
        return self.filter(q)
1770

    
1771

    
1772
class Project(models.Model):
1773

    
1774
    id                          =   models.OneToOneField(Chain,
1775
                                                      related_name='chained_project',
1776
                                                      db_column='id',
1777
                                                      primary_key=True)
1778

    
1779
    application                 =   models.OneToOneField(
1780
                                            ProjectApplication,
1781
                                            related_name='project')
1782
    last_approval_date          =   models.DateTimeField(null=True)
1783

    
1784
    members                     =   models.ManyToManyField(
1785
                                            AstakosUser,
1786
                                            through='ProjectMembership')
1787

    
1788
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1789
    deactivation_date           =   models.DateTimeField(null=True)
1790

    
1791
    creation_date               =   models.DateTimeField(auto_now_add=True)
1792
    name                        =   models.CharField(
1793
                                            max_length=80,
1794
                                            null=True,
1795
                                            db_index=True,
1796
                                            unique=True)
1797

    
1798
    APPROVED    = 1
1799
    SUSPENDED   = 10
1800
    TERMINATED  = 100
1801

    
1802
    state                       =   models.IntegerField(default=APPROVED,
1803
                                                        db_index=True)
1804

    
1805
    objects     =   ProjectManager()
1806

    
1807
    # Compiled queries
1808
    Q_TERMINATED  = Q(state=TERMINATED)
1809
    Q_SUSPENDED   = Q(state=SUSPENDED)
1810
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1811

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

    
1816
    __repr__ = __str__
1817

    
1818
    def __unicode__(self):
1819
        return _("<project %s '%s'>") % (self.id, self.application.name)
1820

    
1821
    STATE_DISPLAY = {
1822
        APPROVED   : 'Active',
1823
        SUSPENDED  : 'Suspended',
1824
        TERMINATED : 'Terminated'
1825
        }
1826

    
1827
    def state_display(self):
1828
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1829

    
1830
    def expiration_info(self):
1831
        return (str(self.id), self.name, self.state_display(),
1832
                str(self.application.end_date))
1833

    
1834
    def is_deactivated(self, reason=None):
1835
        if reason is not None:
1836
            return self.state == reason
1837

    
1838
        return self.state != self.APPROVED
1839

    
1840
    ### Deactivation calls
1841

    
1842
    def terminate(self):
1843
        self.deactivation_reason = 'TERMINATED'
1844
        self.deactivation_date = datetime.now()
1845
        self.state = self.TERMINATED
1846
        self.name = None
1847
        self.save()
1848

    
1849
    def suspend(self):
1850
        self.deactivation_reason = 'SUSPENDED'
1851
        self.deactivation_date = datetime.now()
1852
        self.state = self.SUSPENDED
1853
        self.save()
1854

    
1855
    def resume(self):
1856
        self.deactivation_reason = None
1857
        self.deactivation_date = None
1858
        self.state = self.APPROVED
1859
        self.save()
1860

    
1861
    ### Logical checks
1862

    
1863
    def is_inconsistent(self):
1864
        now = datetime.now()
1865
        dates = [self.creation_date,
1866
                 self.last_approval_date,
1867
                 self.deactivation_date]
1868
        return any([date > now for date in dates])
1869

    
1870
    def is_approved(self):
1871
        return self.state == self.APPROVED
1872

    
1873
    @property
1874
    def is_alive(self):
1875
        return not self.is_terminated
1876

    
1877
    @property
1878
    def is_terminated(self):
1879
        return self.is_deactivated(self.TERMINATED)
1880

    
1881
    @property
1882
    def is_suspended(self):
1883
        return self.is_deactivated(self.SUSPENDED)
1884

    
1885
    def violates_resource_grants(self):
1886
        return False
1887

    
1888
    def violates_members_limit(self, adding=0):
1889
        application = self.application
1890
        limit = application.limit_on_members_number
1891
        if limit is None:
1892
            return False
1893
        return (len(self.approved_members) + adding > limit)
1894

    
1895

    
1896
    ### Other
1897

    
1898
    def count_pending_memberships(self):
1899
        memb_set = self.projectmembership_set
1900
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1901
        return memb_count
1902

    
1903
    def members_count(self):
1904
        return self.approved_memberships.count()
1905

    
1906
    @property
1907
    def approved_memberships(self):
1908
        query = ProjectMembership.Q_ACCEPTED_STATES
1909
        return self.projectmembership_set.filter(query)
1910

    
1911
    @property
1912
    def approved_members(self):
1913
        return [m.person for m in self.approved_memberships]
1914

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

    
1924
        m, created = ProjectMembership.objects.get_or_create(
1925
            person=user, project=self
1926
        )
1927
        m.accept()
1928

    
1929
    def remove_member(self, user):
1930
        """
1931
        Raises:
1932
            django.exceptions.PermissionDenied
1933
            astakos.im.models.AstakosUser.DoesNotExist
1934
            astakos.im.models.ProjectMembership.DoesNotExist
1935
        """
1936
        if isinstance(user, (int, long)):
1937
            user = AstakosUser.objects.get(user=user)
1938

    
1939
        m = ProjectMembership.objects.get(person=user, project=self)
1940
        m.remove()
1941

    
1942

    
1943
CHAIN_STATE = {
1944
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1945
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1946
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1947
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1948
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1949

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

    
1956
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1957
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1958
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1959
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1960
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1961

    
1962
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1963
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1964
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1965
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1966
    }
1967

    
1968

    
1969
class ProjectMembershipManager(ForUpdateManager):
1970

    
1971
    def any_accepted(self):
1972
        q = self.model.Q_ACTUALLY_ACCEPTED
1973
        return self.filter(q)
1974

    
1975
    def actually_accepted(self):
1976
        q = self.model.Q_ACTUALLY_ACCEPTED
1977
        return self.filter(q)
1978

    
1979
    def requested(self):
1980
        return self.filter(state=ProjectMembership.REQUESTED)
1981

    
1982
    def suspended(self):
1983
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1984

    
1985
class ProjectMembership(models.Model):
1986

    
1987
    person              =   models.ForeignKey(AstakosUser)
1988
    request_date        =   models.DateField(auto_now_add=True)
1989
    project             =   models.ForeignKey(Project)
1990

    
1991
    REQUESTED           =   0
1992
    ACCEPTED            =   1
1993
    LEAVE_REQUESTED     =   5
1994
    # User deactivation
1995
    USER_SUSPENDED      =   10
1996

    
1997
    REMOVED             =   200
1998

    
1999
    ASSOCIATED_STATES   =   set([REQUESTED,
2000
                                 ACCEPTED,
2001
                                 LEAVE_REQUESTED,
2002
                                 USER_SUSPENDED,
2003
                                 ])
2004

    
2005
    ACCEPTED_STATES     =   set([ACCEPTED,
2006
                                 LEAVE_REQUESTED,
2007
                                 USER_SUSPENDED,
2008
                                 ])
2009

    
2010
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2011

    
2012
    state               =   models.IntegerField(default=REQUESTED,
2013
                                                db_index=True)
2014
    acceptance_date     =   models.DateField(null=True, db_index=True)
2015
    leave_request_date  =   models.DateField(null=True)
2016

    
2017
    objects     =   ProjectMembershipManager()
2018

    
2019
    # Compiled queries
2020
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2021
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2022

    
2023
    MEMBERSHIP_STATE_DISPLAY = {
2024
        REQUESTED           : _('Requested'),
2025
        ACCEPTED            : _('Accepted'),
2026
        LEAVE_REQUESTED     : _('Leave Requested'),
2027
        USER_SUSPENDED      : _('Suspended'),
2028
        REMOVED             : _('Pending removal'),
2029
        }
2030

    
2031
    USER_FRIENDLY_STATE_DISPLAY = {
2032
        REQUESTED           : _('Join requested'),
2033
        ACCEPTED            : _('Accepted member'),
2034
        LEAVE_REQUESTED     : _('Requested to leave'),
2035
        USER_SUSPENDED      : _('Suspended member'),
2036
        REMOVED             : _('Pending removal'),
2037
        }
2038

    
2039
    def state_display(self):
2040
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2041

    
2042
    def user_friendly_state_display(self):
2043
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2044

    
2045
    class Meta:
2046
        unique_together = ("person", "project")
2047
        #index_together = [["project", "state"]]
2048

    
2049
    def __str__(self):
2050
        return uenc(_("<'%s' membership in '%s'>") % (
2051
                self.person.username, self.project))
2052

    
2053
    __repr__ = __str__
2054

    
2055
    def __init__(self, *args, **kwargs):
2056
        self.state = self.REQUESTED
2057
        super(ProjectMembership, self).__init__(*args, **kwargs)
2058

    
2059
    def _set_history_item(self, reason, date=None):
2060
        if isinstance(reason, basestring):
2061
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2062

    
2063
        history_item = ProjectMembershipHistory(
2064
                            serial=self.id,
2065
                            person=self.person_id,
2066
                            project=self.project_id,
2067
                            date=date or datetime.now(),
2068
                            reason=reason)
2069
        history_item.save()
2070
        serial = history_item.id
2071

    
2072
    def can_accept(self):
2073
        return self.state == self.REQUESTED
2074

    
2075
    def accept(self):
2076
        if not self.can_accept():
2077
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2078
            raise AssertionError(m)
2079

    
2080
        now = datetime.now()
2081
        self.acceptance_date = now
2082
        self._set_history_item(reason='ACCEPT', date=now)
2083
        self.state = self.ACCEPTED
2084
        self.save()
2085

    
2086
    def can_leave(self):
2087
        return self.state in self.ACCEPTED_STATES
2088

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

    
2095
        self.leave_request_date = datetime.now()
2096
        self.state = self.LEAVE_REQUESTED
2097
        self.save()
2098

    
2099
    def can_deny_leave(self):
2100
        return self.state == self.LEAVE_REQUESTED
2101

    
2102
    def leave_request_deny(self):
2103
        if not self.can_deny_leave():
2104
            m = _("%s: attempt to deny leave request in state '%s'") % (
2105
                self, self.state)
2106
            raise AssertionError(m)
2107

    
2108
        self.leave_request_date = None
2109
        self.state = self.ACCEPTED
2110
        self.save()
2111

    
2112
    def can_cancel_leave(self):
2113
        return self.state == self.LEAVE_REQUESTED
2114

    
2115
    def leave_request_cancel(self):
2116
        if not self.can_cancel_leave():
2117
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2118
                self, self.state)
2119
            raise AssertionError(m)
2120

    
2121
        self.leave_request_date = None
2122
        self.state = self.ACCEPTED
2123
        self.save()
2124

    
2125
    def can_remove(self):
2126
        return self.state in self.ACCEPTED_STATES
2127

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

    
2133
        self._set_history_item(reason='REMOVE')
2134
        self.delete()
2135

    
2136
    def can_reject(self):
2137
        return self.state == self.REQUESTED
2138

    
2139
    def reject(self):
2140
        if not self.can_reject():
2141
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2142
            raise AssertionError(m)
2143

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

    
2149
    def can_cancel(self):
2150
        return self.state == self.REQUESTED
2151

    
2152
    def cancel(self):
2153
        if not self.can_cancel():
2154
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2155
            raise AssertionError(m)
2156

    
2157
        # rejected requests don't need sync,
2158
        # because they were never effected
2159
        self._set_history_item(reason='CANCEL')
2160
        self.delete()
2161

    
2162

    
2163
class Serial(models.Model):
2164
    serial  =   models.AutoField(primary_key=True)
2165

    
2166

    
2167
class ProjectMembershipHistory(models.Model):
2168
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2169
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2170

    
2171
    person  =   models.BigIntegerField()
2172
    project =   models.BigIntegerField()
2173
    date    =   models.DateField(auto_now_add=True)
2174
    reason  =   models.IntegerField()
2175
    serial  =   models.BigIntegerField()
2176

    
2177
### SIGNALS ###
2178
################
2179

    
2180
def create_astakos_user(u):
2181
    try:
2182
        AstakosUser.objects.get(user_ptr=u.pk)
2183
    except AstakosUser.DoesNotExist:
2184
        extended_user = AstakosUser(user_ptr_id=u.pk)
2185
        extended_user.__dict__.update(u.__dict__)
2186
        extended_user.save()
2187
        if not extended_user.has_auth_provider('local'):
2188
            extended_user.add_auth_provider('local')
2189
    except BaseException, e:
2190
        logger.exception(e)
2191

    
2192
def fix_superusers():
2193
    # Associate superusers with AstakosUser
2194
    admins = User.objects.filter(is_superuser=True)
2195
    for u in admins:
2196
        create_astakos_user(u)
2197

    
2198
def user_post_save(sender, instance, created, **kwargs):
2199
    if not created:
2200
        return
2201
    create_astakos_user(instance)
2202
post_save.connect(user_post_save, sender=User)
2203

    
2204
def astakosuser_post_save(sender, instance, created, **kwargs):
2205
    pass
2206

    
2207
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2208

    
2209
def resource_post_save(sender, instance, created, **kwargs):
2210
    pass
2211

    
2212
post_save.connect(resource_post_save, sender=Resource)
2213

    
2214
def renew_token(sender, instance, **kwargs):
2215
    if not instance.auth_token:
2216
        instance.renew_token()
2217
pre_save.connect(renew_token, sender=AstakosUser)
2218
pre_save.connect(renew_token, sender=Service)