Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (65.4 kB)

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

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

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

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

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

    
59
from synnefo.lib.utils import dict_merge
60

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

    
64
import astakos.im.messages as astakos_messages
65
from 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
        msg = 'Token renewed for %s'
566
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
567

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
709
        modules = astakos_settings.IM_MODULES
710

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

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

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

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

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

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

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

    
744
    def get_inactive_message(self, provider_module, identifier=None):
745
        provider = self.get_auth_provider(provider_module, identifier)
746

    
747
        msg_extra = ''
748
        message = ''
749

    
750
        msg_inactive = provider.get_account_inactive_msg
751
        msg_pending = provider.get_pending_activation_msg
752
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
753
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
754
        msg_pending_mod = provider.get_pending_moderation_msg
755
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
756
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
757

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

    
773
        return mark_safe(message + u' ' + msg_extra)
774

    
775
    def owns_application(self, application):
776
        return application.owner == self
777

    
778
    def owns_project(self, project):
779
        return project.application.owner == self
780

    
781
    def is_associated(self, project):
782
        try:
783
            m = ProjectMembership.objects.get(person=self, project=project)
784
            return m.state in ProjectMembership.ASSOCIATED_STATES
785
        except ProjectMembership.DoesNotExist:
786
            return False
787

    
788
    def get_membership(self, project):
789
        try:
790
            return ProjectMembership.objects.get(
791
                project=project,
792
                person=self)
793
        except ProjectMembership.DoesNotExist:
794
            return None
795

    
796
    def membership_display(self, project):
797
        m = self.get_membership(project)
798
        if m is None:
799
            return _('Not a member')
800
        else:
801
            return m.user_friendly_state_display()
802

    
803
    def non_owner_can_view(self, maybe_project):
804
        if self.is_project_admin():
805
            return True
806
        if maybe_project is None:
807
            return False
808
        project = maybe_project
809
        if self.is_associated(project):
810
            return True
811
        if project.is_deactivated():
812
            return False
813
        return True
814

    
815

    
816
class AstakosUserAuthProviderManager(models.Manager):
817

    
818
    def active(self, **filters):
819
        return self.filter(active=True, **filters)
820

    
821
    def remove_unverified_providers(self, provider, **filters):
822
        try:
823
            existing = self.filter(module=provider, user__email_verified=False,
824
                                   **filters)
825
            for p in existing:
826
                p.user.delete()
827
        except:
828
            pass
829

    
830
    def unverified(self, provider, **filters):
831
        try:
832
            return self.get(module=provider, user__email_verified=False,
833
                            **filters).settings
834
        except AstakosUserAuthProvider.DoesNotExist:
835
            return None
836

    
837
    def verified(self, provider, **filters):
838
        try:
839
            return self.get(module=provider, user__email_verified=True,
840
                            **filters).settings
841
        except AstakosUserAuthProvider.DoesNotExist:
842
            return None
843

    
844

    
845
class AuthProviderPolicyProfileManager(models.Manager):
846

    
847
    def active(self):
848
        return self.filter(active=True)
849

    
850
    def for_user(self, user, provider):
851
        policies = {}
852
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
853
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
854
        exclusive_q = exclusive_q1 | exclusive_q2
855

    
856
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
857
            policies.update(profile.policies)
858

    
859
        user_groups = user.groups.all().values('pk')
860
        for profile in self.active().filter(groups__in=user_groups).filter(
861
                exclusive_q):
862
            policies.update(profile.policies)
863
        return policies
864

    
865
    def add_policy(self, name, provider, group_or_user, exclusive=False,
866
                   **policies):
867
        is_group = isinstance(group_or_user, Group)
868
        profile, created = self.get_or_create(name=name, provider=provider,
869
                                              is_exclusive=exclusive)
870
        profile.is_exclusive = exclusive
871
        profile.save()
872
        if is_group:
873
            profile.groups.add(group_or_user)
874
        else:
875
            profile.users.add(group_or_user)
876
        profile.set_policies(policies)
877
        profile.save()
878
        return profile
879

    
880

    
881
class AuthProviderPolicyProfile(models.Model):
882
    name = models.CharField(_('Name'), max_length=255, blank=False,
883
                            null=False, db_index=True)
884
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
885
                                null=False)
886

    
887
    # apply policies to all providers excluding the one set in provider field
888
    is_exclusive = models.BooleanField(default=False)
889

    
890
    policy_add = models.NullBooleanField(null=True, default=None)
891
    policy_remove = models.NullBooleanField(null=True, default=None)
892
    policy_create = models.NullBooleanField(null=True, default=None)
893
    policy_login = models.NullBooleanField(null=True, default=None)
894
    policy_limit = models.IntegerField(null=True, default=None)
895
    policy_required = models.NullBooleanField(null=True, default=None)
896
    policy_automoderate = models.NullBooleanField(null=True, default=None)
