Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 2dc27ac1

History | View | Annotate | Download (67.1 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
class AstakosUserManager(UserManager):
307

    
308
    def get_auth_provider_user(self, provider, **kwargs):
309
        """
310
        Retrieve AstakosUser instance associated with the specified third party
311
        id.
312
        """
313
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
314
                          kwargs.iteritems()))
315
        return self.get(auth_providers__module=provider, **kwargs)
316

    
317
    def get_by_email(self, email):
318
        return self.get(email=email)
319

    
320
    def get_by_identifier(self, email_or_username, **kwargs):
321
        try:
322
            return self.get(email__iexact=email_or_username, **kwargs)
323
        except AstakosUser.DoesNotExist:
324
            return self.get(username__iexact=email_or_username, **kwargs)
325

    
326
    def user_exists(self, email_or_username, **kwargs):
327
        qemail = Q(email__iexact=email_or_username)
328
        qusername = Q(username__iexact=email_or_username)
329
        qextra = Q(**kwargs)
330
        return self.filter((qemail | qusername) & qextra).exists()
331

    
332
    def verified_user_exists(self, email_or_username):
333
        return self.user_exists(email_or_username, email_verified=True)
334

    
335
    def verified(self):
336
        return self.filter(email_verified=True)
337

    
338
    def accepted(self):
339
        return self.filter(moderated=True, is_rejected=False)
340

    
341
    def uuid_catalog(self, l=None):
342
        """
343
        Returns a uuid to username mapping for the uuids appearing in l.
344
        If l is None returns the mapping for all existing users.
345
        """
346
        q = self.filter(uuid__in=l) if l is not None else self
347
        return dict(q.values_list('uuid', 'username'))
348

    
349
    def displayname_catalog(self, l=None):
350
        """
351
        Returns a username to uuid mapping for the usernames appearing in l.
352
        If l is None returns the mapping for all existing users.
353
        """
354
        if l is not None:
355
            lmap = dict((x.lower(), x) for x in l)
356
            q = self.filter(username__in=lmap.keys())
357
            values = ((lmap[n], u)
358
                      for n, u in q.values_list('username', 'uuid'))
359
        else:
360
            q = self
361
            values = self.values_list('username', 'uuid')
362
        return dict(values)
363

    
364

    
365
class AstakosUser(User):
366
    """
367
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
368
    """
369
    affiliation = models.CharField(_('Affiliation'), max_length=255,
370
                                   blank=True, null=True)
371

    
372
    #for invitations
373
    user_level = astakos_settings.DEFAULT_USER_LEVEL
374
    level = models.IntegerField(_('Inviter level'), default=user_level)
375
    invitations = models.IntegerField(
376
        _('Invitations left'),
377
        default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
378

    
379
    auth_token = models.CharField(
380
        _('Authentication Token'),
381
        max_length=64,
382
        unique=True,
383
        null=True,
384
        blank=True,
385
        help_text=_('Renew your authentication '
386
                    'token. Make sure to set the new '
387
                    'token in any client you may be '
388
                    'using, to preserve its '
389
                    'functionality.'))
390
    auth_token_created = models.DateTimeField(_('Token creation date'),
391
                                              null=True)
392
    auth_token_expires = models.DateTimeField(
393
        _('Token expiration date'), null=True)
394

    
395
    updated = models.DateTimeField(_('Update date'))
396

    
397
    # Arbitrary text to identify the reason user got deactivated.
398
    # To be used as a reference from administrators.
399
    deactivated_reason = models.TextField(
400
        _('Reason the user was disabled for'),
401
        default=None, null=True)
402
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
403
                                          blank=True)
404

    
405
    has_credits = models.BooleanField(_('Has credits?'), default=False)
406

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

    
410
    # user email is verified
411
    email_verified = models.BooleanField(_('Email verified?'), default=False)
412

    
413
    # unique string used in user email verification url
414
    verification_code = models.CharField(max_length=255, null=True,
415
                                         blank=False, unique=True)
416

    
417
    # date user email verified
418
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
419
                                       blank=True)
420

    
421
    # email verification notice was sent to the user at this time
422
    activation_sent = models.DateTimeField(_('Activation sent date'),
423
                                           null=True, blank=True)
424

    
425
    # user got rejected during moderation process
426
    is_rejected = models.BooleanField(_('Account rejected'),
427
                                      default=False)
428
    # reason user got rejected
429
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
430
                                       blank=True)
431
    # moderation status
432
    moderated = models.BooleanField(_('User moderated'), default=False)
433
    # date user moderated (either accepted or rejected)
434
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
435
                                        blank=True, null=True)
436
    # a snapshot of user instance the time got moderated
437
    moderated_data = models.TextField(null=True, default=None, blank=True)
438
    # a string which identifies how the user got moderated
439
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
440
                                       default=None, null=True, blank=True)
441
    # the email used to accept the user
442
    accepted_email = models.EmailField(null=True, default=None, blank=True)
443

    
444
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
445
                                           default=False)
446
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
447
                                             null=True, blank=True)
448
    # permanent unique user identifier
449
    uuid = models.CharField(max_length=255, null=True, blank=False,
450
                            unique=True)
451

    
452
    policy = models.ManyToManyField(
453
        Resource, null=True, through='AstakosUserQuota')
454

    
455
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
456
                                          default=False, db_index=True)
457

    
458
    objects = AstakosUserManager()
459

    
460
    def __init__(self, *args, **kwargs):
461
        super(AstakosUser, self).__init__(*args, **kwargs)
462
        if not self.id:
