Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 8af74daf

History | View | Annotate | Download (66.2 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
    project_default = models.BigIntegerField()
237
    ui_visible = models.BooleanField(default=True)
238
    api_visible = models.BooleanField(default=True)
239

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

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

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

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

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

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

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

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

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

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

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

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

    
299

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

    
306

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

    
314

    
315
class AstakosUserManager(UserManager):
316

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

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

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

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

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

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

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

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

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

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

    
378

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
472
    objects = AstakosUserManager()
473

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
711
        modules = astakos_settings.IM_MODULES
712

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

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

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

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

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

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

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

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

    
752
        msg_extra = ''
753
        message = ''
754

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

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

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

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

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

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

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

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

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

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

    
826

    
827
class AstakosUserAuthProviderManager(models.Manager):
828

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

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

    
841
    def unverified(self, provider, **filters):
842
        try:
843

    
844
            return self.select_for_update().get(module=provider,
845
                                                user__email_verified=False,
846
                                                **filters).settings
847
        except AstakosUserAuthProvider.DoesNotExist:
848
            return None
849

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

    
857

    
858
class AuthProviderPolicyProfileManager(models.Manager):
859

    
860
    def active(self):
861
        return self.filter(active=True)
862

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

    
869
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
870
            policies.update(profile.policies)
871

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

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

    
893

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

    
900
    # apply policies to all providers excluding the one set in provider field
901
    is_exclusive = models.BooleanField(default=False)
902

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

    
912
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
913
                     'automoderate')
914

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

    
921
    objects = AuthProviderPolicyProfileManager()
922

    
923
    class Meta:
924
        ordering = ['priority']
925

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

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

    
942

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

    
961
    objects = AstakosUserAuthProviderManager()
962

    
963
    class Meta:
964
        unique_together = (('identifier', 'module', 'user'), )
965
        ordering = ('module', 'created')
966

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

    
976
        for key, value in self.info.iteritems():
977
            setattr(self, 'info_%s' % key, value)
978

    
979
    @property
980
    def settings(self):
981
        extra_data = {}
982

    
983
        info_data = {}
984
        if self.info_data:
985
            info_data = json.loads(self.info_data)
986

    
987
        extra_data['info'] = info_data
988

    
989
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
990
            extra_data[key] = getattr(self, key)
991

    
992
        extra_data['instance'] = self
993
        return auth.get_provider(self.module, self.user,
994
                                 self.identifier, **extra_data)
995

    
996
    def __repr__(self):
997
        return '<AstakosUserAuthProvider %s:%s>' % (
998
            self.module, self.identifier)
999

    
1000
    def __unicode__(self):
1001
        if self.identifier:
1002
            return "%s:%s" % (self.module, self.identifier)
1003
        if self.auth_backend:
1004
            return "%s:%s" % (self.module, self.auth_backend)
1005
        return self.module
1006

    
1007
    def save(self, *args, **kwargs):
1008
        self.info_data = json.dumps(self.info)
1009
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1010

    
1011

    
1012
class AstakosUserQuota(models.Model):
1013
    capacity = models.BigIntegerField()
1014
    resource = models.ForeignKey(Resource)
1015
    user = models.ForeignKey(AstakosUser)
1016

    
1017
    class Meta:
1018
        unique_together = ("resource", "user")
1019

    
1020

    
1021
class ApprovalTerms(models.Model):
1022
    """
1023
    Model for approval terms
1024
    """
1025

    
1026
    date = models.DateTimeField(
1027
        _('Issue date'), db_index=True, auto_now_add=True)
1028
    location = models.CharField(_('Terms location'), max_length=255)
1029

    
1030

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

    
1045
    def __init__(self, *args, **kwargs):
1046
        super(Invitation, self).__init__(*args, **kwargs)
1047
        if not self.id:
1048
            self.code = _generate_invitation_code()
1049

    
1050
    def consume(self):
1051
        self.is_consumed = True
1052
        self.consumed = datetime.now()
1053
        self.save()
1054

    
1055
    def __unicode__(self):
1056
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1057

    
1058

    
1059
class EmailChangeManager(models.Manager):
1060

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

1067
        If the key is valid and has not expired, return the ``User``
1068
        after activating.
1069

1070
        If the key is not valid or has expired, return ``None``.
1071

1072
        If the key is valid but the ``User`` is already active,
1073
        return ``None``.
1074

1075
        After successful email change the activation record is deleted.
1076

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

    
1107

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

    
1120
    objects = EmailChangeManager()
1121

    
1122
    def get_url(self):
1123
        return reverse('email_change_confirm',
1124
                       kwargs={'activation_key': self.activation_key})
1125

    
1126
    def activation_key_expired(self):