897
    policy_switch = models.NullBooleanField(null=True, default=None)
898

    
899
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
900
                     'automoderate')
901

    
902
    priority = models.IntegerField(null=False, default=1)
903
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
904
    users = models.ManyToManyField(AstakosUser,
905
                                   related_name='authpolicy_profiles')
906
    active = models.BooleanField(default=True)
907

    
908
    objects = AuthProviderPolicyProfileManager()
909

    
910
    class Meta:
911
        ordering = ['priority']
912

    
913
    @property
914
    def policies(self):
915
        policies = {}
916
        for pkey in self.POLICY_FIELDS:
917
            value = getattr(self, 'policy_%s' % pkey, None)
918
            if value is None:
919
                continue
920
            policies[pkey] = value
921
        return policies
922

    
923
    def set_policies(self, policies_dict):
924
        for key, value in policies_dict.iteritems():
925
            if key in self.POLICY_FIELDS:
926
                setattr(self, 'policy_%s' % key, value)
927
        return self.policies
928

    
929

    
930
class AstakosUserAuthProvider(models.Model):
931
    """
932
    Available user authentication methods.
933
    """
934
    affiliation = models.CharField(_('Affiliation'), max_length=255,
935
                                   blank=True, null=True, default=None)
936
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
937
    module = models.CharField(_('Provider'), max_length=255, blank=False,
938
                              default='local')
939
    identifier = models.CharField(_('Third-party identifier'),
940
                                  max_length=255, null=True,
941
                                  blank=True)
942
    active = models.BooleanField(default=True)
943
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
944
                                    default='astakos')
945
    info_data = models.TextField(default="", null=True, blank=True)
946
    created = models.DateTimeField('Creation date', auto_now_add=True)
947

    
948
    objects = AstakosUserAuthProviderManager()
949

    
950
    class Meta:
951
        unique_together = (('identifier', 'module', 'user'), )
952
        ordering = ('module', 'created')
953

    
954
    def __init__(self, *args, **kwargs):
955
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
956
        try:
957
            self.info = json.loads(self.info_data)
958
            if not self.info:
959
                self.info = {}
960
        except Exception:
961
            self.info = {}
962

    
963
        for key, value in self.info.iteritems():
964
            setattr(self, 'info_%s' % key, value)
965

    
966
    @property
967
    def settings(self):
968
        extra_data = {}
969

    
970
        info_data = {}
971
        if self.info_data:
972
            info_data = json.loads(self.info_data)
973

    
974
        extra_data['info'] = info_data
975

    
976
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
977
            extra_data[key] = getattr(self, key)
978

    
979
        extra_data['instance'] = self
980
        return auth.get_provider(self.module, self.user,
981
                                 self.identifier, **extra_data)
982

    
983
    def __repr__(self):
984
        return '<AstakosUserAuthProvider %s:%s>' % (
985
            self.module, self.identifier)
986

    
987
    def __unicode__(self):
988
        if self.identifier:
989
            return "%s:%s" % (self.module, self.identifier)
990
        if self.auth_backend:
991
            return "%s:%s" % (self.module, self.auth_backend)
992
        return self.module
993

    
994
    def save(self, *args, **kwargs):
995
        self.info_data = json.dumps(self.info)
996
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
997

    
998

    
999
class AstakosUserQuota(models.Model):
1000
    capacity = models.BigIntegerField()
1001
    resource = models.ForeignKey(Resource)
1002
    user = models.ForeignKey(AstakosUser)
1003

    
1004
    class Meta:
1005
        unique_together = ("resource", "user")
1006

    
1007

    
1008
class ApprovalTerms(models.Model):
1009
    """
1010
    Model for approval terms
1011
    """
1012

    
1013
    date = models.DateTimeField(
1014
        _('Issue date'), db_index=True, auto_now_add=True)
1015
    location = models.CharField(_('Terms location'), max_length=255)
1016

    
1017

    
1018
class Invitation(models.Model):
1019
    """
1020
    Model for registring invitations
1021
    """
1022
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1023
                                null=True)
1024
    realname = models.CharField(_('Real name'), max_length=255)
1025
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1026
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1027
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1028
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1029
    consumed = models.DateTimeField(_('Consumption date'),
1030
                                    null=True, blank=True)
1031

    
1032
    def __init__(self, *args, **kwargs):
1033
        super(Invitation, self).__init__(*args, **kwargs)
1034
        if not self.id:
1035
            self.code = _generate_invitation_code()
1036

    
1037
    def consume(self):
1038
        self.is_consumed = True
1039
        self.consumed = datetime.now()
1040
        self.save()
1041

    
1042
    def __unicode__(self):
1043
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1044

    
1045

    
1046
class EmailChangeManager(models.Manager):
1047

    
1048
    @transaction.commit_on_success
1049
    def change_email(self, activation_key):
