Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (74 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
    api_url = models.CharField(_('Service API url'), max_length=255, null=True)
125
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
126
                                  null=True, blank=True)
127
    auth_token_created = models.DateTimeField(_('Token creation date'),
128
                                              null=True)
129
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
130
                                              null=True)
131

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

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

    
145
    def __str__(self):
146
        return self.name
147

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

    
160
        def service_by_order(s):
161
            return s[1].get('order')
162

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

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

    
174
        order_key = service_by_order
175
        if orderfor == 'dashboard':
176
            order_key = service_by_dashbaord_order
177

    
178
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
179
                                             key=order_key))
180
        return ordered_catalog
181

    
182

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

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

    
201
    objects = ForUpdateManager()
202

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

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

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

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

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

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

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

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

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

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

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

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

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

    
266

    
267
class AstakosUserManager(UserManager):
268

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

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

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

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

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

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

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

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

    
321

    
322

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

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

    
339

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

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

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

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

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

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

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

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

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

    
383
    objects = AstakosUserManager()
384

    
385
    forupdate = ForUpdateManager()
386

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
501
        self.update_uuid()
502

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
649
        modules = astakos_settings.IM_MODULES
650

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

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

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

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

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

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

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

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

    
687
        msg_extra = ''
688
        message = ''
689

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

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

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

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

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

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

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

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

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

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

    
760

    
761
class AstakosUserAuthProviderManager(models.Manager):
762

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

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

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

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

    
789

    
790
class AuthProviderPolicyProfileManager(models.Manager):
791

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

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

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

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

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

    
825

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

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

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

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

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

    
853
    objects = AuthProviderPolicyProfileManager()
854

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

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

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

    
874

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

    
893
    objects = AstakosUserAuthProviderManager()
894

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

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

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

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

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

    
919
        extra_data['info'] = info_data
920

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

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

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

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

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

    
942

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

    
970
    update_or_create = _update_or_create
971

    
972

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

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

    
982

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

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

    
992

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

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

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

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

    
1019

    
1020
class EmailChangeManager(models.Manager):
1021

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

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

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

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

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

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

    
1067

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

    
1080
    objects = EmailChangeManager()
1081

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

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

    
1090

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

    
1098

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

    
1108

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

    
1117

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

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

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

    
1149
        return user
1150

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

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

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

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

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

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

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

    
1196

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

    
1202
    objects = ForUpdateManager()
1203

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

    
1207

    
1208
### PROJECTS ###
1209
################
1210

    
1211
class ChainManager(ForUpdateManager):
1212

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

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

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

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

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

    
1239
        return d
1240

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

    
1249

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

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

    
1256
    objects = ChainManager()
1257

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

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

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

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

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

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

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

    
1307

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

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

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

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

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

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

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

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

    
1348

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

    
1353

    
1354
class ProjectApplicationManager(ForUpdateManager):
1355

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

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

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

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

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

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

    
1392

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

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

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

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

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

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

    
1440
    objects                 =   ProjectApplicationManager()
1441

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1522
    def pending_modifications(self):
1523
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1524

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

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

    
1538
    def chained_applications(self):
1539
        return ProjectApplication.objects.filter(chain=self.chain)
1540

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

    
1544
    def has_pending_modifications(self):
1545
        return bool(self.last_pending())
1546

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

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

    
1559
    def has_denied_modifications(self):
1560
        return bool(self.last_denied())
1561

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

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

    
1575
    def project_exists(self):
1576
        return self.get_project() is not None
1577

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

    
1586
    def can_cancel(self):
1587
        return self.state == self.PENDING
1588

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

    
1595
        self.state = self.CANCELLED
1596
        self.save()
1597

    
1598
    def can_dismiss(self):
1599
        return self.state == self.DENIED
1600

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

    
1607
        self.state = self.DISMISSED
1608
        self.save()
1609

    
1610
    def can_deny(self):
1611
        return self.state == self.PENDING
1612

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

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

    
1624
    def can_approve(self):
1625
        return self.state == self.PENDING
1626

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

1632
        Raises:
1633
            PermissionDenied
