Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (69.6 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 uuid
35
import logging
36
import json
37
import copy
38

    
39
from datetime import datetime, timedelta
40
import base64
41
from urllib import quote
42
from random import randint
43
import os
44

    
45
from django.db import models, transaction
46
from django.contrib.auth.models import User, UserManager, Group, Permission
47
from django.utils.translation import ugettext as _
48
from django.db.models.signals import pre_save, post_save
49
from django.contrib.contenttypes.models import ContentType
50

    
51
from django.db.models import Q
52
from django.core.urlresolvers import reverse
53
from django.utils.http import int_to_base36
54
from django.contrib.auth.tokens import default_token_generator
55
from django.conf import settings
56
from django.utils.importlib import import_module
57
from django.utils.safestring import mark_safe
58

    
59
from synnefo.lib.utils import dict_merge
60

    
61
from astakos.im import settings as astakos_settings
62
from astakos.im import auth_providers as auth
63

    
64
import astakos.im.messages as astakos_messages
65
from synnefo.lib.ordereddict import OrderedDict
66

    
67
from synnefo.util import units
68
from astakos.im import presentation
69

    
70
logger = logging.getLogger(__name__)
71

    
72
DEFAULT_CONTENT_TYPE = None
73
_content_type = None
74

    
75

    
76
def get_content_type():
77
    global _content_type
78
    if _content_type is not None:
79
        return _content_type
80

    
81
    try:
82
        content_type = ContentType.objects.get(app_label='im',
83
                                               model='astakosuser')
84
    except:
85
        content_type = DEFAULT_CONTENT_TYPE
86
    _content_type = content_type
87
    return content_type
88

    
89
inf = float('inf')
90

    
91

    
92
def generate_token():
93
    s = os.urandom(32)
94
    return base64.urlsafe_b64encode(s).rstrip('=')
95

    
96

    
97
def _partition_by(f, l):
98
    d = {}
99
    for x in l:
100
        group = f(x)
101
        group_l = d.get(group, [])
102
        group_l.append(x)
103
        d[group] = group_l
104
    return d
105

    
106

    
107
def first_of_group(f, l):
108
    Nothing = type("Nothing", (), {})
109
    last_group = Nothing
110
    d = {}
111
    for x in l:
112
        group = f(x)
113
        if group != last_group:
114
            last_group = group
115
            d[group] = x
116
    return d
117

    
118

    
119
class Component(models.Model):
120
    name = models.CharField(_('Name'), max_length=255, unique=True,
121
                            db_index=True)
122
    url = models.CharField(_('Component url'), max_length=1024, null=True,
123
                           help_text=_("URL the component is accessible from"))
124
    base_url = models.CharField(max_length=1024, null=True)
125
    auth_token = models.CharField(_('Authentication Token'), max_length=64,
126
                                  null=True, blank=True, unique=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
        for i in range(10):
134
            new_token = generate_token()
135
            count = Component.objects.filter(auth_token=new_token).count()
136
            if count == 0:
137
                break
138
            continue
139
        else:
140
            raise ValueError('Could not generate a token')
141

    
142
        self.auth_token = new_token
143
        self.auth_token_created = datetime.now()
144
        if expiration_date:
145
            self.auth_token_expires = expiration_date
146
        else:
147
            self.auth_token_expires = None
148
        msg = 'Token renewed for component %s'
149
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.name)
150

    
151
    def __unicode__(self):
152
        return self.name
153

    
154
    @classmethod
155
    def catalog(cls, orderfor=None):
156
        catalog = {}
157
        components = list(cls.objects.all())
158
        default_metadata = presentation.COMPONENTS
159
        metadata = {}
160

    
161
        for component in components:
162
            d = {'url': component.url,
163
                 'name': component.name}
164
            if component.name in default_metadata:
165
                metadata[component.name] = default_metadata.get(component.name)
166
                metadata[component.name].update(d)
167
            else:
168
                metadata[component.name] = d
169

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

    
173
        def component_by_dashboard_order(s):
174
            return s[1].get('dashboard').get('order')
175

    
176
        metadata = dict_merge(metadata,
177
                              astakos_settings.COMPONENTS_META)
178

    
179
        for component, info in metadata.iteritems():
180
            default_meta = presentation.component_defaults(component)
181
            base_meta = metadata.get(component, {})
182
            settings_meta = astakos_settings.COMPONENTS_META.get(component, {})
183
            component_meta = dict_merge(default_meta, base_meta)
184
            meta = dict_merge(component_meta, settings_meta)
185
            catalog[component] = meta
186

    
187
        order_key = component_by_order
188
        if orderfor == 'dashboard':
189
            order_key = component_by_dashboard_order
190

    
191
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
192
                                             key=order_key))
193
        return ordered_catalog
194

    
195

    
196
_presentation_data = {}
197

    
198

    
199
def get_presentation(resource):
200
    global _presentation_data
201
    resource_presentation = _presentation_data.get(resource, {})
202
    if not resource_presentation:
203
        resources_presentation = presentation.RESOURCES.get('resources', {})
204
        resource_presentation = resources_presentation.get(resource, {})
205
        _presentation_data[resource] = resource_presentation
206
    return resource_presentation
207

    
208

    
209
class Service(models.Model):
210
    component = models.ForeignKey(Component)
211
    name = models.CharField(max_length=255, unique=True)
212
    type = models.CharField(max_length=255)
213

    
214

    
215
class Endpoint(models.Model):
216
    service = models.ForeignKey(Service, related_name='endpoints')
217

    
218

    
219
class EndpointData(models.Model):
220
    endpoint = models.ForeignKey(Endpoint, related_name='data')
221
    key = models.CharField(max_length=255)
222
    value = models.CharField(max_length=1024)
223

    
224
    class Meta:
225
        unique_together = (('endpoint', 'key'),)
226

    
227

    
228
class Resource(models.Model):
229
    name = models.CharField(_('Name'), max_length=255, unique=True)
230
    desc = models.TextField(_('Description'), null=True)
231
    service_type = models.CharField(_('Type'), max_length=255)
232
    service_origin = models.CharField(max_length=255, db_index=True)
233
    unit = models.CharField(_('Unit'), null=True, max_length=255)
234
    uplimit = models.BigIntegerField(default=0)
235
    project_default = models.BigIntegerField()
236
    ui_visible = models.BooleanField(default=True)
237
    api_visible = models.BooleanField(default=True)
238

    
239
    def __unicode__(self):
240
        return self.name
241

    
242
    def full_name(self):
243
        return unicode(self)
244

    
245
    def get_info(self):
246
        return {'service': self.service_origin,
247
                'description': self.desc,
248
                'unit': self.unit,
249
                'ui_visible': self.ui_visible,
250
                'api_visible': self.api_visible,
251
                }
252

    
253
    @property
254
    def group(self):
255
        default = self.name
256
        return get_presentation(unicode(self)).get('group', default)
257

    
258
    @property
259
    def help_text(self):
260
        default = "%s resource" % self.name
261
        return get_presentation(unicode(self)).get('help_text', default)
262

    
263
    @property
264
    def help_text_input_each(self):
265
        default = "%s resource" % self.name
266
        return get_presentation(unicode(self)).get(
267
            'help_text_input_each', default)
268

    
269
    @property
270
    def is_abbreviation(self):
271
        return get_presentation(unicode(self)).get('is_abbreviation', False)
272

    
273
    @property
274
    def report_desc(self):
275
        default = "%s resource" % self.name
276
        return get_presentation(unicode(self)).get('report_desc', default)
277

    
278
    @property
279
    def placeholder(self):
280
        return get_presentation(unicode(self)).get('placeholder', self.unit)
281

    
282
    @property
283
    def verbose_name(self):
284
        return get_presentation(unicode(self)).get('verbose_name', self.name)
285

    
286
    @property
287
    def display_name(self):
288
        name = self.verbose_name
289
        if self.is_abbreviation:
290
            name = name.upper()
291
        return name
292

    
293
    @property
294
    def pluralized_display_name(self):
295
        if not self.unit:
296
            return '%ss' % self.display_name
297
        return self.display_name
298

    
299

    
300
def get_resource_names():
301
    _RESOURCE_NAMES = []
302
    resources = Resource.objects.select_related('service').all()
303
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
304
    return _RESOURCE_NAMES
305

    
306

    
307
def split_realname(value):
308
    parts = value.split(' ')
309
    if len(parts) == 2:
310
        return parts
311
    else:
312
        return ('', value)
313

    
314

    
315
class AstakosUserManager(UserManager):
316

    
317
    def get_auth_provider_user(self, provider, **kwargs):
318
        """
319
        Retrieve AstakosUser instance associated with the specified third party
320
        id.
321
        """
322
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
323
                          kwargs.iteritems()))