1050
        """
1051
        Validate an activation key and change the corresponding
1052
        ``User`` if valid.
1053

1054
        If the key is valid and has not expired, return the ``User``
1055
        after activating.
1056

1057
        If the key is not valid or has expired, return ``None``.
1058

1059
        If the key is valid but the ``User`` is already active,
1060
        return ``None``.
1061

1062
        After successful email change the activation record is deleted.
1063

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

    
1094

    
1095
class EmailChange(models.Model):
1096
    new_email_address = models.EmailField(
1097
        _(u'new e-mail address'),
1098
        help_text=_('Provide a new email address. Until you verify the new '
1099
                    'address by following the activation link that will be '
1100
                    'sent to it, your old email address will remain active.'))
1101
    user = models.ForeignKey(
1102
        AstakosUser, unique=True, related_name='emailchanges')
1103
    requested_at = models.DateTimeField(auto_now_add=True)
1104
    activation_key = models.CharField(
1105
        max_length=40, unique=True, db_index=True)
1106

    
1107
    objects = EmailChangeManager()
1108

    
1109
    def get_url(self):
1110
        return reverse('email_change_confirm',
1111
                       kwargs={'activation_key': self.activation_key})
1112

    
1113
    def activation_key_expired(self):
1114
        expiration_date = timedelta(
1115
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1116
        return self.requested_at + expiration_date < datetime.now()
1117

    
1118

    
1119
class AdditionalMail(models.Model):
1120
    """
1121
    Model for registring invitations
1122
    """
1123
    owner = models.ForeignKey(AstakosUser)
1124
    email = models.EmailField()
1125

    
1126

    
1127
def _generate_invitation_code():
1128
    while True:
1129
        code = randint(1, 2L ** 63 - 1)
1130
        try:
1131
            Invitation.objects.get(code=code)
1132
            # An invitation with this code already exists, try again
1133
        except Invitation.DoesNotExist:
1134
            return code
1135

    
1136

    
1137
def get_latest_terms():
1138
    try:
1139
        term = ApprovalTerms.objects.order_by('-id')[0]
1140
        return term
1141
    except IndexError:
1142
        pass
1143
    return None
1144

    
1145

    
1146
class PendingThirdPartyUser(models.Model):
1147
    """
1148
    Model for registring successful third party user authentications
1149
    """
1150
    third_party_identifier = models.CharField(
1151
        _('Third-party identifier'), max_length=255, null=True, blank=True)
1152
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1153
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1154
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1155
                                  null=True)
1156
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1157
                                 null=True)
1158
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1159
                                   null=True)
1160
    username = models.CharField(
1161
        _('username'), max_length=30, unique=True,
1162
        help_text=_("Required. 30 characters or fewer. "
1163
                    "Letters, numbers and @/./+/-/_ characters"))
1164
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1165
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1166
    info = models.TextField(default="", null=True, blank=True)
1167

    
1168
    class Meta:
1169
        unique_together = ("provider", "third_party_identifier")
1170

    
1171
    def get_user_instance(self):
1172
        """
1173
        Create a new AstakosUser instance based on details provided when user
1174
        initially signed up.