463
            self.is_active = False
464

    
465
    @property
466
    def realname(self):
467
        return '%s %s' % (self.first_name, self.last_name)
468

    
469
    @property
470
    def log_display(self):
471
        """
472
        Should be used in all logger.* calls that refer to a user so that
473
        user display is consistent across log entries.
474
        """
475
        return '%s::%s' % (self.uuid, self.email)
476

    
477
    @realname.setter
478
    def realname(self, value):
479
        parts = value.split(' ')
480
        if len(parts) == 2:
481
            self.first_name = parts[0]
482
            self.last_name = parts[1]
483
        else:
484
            self.last_name = parts[0]
485

    
486
    def add_permission(self, pname):
487
        if self.has_perm(pname):
488
            return
489
        p, created = Permission.objects.get_or_create(
490
            codename=pname,
491
            name=pname.capitalize(),
492
            content_type=get_content_type())
493
        self.user_permissions.add(p)
494

    
495
    def remove_permission(self, pname):
496
        if self.has_perm(pname):
497
            return
498
        p = Permission.objects.get(codename=pname,
499
                                   content_type=get_content_type())
500
        self.user_permissions.remove(p)
501

    
502
    def add_group(self, gname):
503
        group, _ = Group.objects.get_or_create(name=gname)
504
        self.groups.add(group)
505

    
506
    def is_accepted(self):
507
        return self.moderated and not self.is_rejected
508

    
509
    def is_project_admin(self, application_id=None):
510
        return self.uuid in astakos_settings.PROJECT_ADMINS
511

    
512
    @property
513
    def invitation(self):
514
        try:
515
            return Invitation.objects.get(username=self.email)
516
        except Invitation.DoesNotExist:
517
            return None
518

    
519
    @property
520
    def policies(self):
521
        return self.astakosuserquota_set.select_related().all()
522

    
523
    def get_resource_policy(self, resource):
524
        return AstakosUserQuota.objects.select_related("resource").\
525
            get(user=self, resource__name=resource)
526

    
527
    def update_uuid(self):
528
        while not self.uuid:
529
            uuid_val = str(uuid.uuid4())
530
            try:
531
                AstakosUser.objects.get(uuid=uuid_val)
532
            except AstakosUser.DoesNotExist:
533
                self.uuid = uuid_val
534
        return self.uuid
535

    
536
    def save(self, update_timestamps=True, **kwargs):
537
        if update_timestamps:
538
            if not self.id:
539
                self.date_joined = datetime.now()
540
            self.updated = datetime.now()
541

    
542
        self.update_uuid()
543

    
544
        if not self.verification_code:
545
            self.renew_verification_code()
546

    
547
        # username currently matches email
548
        if self.username != self.email.lower():
549
            self.username = self.email.lower()
550

    
551
        super(AstakosUser, self).save(**kwargs)
552

    
553
    def renew_verification_code(self):
554
        self.verification_code = str(uuid.uuid4())
555
        logger.info("Verification code renewed for %s" % self.log_display)
556

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

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

    
576
    def token_expired(self):
577
        return self.auth_token_expires < datetime.now()
578

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

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

    
593
    def __unicode__(self):
594
        return '%s (%s)' % (self.realname, self.email)
595

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

    
603
    def email_change_is_pending(self):
604
        return self.emailchanges.count() > 0
605

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

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

    
632
    @property
633
    def signed_terms(self):
634
        term = get_latest_terms()
635
        if not term:
636
            return True
637
        if not self.has_signed_terms:
638
            return False
639
        if not self.date_signed_terms:
640
            return False
641
        if self.date_signed_terms < term.date:
642
            self.has_signed_terms = False
643
            self.date_signed_terms = None
644
            self.save()
645
            return False
646
        return True
647

    
648
    def set_invitations_level(self):
649
        """
650
        Update user invitation level
651
        """
652
        level = self.invitation.inviter.level + 1
653
        self.level = level
654
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
655

    
656
    def can_change_password(self):
657
        return self.has_auth_provider('local', auth_backend='astakos')
658

    
659
    def can_change_email(self):
660
        if not self.has_auth_provider('local'):
661
            return True
662

    
663
        local = self.get_auth_provider('local')._instance
664
        return local.auth_backend == 'astakos'
665

    
666
    # Auth providers related methods
667
    def get_auth_provider(self, module=None, identifier=None, **filters):
668
        if not module:
669
            return self.auth_providers.active()[0].settings
670

    
671
        params = {'module': module}
672
        if identifier:
673
            params['identifier'] = identifier
674
        params.update(filters)
675
        return self.auth_providers.active().get(**params).settings
676

    
677
    def has_auth_provider(self, provider, **kwargs):
678
        return bool(self.auth_providers.active().filter(module=provider,
679
                                                        **kwargs).count())
680

    
681
    def get_required_providers(self, **kwargs):
682
        return auth.REQUIRED_PROVIDERS.keys()
683

    
684
    def missing_required_providers(self):
685
        required = self.get_required_providers()
686
        missing = []
687
        for provider in required:
688
            if not self.has_auth_provider(provider):
689
                missing.append(auth.get_provider(provider, self))
690
        return missing
691

    
692
    def get_available_auth_providers(self, **filters):
693
        """
694
        Returns a list of providers available for add by the user.
695
        """
696
        modules = astakos_settings.IM_MODULES
697
        providers = []
698
        for p in modules:
699
            providers.append(auth.get_provider(p, self))
700
        available = []
701

    
702
        for p in providers:
703
            if p.get_add_policy:
704
                available.append(p)
