Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (67.4 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 snf_django.lib.db.managers import ForUpdateManager
66
from synnefo.lib.ordereddict import OrderedDict
67

    
68
from snf_django.lib.db.fields import intDecimalField
69
from synnefo.util.text import uenc, udec
70
from synnefo.util import units
71
from astakos.im import presentation
72

    
73
logger = logging.getLogger(__name__)
74

    
75
DEFAULT_CONTENT_TYPE = None
76
_content_type = None
77

    
78

    
79
def get_content_type():
80
    global _content_type
81
    if _content_type is not None:
82
        return _content_type
83

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

    
92
inf = float('inf')
93

    
94

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

    
99

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

    
109

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

    
121

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

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

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

    
154
    def __str__(self):
155
        return self.name
156

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

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

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

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

    
179
        metadata = dict_merge(metadata,
180
                              astakos_settings.COMPONENTS_META)
181

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

    
190
        order_key = component_by_order
191
        if orderfor == 'dashboard':
192
            order_key = component_by_dashboard_order
193

    
194
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
195
                                             key=order_key))
196
        return ordered_catalog
197

    
198

    
199
_presentation_data = {}
200

    
201

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

    
211

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

    
217

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

    
221

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

    
227
    class Meta:
228
        unique_together = (('endpoint', 'key'),)
229

    
230

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

    
240
    objects = ForUpdateManager()
241

    
242
    def __str__(self):
243
        return self.name
244

    
245
    def full_name(self):
246
        return str(self)
247

    
248
    def get_info(self):
249
        return {'service': self.service_origin,
250
                'description': self.desc,
251
                'unit': self.unit,
252
                'allow_in_projects': self.allow_in_projects,
253
                }
254

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

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

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

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

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

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

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

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

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

    
300

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

    
307

    
308
class AstakosUserManager(UserManager):
309

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

    
319
    def get_by_email(self, email):
320
        return self.get(email=email)
321

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

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

    
334
    def verified_user_exists(self, email_or_username):
335
        return self.user_exists(email_or_username, email_verified=True)
336

    
337
    def verified(self):
338
        return self.filter(email_verified=True)
339

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

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

    
363

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
457
    objects = AstakosUserManager()
458
    forupdate = ForUpdateManager()
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_project_admin(self, application_id=None):
507
        return self.uuid in astakos_settings.PROJECT_ADMINS
508

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

    
516
    @property
517
    def policies(self):
518
        return self.astakosuserquota_set.select_related().all()
519

    
520
    def get_resource_policy(self, resource):
521
        resource = Resource.objects.get(name=resource)
522
        default_capacity = resource.uplimit
523
        try:
524
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
525
            return policy, default_capacity
526
        except AstakosUserQuota.DoesNotExist:
527
            return None, default_capacity
528

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

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

    
544
        self.update_uuid()
545

    
546
        if not self.verification_code:
547
            self.renew_verification_code()
548

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

    
553
        super(AstakosUser, self).save(**kwargs)
554

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

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

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

    
578
    def token_expired(self):
579
        return self.auth_token_expires < datetime.now()
580

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

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

    
595
    def __unicode__(self):
596
        return '%s (%s)' % (self.realname, self.email)
597

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

    
605
    def email_change_is_pending(self):
606
        return self.emailchanges.count() > 0
607

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

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

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

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

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

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

    
665
        local = self.get_auth_provider('local')._instance
666
        return local.auth_backend == 'astakos'
667

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

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

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

    
683
    def get_required_providers(self, **kwargs):
684
        return auth.REQUIRED_PROVIDERS.keys()
685

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

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

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

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

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

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

    
731
        modules = astakos_settings.IM_MODULES
732

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

    
738
        providers = sorted(providers, key=key)
739
        return providers
740

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

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

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

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

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

    
766
    def get_inactive_message(self, provider_module, identifier=None):
767
        provider = self.get_auth_provider(provider_module, identifier)
768

    
769
        msg_extra = ''
