Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (67 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
    auth_token = models.CharField(_('Authentication Token'), max_length=64,
128
                                  null=True, blank=True, unique=True)
129
    auth_token_created = models.DateTimeField(_('Token creation date'),
130
                                              null=True)
131
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
132
                                              null=True)
133

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

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

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

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

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

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

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

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

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

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

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

    
197

    
198
_presentation_data = {}
199

    
200

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

    
210

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

    
216

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

    
220

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

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

    
229

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

    
239
    objects = ForUpdateManager()
240

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

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

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

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

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

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

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

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

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

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

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

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

    
299

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

    
306

    
307
class AstakosUserManager(UserManager):
308

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

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

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

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

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

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

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

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

    
362

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
456
    objects = AstakosUserManager()
457
    forupdate = ForUpdateManager()
458

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

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

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

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

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

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

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

    
505
    def is_project_admin(self, application_id=None):
506
        return self.uuid in astakos_settings.PROJECT_ADMINS
507

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

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

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

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

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

    
543
        self.update_uuid()
544

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
730
        modules = astakos_settings.IM_MODULES
731

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

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

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

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

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

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

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

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

    
768
        msg_extra = ''
769
        message = ''
770

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

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

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

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

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

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

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

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

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

    
836

    
837
class AstakosUserAuthProviderManager(models.Manager):
838

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

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

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

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

    
865

    
866
class AuthProviderPolicyProfileManager(models.Manager):
867

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

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

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

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

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

    
901

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

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

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

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

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

    
929
    objects = AuthProviderPolicyProfileManager()
930

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

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

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

    
950

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

    
969
    objects = AstakosUserAuthProviderManager()
970

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

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

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

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

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

    
995
        extra_data['info'] = info_data
996

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

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

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

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

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

    
1019

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

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

    
1028

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

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

    
1038

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

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

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

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

    
1066

    
1067
class EmailChangeManager(models.Manager):
1068

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

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

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

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

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

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

    
1115

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

    
1128
    objects = EmailChangeManager()
1129

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

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

    
1139

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

    
1147

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

    
1157

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

    
1166

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

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

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

    
1208
        return user
1209

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

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

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

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

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

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

    
1252

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

    
1257

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

    
1263
    objects = ForUpdateManager()
1264

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

    
1268

    
1269
### PROJECTS ###
1270
################
1271

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

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

    
1279

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

    
1284

    
1285
class ProjectApplicationManager(ForUpdateManager):
1286

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

    
1299

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

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

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

    
1347
    objects = ProjectApplicationManager()
1348

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1483

    
1484
class ProjectResourceGrant(models.Model):
1485

    
1486
    resource = models.ForeignKey(Resource)
1487
    project_application = models.ForeignKey(ProjectApplication,
1488
                                            null=True)
1489
    project_capacity = intDecimalField(null=True)
1490
    member_capacity = intDecimalField(default=0)
1491

    
1492
    class Meta:
1493
        unique_together = ("resource", "project_application")
1494

    
1495
    def display_member_capacity(self):
1496
        return units.show(self.member_capacity, self.resource.unit)
1497

    
1498
    def __str__(self):
1499
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1500
                                        self.display_member_capacity())
1501

    
1502

    
1503
def _distinct(f, l):
1504
    d = {}
1505
    last = None
1506
    for x in l:
1507
        group = f(x)
1508
        if group == last:
1509
            continue
1510
        last = group
1511
        d[group] = x
1512
    return d
1513

    
1514

    
1515
def invert_dict(d):
1516
    return dict((v, k) for k, v in d.iteritems())
1517

    
1518

    
1519
class ProjectManager(ForUpdateManager):
1520

    
1521
    def all_with_pending(self, flt=None):
1522
        flt = Q() if flt is None else flt
1523
        projects = list(self.select_related(
1524
            'application', 'application__owner').filter(flt))
1525

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

    
1532
    def expired_projects(self):
1533
        model = self.model
1534
        q = ((model.o_state_q(model.O_ACTIVE) |
1535
              model.o_state_q(model.O_SUSPENDED)) &
1536
             Q(application__end_date__lt=datetime.now()))
1537
        return self.filter(q)
1538

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

    
1553
        relevant = model.o_states_q(model.RELEVANT_STATES)