1634
        """
1635

    
1636
        if not transaction.is_managed():
1637
            raise AssertionError("NOPE")
1638

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

    
1645
        now = datetime.now()
1646
        project = self._get_project_for_update()
1647

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

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

    
1664
        project.name = new_project_name
1665
        project.application = self
1666
        project.last_approval_date = now
1667

    
1668
        project.save()
1669

    
1670
        self.state = self.APPROVED
1671
        self.response_date = now
1672
        self.save()
1673
        return project
1674

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

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

    
1683
class ProjectResourceGrant(models.Model):
1684

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

    
1691
    objects = ExtendedManager()
1692

    
1693
    class Meta:
1694
        unique_together = ("resource", "project_application")
1695

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

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

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

    
1737

    
1738
class ProjectManager(ForUpdateManager):
1739

    
1740
    def terminated_projects(self):
1741
        q = self.model.Q_TERMINATED
1742
        return self.filter(q)
1743

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

    
1748
    def deactivated_projects(self):
1749
        q = self.model.Q_DEACTIVATED
1750
        return self.filter(q)
1751

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

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

    
1763

    
1764
class Project(models.Model):
1765

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

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

    
1776
    members                     =   models.ManyToManyField(
1777
                                            AstakosUser,
1778
                                            through='ProjectMembership')
1779

    
1780
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1781
    deactivation_date           =   models.DateTimeField(null=True)
1782

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

    
1790
    APPROVED    = 1
1791
    SUSPENDED   = 10
1792
    TERMINATED  = 100
1793

    
1794
    state                       =   models.IntegerField(default=APPROVED,
1795
                                                        db_index=True)
1796

    
1797
    objects     =   ProjectManager()
1798

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

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

    
1808
    __repr__ = __str__
1809

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

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

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

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

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

    
1830
        return self.state != self.APPROVED
1831

    
1832
    ### Deactivation calls
1833

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

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

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

    
1853
    ### Logical checks
1854

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

    
1862
    def is_approved(self):
1863
        return self.state == self.APPROVED
1864

    
1865
    @property
1866
    def is_alive(self):
1867
        return not self.is_terminated
1868

    
1869
    @property
1870
    def is_terminated(self):
1871
        return self.is_deactivated(self.TERMINATED)
1872

    
1873
    @property
1874
    def is_suspended(self):
1875
        return self.is_deactivated(self.SUSPENDED)
1876

    
1877
    def violates_resource_grants(self):
1878
        return False
1879

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

    
1887

    
1888
    ### Other
1889

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

    
1895
    def members_count(self):
1896
        return self.approved_memberships.count()
1897

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

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

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

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

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

    
1931
        m = ProjectMembership.objects.get(person=user, project=self)
1932
        m.remove()
1933

    
1934

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

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

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

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

    
1960

    
1961
class ProjectMembershipManager(ForUpdateManager):
1962

    
1963
    def any_accepted(self):
1964
        q = self.model.Q_ACTUALLY_ACCEPTED
1965
        return self.filter(q)
1966

    
1967
    def actually_accepted(self):
1968
        q = self.model.Q_ACTUALLY_ACCEPTED
1969
        return self.filter(q)
1970

    
1971
    def requested(self):
1972
        return self.filter(state=ProjectMembership.REQUESTED)
1973

    
1974
    def suspended(self):
1975
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1976

    
1977
class ProjectMembership(models.Model):
1978

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

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

    
1989
    REMOVED             =   200
1990

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

    
1997
    ACCEPTED_STATES     =   set([ACCEPTED,
1998
                                 LEAVE_REQUESTED,
1999
                                 USER_SUSPENDED,
2000
                                 ])
2001

    
2002
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2003

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

    
2009
    objects     =   ProjectMembershipManager()
2010

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

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

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

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

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

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

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

    
2045
    __repr__ = __str__
2046

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

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

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

    
2064
    def can_accept(self):
2065
        return self.state == self.REQUESTED
2066

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

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

    
2078
    def can_leave(self):
2079
        return self.state in self.ACCEPTED_STATES
2080

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

    
2087
        self.leave_request_date = datetime.now()
2088
        self.state = self.LEAVE_REQUESTED
2089
        self.save()
2090

    
2091
    def can_deny_leave(self):
2092
        return self.state == self.LEAVE_REQUESTED
2093

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

    
2100
        self.leave_request_date = None
2101
        self.state = self.ACCEPTED
2102
        self.save()
2103

    
2104
    def can_cancel_leave(self):
2105
        return self.state == self.LEAVE_REQUESTED
2106

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

    
2113
        self.leave_request_date = None
2114
        self.state = self.ACCEPTED
2115
        self.save()
2116

    
2117
    def can_remove(self):
2118
        return self.state in self.ACCEPTED_STATES
2119

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

    
2125
        self._set_history_item(reason='REMOVE')
2126
        self.delete()
2127

    
2128
    def can_reject(self):
2129
        return self.state == self.REQUESTED
2130

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

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

    
2141
    def can_cancel(self):
2142
        return self.state == self.REQUESTED
2143

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

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

    
2154

    
2155
class Serial(models.Model):
2156
    serial  =   models.AutoField(primary_key=True)
2157

    
2158

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

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

    
2169
### SIGNALS ###
2170
################
2171

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

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

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

    
2196
def astakosuser_post_save(sender, instance, created, **kwargs):
2197
    pass
2198

    
2199
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2200

    
2201
def resource_post_save(sender, instance, created, **kwargs):
2202
    pass
2203

    
2204
post_save.connect(resource_post_save, sender=Resource)
2205

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