770
        message = ''
771

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

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

    
795
        return mark_safe(message + u' ' + msg_extra)
796

    
797
    def owns_application(self, application):
798
        return application.owner == self
799

    
800
    def owns_project(self, project):
801
        return project.application.owner == self
802

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

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

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

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

    
837

    
838
class AstakosUserAuthProviderManager(models.Manager):
839

    
840
    def active(self, **filters):
841
        return self.filter(active=True, **filters)
842

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

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

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

    
866

    
867
class AuthProviderPolicyProfileManager(models.Manager):
868

    
869
    def active(self):
870
        return self.filter(active=True)
871

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

    
878
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
879
            policies.update(profile.policies)
880

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

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

    
902

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

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

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

    
921
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
922
                     'automoderate')
923

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

    
930
    objects = AuthProviderPolicyProfileManager()
931

    
932
    class Meta:
933
        ordering = ['priority']
934

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

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

    
951

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

    
970
    objects = AstakosUserAuthProviderManager()
971

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

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

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

    
988
    @property
989
    def settings(self):
990
        extra_data = {}
991

    
992
        info_data = {}
993
        if self.info_data:
994
            info_data = json.loads(self.info_data)
995

    
996
        extra_data['info'] = info_data
997

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

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

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

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

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

    
1020

    
1021
class AstakosUserQuota(models.Model):
1022
    capacity = intDecimalField()
1023
    resource = models.ForeignKey(Resource)
1024
    user = models.ForeignKey(AstakosUser)
1025

    
1026
    class Meta:
1027
        unique_together = ("resource", "user")
1028

    
1029

    
1030
class ApprovalTerms(models.Model):
1031
    """
1032
    Model for approval terms
1033
    """
1034

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

    
1039

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

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

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

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

    
1067

    
1068
class EmailChangeManager(models.Manager):
1069

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

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

1079
        If the key is not valid or has expired, return ``None``.
1080

1081
        If the key is valid but the ``User`` is already active,
1082
        return ``None``.
1083

1084
        After successful email change the activation record is deleted.
1085

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

    
1116

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

    
1129
    objects = EmailChangeManager()
1130

    
1131
    def get_url(self):
1132
        return reverse('email_change_confirm',
1133
                       kwargs={'activation_key': self.activation_key})
1134

    
1135
    def activation_key_expired(self):
