Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (65.9 kB)

1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import uuid
35
import logging
36
import json
37
import copy
38

    
39
from datetime import datetime, timedelta
40
import base64
41
from urllib import quote
42
from random import randint
43
import os
44

    
45
from django.db import models, transaction
46
from django.contrib.auth.models import User, UserManager, Group, Permission
47
from django.utils.translation import ugettext as _
48
from django.db.models.signals import pre_save, post_save
49
from django.contrib.contenttypes.models import ContentType
50

    
51
from django.db.models import Q
52
from django.core.urlresolvers import reverse
53
from django.utils.http import int_to_base36
54
from django.contrib.auth.tokens import default_token_generator
55
from django.conf import settings
56
from django.utils.importlib import import_module
57
from django.utils.safestring import mark_safe
58

    
59
from synnefo.lib.utils import dict_merge
60

    
61
from astakos.im import settings as astakos_settings
62
from astakos.im import auth_providers as auth
63

    
64
import astakos.im.messages as astakos_messages
65
from synnefo.lib.ordereddict import OrderedDict
66

    
67
from synnefo.util.text import uenc, udec
68
from synnefo.util import units
69
from astakos.im import presentation
70

    
71
logger = logging.getLogger(__name__)
72

    
73
DEFAULT_CONTENT_TYPE = None
74
_content_type = None
75

    
76

    
77
def get_content_type():
78
    global _content_type
79
    if _content_type is not None:
80
        return _content_type
81

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

    
90
inf = float('inf')
91

    
92

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

    
97

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

    
107

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

    
119

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

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

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

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

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

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

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

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

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

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

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

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

    
196

    
197
_presentation_data = {}
198

    
199

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

    
209

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

    
215

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

    
219

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

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

    
228

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

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

    
242
    def full_name(self):
243
        return str(self)
244

    
245
    def get_info(self):
246
        return {'service': self.service_origin,
247
                'description': self.desc,
248
                'unit': self.unit,
249
                'ui_visible': self.ui_visible,
250
                'api_visible': self.api_visible,
251
                }
252

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

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

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

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

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

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

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

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

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

    
298

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

    
305

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

    
313

    
314
class AstakosUserManager(UserManager):
315

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

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

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

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

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

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

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

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

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

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

    
377

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
471
    objects = AstakosUserManager()
472

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
710
        modules = astakos_settings.IM_MODULES
711

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

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

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

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

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

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

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

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

    
751
        msg_extra = ''
752
        message = ''
753

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

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

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

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

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

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

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

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

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

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

    
825

    
826
class AstakosUserAuthProviderManager(models.Manager):
827

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

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

    
840
    def unverified(self, provider, **filters):
841
        try:
842
            return self.get(module=provider, user__email_verified=False,
843
                            **filters).settings
844
        except AstakosUserAuthProvider.DoesNotExist:
845
            return None
846

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

    
854

    
855
class AuthProviderPolicyProfileManager(models.Manager):
856

    
857
    def active(self):
858
        return self.filter(active=True)
859

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

    
866
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
867
            policies.update(profile.policies)
868

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

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

    
890

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

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

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

    
909
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
910
                     'automoderate')
911

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

    
918
    objects = AuthProviderPolicyProfileManager()
919

    
920
    class Meta:
921
        ordering = ['priority']
922

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

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

    
939

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

    
958
    objects = AstakosUserAuthProviderManager()
959

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

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

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

    
976
    @property
977
    def settings(self):
978
        extra_data = {}
979

    
980
        info_data = {}
981
        if self.info_data:
982
            info_data = json.loads(self.info_data)
983

    
984
        extra_data['info'] = info_data
985

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

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

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

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

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

    
1008

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

    
1014
    class Meta:
1015
        unique_together = ("resource", "user")
1016

    
1017

    
1018
class ApprovalTerms(models.Model):
1019
    """
1020
    Model for approval terms
1021
    """
1022

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

    
1027

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

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

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

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

    
1055

    
1056
class EmailChangeManager(models.Manager):
1057

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

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

1067
        If the key is not valid or has expired, return ``None``.
1068

1069
        If the key is valid but the ``User`` is already active,
1070
        return ``None``.
1071

1072
        After successful email change the activation record is deleted.
1073

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

    
1104

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

    
1117
    objects = EmailChangeManager()
1118

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

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

    
1128

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

    
1136

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

    
1146

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

    
1155

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

    
1178
    class Meta:
1179
        unique_together = ("provider", "third_party_identifier")
1180

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

    
1197
        return user
1198

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

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

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

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

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

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

    
1238

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

    
1243

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

    
1249
    class Meta:
1250
        unique_together = ("user", "setting")
1251

    
1252

    
1253
### PROJECTS ###
1254
################
1255

    
1256
class Chain(models.Model):
1257
    chain = models.AutoField(primary_key=True)
1258

    
1259
    def __str__(self):
1260
        return "%s" % (self.chain,)
1261

    
1262

    
1263
def new_chain():
1264
    c = Chain.objects.create()
1265
    return c
1266

    
1267

    
1268
class ProjectApplicationManager(models.Manager):
1269

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

    
1282

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

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

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

    
1330
    objects = ProjectApplicationManager()
1331

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

    
1337
    class Meta:
1338
        unique_together = ("chain", "id")
1339

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

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

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

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

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

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

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

    
1377
    def chained_applications(self):
1378
        return ProjectApplication.objects.filter(chain=self.chain)
1379

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

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

    
1392
    def has_denied_modifications(self):
1393
        return bool(self.last_denied())
1394

    
1395
    def can_cancel(self):
