Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (65.9 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
    ui_visible = models.BooleanField(default=True)
237
    api_visible = models.BooleanField(default=True)
238

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

    
242
    def full_name(self):
243
        return str(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(str(self)).get('group', default)
257

    
258
    @property
259
    def help_text(self):
260
        default = "%s resource" % self.name
261
        return get_presentation(str(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(str(self)).get('help_text_input_each', default)
267

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

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

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

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

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

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

    
298

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

    
305

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

    
313

    
314
class AstakosUserManager(UserManager):
315

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

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

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

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

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

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

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

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

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

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

    
377

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
471
    objects = AstakosUserManager()
472

    
473
    @property
474
    def realname(self):
475
        return '%s %s' % (self.first_name, self.last_name)
476

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

    
485
    @realname.setter
486
    def realname(self, value):
487
        first, last = split_realname(value)
488
        self.first_name = first
489
        self.last_name = last
490

    
491
    def add_permission(self, pname):
492
        if self.has_perm(pname):
493
            return
494
        p, created = Permission.objects.get_or_create(
495
            codename=pname,
496
            name=pname.capitalize(),
497
            content_type=get_content_type())
498
        self.user_permissions.add(p)
499

    
500
    def remove_permission(self, pname):
501
        if self.has_perm(pname):
502
            return
503
        p = Permission.objects.get(codename=pname,
504
                                   content_type=get_content_type())
505
        self.user_permissions.remove(p)
506

    
507
    def add_group(self, gname):
508
        group, _ = Group.objects.get_or_create(name=gname)
509
        self.groups.add(group)
510

    
511
    def is_accepted(self):
512
        return self.moderated and not self.is_rejected
513

    
514
    def is_project_admin(self, application_id=None):
515
        return self.uuid in astakos_settings.PROJECT_ADMINS
516

    
517
    @property
518
    def invitation(self):
519
        try:
520
            return Invitation.objects.get(username=self.email)
521
        except Invitation.DoesNotExist:
522
            return None
523

    
524
    @property
525
    def policies(self):
526
        return self.astakosuserquota_set.select_related().all()
527

    
528
    def get_resource_policy(self, resource):
529
        return AstakosUserQuota.objects.select_related("resource").\
530
            get(user=self, resource__name=resource)
531

    
532
    def fix_username(self):
533
        self.username = self.email.lower()
534

    
535
    def set_email(self, email):
536
        self.email = email
537
        self.fix_username()
538

    
539
    def save(self, update_timestamps=True, **kwargs):
540
        if update_timestamps:
541
            self.updated = datetime.now()
542

    
543
        super(AstakosUser, self).save(**kwargs)
544

    
545
    def renew_verification_code(self):
546
        self.verification_code = str(uuid.uuid4())
547
        logger.info("Verification code renewed for %s" % self.log_display)
548

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

    
559
        self.auth_token = new_token
560
        self.auth_token_created = datetime.now()
561
        self.auth_token_expires = self.auth_token_created + \
562
            timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
563
        if flush_sessions:
564
            self.flush_sessions(current_key)
565
        self.delete_online_access_tokens()
566
        msg = 'Token renewed for %s'
567
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
568

    
569
    def token_expired(self):
570
        return self.auth_token_expires < datetime.now()
571

    
572
    def flush_sessions(self, current_key=None):
573
        q = self.sessions
574
        if current_key:
575
            q = q.exclude(session_key=current_key)
576

    
577
        keys = q.values_list('session_key', flat=True)
578
        if keys:
579
            msg = 'Flushing sessions: %s'
580
            logger.log(astakos_settings.LOGGING_LEVEL, msg, ','.join(keys))
581
        engine = import_module(settings.SESSION_ENGINE)
582
        for k in keys:
583
            s = engine.SessionStore(k)
584
            s.flush()
585

    
586
    def __unicode__(self):
587
        return '%s (%s)' % (self.realname, self.email)
588

    
589
    def conflicting_email(self):
590
        q = AstakosUser.objects.exclude(username=self.username)
591
        q = q.filter(email__iexact=self.email)
592
        if q.count() != 0:
593
            return True
594
        return False
595

    
596
    def email_change_is_pending(self):
597
        return self.emailchanges.count() > 0
598

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

    
617
        if self.moderated and not self.is_rejected:
618
            if self.accepted_policy == 'manual':
619
                msg += " (manually accepted)"
620
            else:
621
                msg += " (accepted policy: %s)" % \
622
                    self.accepted_policy
623
        return msg
624

    
625
    @property
626
    def signed_terms(self):
627
        return self.has_signed_terms
628

    
629
    def set_invitations_level(self):
630
        """
631
        Update user invitation level
632
        """
633
        level = self.invitation.inviter.level + 1
634
        self.level = level
635
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
636

    
637
    def can_change_password(self):
638
        return self.has_auth_provider('local', auth_backend='astakos')
639

    
640
    def can_change_email(self):
641
        if not self.has_auth_provider('local'):
642
            return True
643

    
644
        local = self.get_auth_provider('local')._instance
645
        return local.auth_backend == 'astakos'
646

    
647
    # Auth providers related methods
648
    def get_auth_provider(self, module=None, identifier=None, **filters):
649
        if not module:
650
            return self.auth_providers.active()[0].settings
651

    
652
        params = {'module': module}
653
        if identifier:
654
            params['identifier'] = identifier
655
        params.update(filters)
656
        return self.auth_providers.active().get(**params).settings
657

    
658
    def has_auth_provider(self, provider, **kwargs):
659
        return bool(self.auth_providers.active().filter(module=provider,
660
                                                        **kwargs).count())
661

    
662
    def get_required_providers(self, **kwargs):
663
        return auth.REQUIRED_PROVIDERS.keys()
664

    
665
    def missing_required_providers(self):
666
        required = self.get_required_providers()
667
        missing = []
668
        for provider in required:
669
            if not self.has_auth_provider(provider):
670
                missing.append(auth.get_provider(provider, self))
671
        return missing
672

    
673
    def get_available_auth_providers(self, **filters):
674
        """
675
        Returns a list of providers available for add by the user.
676
        """
677
        modules = astakos_settings.IM_MODULES
678
        providers = []
679
        for p in modules:
680
            providers.append(auth.get_provider(p, self))
681
        available = []
682

    
683
        for p in providers:
684
            if p.get_add_policy:
685
                available.append(p)
686
        return available
687

    
688
    def get_disabled_auth_providers(self, **filters):
689
        providers = self.get_auth_providers(**filters)
690
        disabled = []
691
        for p in providers:
692
            if not p.get_login_policy:
693
                disabled.append(p)
694
        return disabled
695

    
696
    def get_enabled_auth_providers(self, **filters):
697
        providers = self.get_auth_providers(**filters)
698
        enabled = []
699
        for p in providers:
700
            if p.get_login_policy:
701
                enabled.append(p)
702
        return enabled
703

    
704
    def get_auth_providers(self, **filters):
705
        providers = []
706
        for provider in self.auth_providers.active(**filters):
707
            if provider.settings.module_enabled:
708
                providers.append(provider.settings)
709

    
710
        modules = astakos_settings.IM_MODULES
711

    
712
        def key(p):
713
            if not p.module in modules:
714
                return 100
715
            return modules.index(p.module)
716

    
717
        providers = sorted(providers, key=key)
718
        return providers
719

    
720
    # URL methods
721
    @property
722
    def auth_providers_display(self):
723
        return ",".join(["%s:%s" % (p.module, p.identifier) for p in
724
                         self.get_enabled_auth_providers()])
725

    
726
    def add_auth_provider(self, module='local', identifier=None, **params):
727
        provider = auth.get_provider(module, self, identifier, **params)
728
        provider.add_to_user()
729

    
730
    def get_resend_activation_url(self):
731
        return reverse('send_activation', kwargs={'user_id': self.pk})
732

    
733
    def get_activation_url(self, nxt=False):
734
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
735
                              quote(self.verification_code))
736
        if nxt:
737
            url += "&next=%s" % quote(nxt)
738
        return url
739

    
740
    def get_password_reset_url(self, token_generator=default_token_generator):
741
        return reverse('astakos.im.views.target.local.password_reset_confirm',
742
                       kwargs={'uidb36': int_to_base36(self.id),
743
                               'token': token_generator.make_token(self)})
744

    
745
    def get_inactive_message(self, provider_module, identifier=None):
746
        try:
747
            provider = self.get_auth_provider(provider_module, identifier)
748
        except AstakosUserAuthProvider.DoesNotExist:
749
            provider = auth.get_provider(provider_module, self)
750

    
751
        msg_extra = ''
752
        message = ''
753

    
754
        msg_inactive = provider.get_account_inactive_msg
755
        msg_pending = provider.get_pending_activation_msg
756
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
757
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
758
        msg_pending_mod = provider.get_pending_moderation_msg
759
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
760
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
761

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

    
777
        return mark_safe(message + u' ' + msg_extra)
778

    
779
    def owns_application(self, application):
780
        return application.owner == self
781

    
782
    def owns_project(self, project):
783
        return project.application.owner == self
784

    
785
    def is_associated(self, project):
786
        try:
787
            m = ProjectMembership.objects.get(person=self, project=project)
788
            return m.state in ProjectMembership.ASSOCIATED_STATES
789
        except ProjectMembership.DoesNotExist:
790
            return False
791

    
792
    def get_membership(self, project):
793
        try:
794
            return ProjectMembership.objects.get(
795
                project=project,
796
                person=self)
797
        except ProjectMembership.DoesNotExist:
798
            return None
799

    
800
    def membership_display(self, project):
801
        m = self.get_membership(project)
802
        if m is None:
803
            return _('Not a member')
804
        else:
805
            return m.user_friendly_state_display()
806

    
807
    def non_owner_can_view(self, maybe_project):
808
        if self.is_project_admin():
809
            return True
810
        if maybe_project is None:
811
            return False
812
        project = maybe_project
813
        if self.is_associated(project):
814
            return True
815
        if project.is_deactivated():
816
            return False
817
        return True
818

    
819
    def delete_online_access_tokens(self):
820
        offline_tokens = self.token_set.filter(access_token='online')
821
        logger.info('The following access tokens will be deleted: %s',
822
                    offline_tokens)
823
        offline_tokens.delete()
824

    
825

    
826
class AstakosUserAuthProviderManager(models.Manager):
827

    
828
    def active(self, **filters):
829
        return self.filter(active=True, **filters)
830

    
831
    def remove_unverified_providers(self, provider, **filters):
832
        try:
833
            existing = self.filter(module=provider, user__email_verified=False,
834
                                   **filters)
835
            for p in existing:
836
                p.user.delete()
837
        except:
838
            pass
839

    
840
    def unverified(self, provider, **filters):
841
        try:
842

    
843
            return self.select_for_update().get(module=provider,
844
                                                user__email_verified=False,
845
                                                **filters).settings
846
        except AstakosUserAuthProvider.DoesNotExist:
847
            return None
848

    
849
    def verified(self, provider, **filters):
850
        try:
851
            return self.get(module=provider, user__email_verified=True,
852
                            **filters).settings
853
        except AstakosUserAuthProvider.DoesNotExist:
854
            return None
855

    
856

    
857
class AuthProviderPolicyProfileManager(models.Manager):
858

    
859
    def active(self):
860
        return self.filter(active=True)
861

    
862
    def for_user(self, user, provider):
863
        policies = {}
864
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
865
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
866
        exclusive_q = exclusive_q1 | exclusive_q2
867

    
868
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
869
            policies.update(profile.policies)
870

    
871
        user_groups = user.groups.all().values('pk')
872
        for profile in self.active().filter(groups__in=user_groups).filter(
873
                exclusive_q):
874
            policies.update(profile.policies)
875
        return policies
876

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

    
892

    
893
class AuthProviderPolicyProfile(models.Model):
894
    name = models.CharField(_('Name'), max_length=255, blank=False,
895
                            null=False, db_index=True)
896
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
897
                                null=False)
898

    
899
    # apply policies to all providers excluding the one set in provider field
900
    is_exclusive = models.BooleanField(default=False)
901

    
902
    policy_add = models.NullBooleanField(null=True, default=None)
903
    policy_remove = models.NullBooleanField(null=True, default=None)
904
    policy_create = models.NullBooleanField(null=True, default=None)
905
    policy_login = models.NullBooleanField(null=True, default=None)
906
    policy_limit = models.IntegerField(null=True, default=None)
907
    policy_required = models.NullBooleanField(null=True, default=None)
908
    policy_automoderate = models.NullBooleanField(null=True, default=None)
909
    policy_switch = models.NullBooleanField(null=True, default=None)
910

    
911
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
912
                     'automoderate')
913

    
914
    priority = models.IntegerField(null=False, default=1)
915
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
916
    users = models.ManyToManyField(AstakosUser,
917
                                   related_name='authpolicy_profiles')
918
    active = models.BooleanField(default=True)
919

    
920
    objects = AuthProviderPolicyProfileManager()
921

    
922
    class Meta:
923
        ordering = ['priority']
924

    
925
    @property
926
    def policies(self):
927
        policies = {}
928
        for pkey in self.POLICY_FIELDS:
929
            value = getattr(self, 'policy_%s' % pkey, None)
930
            if value is None:
931
                continue
932
            policies[pkey] = value
933
        return policies
934

    
935
    def set_policies(self, policies_dict):
936
        for key, value in policies_dict.iteritems():
937
            if key in self.POLICY_FIELDS:
938
                setattr(self, 'policy_%s' % key, value)
939
        return self.policies
940

    
941

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

    
960
    objects = AstakosUserAuthProviderManager()
961

    
962
    class Meta:
963
        unique_together = (('identifier', 'module', 'user'), )
964
        ordering = ('module', 'created')
965

    
966
    def __init__(self, *args, **kwargs):
967
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
968
        try:
969
            self.info = json.loads(self.info_data)
970
            if not self.info:
971
                self.info = {}
972
        except Exception:
973
            self.info = {}
974

    
975
        for key, value in self.info.iteritems():
976
            setattr(self, 'info_%s' % key, value)
977

    
978
    @property
979
    def settings(self):
980
        extra_data = {}
981

    
982
        info_data = {}
983
        if self.info_data:
984
            info_data = json.loads(self.info_data)
985

    
986
        extra_data['info'] = info_data
987

    
988
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
989
            extra_data[key] = getattr(self, key)
990

    
991
        extra_data['instance'] = self
992
        return auth.get_provider(self.module, self.user,
993
                                 self.identifier, **extra_data)
994

    
995
    def __repr__(self):
996
        return '<AstakosUserAuthProvider %s:%s>' % (
997
            self.module, self.identifier)
998

    
999
    def __unicode__(self):
1000
        if self.identifier:
1001
            return "%s:%s" % (self.module, self.identifier)
1002
        if self.auth_backend:
1003
            return "%s:%s" % (self.module, self.auth_backend)
1004
        return self.module
1005

    
1006
    def save(self, *args, **kwargs):
1007
        self.info_data = json.dumps(self.info)
1008
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1009

    
1010

    
1011
class AstakosUserQuota(models.Model):
1012
    capacity = models.BigIntegerField()
1013
    resource = models.ForeignKey(Resource)
1014
    user = models.ForeignKey(AstakosUser)
1015

    
1016
    class Meta:
1017
        unique_together = ("resource", "user")
1018

    
1019

    
1020
class ApprovalTerms(models.Model):
1021
    """
1022
    Model for approval terms
1023
    """
1024

    
1025
    date = models.DateTimeField(
1026
        _('Issue date'), db_index=True, auto_now_add=True)
1027
    location = models.CharField(_('Terms location'), max_length=255)
1028

    
1029

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

    
1044
    def __init__(self, *args, **kwargs):
1045
        super(Invitation, self).__init__(*args, **kwargs)
1046
        if not self.id:
1047
            self.code = _generate_invitation_code()
1048

    
1049
    def consume(self):
1050
        self.is_consumed = True
1051
        self.consumed = datetime.now()
1052
        self.save()
1053

    
1054
    def __unicode__(self):
1055
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1056

    
1057

    
1058
class EmailChangeManager(models.Manager):
1059

    
1060
    @transaction.commit_on_success
1061
    def change_email(self, activation_key):
1062
        """
1063
        Validate an activation key and change the corresponding
1064
        ``User`` if valid.
1065

1066
        If the key is valid and has not expired, return the ``User``
1067
        after activating.
1068

1069
        If the key is not valid or has expired, return ``None``.
1070

1071
        If the key is valid but the ``User`` is already active,
1072
        return ``None``.
1073

1074
        After successful email change the activation record is deleted.
1075

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

    
1106

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

    
1119
    objects = EmailChangeManager()
1120

    
1121
    def get_url(self):
1122
        return reverse('email_change_confirm',
1123
                       kwargs={'activation_key': self.activation_key})
1124

    
1125
    def activation_key_expired(self):
1126
        expiration_date = timedelta(
1127
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1128
        return self.requested_at + expiration_date < datetime.now()
1129

    
1130

    
1131
class AdditionalMail(models.Model):
1132
    """
1133
    Model for registring invitations
1134
    """
1135
    owner = models.ForeignKey(AstakosUser)
1136
    email = models.EmailField()
1137

    
1138

    
1139
def _generate_invitation_code():
1140
    while True:
1141
        code = randint(1, 2L ** 63 - 1)
1142
        try:
1143
            Invitation.objects.get(code=code)
1144
            # An invitation with this code already exists, try again
1145
        except Invitation.DoesNotExist:
1146
            return code
1147

    
1148

    
1149
def get_latest_terms():
1150
    try:
1151
        term = ApprovalTerms.objects.order_by('-id')[0]
1152
        return term
1153
    except IndexError:
1154
        pass
1155
    return None
1156

    
1157

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

    
1180
    class Meta:
1181
        unique_together = ("provider", "third_party_identifier")
1182

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

    
1199
        return user
1200

    
1201
    @property
1202
    def realname(self):
1203
        return '%s %s' % (self.first_name, self.last_name)
1204

    
1205
    @realname.setter
1206
    def realname(self, value):
1207
        first, last = split_realname(value)
1208
        self.first_name = first
1209
        self.last_name = last
1210

    
1211
    def save(self, *args, **kwargs):
1212
        if not self.id:
1213
            # set username
1214
            while not self.username:
1215
                username = uuid.uuid4().hex[:30]
1216
                try:
1217
                    AstakosUser.objects.get(username=username)
1218
                except AstakosUser.DoesNotExist:
1219
                    self.username = username
1220
        super(PendingThirdPartyUser, self).save(*args, **kwargs)
1221

    
1222
    def generate_token(self):
1223
        self.password = self.third_party_identifier
1224
        self.last_login = datetime.now()
1225
        self.token = default_token_generator.make_token(self)
1226

    
1227
    def existing_user(self):
1228
        return AstakosUser.objects.filter(
1229
            auth_providers__module=self.provider,
1230
            auth_providers__identifier=self.third_party_identifier)
1231

    
1232
    def get_provider(self, user):
1233
        params = {
1234
            'info_data': self.info,
1235
            'affiliation': self.affiliation
1236
        }
1237
        return auth.get_provider(self.provider, user,
1238
                                 self.third_party_identifier, **params)
1239

    
1240

    
1241
class SessionCatalog(models.Model):
1242
    session_key = models.CharField(_('session key'), max_length=40)
1243
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1244

    
1245

    
1246
class UserSetting(models.Model):
1247
    user = models.ForeignKey(AstakosUser)
1248
    setting = models.CharField(max_length=255)
1249
    value = models.IntegerField()
1250

    
1251
    class Meta:
1252
        unique_together = ("user", "setting")
1253

    
1254

    
1255
### PROJECTS ###
1256
################
1257

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

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

    
1264

    
1265
def new_chain():
1266
    c = Chain.objects.create()
1267
    return c
1268

    
1269

    
1270
class ProjectApplicationManager(models.Manager):
1271

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

    
1284

    
1285
class ProjectApplication(models.Model):
1286
    applicant = models.ForeignKey(
1287
        AstakosUser,
1288
        related_name='projects_applied',
1289
        db_index=True)
1290

    
1291
    PENDING = 0
1292
    APPROVED = 1
1293
    REPLACED = 2
1294
    DENIED = 3
1295
    DISMISSED = 4
1296
    CANCELLED = 5
1297

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

    
1332
    objects = ProjectApplicationManager()
1333

    
1334
    # Compiled queries
1335
    Q_PENDING = Q(state=PENDING)
1336
    Q_APPROVED = Q(state=APPROVED)
1337
    Q_DENIED = Q(state=DENIED)
1338

    
1339
    class Meta:
1340
        unique_together = ("chain", "id")
1341

    
1342
    def __unicode__(self):
1343
        return "%s applied by %s" % (self.name, self.applicant)
1344

    
1345
    # TODO: Move to a more suitable place
1346
    APPLICATION_STATE_DISPLAY = {
1347
        PENDING:   _('Pending review'),
1348
        APPROVED:  _('Approved'),
1349
        REPLACED:  _('Replaced'),
1350
        DENIED:    _('Denied'),
1351
        DISMISSED: _('Dismissed'),
1352
        CANCELLED: _('Cancelled')
1353
    }
1354

    
1355
    @property
1356
    def log_display(self):
1357
        return "application %s (%s) for project %s" % (
1358
            self.id, self.name, self.chain)
1359

    
1360
    def state_display(self):
1361
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1362

    
1363
    @property
1364
    def grants(self):
1365
        return self.projectresourcegrant_set.values('member_capacity',
1366
                                                    'resource__name')
1367

    
1368
    @property
1369
    def resource_policies(self):
1370
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1371

    
1372
    def is_modification(self):
1373
        # if self.state != self.PENDING:
1374
        #     return False
1375
        parents = self.chained_applications().filter(id__lt=self.id)
1376
        parents = parents.filter(state__in=[self.APPROVED])
1377
        return parents.count() > 0
1378

    
1379
    def chained_applications(self):
1380
        return ProjectApplication.objects.filter(chain=self.chain)
1381

    
1382
    def denied_modifications(self):
1383
        q = self.chained_applications()
1384
        q = q.filter(Q(state=self.DENIED))
1385
        q = q.filter(~Q(id=self.id))
1386
        return q
1387

    
1388
    def last_denied(self):
1389
        try:
1390
            return self.denied_modifications().order_by('-id')[0]
1391
        except IndexError:
1392
            return None
1393

    
1394
    def has_denied_modifications(self):
1395
        return bool(self.last_denied())
1396

    
1397
    def can_cancel(self):
1398
        return self.state == self.PENDING
1399

    
1400
    def cancel(self, actor=None, reason=None):
1401
        if not self.can_cancel():
1402
            m = _("cannot cancel: application '%s' in state '%s'") % (
1403
                self.id, self.state)
1404
            raise AssertionError(m)
1405

    
1406
        self.state = self.CANCELLED
1407
        self.waive_date = datetime.now()
1408
        self.waive_reason = reason
1409
        self.waive_actor = actor
1410
        self.save()
1411

    
1412
    def can_dismiss(self):
1413
        return self.state == self.DENIED
1414

    
1415
    def dismiss(self, actor=None, reason=None):
1416
        if not self.can_dismiss():
1417
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1418
                self.id, self.state)
1419
            raise AssertionError(m)
1420

    
1421
        self.state = self.DISMISSED
1422
        self.waive_date = datetime.now()
1423
        self.waive_reason = reason
1424
        self.waive_actor = actor
1425
        self.save()
1426

    
1427
    def can_deny(self):
1428
        return self.state == self.PENDING
1429

    
1430
    def deny(self, actor=None, reason=None):
1431
        if not self.can_deny():
1432
            m = _("cannot deny: application '%s' in state '%s'") % (
1433
                self.id, self.state)
1434
            raise AssertionError(m)
1435

    
1436
        self.state = self.DENIED
1437
        self.response_date = datetime.now()
1438
        self.response = reason
1439
        self.response_actor = actor
1440
        self.save()
1441

    
1442
    def can_approve(self):
1443
        return self.state == self.PENDING
1444

    
1445
    def approve(self, actor=None, reason=None):
1446
        if not self.can_approve():
1447
            m = _("cannot approve: project '%s' in state '%s'") % (
1448
                self.name, self.state)
1449
            raise AssertionError(m)  # invalid argument
1450

    
1451
        now = datetime.now()
1452
        self.state = self.APPROVED
1453
        self.response_date = now
1454
        self.response = reason
1455
        self.response_actor = actor
1456
        self.save()
1457

    
1458
    @property
1459
    def member_join_policy_display(self):
1460
        policy = self.member_join_policy
1461
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1462

    
1463
    @property
1464
    def member_leave_policy_display(self):
1465
        policy = self.member_leave_policy
1466
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1467

    
1468

    
1469
class ProjectResourceGrantManager(models.Manager):
1470
    def grants_per_app(self, applications):
1471
        app_ids = [app.id for app in applications]
1472
        grants = self.filter(
1473
            project_application__in=app_ids).select_related("resource")
1474
        return _partition_by(lambda g: g.project_application_id, grants)
1475

    
1476

    
1477
class ProjectResourceGrant(models.Model):
1478

    
1479
    resource = models.ForeignKey(Resource)
1480
    project_application = models.ForeignKey(ProjectApplication,
1481
                                            null=True)
1482
    project_capacity = models.BigIntegerField(null=True)
1483
    member_capacity = models.BigIntegerField(default=0)
1484

    
1485
    objects = ProjectResourceGrantManager()
1486

    
1487
    class Meta:
1488
        unique_together = ("resource", "project_application")
1489

    
1490
    def display_member_capacity(self):
1491
        return units.show(self.member_capacity, self.resource.unit)
1492

    
1493
    def __str__(self):
1494
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1495
                                        self.display_member_capacity())
1496

    
1497

    
1498
def _distinct(f, l):
1499
    d = {}
1500
    last = None
1501
    for x in l:
1502
        group = f(x)
1503
        if group == last:
1504
            continue
1505
        last = group
1506
        d[group] = x
1507
    return d
1508

    
1509

    
1510
def invert_dict(d):
1511
    return dict((v, k) for k, v in d.iteritems())
1512

    
1513

    
1514
class ProjectManager(models.Manager):
1515

    
1516
    def all_with_pending(self, flt=None):
1517
        flt = Q() if flt is None else flt
1518
        projects = list(self.select_related(
1519
            'application', 'application__owner').filter(flt))
1520

    
1521
        objs = ProjectApplication.objects.select_related('owner')
1522
        apps = objs.filter(state=ProjectApplication.PENDING,
1523
                           chain__in=projects).order_by('chain', '-id')
1524
        app_d = _distinct(lambda app: app.chain_id, apps)
1525
        return [(project, app_d.get(project.pk)) for project in projects]
1526

    
1527
    def expired_projects(self):
1528
        model = self.model
1529
        q = ((model.o_state_q(model.O_ACTIVE) |
1530
              model.o_state_q(model.O_SUSPENDED)) &
1531
             Q(application__end_date__lt=datetime.now()))
1532
        return self.filter(q)
1533

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

    
1548
        relevant = model.o_states_q(model.RELEVANT_STATES)
1549
        return self.filter(flt, relevant).order_by(
1550
            'application__issue_date').select_related(
1551
            'application', 'application__owner', 'application__applicant')
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

    
1560
class Project(models.Model):
1561

    
1562
    id = models.BigIntegerField(db_column='id', primary_key=True)
1563

    
1564
    application = models.OneToOneField(
1565
        ProjectApplication,
1566
        related_name='project')
1567

    
1568
    members = models.ManyToManyField(
1569
        AstakosUser,
1570
        through='ProjectMembership')
1571

    
1572
    creation_date = models.DateTimeField(auto_now_add=True)
1573
    name = models.CharField(
1574
        max_length=80,
1575
        null=True,
1576
        db_index=True,
1577
        unique=True)
1578

    
1579
    NORMAL = 1
1580
    SUSPENDED = 10
1581
    TERMINATED = 100
1582

    
1583
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1584

    
1585
    state = models.IntegerField(default=NORMAL,
1586
                                db_index=True)
1587

    
1588
    objects = ProjectManager()
1589

    
1590
    def __str__(self):
1591
        return uenc(_("<project %s '%s'>") %
1592
                    (self.id, udec(self.application.name)))
1593

    
1594
    __repr__ = __str__
1595

    
1596
    def __unicode__(self):
1597
        return _("<project %s '%s'>") % (self.id, self.application.name)
1598

    
1599
    O_PENDING = 0
1600
    O_ACTIVE = 1
1601
    O_DENIED = 3
1602
    O_DISMISSED = 4
1603
    O_CANCELLED = 5
1604
    O_SUSPENDED = 10
1605
    O_TERMINATED = 100
1606

    
1607
    O_STATE_DISPLAY = {
1608
        O_PENDING:    _("Pending"),
1609
        O_ACTIVE:     _("Active"),
1610
        O_DENIED:     _("Denied"),
1611
        O_DISMISSED:  _("Dismissed"),
1612
        O_CANCELLED:  _("Cancelled"),
1613
        O_SUSPENDED:  _("Suspended"),
1614
        O_TERMINATED: _("Terminated"),
1615
    }
1616

    
1617
    OVERALL_STATE = {
1618
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1619
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1620
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1621
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1622
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1623
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1624
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1625
    }
1626

    
1627
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1628

    
1629
    @classmethod
1630
    def o_state_q(cls, o_state):
1631
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1632
        return Q(state=p_state, application__state=a_state)
1633

    
1634
    @classmethod
1635
    def o_states_q(cls, o_states):
1636
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1637

    
1638
    INITIALIZED_STATES = [O_ACTIVE,
1639
                          O_SUSPENDED,
1640
                          O_TERMINATED,
1641
                          ]
1642

    
1643
    RELEVANT_STATES = [O_PENDING,
1644
                       O_DENIED,
1645
                       O_ACTIVE,
1646
                       O_SUSPENDED,
1647
                       O_TERMINATED,
1648
                       ]
1649

    
1650
    SKIP_STATES = [O_DISMISSED,
1651
                   O_CANCELLED,
1652
                   O_TERMINATED,
1653
                   ]
1654

    
1655
    @classmethod
1656
    def _overall_state(cls, project_state, app_state):
1657
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1658

    
1659
    def overall_state(self):
1660
        return self._overall_state(self.state, self.application.state)
1661

    
1662
    def last_pending_application(self):
1663
        apps = self.chained_apps.filter(
1664
            state=ProjectApplication.PENDING).order_by('-id')
1665
        if apps:
1666
            return apps[0]
1667
        return None
1668

    
1669
    def last_pending_modification(self):
1670
        last_pending = self.last_pending_application()
1671
        if last_pending == self.application:
1672
            return None
1673
        return last_pending
1674

    
1675
    def state_display(self):
1676
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1677

    
1678
    def expiration_info(self):
1679
        return (str(self.id), self.name, self.state_display(),
1680
                str(self.application.end_date))
1681

    
1682
    def last_deactivation(self):
1683
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1684
        ls = objs.order_by("-date")
1685
        if not ls:
1686
            return None
1687
        return ls[0]
1688

    
1689
    def is_deactivated(self, reason=None):
1690
        if reason is not None:
1691
            return self.state == reason
1692

    
1693
        return self.state != self.NORMAL
1694

    
1695
    def is_active(self):
1696
        return self.overall_state() == self.O_ACTIVE
1697

    
1698
    def is_initialized(self):
1699
        return self.overall_state() in self.INITIALIZED_STATES
1700

    
1701
    ### Deactivation calls
1702

    
1703
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1704
                    comments=None):
1705
        now = datetime.now()
1706
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1707
                        actor=actor, reason=reason, comments=comments)
1708

    
1709
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1710
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1711
                         comments=comments)
1712
        self.state = to_state
1713
        self.save()
1714

    
1715
    def terminate(self, actor=None, reason=None):
1716
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1717
        self.name = None
1718
        self.save()
1719

    
1720
    def suspend(self, actor=None, reason=None):
1721
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1722

    
1723
    def resume(self, actor=None, reason=None):
1724
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1725
        if self.name is None:
1726
            self.name = self.application.name
1727
            self.save()
1728

    
1729
    ### Logical checks
1730

    
1731
    @property
1732
    def is_alive(self):
1733
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1734

    
1735
    @property
1736
    def is_terminated(self):
1737
        return self.is_deactivated(self.TERMINATED)
1738

    
1739
    @property
1740
    def is_suspended(self):
1741
        return self.is_deactivated(self.SUSPENDED)
1742

    
1743
    def violates_members_limit(self, adding=0):
1744
        application = self.application
1745
        limit = application.limit_on_members_number
1746
        if limit is None:
1747
            return False
1748
        return (len(self.approved_members) + adding > limit)
1749

    
1750
    ### Other
1751

    
1752
    def count_pending_memberships(self):
1753
        return self.projectmembership_set.requested().count()
1754

    
1755
    def members_count(self):
1756
        return self.approved_memberships.count()
1757

    
1758
    @property
1759
    def approved_memberships(self):
1760
        query = ProjectMembership.Q_ACCEPTED_STATES
1761
        return self.projectmembership_set.filter(query)
1762

    
1763
    @property
1764
    def approved_members(self):
1765
        return [m.person for m in self.approved_memberships]
1766

    
1767

    
1768
class ProjectLogManager(models.Manager):
1769
    def last_deactivations(self, projects):
1770
        logs = self.filter(
1771
            project__in=projects,
1772
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1773
        return first_of_group(lambda l: l.project_id, logs)
1774

    
1775

    
1776
class ProjectLog(models.Model):
1777
    project = models.ForeignKey(Project, related_name="log")
1778
    from_state = models.IntegerField(null=True)
1779
    to_state = models.IntegerField()
1780
    date = models.DateTimeField()
1781
    actor = models.ForeignKey(AstakosUser, null=True)
1782
    reason = models.TextField(null=True)
1783
    comments = models.TextField(null=True)
1784

    
1785
    objects = ProjectLogManager()
1786

    
1787

    
1788
class ProjectLock(models.Model):
1789
    pass
1790

    
1791

    
1792
class ProjectMembershipManager(models.Manager):
1793

    
1794
    def any_accepted(self):
1795
        q = self.model.Q_ACCEPTED_STATES
1796
        return self.filter(q)
1797

    
1798
    def actually_accepted(self):
1799
        q = self.model.Q_ACTUALLY_ACCEPTED
1800
        return self.filter(q)
1801

    
1802
    def requested(self):
1803
        return self.filter(state=ProjectMembership.REQUESTED)
1804

    
1805
    def suspended(self):
1806
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1807

    
1808
    def associated(self):
1809
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1810

    
1811
    def any_accepted_per_project(self, projects):
1812
        ms = self.any_accepted().filter(project__in=projects)
1813
        return _partition_by(lambda m: m.project_id, ms)
1814

    
1815
    def requested_per_project(self, projects):
1816
        ms = self.requested().filter(project__in=projects)
1817
        return _partition_by(lambda m: m.project_id, ms)
1818

    
1819
    def one_per_project(self):
1820
        ms = self.all().select_related(
1821
            'project', 'project__application',
1822
            'project__application__owner', 'project_application__applicant',
1823
            'person')
1824
        m_per_p = {}
1825
        for m in ms:
1826
            m_per_p[m.project_id] = m
1827
        return m_per_p
1828

    
1829

    
1830
class ProjectMembership(models.Model):
1831

    
1832
    person = models.ForeignKey(AstakosUser)
1833
    project = models.ForeignKey(Project)
1834

    
1835
    REQUESTED = 0
1836
    ACCEPTED = 1
1837
    LEAVE_REQUESTED = 5
1838
    # User deactivation
1839
    USER_SUSPENDED = 10
1840
    REJECTED = 100
1841
    CANCELLED = 101
1842
    REMOVED = 200
1843

    
1844
    ASSOCIATED_STATES = set([REQUESTED,
1845
                             ACCEPTED,
1846
                             LEAVE_REQUESTED,
1847
                             USER_SUSPENDED,
1848
                             ])
1849

    
1850
    ACCEPTED_STATES = set([ACCEPTED,
1851
                           LEAVE_REQUESTED,
1852
                           USER_SUSPENDED,
1853
                           ])
1854

    
1855
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1856

    
1857
    state = models.IntegerField(default=REQUESTED,
1858
                                db_index=True)
1859

    
1860
    objects = ProjectMembershipManager()
1861

    
1862
    # Compiled queries
1863
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1864
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1865

    
1866
    MEMBERSHIP_STATE_DISPLAY = {
1867
        REQUESTED:       _('Requested'),
1868
        ACCEPTED:        _('Accepted'),
1869
        LEAVE_REQUESTED: _('Leave Requested'),
1870
        USER_SUSPENDED:  _('Suspended'),
1871
        REJECTED:        _('Rejected'),
1872
        CANCELLED:       _('Cancelled'),
1873
        REMOVED:         _('Removed'),
1874
    }
1875

    
1876
    USER_FRIENDLY_STATE_DISPLAY = {
1877
        REQUESTED:       _('Join requested'),
1878
        ACCEPTED:        _('Accepted member'),
1879
        LEAVE_REQUESTED: _('Requested to leave'),
1880
        USER_SUSPENDED:  _('Suspended member'),
1881
        REJECTED:        _('Request rejected'),
1882
        CANCELLED:       _('Request cancelled'),
1883
        REMOVED:         _('Removed member'),
1884
    }
1885

    
1886
    def state_display(self):
1887
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1888

    
1889
    def user_friendly_state_display(self):
1890
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1891

    
1892
    class Meta:
1893
        unique_together = ("person", "project")
1894
        #index_together = [["project", "state"]]
1895

    
1896
    def __str__(self):
1897
        return uenc(_("<'%s' membership in '%s'>") %
1898
                    (self.person.username, self.project))
1899

    
1900
    __repr__ = __str__
1901

    
1902
    def latest_log(self):
1903
        logs = self.log.all()
1904
        logs_d = _partition_by(lambda l: l.to_state, logs)
1905
        for s, s_logs in logs_d.iteritems():
1906
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1907
        return logs_d
1908

    
1909
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1910
                    comments=None):
1911
        now = datetime.now()
1912
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1913
                        actor=actor, reason=reason, comments=comments)
1914

    
1915
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1916
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1917
                         comments=comments)
1918
        self.state = to_state
1919
        self.save()
1920

    
1921
    ACTION_CHECKS = {
1922
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
1923
        "accept": lambda m: m.state == m.REQUESTED,
1924
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
1925
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
1926
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
1927
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1928
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1929
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
1930
        "reject": lambda m: m.state == m.REQUESTED,
1931
        "cancel": lambda m: m.state == m.REQUESTED,
1932
    }
1933

    
1934
    ACTION_STATES = {
1935
        "join":          REQUESTED,
1936
        "accept":        ACCEPTED,
1937
        "enroll":        ACCEPTED,
1938
        "leave_request": LEAVE_REQUESTED,
1939
        "deny_leave":    ACCEPTED,
1940
        "cancel_leave":  ACCEPTED,
1941
        "remove":        REMOVED,
1942
        "reject":        REJECTED,
1943
        "cancel":        CANCELLED,
1944
    }
1945

    
1946
    def check_action(self, action):
1947
        try:
1948
            check = self.ACTION_CHECKS[action]
1949
        except KeyError:
1950
            raise ValueError("No check found for action '%s'" % action)
1951
        return check(self)
1952

    
1953
    def perform_action(self, action, actor=None, reason=None):
1954
        if not self.check_action(action):
1955
            m = _("%s: attempted action '%s' in state '%s'") % (
1956
                self, action, self.state)
1957
            raise AssertionError(m)
1958
        try:
1959
            s = self.ACTION_STATES[action]
1960
        except KeyError:
1961
            raise ValueError("No such action '%s'" % action)
1962
        return self.set_state(s, actor=actor, reason=reason)
1963

    
1964

    
1965
class ProjectMembershipLogManager(models.Manager):
1966
    def last_logs(self, memberships):
1967
        logs = self.filter(membership__in=memberships).order_by("-date")
1968
        logs = _partition_by(lambda l: l.membership_id, logs)
1969

    
1970
        for memb_id, m_logs in logs.iteritems():
1971
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
1972
        return logs
1973

    
1974

    
1975
class ProjectMembershipLog(models.Model):
1976
    membership = models.ForeignKey(ProjectMembership, related_name="log")
1977
    from_state = models.IntegerField(null=True)
1978
    to_state = models.IntegerField()
1979
    date = models.DateTimeField()
1980
    actor = models.ForeignKey(AstakosUser, null=True)
1981
    reason = models.TextField(null=True)
1982
    comments = models.TextField(null=True)
1983

    
1984
    objects = ProjectMembershipLogManager()
1985

    
1986

    
1987
### SIGNALS ###
1988
################
1989

    
1990
def resource_post_save(sender, instance, created, **kwargs):
1991
    pass
1992

    
1993
post_save.connect(resource_post_save, sender=Resource)
1994

    
1995

    
1996
def renew_token(sender, instance, **kwargs):
1997
    if not instance.auth_token:
1998
        instance.renew_token()
1999
pre_save.connect(renew_token, sender=Component)