705
        return available
706

    
707
    def get_disabled_auth_providers(self, **filters):
708
        providers = self.get_auth_providers(**filters)
709
        disabled = []
710
        for p in providers:
711
            if not p.get_login_policy:
712
                disabled.append(p)
713
        return disabled
714

    
715
    def get_enabled_auth_providers(self, **filters):
716
        providers = self.get_auth_providers(**filters)
717
        enabled = []
718
        for p in providers:
719
            if p.get_login_policy:
720
                enabled.append(p)
721
        return enabled
722

    
723
    def get_auth_providers(self, **filters):
724
        providers = []
725
        for provider in self.auth_providers.active(**filters):
726
            if provider.settings.module_enabled:
727
                providers.append(provider.settings)
728

    
729
        modules = astakos_settings.IM_MODULES
730

    
731
        def key(p):
732
            if not p.module in modules:
733
                return 100
734
            return modules.index(p.module)
735

    
736
        providers = sorted(providers, key=key)
737
        return providers
738

    
739
    # URL methods
740
    @property
741
    def auth_providers_display(self):
742
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
743
                         self.get_enabled_auth_providers()])
744

    
745
    def add_auth_provider(self, module='local', identifier=None, **params):
746
        provider = auth.get_provider(module, self, identifier, **params)
747
        provider.add_to_user()
748

    
749
    def get_resend_activation_url(self):
750
        return reverse('send_activation', kwargs={'user_id': self.pk})
751

    
752
    def get_activation_url(self, nxt=False):
753
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
754
                              quote(self.verification_code))
755
        if nxt:
756
            url += "&next=%s" % quote(nxt)
757
        return url
758

    
759
    def get_password_reset_url(self, token_generator=default_token_generator):
760
        return reverse('astakos.im.views.target.local.password_reset_confirm',
761
                       kwargs={'uidb36': int_to_base36(self.id),
762
                               'token': token_generator.make_token(self)})
763

    
764
    def get_inactive_message(self, provider_module, identifier=None):
765
        provider = self.get_auth_provider(provider_module, identifier)
766

    
767
        msg_extra = ''
768
        message = ''
769

    
770
        msg_inactive = provider.get_account_inactive_msg
771
        msg_pending = provider.get_pending_activation_msg
772
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
773
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
774
        msg_pending_mod = provider.get_pending_moderation_msg
775
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
776
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
777

    
778
        if not self.email_verified:
779
            message = msg_pending
780
            url = self.get_resend_activation_url()
781
            msg_extra = msg_pending_help + \
782
                u' ' + \
783
                '<a href="%s">%s?</a>' % (url, msg_resend)
784
        else:
785
            if not self.moderated:
786
                message = msg_pending_mod
787
            else:
788
                if self.is_rejected:
789
                    message = msg_rejected
790
                else:
791
                    message = msg_inactive
792

    
793
        return mark_safe(message + u' ' + msg_extra)
794

    
795
    def owns_application(self, application):
796
        return application.owner == self
797

    
798
    def owns_project(self, project):
799
        return project.application.owner == self
800

    
801
    def is_associated(self, project):
802
        try:
803
            m = ProjectMembership.objects.get(person=self, project=project)
804
            return m.state in ProjectMembership.ASSOCIATED_STATES
805
        except ProjectMembership.DoesNotExist:
806
            return False
807

    
808
    def get_membership(self, project):
809
        try:
810
            return ProjectMembership.objects.get(
811
                project=project,
812
                person=self)
813
        except ProjectMembership.DoesNotExist:
814
            return None
815

    
816
    def membership_display(self, project):
817
        m = self.get_membership(project)
818
        if m is None:
819
            return _('Not a member')
820
        else:
821
            return m.user_friendly_state_display()
822

    
823
    def non_owner_can_view(self, maybe_project):
824
        if self.is_project_admin():
825
            return True
826
        if maybe_project is None:
827
            return False
828
        project = maybe_project
829
        if self.is_associated(project):
830
            return True
831
        if project.is_deactivated():
832
            return False
833
        return True
834

    
835

    
836
class AstakosUserAuthProviderManager(models.Manager):
837

    
838
    def active(self, **filters):
839
        return self.filter(active=True, **filters)
840

    
841
    def remove_unverified_providers(self, provider, **filters):
842
        try:
843
            existing = self.filter(module=provider, user__email_verified=False,
844
                                   **filters)
845
            for p in existing:
846
                p.user.delete()
847
        except:
848
            pass
849

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

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

    
864

    
865
class AuthProviderPolicyProfileManager(models.Manager):
866

    
867
    def active(self):
868
        return self.filter(active=True)
869

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

    
876
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
877
            policies.update(profile.policies)
878

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

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

    
900

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

    
907
    # apply policies to all providers excluding the one set in provider field
908
    is_exclusive = models.BooleanField(default=False)
909

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

    
919
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
920
                     'automoderate')
921

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

    
928
    objects = AuthProviderPolicyProfileManager()
929

    
930
    class Meta:
931
        ordering = ['priority']
932

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

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

    
949

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

    
968
    objects = AstakosUserAuthProviderManager()
969

    
970
    class Meta:
971
        unique_together = (('identifier', 'module', 'user'), )
972
        ordering = ('module', 'created')
973

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

    
983
        for key, value in self.info.iteritems():
984
            setattr(self, 'info_%s' % key, value)
985

    
986
    @property
987
    def settings(self):
988
        extra_data = {}
989

    
990
        info_data = {}
991
        if self.info_data:
992
            info_data = json.loads(self.info_data)