1136
        expiration_date = timedelta(
1137
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1138
        return self.requested_at + expiration_date < datetime.now()
1139

    
1140

    
1141
class AdditionalMail(models.Model):
1142
    """
1143
    Model for registring invitations
1144
    """
1145
    owner = models.ForeignKey(AstakosUser)
1146
    email = models.EmailField()
1147

    
1148

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

    
1158

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

    
1167

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

    
1190
    class Meta:
1191
        unique_together = ("provider", "third_party_identifier")
1192

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

    
1209
        return user
1210

    
1211
    @property
1212
    def realname(self):
1213
        return '%s %s' % (self.first_name, self.last_name)
1214

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

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

    
1235
    def generate_token(self):
1236
        self.password = self.third_party_identifier
1237
        self.last_login = datetime.now()
1238
        self.token = default_token_generator.make_token(self)
1239

    
1240
    def existing_user(self):
1241
        return AstakosUser.objects.filter(
1242
            auth_providers__module=self.provider,
1243
            auth_providers__identifier=self.third_party_identifier)
1244

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

    
1253

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

    
1258

    
1259
class UserSetting(models.Model):
1260
    user = models.ForeignKey(AstakosUser)
1261
    setting = models.CharField(max_length=255)
1262
    value = models.IntegerField()
1263

    
1264
    objects = ForUpdateManager()
1265

    
1266
    class Meta:
1267
        unique_together = ("user", "setting")
1268

    
1269

    
1270
### PROJECTS ###
1271
################
1272

    
1273
class Chain(models.Model):
1274
    chain = models.AutoField(primary_key=True)
1275
    objects = ForUpdateManager()
1276

    
1277
    def __str__(self):
1278
        return "%s" % (self.chain,)
1279

    
1280

    
1281
def new_chain():
1282
    c = Chain.objects.create()
1283
    return c
1284

    
1285

    
1286
class ProjectApplicationManager(ForUpdateManager):
1287

    
1288
    def pending_per_project(self, projects):
1289
        apps = self.filter(state=self.model.PENDING,
1290
                           chain__in=projects).order_by('chain', '-id')
1291
        checked_chain = None
1292
        projs = {}
1293
        for app in apps:
1294
            chain = app.chain_id
1295
            if chain != checked_chain:
1296
                checked_chain = chain
1297
                projs[chain] = app
1298
        return projs
1299

    
1300

    
1301
class ProjectApplication(models.Model):
1302
    applicant = models.ForeignKey(
1303
        AstakosUser,
1304
        related_name='projects_applied',
1305
        db_index=True)
1306

    
1307
    PENDING = 0
1308
    APPROVED = 1
1309
    REPLACED = 2
1310
    DENIED = 3
1311
    DISMISSED = 4
1312
    CANCELLED = 5
1313

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

    
1348
    objects = ProjectApplicationManager()
1349

    
1350
    # Compiled queries
1351
    Q_PENDING = Q(state=PENDING)
1352
    Q_APPROVED = Q(state=APPROVED)
1353
    Q_DENIED = Q(state=DENIED)
1354

    
1355
    class Meta:
1356
        unique_together = ("chain", "id")
1357

    
1358
    def __unicode__(self):
1359
        return "%s applied by %s" % (self.name, self.applicant)
1360

    
1361
    # TODO: Move to a more suitable place
1362
    APPLICATION_STATE_DISPLAY = {
1363
        PENDING:   _('Pending review'),
1364
        APPROVED:  _('Approved'),
1365
        REPLACED:  _('Replaced'),
1366
        DENIED:    _('Denied'),
1367
        DISMISSED: _('Dismissed'),
1368
        CANCELLED: _('Cancelled')
1369
    }
1370

    
1371
    @property
1372
    def log_display(self):
1373
        return "application %s (%s) for project %s" % (
1374
            self.id, self.name, self.chain)
1375

    
1376
    def state_display(self):
1377
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1378

    
1379
    @property
1380
    def grants(self):
1381
        return self.projectresourcegrant_set.values('member_capacity',
1382
                                                    'resource__name')
1383

    
1384
    @property
1385
    def resource_policies(self):
1386
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1387

    
1388
    def is_modification(self):
1389
        # if self.state != self.PENDING:
1390
        #     return False
1391
        parents = self.chained_applications().filter(id__lt=self.id)
1392
        parents = parents.filter(state__in=[self.APPROVED])
1393
        return parents.count() > 0
1394

    
1395
    def chained_applications(self):
1396
        return ProjectApplication.objects.filter(chain=self.chain)
1397

    
1398
    def denied_modifications(self):
1399
        q = self.chained_applications()
1400
        q = q.filter(Q(state=self.DENIED))
1401
        q = q.filter(~Q(id=self.id))
1402
        return q
1403

    
1404
    def last_denied(self):
1405
        try:
1406
            return self.denied_modifications().order_by('-id')[0]
1407
        except IndexError:
1408
            return None
1409

    
1410
    def has_denied_modifications(self):
1411
        return bool(self.last_denied())
1412

    
1413
    def can_cancel(self):
1414
        return self.state == self.PENDING
1415

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

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

    
1428
    def can_dismiss(self):
1429
        return self.state == self.DENIED
1430

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

    
1437
        self.state = self.DISMISSED
1438
        self.waive_date = datetime.now()
1439
        self.waive_reason = reason
1440
        self.waive_actor = actor
1441
        self.save()
1442

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

    
1446
    def deny(self, actor=None, reason=None):
1447
        if not self.can_deny():
1448
            m = _("cannot deny: application '%s' in state '%s'") % (
1449
                self.id, self.state)
1450
            raise AssertionError(m)
1451

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

    
1458
    def can_approve(self):
1459
        return self.state == self.PENDING
1460

    
1461
    def approve(self, actor=None, reason=None):
1462
        if not self.can_approve():
1463
            m = _("cannot approve: project '%s' in state '%s'") % (
1464
                self.name, self.state)
1465
            raise AssertionError(m)  # invalid argument
1466

    
1467
        now = datetime.now()
1468
        self.state = self.APPROVED
1469
        self.response_date = now
1470
        self.response = reason
1471
        self.response_actor = actor
1472
        self.save()
1473

    
1474
    @property
1475
    def member_join_policy_display(self):
1476
        policy = self.member_join_policy
1477
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1478

    
1479
    @property
1480
    def member_leave_policy_display(self):
1481
        policy = self.member_leave_policy
1482
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1483

    
1484

    
1485
class ProjectResourceGrantManager(models.Manager):
1486
    def grants_per_app(self, applications):
1487
        app_ids = [app.id for app in applications]
1488
        grants = self.filter(
1489
            project_application__in=app_ids).select_related("resource")
1490
        return _partition_by(lambda g: g.project_application_id, grants)
1491

    
1492

    
1493
class ProjectResourceGrant(models.Model):
1494

    
1495
    resource = models.ForeignKey(Resource)
1496
    project_application = models.ForeignKey(ProjectApplication,
1497
                                            null=True)
1498
    project_capacity = intDecimalField(null=True)
1499
    member_capacity = intDecimalField(default=0)
1500

    
1501
    objects = ProjectResourceGrantManager()
1502

    
1503
    class Meta:
1504
        unique_together = ("resource", "project_application")
1505

    
1506
    def display_member_capacity(self):
1507
        return units.show(self.member_capacity, self.resource.unit)
1508

    
1509
    def __str__(self):
1510
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1511
                                        self.display_member_capacity())