1175
        """
1176
        d = copy.copy(self.__dict__)
1177
        d.pop('_state', None)
1178
        d.pop('id', None)
1179
        d.pop('token', None)
1180
        d.pop('created', None)
1181
        d.pop('info', None)
1182
        d.pop('affiliation', None)
1183
        d.pop('provider', None)
1184
        d.pop('third_party_identifier', None)
1185
        user = AstakosUser(**d)
1186

    
1187
        return user
1188

    
1189
    @property
1190
    def realname(self):
1191
        return '%s %s' % (self.first_name, self.last_name)
1192

    
1193
    @realname.setter
1194
    def realname(self, value):
1195
        first, last = split_realname(value)
1196
        self.first_name = first
1197
        self.last_name = last
1198

    
1199
    def save(self, *args, **kwargs):
1200
        if not self.id:
1201
            # set username
1202
            while not self.username:
1203
                username = uuid.uuid4().hex[:30]
1204
                try:
1205
                    AstakosUser.objects.get(username=username)
1206
                except AstakosUser.DoesNotExist:
1207
                    self.username = username
1208
        super(PendingThirdPartyUser, self).save(*args, **kwargs)
1209

    
1210
    def generate_token(self):
1211
        self.password = self.third_party_identifier
1212
        self.last_login = datetime.now()
1213
        self.token = default_token_generator.make_token(self)
1214

    
1215
    def existing_user(self):
1216
        return AstakosUser.objects.filter(
1217
            auth_providers__module=self.provider,
1218
            auth_providers__identifier=self.third_party_identifier)
1219

    
1220
    def get_provider(self, user):
1221
        params = {
1222
            'info_data': self.info,
1223
            'affiliation': self.affiliation
1224
        }
1225
        return auth.get_provider(self.provider, user,
1226
                                 self.third_party_identifier, **params)
1227

    
1228

    
1229
class SessionCatalog(models.Model):
1230
    session_key = models.CharField(_('session key'), max_length=40)
1231
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1232

    
1233

    
1234
class UserSetting(models.Model):
1235
    user = models.ForeignKey(AstakosUser)
1236
    setting = models.CharField(max_length=255)
1237
    value = models.IntegerField()
1238

    
1239
    class Meta:
1240
        unique_together = ("user", "setting")
1241

    
1242

    
1243
### PROJECTS ###
1244
################
1245

    
1246
class Chain(models.Model):
1247
    chain = models.AutoField(primary_key=True)
1248

    
1249
    def __str__(self):
1250
        return "%s" % (self.chain,)
1251

    
1252

    
1253
def new_chain():
1254
    c = Chain.objects.create()
1255
    return c
1256

    
1257

    
1258
class ProjectApplicationManager(models.Manager):
1259

    
1260
    def pending_per_project(self, projects):
1261
        apps = self.filter(state=self.model.PENDING,
1262
                           chain__in=projects).order_by('chain', '-id')
1263
        checked_chain = None
1264
        projs = {}
1265
        for app in apps:
1266
            chain = app.chain_id
1267
            if chain != checked_chain:
1268
                checked_chain = chain
1269
                projs[chain] = app
1270
        return projs
1271

    
1272

    
1273
class ProjectApplication(models.Model):
1274
    applicant = models.ForeignKey(
1275
        AstakosUser,
1276
        related_name='projects_applied',
1277
        db_index=True)
1278

    
1279
    PENDING = 0
1280
    APPROVED = 1
1281
    REPLACED = 2
1282
    DENIED = 3
1283
    DISMISSED = 4
1284
    CANCELLED = 5
1285

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

    
1320
    objects = ProjectApplicationManager()
1321

    
1322
    # Compiled queries
1323
    Q_PENDING = Q(state=PENDING)
1324
    Q_APPROVED = Q(state=APPROVED)
1325
    Q_DENIED = Q(state=DENIED)
1326

    
1327
    class Meta:
1328
        unique_together = ("chain", "id")
1329

    
1330
    def __unicode__(self):
1331
        return "%s applied by %s" % (self.name, self.applicant)
1332

    
1333
    # TODO: Move to a more suitable place
1334
    APPLICATION_STATE_DISPLAY = {
1335
        PENDING:   _('Pending review'),
1336
        APPROVED:  _('Approved'),
1337
        REPLACED:  _('Replaced'),
1338
        DENIED:    _('Denied'),
1339
        DISMISSED: _('Dismissed'),
1340
        CANCELLED: _('Cancelled')
1341
    }
1342

    
1343
    @property
1344
    def log_display(self):
1345
        return "application %s (%s) for project %s" % (
1346
            self.id, self.name, self.chain)
1347

    
1348
    def state_display(self):
1349
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1350

    
1351
    @property
1352
    def grants(self):
1353
        return self.projectresourcegrant_set.values('member_capacity',
1354
                                                    'resource__name')
1355

    
1356
    @property
1357
    def resource_policies(self):
1358
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1359

    
1360
    def is_modification(self):
1361
        # if self.state != self.PENDING:
1362
        #     return False
1363
        parents = self.chained_applications().filter(id__lt=self.id)
1364
        parents = parents.filter(state__in=[self.APPROVED])
1365
        return parents.count() > 0
1366

    
1367
    def chained_applications(self):
1368
        return ProjectApplication.objects.filter(chain=self.chain)
1369

    
1370
    def denied_modifications(self):
1371
        q = self.chained_applications()
1372
        q = q.filter(Q(state=self.DENIED))
1373
        q = q.filter(~Q(id=self.id))
1374
        return q
1375

    
1376
    def last_denied(self):
1377
        try:
1378
            return self.denied_modifications().order_by('-id')[0]
1379
        except IndexError:
1380
            return None
1381

    
1382
    def has_denied_modifications(self):
1383
        return bool(self.last_denied())
1384

    
1385
    def can_cancel(self):
1386
        return self.state == self.PENDING
1387

    
1388
    def cancel(self, actor=None, reason=None):
1389
        if not self.can_cancel():
1390
            m = _("cannot cancel: application '%s' in state '%s'") % (
1391
                self.id, self.state)
1392
            raise AssertionError(m)
1393

    
1394
        self.state = self.CANCELLED
1395
        self.waive_date = datetime.now()
1396
        self.waive_reason = reason
1397
        self.waive_actor = actor
1398
        self.save()
1399

    
1400
    def can_dismiss(self):
1401
        return self.state == self.DENIED
1402

    
1403
    def dismiss(self, actor=None, reason=None):
1404
        if not self.can_dismiss():
1405
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1406
                self.id, self.state)
1407
            raise AssertionError(m)
1408

    
1409
        self.state = self.DISMISSED
1410
        self.waive_date = datetime.now()
1411
        self.waive_reason = reason
1412
        self.waive_actor = actor
1413
        self.save()
1414

    
1415
    def can_deny(self):
1416
        return self.state == self.PENDING
1417

    
1418
    def deny(self, actor=None, reason=None):
1419
        if not self.can_deny():
1420
            m = _("cannot deny: application '%s' in state '%s'") % (
1421
                self.id, self.state)
1422
            raise AssertionError(m)
1423

    
1424
        self.state = self.DENIED
1425
        self.response_date = datetime.now()
1426
        self.response = reason
1427
        self.response_actor = actor
1428
        self.save()
1429

    
1430
    def can_approve(self):
1431
        return self.state == self.PENDING
1432

    
1433
    def approve(self, actor=None, reason=None):
1434
        if not self.can_approve():
1435
            m = _("cannot approve: project '%s' in state '%s'") % (
1436
                self.name, self.state)
1437
            raise AssertionError(m)  # invalid argument
1438

    
1439
        now = datetime.now()
1440
        self.state = self.APPROVED
1441
        self.response_date = now
1442
        self.response = reason
1443
        self.response_actor = actor
1444
        self.save()
1445

    
1446
    @property
1447
    def member_join_policy_display(self):
1448
        policy = self.member_join_policy
1449
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1450

    
1451
    @property
1452
    def member_leave_policy_display(self):
1453
        policy = self.member_leave_policy
1454
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1455

    
1456

    
1457
class ProjectResourceGrantManager(models.Manager):
1458
    def grants_per_app(self, applications):
1459
        app_ids = [app.id for app in applications]
1460
        grants = self.filter(
1461
            project_application__in=app_ids).select_related("resource")
1462
        return _partition_by(lambda g: g.project_application_id, grants)
1463

    
1464

    
1465
class ProjectResourceGrant(models.Model):
1466

    
1467
    resource = models.ForeignKey(Resource)
1468
    project_application = models.ForeignKey(ProjectApplication,
1469
                                            null=True)
1470
    project_capacity = models.BigIntegerField(null=True)
1471
    member_capacity = models.BigIntegerField(default=0)
1472

    
1473
    objects = ProjectResourceGrantManager()
1474

    
1475
    class Meta:
1476
        unique_together = ("resource", "project_application")
1477

    
1478
    def display_member_capacity(self):
1479
        return units.show(self.member_capacity, self.resource.unit)
1480

    
1481
    def __str__(self):
1482
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1483
                                        self.display_member_capacity())
1484

    
1485

    
1486
def _distinct(f, l):
1487
    d = {}
1488
    last = None
1489
    for x in l:
1490
        group = f(x)
1491
        if group == last:
1492
            continue
1493
        last = group
1494
        d[group] = x
1495
    return d
1496

    
1497

    
1498
def invert_dict(d):
1499
    return dict((v, k) for k, v in d.iteritems())
1500

    
1501

    
1502
class ProjectManager(models.Manager):
1503

    
1504
    def all_with_pending(self, flt=None):
1505
        flt = Q() if flt is None else flt
1506
        projects = list(self.select_related(
1507
            'application', 'application__owner').filter(flt))
1508

    
1509
        objs = ProjectApplication.objects.select_related('owner')
1510
        apps = objs.filter(state=ProjectApplication.PENDING,
1511
                           chain__in=projects).order_by('chain', '-id')
1512
        app_d = _distinct(lambda app: app.chain_id, apps)
1513
        return [(project, app_d.get(project.pk)) for project in projects]
1514

    
1515
    def expired_projects(self):
1516
        model = self.model
1517
        q = ((model.o_state_q(model.O_ACTIVE) |
1518
              model.o_state_q(model.O_SUSPENDED)) &
1519
             Q(application__end_date__lt=datetime.now()))
1520
        return self.filter(q)
1521

    
1522
    def user_accessible_projects(self, user):
1523
        """