1127
        expiration_date = timedelta(
1128
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1129
        return self.requested_at + expiration_date < datetime.now()
1130

    
1131

    
1132
class AdditionalMail(models.Model):
1133
    """
1134
    Model for registring invitations
1135
    """
1136
    owner = models.ForeignKey(AstakosUser)
1137
    email = models.EmailField()
1138

    
1139

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

    
1149

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

    
1158

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

    
1181
    class Meta:
1182
        unique_together = ("provider", "third_party_identifier")
1183

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

    
1200
        return user
1201

    
1202
    @property
1203
    def realname(self):
1204
        return '%s %s' % (self.first_name, self.last_name)
1205

    
1206
    @realname.setter
1207
    def realname(self, value):
1208
        first, last = split_realname(value)
1209
        self.first_name = first
1210
        self.last_name = last
1211

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

    
1223
    def generate_token(self):
1224
        self.password = self.third_party_identifier
1225
        self.last_login = datetime.now()
1226
        self.token = default_token_generator.make_token(self)
1227

    
1228
    def existing_user(self):
1229
        return AstakosUser.objects.filter(
1230
            auth_providers__module=self.provider,
1231
            auth_providers__identifier=self.third_party_identifier)
1232

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

    
1241

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

    
1246

    
1247
class UserSetting(models.Model):
1248
    user = models.ForeignKey(AstakosUser)
1249
    setting = models.CharField(max_length=255)
1250
    value = models.IntegerField()
1251

    
1252
    class Meta:
1253
        unique_together = ("user", "setting")
1254

    
1255

    
1256
### PROJECTS ###
1257
################
1258

    
1259
class Chain(models.Model):
1260
    chain = models.AutoField(primary_key=True)
1261

    
1262
    def __str__(self):
1263
        return "%s" % (self.chain,)
1264

    
1265

    
1266
def new_chain():
1267
    c = Chain.objects.create()
1268
    return c
1269

    
1270

    
1271
class ProjectApplicationManager(models.Manager):
1272

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

    
1285

    
1286
class ProjectApplication(models.Model):
1287
    applicant = models.ForeignKey(
1288
        AstakosUser,
1289
        related_name='projects_applied',
1290
        db_index=True)
1291

    
1292
    PENDING = 0
1293
    APPROVED = 1
1294
    REPLACED = 2
1295
    DENIED = 3
1296
    DISMISSED = 4
1297
    CANCELLED = 5
1298

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

    
1334
    objects = ProjectApplicationManager()
1335

    
1336
    # Compiled queries
1337
    Q_PENDING = Q(state=PENDING)
1338
    Q_APPROVED = Q(state=APPROVED)
1339
    Q_DENIED = Q(state=DENIED)
1340

    
1341
    class Meta:
1342
        unique_together = ("chain", "id")
1343

    
1344
    def __unicode__(self):
1345
        return "%s applied by %s" % (self.name, self.applicant)
1346

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

    
1357
    @property
1358
    def log_display(self):
1359
        return "application %s (%s) for project %s" % (
1360
            self.id, self.name, self.chain)
1361

    
1362
    def state_display(self):
1363
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1364

    
1365
    @property
1366
    def grants(self):
1367
        return self.projectresourcegrant_set.values('member_capacity',
1368
                                                    'resource__name')
1369

    
1370
    @property
1371
    def resource_policies(self):
1372
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1373

    
1374
    def is_modification(self):
1375
        # if self.state != self.PENDING:
1376
        #     return False
1377
        parents = self.chained_applications().filter(id__lt=self.id)
1378
        parents = parents.filter(state__in=[self.APPROVED])
1379
        return parents.count() > 0
1380

    
1381
    def chained_applications(self):
1382
        return ProjectApplication.objects.filter(chain=self.chain)
1383

    
1384
    def denied_modifications(self):
1385
        q = self.chained_applications()
1386
        q = q.filter(Q(state=self.DENIED))
1387
        q = q.filter(~Q(id=self.id))
1388
        return q
1389

    
1390
    def last_denied(self):
1391
        try:
1392
            return self.denied_modifications().order_by('-id')[0]
1393
        except IndexError:
1394
            return None
1395

    
1396
    def has_denied_modifications(self):
1397
        return bool(self.last_denied())
1398

    
1399
    def can_cancel(self):
1400
        return self.state == self.PENDING
1401

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

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

    
1414
    def can_dismiss(self):
1415
        return self.state == self.DENIED
1416

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

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

    
1429
    def can_deny(self):
1430
        return self.state == self.PENDING
1431

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

    
1438
        self.state = self.DENIED
1439
        self.response_date = datetime.now()
1440
        self.response = reason
1441
        self.response_actor = actor
1442
        self.save()
1443

    
1444
    def can_approve(self):
1445
        return self.state == self.PENDING
1446

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

    
1453
        now = datetime.now()
1454
        self.state = self.APPROVED
1455
        self.response_date = now
1456
        self.response = reason
1457
        self.response_actor = actor
1458
        self.save()
1459

    
1460
    @property
1461
    def member_join_policy_display(self):
1462
        policy = self.member_join_policy
1463
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1464

    
1465
    @property
1466
    def member_leave_policy_display(self):
1467
        policy = self.member_leave_policy
1468
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1469

    
1470

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

    
1478

    
1479
class ProjectResourceGrant(models.Model):
1480

    
1481
    resource = models.ForeignKey(Resource)
1482
    project_application = models.ForeignKey(ProjectApplication,
1483
                                            null=True)
1484
    project_capacity = models.BigIntegerField(null=True)
1485
    member_capacity = models.BigIntegerField(default=0)
1486

    
1487
    objects = ProjectResourceGrantManager()
1488

    
1489
    class Meta:
1490
        unique_together = ("resource", "project_application")
1491

    
1492
    def display_member_capacity(self):
1493
        return units.show(self.member_capacity, self.resource.unit)
1494

    
1495
    def __str__(self):
1496
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1497
                                        self.display_member_capacity())