1512

    
1513

    
1514
def _distinct(f, l):
1515
    d = {}
1516
    last = None
1517
    for x in l:
1518
        group = f(x)
1519
        if group == last:
1520
            continue
1521
        last = group
1522
        d[group] = x
1523
    return d
1524

    
1525

    
1526
def invert_dict(d):
1527
    return dict((v, k) for k, v in d.iteritems())
1528

    
1529

    
1530
class ProjectManager(ForUpdateManager):
1531

    
1532
    def all_with_pending(self, flt=None):
1533
        flt = Q() if flt is None else flt
1534
        projects = list(self.select_related(
1535
            'application', 'application__owner').filter(flt))
1536

    
1537
        objs = ProjectApplication.objects.select_related('owner')
1538
        apps = objs.filter(state=ProjectApplication.PENDING,
1539
                           chain__in=projects).order_by('chain', '-id')
1540
        app_d = _distinct(lambda app: app.chain_id, apps)
1541
        return [(project, app_d.get(project.pk)) for project in projects]
1542

    
1543
    def expired_projects(self):
1544
        model = self.model
1545
        q = ((model.o_state_q(model.O_ACTIVE) |
1546
              model.o_state_q(model.O_SUSPENDED)) &
1547
             Q(application__end_date__lt=datetime.now()))
1548
        return self.filter(q)
1549

    
1550
    def user_accessible_projects(self, user):
1551
        """
1552
        Return projects accessible by specified user.
1553
        """
1554
        model = self.model
1555
        if user.is_project_admin():
1556
            flt = Q()
1557
        else:
1558
            membs = user.projectmembership_set.associated()
1559
            memb_projects = membs.values_list("project", flat=True)
1560
            flt = (Q(application__owner=user) |
1561
                   Q(application__applicant=user) |
1562
                   Q(id__in=memb_projects))
1563

    
1564
        relevant = model.o_states_q(model.RELEVANT_STATES)