1524
        Return projects accessible by specified user.
1525
        """
1526
        model = self.model
1527
        if user.is_project_admin():
1528
            flt = Q()
1529
        else:
1530
            membs = user.projectmembership_set.associated()
1531
            memb_projects = membs.values_list("project", flat=True)
1532
            flt = (Q(application__owner=user) |
1533
                   Q(application__applicant=user) |
1534
                   Q(id__in=memb_projects))
1535

    
1536
        relevant = model.o_states_q(model.RELEVANT_STATES)
1537
        return self.filter(flt, relevant).order_by(
1538
            'application__issue_date').select_related(
1539
            'application', 'application__owner', 'application__applicant')
1540

    
1541
    def search_by_name(self, *search_strings):
1542
        q = Q()
1543
        for s in search_strings:
1544
            q = q | Q(name__icontains=s)
1545
        return self.filter(q)
1546

    
1547

    
1548
class Project(models.Model):
1549

    
1550
    id = models.BigIntegerField(db_column='id', primary_key=True)
1551

    
1552
    application = models.OneToOneField(
1553
        ProjectApplication,
1554
        related_name='project')
1555

    
1556
    members = models.ManyToManyField(
1557
        AstakosUser,
1558
        through='ProjectMembership')
1559

    
1560
    creation_date = models.DateTimeField(auto_now_add=True)
1561
    name = models.CharField(
1562
        max_length=80,
1563
        null=True,
1564
        db_index=True,
1565
        unique=True)
1566

    
1567
    NORMAL = 1
1568
    SUSPENDED = 10
1569
    TERMINATED = 100
1570

    
1571
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1572

    
1573
    state = models.IntegerField(default=NORMAL,
1574
                                db_index=True)
1575

    
1576
    objects = ProjectManager()
1577

    
1578
    def __str__(self):
1579
        return uenc(_("<project %s '%s'>") %
1580
                    (self.id, udec(self.application.name)))
1581

    
1582
    __repr__ = __str__
1583

    
1584
    def __unicode__(self):
1585
        return _("<project %s '%s'>") % (self.id, self.application.name)
1586

    
1587
    O_PENDING = 0
1588
    O_ACTIVE = 1
1589
    O_DENIED = 3
1590
    O_DISMISSED = 4
1591
    O_CANCELLED = 5
1592
    O_SUSPENDED = 10
1593
    O_TERMINATED = 100
1594

    
1595
    O_STATE_DISPLAY = {
1596
        O_PENDING:    _("Pending"),
1597
        O_ACTIVE:     _("Active"),
1598
        O_DENIED:     _("Denied"),
1599
        O_DISMISSED:  _("Dismissed"),
1600
        O_CANCELLED:  _("Cancelled"),
1601
        O_SUSPENDED:  _("Suspended"),
1602
        O_TERMINATED: _("Terminated"),
1603
    }
1604

    
1605
    OVERALL_STATE = {
1606
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1607
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1608
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1609
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1610
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1611
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1612
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1613
    }
1614

    
1615
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1616

    
1617
    @classmethod
1618
    def o_state_q(cls, o_state):
1619
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1620
        return Q(state=p_state, application__state=a_state)
1621

    
1622
    @classmethod
1623
    def o_states_q(cls, o_states):
1624
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1625

    
1626
    INITIALIZED_STATES = [O_ACTIVE,
1627
                          O_SUSPENDED,
1628
                          O_TERMINATED,
1629
                          ]
1630

    
1631
    RELEVANT_STATES = [O_PENDING,
1632
                       O_DENIED,
1633
                       O_ACTIVE,
1634
                       O_SUSPENDED,
1635
                       O_TERMINATED,
1636
                       ]
1637

    
1638
    SKIP_STATES = [O_DISMISSED,
1639
                   O_CANCELLED,
1640
                   O_TERMINATED,
1641
                   ]
1642

    
1643
    @classmethod
1644
    def _overall_state(cls, project_state, app_state):
1645
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1646

    
1647
    def overall_state(self):
1648
        return self._overall_state(self.state, self.application.state)
1649

    
1650
    def last_pending_application(self):
1651
        apps = self.chained_apps.filter(
1652
            state=ProjectApplication.PENDING).order_by('-id')
1653
        if apps:
1654
            return apps[0]
1655
        return None
1656

    
1657
    def last_pending_modification(self):
1658
        last_pending = self.last_pending_application()
1659
        if last_pending == self.application:
1660
            return None
1661
        return last_pending
1662

    
1663
    def state_display(self):
1664
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1665

    
1666
    def expiration_info(self):
1667
        return (str(self.id), self.name, self.state_display(),
1668
                str(self.application.end_date))
1669

    
1670
    def last_deactivation(self):
1671
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1672
        ls = objs.order_by("-date")
1673
        if not ls:
1674
            return None
1675
        return ls[0]
1676

    
1677
    def is_deactivated(self, reason=None):
1678
        if reason is not None:
1679
            return self.state == reason
1680

    
1681
        return self.state != self.NORMAL
1682

    
1683
    def is_active(self):
1684
        return self.overall_state() == self.O_ACTIVE
1685

    
1686
    def is_initialized(self):
1687
        return self.overall_state() in self.INITIALIZED_STATES
1688

    
1689
    ### Deactivation calls
1690

    
1691
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1692
                    comments=None):
1693
        now = datetime.now()
1694
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1695
                        actor=actor, reason=reason, comments=comments)
1696

    
1697
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1698
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1699
                         comments=comments)
1700
        self.state = to_state
1701
        self.save()
1702

    
1703
    def terminate(self, actor=None, reason=None):
1704
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1705
        self.name = None
1706
        self.save()
1707

    
1708
    def suspend(self, actor=None, reason=None):
1709
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1710

    
1711
    def resume(self, actor=None, reason=None):
1712
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1713
        if self.name is None:
1714
            self.name = self.application.name
1715
            self.save()
1716

    
1717
    ### Logical checks
1718

    
1719
    @property
1720
    def is_alive(self):
1721
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1722

    
1723
    @property
1724
    def is_terminated(self):
1725
        return self.is_deactivated(self.TERMINATED)
1726

    
1727
    @property
1728
    def is_suspended(self):
1729
        return self.is_deactivated(self.SUSPENDED)
1730

    
1731
    def violates_members_limit(self, adding=0):
1732
        application = self.application
1733
        limit = application.limit_on_members_number
1734
        if limit is None:
1735
            return False
1736
        return (len(self.approved_members) + adding > limit)
1737

    
1738
    ### Other
1739

    
1740
    def count_pending_memberships(self):
1741
        return self.projectmembership_set.requested().count()
1742

    
1743
    def members_count(self):
1744
        return self.approved_memberships.count()
1745

    
1746
    @property
1747
    def approved_memberships(self):
1748
        query = ProjectMembership.Q_ACCEPTED_STATES
1749
        return self.projectmembership_set.filter(query)
1750

    
1751
    @property
1752
    def approved_members(self):
1753
        return [m.person for m in self.approved_memberships]
1754

    
1755

    
1756
class ProjectLogManager(models.Manager):
1757
    def last_deactivations(self, projects):
1758
        logs = self.filter(
1759
            project__in=projects,
1760
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1761
        return first_of_group(lambda l: l.project_id, logs)
1762

    
1763

    
1764
class ProjectLog(models.Model):
1765
    project = models.ForeignKey(Project, related_name="log")
1766
    from_state = models.IntegerField(null=True)
1767
    to_state = models.IntegerField()
1768
    date = models.DateTimeField()
1769
    actor = models.ForeignKey(AstakosUser, null=True)
1770
    reason = models.TextField(null=True)
1771
    comments = models.TextField(null=True)
1772

    
1773
    objects = ProjectLogManager()
1774

    
1775

    
1776
class ProjectLock(models.Model):
1777
    pass
1778

    
1779

    
1780
class ProjectMembershipManager(models.Manager):
1781

    
1782
    def any_accepted(self):
1783
        q = self.model.Q_ACCEPTED_STATES
1784
        return self.filter(q)
1785

    
1786
    def actually_accepted(self):
1787
        q = self.model.Q_ACTUALLY_ACCEPTED
1788
        return self.filter(q)
1789

    
1790
    def requested(self):
1791
        return self.filter(state=ProjectMembership.REQUESTED)
1792

    
1793
    def suspended(self):
1794
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1795

    
1796
    def associated(self):
1797
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1798

    
1799
    def any_accepted_per_project(self, projects):
1800
        ms = self.any_accepted().filter(project__in=projects)
1801
        return _partition_by(lambda m: m.project_id, ms)
1802

    
1803
    def requested_per_project(self, projects):
1804
        ms = self.requested().filter(project__in=projects)
1805
        return _partition_by(lambda m: m.project_id, ms)
1806

    
1807
    def one_per_project(self):
1808
        ms = self.all().select_related(
1809
            'project', 'project__application',
1810
            'project__application__owner', 'project_application__applicant',
1811
            'person')
1812
        m_per_p = {}
1813
        for m in ms:
1814
            m_per_p[m.project_id] = m
1815
        return m_per_p
1816

    
1817

    
1818
class ProjectMembership(models.Model):
1819

    
1820
    person = models.ForeignKey(AstakosUser)
1821
    project = models.ForeignKey(Project)
1822

    
1823
    REQUESTED = 0
1824
    ACCEPTED = 1
1825
    LEAVE_REQUESTED = 5
1826
    # User deactivation
1827
    USER_SUSPENDED = 10
1828
    REJECTED = 100
1829
    CANCELLED = 101
1830
    REMOVED = 200
1831

    
1832
    ASSOCIATED_STATES = set([REQUESTED,
1833
                             ACCEPTED,
1834
                             LEAVE_REQUESTED,
1835
                             USER_SUSPENDED,
1836
                             ])
1837

    
1838
    ACCEPTED_STATES = set([ACCEPTED,
1839
                           LEAVE_REQUESTED,
1840
                           USER_SUSPENDED,
1841
                           ])
1842

    
1843
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1844

    
1845
    state = models.IntegerField(default=REQUESTED,
1846
                                db_index=True)
1847

    
1848
    objects = ProjectMembershipManager()
1849

    
1850
    # Compiled queries
1851
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1852
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1853

    
1854
    MEMBERSHIP_STATE_DISPLAY = {
1855
        REQUESTED:       _('Requested'),
1856
        ACCEPTED:        _('Accepted'),
1857
        LEAVE_REQUESTED: _('Leave Requested'),
1858
        USER_SUSPENDED:  _('Suspended'),
1859
        REJECTED:        _('Rejected'),
1860
        CANCELLED:       _('Cancelled'),
1861
        REMOVED:         _('Removed'),
1862
    }
1863

    
1864
    USER_FRIENDLY_STATE_DISPLAY = {
1865
        REQUESTED:       _('Join requested'),
1866
        ACCEPTED:        _('Accepted member'),
1867
        LEAVE_REQUESTED: _('Requested to leave'),
1868
        USER_SUSPENDED:  _('Suspended member'),
1869
        REJECTED:        _('Request rejected'),
1870
        CANCELLED:       _('Request cancelled'),
1871
        REMOVED:         _('Removed member'),
1872
    }
1873

    
1874
    def state_display(self):
1875
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1876

    
1877
    def user_friendly_state_display(self):
1878
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1879

    
1880
    class Meta:
1881
        unique_together = ("person", "project")
1882
        #index_together = [["project", "state"]]
1883

    
1884
    def __str__(self):
1885
        return uenc(_("<'%s' membership in '%s'>") %
1886
                    (self.person.username, self.project))
1887

    
1888
    __repr__ = __str__
1889

    
1890
    def latest_log(self):
1891
        logs = self.log.all()
1892
        logs_d = _partition_by(lambda l: l.to_state, logs)
1893
        for s, s_logs in logs_d.iteritems():
1894
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1895
        return logs_d
1896

    
1897
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1898
                    comments=None):
1899
        now = datetime.now()
1900
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1901
                        actor=actor, reason=reason, comments=comments)
1902

    
1903
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1904
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1905
                         comments=comments)
1906
        self.state = to_state
1907
        self.save()
1908

    
1909
    ACTION_CHECKS = {
1910
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
1911
        "accept": lambda m: m.state == m.REQUESTED,
1912
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
1913
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
1914
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
1915
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1916
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1917
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
1918
        "reject": lambda m: m.state == m.REQUESTED,
1919
        "cancel": lambda m: m.state == m.REQUESTED,
1920
    }
1921

    
1922
    ACTION_STATES = {
1923
        "join":          REQUESTED,
1924
        "accept":        ACCEPTED,
1925
        "enroll":        ACCEPTED,
1926
        "leave_request": LEAVE_REQUESTED,
1927
        "deny_leave":    ACCEPTED,
1928
        "cancel_leave":  ACCEPTED,
1929
        "remove":        REMOVED,
1930
        "reject":        REJECTED,
1931
        "cancel":        CANCELLED,
1932
    }
1933

    
1934
    def check_action(self, action):
1935
        try:
1936
            check = self.ACTION_CHECKS[action]
1937
        except KeyError:
1938
            raise ValueError("No check found for action '%s'" % action)
1939
        return check(self)
1940

    
1941
    def perform_action(self, action, actor=None, reason=None):
1942
        if not self.check_action(action):
1943
            m = _("%s: attempted action '%s' in state '%s'") % (
1944
                self, action, self.state)
1945
            raise AssertionError(m)
1946
        try:
1947
            s = self.ACTION_STATES[action]
1948
        except KeyError:
1949
            raise ValueError("No such action '%s'" % action)
1950
        return self.set_state(s, actor=actor, reason=reason)
1951

    
1952

    
1953
class ProjectMembershipLogManager(models.Manager):
1954
    def last_logs(self, memberships):
1955
        logs = self.filter(membership__in=memberships).order_by("-date")
1956
        logs = _partition_by(lambda l: l.membership_id, logs)
1957

    
1958
        for memb_id, m_logs in logs.iteritems():
1959
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
1960
        return logs
1961

    
1962

    
1963
class ProjectMembershipLog(models.Model):
1964
    membership = models.ForeignKey(ProjectMembership, related_name="log")
1965
    from_state = models.IntegerField(null=True)
1966
    to_state = models.IntegerField()
1967
    date = models.DateTimeField()
1968
    actor = models.ForeignKey(AstakosUser, null=True)
1969
    reason = models.TextField(null=True)
1970
    comments = models.TextField(null=True)
1971

    
1972
    objects = ProjectMembershipLogManager()
1973

    
1974

    
1975
### SIGNALS ###
1976
################
1977

    
1978
def resource_post_save(sender, instance, created, **kwargs):
1979
    pass
1980

    
1981
post_save.connect(resource_post_save, sender=Resource)
1982

    
1983

    
1984
def renew_token(sender, instance, **kwargs):
1985
    if not instance.auth_token:
1986
        instance.renew_token()
1987
pre_save.connect(renew_token, sender=Component)