993

    
994
        extra_data['info'] = info_data
995

    
996
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
997
            extra_data[key] = getattr(self, key)
998

    
999
        extra_data['instance'] = self
1000
        return auth.get_provider(self.module, self.user,
1001
                                 self.identifier, **extra_data)
1002

    
1003
    def __repr__(self):
1004
        return '<AstakosUserAuthProvider %s:%s>' % (
1005
            self.module, self.identifier)
1006

    
1007
    def __unicode__(self):
1008
        if self.identifier:
1009
            return "%s:%s" % (self.module, self.identifier)
1010
        if self.auth_backend:
1011
            return "%s:%s" % (self.module, self.auth_backend)
1012
        return self.module
1013

    
1014
    def save(self, *args, **kwargs):
1015
        self.info_data = json.dumps(self.info)
1016
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1017

    
1018

    
1019
class AstakosUserQuota(models.Model):
1020
    capacity = models.BigIntegerField()
1021
    resource = models.ForeignKey(Resource)
1022
    user = models.ForeignKey(AstakosUser)
1023

    
1024
    class Meta:
1025
        unique_together = ("resource", "user")
1026

    
1027

    
1028
class ApprovalTerms(models.Model):
1029
    """
1030
    Model for approval terms
1031
    """
1032

    
1033
    date = models.DateTimeField(
1034
        _('Issue date'), db_index=True, auto_now_add=True)
1035
    location = models.CharField(_('Terms location'), max_length=255)
1036

    
1037

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

    
1052
    def __init__(self, *args, **kwargs):
1053
        super(Invitation, self).__init__(*args, **kwargs)
1054
        if not self.id:
1055
            self.code = _generate_invitation_code()
1056

    
1057
    def consume(self):
1058
        self.is_consumed = True
1059
        self.consumed = datetime.now()
1060
        self.save()
1061

    
1062
    def __unicode__(self):
1063
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1064

    
1065

    
1066
class EmailChangeManager(models.Manager):
1067

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

1074
        If the key is valid and has not expired, return the ``User``
1075
        after activating.
1076

1077
        If the key is not valid or has expired, return ``None``.
1078

1079
        If the key is valid but the ``User`` is already active,
1080
        return ``None``.
1081

1082
        After successful email change the activation record is deleted.
1083

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

    
1113

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

    
1126
    objects = EmailChangeManager()
1127

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

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

    
1137

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

    
1145

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

    
1155

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

    
1164

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

    
1187
    class Meta:
1188
        unique_together = ("provider", "third_party_identifier")
1189

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

    
1206
        return user
1207

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

    
1212
    @realname.setter
1213
    def realname(self, value):
1214
        parts = value.split(' ')
1215
        if len(parts) == 2:
1216
            self.first_name = parts[0]
1217
            self.last_name = parts[1]
1218
        else:
1219
            self.last_name = parts[0]
1220

    
1221
    def save(self, *args, **kwargs):
1222
        if not self.id:
1223
            # set username
1224
            while not self.username:
1225
                username = uuid.uuid4().hex[:30]
1226
                try:
1227
                    AstakosUser.objects.get(username=username)
1228
                except AstakosUser.DoesNotExist:
1229
                    self.username = username
1230
        super(PendingThirdPartyUser, self).save(*args, **kwargs)
1231

    
1232
    def generate_token(self):
1233
        self.password = self.third_party_identifier
1234
        self.last_login = datetime.now()
1235
        self.token = default_token_generator.make_token(self)
1236

    
1237
    def existing_user(self):
1238
        return AstakosUser.objects.filter(
1239
            auth_providers__module=self.provider,
1240
            auth_providers__identifier=self.third_party_identifier)
1241

    
1242
    def get_provider(self, user):
1243
        params = {
1244
            'info_data': self.info,
1245
            'affiliation': self.affiliation
1246
        }
1247
        return auth.get_provider(self.provider, user,
1248
                                 self.third_party_identifier, **params)
1249

    
1250

    
1251
class SessionCatalog(models.Model):
1252
    session_key = models.CharField(_('session key'), max_length=40)
1253
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1254

    
1255

    
1256
class UserSetting(models.Model):
1257
    user = models.ForeignKey(AstakosUser)
1258
    setting = models.CharField(max_length=255)
1259
    value = models.IntegerField()
1260

    
1261
    class Meta:
1262
        unique_together = ("user", "setting")
1263

    
1264

    
1265
### PROJECTS ###
1266
################
1267

    
1268
class Chain(models.Model):
1269
    chain = models.AutoField(primary_key=True)
1270

    
1271
    def __str__(self):
1272
        return "%s" % (self.chain,)
1273

    
1274

    
1275
def new_chain():
1276
    c = Chain.objects.create()
1277
    return c
1278

    
1279

    
1280
class ProjectApplicationManager(models.Manager):
1281

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

    
1294

    
1295
class ProjectApplication(models.Model):
1296
    applicant = models.ForeignKey(
1297
        AstakosUser,
1298
        related_name='projects_applied',
1299
        db_index=True)
1300

    
1301
    PENDING = 0
1302
    APPROVED = 1
1303
    REPLACED = 2
1304
    DENIED = 3
1305
    DISMISSED = 4
1306
    CANCELLED = 5
1307

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

    
1342
    objects = ProjectApplicationManager()
1343

    
1344
    # Compiled queries
1345
    Q_PENDING = Q(state=PENDING)
1346
    Q_APPROVED = Q(state=APPROVED)
1347
    Q_DENIED = Q(state=DENIED)
