Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 62d30634

History | View | Annotate | Download (66 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
    private = models.BooleanField(default=False)
1332

    
1333
    objects = ProjectApplicationManager()
1334

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1469

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

    
1477

    
1478
class ProjectResourceGrant(models.Model):
1479

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

    
1486
    objects = ProjectResourceGrantManager()
1487

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

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

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

    
1498

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

    
1510

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

    
1514

    
1515
class ProjectManager(models.Manager):
1516

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

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

    
1528
    def expired_projects(self):
1529
        model = self.model
1530
        q = ((model.o_state_q(model.O_ACTIVE) |
1531
              model.o_state_q(model.O_SUSPENDED)) &
1532
             Q(application__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(application__owner=user) |
1546
                   Q(application__applicant=user) |
1547
                   Q(id__in=memb_projects))
1548

    
1549
        relevant = model.o_states_q(model.RELEVANT_STATES)
1550
        return self.filter(flt, relevant).order_by(
1551
            'application__issue_date').select_related(
1552
            'application', 'application__owner', 'application__applicant')
1553

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

    
1560

    
1561
class Project(models.Model):
1562

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

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

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

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

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

    
1584
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1585

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

    
1589
    objects = ProjectManager()
1590

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

    
1595
    __repr__ = __str__
1596

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

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

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

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

    
1628
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1629

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1694
        return self.state != self.NORMAL
1695

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

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

    
1702
    ### Deactivation calls
1703

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

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

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

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

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

    
1730
    ### Logical checks
1731

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

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

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

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

    
1751
    ### Other
1752

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

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

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

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

    
1768

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

    
1776

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

    
1786
    objects = ProjectLogManager()
1787

    
1788

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

    
1792

    
1793
class ProjectMembershipManager(models.Manager):
1794

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

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

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

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

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

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

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

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

    
1830

    
1831
class ProjectMembership(models.Model):
1832

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

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

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

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

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

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

    
1861
    objects = ProjectMembershipManager()
1862

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

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

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

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

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

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

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

    
1901
    __repr__ = __str__
1902

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

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

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

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

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

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

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

    
1965

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

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

    
1975

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

    
1985
    objects = ProjectMembershipLogManager()
1986

    
1987

    
1988
### SIGNALS ###
1989
################
1990

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

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

    
1996

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