324
        return self.get(auth_providers__module=provider, **kwargs)
325

    
326
    def get_by_email(self, email):
327
        return self.get(email=email)
328

    
329
    def get_by_identifier(self, email_or_username, **kwargs):
330
        try:
331
            return self.get(email__iexact=email_or_username, **kwargs)
332
        except AstakosUser.DoesNotExist:
333
            return self.get(username__iexact=email_or_username, **kwargs)
334

    
335
    def user_exists(self, email_or_username, **kwargs):
336
        qemail = Q(email__iexact=email_or_username)
337
        qusername = Q(username__iexact=email_or_username)
338
        qextra = Q(**kwargs)
339
        return self.filter((qemail | qusername) & qextra).exists()
340

    
341
    def unverified_namesakes(self, email_or_username):
342
        q = Q(email__iexact=email_or_username)
343
        q |= Q(username__iexact=email_or_username)
344
        return self.filter(q & Q(email_verified=False))
345

    
346
    def verified_user_exists(self, email_or_username):
347
        return self.user_exists(email_or_username, email_verified=True)
348

    
349
    def verified(self):
350
        return self.filter(email_verified=True)
351

    
352
    def accepted(self):
353
        return self.filter(moderated=True, is_rejected=False)
354

    
355
    def uuid_catalog(self, l=None):
356
        """
357
        Returns a uuid to username mapping for the uuids appearing in l.
358
        If l is None returns the mapping for all existing users.
359
        """
360
        q = self.filter(uuid__in=l) if l is not None else self
361
        return dict(q.values_list('uuid', 'username'))
362

    
363
    def displayname_catalog(self, l=None):
364
        """
365
        Returns a username to uuid mapping for the usernames appearing in l.
366
        If l is None returns the mapping for all existing users.
367
        """
368
        if l is not None:
369
            lmap = dict((x.lower(), x) for x in l)
370
            q = self.filter(username__in=lmap.keys())
371
            values = ((lmap[n], u)
372
                      for n, u in q.values_list('username', 'uuid'))
373
        else:
374
            q = self
375
            values = self.values_list('username', 'uuid')
376
        return dict(values)
377

    
378

    
379
class AstakosUser(User):
380
    """
381
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
382
    """
383
    affiliation = models.CharField(_('Affiliation'), max_length=255,
384
                                   blank=True, null=True)
385

    
386
    #for invitations
387
    user_level = astakos_settings.DEFAULT_USER_LEVEL
388
    level = models.IntegerField(_('Inviter level'), default=user_level)
389
    invitations = models.IntegerField(
390
        _('Invitations left'),
391
        default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
392

    
393
    auth_token = models.CharField(
394
        _('Authentication Token'),
395
        max_length=64,
396
        unique=True,
397
        null=True,
398
        blank=True,
399
        help_text=_('Renew your authentication '
400
                    'token. Make sure to set the new '
401
                    'token in any client you may be '
402
                    'using, to preserve its '
403
                    'functionality.'))
404
    auth_token_created = models.DateTimeField(_('Token creation date'),
405
                                              null=True)
406
    auth_token_expires = models.DateTimeField(
407
        _('Token expiration date'), null=True)
408

    
409
    updated = models.DateTimeField(_('Update date'))
410

    
411
    # Arbitrary text to identify the reason user got deactivated.
412
    # To be used as a reference from administrators.
413
    deactivated_reason = models.TextField(
414
        _('Reason the user was disabled for'),
415
        default=None, null=True)
416
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
417
                                          blank=True)
418

    
419
    has_credits = models.BooleanField(_('Has credits?'), default=False)
420

    
421
    # this is set to True when user profile gets updated for the first time
422
    is_verified = models.BooleanField(_('Is verified?'), default=False)
423

    
424
    # user email is verified
425
    email_verified = models.BooleanField(_('Email verified?'), default=False)
426

    
427
    # unique string used in user email verification url
428
    verification_code = models.CharField(max_length=255, null=True,
429
                                         blank=False, unique=True)
430

    
431
    # date user email verified
432
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
433
                                       blank=True)
434

    
435
    # email verification notice was sent to the user at this time
436
    activation_sent = models.DateTimeField(_('Activation sent date'),
437
                                           null=True, blank=True)
438

    
439
    # user got rejected during moderation process
440
    is_rejected = models.BooleanField(_('Account rejected'),
441
                                      default=False)
442
    # reason user got rejected
443
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
444
                                       blank=True)
445
    # moderation status
446
    moderated = models.BooleanField(_('User moderated'), default=False)
447
    # date user moderated (either accepted or rejected)
448
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
449
                                        blank=True, null=True)
450
    # a snapshot of user instance the time got moderated
451
    moderated_data = models.TextField(null=True, default=None, blank=True)
452
    # a string which identifies how the user got moderated
453
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
454
                                       default=None, null=True, blank=True)
455
    # the email used to accept the user
456
    accepted_email = models.EmailField(null=True, default=None, blank=True)
457

    
458
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
459
                                           default=False)
460
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
461
                                             null=True, blank=True)
462
    # permanent unique user identifier
463
    uuid = models.CharField(max_length=255, null=False, blank=False,
464
                            unique=True)
465

    
466
    policy = models.ManyToManyField(
467
        Resource, null=True, through='AstakosUserQuota')
468

    
469
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
470
                                          default=False, db_index=True)
471

    
472
    # This could have been OneToOneField, but fails due to
473
    # https://code.djangoproject.com/ticket/13781 (fixed in v1.6)
474
    base_project = models.ForeignKey('Project', related_name="base_user")
475

    
476
    objects = AstakosUserManager()
477

    
478
    @property
479
    def realname(self):
480
        return '%s %s' % (self.first_name, self.last_name)
481

    
482
    @property
483
    def log_display(self):
484
        """
485
        Should be used in all logger.* calls that refer to a user so that
486
        user display is consistent across log entries.
487
        """
488
        return '%s::%s' % (self.uuid, self.email)
489

    
490
    @realname.setter
491
    def realname(self, value):
492
        first, last = split_realname(value)
493
        self.first_name = first
494
        self.last_name = last
495

    
496
    def add_permission(self, pname):
497
        if self.has_perm(pname):
498
            return
499
        p, created = Permission.objects.get_or_create(
500
            codename=pname,
501
            name=pname.capitalize(),
502
            content_type=get_content_type())
503
        self.user_permissions.add(p)
504

    
505
    def remove_permission(self, pname):
506
        if self.has_perm(pname):
507
            return
508
        p = Permission.objects.get(codename=pname,
509
                                   content_type=get_content_type())
510
        self.user_permissions.remove(p)
511

    
512
    def add_group(self, gname):
513
        group, _ = Group.objects.get_or_create(name=gname)
514
        self.groups.add(group)
515

    
516
    def is_accepted(self):
517
        return self.moderated and not self.is_rejected
518

    
519
    def is_project_admin(self):
520
        return self.uuid in astakos_settings.PROJECT_ADMINS
521

    
522
    @property
523
    def invitation(self):
524
        try:
525
            return Invitation.objects.get(username=self.email)
526
        except Invitation.DoesNotExist:
527
            return None
528

    
529
    @property
530
    def policies(self):
531
        return self.astakosuserquota_set.select_related().all()
532

    
533
    def get_resource_policy(self, resource):
534
        return AstakosUserQuota.objects.select_related("resource").\
535
            get(user=self, resource__name=resource)
536

    
537
    def fix_username(self):
538
        self.username = self.email.lower()
539

    
540
    def set_email(self, email):
541
        self.email = email
542
        self.fix_username()
543

    
544
    def save(self, update_timestamps=True, **kwargs):
545
        if update_timestamps:
546
            self.updated = datetime.now()
547

    
548
        super(AstakosUser, self).save(**kwargs)
549

    
550
    def renew_verification_code(self):
551
        self.verification_code = str(uuid.uuid4())
552
        logger.info("Verification code renewed for %s" % self.log_display)
553

    
554
    def renew_token(self, flush_sessions=False, current_key=None):
555
        for i in range(10):
556
            new_token = generate_token()
557
            count = AstakosUser.objects.filter(auth_token=new_token).count()
558
            if count == 0:
559
                break
560
            continue
561
        else:
562
            raise ValueError('Could not generate a token')
563

    
564
        self.auth_token = new_token
565
        self.auth_token_created = datetime.now()
566
        self.auth_token_expires = self.auth_token_created + \
567
            timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
568
        if flush_sessions:
569
            self.flush_sessions(current_key)
570
        self.delete_online_access_tokens()
571
        msg = 'Token renewed for %s'
572
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
573

    
574
    def token_expired(self):
575
        return self.auth_token_expires < datetime.now()
576

    
577
    def flush_sessions(self, current_key=None):