1498

    
1499

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

    
1511

    
1512
def invert_dict(d):
1513
    return dict((v, k) for k, v in d.iteritems())
1514

    
1515

    
1516
class ProjectManager(models.Manager):
1517

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

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

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

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

    
1550
        relevant = model.o_states_q(model.RELEVANT_STATES)
1551
        return self.filter(flt, relevant).order_by(
1552
            'application__issue_date').select_related(
1553
            'application', 'application__owner', 'application__applicant')
1554

    
1555
    def search_by_name(self, *search_strings):
1556
        q = Q()
1557
        for s in search_strings:
1558
            q = q | Q(name__icontains=s)
1559
        return self.filter(q)
1560

    
1561

    
1562
class Project(models.Model):
1563

    
1564
    id = models.BigIntegerField(db_column='id', primary_key=True)
1565

    
1566
    application = models.OneToOneField(
1567
        ProjectApplication,
1568
        related_name='project')
1569

    
1570
    members = models.ManyToManyField(
1571
        AstakosUser,
1572
        through='ProjectMembership')
1573

    
1574
    creation_date = models.DateTimeField(auto_now_add=True)
1575
    name = models.CharField(
1576
        max_length=80,
1577
        null=True,
1578
        db_index=True,
1579
        unique=True)
1580

    
1581
    NORMAL = 1
1582
    SUSPENDED = 10
1583
    TERMINATED = 100
1584

    
1585
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1586

    
1587
    state = models.IntegerField(default=NORMAL,
1588
                                db_index=True)
1589
    uuid = models.CharField(max_length=255, unique=True)
1590

    
1591
    objects = ProjectManager()
1592

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

    
1597
    __repr__ = __str__
1598

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

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

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

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

    
1630
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1631

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

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

    
1641
    INITIALIZED_STATES = [O_ACTIVE,
1642
                          O_SUSPENDED,
1643
                          O_TERMINATED,
1644
                          ]
1645

    
1646
    RELEVANT_STATES = [O_PENDING,
1647
                       O_DENIED,
1648
                       O_ACTIVE,
1649
                       O_SUSPENDED,
1650
                       O_TERMINATED,
1651
                       ]
1652

    
1653
    SKIP_STATES = [O_DISMISSED,
1654
                   O_CANCELLED,
1655
                   O_TERMINATED,
1656
                   ]
1657

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

    
1662
    def overall_state(self):
1663
        return self._overall_state(self.state, self.application.state)
1664

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

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

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

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

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

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

    
1696
        return self.state != self.NORMAL
1697

    
1698
    def is_active(self):
1699
        return self.overall_state() == self.O_ACTIVE
1700

    
1701
    def is_initialized(self):
1702
        return self.overall_state() in self.INITIALIZED_STATES
1703

    
1704
    ### Deactivation calls
1705

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

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

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

    
1723
    def suspend(self, actor=None, reason=None):
1724
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1725

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

    
1732
    ### Logical checks
1733

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

    
1738
    @property
1739
    def is_terminated(self):
1740
        return self.is_deactivated(self.TERMINATED)
1741

    
1742
    @property
1743
    def is_suspended(self):
1744
        return self.is_deactivated(self.SUSPENDED)
1745

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

    
1753
    ### Other
1754

    
1755
    def count_pending_memberships(self):
1756
        return self.projectmembership_set.requested().count()
1757

    
1758
    def members_count(self):
1759
        return self.approved_memberships.count()
1760

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

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

    
1770

    
1771
def create_project(**kwargs):
1772
    if "uuid" not in kwargs:
1773
        kwargs["uuid"] = str(uuid.uuid4())
1774
    return Project.objects.create(**kwargs)
1775

    
1776

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

    
1784

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

    
1794
    objects = ProjectLogManager()
1795

    
1796

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

    
1800

    
1801
class ProjectMembershipManager(models.Manager):
1802

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

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

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

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

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

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

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

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

    
1838

    
1839
class ProjectMembership(models.Model):
1840

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

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

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

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

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

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

    
1869
    objects = ProjectMembershipManager()
1870

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

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

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

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

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

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

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

    
1909
    __repr__ = __str__
1910

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

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

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

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

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

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

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

    
1973

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

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

    
1983

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

    
1993
    objects = ProjectMembershipLogManager()
1994

    
1995

    
1996
### SIGNALS ###
1997
################
1998

    
1999
def resource_post_save(sender, instance, created, **kwargs):
2000
    pass
2001

    
2002
post_save.connect(resource_post_save, sender=Resource)
2003

    
2004

    
2005
def renew_token(sender, instance, **kwargs):
2006
    if not instance.auth_token:
2007
        instance.renew_token()
2008
pre_save.connect(renew_token, sender=Component)