1554
        return self.filter(flt, relevant).order_by(
1555
            'application__issue_date').select_related(
1556
                'application', 'application__owner', 'application__applicant')
1557

    
1558
    def search_by_name(self, *search_strings):
1559
        q = Q()
1560
        for s in search_strings:
1561
            q = q | Q(name__icontains=s)
1562
        return self.filter(q)
1563

    
1564

    
1565
class Project(models.Model):
1566

    
1567
    id = models.BigIntegerField(db_column='id', primary_key=True)
1568

    
1569
    application = models.OneToOneField(
1570
        ProjectApplication,
1571
        related_name='project')
1572

    
1573
    members = models.ManyToManyField(
1574
        AstakosUser,
1575
        through='ProjectMembership')
1576

    
1577
    creation_date = models.DateTimeField(auto_now_add=True)
1578
    name = models.CharField(
1579
        max_length=80,
1580
        null=True,
1581
        db_index=True,
1582
        unique=True)
1583

    
1584
    NORMAL = 1
1585
    SUSPENDED = 10
1586
    TERMINATED = 100
1587

    
1588
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1589

    
1590
    state = models.IntegerField(default=NORMAL,
1591
                                db_index=True)
1592

    
1593
    objects = ProjectManager()
1594

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

    
1599
    __repr__ = __str__
1600

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

    
1604
    O_PENDING = 0
1605
    O_ACTIVE = 1
1606
    O_DENIED = 3
1607
    O_DISMISSED = 4
1608
    O_CANCELLED = 5
1609
    O_SUSPENDED = 10
1610
    O_TERMINATED = 100
1611

    
1612
    O_STATE_DISPLAY = {
1613
        O_PENDING:    _("Pending"),
1614
        O_ACTIVE:     _("Active"),
1615
        O_DENIED:     _("Denied"),
1616
        O_DISMISSED:  _("Dismissed"),
1617
        O_CANCELLED:  _("Cancelled"),
1618
        O_SUSPENDED:  _("Suspended"),
1619
        O_TERMINATED: _("Terminated"),
1620
    }
1621

    
1622
    OVERALL_STATE = {
1623
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1624
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1625
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1626
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1627
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1628
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1629
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1630
    }
1631

    
1632
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1633

    
1634
    @classmethod
1635
    def o_state_q(cls, o_state):
1636
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1637
        return Q(state=p_state, application__state=a_state)
1638

    
1639
    @classmethod
1640
    def o_states_q(cls, o_states):
1641
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1642

    
1643
    INITIALIZED_STATES = [O_ACTIVE,
1644
                          O_SUSPENDED,
1645
                          O_TERMINATED,
1646
                          ]
1647

    
1648
    RELEVANT_STATES = [O_PENDING,
1649
                       O_DENIED,
1650
                       O_ACTIVE,
1651
                       O_SUSPENDED,
1652
                       O_TERMINATED,
1653
                       ]
1654

    
1655
    SKIP_STATES = [O_DISMISSED,
1656
                   O_CANCELLED,
1657
                   O_TERMINATED,
1658
                   ]
1659

    
1660
    @classmethod
1661
    def _overall_state(cls, project_state, app_state):
1662
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1663

    
1664
    def overall_state(self):
1665
        return self._overall_state(self.state, self.application.state)
1666

    
1667
    def last_pending_application(self):
1668
        apps = self.chained_apps.filter(
1669
            state=ProjectApplication.PENDING).order_by('-id')
1670
        if apps:
1671
            return apps[0]
1672
        return None
1673

    
1674
    def last_pending_modification(self):
1675
        last_pending = self.last_pending_application()
1676
        if last_pending == self.application:
1677
            return None
1678
        return last_pending
1679

    
1680
    def state_display(self):
1681
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1682

    
1683
    def expiration_info(self):
1684
        return (str(self.id), self.name, self.state_display(),
1685
                str(self.application.end_date))
1686

    
1687
    def last_deactivation(self):
1688
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1689
        ls = objs.order_by("-date")
1690
        if not ls:
1691
            return None
1692
        return ls[0]
1693

    
1694
    def is_deactivated(self, reason=None):
1695
        if reason is not None:
1696
            return self.state == reason
1697

    
1698
        return self.state != self.NORMAL