1348

    
1349
    class Meta:
1350
        unique_together = ("chain", "id")
1351

    
1352
    def __unicode__(self):
1353
        return "%s applied by %s" % (self.name, self.applicant)
1354

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

    
1365
    @property
1366
    def log_display(self):
1367
        return "application %s (%s) for project %s" % (
1368
            self.id, self.name, self.chain)
1369

    
1370
    def state_display(self):
1371
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1372

    
1373
    @property
1374
    def grants(self):
1375
        return self.projectresourcegrant_set.values('member_capacity',
1376
                                                    'resource__name')
1377

    
1378
    @property
1379
    def resource_policies(self):
1380
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1381

    
1382
    def is_modification(self):
1383
        # if self.state != self.PENDING:
1384
        #     return False
1385
        parents = self.chained_applications().filter(id__lt=self.id)
1386
        parents = parents.filter(state__in=[self.APPROVED])
1387
        return parents.count() > 0
1388

    
1389
    def chained_applications(self):
1390
        return ProjectApplication.objects.filter(chain=self.chain)
1391

    
1392
    def denied_modifications(self):
1393
        q = self.chained_applications()
1394
        q = q.filter(Q(state=self.DENIED))
1395
        q = q.filter(~Q(id=self.id))
1396
        return q
1397

    
1398
    def last_denied(self):
1399
        try:
1400
            return self.denied_modifications().order_by('-id')[0]
1401
        except IndexError:
1402
            return None
1403

    
1404
    def has_denied_modifications(self):
1405
        return bool(self.last_denied())
1406

    
1407
    def can_cancel(self):
1408
        return self.state == self.PENDING
1409

    
1410
    def cancel(self, actor=None, reason=None):
1411
        if not self.can_cancel():
1412
            m = _("cannot cancel: application '%s' in state '%s'") % (
1413
                self.id, self.state)
1414
            raise AssertionError(m)
1415

    
1416
        self.state = self.CANCELLED
1417
        self.waive_date = datetime.now()
1418
        self.waive_reason = reason
1419
        self.waive_actor = actor
1420
        self.save()
1421

    
1422
    def can_dismiss(self):
1423
        return self.state == self.DENIED
1424

    
1425
    def dismiss(self, actor=None, reason=None):
1426
        if not self.can_dismiss():
1427
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1428
                self.id, self.state)
1429
            raise AssertionError(m)
1430

    
1431
        self.state = self.DISMISSED
1432
        self.waive_date = datetime.now()
1433
        self.waive_reason = reason
1434
        self.waive_actor = actor
1435
        self.save()
1436

    
1437
    def can_deny(self):
1438
        return self.state == self.PENDING
1439

    
1440
    def deny(self, actor=None, reason=None):
1441
        if not self.can_deny():
1442
            m = _("cannot deny: application '%s' in state '%s'") % (
1443
                self.id, self.state)
1444
            raise AssertionError(m)
1445

    
1446
        self.state = self.DENIED
1447
        self.response_date = datetime.now()
1448
        self.response = reason
1449
        self.response_actor = actor
1450
        self.save()
1451

    
1452
    def can_approve(self):
1453
        return self.state == self.PENDING
1454

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

    
1461
        now = datetime.now()
1462
        self.state = self.APPROVED
1463
        self.response_date = now
1464
        self.response = reason
1465
        self.response_actor = actor
1466
        self.save()
1467

    
1468
    @property
1469
    def member_join_policy_display(self):
1470
        policy = self.member_join_policy
1471
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1472

    
1473
    @property
1474
    def member_leave_policy_display(self):
1475
        policy = self.member_leave_policy
1476
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1477

    
1478

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

    
1486

    
1487
class ProjectResourceGrant(models.Model):
1488

    
1489
    resource = models.ForeignKey(Resource)
1490
    project_application = models.ForeignKey(ProjectApplication,
1491
                                            null=True)
1492
    project_capacity = models.BigIntegerField(null=True)
1493
    member_capacity = models.BigIntegerField(default=0)
1494

    
1495
    objects = ProjectResourceGrantManager()
1496

    
1497
    class Meta:
1498
        unique_together = ("resource", "project_application")
1499

    
1500
    def display_member_capacity(self):
1501
        return units.show(self.member_capacity, self.resource.unit)
1502

    
1503
    def __str__(self):
1504
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1505
                                        self.display_member_capacity())
1506

    
1507

    
1508
def _distinct(f, l):
1509
    d = {}
1510
    last = None
1511
    for x in l:
1512
        group = f(x)
1513
        if group == last:
1514
            continue
1515
        last = group
1516
        d[group] = x
1517
    return d
1518

    
1519

    
1520
def invert_dict(d):
1521
    return dict((v, k) for k, v in d.iteritems())
1522

    
1523

    
1524
class ProjectManager(models.Manager):
1525

    
1526
    def all_with_pending(self, flt=None):
1527
        flt = Q() if flt is None else flt
1528
        projects = list(self.select_related(
1529
            'application', 'application__owner').filter(flt))
1530

    
1531
        objs = ProjectApplication.objects.select_related('owner')
1532
        apps = objs.filter(state=ProjectApplication.PENDING,
1533
                           chain__in=projects).order_by('chain', '-id')
1534
        app_d = _distinct(lambda app: app.chain_id, apps)
1535
        return [(project, app_d.get(project.pk)) for project in projects]
1536

    
1537
    def expired_projects(self):
1538
        model = self.model
1539
        q = ((model.o_state_q(model.O_ACTIVE) |
1540
              model.o_state_q(model.O_SUSPENDED)) &
1541
             Q(application__end_date__lt=datetime.now()))
