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

import hashlib
35
import uuid
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
36
import logging
37
38
39
40

from time import asctime
from datetime import datetime, timedelta
from base64 import b64encode
41
from urlparse import urlparse
42
from urllib import quote
43
from random import randint
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
44
from collections import defaultdict
45

46
from django.db import models, IntegrityError
Olga Brani's avatar
Olga Brani committed
47
from django.contrib.auth.models import User, UserManager, Group, Permission
48
from django.utils.translation import ugettext as _
49
from django.db import transaction
50
from django.core.exceptions import ValidationError
51
52
53
from django.db.models.signals import (
    pre_save, post_save, post_syncdb, post_delete
)
Olga Brani's avatar
Olga Brani committed
54
55
from django.contrib.contenttypes.models import ContentType

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
56
from django.dispatch import Signal
57
from django.db.models import Q
58
59
60
from django.core.urlresolvers import reverse
from django.utils.http import int_to_base36
from django.contrib.auth.tokens import default_token_generator
61
from django.conf import settings
62
from django.utils.importlib import import_module
63
from django.core.validators import email_re
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
64
65
66
from django.core.exceptions import PermissionDenied
from django.views.generic.create_update import lookup_object
from django.core.exceptions import ObjectDoesNotExist
67

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
68
69
70
from astakos.im.settings import (
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
    AUTH_TOKEN_DURATION, BILLING_FIELDS,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
71
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
72
73
74
75
    SITENAME, SERVICES,
    PROJECT_CREATION_SUBJECT, PROJECT_APPROVED_SUBJECT,
    PROJECT_TERMINATION_SUBJECT, PROJECT_SUSPENSION_SUBJECT,
    PROJECT_MEMBERSHIP_CHANGE_SUBJECT
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
76
)
77
78
from astakos.im.endpoints.qh import (
    register_users, send_quota, register_resources
79
)
80
from astakos.im import auth_providers
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
81
from astakos.im.endpoints.aquarium.producer import report_user_event
Olga Brani's avatar
Olga Brani committed
82
from astakos.im.functions import send_invitation
root's avatar
root committed
83
#from astakos.im.tasks import propagate_groupmembers_quota
84

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
85
86
from astakos.im.notifications import build_notification

Olga Brani's avatar
Olga Brani committed
87
import astakos.im.messages as astakos_messages
88

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
89
90
logger = logging.getLogger(__name__)

Olga Brani's avatar
Olga Brani committed
91
DEFAULT_CONTENT_TYPE = None
92
93
_content_type = None

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
94
95
PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'

96
97
98
99
100
101
102
103
104
105
106
def get_content_type():
    global _content_type
    if _content_type is not None:
        return _content_type

    try:
        content_type = ContentType.objects.get(app_label='im', model='astakosuser')
    except:
        content_type = DEFAULT_CONTENT_TYPE
    _content_type = content_type
    return content_type
Olga Brani's avatar
Olga Brani committed
107
108
109

RESOURCE_SEPARATOR = '.'