1565
        return self.filter(flt, relevant).order_by(
1566
            'application__issue_date').select_related(
1567
                'application', 'application__owner', 'application__applicant')
1568

    
1569
    def search_by_name(self, *search_strings):
1570
        q = Q()
1571
        for s in search_strings:
1572
            q = q | Q(name__icontains=s)
1573
        return self.filter(q)
1574

    
1575

    
1576
class Project(models.Model):
1577

    
1578
    id = models.BigIntegerField(db_column='id', primary_key=True)
1579

    
1580
    application = models.OneToOneField(
1581
        ProjectApplication,
1582
        related_name='project')
1583

    
1584
    members = models.ManyToManyField(
1585
        AstakosUser,
1586
        through='ProjectMembership')
1587

    
1588
    creation_date = models.DateTimeField(auto_now_add=True)
1589
    name = models.CharField(
1590
        max_length=80,
1591
        null=True,
1592
        db_index=True,
1593
        unique=True)
1594

    
1595
    NORMAL = 1
1596
    SUSPENDED = 10
1597
    TERMINATED = 100
1598

    
1599
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1600

    
1601
    state = models.IntegerField(default=NORMAL,
1602
                                db_index=True)
1603

    
1604
    objects = ProjectManager()
1605

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

    
1610
    __repr__ = __str__
1611

    
1612
    def __unicode__(self):
1613
        return _("<project %s '%s'>") % (self.id, self.application.name)
1614

    
1615
    O_PENDING = 0
1616
    O_ACTIVE = 1
1617
    O_DENIED = 3
1618
    O_DISMISSED = 4
1619
    O_CANCELLED = 5
1620
    O_SUSPENDED = 10
1621
    O_TERMINATED = 100
1622

    
1623
    O_STATE_DISPLAY = {
1624
        O_PENDING:    _("Pending"),
1625
        O_ACTIVE:     _("Active"),
1626
        O_DENIED:     _("Denied"),
1627
        O_DISMISSED:  _("Dismissed"),
1628
        O_CANCELLED:  _("Cancelled"),
1629
        O_SUSPENDED:  _("Suspended"),
1630
        O_TERMINATED: _("Terminated"),
1631
    }
1632

    
1633
    OVERALL_STATE = {
1634
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1635
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1636
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1637
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1638
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1639
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1640
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1641
    }
1642

    
1643
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1644

    
1645
    @classmethod
1646
    def o_state_q(cls, o_state):
1647
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1648
        return Q(state=p_state, application__state=a_state)
1649

    
1650
    @classmethod
1651
    def o_states_q(cls, o_states):
1652
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1653

    
1654
    INITIALIZED_STATES = [O_ACTIVE,
1655
                          O_SUSPENDED,
1656
                          O_TERMINATED,
1657
                          ]
1658

    
1659
    RELEVANT_STATES = [O_PENDING,
1660
                       O_DENIED,
1661
                       O_ACTIVE,
1662
                       O_SUSPENDED,
1663
                       O_TERMINATED,
1664
                       ]
1665

    
1666
    SKIP_STATES = [O_DISMISSED,
1667
                   O_CANCELLED,
1668
                   O_TERMINATED,
1669
                   ]
1670

    
1671
    @classmethod
1672
    def _overall_state(cls, project_state, app_state):
1673
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1674

    
1675
    def overall_state(self):
1676
        return self._overall_state(self.state, self.application.state)
1677

    
1678
    def last_pending_application(self):
1679
        apps = self.chained_apps.filter(
1680
            state=ProjectApplication.PENDING).order_by('-id')
1681
        if apps:
1682
            return apps[0]
1683
        return None
1684

    
1685
    def last_pending_modification(self):
1686
        last_pending = self.last_pending_application()
1687
        if last_pending == self.application:
1688
            return None
1689
        return last_pending
1690

    
1691
    def state_display(self):
1692
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1693

    
1694
    def expiration_info(self):