1396
        return self.state == self.PENDING
1397

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

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

    
1410
    def can_dismiss(self):
1411
        return self.state == self.DENIED
1412

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

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

    
1425
    def can_deny(self):
1426
        return self.state == self.PENDING
1427

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

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

    
1440
    def can_approve(self):
1441
        return self.state == self.PENDING
1442

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

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

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

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

    
1466

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

    
1474

    
1475
class ProjectResourceGrant(models.Model):
1476

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

    
1483
    objects = ProjectResourceGrantManager()
1484

    
1485
    class Meta:
1486
        unique_together = ("resource", "project_application")
1487

    
1488
    def display_member_capacity(self):
1489
        return units.show(self.member_capacity, self.resource.unit)
1490

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

    
1495

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

    
1507

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

    
1511

    
1512
class ProjectManager(models.Manager):
1513

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

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

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

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

    
1546
        relevant = model.o_states_q(model.RELEVANT_STATES)
1547
        return self.filter(flt, relevant).order_by(
1548
            'application__issue_date').select_related(
1549
            'application', 'application__owner', 'application__applicant')
1550

    
1551
    def search_by_name(self, *search_strings):
1552
        q = Q()
1553
        for s in search_strings:
1554
            q = q | Q(name__icontains=s)
1555
        return self.filter(q)
1556

    
1557

    
1558
class Project(models.Model):
1559

    
1560
    id = models.BigIntegerField(db_column='id', primary_key=True)
1561

    
1562
    application = models.OneToOneField(
1563
        ProjectApplication,
1564
        related_name='project')
1565

    
1566
    members = models.ManyToManyField(
1567
        AstakosUser,
1568
        through='ProjectMembership')
1569

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

    
1577
    NORMAL = 1
1578
    SUSPENDED = 10
1579
    TERMINATED = 100
1580

    
1581
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1582

    
1583
    state = models.IntegerField(default=NORMAL,
1584
                                db_index=True)
1585

    
1586
    objects = ProjectManager()
1587

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

    
1592
    __repr__ = __str__
1593

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

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

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

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

    
1625
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1626

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

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

    
1636
    INITIALIZED_STATES = [O_ACTIVE,
1637
                          O_SUSPENDED,
1638
                          O_TERMINATED,
1639
                          ]
1640

    
1641
    RELEVANT_STATES = [O_PENDING,
1642
                       O_DENIED,
1643
                       O_ACTIVE,
1644
                       O_SUSPENDED,
1645
                       O_TERMINATED,
1646
                       ]
1647

    
1648
    SKIP_STATES = [O_DISMISSED,
1649
                   O_CANCELLED,
1650
                   O_TERMINATED,
1651
                   ]
1652

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

    
1657
    def overall_state(self):
1658
        return self._overall_state(self.state, self.application.state)
1659

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

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

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

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

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

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

    
1691
        return self.state != self.NORMAL
1692

    
1693
    def is_active(self):
1694
        return self.overall_state() == self.O_ACTIVE
1695

    
1696
    def is_initialized(self):
1697
        return self.overall_state() in self.INITIALIZED_STATES
1698

    
1699
    ### Deactivation calls
1700

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

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

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

    
1718
    def suspend(self, actor=None, reason=None):
1719
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1720

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

    
1727
    ### Logical checks
1728

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

    
1733
    @property
1734
    def is_terminated(self):
1735
        return self.is_deactivated(self.TERMINATED)
1736

    
1737
    @property
1738
    def is_suspended(self):
1739
        return self.is_deactivated(self.SUSPENDED)
1740

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

    
1748
    ### Other
1749

    
1750
    def count_pending_memberships(self):
1751
        return self.projectmembership_set.requested().count()
1752

    
1753
    def members_count(self):
1754
        return self.approved_memberships.count()
1755

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

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

    
1765

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

    
1773

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

    
1783
    objects = ProjectLogManager()
1784

    
1785

    
1786
class ProjectLock(models.Model):
1787
    pass
1788

    
1789

    
1790
class ProjectMembershipManager(models.Manager):
1791

    
1792
    def any_accepted(self):
1793
        q = self.model.Q_ACCEPTED_STATES
1794
        return self.filter(q)
1795

    
1796
    def actually_accepted(self):
1797
        q = self.model.Q_ACTUALLY_ACCEPTED
1798
        return self.filter(q)
1799

    
1800
    def requested(self):
1801
        return self.filter(state=ProjectMembership.REQUESTED)
1802

    
1803
    def suspended(self):
1804
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1805

    
1806
    def associated(self):
1807
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1808

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

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

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

    
1827

    
1828
class ProjectMembership(models.Model):
1829

    
1830
    person = models.ForeignKey(AstakosUser)
1831
    project = models.ForeignKey(Project)
1832

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

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

    
1848
    ACCEPTED_STATES = set([ACCEPTED,
1849
                           LEAVE_REQUESTED,
1850
                           USER_SUSPENDED,
1851
                           ])
1852

    
1853
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1854

    
1855
    state = models.IntegerField(default=REQUESTED,
1856
                                db_index=True)
1857

    
1858
    objects = ProjectMembershipManager()
1859

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

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

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

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

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

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

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

    
1898
    __repr__ = __str__
1899

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

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

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

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

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

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

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

    
1962

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

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

    
1972

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

    
1982
    objects = ProjectMembershipLogManager()
1983

    
1984

    
1985
### SIGNALS ###
1986
################
1987

    
1988
def resource_post_save(sender, instance, created, **kwargs):
1989
    pass
1990

    
1991
post_save.connect(resource_post_save, sender=Resource)
1992

    
1993

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