578
        q = self.sessions
579
        if current_key:
580
            q = q.exclude(session_key=current_key)
581

    
582
        keys = q.values_list('session_key', flat=True)
583
        if keys:
584
            msg = 'Flushing sessions: %s'
585
            logger.log(astakos_settings.LOGGING_LEVEL, msg, ','.join(keys))
586
        engine = import_module(settings.SESSION_ENGINE)
587
        for k in keys:
588
            s = engine.SessionStore(k)
589
            s.flush()
590

    
591
    def __unicode__(self):
592
        return '%s (%s)' % (self.realname, self.email)
593

    
594
    def conflicting_email(self):
595
        q = AstakosUser.objects.exclude(username=self.username)
596
        q = q.filter(email__iexact=self.email)
597
        if q.count() != 0:
598
            return True
599
        return False
600

    
601
    def email_change_is_pending(self):
602
        return self.emailchanges.count() > 0
603

    
604
    @property
605
    def status_display(self):
606
        msg = ""
607
        if self.is_active:
608
            msg = "Accepted/Active"
609
        if self.is_rejected:
610
            msg = "Rejected"
611
            if self.rejected_reason:
612
                msg += " (%s)" % self.rejected_reason
613
        if not self.email_verified:
614
            msg = "Pending email verification"
615
        if not self.moderated:
616
            msg = "Pending moderation"
617
        if not self.is_active and self.email_verified:
618
            msg = "Accepted/Inactive"
619
            if self.deactivated_reason:
620
                msg += " (%s)" % (self.deactivated_reason)
621

    
622
        if self.moderated and not self.is_rejected:
623
            if self.accepted_policy == 'manual':
624
                msg += " (manually accepted)"
625
            else:
626
                msg += " (accepted policy: %s)" % \
627
                    self.accepted_policy
628
        return msg
629

    
630
    @property
631
    def signed_terms(self):
632
        return self.has_signed_terms
633

    
634
    def set_invitations_level(self):
635
        """
636
        Update user invitation level
637
        """
638
        level = self.invitation.inviter.level + 1
639
        self.level = level
640
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
641

    
642
    def can_change_password(self):
643
        return self.has_auth_provider('local', auth_backend='astakos')
644

    
645
    def can_change_email(self):
646
        if not self.has_auth_provider('local'):
647
            return True
648

    
649
        local = self.get_auth_provider('local')._instance
650
        return local.auth_backend == 'astakos'
651

    
652
    # Auth providers related methods
653
    def get_auth_provider(self, module=None, identifier=None, **filters):
654
        if not module:
655
            return self.auth_providers.active()[0].settings
656

    
657
        params = {'module': module}
658
        if identifier:
659
            params['identifier'] = identifier
660
        params.update(filters)
661
        return self.auth_providers.active().get(**params).settings
662

    
663
    def has_auth_provider(self, provider, **kwargs):
664
        return bool(self.auth_providers.active().filter(module=provider,
665
                                                        **kwargs).count())
666

    
667
    def get_required_providers(self, **kwargs):
668
        return auth.REQUIRED_PROVIDERS.keys()
669

    
670
    def missing_required_providers(self):
671
        required = self.get_required_providers()
672
        missing = []
673
        for provider in required:
674
            if not self.has_auth_provider(provider):
675
                missing.append(auth.get_provider(provider, self))
676
        return missing
677

    
678
    def get_available_auth_providers(self, **filters):
679
        """
680
        Returns a list of providers available for add by the user.
681
        """
682
        modules = astakos_settings.IM_MODULES
683
        providers = []
684
        for p in modules:
685
            providers.append(auth.get_provider(p, self))
686
        available = []
687

    
688
        for p in providers:
689
            if p.get_add_policy:
690
                available.append(p)
691
        return available
692

    
693
    def get_disabled_auth_providers(self, **filters):
694
        providers = self.get_auth_providers(**filters)
695
        disabled = []
696
        for p in providers:
697
            if not p.get_login_policy:
698
                disabled.append(p)
699
        return disabled
700

    
701
    def get_enabled_auth_providers(self, **filters):
702
        providers = self.get_auth_providers(**filters)
703
        enabled = []
704
        for p in providers:
705
            if p.get_login_policy:
706
                enabled.append(p)
707
        return enabled
708

    
709
    def get_auth_providers(self, **filters):
710
        providers = []
711
        for provider in self.auth_providers.active(**filters):
712
            if provider.settings.module_enabled:
713
                providers.append(provider.settings)
714

    
715
        modules = astakos_settings.IM_MODULES
716

    
717
        def key(p):
718
            if not p.module in modules:
719
                return 100
720
            return modules.index(p.module)
721

    
722
        providers = sorted(providers, key=key)
723
        return providers
724

    
725
    # URL methods
726
    @property
727
    def auth_providers_display(self):
728
        return ",".join(["%s:%s" % (p.module, p.identifier) for p in
729
                         self.get_enabled_auth_providers()])
730

    
731
    def add_auth_provider(self, module='local', identifier=None, **params):
732
        provider = auth.get_provider(module, self, identifier, **params)
733
        provider.add_to_user()
734

    
735
    def get_resend_activation_url(self):
736
        return reverse('send_activation', kwargs={'user_id': self.pk})
737

    
738
    def get_activation_url(self, nxt=False):
739
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
740
                              quote(self.verification_code))
741
        if nxt:
742
            url += "&next=%s" % quote(nxt)
743
        return url
744

    
745
    def get_password_reset_url(self, token_generator=default_token_generator):
746
        return reverse('astakos.im.views.target.local.password_reset_confirm',
747
                       kwargs={'uidb36': int_to_base36(self.id),
748
                               'token': token_generator.make_token(self)})
749

    
750
    def get_inactive_message(self, provider_module, identifier=None):
751
        try:
752
            provider = self.get_auth_provider(provider_module, identifier)
753
        except AstakosUserAuthProvider.DoesNotExist:
754
            provider = auth.get_provider(provider_module, self)
755

    
756
        msg_extra = ''
757
        message = ''
758

    
759
        msg_inactive = provider.get_account_inactive_msg
760
        msg_pending = provider.get_pending_activation_msg
761
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
762
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
763
        msg_pending_mod = provider.get_pending_moderation_msg
764
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
765
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
766

    
767
        if not self.email_verified:
768
            message = msg_pending
769
            url = self.get_resend_activation_url()
770
            msg_extra = msg_pending_help + \
771
                u' ' + \
772
                '<a href="%s">%s?</a>' % (url, msg_resend)
773
        else:
774
            if not self.moderated:
775
                message = msg_pending_mod
776
            else:
777
                if self.is_rejected:
778
                    message = msg_rejected
779
                else:
780
                    message = msg_inactive
781

    
782
        return mark_safe(message + u' ' + msg_extra)
783

    
784
    def owns_application(self, application):
785
        return application.owner == self
786

    
787
    def owns_project(self, project):
788
        return project.owner == self
789

    
790
    def is_associated(self, project):
791
        try:
792
            m = ProjectMembership.objects.get(person=self, project=project)
793
            return m.state in ProjectMembership.ASSOCIATED_STATES
794
        except ProjectMembership.DoesNotExist:
795
            return False
796

    
797
    def get_membership(self, project):
798
        try:
799
            return ProjectMembership.objects.get(
800
                project=project,
801
                person=self)
802
        except ProjectMembership.DoesNotExist:
803
            return None
804

    
805
    def membership_display(self, project):
806
        m = self.get_membership(project)
807
        if m is None:
808
            return _('Not a member')
809
        else:
810
            return m.user_friendly_state_display()
811

    
812
    def non_owner_can_view(self, maybe_project):
813
        if self.is_project_admin():
814
            return True
815
        if maybe_project is None:
816
            return False
817
        project = maybe_project
818
        if self.is_associated(project):
819
            return True
820
        if project.is_deactivated():
821
            return False
822
        return True
823

    
824
    def delete_online_access_tokens(self):
825
        offline_tokens = self.token_set.filter(access_token='online')
826
        logger.info('The following access tokens will be deleted: %s',
827
                    offline_tokens)
828
        offline_tokens.delete()
829

    
830

    
831
class AstakosUserAuthProviderManager(models.Manager):
832

    
833
    def active(self, **filters):
834
        return self.filter(active=True, **filters)
835

    
836
    def remove_unverified_providers(self, provider, **filters):
837
        try:
838
            existing = self.filter(module=provider, user__email_verified=False,
839
                                   **filters)
840
            for p in existing:
841
                p.user.delete()
842
        except:
843
            pass
844

    
845
    def unverified(self, provider, **filters):
846
        try:
847

    
848
            return self.select_for_update().get(module=provider,
849
                                                user__email_verified=False,
850
                                                **filters).settings