1695
        return (str(self.id), self.name, self.state_display(),
1696
                str(self.application.end_date))
1697

    
1698
    def last_deactivation(self):
1699
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1700
        ls = objs.order_by("-date")
1701
        if not ls:
1702
            return None
1703
        return ls[0]
1704

    
1705
    def is_deactivated(self, reason=None):
1706
        if reason is not None:
1707
            return self.state == reason
1708

    
1709
        return self.state != self.NORMAL
1710

    
1711
    def is_active(self):
1712
        return self.overall_state() == self.O_ACTIVE
1713

    
1714
    def is_initialized(self):
1715
        return self.overall_state() in self.INITIALIZED_STATES
1716

    
1717
    ### Deactivation calls
1718

    
1719
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1720
                    comments=None):
1721
        now = datetime.now()
1722
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1723
                        actor=actor, reason=reason, comments=comments)
1724

    
1725
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1726
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1727
                         comments=comments)
1728
        self.state = to_state
1729
        self.save()
1730

    
1731
    def terminate(self, actor=None, reason=None):
1732
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1733
        self.name = None
1734
        self.save()
1735

    
1736
    def suspend(self, actor=None, reason=None):
1737
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1738

    
1739
    def resume(self, actor=None, reason=None):
1740
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1741
        if self.name is None:
1742
            self.name = self.application.name
1743
            self.save()
1744

    
1745
    ### Logical checks
1746

    
1747
    @property
1748
    def is_alive(self):
1749
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1750

    
1751
    @property
1752
    def is_terminated(self):
1753
        return self.is_deactivated(self.TERMINATED)
1754

    
1755
    @property
1756
    def is_suspended(self):
1757
        return self.is_deactivated(self.SUSPENDED)
1758

    
1759
    def violates_members_limit(self, adding=0):
1760
        application = self.application
1761
        limit = application.limit_on_members_number
1762
        if limit is None:
1763
            return False
1764
        return (len(self.approved_members) + adding > limit)
1765

    
1766
    ### Other
1767

    
1768
    def count_pending_memberships(self):
1769
        return self.projectmembership_set.requested().count()
1770

    
1771
    def members_count(self):
1772
        return self.approved_memberships.count()
1773

    
1774
    @property
1775
    def approved_memberships(self):
1776
        query = ProjectMembership.Q_ACCEPTED_STATES
1777
        return self.projectmembership_set.filter(query)
1778

    
1779
    @property
1780
    def approved_members(self):
1781
        return [m.person for m in self.approved_memberships]
1782

    
1783

    
1784
class ProjectLogManager(models.Manager):
1785
    def last_deactivations(self, projects):