110
inf = float('inf')
111

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
112
class Service(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
113
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
114
115
    url = models.FilePathField()
    icon = models.FilePathField(blank=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
116
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
117
                                  null=True, blank=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
118
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
119
    auth_token_expires = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
120
        _('Token expiration date'), null=True)
121

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
122
123
124
125
126
127
128
129
130
    def renew_token(self):
        md5 = hashlib.md5()
        md5.update(self.name.encode('ascii', 'ignore'))
        md5.update(self.url.encode('ascii', 'ignore'))
        md5.update(asctime())

        self.auth_token = b64encode(md5.digest())
        self.auth_token_created = datetime.now()
        self.auth_token_expires = self.auth_token_created + \
131
132
            timedelta(hours=AUTH_TOKEN_DURATION)

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
133
134
135
    def __str__(self):
        return self.name

Olga Brani's avatar
Olga Brani committed
136
137
138
139
140
141
142
143
    @property
    def resources(self):
        return self.resource_set.all()

    @resources.setter
    def resources(self, resources):
        for s in resources:
            self.resource_set.create(**s)
144

Olga Brani's avatar
Olga Brani committed
145
146
147
148
149
150
151
152
153
154
155
    def add_resource(self, service, resource, uplimit, update=True):
        """Raises ObjectDoesNotExist, IntegrityError"""
        resource = Resource.objects.get(service__name=service, name=resource)
        if update:
            AstakosUserQuota.objects.update_or_create(user=self,
                                                      resource=resource,
                                                      defaults={'uplimit': uplimit})
        else:
            q = self.astakosuserquota_set
            q.create(resource=resource, uplimit=uplimit)

156

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
157
class ResourceMetadata(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
158
159
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
    value = models.CharField(_('Value'), max_length=255)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
160

161

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
162
class Resource(models.Model):
163
    name = models.CharField(_('Name'), max_length=255)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
164
165
    meta = models.ManyToManyField(ResourceMetadata)
    service = models.ForeignKey(Service)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
166
167
168
    desc = models.TextField(_('Description'), null=True)
    unit = models.CharField(_('Name'), null=True, max_length=255)
    group = models.CharField(_('Group'), null=True, max_length=255)
169
170
171
    
    class Meta:
        unique_together = ("name", "service")
172

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
173
    def __str__(self):
Olga Brani's avatar
Olga Brani committed
174
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
175

176

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
177
class GroupKind(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
178
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
179

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
180
181
182
    def __str__(self):
        return self.name

183

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
184
185
class AstakosGroup(Group):
    kind = models.ForeignKey(GroupKind)
186
    homepage = models.URLField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
187
188
        _('Homepage Url'), max_length=255, null=True, blank=True)
    desc = models.TextField(_('Description'), null=True)
Olga Brani's avatar
Olga Brani committed
189
190
191
192
193
194
195
    policy = models.ManyToManyField(
        Resource,
        null=True,
        blank=True,
        through='AstakosGroupQuota'
    )
    creation_date = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
196
        _('Creation date'),
Olga Brani's avatar
Olga Brani committed
197
198
        default=datetime.now()
    )
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
199
200
201
202
    issue_date = models.DateTimeField(
        _('Start date'),
        null=True
    )
Olga Brani's avatar
Olga Brani committed
203
    expiration_date = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
204
205
        _('Expiration date'),
        null=True
Olga Brani's avatar
Olga Brani committed
206
207
    )
    moderation_enabled = models.BooleanField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
208
        _('Moderated membership?'),
Olga Brani's avatar
Olga Brani committed
209
210
211
        default=True
    )
    approval_date = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
212
        _('Activation date'),
Olga Brani's avatar
Olga Brani committed
213
214
215
216
        null=True,
        blank=True
    )
    estimated_participants = models.PositiveIntegerField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
217
        _('Estimated #members'),
Olga Brani's avatar
Olga Brani committed
218
219
220
221
        null=True,
        blank=True,
    )
    max_participants = models.PositiveIntegerField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
222
        _('Maximum numder of participants'),
Olga Brani's avatar
Olga Brani committed
223
224
225
        null=True,
        blank=True
    )