1542
        return self.filter(q)
1543

    
1544
    def user_accessible_projects(self, user):
1545
        """
1546
        Return projects accessible by specified user.
1547
        """
1548
        model = self.model
1549
        if user.is_project_admin():
1550
            flt = Q()
1551
        else:
1552
            membs = user.projectmembership_set.associated()
1553
            memb_projects = membs.values_list("project", flat=True)
1554
            flt = (Q(application__owner=user) |
1555
                   Q(application__applicant=user) |
1556
                   Q(id__in=memb_projects))
1557

    
1558
        relevant = model.o_states_q(model.RELEVANT_STATES)
1559
        return self.filter(flt, relevant).order_by(
1560
            'application__issue_date').select_related(
1561
            'application', 'application__owner', 'application__applicant')
1562

    
1563
    def search_by_name(self, *search_strings):
1564
        q = Q()
1565
        for s in search_strings:
1566
            q = q | Q(name__icontains=s)
1567
        return self.filter(q)
1568

    
1569

    
1570
class Project(models.Model):
1571

    
1572
    id = models.BigIntegerField(db_column='id', primary_key=True)
1573

    
1574
    application = models.OneToOneField(
1575
        ProjectApplication,
1576
        related_name='project')
1577

    
1578
    members = models.ManyToManyField(
1579
        AstakosUser,
1580
        through='ProjectMembership')
1581

    
1582
    creation_date = models.DateTimeField(auto_now_add=True)
1583
    name = models.CharField(
1584
        max_length=80,
1585
        null=True,
1586
        db_index=True,
1587
        unique=True)
1588

    
1589
    NORMAL = 1
1590
    SUSPENDED = 10
1591
    TERMINATED = 100
1592

    
1593
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1594

    
1595
    state = models.IntegerField(default=NORMAL,
1596
                                db_index=True)
1597

    
1598
    objects = ProjectManager()
1599

    
1600
    def __str__(self):
1601
        return uenc(_("<project %s '%s'>") %
1602
                    (self.id, udec(self.application.name)))
1603

    
1604
    __repr__ = __str__
1605

    
1606
    def __unicode__(self):
1607
        return _("<project %s '%s'>") % (self.id, self.application.name)
1608

    
1609
    O_PENDING = 0
1610
    O_ACTIVE = 1
1611
    O_DENIED = 3
1612
    O_DISMISSED = 4
1613
    O_CANCELLED = 5
1614
    O_SUSPENDED = 10
1615
    O_TERMINATED = 100
1616

    
1617
    O_STATE_DISPLAY = {
1618
        O_PENDING:    _("Pending"),
1619
        O_ACTIVE:     _("Active"),
1620
        O_DENIED:     _("Denied"),
1621
        O_DISMISSED:  _("Dismissed"),
1622
        O_CANCELLED:  _("Cancelled"),
1623
        O_SUSPENDED:  _("Suspended"),
1624
        O_TERMINATED: _("Terminated"),
1625
    }
1626

    
1627
    OVERALL_STATE = {
1628
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1629
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1630
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1631
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1632
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1633
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1634
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1635
    }
1636

    
1637
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1638

    
1639
    @classmethod
1640
    def o_state_q(cls, o_state):
1641
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1642
        return Q(state=p_state, application__state=a_state)
1643

    
1644
    @classmethod
1645
    def o_states_q(cls, o_states):
1646
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1647

    
1648
    INITIALIZED_STATES = [O_ACTIVE,
1649
                          O_SUSPENDED,
1650
                          O_TERMINATED,
1651
                          ]
1652

    
1653
    RELEVANT_STATES = [O_PENDING,
1654
                       O_DENIED,
1655
                       O_ACTIVE,
1656
                       O_SUSPENDED,
1657
                       O_TERMINATED,
1658
                       ]
1659

    
1660
    SKIP_STATES = [O_DISMISSED,
1661
                   O_CANCELLED,
1662
                   O_TERMINATED,
1663
                   ]
1664

    
1665
    @classmethod
1666
    def _overall_state(cls, project_state, app_state):
1667
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1668

    
1669
    def overall_state(self):
1670
        return self._overall_state(self.state, self.application.state)
1671

    
1672
    def last_pending_application(self):
1673
        apps = self.chained_apps.filter(
1674
            state=ProjectApplication.PENDING).order_by('-id')
1675
        if apps:
1676
            return apps[0]
1677
        return None
1678

    
1679
    def last_pending_modification(self):
1680
        last_pending = self.last_pending_application()
1681
        if last_pending == self.application:
1682
            return None
1683
        return last_pending
1684

    
1685
    def state_display(self):
1686
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1687

    
1688
    def expiration_info(self):
1689
        return (str(self.id), self.name, self.state_display(),
1690
                str(self.application.end_date))
1691

    
1692
    def last_deactivation(self):
1693
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1694
        ls = objs.order_by("-date")
1695
        if not ls:
1696
            return None
1697
        return ls[0]
1698

    
1699
    def is_deactivated(self, reason=None):
1700
        if reason is not None:
1701
            return self.state == reason
1702

    
1703
        return self.state != self.NORMAL
1704

    
1705
    def is_active(self):
1706
        return self.overall_state() == self.O_ACTIVE
1707

    
1708
    def is_initialized(self):
1709
        return self.overall_state() in self.INITIALIZED_STATES
1710

    
1711
    ### Deactivation calls
1712

    
1713
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1714
                    comments=None):