1786
        logs = self.filter(
1787
            project__in=projects,
1788
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1789
        return first_of_group(lambda l: l.project_id, logs)
1790

    
1791

    
1792
class ProjectLog(models.Model):
1793
    project = models.ForeignKey(Project, related_name="log")
1794
    from_state = models.IntegerField(null=True)
1795
    to_state = models.IntegerField()
1796
    date = models.DateTimeField()
1797
    actor = models.ForeignKey(AstakosUser, null=True)
1798
    reason = models.TextField(null=True)
1799
    comments = models.TextField(null=True)
1800

    
1801
    objects = ProjectLogManager()
1802

    
1803

    
1804
class ProjectLock(models.Model):
1805
    objects = ForUpdateManager()
1806

    
1807

    
1808
class ProjectMembershipManager(ForUpdateManager):
1809

    
1810
    def any_accepted(self):
1811
        q = self.model.Q_ACCEPTED_STATES
1812
        return self.filter(q)
1813

    
1814
    def actually_accepted(self):
1815
        q = self.model.Q_ACTUALLY_ACCEPTED
1816
        return self.filter(q)
1817

    
1818
    def requested(self):
1819
        return self.filter(state=ProjectMembership.REQUESTED)
1820

    
1821
    def suspended(self):
1822
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1823

    
1824
    def associated(self):
1825
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1826

    
1827
    def any_accepted_per_project(self, projects):
1828
        ms = self.any_accepted().filter(project__in=projects)
1829
        return _partition_by(lambda m: m.project_id, ms)
1830

    
1831
    def requested_per_project(self, projects):
1832
        ms = self.requested().filter(project__in=projects)
1833
        return _partition_by(lambda m: m.project_id, ms)
1834

    
1835
    def one_per_project(self):
1836
        ms = self.all().select_related(
1837
            'project', 'project__application',
1838
            'project__application__owner', 'project_application__applicant',
1839
            'person')
1840
        m_per_p = {}
1841
        for m in ms:
1842
            m_per_p[m.project_id] = m
1843
        return m_per_p
1844

    
1845

    
1846
class ProjectMembership(models.Model):
1847

    
1848
    person = models.ForeignKey(AstakosUser)
1849
    project = models.ForeignKey(Project)
1850

    
1851
    REQUESTED = 0
1852
    ACCEPTED = 1
1853
    LEAVE_REQUESTED = 5
1854
    # User deactivation
1855
    USER_SUSPENDED = 10
1856
    REJECTED = 100
1857
    CANCELLED = 101
1858
    REMOVED = 200
1859

    
1860
    ASSOCIATED_STATES = set([REQUESTED,
1861
                             ACCEPTED,
1862
                             LEAVE_REQUESTED,
1863
                             USER_SUSPENDED,
1864
                             ])
1865

    
1866
    ACCEPTED_STATES = set([ACCEPTED,
1867
                           LEAVE_REQUESTED,
1868
                           USER_SUSPENDED,
1869
                           ])
1870

    
1871
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1872

    
1873
    state = models.IntegerField(default=REQUESTED,
1874
                                db_index=True)
1875

    
1876
    objects = ProjectMembershipManager()
1877

    
1878
    # Compiled queries
1879
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1880
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1881

    
1882
    MEMBERSHIP_STATE_DISPLAY = {
1883
        REQUESTED:       _('Requested'),
1884
        ACCEPTED:        _('Accepted'),
1885
        LEAVE_REQUESTED: _('Leave Requested'),
1886
        USER_SUSPENDED:  _('Suspended'),
1887
        REJECTED:        _('Rejected'),
1888
        CANCELLED:       _('Cancelled'),
1889
        REMOVED:         _('Removed'),
1890
    }
1891

    
1892
    USER_FRIENDLY_STATE_DISPLAY = {
1893
        REQUESTED:       _('Join requested'),
1894
        ACCEPTED:        _('Accepted member'),
1895
        LEAVE_REQUESTED: _('Requested to leave'),
1896
        USER_SUSPENDED:  _('Suspended member'),
1897
        REJECTED:        _('Request rejected'),
1898
        CANCELLED:       _('Request cancelled'),
1899
        REMOVED:         _('Removed member'),
1900
    }
1901

    
1902
    def state_display(self):
1903
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1904

    
1905
    def user_friendly_state_display(self):
1906
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1907

    
1908
    class Meta:
1909
        unique_together = ("person", "project")
1910
        #index_together = [["project", "state"]]
1911

    
1912
    def __str__(self):
1913
        return uenc(_("<'%s' membership in '%s'>") %
1914
                    (self.person.username, self.project))
1915

    
1916
    __repr__ = __str__
1917

    
1918
    def latest_log(self):
1919
        logs = self.log.all()
1920
        logs_d = _partition_by(lambda l: l.to_state, logs)
1921
        for s, s_logs in logs_d.iteritems():
1922
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1923
        return logs_d
1924

    
1925
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1926
                    comments=None):
1927
        now = datetime.now()
1928
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1929
                        actor=actor, reason=reason, comments=comments)
1930

    
1931
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1932
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1933
                         comments=comments)
1934
        self.state = to_state
1935
        self.save()