1699

    
1700
    def is_active(self):
1701
        return self.overall_state() == self.O_ACTIVE
1702

    
1703
    def is_initialized(self):
1704
        return self.overall_state() in self.INITIALIZED_STATES
1705

    
1706
    ### Deactivation calls
1707

    
1708
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1709
                    comments=None):
1710
        now = datetime.now()
1711
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1712
                        actor=actor, reason=reason, comments=comments)
1713

    
1714
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1715
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1716
                         comments=comments)
1717
        self.state = to_state
1718
        self.save()
1719

    
1720
    def terminate(self, actor=None, reason=None):
1721
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1722
        self.name = None
1723
        self.save()
1724

    
1725
    def suspend(self, actor=None, reason=None):
1726
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1727

    
1728
    def resume(self, actor=None, reason=None):
1729
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1730
        if self.name is None:
1731
            self.name = self.application.name
1732
            self.save()
1733

    
1734
    ### Logical checks
1735

    
1736
    @property
1737
    def is_alive(self):
1738
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1739

    
1740
    @property
1741
    def is_terminated(self):
1742
        return self.is_deactivated(self.TERMINATED)
1743

    
1744
    @property
1745
    def is_suspended(self):
1746
        return self.is_deactivated(self.SUSPENDED)
1747

    
1748
    def violates_members_limit(self, adding=0):
1749
        application = self.application
1750
        limit = application.limit_on_members_number
1751
        if limit is None:
1752
            return False
1753
        return (len(self.approved_members) + adding > limit)
1754

    
1755
    ### Other
1756

    
1757
    def count_pending_memberships(self):
1758
        return self.projectmembership_set.requested().count()
1759

    
1760
    def members_count(self):
1761
        return self.approved_memberships.count()
1762

    
1763
    @property
1764
    def approved_memberships(self):
1765
        query = ProjectMembership.Q_ACCEPTED_STATES
1766
        return self.projectmembership_set.filter(query)
1767

    
1768
    @property
1769
    def approved_members(self):
1770
        return [m.person for m in self.approved_memberships]
1771

    
1772

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

    
1780

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

    
1790
    objects = ProjectLogManager()
1791

    
1792

    
1793
class ProjectLock(models.Model):
1794
    objects = ForUpdateManager()
1795

    
1796

    
1797
class ProjectMembershipManager(ForUpdateManager):
1798

    
1799
    def any_accepted(self):
1800
        q = self.model.Q_ACCEPTED_STATES
1801
        return self.filter(q)
1802

    
1803
    def actually_accepted(self):
1804
        q = self.model.Q_ACTUALLY_ACCEPTED
1805
        return self.filter(q)
1806

    
1807
    def requested(self):
1808
        return self.filter(state=ProjectMembership.REQUESTED)
1809

    
1810
    def suspended(self):
1811
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1812

    
1813
    def associated(self):
1814
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1815

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

    
1820
    def requested_per_project(self, projects):
1821
        ms = self.requested().filter(project__in=projects)
1822
        return _partition_by(lambda m: m.project_id, ms)
1823

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

    
1834

    
1835
class ProjectMembership(models.Model):
1836

    
1837
    person = models.ForeignKey(AstakosUser)
1838
    project = models.ForeignKey(Project)
1839

    
1840
    REQUESTED = 0
1841
    ACCEPTED = 1
1842
    LEAVE_REQUESTED = 5
1843
    # User deactivation
1844
    USER_SUSPENDED = 10
1845
    REJECTED = 100
1846
    CANCELLED = 101
1847
    REMOVED = 200
1848

    
1849
    ASSOCIATED_STATES = set([REQUESTED,
1850
                             ACCEPTED,
1851
                             LEAVE_REQUESTED,
1852
                             USER_SUSPENDED,
1853
                             ])
1854

    
1855
    ACCEPTED_STATES = set([ACCEPTED,
1856
                           LEAVE_REQUESTED,
1857
                           USER_SUSPENDED,
1858
                           ])
1859

    
1860
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1861

    
1862
    state = models.IntegerField(default=REQUESTED,
1863
                                db_index=True)
1864

    
1865
    objects = ProjectMembershipManager()
1866

    
1867
    # Compiled queries