1715
        now = datetime.now()
1716
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1717
                        actor=actor, reason=reason, comments=comments)
1718

    
1719
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1720
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1721
                         comments=comments)
1722
        self.state = to_state
1723
        self.save()
1724

    
1725
    def terminate(self, actor=None, reason=None):
1726
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1727
        self.name = None
1728
        self.save()
1729

    
1730
    def suspend(self, actor=None, reason=None):
1731
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1732

    
1733
    def resume(self, actor=None, reason=None):
1734
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1735
        if self.name is None:
1736
            self.name = self.application.name
1737
            self.save()
1738

    
1739
    ### Logical checks
1740

    
1741
    @property
1742
    def is_alive(self):
1743
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1744

    
1745
    @property
1746
    def is_terminated(self):
1747
        return self.is_deactivated(self.TERMINATED)
1748

    
1749
    @property
1750
    def is_suspended(self):
1751
        return self.is_deactivated(self.SUSPENDED)
1752

    
1753
    def violates_members_limit(self, adding=0):
1754
        application = self.application
1755
        limit = application.limit_on_members_number
1756
        if limit is None:
1757
            return False
1758
        return (len(self.approved_members) + adding > limit)
1759

    
1760
    ### Other
1761

    
1762
    def count_pending_memberships(self):
1763
        return self.projectmembership_set.requested().count()
1764

    
1765
    def members_count(self):
1766
        return self.approved_memberships.count()
1767

    
1768
    @property
1769
    def approved_memberships(self):
1770
        query = ProjectMembership.Q_ACCEPTED_STATES
1771
        return self.projectmembership_set.filter(query)
1772

    
1773
    @property
1774
    def approved_members(self):
1775
        return [m.person for m in self.approved_memberships]
1776

    
1777

    
1778
class ProjectLogManager(models.Manager):
1779
    def last_deactivations(self, projects):
1780
        logs = self.filter(
1781
            project__in=projects,
1782
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1783
        return first_of_group(lambda l: l.project_id, logs)
1784

    
1785

    
1786
class ProjectLog(models.Model):
1787
    project = models.ForeignKey(Project, related_name="log")
1788
    from_state = models.IntegerField(null=True)
1789
    to_state = models.IntegerField()
1790
    date = models.DateTimeField()
1791
    actor = models.ForeignKey(AstakosUser, null=True)
1792
    reason = models.TextField(null=True)
1793
    comments = models.TextField(null=True)
1794

    
1795
    objects = ProjectLogManager()
1796

    
1797

    
1798
class ProjectLock(models.Model):
1799
    pass
1800

    
1801

    
1802
class ProjectMembershipManager(models.Manager):
1803

    
1804
    def any_accepted(self):
1805
        q = self.model.Q_ACCEPTED_STATES
1806
        return self.filter(q)
1807

    
1808
    def actually_accepted(self):
1809
        q = self.model.Q_ACTUALLY_ACCEPTED
1810
        return self.filter(q)
1811

    
1812
    def requested(self):
1813
        return self.filter(state=ProjectMembership.REQUESTED)
1814

    
1815
    def suspended(self):
1816
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1817

    
1818
    def associated(self):
1819
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1820

    
1821
    def any_accepted_per_project(self, projects):
1822
        ms = self.any_accepted().filter(project__in=projects)
1823
        return _partition_by(lambda m: m.project_id, ms)
1824

    
1825
    def requested_per_project(self, projects):
1826
        ms = self.requested().filter(project__in=projects)
1827
        return _partition_by(lambda m: m.project_id, ms)
1828

    
1829
    def one_per_project(self):
1830
        ms = self.all().select_related(
1831
            'project', 'project__application',
1832
            'project__application__owner', 'project_application__applicant',
1833
            'person')
1834
        m_per_p = {}
1835
        for m in ms:
1836
            m_per_p[m.project_id] = m
1837
        return m_per_p
1838

    
1839

    
1840
class ProjectMembership(models.Model):
1841

    
1842
    person = models.ForeignKey(AstakosUser)
1843
    project = models.ForeignKey(Project)
1844

    
1845
    REQUESTED = 0
1846
    ACCEPTED = 1
1847
    LEAVE_REQUESTED = 5
1848
    # User deactivation
1849
    USER_SUSPENDED = 10
1850
    REJECTED = 100
1851
    CANCELLED = 101
1852
    REMOVED = 200
1853

    
1854
    ASSOCIATED_STATES = set([REQUESTED,
1855
                             ACCEPTED,
1856
                             LEAVE_REQUESTED,
1857
                             USER_SUSPENDED,
1858
                             ])
1859

    
1860
    ACCEPTED_STATES = set([ACCEPTED,
1861
                           LEAVE_REQUESTED,
1862
                           USER_SUSPENDED,
1863
                           ])
1864

    
1865
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1866

    
1867
    state = models.IntegerField(default=REQUESTED,
1868
                                db_index=True)
1869

    
1870
    objects = ProjectMembershipManager()
1871

    
1872
    # Compiled queries
1873
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1874
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1875

    
1876
    MEMBERSHIP_STATE_DISPLAY = {
1877
        REQUESTED:       _('Requested'),
1878
        ACCEPTED:        _('Accepted'),
1879
        LEAVE_REQUESTED: _('Leave Requested'),
1880
        USER_SUSPENDED:  _('Suspended'),
1881
        REJECTED:        _('Rejected'),
1882
        CANCELLED:       _('Cancelled'),
1883
        REMOVED:         _('Removed'),
1884
    }
1885

    
1886
    USER_FRIENDLY_STATE_DISPLAY = {
1887
        REQUESTED:       _('Join requested'),
1888
        ACCEPTED:        _('Accepted member'),
1889
        LEAVE_REQUESTED: _('Requested to leave'),
1890
        USER_SUSPENDED:  _('Suspended member'),
1891
        REJECTED:        _('Request rejected'),
1892
        CANCELLED:       _('Request cancelled'),
1893
        REMOVED:         _('Removed member'),
1894
    }
1895

    
1896
    def state_display(self):
1897
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1898

    
1899
    def user_friendly_state_display(self):
1900
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1901

    
1902
    class Meta:
1903
        unique_together = ("person", "project")
1904
        #index_together = [["project", "state"]]
1905

    
1906
    def __str__(self):
1907
        return uenc(_("<'%s' membership in '%s'>") %
1908
                    (self.person.username, self.project))
1909

    
1910
    __repr__ = __str__
1911

    
1912
    def latest_log(self):
1913
        logs = self.log.all()
1914
        logs_d = _partition_by(lambda l: l.to_state, logs)
1915
        for s, s_logs in logs_d.iteritems():
1916
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1917
        return logs_d
1918

    
1919
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1920
                    comments=None):