1936

    
1937
    ACTION_CHECKS = {
1938
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
1939
        "accept": lambda m: m.state == m.REQUESTED,
1940
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
1941
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
1942
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
1943
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1944
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1945
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
1946
        "reject": lambda m: m.state == m.REQUESTED,
1947
        "cancel": lambda m: m.state == m.REQUESTED,
1948
    }
1949

    
1950
    ACTION_STATES = {
1951
        "join":          REQUESTED,
1952
        "accept":        ACCEPTED,
1953
        "enroll":        ACCEPTED,
1954
        "leave_request": LEAVE_REQUESTED,
1955
        "deny_leave":    ACCEPTED,
1956
        "cancel_leave":  ACCEPTED,
1957
        "remove":        REMOVED,
1958
        "reject":        REJECTED,
1959
        "cancel":        CANCELLED,
1960
    }
1961

    
1962
    def check_action(self, action):
1963
        try:
1964
            check = self.ACTION_CHECKS[action]
1965
        except KeyError:
1966
            raise ValueError("No check found for action '%s'" % action)
1967
        return check(self)
1968

    
1969
    def perform_action(self, action, actor=None, reason=None):
1970
        if not self.check_action(action):
1971
            m = _("%s: attempted action '%s' in state '%s'") % (
1972
                self, action, self.state)
1973
            raise AssertionError(m)
1974
        try:
1975
            s = self.ACTION_STATES[action]
1976
        except KeyError:
1977
            raise ValueError("No such action '%s'" % action)
1978
        return self.set_state(s, actor=actor, reason=reason)
1979

    
1980

    
1981
class ProjectMembershipLogManager(models.Manager):
1982
    def last_logs(self, memberships):
1983
        logs = self.filter(membership__in=memberships).order_by("-date")
1984
        logs = _partition_by(lambda l: l.membership_id, logs)
1985

    
1986
        for memb_id, m_logs in logs.iteritems():
1987
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
1988
        return logs
1989

    
1990

    
1991
class ProjectMembershipLog(models.Model):
1992
    membership = models.ForeignKey(ProjectMembership, related_name="log")
1993
    from_state = models.IntegerField(null=True)
1994
    to_state = models.IntegerField()
1995
    date = models.DateTimeField()
1996
    actor = models.ForeignKey(AstakosUser, null=True)
1997
    reason = models.TextField(null=True)
1998
    comments = models.TextField(null=True)
1999

    
2000
    objects = ProjectMembershipLogManager()
2001

    
2002

    
2003
### SIGNALS ###
2004
################
2005

    
2006
def create_astakos_user(u):
2007
    try:
2008
        AstakosUser.objects.get(user_ptr=u.pk)
2009
    except AstakosUser.DoesNotExist:
2010
        extended_user = AstakosUser(user_ptr_id=u.pk)
2011
        extended_user.__dict__.update(u.__dict__)
2012
        extended_user.save()
2013
        if not extended_user.has_auth_provider('local'):
2014
            extended_user.add_auth_provider('local')
2015
    except BaseException, e:
2016
        logger.exception(e)
2017

    
2018

    
2019
def fix_superusers():
2020
    # Associate superusers with AstakosUser
2021
    admins = User.objects.filter(is_superuser=True)
2022
    for u in admins:
2023
        create_astakos_user(u)
2024

    
2025

    
2026
def user_post_save(sender, instance, created, **kwargs):
2027
    if not created:
2028
        return
2029
    create_astakos_user(instance)
2030
post_save.connect(user_post_save, sender=User)
2031

    
2032

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

    
2036
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2037

    
2038

    
2039
def resource_post_save(sender, instance, created, **kwargs):
2040
    pass
2041

    
2042
post_save.connect(resource_post_save, sender=Resource)
2043

    
2044

    
2045
def renew_token(sender, instance, **kwargs):
2046
    if not instance.auth_token:
2047
        instance.renew_token()
2048
pre_save.connect(renew_token, sender=AstakosUser)
2049
pre_save.connect(renew_token, sender=Component)