851
        except AstakosUserAuthProvider.DoesNotExist:
852
            return None
853

    
854
    def verified(self, provider, **filters):
855
        try:
856
            return self.get(module=provider, user__email_verified=True,
857
                            **filters).settings
858
        except AstakosUserAuthProvider.DoesNotExist:
859
            return None
860

    
861

    
862
class AuthProviderPolicyProfileManager(models.Manager):
863

    
864
    def active(self):
865
        return self.filter(active=True)
866

    
867
    def for_user(self, user, provider):
868
        policies = {}
869
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
870
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
871
        exclusive_q = exclusive_q1 | exclusive_q2
872

    
873
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
874
            policies.update(profile.policies)
875

    
876
        user_groups = user.groups.all().values('pk')
877
        for profile in self.active().filter(groups__in=user_groups).filter(
878
                exclusive_q):
879
            policies.update(profile.policies)
880
        return policies
881

    
882
    def add_policy(self, name, provider, group_or_user, exclusive=False,
883
                   **policies):
884
        is_group = isinstance(group_or_user, Group)
885
        profile, created = self.get_or_create(name=name, provider=provider,
886
                                              is_exclusive=exclusive)
887
        profile.is_exclusive = exclusive
888
        profile.save()
889
        if is_group:
890
            profile.groups.add(group_or_user)
891
        else:
892
            profile.users.add(group_or_user)
893
        profile.set_policies(policies)
894
        profile.save()
895
        return profile
896

    
897

    
898
class AuthProviderPolicyProfile(models.Model):
899
    name = models.CharField(_('Name'), max_length=255, blank=False,
900
                            null=False, db_index=True)
901
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
902
                                null=False)
903

    
904
    # apply policies to all providers excluding the one set in provider field
905
    is_exclusive = models.BooleanField(default=False)
906

    
907
    policy_add = models.NullBooleanField(null=True, default=None)
908
    policy_remove = models.NullBooleanField(null=True, default=None)
909
    policy_create = models.NullBooleanField(null=True, default=None)
910
    policy_login = models.NullBooleanField(null=True, default=None)
911
    policy_limit = models.IntegerField(null=True, default=None)
912
    policy_required = models.NullBooleanField(null=True, default=None)
913
    policy_automoderate = models.NullBooleanField(null=True, default=None)
914
    policy_switch = models.NullBooleanField(null=True, default=None)
915

    
916
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
917
                     'automoderate')
918

    
919
    priority = models.IntegerField(null=False, default=1)
920
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
921
    users = models.ManyToManyField(AstakosUser,
922
                                   related_name='authpolicy_profiles')
923
    active = models.BooleanField(default=True)
924

    
925
    objects = AuthProviderPolicyProfileManager()
926

    
927
    class Meta:
928
        ordering = ['priority']
929

    
930
    @property
931
    def policies(self):
932
        policies = {}
933
        for pkey in self.POLICY_FIELDS:
934
            value = getattr(self, 'policy_%s' % pkey, None)
935
            if value is None:
936
                continue
937
            policies[pkey] = value
938
        return policies
939

    
940
    def set_policies(self, policies_dict):
941
        for key, value in policies_dict.iteritems():
942
            if key in self.POLICY_FIELDS:
943
                setattr(self, 'policy_%s' % key, value)
944
        return self.policies
945

    
946

    
947
class AstakosUserAuthProvider(models.Model):
948
    """
949
    Available user authentication methods.
950
    """
951
    affiliation = models.CharField(_('Affiliation'), max_length=255,
952
                                   blank=True, null=True, default=None)
953
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
954
    module = models.CharField(_('Provider'), max_length=255, blank=False,
955
                              default='local')
956
    identifier = models.CharField(_('Third-party identifier'),
957
                                  max_length=255, null=True,
958
                                  blank=True)
959
    active = models.BooleanField(default=True)
960
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
961
                                    default='astakos')
962
    info_data = models.TextField(default="", null=True, blank=True)
963
    created = models.DateTimeField('Creation date', auto_now_add=True)
964

    
965
    objects = AstakosUserAuthProviderManager()
966

    
967
    class Meta:
968
        unique_together = (('identifier', 'module', 'user'), )
969
        ordering = ('module', 'created')
970

    
971
    def __init__(self, *args, **kwargs):
972
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
973
        try:
974
            self.info = json.loads(self.info_data)
975
            if not self.info:
976
                self.info = {}
977
        except Exception:
978
            self.info = {}
979

    
980
        for key, value in self.info.iteritems():
981
            setattr(self, 'info_%s' % key, value)
982

    
983
    @property
984
    def settings(self):
985
        extra_data = {}
986

    
987
        info_data = {}
988
        if self.info_data:
989
            info_data = json.loads(self.info_data)
990

    
991
        extra_data['info'] = info_data
992

    
993
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
994
            extra_data[key] = getattr(self, key)
995

    
996
        extra_data['instance'] = self
997
        return auth.get_provider(self.module, self.user,
998
                                 self.identifier, **extra_data)
999

    
1000
    def __repr__(self):
1001
        return '<AstakosUserAuthProvider %s:%s>' % (
1002
            self.module, self.identifier)
1003

    
1004
    def __unicode__(self):
1005
        if self.identifier:
1006
            return "%s:%s" % (self.module, self.identifier)
1007
        if self.auth_backend:
1008
            return "%s:%s" % (self.module, self.auth_backend)
1009
        return self.module
1010

    
1011
    def save(self, *args, **kwargs):
1012
        self.info_data = json.dumps(self.info)
1013
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1014

    
1015

    
1016
class AstakosUserQuota(models.Model):
1017
    capacity = models.BigIntegerField()
1018
    resource = models.ForeignKey(Resource)
1019
    user = models.ForeignKey(AstakosUser)
1020

    
1021
    class Meta:
1022
        unique_together = ("resource", "user")
1023

    
1024

    
1025
class ApprovalTerms(models.Model):
1026
    """
1027
    Model for approval terms
1028
    """
1029

    
1030
    date = models.DateTimeField(
1031
        _('Issue date'), db_index=True, auto_now_add=True)
1032
    location = models.CharField(_('Terms location'), max_length=255)
1033

    
1034

    
1035
class Invitation(models.Model):
1036
    """
1037
    Model for registring invitations
1038
    """
1039
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1040
                                null=True)
1041
    realname = models.CharField(_('Real name'), max_length=255)
1042
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1043
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1044
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1045
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1046
    consumed = models.DateTimeField(_('Consumption date'),
1047
                                    null=True, blank=True)
1048

    
1049
    def __init__(self, *args, **kwargs):
1050
        super(Invitation, self).__init__(*args, **kwargs)
1051
        if not self.id:
1052
            self.code = _generate_invitation_code()
1053

    
1054
    def consume(self):
1055
        self.is_consumed = True
1056
        self.consumed = datetime.now()
1057
        self.save()
1058

    
1059
    def __unicode__(self):
1060
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1061

    
1062

    
1063
class EmailChangeManager(models.Manager):
1064

    
1065
    @transaction.commit_on_success
1066
    def change_email(self, activation_key):
1067
        """
1068
        Validate an activation key and change the corresponding
1069
        ``User`` if valid.
1070

1071
        If the key is valid and has not expired, return the ``User``
1072
        after activating.
1073

1074
        If the key is not valid or has expired, return ``None``.
1075

1076
        If the key is valid but the ``User`` is already active,
1077
        return ``None``.
1078

1079
        After successful email change the activation record is deleted.
1080

1081
        Throws ValueError if there is already
1082
        """
1083
        try:
1084
            email_change = self.model.objects.get(
1085
                activation_key=activation_key)
1086
            if email_change.activation_key_expired():
1087
                email_change.delete()
1088
                raise EmailChange.DoesNotExist
1089
            # is there an active user with this address?
1090
            try:
1091
                AstakosUser.objects.get(
1092
                    email__iexact=email_change.new_email_address)
1093
            except AstakosUser.DoesNotExist:
1094
                pass
1095
            else:
1096
                raise ValueError(_('The new email address is reserved.'))
1097
            # update user
1098
            user = AstakosUser.objects.select_for_update().\
1099
                get(pk=email_change.user_id)
1100
            old_email = user.email
1101
            user.set_email(email_change.new_email_address)
1102
            user.save()
1103
            email_change.delete()
1104
            msg = "User %s changed email from %s to %s"
1105
            logger.log(astakos_settings.LOGGING_LEVEL, msg, user.log_display,
1106
                       old_email, user.email)
1107
            return user
1108
        except EmailChange.DoesNotExist:
1109
            raise ValueError(_('Invalid activation key.'))
