Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (69.7 kB)

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

    
34
import 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.text import uenc, udec
68
from synnefo.util import units
69
from astakos.im import presentation
70

    
71
logger = logging.getLogger(__name__)
72

    
73
DEFAULT_CONTENT_TYPE = None
74
_content_type = None
75

    
76

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

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

    
90
inf = float('inf')
91

    
92

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

    
97

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

    
107

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

    
119

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

    
133
    def renew_token(self, expiration_date=None):
134
        for i in range(10):
135
            new_token = generate_token()
136
            count = Component.objects.filter(auth_token=new_token).count()
137
            if count == 0:
138
                break
139
            continue
140
        else:
141
            raise ValueError('Could not generate a token')
142

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

    
152
    def __str__(self):
153
        return self.name
154

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

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

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

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

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

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

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

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

    
196

    
197
_presentation_data = {}
198

    
199

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

    
209

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

    
215

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

    
219

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

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

    
228

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

    
240
    def __str__(self):
241
        return self.name
242

    
243
    def full_name(self):
244
        return str(self)
245

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

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

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

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

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

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

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

    
282
    @property
283
    def verbose_name(self):
284
        return get_presentation(str(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 __str__(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 [str(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 __str__(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 __str__(self):
1632
        return uenc(_("<project %s '%s'>") %
1633
                    (self.id, udec(self.realname)))
1634

    
1635
    __repr__ = __str__
1636

    
1637
    def __unicode__(self):
1638
        return _("<project %s '%s'>") % (self.id, self.realname)
1639

    
1640
    O_UNINITIALIZED = -1
1641
    O_PENDING = 0
1642
    O_ACTIVE = 1
1643
    O_ACTIVE_PENDING = 2
1644
    O_DENIED = 3
1645
    O_DISMISSED = 4
1646
    O_CANCELLED = 5
1647
    O_SUSPENDED = 10
1648
    O_TERMINATED = 100
1649
    O_DELETED = 1000
1650

    
1651
    O_STATE_DISPLAY = {
1652
        O_UNINITIALIZED: _("Uninitialized"),
1653
        O_PENDING:    _("Pending"),
1654
        O_ACTIVE:     _("Active"),
1655
        O_DENIED:     _("Denied"),
1656
        O_DISMISSED:  _("Dismissed"),
1657
        O_CANCELLED:  _("Cancelled"),
1658
        O_SUSPENDED:  _("Suspended"),
1659
        O_TERMINATED: _("Terminated"),
1660
        O_DELETED:    _("Deleted"),
1661
    }
1662

    
1663
    O_STATE_UNINITIALIZED = {
1664
        None: O_UNINITIALIZED,
1665
        ProjectApplication.PENDING: O_PENDING,
1666
        ProjectApplication.DENIED:  O_DENIED,
1667
        }
1668
    O_STATE_DELETED = {
1669
        None: O_DELETED,
1670
        ProjectApplication.DISMISSED: O_DISMISSED,
1671
        ProjectApplication.CANCELLED: O_CANCELLED,
1672
        }
1673

    
1674
    OVERALL_STATE = {
1675
        NORMAL: lambda app_state: Project.O_ACTIVE,
1676
        UNINITIALIZED: lambda app_state: Project.O_STATE_UNINITIALIZED.get(
1677
            app_state, None),
1678
        DELETED: lambda app_state: Project.O_STATE_DELETED.get(
1679
            app_state, None),
1680
        SUSPENDED: lambda app_state: Project.O_SUSPENDED,
1681
        TERMINATED: lambda app_state: Project.O_TERMINATED,
1682
        }
1683

    
1684
    @classmethod
1685
    def _overall_state(cls, project_state, app_state):
1686
        os = cls.OVERALL_STATE.get(project_state, None)
1687
        if os is None:
1688
            return None
1689
        return os(app_state)
1690

    
1691
    def overall_state(self):
1692
        app_state = (self.last_application.state
1693
                     if self.last_application else None)
1694
        return self._overall_state(self.state, app_state)
1695

    
1696
    def last_pending_application(self):
1697
        app = self.last_application
1698
        if app and app.state == ProjectApplication.PENDING:
1699
            return app
1700
        return None
1701

    
1702
    def last_pending_modification(self):
1703
        last_pending = self.last_pending_application()
1704
        if self.state != Project.UNINITIALIZED:
1705
            return last_pending
1706
        return None
1707

    
1708
    def state_display(self):
1709
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1710

    
1711
    def expiration_info(self):
1712
        return (str(self.id), self.name, self.state_display(),
1713
                str(self.end_date))
1714

    
1715
    def last_deactivation(self):
1716
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1717
        ls = objs.order_by("-date")
1718
        if not ls:
1719
            return None
1720
        return ls[0]
1721

    
1722
    def is_deactivated(self, reason=None):
1723
        if reason is not None:
1724
            return self.state == reason
1725

    
1726
        return self.state != self.NORMAL
1727

    
1728
    def is_active(self):
1729
        return self.state == self.NORMAL
1730

    
1731
    def is_initialized(self):
1732
        return self.state in self.INITIALIZED_STATES
1733

    
1734
    ### Deactivation calls
1735

    
1736
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1737
                    comments=None):
1738
        now = datetime.now()
1739
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1740
                        actor=actor, reason=reason, comments=comments)
1741

    
1742
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1743
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1744
                         comments=comments)
1745
        self.state = to_state
1746
        self.save()
1747

    
1748
    def terminate(self, actor=None, reason=None):
1749
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1750
        self.name = None
1751
        self.save()
1752

    
1753
    def suspend(self, actor=None, reason=None):
1754
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1755

    
1756
    def resume(self, actor=None, reason=None):
1757
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1758
        if self.name is None:
1759
            self.name = self.realname
1760
            self.save()
1761

    
1762
    def activate(self, actor=None, reason=None):
1763
        assert self.state != self.DELETED, \
1764
            "cannot activate: %s is deleted" % self
1765
        if self.state != self.NORMAL:
1766
            self.set_state(self.NORMAL, actor=actor, reason=reason)
1767
        if self.name != self.realname:
1768
            self.name = self.realname
1769
            self.save()
1770

    
1771
    def set_deleted(self, actor=None, reason=None):
1772
        self.set_state(self.DELETED, actor=actor, reason=reason)
1773

    
1774
    def can_modify(self):
1775
        return self.state not in [self.UNINITIALIZED, self.DELETED]
1776

    
1777
    ### Logical checks
1778
    @property
1779
    def is_alive(self):
1780
        return self.state in [self.NORMAL, self.SUSPENDED]
1781

    
1782
    @property
1783
    def is_terminated(self):
1784
        return self.is_deactivated(self.TERMINATED)
1785

    
1786
    @property
1787
    def is_suspended(self):
1788
        return self.is_deactivated(self.SUSPENDED)
1789

    
1790
    def violates_members_limit(self, adding=0):
1791
        limit = self.limit_on_members_number
1792
        return (len(self.approved_members) + adding > limit)
1793

    
1794
    ### Other
1795

    
1796
    def count_pending_memberships(self):
1797
        return self.projectmembership_set.requested().count()
1798

    
1799
    def members_count(self):
1800
        return self.approved_memberships.count()
1801

    
1802
    @property
1803
    def approved_memberships(self):
1804
        query = ProjectMembership.Q_ACCEPTED_STATES
1805
        return self.projectmembership_set.filter(query)
1806

    
1807
    @property
1808
    def approved_members(self):
1809
        return [m.person for m in self.approved_memberships]
1810

    
1811
    @property
1812
    def member_join_policy_display(self):
1813
        policy = self.member_join_policy
1814
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1815

    
1816
    @property
1817
    def member_leave_policy_display(self):
1818
        policy = self.member_leave_policy
1819
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1820

    
1821
    @property
1822
    def resource_set(self):
1823
        return self.projectresourcequota_set.order_by('resource__name')
1824

    
1825

    
1826
def create_project(**kwargs):
1827
    if "uuid" not in kwargs:
1828
        kwargs["uuid"] = str(uuid.uuid4())
1829
    return Project.objects.create(**kwargs)
1830

    
1831

    
1832
class ProjectResourceQuotaManager(models.Manager):
1833
    def quotas_per_project(self, projects):
1834
        proj_ids = [proj.id for proj in projects]
1835
        quotas = self.filter(
1836
            project__in=proj_ids).select_related("resource")
1837
        return _partition_by(lambda g: g.project_id, quotas)
1838

    
1839

    
1840
class ProjectResourceQuota(models.Model):
1841

    
1842
    resource = models.ForeignKey(Resource)
1843
    project = models.ForeignKey(Project)
1844
    project_capacity = models.BigIntegerField(default=0)
1845
    member_capacity = models.BigIntegerField(default=0)
1846

    
1847
    objects = ProjectResourceQuotaManager()
1848

    
1849
    class Meta:
1850
        unique_together = ("resource", "project")
1851

    
1852
    def display_member_capacity(self):
1853
        return units.show(self.member_capacity, self.resource.unit)
1854

    
1855
    def display_project_capacity(self):
1856
        return units.show(self.project_capacity, self.resource.unit)
1857

    
1858

    
1859
class ProjectLogManager(models.Manager):
1860
    def last_deactivations(self, projects):
1861
        logs = self.filter(
1862
            project__in=projects,
1863
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1864
        return first_of_group(lambda l: l.project_id, logs)
1865

    
1866

    
1867
class ProjectLog(models.Model):
1868
    project = models.ForeignKey(Project, related_name="log")
1869
    from_state = models.IntegerField(null=True)
1870
    to_state = models.IntegerField()
1871
    date = models.DateTimeField()
1872
    actor = models.ForeignKey(AstakosUser, null=True)
1873
    reason = models.TextField(null=True)
1874
    comments = models.TextField(null=True)
1875

    
1876
    objects = ProjectLogManager()
1877

    
1878

    
1879
class ProjectLock(models.Model):
1880
    pass
1881

    
1882

    
1883
class ProjectMembershipManager(models.Manager):
1884

    
1885
    def any_accepted(self):
1886
        q = self.model.Q_ACCEPTED_STATES
1887
        return self.filter(q)
1888

    
1889
    def actually_accepted(self, projects=None):
1890
        q = self.model.Q_ACTUALLY_ACCEPTED
1891
        if projects is not None:
1892
            q &= Q(project__in=projects)
1893
        return self.filter(q)
1894

    
1895
    def actually_accepted_and_active(self):
1896
        q = self.model.Q_ACTUALLY_ACCEPTED
1897
        q &= Q(project__state=Project.NORMAL)
1898
        return self.filter(q)
1899

    
1900
    def initialized(self, projects=None):
1901
        q = Q(initialized=True)
1902
        if projects is not None:
1903
            q &= Q(project__in=projects)
1904
        return self.filter(q)
1905

    
1906
    def requested(self):
1907
        return self.filter(state=ProjectMembership.REQUESTED)
1908

    
1909
    def suspended(self):
1910
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1911

    
1912
    def associated(self):
1913
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1914

    
1915
    def any_accepted_per_project(self, projects):
1916
        ms = self.any_accepted().filter(project__in=projects)
1917
        return _partition_by(lambda m: m.project_id, ms)
1918

    
1919
    def requested_per_project(self, projects):
1920
        ms = self.requested().filter(project__in=projects)
1921
        return _partition_by(lambda m: m.project_id, ms)
1922

    
1923
    def one_per_project(self):
1924
        ms = self.all().select_related(
1925
            'project', 'project__application',
1926
            'project__application__owner', 'project_application__applicant',
1927
            'person')
1928
        m_per_p = {}
1929
        for m in ms:
1930
            m_per_p[m.project_id] = m
1931
        return m_per_p
1932

    
1933

    
1934
class ProjectMembership(models.Model):
1935

    
1936
    person = models.ForeignKey(AstakosUser)
1937
    project = models.ForeignKey(Project)
1938

    
1939
    REQUESTED = 0
1940
    ACCEPTED = 1
1941
    LEAVE_REQUESTED = 5
1942
    # User deactivation
1943
    USER_SUSPENDED = 10
1944
    REJECTED = 100
1945
    CANCELLED = 101
1946
    REMOVED = 200
1947

    
1948
    ASSOCIATED_STATES = set([REQUESTED,
1949
                             ACCEPTED,
1950
                             LEAVE_REQUESTED,
1951
                             USER_SUSPENDED,
1952
                             ])
1953

    
1954
    ACCEPTED_STATES = set([ACCEPTED,
1955
                           LEAVE_REQUESTED,
1956
                           USER_SUSPENDED,
1957
                           ])
1958

    
1959
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1960

    
1961
    state = models.IntegerField(default=REQUESTED,
1962
                                db_index=True)
1963

    
1964
    initialized = models.BooleanField(default=False)
1965
    objects = ProjectMembershipManager()
1966

    
1967
    # Compiled queries
1968
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1969
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1970

    
1971
    MEMBERSHIP_STATE_DISPLAY = {
1972
        REQUESTED:       _('Requested'),
1973
        ACCEPTED:        _('Accepted'),
1974
        LEAVE_REQUESTED: _('Leave Requested'),
1975
        USER_SUSPENDED:  _('Suspended'),
1976
        REJECTED:        _('Rejected'),
1977
        CANCELLED:       _('Cancelled'),
1978
        REMOVED:         _('Removed'),
1979
    }
1980

    
1981
    USER_FRIENDLY_STATE_DISPLAY = {
1982
        REQUESTED:       _('Join requested'),
1983
        ACCEPTED:        _('Accepted member'),
1984
        LEAVE_REQUESTED: _('Requested to leave'),
1985
        USER_SUSPENDED:  _('Suspended member'),
1986
        REJECTED:        _('Request rejected'),
1987
        CANCELLED:       _('Request cancelled'),
1988
        REMOVED:         _('Removed member'),
1989
    }
1990

    
1991
    def state_display(self):
1992
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1993

    
1994
    def user_friendly_state_display(self):
1995
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1996

    
1997
    class Meta:
1998
        unique_together = ("person", "project")
1999
        #index_together = [["project", "state"]]
2000

    
2001
    def __str__(self):
2002
        return uenc(_("<'%s' membership in '%s'>") %
2003
                    (self.person.username, self.project))
2004

    
2005
    __repr__ = __str__
2006

    
2007
    def latest_log(self):
2008
        logs = self.log.all()
2009
        logs_d = _partition_by(lambda l: l.to_state, logs)
2010
        for s, s_logs in logs_d.iteritems():
2011
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
2012
        return logs_d
2013

    
2014
    def _log_create(self, from_state, to_state, actor=None, reason=None,
2015
                    comments=None):
2016
        now = datetime.now()
2017
        self.log.create(from_state=from_state, to_state=to_state, date=now,
2018
                        actor=actor, reason=reason, comments=comments)
2019

    
2020
    def set_state(self, to_state, actor=None, reason=None, comments=None):
2021
        self._log_create(self.state, to_state, actor=actor, reason=reason,
2022
                         comments=comments)
2023
        self.state = to_state
2024
        self.save()
2025

    
2026
    def is_active(self):
2027
        return (self.project.state == Project.NORMAL and
2028
                self.state in self.ACTUALLY_ACCEPTED)
2029

    
2030
    ACTION_CHECKS = {
2031
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
2032
        "accept": lambda m: m.state == m.REQUESTED,
2033
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
2034
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
2035
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
2036
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
2037
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
2038
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
2039
        "reject": lambda m: m.state == m.REQUESTED,
2040
        "cancel": lambda m: m.state == m.REQUESTED,
2041
    }
2042

    
2043
    ACTION_STATES = {
2044
        "join":          REQUESTED,
2045
        "accept":        ACCEPTED,
2046
        "enroll":        ACCEPTED,
2047
        "leave_request": LEAVE_REQUESTED,
2048
        "deny_leave":    ACCEPTED,
2049
        "cancel_leave":  ACCEPTED,
2050
        "remove":        REMOVED,
2051
        "reject":        REJECTED,
2052
        "cancel":        CANCELLED,
2053
    }
2054

    
2055
    def check_action(self, action):
2056
        try:
2057
            check = self.ACTION_CHECKS[action]
2058
        except KeyError:
2059
            raise ValueError("No check found for action '%s'" % action)
2060
        return check(self)
2061

    
2062
    def perform_action(self, action, actor=None, reason=None):
2063
        if not self.check_action(action):
2064
            m = _("%s: attempted action '%s' in state '%s'") % (
2065
                self, action, self.state)
2066
            raise AssertionError(m)
2067
        try:
2068
            s = self.ACTION_STATES[action]
2069
        except KeyError:
2070
            raise ValueError("No such action '%s'" % action)
2071
        if action == "accept":
2072
            self.initialized = True
2073
        return self.set_state(s, actor=actor, reason=reason)
2074

    
2075

    
2076
class ProjectMembershipLogManager(models.Manager):
2077
    def last_logs(self, memberships):
2078
        logs = self.filter(membership__in=memberships).order_by("-date")
2079
        logs = _partition_by(lambda l: l.membership_id, logs)
2080

    
2081
        for memb_id, m_logs in logs.iteritems():
2082
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
2083
        return logs
2084

    
2085

    
2086
class ProjectMembershipLog(models.Model):
2087
    membership = models.ForeignKey(ProjectMembership, related_name="log")
2088
    from_state = models.IntegerField(null=True)
2089
    to_state = models.IntegerField()
2090
    date = models.DateTimeField()
2091
    actor = models.ForeignKey(AstakosUser, null=True)
2092
    reason = models.TextField(null=True)
2093
    comments = models.TextField(null=True)
2094

    
2095
    objects = ProjectMembershipLogManager()
2096

    
2097

    
2098
### SIGNALS ###
2099
################
2100

    
2101
def resource_post_save(sender, instance, created, **kwargs):
2102
    pass
2103

    
2104
post_save.connect(resource_post_save, sender=Resource)
2105

    
2106

    
2107
def renew_token(sender, instance, **kwargs):
2108
    if not instance.auth_token:
2109
        instance.renew_token()
2110
pre_save.connect(renew_token, sender=Component)