226

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
227
228
    @property
    def is_disabled(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
229
230
231
        if not self.approval_date:
            return True
        return False
232

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
233
    @property
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
234
    def is_enabled(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
235
236
237
238
239
240
241
242
243
244
245
246
        if self.is_disabled:
            return False
        if not self.issue_date:
            return False
        if not self.expiration_date:
            return True
        now = datetime.now()
        if self.issue_date > now:
            return False
        if now >= self.expiration_date:
            return False
        return True
247

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
248
    def enable(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
249
250
        if self.is_enabled:
            return
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
251
252
        self.approval_date = datetime.now()
        self.save()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
253
        quota_disturbed.send(sender=self, users=self.approved_members)
root's avatar
root committed
254
255
256
257
        #propagate_groupmembers_quota.apply_async(
        #    args=[self], eta=self.issue_date)
        #propagate_groupmembers_quota.apply_async(
        #    args=[self], eta=self.expiration_date)
258

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
259
    def disable(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
260
261
        if self.is_disabled:
            return
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
262
263
        self.approval_date = None
        self.save()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
264
        quota_disturbed.send(sender=self, users=self.approved_members)
265

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
266
    def approve_member(self, person):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
267
        m, created = self.membership_set.get_or_create(person=person)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
268
        m.approve()
269

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
270
271
    @property
    def members(self):
272
273
        q = self.membership_set.select_related().all()
        return [m.person for m in q]
274

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
275
276
    @property
    def approved_members(self):
277
278
        q = self.membership_set.select_related().all()
        return [m.person for m in q if m.is_approved]
279

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
280
    @property
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
281
    def quota(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
282
        d = defaultdict(int)
283
        for q in self.astakosgroupquota_set.select_related().all():
284
            d[q.resource] += q.uplimit or inf
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
285
        return d
286

Olga Brani's avatar
Olga Brani committed
287
288
289
290
291
292
293
294
295
296
297
298
    def add_policy(self, service, resource, uplimit, update=True):
        """Raises ObjectDoesNotExist, IntegrityError"""
        resource = Resource.objects.get(service__name=service, name=resource)
        if update:
            AstakosGroupQuota.objects.update_or_create(
                group=self,
                resource=resource,
                defaults={'uplimit': uplimit}
            )
        else:
            q = self.astakosgroupquota_set
            q.create(resource=resource, uplimit=uplimit)
299

Olga Brani's avatar
Olga Brani committed
300
301
302
303
304
305
306
307
308
309
310
311
    @property
    def policies(self):
        return self.astakosgroupquota_set.select_related().all()

    @policies.setter
    def policies(self, policies):
        for p in policies:
            service = p.get('service', None)
            resource = p.get('resource', None)
            uplimit = p.get('uplimit', 0)
            update = p.get('update', True)
            self.add_policy(service, resource, uplimit, update)
312

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
313
314
315
    @property
    def owners(self):
        return self.owner.all()
316

317
318
319
320
    @property
    def owner_details(self):
        return self.owner.select_related().all()

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
321
322
323
324
    @owners.setter
    def owners(self, l):
        self.owner = l
        map(self.approve_member, l)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
325

326
327
328
329
330
331
332
333
334
335
336
337
338
_default_quota = {}
def get_default_quota():
    global _default_quota
    if _default_quota:
        return _default_quota
    for s, data in SERVICES.iteritems():
        map(
            lambda d:_default_quota.update(
                {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
            ),
            data.get('resources', {})
        )
    return _default_quota
339

340
class AstakosUserManager(UserManager):
341
342
343
344
345
346
347
348
349
350

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
351
class AstakosUser(User):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
352
353
354
    """
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
355
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
356
357
358
359
                                   null=True)

    # DEPRECATED FIELDS: provider, third_party_identifier moved in
    #                    AstakosUserProvider model.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
360
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
361
362
                                null=True)
    # ex. screen_name for twitter, eppn for shibboleth
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
363
    third_party_identifier = models.CharField(_('Third-party identifier'),
364
365
366
                                              max_length=255, null=True,
                                              blank=True)

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
367

368
    #for invitations
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
369
    user_level = DEFAULT_USER_LEVEL
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
370
    level = models.IntegerField(_('Inviter level'), default=user_level)
371
    invitations = models.IntegerField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
372
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
373

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
374
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
375
                                  null=True, blank=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
376
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
377
    auth_token_expires = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
378
        _('Token expiration date'), null=True)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
379

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
380
381
    updated = models.DateTimeField(_('Update date'))
    is_verified = models.BooleanField(_('Is verified?'), default=False)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
382

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
383
    email_verified = models.BooleanField(_('Email verified?'), default=False)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
384

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
385
    has_credits = models.BooleanField(_('Has credits?'), default=False)
386
    has_signed_terms = models.BooleanField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
387
        _('I agree with the terms'), default=False)
388
    date_signed_terms = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
389
        _('Signed terms date'), null=True, blank=True)
390
391

    activation_sent = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
392
        _('Activation sent data'), null=True, blank=True)
393
394
395
396
397
398

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

    astakos_groups = models.ManyToManyField(
        AstakosGroup, verbose_name=_('agroups'), blank=True,
Olga Brani's avatar
Olga Brani committed
399
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
400
        through='Membership')
401

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
402
    __has_signed_terms = False
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
403
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
Olga Brani's avatar
Olga Brani committed
404
                                           default=False, db_index=True)
405
406

    objects = AstakosUserManager()
407

408
409
410
    owner = models.ManyToManyField(
        AstakosGroup, related_name='owner', null=True)

411
412
    class Meta:
        unique_together = ("provider", "third_party_identifier")
413

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
414
415
416
    def __init__(self, *args, **kwargs):
        super(AstakosUser, self).__init__(*args, **kwargs)
        self.__has_signed_terms = self.has_signed_terms
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
417
        if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
418
            self.is_active = False
419

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
420
421
    @property
    def realname(self):
422
        return '%s %s' % (self.first_name, self.last_name)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
423

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
424
425
426
427
428
429
430
431
    @realname.setter
    def realname(self, value):
        parts = value.split(' ')
        if len(parts) == 2:
            self.first_name = parts[0]
            self.last_name = parts[1]
        else:
            self.last_name = parts[0]
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
432

Olga Brani's avatar
Olga Brani committed
433
434
435
    def add_permission(self, pname):
        if self.has_perm(pname):
            return
436
437
438
439
        p, created = Permission.objects.get_or_create(
                                    codename=pname,
                                    name=pname.capitalize(),
                                    content_type=get_content_type())
Olga Brani's avatar
Olga Brani committed
440
441
442
443
444
445
        self.user_permissions.add(p)

    def remove_permission(self, pname):
        if self.has_perm(pname):
            return
        p = Permission.objects.get(codename=pname,
446
                                   content_type=get_content_type())
Olga Brani's avatar
Olga Brani committed
447
448
        self.user_permissions.remove(p)

449
450
451
    @property
    def invitation(self):
        try:
452
            return Invitation.objects.get(username=self.email)
453
454
        except Invitation.DoesNotExist:
            return None
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
455

Olga Brani's avatar
Olga Brani committed
456
457
458
459
460
461
462
    def invite(self, email, realname):
        inv = Invitation(inviter=self, username=email, realname=realname)
        inv.save()
        send_invitation(inv)
        self.invitations = max(0, self.invitations - 1)
        self.save()

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
463
464
    @property
    def quota(self):
Olga Brani's avatar
Olga Brani committed
465
        """Returns a dict with the sum of quota limits per resource"""
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
466
        d = defaultdict(int)
467
468
        default_quota = get_default_quota()
        d.update(default_quota)
Olga Brani's avatar
Olga Brani committed
469
        for q in self.policies:
470
            d[q.resource] += q.uplimit or inf
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
471
472
        for m in self.projectmembership_set.select_related().all():
            if not m.acceptance_date:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
473
                continue
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
474
475
            p = m.project
            if not p.is_active:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
476
                continue
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
477
478
            grants = p.application.definition.projectresourcegrant_set.all()
            for g in grants:
479
                d[str(g.resource)] += g.member_limit or inf
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
480
481
        # TODO set default for remaining
        return d
482

Olga Brani's avatar
Olga Brani committed
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
    @property
    def policies(self):
        return self.astakosuserquota_set.select_related().all()

    @policies.setter
    def policies(self, policies):
        for p in policies:
            service = policies.get('service', None)
            resource = policies.get('resource', None)
            uplimit = policies.get('uplimit', 0)
            update = policies.get('update', True)
            self.add_policy(service, resource, uplimit, update)

    def add_policy(self, service, resource, uplimit, update=True):
        """Raises ObjectDoesNotExist, IntegrityError"""
        resource = Resource.objects.get(service__name=service, name=resource)
        if update:
            AstakosUserQuota.objects.update_or_create(user=self,
                                                      resource=resource,
                                                      defaults={'uplimit': uplimit})
        else:
            q = self.astakosuserquota_set
            q.create(resource=resource, uplimit=uplimit)

    def remove_policy(self, service, resource):
        """Raises ObjectDoesNotExist, IntegrityError"""
        resource = Resource.objects.get(service__name=service, name=resource)
        q = self.policies.get(resource=resource).delete()

    @property
    def extended_groups(self):
        return self.membership_set.select_related().all()

    @extended_groups.setter
    def extended_groups(self, groups):
        #TODO exceptions
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
519
        for name in (groups or ()):
Olga Brani's avatar
Olga Brani committed
520
521
522
            group = AstakosGroup.objects.get(name=name)
            self.membership_set.create(group=group)

523
524
525
    def save(self, update_timestamps=True, **kwargs):
        if update_timestamps:
            if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
526
                self.date_joined = datetime.now()
527
            self.updated = datetime.now()
528

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
529
530
531
        # update date_signed_terms if necessary
        if self.__has_signed_terms != self.has_signed_terms:
            self.date_signed_terms = datetime.now()
532

533
534
        if not self.id:
            # set username
535
            self.username = self.email
536

537
        self.validate_unique_email_isactive()
538
539
540
        if self.is_active and self.activation_sent:
            # reset the activation sent
            self.activation_sent = None
541

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
542
        super(AstakosUser, self).save(**kwargs)
543

544
    def renew_token(self, flush_sessions=False, current_key=None):
545
        md5 = hashlib.md5()
546
        md5.update(settings.SECRET_KEY)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
547
        md5.update(self.username)
548
549
        md5.update(self.realname.encode('ascii', 'ignore'))
        md5.update(asctime())
550

551
552
553
        self.auth_token = b64encode(md5.digest())
        self.auth_token_created = datetime.now()
        self.auth_token_expires = self.auth_token_created + \
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
554
                                  timedelta(hours=AUTH_TOKEN_DURATION)
555
556
        if flush_sessions:
            self.flush_sessions(current_key)
557
        msg = 'Token renewed for %s' % self.email
558
        logger.log(LOGGING_LEVEL, msg)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
559

560
561
562
563
    def flush_sessions(self, current_key=None):
        q = self.sessions
        if current_key:
            q = q.exclude(session_key=current_key)
564

565
566
567
        keys = q.values_list('session_key', flat=True)
        if keys:
            msg = 'Flushing sessions: %s' % ','.join(keys)
568
            logger.log(LOGGING_LEVEL, msg, [])
569
570
571
572
573
        engine = import_module(settings.SESSION_ENGINE)
        for k in keys:
            s = engine.SessionStore(k)
            s.flush()

574
    def __unicode__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
575
        return '%s (%s)' % (self.realname, self.email)
576

577
    def conflicting_email(self):
578
        q = AstakosUser.objects.exclude(username=self.username)
579
        q = q.filter(email__iexact=self.email)
580
581
582
        if q.count() != 0:
            return True
        return False
583

584
    def validate_unique_email_isactive(self):
585
586
587
        """
        Implements a unique_together constraint for email and is_active fields.
        """
588
        q = AstakosUser.objects.all()
589
590
        q = q.filter(email = self.email)
        q = q.filter(is_active = self.is_active)
591
592
        if self.id:
            q = q.filter(~Q(id = self.id))
593
        if q.count() != 0:
Olga Brani's avatar
Olga Brani committed
594
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
595

596
    @property
597
598
599
600
601
602
603
604
605
606
    def signed_terms(self):
        term = get_latest_terms()
        if not term:
            return True
        if not self.has_signed_terms:
            return False
        if not self.date_signed_terms:
            return False
        if self.date_signed_terms < term.date:
            self.has_signed_terms = False
607
            self.date_signed_terms = None
608
609
610
611
            self.save()
            return False
        return True

612
613
614
615
616
617
618
619
620
621
622
623
624
625
    def set_invitations_level(self):
        """
        Update user invitation level
        """
        level = self.invitation.inviter.level + 1
        self.level = level
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)

    def can_login_with_auth_provider(self, provider):
        if not self.has_auth_provider(provider):
            return False
        else:
            return auth_providers.get_provider(provider).is_available_for_login()

626
    def can_add_auth_provider(self, provider, **kwargs):
627
628
629
        provider_settings = auth_providers.get_provider(provider)
        if not provider_settings.is_available_for_login():
            return False
630

631
632
633
        if self.has_auth_provider(provider) and \
           provider_settings.one_per_user:
            return False
634
635
636
637
638
639
640
641
642
643
644

        if 'identifier' in kwargs:
            try:
                # provider with specified params already exist
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
                                                                   **kwargs)
            except AstakosUser.DoesNotExist:
                return True
            else:
                return False

645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
        return True

    def can_remove_auth_provider(self, provider):
        if len(self.get_active_auth_providers()) <= 1:
            return False
        return True

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

    def has_auth_provider(self, provider, **kwargs):
        return bool(self.auth_providers.filter(module=provider,
                                               **kwargs).count())

    def add_auth_provider(self, provider, **kwargs):
660
661
662
663
        if self.can_add_auth_provider(provider, **kwargs):
            self.auth_providers.create(module=provider, active=True, **kwargs)
        else:
            raise Exception('Cannot add provider')
664
665
666
667
668
669
670
671
672
673
674
675

    def add_pending_auth_provider(self, pending):
        """
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
        the current user.
        """
        if not isinstance(pending, PendingThirdPartyUser):
            pending = PendingThirdPartyUser.objects.get(token=pending)

        provider = self.add_auth_provider(pending.provider,
                               identifier=pending.third_party_identifier)

676
        if email_re.match(pending.email or '') and pending.email != self.email:
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
            self.additionalmail_set.get_or_create(email=pending.email)

        pending.delete()
        return provider

    def remove_auth_provider(self, provider, **kwargs):
        self.auth_providers.get(module=provider, **kwargs).delete()

    # user urls
    def get_resend_activation_url(self):
        return reverse('send_activation', {'user_id': self.pk})

    def get_activation_url(self, nxt=False):
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
                                 quote(self.auth_token))
        if nxt:
            url += "&next=%s" % quote(nxt)
        return url

    def get_password_reset_url(self, token_generator=default_token_generator):
        return reverse('django.contrib.auth.views.password_reset_confirm',
                          kwargs={'uidb36':int_to_base36(self.id),
                                  'token':token_generator.make_token(self)})

    def get_auth_providers(self):
        return self.auth_providers.all()

    def get_available_auth_providers(self):
        """
        Returns a list of providers available for user to connect to.
        """
        providers = []
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
710
            if self.can_add_auth_provider(module):
711
712
713
714
715
716
717
718
719
720
721
                providers.append(provider_settings(self))

        return providers

    def get_active_auth_providers(self):
        providers = []
        for provider in self.auth_providers.active():
            if auth_providers.get_provider(provider.module).is_available_for_login():
                providers.append(provider)
        return providers

722
723
724
725
    @property
    def auth_providers_display(self):
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))

726
727
728
729
730
731
732
733
734
735
736

class AstakosUserAuthProviderManager(models.Manager):

    def active(self):
        return self.filter(active=True)


class AstakosUserAuthProvider(models.Model):
    """
    Available user authentication methods.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
737
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
738
739
                                   null=True, default=None)
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
740
    module = models.CharField(_('Provider'), max_length=255, blank=False,
741
                                default='local')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
742
    identifier = models.CharField(_('Third-party identifier'),
743
744
745
                                              max_length=255, null=True,
                                              blank=True)
    active = models.BooleanField(default=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
746
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
                                   default='astakos')

    objects = AstakosUserAuthProviderManager()

    class Meta:
        unique_together = (('identifier', 'module', 'user'), )

    @property
    def settings(self):
        return auth_providers.get_provider(self.module)

    @property
    def details_display(self):
        return self.settings.details_tpl % self.__dict__

    def can_remove(self):
        return self.user.can_remove_auth_provider(self.module)

    def delete(self, *args, **kwargs):
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
767
768
769
        if self.module == 'local':
            self.user.set_unusable_password()
            self.user.save()
770
771
        return ret

772
773
774
    def __repr__(self):
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)

775
776
777
778
779
780
781
782
    def __unicode__(self):
        if self.identifier:
            return "%s:%s" % (self.module, self.identifier)
        if self.auth_backend:
            return "%s:%s" % (self.module, self.auth_backend)
        return self.module


783

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
784
785
786
class Membership(models.Model):
    person = models.ForeignKey(AstakosUser)
    group = models.ForeignKey(AstakosGroup)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
787
788
    date_requested = models.DateField(default=datetime.now(), blank=True)
    date_joined = models.DateField(null=True, db_index=True, blank=True)
789

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
790
791
    class Meta:
        unique_together = ("person", "group")
792

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
793
    def save(self, *args, **kwargs):
794
795
796
        if not self.id:
            if not self.group.moderation_enabled:
                self.date_joined = datetime.now()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
797
        super(Membership, self).save(*args, **kwargs)
798

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
799
800
801
802
803
    @property
    def is_approved(self):
        if self.date_joined:
            return True
        return False
804

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
805
    def approve(self):
806
807
        if self.is_approved:
            return
Olga Brani's avatar
Olga Brani committed
808
        if self.group.max_participants:
809
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
810
            'Maximum participant number has been reached.'
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
811
812
        self.date_joined = datetime.now()
        self.save()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
813
        quota_disturbed.send(sender=self, users=(self.person,))
814

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
815
    def disapprove(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
816
        approved = self.is_approved()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
817
        self.delete()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
818
819
        if approved:
            quota_disturbed.send(sender=self, users=(self.person,))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
820

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
821
class ExtendedManager(models.Manager):
Olga Brani's avatar
Olga Brani committed
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
    def _update_or_create(self, **kwargs):
        assert kwargs, \
            'update_or_create() must be passed at least one keyword argument'
        obj, created = self.get_or_create(**kwargs)
        defaults = kwargs.pop('defaults', {})
        if created:
            return obj, True, False
        else:
            try:
                params = dict(
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
                params.update(defaults)
                for attr, val in params.items():
                    if hasattr(obj, attr):
                        setattr(obj, attr, val)
                sid = transaction.savepoint()
                obj.save(force_update=True)
                transaction.savepoint_commit(sid)
                return obj, False, True
            except IntegrityError, e:
                transaction.savepoint_rollback(sid)
                try:
                    return self.get(**kwargs), False, False
                except self.model.DoesNotExist:
                    raise e

    update_or_create = _update_or_create
849

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
850
class AstakosGroupQuota(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
851
852
853
    objects = ExtendedManager()
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
854
855
    resource = models.ForeignKey(Resource)
    group = models.ForeignKey(AstakosGroup, blank=True)
856

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
857
858
859
860
    class Meta:
        unique_together = ("resource", "group")

class AstakosUserQuota(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
861
862
863
    objects = ExtendedManager()
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
864
865
    resource = models.ForeignKey(Resource)
    user = models.ForeignKey(AstakosUser)
866

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
867
868
    class Meta:
        unique_together = ("resource", "user")
869

870

871
872
873
874
class ApprovalTerms(models.Model):
    """
    Model for approval terms
    """
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
875

876
    date = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
877
878
        _('Issue date'), db_index=True, default=datetime.now())
    location = models.CharField(_('Terms location'), max_length=255)
879

880

881
class Invitation(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
882
883
884
    """
    Model for registring invitations
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
885
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
886
                                null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
887
888
889
890
891
892
    realname = models.CharField(_('Real name'), max_length=255)
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
893

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
894
895
    def __init__(self, *args, **kwargs):
        super(Invitation, self).__init__(*args, **kwargs)
896
897
        if not self.id:
            self.code = _generate_invitation_code()
898

899
900
901
902
    def consume(self):
        self.is_consumed = True
        self.consumed = datetime.now()
        self.save()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
903

904
    def __unicode__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
905
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
906

907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927

class EmailChangeManager(models.Manager):
    @transaction.commit_on_success
    def change_email(self, activation_key):
        """
        Validate an activation key and change the corresponding
        ``User`` if valid.

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

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

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

        After successful email change the activation record is deleted.

        Throws ValueError if there is already
        """
        try:
928
929
            email_change = self.model.objects.get(
                activation_key=activation_key)
930
931
932
933
934
            if email_change.activation_key_expired():
                email_change.delete()
                raise EmailChange.DoesNotExist
            # is there an active user with this address?
            try:
935
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
936
937
938
            except AstakosUser.DoesNotExist:
                pass
            else:
Olga Brani's avatar
Olga Brani committed
939
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
940
941
942
943
944
945
946
            # update user
            user = AstakosUser.objects.get(pk=email_change.user_id)
            user.email = email_change.new_email_address
            user.save()
            email_change.delete()
            return user
        except EmailChange.DoesNotExist:
Olga Brani's avatar
Olga Brani committed
947
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
948
949
950


class EmailChange(models.Model):
Olga Brani's avatar
Olga Brani committed
951
    new_email_address = models.EmailField(_(u'new e-mail address'),
Olga Brani's avatar
Olga Brani committed
952
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
953
954
    user = models.ForeignKey(
        AstakosUser, unique=True, related_name='emailchange_user')
955
    requested_at = models.DateTimeField(default=datetime.now())
956
957
    activation_key = models.CharField(
        max_length=40, unique=True, db_index=True)
958
959
960
961
962

    objects = EmailChangeManager()

    def activation_key_expired(self):
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
963
964
        return self.requested_at + expiration_date < datetime.now()

965

966
967
968
969
970
class AdditionalMail(models.Model):
    """
    Model for registring invitations
    """
    owner = models.ForeignKey(AstakosUser)
971
    email = models.EmailField()
972

973

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
974
975
def _generate_invitation_code():
    while True:
976
        code = randint(1, 2L ** 63 - 1)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
977
978
979
980
981
982
        try:
            Invitation.objects.get(code=code)
            # An invitation with this code already exists, try again
        except Invitation.DoesNotExist:
            return code

983

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
984
985
986
987
988
989
990
991
def get_latest_terms():
    try:
        term = ApprovalTerms.objects.order_by('-id')[0]
        return term
    except IndexError:
        pass
    return None

992
993
994
995
class PendingThirdPartyUser(models.Model):
    """
    Model for registring successful third party user authentications
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
996
997
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
998
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
999
1000
1001
1002
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1003
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1004
1005
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)

1006
1007
    class Meta:
        unique_together = ("provider", "third_party_identifier")
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020

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

    @realname.setter
    def realname(self, value):
        parts = value.split(' ')
        if len(parts) == 2:
            self.first_name = parts[0]
            self.last_name = parts[1]
        else:
            self.last_name = parts[0]
1021