1110

    
1111

    
1112
class EmailChange(models.Model):
1113
    new_email_address = models.EmailField(
1114
        _(u'new e-mail address'),
1115
        help_text=_('Provide a new email address. Until you verify the new '
1116
                    'address by following the activation link that will be '
1117
                    'sent to it, your old email address will remain active.'))
1118
    user = models.ForeignKey(
1119
        AstakosUser, unique=True, related_name='emailchanges')
1120
    requested_at = models.DateTimeField(auto_now_add=True)
1121
    activation_key = models.CharField(
1122
        max_length=40, unique=True, db_index=True)
1123

    
1124
    objects = EmailChangeManager()
1125

    
1126
    def get_url(self):
1127
        return reverse('email_change_confirm',
1128
                       kwargs={'activation_key': self.activation_key})
1129

    
1130
    def activation_key_expired(self):
1131
        expiration_date = timedelta(
1132
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1133
        return self.requested_at + expiration_date < datetime.now()
1134

    
1135

    
1136
class AdditionalMail(models.Model):
1137
    """
1138
    Model for registring invitations
1139
    """
1140
    owner = models.ForeignKey(AstakosUser)
1141
    email = models.EmailField()
1142

    
1143

    
1144
def _generate_invitation_code():
1145
    while True:
1146
        code = randint(1, 2L ** 63 - 1)
1147
        try:
1148
            Invitation.objects.get(code=code)
1149
            # An invitation with this code already exists, try again
1150
        except Invitation.DoesNotExist:
1151
            return code
1152

    
1153

    
1154
def get_latest_terms():
1155
    try:
1156
        term = ApprovalTerms.objects.order_by('-id')[0]
1157
        return term
1158
    except IndexError:
1159
        pass
1160
    return None
1161

    
1162

    
1163
class PendingThirdPartyUser(models.Model):
1164
    """
1165
    Model for registring successful third party user authentications
1166
    """
1167
    third_party_identifier = models.CharField(
1168
        _('Third-party identifier'), max_length=255, null=True, blank=True)
1169
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1170
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1171
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1172
                                  null=True)
1173
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1174
                                 null=True)
1175
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1176
                                   null=True)
1177
    username = models.CharField(
1178
        _('username'), max_length=30, unique=True,
1179
        help_text=_("Required. 30 characters or fewer. "
1180
                    "Letters, numbers and @/./+/-/_ characters"))
1181
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1182
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1183
    info = models.TextField(default="", null=True, blank=True)
1184

    
1185
    class Meta:
1186
        unique_together = ("provider", "third_party_identifier")
1187

    
1188
    def get_user_instance(self):
1189
        """
1190
        Create a new AstakosUser instance based on details provided when user
1191
        initially signed up.
1192
        """
1193
        d = copy.copy(self.__dict__)
1194
        d.pop('_state', None)
1195
        d.pop('id', None)
1196
        d.pop('token', None)
1197
        d.pop('created', None)
1198
        d.pop('info', None)
1199
        d.pop('affiliation', None)
1200
        d.pop('provider', None)
1201
        d.pop('third_party_identifier', None)
1202
        user = AstakosUser(**d)
1203

    
1204
        return user
1205

    
1206
    @property
1207
    def realname(self):
1208
        return '%s %s' % (self.first_name, self.last_name)
1209

    
1210
    @realname.setter
1211
    def realname(self, value):
1212
        first, last = split_realname(value)
1213
        self.first_name = first
1214
        self.last_name = last
1215

    
1216
    def save(self, *args, **kwargs):
1217
        if not self.id:
1218
            # set username
1219
            while not self.username:
1220
                username = uuid.uuid4().hex[:30]
1221
                try:
1222
                    AstakosUser.objects.get(username=username)
1223
                except AstakosUser.DoesNotExist:
1224
                    self.username = username
1225
        super(PendingThirdPartyUser, self).save(*args, **kwargs)
1226

    
1227
    def generate_token(self):
1228
        self.password = self.third_party_identifier
1229
        self.last_login = datetime.now()
1230
        self.token = default_token_generator.make_token(self)
1231

    
1232
    def existing_user(self):
1233
        return AstakosUser.objects.filter(
1234
            auth_providers__module=self.provider,
1235
            auth_providers__identifier=self.third_party_identifier)
1236

    
1237
    def get_provider(self, user):
1238
        params = {
1239
            'info_data': self.info,
1240
            'affiliation': self.affiliation
1241
        }
1242
        return auth.get_provider(self.provider, user,
1243
                                 self.third_party_identifier, **params)
1244

    
1245

    
1246
class SessionCatalog(models.Model):
1247
    session_key = models.CharField(_('session key'), max_length=40)
1248
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1249

    
1250

    
1251
class UserSetting(models.Model):
1252
    user = models.ForeignKey(AstakosUser)
1253
    setting = models.CharField(max_length=255)
1254
    value = models.IntegerField()
1255

    
1256
    class Meta:
1257
        unique_together = ("user", "setting")
1258

    
1259

    
1260
### PROJECTS ###
1261
################
1262

    
1263
class Chain(models.Model):
1264
    chain = models.AutoField(primary_key=True)
1265

    
1266
    def __unicode__(self):
1267
        return "%s" % (self.chain,)
1268

    
1269

    
1270
def new_chain():
1271
    c = Chain.objects.create()
1272
    return c
1273

    
1274

    
1275
class ProjectApplicationManager(models.Manager):
1276

    
1277
    def pending_per_project(self, projects):
1278
        apps = self.filter(state=self.model.PENDING,
1279
                           chain__in=projects).order_by('chain', '-id')
1280
        checked_chain = None
1281
        projs = {}
1282
        for app in apps:
1283
            chain = app.chain_id
1284
            if chain != checked_chain:
1285
                checked_chain = chain
1286
                projs[chain] = app
1287
        return projs
1288

    
1289

    
1290
class ProjectApplication(models.Model):
1291
    applicant = models.ForeignKey(
1292
        AstakosUser,
1293
        related_name='projects_applied',
1294
        db_index=True)
1295

    
1296
    PENDING = 0
1297
    APPROVED = 1
1298
    REPLACED = 2
1299
    DENIED = 3
1300
    DISMISSED = 4
1301
    CANCELLED = 5
1302

    
1303
    state = models.IntegerField(default=PENDING,
1304
                                db_index=True)
1305
    owner = models.ForeignKey(
1306
        AstakosUser,
1307
        related_name='projects_owned',
1308
        null=True,
1309
        db_index=True)
1310
    chain = models.ForeignKey('Project',
1311
                              related_name='chained_apps',
1312
                              db_column='chain')
1313
    name = models.CharField(max_length=80, null=True)
1314
    homepage = models.URLField(max_length=255, null=True,
1315
                               verify_exists=False)
1316
    description = models.TextField(null=True, blank=True)
1317
    start_date = models.DateTimeField(null=True, blank=True)
1318
    end_date = models.DateTimeField(null=True)
1319
    member_join_policy = models.IntegerField(null=True)
1320
    member_leave_policy = models.IntegerField(null=True)
1321
    limit_on_members_number = models.BigIntegerField(null=True)
1322
    resource_grants = models.ManyToManyField(
1323
        Resource,
1324
        null=True,
1325
        blank=True,
1326
        through='ProjectResourceGrant')
1327
    comments = models.TextField(null=True, blank=True)
1328
    issue_date = models.DateTimeField(auto_now_add=True)
1329
    response_date = models.DateTimeField(null=True, blank=True)
1330
    response = models.TextField(null=True, blank=True)
1331
    response_actor = models.ForeignKey(AstakosUser, null=True,
1332
                                       related_name='responded_apps')
1333
    waive_date = models.DateTimeField(null=True, blank=True)
1334
    waive_reason = models.TextField(null=True, blank=True)
1335
    waive_actor = models.ForeignKey(AstakosUser, null=True,
1336
                                    related_name='waived_apps')
1337
    private = models.NullBooleanField(default=False)
1338

    
1339
    objects = ProjectApplicationManager()
1340

    
1341
    # Compiled queries
1342
    Q_PENDING = Q(state=PENDING)
1343
    Q_APPROVED = Q(state=APPROVED)
1344
    Q_DENIED = Q(state=DENIED)
1345

    
1346
    class Meta:
1347
        unique_together = ("chain", "id")
1348

    
1349
    def __unicode__(self):
1350
        return "%s applied by %s" % (self.name, self.applicant)
1351

    
1352
    # TODO: Move to a more suitable place
1353
    APPLICATION_STATE_DISPLAY = {
1354
        PENDING:   _('Pending review'),
1355
        APPROVED:  _('Approved'),
1356
        REPLACED:  _('Replaced'),
1357
        DENIED:    _('Denied'),
1358
        DISMISSED: _('Dismissed'),
1359
        CANCELLED: _('Cancelled')
1360
    }