1921
        now = datetime.now()
1922
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1923
                        actor=actor, reason=reason, comments=comments)
1924

    
1925
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1926
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1927
                         comments=comments)
1928
        self.state = to_state
1929
        self.save()
1930

    
1931
    ACTION_CHECKS = {
1932
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
1933
        "accept": lambda m: m.state == m.REQUESTED,
1934
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
1935
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
1936
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
1937
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1938
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1939
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
1940
        "reject": lambda m: m.state == m.REQUESTED,
1941
        "cancel": lambda m: m.state == m.REQUESTED,
1942
    }
1943

    
1944
    ACTION_STATES = {
1945
        "join":          REQUESTED,
1946
        "accept":        ACCEPTED,
1947
        "enroll":        ACCEPTED,
1948
        "leave_request": LEAVE_REQUESTED,
1949
        "deny_leave":    ACCEPTED,
1950
        "cancel_leave":  ACCEPTED,
1951
        "remove":        REMOVED,
1952
        "reject":        REJECTED,
1953
        "cancel":        CANCELLED,
1954
    }
1955

    
1956
    def check_action(self, action):
1957
        try:
1958
            check = self.ACTION_CHECKS[action]
1959
        except KeyError:
1960
            raise ValueError("No check found for action '%s'" % action)
1961
        return check(self)
1962

    
1963
    def perform_action(self, action, actor=None, reason=None):
1964
        if not self.check_action(action):
1965
            m = _("%s: attempted action '%s' in state '%s'") % (
1966
                self, action, self.state)
1967
            raise AssertionError(m)
1968
        try:
1969
            s = self.ACTION_STATES[action]
1970
        except KeyError:
1971
            raise ValueError("No such action '%s'" % action)
1972
        return self.set_state(s, actor=actor, reason=reason)
1973

    
1974

    
1975
class ProjectMembershipLogManager(models.Manager):
1976
    def last_logs(self, memberships):
1977
        logs = self.filter(membership__in=memberships).order_by("-date")
1978
        logs = _partition_by(lambda l: l.membership_id, logs)
1979

    
1980
        for memb_id, m_logs in logs.iteritems():
1981
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
1982
        return logs
1983

    
1984

    
1985
class ProjectMembershipLog(models.Model):
1986
    membership = models.ForeignKey(ProjectMembership, related_name="log")
1987
    from_state = models.IntegerField(null=True)
1988
    to_state = models.IntegerField()
1989
    date = models.DateTimeField()
1990
    actor = models.ForeignKey(AstakosUser, null=True)
1991
    reason = models.TextField(null=True)
1992
    comments = models.TextField(null=True)
1993

    
1994
    objects = ProjectMembershipLogManager()
1995

    
1996

    
1997
### SIGNALS ###
1998
################
1999

    
2000
def create_astakos_user(u):
2001
    try:
2002
        AstakosUser.objects.get(user_ptr=u.pk)
2003
    except AstakosUser.DoesNotExist:
2004
        extended_user = AstakosUser(user_ptr_id=u.pk)
2005
        extended_user.__dict__.update(u.__dict__)
2006
        extended_user.save()
2007
        if not extended_user.has_auth_provider('local'):
2008
            extended_user.add_auth_provider('local')
2009
    except BaseException, e:
2010
        logger.exception(e)
2011

    
2012

    
2013
def fix_superusers():
2014
    # Associate superusers with AstakosUser
2015
    admins = User.objects.filter(is_superuser=True)
2016
    for u in admins:
2017
        create_astakos_user(u)
2018

    
2019

    
2020
def user_post_save(sender, instance, created, **kwargs):
2021
    if not created:
2022
        return
2023
    create_astakos_user(instance)
2024
post_save.connect(user_post_save, sender=User)
2025

    
2026

    
2027
def astakosuser_post_save(sender, instance, created, **kwargs):
2028
    pass
2029

    
2030
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2031

    
2032

    
2033
def resource_post_save(sender, instance, created, **kwargs):
2034
    pass
2035

    
2036
post_save.connect(resource_post_save, sender=Resource)
2037

    
2038

    
2039
def renew_token(sender, instance, **kwargs):
2040
    if not instance.auth_token:
2041
        instance.renew_token()
2042
pre_save.connect(renew_token, sender=AstakosUser)
2043
pre_save.connect(renew_token, sender=Component)