1868
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1869
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1870

    
1871
    MEMBERSHIP_STATE_DISPLAY = {
1872
        REQUESTED:       _('Requested'),
1873
        ACCEPTED:        _('Accepted'),
1874
        LEAVE_REQUESTED: _('Leave Requested'),
1875
        USER_SUSPENDED:  _('Suspended'),
1876
        REJECTED:        _('Rejected'),
1877
        CANCELLED:       _('Cancelled'),
1878
        REMOVED:         _('Removed'),
1879
    }
1880

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

    
1891
    def state_display(self):
1892
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1893

    
1894
    def user_friendly_state_display(self):
1895
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1896

    
1897
    class Meta:
1898
        unique_together = ("person", "project")
1899
        #index_together = [["project", "state"]]
1900

    
1901
    def __str__(self):
1902
        return uenc(_("<'%s' membership in '%s'>") %
1903
                    (self.person.username, self.project))
1904

    
1905
    __repr__ = __str__
1906

    
1907
    def latest_log(self):
1908
        logs = self.log.all()
1909
        logs_d = _partition_by(lambda l: l.to_state, logs)
1910
        for s, s_logs in logs_d.iteritems():
1911
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1912
        return logs_d
1913

    
1914
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1915
                    comments=None):
1916
        now = datetime.now()
1917
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1918
                        actor=actor, reason=reason, comments=comments)
1919

    
1920
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1921
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1922
                         comments=comments)
1923
        self.state = to_state
1924
        self.save()
1925

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

    
1939
    ACTION_STATES = {
1940
        "join":          REQUESTED,
1941
        "accept":        ACCEPTED,
1942
        "enroll":        ACCEPTED,
1943
        "leave_request": LEAVE_REQUESTED,
1944
        "deny_leave":    ACCEPTED,
1945
        "cancel_leave":  ACCEPTED,
1946
        "remove":        REMOVED,
1947
        "reject":        REJECTED,
1948
        "cancel":        CANCELLED,
1949
    }
1950

    
1951
    def check_action(self, action):
1952
        try:
1953
            check = self.ACTION_CHECKS[action]
1954
        except KeyError:
1955
            raise ValueError("No check found for action '%s'" % action)
1956
        return check(self)
1957

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

    
1969

    
1970
class ProjectMembershipLogManager(models.Manager):
1971
    def last_logs(self, memberships):
1972
        logs = self.filter(membership__in=memberships).order_by("-date")
1973
        logs = _partition_by(lambda l: l.membership_id, logs)
1974

    
1975
        for memb_id, m_logs in logs.iteritems():
1976
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
1977
        return logs
1978

    
1979

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

    
1989
    objects = ProjectMembershipLogManager()
1990

    
1991

    
1992
### SIGNALS ###
1993
################
1994

    
1995
def create_astakos_user(u):
1996
    try:
1997
        AstakosUser.objects.get(user_ptr=u.pk)
1998
    except AstakosUser.DoesNotExist:
1999
        extended_user = AstakosUser(user_ptr_id=u.pk)
2000
        extended_user.__dict__.update(u.__dict__)
2001
        extended_user.save()
2002
        if not extended_user.has_auth_provider('local'):
2003
            extended_user.add_auth_provider('local')
2004
    except BaseException, e:
2005
        logger.exception(e)
2006

    
2007

    
2008
def fix_superusers():
2009
    # Associate superusers with AstakosUser
2010
    admins = User.objects.filter(is_superuser=True)
2011
    for u in admins:
2012
        create_astakos_user(u)
2013

    
2014

    
2015
def user_post_save(sender, instance, created, **kwargs):
2016
    if not created:
2017
        return
2018
    create_astakos_user(instance)
2019
post_save.connect(user_post_save, sender=User)
2020

    
2021

    
2022
def astakosuser_post_save(sender, instance, created, **kwargs):
2023
    pass
2024

    
2025
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2026

    
2027

    
2028
def resource_post_save(sender, instance, created, **kwargs):
2029
    pass
2030

    
2031
post_save.connect(resource_post_save, sender=Resource)
2032

    
2033

    
2034
def renew_token(sender, instance, **kwargs):
2035
    if not instance.auth_token:
2036
        instance.renew_token()
2037
pre_save.connect(renew_token, sender=AstakosUser)
2038
pre_save.connect(renew_token, sender=Component)