1361

    
1362
    @property
1363
    def log_display(self):
1364
        return "application %s (%s) for project %s" % (
1365
            self.id, self.name, self.chain)
1366

    
1367
    def state_display(self):
1368
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1369

    
1370
    @property
1371
    def resource_set(self):
1372
        return self.projectresourcegrant_set.order_by('resource__name')
1373

    
1374
    @property
1375
    def resource_policies(self):
1376
        return [unicode(rp) for rp in self.projectresourcegrant_set.all()]
1377

    
1378
    def is_modification(self):
1379
        # if self.state != self.PENDING:
1380
        #     return False
1381
        parents = self.chained_applications().filter(id__lt=self.id)
1382
        parents = parents.filter(state__in=[self.APPROVED])
1383
        return parents.count() > 0
1384

    
1385
    def chained_applications(self):
1386
        return ProjectApplication.objects.filter(chain=self.chain)
1387

    
1388
    def denied_modifications(self):
1389
        q = self.chained_applications()
1390
        q = q.filter(Q(state=self.DENIED))
1391
        q = q.filter(~Q(id=self.id))
1392
        return q
1393

    
1394
    def last_denied(self):
1395
        try:
1396
            return self.denied_modifications().order_by('-id')[0]
1397
        except IndexError:
1398
            return None
1399

    
1400
    def has_denied_modifications(self):
1401
        return bool(self.last_denied())
1402

    
1403
    def can_cancel(self):
1404
        return self.state == self.PENDING
1405

    
1406
    def cancel(self, actor=None, reason=None):
1407
        if not self.can_cancel():
1408
            m = _("cannot cancel: application '%s' in state '%s'") % (
1409
                self.id, self.state)
1410
            raise AssertionError(m)
1411

    
1412
        self.state = self.CANCELLED
1413
        self.waive_date = datetime.now()
1414
        self.waive_reason = reason
1415
        self.waive_actor = actor
1416
        self.save()
1417

    
1418
    def can_dismiss(self):
1419
        return self.state == self.DENIED
1420

    
1421
    def dismiss(self, actor=None, reason=None):
1422
        if not self.can_dismiss():
1423
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1424
                self.id, self.state)
1425
            raise AssertionError(m)
1426

    
1427
        self.state = self.DISMISSED
1428
        self.waive_date = datetime.now()
1429
        self.waive_reason = reason
1430
        self.waive_actor = actor
1431
        self.save()
1432

    
1433
    def can_deny(self):
1434
        return self.state == self.PENDING
1435

    
1436
    def deny(self, actor=None, reason=None):
1437
        if not self.can_deny():
1438
            m = _("cannot deny: application '%s' in state '%s'") % (
1439
                self.id, self.state)
1440
            raise AssertionError(m)
1441

    
1442
        self.state = self.DENIED
1443
        self.response_date = datetime.now()
1444
        self.response = reason
1445
        self.response_actor = actor
1446
        self.save()
1447

    
1448
    def can_approve(self):
1449
        return self.state == self.PENDING
1450

    
1451
    def approve(self, actor=None, reason=None):
1452
        if not self.can_approve():
1453
            m = _("cannot approve: project '%s' in state '%s'") % (
1454
                self.name, self.state)
1455
            raise AssertionError(m)  # invalid argument
1456

    
1457
        now = datetime.now()
1458
        self.state = self.APPROVED
1459
        self.response_date = now
1460
        self.response = reason
1461
        self.response_actor = actor
1462
        self.save()
1463

    
1464
    @property
1465
    def member_join_policy_display(self):
1466
        policy = self.member_join_policy
1467
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1468

    
1469
    @property
1470
    def member_leave_policy_display(self):
1471
        policy = self.member_leave_policy
1472
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1473

    
1474

    
1475
class ProjectResourceGrantManager(models.Manager):
1476
    def grants_per_app(self, applications):
1477
        app_ids = [app.id for app in applications]
1478
        grants = self.filter(
1479
            project_application__in=app_ids).select_related("resource")
1480
        return _partition_by(lambda g: g.project_application_id, grants)
1481

    
1482

    
1483
class ProjectResourceGrant(models.Model):
1484

    
1485
    resource = models.ForeignKey(Resource)
1486
    project_application = models.ForeignKey(ProjectApplication)
1487
    project_capacity = models.BigIntegerField()
1488
    member_capacity = models.BigIntegerField()
1489

    
1490
    objects = ProjectResourceGrantManager()
1491

    
1492
    class Meta:
1493
        unique_together = ("resource", "project_application")
1494

    
1495
    def display_member_capacity(self):
1496
        return units.show(self.member_capacity, self.resource.unit)
1497

    
1498
    def display_project_capacity(self):
1499
        return units.show(self.project_capacity, self.resource.unit)
1500

    
1501
    def project_diffs(self):
1502
        project = self.project_application.chain
1503
        try:
1504
            project_resource = project.resource_set.get(resource=self.resource)
1505
        except ProjectResourceQuota.DoesNotExist:
1506
            return [self.project_capacity, self.member_capacity]
1507

    
1508
        project_diff = \
1509
                self.project_capacity - project_resource.project_capacity
1510
        member_diff = self.member_capacity - project_resource.member_capacity
1511
        return [project_diff, member_diff]
1512

    
1513
    def display_project_diff(self):
1514
        proj, member = self.project_diffs()
1515
        proj_abs, member_abs = abs(proj), abs(member)
1516
        unit = self.resource.unit
1517

    
1518
        def disp(v):
1519
            sign = u'+' if v >= 0 else u'-'
1520
            return sign + unicode(units.show(v, unit))
1521
        return map(disp, [proj_abs, member_abs])
1522

    
1523
    def __unicode__(self):
1524
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1525
                                        self.display_member_capacity())
1526

    
1527

    
1528
class ProjectManager(models.Manager):
1529
    def expired_projects(self):
1530
        model = self.model
1531
        q = (Q(state__in=[model.NORMAL, model.SUSPENDED]) &
1532
             Q(end_date__lt=datetime.now()))
1533
        return self.filter(q)
1534

    
1535
    def user_accessible_projects(self, user):
1536
        """
1537
        Return projects accessible by specified user.
1538
        """
1539
        model = self.model
1540
        if user.is_project_admin():
1541
            flt = Q()
1542
        else:
1543
            membs = user.projectmembership_set.associated()
1544
            memb_projects = membs.values_list("project", flat=True)
1545
            flt = (Q(owner=user) |
1546
                   Q(last_application__applicant=user) |
1547
                   Q(id__in=memb_projects))
1548

    
1549
        relevant = ~Q(state=model.DELETED)
1550
        return self.filter(flt, relevant).order_by(
1551
            'creation_date').select_related('last_application', 'owner')
1552

    
1553
    def search_by_name(self, *search_strings):
1554
        q = Q()
1555
        for s in search_strings:
1556
            q = q | Q(name__icontains=s)
1557
        return self.filter(q)
1558

    
1559
    def initialized(self, flt=None):
1560
        q = Q(state__in=self.model.INITIALIZED_STATES)
1561
        if flt is not None:
1562
            q &= flt
1563
        return self.filter(q)
1564

    
1565

    
1566
class Project(models.Model):
1567

    
1568
    id = models.BigIntegerField(db_column='id', primary_key=True)
1569

    
1570
    last_application = models.ForeignKey(ProjectApplication, null=True,
1571
                                         related_name='last_of_project')
1572

    
1573
    members = models.ManyToManyField(
1574
        AstakosUser,
1575
        through='ProjectMembership')
1576

    
1577
    creation_date = models.DateTimeField(auto_now_add=True)
1578
    name = models.CharField(
1579
        max_length=80,
1580
        null=True,
1581
        db_index=True,
1582
        unique=True)
1583

    
1584
    UNINITIALIZED = 0
1585
    NORMAL = 1
1586
    SUSPENDED = 10
1587
    TERMINATED = 100
1588
    DELETED = 1000
1589

    
1590
    INITIALIZED_STATES = [NORMAL,
1591
                          SUSPENDED,
1592
                          TERMINATED,
1593
                          ]
1594

    
1595
    ALIVE_STATES = [NORMAL,
1596
                    SUSPENDED,
1597
                    ]
1598

    
1599
    SKIP_STATES = [DELETED,
1600
                   TERMINATED,
1601
                   ]
1602

    
1603
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1604

    
1605
    state = models.IntegerField(default=UNINITIALIZED,
1606
                                db_index=True)
1607
    uuid = models.CharField(max_length=255, unique=True)
1608

    
1609
    owner = models.ForeignKey(
1610
        AstakosUser,
1611
        related_name='projs_owned',
1612
        null=True,
1613
        db_index=True)
1614
    realname = models.CharField(max_length=80)
1615
    homepage = models.URLField(max_length=255, verify_exists=False)
1616
    description = models.TextField(blank=True)
1617
    end_date = models.DateTimeField()
1618
    member_join_policy = models.IntegerField()
1619
    member_leave_policy = models.IntegerField()
1620
    limit_on_members_number = models.BigIntegerField()
1621
    resource_grants = models.ManyToManyField(
1622
        Resource,
1623
        null=True,
1624
        blank=True,
1625
        through='ProjectResourceQuota')
1626
    private = models.BooleanField(default=False)
1627
    is_base = models.BooleanField(default=False)
1628

    
1629
    objects = ProjectManager()
1630

    
1631
    def __unicode__(self):
1632
        return _("<project %s '%s'>") % (self.id, self.realname)
1633

    
1634
    O_UNINITIALIZED = -1
1635
    O_PENDING = 0
1636
    O_ACTIVE = 1
1637
    O_ACTIVE_PENDING = 2
1638
    O_DENIED = 3
1639
    O_DISMISSED = 4
1640
    O_CANCELLED = 5
1641
    O_SUSPENDED = 10
1642
    O_TERMINATED = 100
1643
    O_DELETED = 1000
1644

    
1645
    O_STATE_DISPLAY = {
1646
        O_UNINITIALIZED: _("Uninitialized"),
1647
        O_PENDING:    _("Pending"),
1648
        O_ACTIVE:     _("Active"),
1649
        O_DENIED:     _("Denied"),
1650
        O_DISMISSED:  _("Dismissed"),
1651
        O_CANCELLED:  _("Cancelled"),
1652
        O_SUSPENDED:  _("Suspended"),
1653
        O_TERMINATED: _("Terminated"),
1654
        O_DELETED:    _("Deleted"),
1655
    }
1656

    
1657
    O_STATE_UNINITIALIZED = {
1658
        None: O_UNINITIALIZED,
1659
        ProjectApplication.PENDING: O_PENDING,
1660
        ProjectApplication.DENIED:  O_DENIED,
1661
        }
1662
    O_STATE_DELETED = {
1663
        None: O_DELETED,
1664
        ProjectApplication.DISMISSED: O_DISMISSED,
1665
        ProjectApplication.CANCELLED: O_CANCELLED,
1666
        }
1667

    
1668
    OVERALL_STATE = {
1669
        NORMAL: lambda app_state: Project.O_ACTIVE,
1670
        UNINITIALIZED: lambda app_state: Project.O_STATE_UNINITIALIZED.get(
1671
            app_state, None),
1672
        DELETED: lambda app_state: Project.O_STATE_DELETED.get(
1673
            app_state, None),
1674
        SUSPENDED: lambda app_state: Project.O_SUSPENDED,
1675
        TERMINATED: lambda app_state: Project.O_TERMINATED,
1676
        }
1677

    
1678
    @classmethod
1679
    def _overall_state(cls, project_state, app_state):
1680
        os = cls.OVERALL_STATE.get(project_state, None)
1681
        if os is None:
1682
            return None
1683
        return os(app_state)
1684

    
1685
    def overall_state(self):
1686
        app_state = (self.last_application.state
1687
                     if self.last_application else None)
1688
        return self._overall_state(self.state, app_state)
1689

    
1690
    def last_pending_application(self):
1691
        app = self.last_application
1692
        if app and app.state == ProjectApplication.PENDING:
1693
            return app
1694
        return None
1695

    
1696
    def last_pending_modification(self):
1697
        last_pending = self.last_pending_application()
1698
        if self.state != Project.UNINITIALIZED:
1699
            return last_pending
1700
        return None
1701

    
1702
    def state_display(self):
1703
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1704

    
1705
    def expiration_info(self):
1706
        return (unicode(self.id), self.name, self.state_display(),
1707
                unicode(self.end_date))
1708

    
1709
    def last_deactivation(self):
1710
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1711
        ls = objs.order_by("-date")
1712
        if not ls:
1713
            return None
1714
        return ls[0]
1715

    
1716
    def is_deactivated(self, reason=None):
1717
        if reason is not None:
1718
            return self.state == reason
1719

    
1720
        return self.state != self.NORMAL
1721

    
1722
    def is_active(self):
1723
        return self.state == self.NORMAL
1724

    
1725
    def is_initialized(self):
1726
        return self.state in self.INITIALIZED_STATES
1727

    
1728
    ### Deactivation calls
1729

    
1730
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1731
                    comments=None):
1732
        now = datetime.now()
1733
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1734
                        actor=actor, reason=reason, comments=comments)
1735

    
1736
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1737
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1738
                         comments=comments)
1739
        self.state = to_state
1740
        self.save()
1741

    
1742
    def terminate(self, actor=None, reason=None):
1743
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1744
        self.name = None
1745
        self.save()
1746

    
1747
    def suspend(self, actor=None, reason=None):
1748
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1749

    
1750
    def resume(self, actor=None, reason=None):
1751
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1752
        if self.name is None:
1753
            self.name = self.realname
1754
            self.save()
1755

    
1756
    def activate(self, actor=None, reason=None):
1757
        assert self.state != self.DELETED, \
1758
            "cannot activate: %s is deleted" % self
1759
        if self.state != self.NORMAL:
1760
            self.set_state(self.NORMAL, actor=actor, reason=reason)
1761
        if self.name != self.realname:
1762
            self.name = self.realname
1763
            self.save()
1764

    
1765
    def set_deleted(self, actor=None, reason=None):
1766
        self.set_state(self.DELETED, actor=actor, reason=reason)
1767

    
1768
    def can_modify(self):
1769
        return self.state not in [self.UNINITIALIZED, self.DELETED]
1770

    
1771
    ### Logical checks
1772
    @property
1773
    def is_alive(self):
1774
        return self.state in [self.NORMAL, self.SUSPENDED]
1775

    
1776
    @property
1777
    def is_terminated(self):
1778
        return self.is_deactivated(self.TERMINATED)
1779

    
1780
    @property
1781
    def is_suspended(self):
1782
        return self.is_deactivated(self.SUSPENDED)
1783

    
1784
    def violates_members_limit(self, adding=0):
1785
        limit = self.limit_on_members_number
1786
        return (len(self.approved_members) + adding > limit)
1787

    
1788
    ### Other
1789

    
1790
    def count_pending_memberships(self):
1791
        return self.projectmembership_set.requested().count()
1792

    
1793
    def members_count(self):
1794
        return self.approved_memberships.count()
1795

    
1796
    @property
1797
    def approved_memberships(self):
1798
        query = ProjectMembership.Q_ACCEPTED_STATES
1799
        return self.projectmembership_set.filter(query)
1800

    
1801
    @property
1802
    def approved_members(self):
1803
        return [m.person for m in self.approved_memberships]
1804

    
1805
    @property
1806
    def member_join_policy_display(self):
1807
        policy = self.member_join_policy
1808
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1809

    
1810
    @property
1811
    def member_leave_policy_display(self):
1812
        policy = self.member_leave_policy
1813
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1814

    
1815
    @property
1816
    def resource_set(self):
1817
        return self.projectresourcequota_set.order_by('resource__name')
1818

    
1819

    
1820
def create_project(**kwargs):
1821
    if "uuid" not in kwargs:
1822
        kwargs["uuid"] = str(uuid.uuid4())
1823
    return Project.objects.create(**kwargs)
1824

    
1825

    
1826
class ProjectResourceQuotaManager(models.Manager):
1827
    def quotas_per_project(self, projects):
1828
        proj_ids = [proj.id for proj in projects]
1829
        quotas = self.filter(
1830
            project__in=proj_ids).select_related("resource")
1831
        return _partition_by(lambda g: g.project_id, quotas)
1832

    
1833

    
1834
class ProjectResourceQuota(models.Model):
1835

    
1836
    resource = models.ForeignKey(Resource)
1837
    project = models.ForeignKey(Project)
1838
    project_capacity = models.BigIntegerField(default=0)
1839
    member_capacity = models.BigIntegerField(default=0)
1840

    
1841
    objects = ProjectResourceQuotaManager()
1842

    
1843
    class Meta:
1844
        unique_together = ("resource", "project")
1845

    
1846
    def display_member_capacity(self):
1847
        return units.show(self.member_capacity, self.resource.unit)
1848

    
1849
    def display_project_capacity(self):
1850
        return units.show(self.project_capacity, self.resource.unit)
1851

    
1852

    
1853
class ProjectLogManager(models.Manager):
1854
    def last_deactivations(self, projects):
1855
        logs = self.filter(
1856
            project__in=projects,
1857
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1858
        return first_of_group(lambda l: l.project_id, logs)
1859

    
1860

    
1861
class ProjectLog(models.Model):
1862
    project = models.ForeignKey(Project, related_name="log")
1863
    from_state = models.IntegerField(null=True)
1864
    to_state = models.IntegerField()
1865
    date = models.DateTimeField()
1866
    actor = models.ForeignKey(AstakosUser, null=True)
1867
    reason = models.TextField(null=True)
1868
    comments = models.TextField(null=True)
1869

    
1870
    objects = ProjectLogManager()
1871

    
1872

    
1873
class ProjectLock(models.Model):
1874
    pass
1875

    
1876

    
1877
class ProjectMembershipManager(models.Manager):
1878

    
1879
    def any_accepted(self):
1880
        q = self.model.Q_ACCEPTED_STATES
1881
        return self.filter(q)
1882

    
1883
    def actually_accepted(self, projects=None):
1884
        q = self.model.Q_ACTUALLY_ACCEPTED
1885
        if projects is not None:
1886
            q &= Q(project__in=projects)
1887
        return self.filter(q)
1888

    
1889
    def actually_accepted_and_active(self):
1890
        q = self.model.Q_ACTUALLY_ACCEPTED
1891
        q &= Q(project__state=Project.NORMAL)
1892
        return self.filter(q)
1893

    
1894
    def initialized(self, projects=None):
1895
        q = Q(initialized=True)
1896
        if projects is not None:
1897
            q &= Q(project__in=projects)
1898
        return self.filter(q)
1899

    
1900
    def requested(self):
1901
        return self.filter(state=ProjectMembership.REQUESTED)
1902

    
1903
    def suspended(self):
1904
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1905

    
1906
    def associated(self):
1907
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1908

    
1909
    def any_accepted_per_project(self, projects):
1910
        ms = self.any_accepted().filter(project__in=projects)
1911
        return _partition_by(lambda m: m.project_id, ms)
1912

    
1913
    def requested_per_project(self, projects):
1914
        ms = self.requested().filter(project__in=projects)
1915
        return _partition_by(lambda m: m.project_id, ms)
1916

    
1917
    def one_per_project(self):
1918
        ms = self.all().select_related(
1919
            'project', 'project__application',
1920
            'project__application__owner', 'project_application__applicant',
1921
            'person')
1922
        m_per_p = {}
1923
        for m in ms:
1924
            m_per_p[m.project_id] = m
1925
        return m_per_p
1926

    
1927

    
1928
class ProjectMembership(models.Model):
1929

    
1930
    person = models.ForeignKey(AstakosUser)
1931
    project = models.ForeignKey(Project)
1932

    
1933
    REQUESTED = 0
1934
    ACCEPTED = 1
1935
    LEAVE_REQUESTED = 5
1936
    # User deactivation
1937
    USER_SUSPENDED = 10
1938
    REJECTED = 100
1939
    CANCELLED = 101
1940
    REMOVED = 200
1941

    
1942
    ASSOCIATED_STATES = set([REQUESTED,
1943
                             ACCEPTED,
1944
                             LEAVE_REQUESTED,
1945
                             USER_SUSPENDED,
1946
                             ])
1947

    
1948
    ACCEPTED_STATES = set([ACCEPTED,
1949
                           LEAVE_REQUESTED,
1950
                           USER_SUSPENDED,
1951
                           ])
1952

    
1953
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1954

    
1955
    state = models.IntegerField(default=REQUESTED,
1956
                                db_index=True)
1957

    
1958
    initialized = models.BooleanField(default=False)
1959
    objects = ProjectMembershipManager()
1960

    
1961
    # Compiled queries
1962
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1963
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1964

    
1965
    MEMBERSHIP_STATE_DISPLAY = {
1966
        REQUESTED:       _('Requested'),
1967
        ACCEPTED:        _('Accepted'),
1968
        LEAVE_REQUESTED: _('Leave Requested'),
1969
        USER_SUSPENDED:  _('Suspended'),
1970
        REJECTED:        _('Rejected'),
1971
        CANCELLED:       _('Cancelled'),
1972
        REMOVED:         _('Removed'),
1973
    }
1974

    
1975
    USER_FRIENDLY_STATE_DISPLAY = {
1976
        REQUESTED:       _('Join requested'),
1977
        ACCEPTED:        _('Accepted member'),
1978
        LEAVE_REQUESTED: _('Requested to leave'),
1979
        USER_SUSPENDED:  _('Suspended member'),
1980
        REJECTED:        _('Request rejected'),
1981
        CANCELLED:       _('Request cancelled'),
1982
        REMOVED:         _('Removed member'),
1983
    }
1984

    
1985
    def state_display(self):
1986
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1987

    
1988
    def user_friendly_state_display(self):
1989
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1990

    
1991
    class Meta:
1992
        unique_together = ("person", "project")
1993
        #index_together = [["project", "state"]]
1994

    
1995
    def __unicode__(self):
1996
        return (_("<'%s' membership in '%s'>") %
1997
                (self.person.username, self.project))
1998

    
1999
    def latest_log(self):
2000
        logs = self.log.all()
2001
        logs_d = _partition_by(lambda l: l.to_state, logs)
2002
        for s, s_logs in logs_d.iteritems():
2003
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
2004
        return logs_d
2005

    
2006
    def _log_create(self, from_state, to_state, actor=None, reason=None,
2007
                    comments=None):
2008
        now = datetime.now()
2009
        self.log.create(from_state=from_state, to_state=to_state, date=now,
2010
                        actor=actor, reason=reason, comments=comments)
2011

    
2012
    def set_state(self, to_state, actor=None, reason=None, comments=None):
2013
        self._log_create(self.state, to_state, actor=actor, reason=reason,
2014
                         comments=comments)
2015
        self.state = to_state
2016
        self.save()
2017

    
2018
    def is_active(self):
2019
        return (self.project.state == Project.NORMAL and
2020
                self.state in self.ACTUALLY_ACCEPTED)
2021

    
2022
    ACTION_CHECKS = {
2023
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
2024
        "accept": lambda m: m.state == m.REQUESTED,
2025
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
2026
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
2027
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
2028
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
2029
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
2030
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
2031
        "reject": lambda m: m.state == m.REQUESTED,
2032
        "cancel": lambda m: m.state == m.REQUESTED,
2033
    }
2034

    
2035
    ACTION_STATES = {
2036
        "join":          REQUESTED,
2037
        "accept":        ACCEPTED,
2038
        "enroll":        ACCEPTED,
2039
        "leave_request": LEAVE_REQUESTED,
2040
        "deny_leave":    ACCEPTED,
2041
        "cancel_leave":  ACCEPTED,
2042
        "remove":        REMOVED,
2043
        "reject":        REJECTED,
2044
        "cancel":        CANCELLED,
2045
    }
2046

    
2047
    def check_action(self, action):
2048
        try:
2049
            check = self.ACTION_CHECKS[action]
2050
        except KeyError:
2051
            raise ValueError("No check found for action '%s'" % action)
2052
        return check(self)
2053

    
2054
    def perform_action(self, action, actor=None, reason=None):
2055
        if not self.check_action(action):
2056
            m = _("%s: attempted action '%s' in state '%s'") % (
2057
                self, action, self.state)
2058
            raise AssertionError(m)
2059
        try:
2060
            s = self.ACTION_STATES[action]
2061
        except KeyError:
2062
            raise ValueError("No such action '%s'" % action)
2063
        if action == "accept":
2064
            self.initialized = True
2065
        return self.set_state(s, actor=actor, reason=reason)
2066

    
2067

    
2068
class ProjectMembershipLogManager(models.Manager):
2069
    def last_logs(self, memberships):
2070
        logs = self.filter(membership__in=memberships).order_by("-date")
2071
        logs = _partition_by(lambda l: l.membership_id, logs)
2072

    
2073
        for memb_id, m_logs in logs.iteritems():
2074
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
2075
        return logs
2076

    
2077

    
2078
class ProjectMembershipLog(models.Model):
2079
    membership = models.ForeignKey(ProjectMembership, related_name="log")
2080
    from_state = models.IntegerField(null=True)
2081
    to_state = models.IntegerField()
2082
    date = models.DateTimeField()
2083
    actor = models.ForeignKey(AstakosUser, null=True)
2084
    reason = models.TextField(null=True)
2085
    comments = models.TextField(null=True)
2086

    
2087
    objects = ProjectMembershipLogManager()
2088

    
2089

    
2090
### SIGNALS ###
2091
################
2092

    
2093
def resource_post_save(sender, instance, created, **kwargs):
2094
    pass
2095

    
2096
post_save.connect(resource_post_save, sender=Resource)
2097

    
2098

    
2099
def renew_token(sender, instance, **kwargs):
2100
    if not instance.auth_token:
2101
        instance.renew_token()
2102
pre_save.connect(renew_token, sender=Component)