root / snf-astakos-app / astakos / im / models.py @ 85d444db
History | View | Annotate | Download (72.1 kB)
1 |
# Copyright 2011-2012 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 hashlib |
35 |
import uuid |
36 |
import logging |
37 |
import json |
38 |
|
39 |
from time import asctime, sleep |
40 |
from datetime import datetime, timedelta |
41 |
from base64 import b64encode |
42 |
from urlparse import urlparse |
43 |
from urllib import quote |
44 |
from random import randint |
45 |
from collections import defaultdict, namedtuple |
46 |
|
47 |
from django.db import models, IntegrityError, transaction, connection |
48 |
from django.contrib.auth.models import User, UserManager, Group, Permission |
49 |
from django.utils.translation import ugettext as _ |
50 |
from django.db import transaction |
51 |
from django.core.exceptions import ValidationError |
52 |
from django.db.models.signals import ( |
53 |
pre_save, post_save, post_syncdb, post_delete) |
54 |
from django.contrib.contenttypes.models import ContentType |
55 |
|
56 |
from django.dispatch import Signal |
57 |
from django.db.models import Q |
58 |
from django.core.urlresolvers import reverse |
59 |
from django.utils.http import int_to_base36 |
60 |
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.utils.safestring import mark_safe |
64 |
from django.core.validators import email_re |
65 |
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist |
66 |
|
67 |
from astakos.im.settings import ( |
68 |
DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, |
69 |
AUTH_TOKEN_DURATION, BILLING_FIELDS, |
70 |
EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL, |
71 |
SITENAME, SERVICES, MODERATION_ENABLED) |
72 |
from astakos.im import settings as astakos_settings |
73 |
from astakos.im.endpoints.qh import ( |
74 |
register_users, send_quota, register_resources, qh_add_quota, QuotaLimits, |
75 |
qh_query_serials, qh_ack_serials) |
76 |
from astakos.im import auth_providers |
77 |
#from astakos.im.endpoints.aquarium.producer import report_user_event
|
78 |
#from astakos.im.tasks import propagate_groupmembers_quota
|
79 |
|
80 |
import astakos.im.messages as astakos_messages |
81 |
from .managers import ForUpdateManager |
82 |
|
83 |
logger = logging.getLogger(__name__) |
84 |
|
85 |
DEFAULT_CONTENT_TYPE = None
|
86 |
_content_type = None
|
87 |
|
88 |
def get_content_type(): |
89 |
global _content_type
|
90 |
if _content_type is not None: |
91 |
return _content_type
|
92 |
|
93 |
try:
|
94 |
content_type = ContentType.objects.get(app_label='im', model='astakosuser') |
95 |
except:
|
96 |
content_type = DEFAULT_CONTENT_TYPE |
97 |
_content_type = content_type |
98 |
return content_type
|
99 |
|
100 |
RESOURCE_SEPARATOR = '.'
|
101 |
|
102 |
inf = float('inf') |
103 |
|
104 |
class Service(models.Model): |
105 |
name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True) |
106 |
url = models.FilePathField() |
107 |
icon = models.FilePathField(blank=True)
|
108 |
auth_token = models.CharField(_('Authentication Token'), max_length=32, |
109 |
null=True, blank=True) |
110 |
auth_token_created = models.DateTimeField(_('Token creation date'), null=True) |
111 |
auth_token_expires = models.DateTimeField( |
112 |
_('Token expiration date'), null=True) |
113 |
|
114 |
def renew_token(self): |
115 |
md5 = hashlib.md5() |
116 |
md5.update(self.name.encode('ascii', 'ignore')) |
117 |
md5.update(self.url.encode('ascii', 'ignore')) |
118 |
md5.update(asctime()) |
119 |
|
120 |
self.auth_token = b64encode(md5.digest())
|
121 |
self.auth_token_created = datetime.now()
|
122 |
self.auth_token_expires = self.auth_token_created + \ |
123 |
timedelta(hours=AUTH_TOKEN_DURATION) |
124 |
|
125 |
def __str__(self): |
126 |
return self.name |
127 |
|
128 |
@property
|
129 |
def resources(self): |
130 |
return self.resource_set.all() |
131 |
|
132 |
@resources.setter
|
133 |
def resources(self, resources): |
134 |
for s in resources: |
135 |
self.resource_set.create(**s)
|
136 |
|
137 |
def add_resource(self, service, resource, uplimit, update=True): |
138 |
"""Raises ObjectDoesNotExist, IntegrityError"""
|
139 |
resource = Resource.objects.get(service__name=service, name=resource) |
140 |
if update:
|
141 |
AstakosUserQuota.objects.update_or_create(user=self,
|
142 |
resource=resource, |
143 |
defaults={'uplimit': uplimit})
|
144 |
else:
|
145 |
q = self.astakosuserquota_set
|
146 |
q.create(resource=resource, uplimit=uplimit) |
147 |
|
148 |
|
149 |
class ResourceMetadata(models.Model): |
150 |
key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True) |
151 |
value = models.CharField(_('Value'), max_length=255) |
152 |
|
153 |
|
154 |
class Resource(models.Model): |
155 |
name = models.CharField(_('Name'), max_length=255) |
156 |
meta = models.ManyToManyField(ResourceMetadata) |
157 |
service = models.ForeignKey(Service) |
158 |
desc = models.TextField(_('Description'), null=True) |
159 |
unit = models.CharField(_('Name'), null=True, max_length=255) |
160 |
group = models.CharField(_('Group'), null=True, max_length=255) |
161 |
|
162 |
class Meta: |
163 |
unique_together = ("name", "service") |
164 |
|
165 |
def __str__(self): |
166 |
return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name) |
167 |
|
168 |
|
169 |
class GroupKind(models.Model): |
170 |
name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True) |
171 |
|
172 |
def __str__(self): |
173 |
return self.name |
174 |
|
175 |
|
176 |
class AstakosGroup(Group): |
177 |
kind = models.ForeignKey(GroupKind) |
178 |
homepage = models.URLField( |
179 |
_('Homepage Url'), max_length=255, null=True, blank=True) |
180 |
desc = models.TextField(_('Description'), null=True) |
181 |
policy = models.ManyToManyField( |
182 |
Resource, |
183 |
null=True,
|
184 |
blank=True,
|
185 |
through='AstakosGroupQuota'
|
186 |
) |
187 |
creation_date = models.DateTimeField( |
188 |
_('Creation date'),
|
189 |
default=datetime.now() |
190 |
) |
191 |
issue_date = models.DateTimeField( |
192 |
_('Start date'),
|
193 |
null=True
|
194 |
) |
195 |
expiration_date = models.DateTimeField( |
196 |
_('Expiration date'),
|
197 |
null=True
|
198 |
) |
199 |
moderation_enabled = models.BooleanField( |
200 |
_('Moderated membership?'),
|
201 |
default=True
|
202 |
) |
203 |
approval_date = models.DateTimeField( |
204 |
_('Activation date'),
|
205 |
null=True,
|
206 |
blank=True
|
207 |
) |
208 |
estimated_participants = models.PositiveIntegerField( |
209 |
_('Estimated #members'),
|
210 |
null=True,
|
211 |
blank=True,
|
212 |
) |
213 |
max_participants = models.PositiveIntegerField( |
214 |
_('Maximum numder of participants'),
|
215 |
null=True,
|
216 |
blank=True
|
217 |
) |
218 |
|
219 |
@property
|
220 |
def is_disabled(self): |
221 |
if not self.approval_date: |
222 |
return True |
223 |
return False |
224 |
|
225 |
@property
|
226 |
def is_enabled(self): |
227 |
if self.is_disabled: |
228 |
return False |
229 |
if not self.issue_date: |
230 |
return False |
231 |
if not self.expiration_date: |
232 |
return True |
233 |
now = datetime.now() |
234 |
if self.issue_date > now: |
235 |
return False |
236 |
if now >= self.expiration_date: |
237 |
return False |
238 |
return True |
239 |
|
240 |
def enable(self): |
241 |
if self.is_enabled: |
242 |
return
|
243 |
self.approval_date = datetime.now()
|
244 |
self.save()
|
245 |
quota_disturbed.send(sender=self, users=self.approved_members) |
246 |
#propagate_groupmembers_quota.apply_async(
|
247 |
# args=[self], eta=self.issue_date)
|
248 |
#propagate_groupmembers_quota.apply_async(
|
249 |
# args=[self], eta=self.expiration_date)
|
250 |
|
251 |
def disable(self): |
252 |
if self.is_disabled: |
253 |
return
|
254 |
self.approval_date = None |
255 |
self.save()
|
256 |
quota_disturbed.send(sender=self, users=self.approved_members) |
257 |
|
258 |
def approve_member(self, person): |
259 |
m, created = self.membership_set.get_or_create(person=person)
|
260 |
m.approve() |
261 |
|
262 |
@property
|
263 |
def members(self): |
264 |
q = self.membership_set.select_related().all()
|
265 |
return [m.person for m in q] |
266 |
|
267 |
@property
|
268 |
def approved_members(self): |
269 |
q = self.membership_set.select_related().all()
|
270 |
return [m.person for m in q if m.is_approved] |
271 |
|
272 |
@property
|
273 |
def quota(self): |
274 |
d = defaultdict(int)
|
275 |
for q in self.astakosgroupquota_set.select_related().all(): |
276 |
d[q.resource] += q.uplimit or inf
|
277 |
return d
|
278 |
|
279 |
def add_policy(self, service, resource, uplimit, update=True): |
280 |
"""Raises ObjectDoesNotExist, IntegrityError"""
|
281 |
resource = Resource.objects.get(service__name=service, name=resource) |
282 |
if update:
|
283 |
AstakosGroupQuota.objects.update_or_create( |
284 |
group=self,
|
285 |
resource=resource, |
286 |
defaults={'uplimit': uplimit}
|
287 |
) |
288 |
else:
|
289 |
q = self.astakosgroupquota_set
|
290 |
q.create(resource=resource, uplimit=uplimit) |
291 |
|
292 |
@property
|
293 |
def policies(self): |
294 |
return self.astakosgroupquota_set.select_related().all() |
295 |
|
296 |
@policies.setter
|
297 |
def policies(self, policies): |
298 |
for p in policies: |
299 |
service = p.get('service', None) |
300 |
resource = p.get('resource', None) |
301 |
uplimit = p.get('uplimit', 0) |
302 |
update = p.get('update', True) |
303 |
self.add_policy(service, resource, uplimit, update)
|
304 |
|
305 |
@property
|
306 |
def owners(self): |
307 |
return self.owner.all() |
308 |
|
309 |
@property
|
310 |
def owner_details(self): |
311 |
return self.owner.select_related().all() |
312 |
|
313 |
@owners.setter
|
314 |
def owners(self, l): |
315 |
self.owner = l
|
316 |
map(self.approve_member, l) |
317 |
|
318 |
_default_quota = {} |
319 |
def get_default_quota(): |
320 |
global _default_quota
|
321 |
if _default_quota:
|
322 |
return _default_quota
|
323 |
for s, data in SERVICES.iteritems(): |
324 |
map(
|
325 |
lambda d:_default_quota.update(
|
326 |
{'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)} |
327 |
), |
328 |
data.get('resources', {})
|
329 |
) |
330 |
return _default_quota
|
331 |
|
332 |
class AstakosUserManager(UserManager): |
333 |
|
334 |
def get_auth_provider_user(self, provider, **kwargs): |
335 |
"""
|
336 |
Retrieve AstakosUser instance associated with the specified third party
|
337 |
id.
|
338 |
"""
|
339 |
kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]), |
340 |
kwargs.iteritems())) |
341 |
return self.get(auth_providers__module=provider, **kwargs) |
342 |
|
343 |
def get_by_email(self, email): |
344 |
return self.get(email=email) |
345 |
|
346 |
def get_by_identifier(self, email_or_username, **kwargs): |
347 |
try:
|
348 |
return self.get(email__iexact=email_or_username, **kwargs) |
349 |
except AstakosUser.DoesNotExist:
|
350 |
return self.get(username__iexact=email_or_username, **kwargs) |
351 |
|
352 |
def user_exists(self, email_or_username, **kwargs): |
353 |
qemail = Q(email__iexact=email_or_username) |
354 |
qusername = Q(username__iexact=email_or_username) |
355 |
return self.filter(qemail | qusername).exists() |
356 |
|
357 |
|
358 |
class AstakosUser(User): |
359 |
"""
|
360 |
Extends ``django.contrib.auth.models.User`` by defining additional fields.
|
361 |
"""
|
362 |
affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True, |
363 |
null=True)
|
364 |
|
365 |
# DEPRECATED FIELDS: provider, third_party_identifier moved in
|
366 |
# AstakosUserProvider model.
|
367 |
provider = models.CharField(_('Provider'), max_length=255, blank=True, |
368 |
null=True)
|
369 |
# ex. screen_name for twitter, eppn for shibboleth
|
370 |
third_party_identifier = models.CharField(_('Third-party identifier'),
|
371 |
max_length=255, null=True, |
372 |
blank=True)
|
373 |
|
374 |
|
375 |
#for invitations
|
376 |
user_level = DEFAULT_USER_LEVEL |
377 |
level = models.IntegerField(_('Inviter level'), default=user_level)
|
378 |
invitations = models.IntegerField( |
379 |
_('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0)) |
380 |
|
381 |
auth_token = models.CharField(_('Authentication Token'), max_length=32, |
382 |
null=True, blank=True) |
383 |
auth_token_created = models.DateTimeField(_('Token creation date'), null=True) |
384 |
auth_token_expires = models.DateTimeField( |
385 |
_('Token expiration date'), null=True) |
386 |
|
387 |
updated = models.DateTimeField(_('Update date'))
|
388 |
is_verified = models.BooleanField(_('Is verified?'), default=False) |
389 |
|
390 |
email_verified = models.BooleanField(_('Email verified?'), default=False) |
391 |
|
392 |
has_credits = models.BooleanField(_('Has credits?'), default=False) |
393 |
has_signed_terms = models.BooleanField( |
394 |
_('I agree with the terms'), default=False) |
395 |
date_signed_terms = models.DateTimeField( |
396 |
_('Signed terms date'), null=True, blank=True) |
397 |
|
398 |
activation_sent = models.DateTimeField( |
399 |
_('Activation sent data'), null=True, blank=True) |
400 |
|
401 |
policy = models.ManyToManyField( |
402 |
Resource, null=True, through='AstakosUserQuota') |
403 |
|
404 |
uuid = models.CharField(max_length=255, null=True, blank=False, unique=True) |
405 |
|
406 |
astakos_groups = models.ManyToManyField( |
407 |
AstakosGroup, verbose_name=_('agroups'), blank=True, |
408 |
help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP), |
409 |
through='Membership')
|
410 |
|
411 |
__has_signed_terms = False
|
412 |
disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
|
413 |
default=False, db_index=True) |
414 |
|
415 |
objects = AstakosUserManager() |
416 |
|
417 |
|
418 |
owner = models.ManyToManyField( |
419 |
AstakosGroup, related_name='owner', null=True) |
420 |
|
421 |
def __init__(self, *args, **kwargs): |
422 |
super(AstakosUser, self).__init__(*args, **kwargs) |
423 |
self.__has_signed_terms = self.has_signed_terms |
424 |
if not self.id: |
425 |
self.is_active = False |
426 |
|
427 |
@property
|
428 |
def realname(self): |
429 |
return '%s %s' % (self.first_name, self.last_name) |
430 |
|
431 |
@realname.setter
|
432 |
def realname(self, value): |
433 |
parts = value.split(' ')
|
434 |
if len(parts) == 2: |
435 |
self.first_name = parts[0] |
436 |
self.last_name = parts[1] |
437 |
else:
|
438 |
self.last_name = parts[0] |
439 |
|
440 |
def add_permission(self, pname): |
441 |
if self.has_perm(pname): |
442 |
return
|
443 |
p, created = Permission.objects.get_or_create( |
444 |
codename=pname, |
445 |
name=pname.capitalize(), |
446 |
content_type=get_content_type()) |
447 |
self.user_permissions.add(p)
|
448 |
|
449 |
def remove_permission(self, pname): |
450 |
if self.has_perm(pname): |
451 |
return
|
452 |
p = Permission.objects.get(codename=pname, |
453 |
content_type=get_content_type()) |
454 |
self.user_permissions.remove(p)
|
455 |
|
456 |
@property
|
457 |
def invitation(self): |
458 |
try:
|
459 |
return Invitation.objects.get(username=self.email) |
460 |
except Invitation.DoesNotExist:
|
461 |
return None |
462 |
|
463 |
@property
|
464 |
def quota(self): |
465 |
"""Returns a dict with the sum of quota limits per resource"""
|
466 |
d = defaultdict(int)
|
467 |
default_quota = get_default_quota() |
468 |
d.update(default_quota) |
469 |
for q in self.policies: |
470 |
d[q.resource] += q.uplimit or inf
|
471 |
for m in self.projectmembership_set.select_related().all(): |
472 |
if not m.acceptance_date: |
473 |
continue
|
474 |
p = m.project |
475 |
if not p.is_active: |
476 |
continue
|
477 |
grants = p.application.projectresourcegrant_set.all() |
478 |
for g in grants: |
479 |
d[str(g.resource)] += g.member_capacity or inf |
480 |
# TODO set default for remaining
|
481 |
return d
|
482 |
|
483 |
@property
|
484 |
def policies(self): |
485 |
return self.astakosuserquota_set.select_related().all() |
486 |
|
487 |
@policies.setter
|
488 |
def policies(self, policies): |
489 |
for p in policies: |
490 |
service = policies.get('service', None) |
491 |
resource = policies.get('resource', None) |
492 |
uplimit = policies.get('uplimit', 0) |
493 |
update = policies.get('update', True) |
494 |
self.add_policy(service, resource, uplimit, update)
|
495 |
|
496 |
def add_policy(self, service, resource, uplimit, update=True): |
497 |
"""Raises ObjectDoesNotExist, IntegrityError"""
|
498 |
resource = Resource.objects.get(service__name=service, name=resource) |
499 |
if update:
|
500 |
AstakosUserQuota.objects.update_or_create(user=self,
|
501 |
resource=resource, |
502 |
defaults={'uplimit': uplimit})
|
503 |
else:
|
504 |
q = self.astakosuserquota_set
|
505 |
q.create(resource=resource, uplimit=uplimit) |
506 |
|
507 |
def remove_policy(self, service, resource): |
508 |
"""Raises ObjectDoesNotExist, IntegrityError"""
|
509 |
resource = Resource.objects.get(service__name=service, name=resource) |
510 |
q = self.policies.get(resource=resource).delete()
|
511 |
|
512 |
def update_uuid(self): |
513 |
while not self.uuid: |
514 |
uuid_val = str(uuid.uuid4())
|
515 |
try:
|
516 |
AstakosUser.objects.get(uuid=uuid_val) |
517 |
except AstakosUser.DoesNotExist, e:
|
518 |
self.uuid = uuid_val
|
519 |
return self.uuid |
520 |
|
521 |
@property
|
522 |
def extended_groups(self): |
523 |
return self.membership_set.select_related().all() |
524 |
|
525 |
@extended_groups.setter
|
526 |
def extended_groups(self, groups): |
527 |
#TODO exceptions
|
528 |
for name in (groups or ()): |
529 |
group = AstakosGroup.objects.get(name=name) |
530 |
self.membership_set.create(group=group)
|
531 |
|
532 |
def save(self, update_timestamps=True, **kwargs): |
533 |
if update_timestamps:
|
534 |
if not self.id: |
535 |
self.date_joined = datetime.now()
|
536 |
self.updated = datetime.now()
|
537 |
|
538 |
# update date_signed_terms if necessary
|
539 |
if self.__has_signed_terms != self.has_signed_terms: |
540 |
self.date_signed_terms = datetime.now()
|
541 |
|
542 |
self.update_uuid()
|
543 |
|
544 |
if self.username != self.email.lower(): |
545 |
# set username
|
546 |
self.username = self.email.lower() |
547 |
|
548 |
self.validate_unique_email_isactive()
|
549 |
|
550 |
super(AstakosUser, self).save(**kwargs) |
551 |
|
552 |
def renew_token(self, flush_sessions=False, current_key=None): |
553 |
md5 = hashlib.md5() |
554 |
md5.update(settings.SECRET_KEY) |
555 |
md5.update(self.username)
|
556 |
md5.update(self.realname.encode('ascii', 'ignore')) |
557 |
md5.update(asctime()) |
558 |
|
559 |
self.auth_token = b64encode(md5.digest())
|
560 |
self.auth_token_created = datetime.now()
|
561 |
self.auth_token_expires = self.auth_token_created + \ |
562 |
timedelta(hours=AUTH_TOKEN_DURATION) |
563 |
if flush_sessions:
|
564 |
self.flush_sessions(current_key)
|
565 |
msg = 'Token renewed for %s' % self.email |
566 |
logger.log(LOGGING_LEVEL, msg) |
567 |
|
568 |
def flush_sessions(self, current_key=None): |
569 |
q = self.sessions
|
570 |
if current_key:
|
571 |
q = q.exclude(session_key=current_key) |
572 |
|
573 |
keys = q.values_list('session_key', flat=True) |
574 |
if keys:
|
575 |
msg = 'Flushing sessions: %s' % ','.join(keys) |
576 |
logger.log(LOGGING_LEVEL, msg, []) |
577 |
engine = import_module(settings.SESSION_ENGINE) |
578 |
for k in keys: |
579 |
s = engine.SessionStore(k) |
580 |
s.flush() |
581 |
|
582 |
def __unicode__(self): |
583 |
return '%s (%s)' % (self.realname, self.email) |
584 |
|
585 |
def conflicting_email(self): |
586 |
q = AstakosUser.objects.exclude(username=self.username)
|
587 |
q = q.filter(email__iexact=self.email)
|
588 |
if q.count() != 0: |
589 |
return True |
590 |
return False |
591 |
|
592 |
def validate_unique_email_isactive(self): |
593 |
"""
|
594 |
Implements a unique_together constraint for email and is_active fields.
|
595 |
"""
|
596 |
q = AstakosUser.objects.all() |
597 |
q = q.filter(email = self.email)
|
598 |
if self.id: |
599 |
q = q.filter(~Q(id = self.id))
|
600 |
if q.count() != 0: |
601 |
m = 'Another account with the same email = %(email)s & \
|
602 |
is_active = %(is_active)s found.' % self.__dict__ |
603 |
raise ValidationError(m)
|
604 |
|
605 |
def email_change_is_pending(self): |
606 |
return self.emailchanges.count() > 0 |
607 |
|
608 |
def email_change_is_pending(self): |
609 |
return self.emailchanges.count() > 0 |
610 |
|
611 |
@property
|
612 |
def signed_terms(self): |
613 |
term = get_latest_terms() |
614 |
if not term: |
615 |
return True |
616 |
if not self.has_signed_terms: |
617 |
return False |
618 |
if not self.date_signed_terms: |
619 |
return False |
620 |
if self.date_signed_terms < term.date: |
621 |
self.has_signed_terms = False |
622 |
self.date_signed_terms = None |
623 |
self.save()
|
624 |
return False |
625 |
return True |
626 |
|
627 |
def set_invitations_level(self): |
628 |
"""
|
629 |
Update user invitation level
|
630 |
"""
|
631 |
level = self.invitation.inviter.level + 1 |
632 |
self.level = level
|
633 |
self.invitations = INVITATIONS_PER_LEVEL.get(level, 0) |
634 |
|
635 |
def can_login_with_auth_provider(self, provider): |
636 |
if not self.has_auth_provider(provider): |
637 |
return False |
638 |
else:
|
639 |
return auth_providers.get_provider(provider).is_available_for_login()
|
640 |
|
641 |
def can_add_auth_provider(self, provider, **kwargs): |
642 |
provider_settings = auth_providers.get_provider(provider) |
643 |
|
644 |
if not provider_settings.is_available_for_add(): |
645 |
return False |
646 |
|
647 |
if self.has_auth_provider(provider) and \ |
648 |
provider_settings.one_per_user: |
649 |
return False |
650 |
|
651 |
if 'provider_info' in kwargs: |
652 |
kwargs.pop('provider_info')
|
653 |
|
654 |
if 'identifier' in kwargs: |
655 |
try:
|
656 |
# provider with specified params already exist
|
657 |
existing_user = AstakosUser.objects.get_auth_provider_user(provider, |
658 |
**kwargs) |
659 |
except AstakosUser.DoesNotExist:
|
660 |
return True |
661 |
else:
|
662 |
return False |
663 |
|
664 |
return True |
665 |
|
666 |
def can_remove_auth_provider(self, module): |
667 |
provider = auth_providers.get_provider(module) |
668 |
existing = self.get_active_auth_providers()
|
669 |
existing_for_provider = self.get_active_auth_providers(module=module)
|
670 |
|
671 |
if len(existing) <= 1: |
672 |
return False |
673 |
|
674 |
if len(existing_for_provider) == 1 and provider.is_required(): |
675 |
return False |
676 |
|
677 |
return True |
678 |
|
679 |
def can_change_password(self): |
680 |
return self.has_auth_provider('local', auth_backend='astakos') |
681 |
|
682 |
def has_required_auth_providers(self): |
683 |
required = auth_providers.REQUIRED_PROVIDERS |
684 |
for provider in required: |
685 |
if not self.has_auth_provider(provider): |
686 |
return False |
687 |
return True |
688 |
|
689 |
def has_auth_provider(self, provider, **kwargs): |
690 |
return bool(self.auth_providers.filter(module=provider, |
691 |
**kwargs).count()) |
692 |
|
693 |
def add_auth_provider(self, provider, **kwargs): |
694 |
info_data = ''
|
695 |
if 'provider_info' in kwargs: |
696 |
info_data = kwargs.pop('provider_info')
|
697 |
if isinstance(info_data, dict): |
698 |
info_data = json.dumps(info_data) |
699 |
|
700 |
if self.can_add_auth_provider(provider, **kwargs): |
701 |
self.auth_providers.create(module=provider, active=True, |
702 |
info_data=info_data, |
703 |
**kwargs) |
704 |
else:
|
705 |
raise Exception('Cannot add provider') |
706 |
|
707 |
def add_pending_auth_provider(self, pending): |
708 |
"""
|
709 |
Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
|
710 |
the current user.
|
711 |
"""
|
712 |
if not isinstance(pending, PendingThirdPartyUser): |
713 |
pending = PendingThirdPartyUser.objects.get(token=pending) |
714 |
|
715 |
provider = self.add_auth_provider(pending.provider,
|
716 |
identifier=pending.third_party_identifier, |
717 |
affiliation=pending.affiliation, |
718 |
provider_info=pending.info) |
719 |
|
720 |
if email_re.match(pending.email or '') and pending.email != self.email: |
721 |
self.additionalmail_set.get_or_create(email=pending.email)
|
722 |
|
723 |
pending.delete() |
724 |
return provider
|
725 |
|
726 |
def remove_auth_provider(self, provider, **kwargs): |
727 |
self.auth_providers.get(module=provider, **kwargs).delete()
|
728 |
|
729 |
# user urls
|
730 |
def get_resend_activation_url(self): |
731 |
return reverse('send_activation', kwargs={'user_id': self.pk}) |
732 |
|
733 |
def get_provider_remove_url(self, module, **kwargs): |
734 |
return reverse('remove_auth_provider', kwargs={ |
735 |
'pk': self.auth_providers.get(module=module, **kwargs).pk}) |
736 |
|
737 |
def get_activation_url(self, nxt=False): |
738 |
url = "%s?auth=%s" % (reverse('astakos.im.views.activate'), |
739 |
quote(self.auth_token))
|
740 |
if nxt:
|
741 |
url += "&next=%s" % quote(nxt)
|
742 |
return url
|
743 |
|
744 |
def get_password_reset_url(self, token_generator=default_token_generator): |
745 |
return reverse('django.contrib.auth.views.password_reset_confirm', |
746 |
kwargs={'uidb36':int_to_base36(self.id), |
747 |
'token':token_generator.make_token(self)}) |
748 |
|
749 |
def get_auth_providers(self): |
750 |
return self.auth_providers.all() |
751 |
|
752 |
def get_available_auth_providers(self): |
753 |
"""
|
754 |
Returns a list of providers available for user to connect to.
|
755 |
"""
|
756 |
providers = [] |
757 |
for module, provider_settings in auth_providers.PROVIDERS.iteritems(): |
758 |
if self.can_add_auth_provider(module): |
759 |
providers.append(provider_settings(self))
|
760 |
|
761 |
return providers
|
762 |
|
763 |
def get_active_auth_providers(self, **filters): |
764 |
providers = [] |
765 |
for provider in self.auth_providers.active(**filters): |
766 |
if auth_providers.get_provider(provider.module).is_available_for_login():
|
767 |
providers.append(provider) |
768 |
return providers
|
769 |
|
770 |
@property
|
771 |
def auth_providers_display(self): |
772 |
return ",".join(map(lambda x:unicode(x), self.auth_providers.active())) |
773 |
|
774 |
def get_inactive_message(self): |
775 |
msg_extra = ''
|
776 |
message = ''
|
777 |
if self.activation_sent: |
778 |
if self.email_verified: |
779 |
message = _(astakos_messages.ACCOUNT_INACTIVE) |
780 |
else:
|
781 |
message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION) |
782 |
if astakos_settings.MODERATION_ENABLED:
|
783 |
msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) |
784 |
else:
|
785 |
url = self.get_resend_activation_url()
|
786 |
msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \ |
787 |
u' ' + \
|
788 |
_('<a href="%s">%s?</a>') % (url,
|
789 |
_(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT))) |
790 |
else:
|
791 |
if astakos_settings.MODERATION_ENABLED:
|
792 |
message = _(astakos_messages.ACCOUNT_PENDING_MODERATION) |
793 |
else:
|
794 |
message = astakos_messages.ACCOUNT_PENDING_ACTIVATION |
795 |
url = self.get_resend_activation_url()
|
796 |
msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
|
797 |
_(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT))) |
798 |
|
799 |
return mark_safe(message + u' '+ msg_extra) |
800 |
|
801 |
|
802 |
class AstakosUserAuthProviderManager(models.Manager): |
803 |
|
804 |
def active(self, **filters): |
805 |
return self.filter(active=True, **filters) |
806 |
|
807 |
|
808 |
class AstakosUserAuthProvider(models.Model): |
809 |
"""
|
810 |
Available user authentication methods.
|
811 |
"""
|
812 |
affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True, |
813 |
null=True, default=None) |
814 |
user = models.ForeignKey(AstakosUser, related_name='auth_providers')
|
815 |
module = models.CharField(_('Provider'), max_length=255, blank=False, |
816 |
default='local')
|
817 |
identifier = models.CharField(_('Third-party identifier'),
|
818 |
max_length=255, null=True, |
819 |
blank=True)
|
820 |
active = models.BooleanField(default=True)
|
821 |
auth_backend = models.CharField(_('Backend'), max_length=255, blank=False, |
822 |
default='astakos')
|
823 |
info_data = models.TextField(default="", null=True, blank=True) |
824 |
created = models.DateTimeField('Creation date', auto_now_add=True) |
825 |
|
826 |
objects = AstakosUserAuthProviderManager() |
827 |
|
828 |
class Meta: |
829 |
unique_together = (('identifier', 'module', 'user'), ) |
830 |
ordering = ('module', 'created') |
831 |
|
832 |
def __init__(self, *args, **kwargs): |
833 |
super(AstakosUserAuthProvider, self).__init__(*args, **kwargs) |
834 |
try:
|
835 |
self.info = json.loads(self.info_data) |
836 |
if not self.info: |
837 |
self.info = {}
|
838 |
except Exception, e: |
839 |
self.info = {}
|
840 |
|
841 |
for key,value in self.info.iteritems(): |
842 |
setattr(self, 'info_%s' % key, value) |
843 |
|
844 |
|
845 |
@property
|
846 |
def settings(self): |
847 |
return auth_providers.get_provider(self.module) |
848 |
|
849 |
@property
|
850 |
def details_display(self): |
851 |
try:
|
852 |
return self.settings.get_details_tpl_display % self.__dict__ |
853 |
except:
|
854 |
return '' |
855 |
|
856 |
@property
|
857 |
def title_display(self): |
858 |
title_tpl = self.settings.get_title_display
|
859 |
try:
|
860 |
if self.settings.get_user_title_display: |
861 |
title_tpl = self.settings.get_user_title_display
|
862 |
except Exception, e: |
863 |
pass
|
864 |
try:
|
865 |
return title_tpl % self.__dict__ |
866 |
except:
|
867 |
return self.settings.get_title_display % self.__dict__ |
868 |
|
869 |
def can_remove(self): |
870 |
return self.user.can_remove_auth_provider(self.module) |
871 |
|
872 |
def delete(self, *args, **kwargs): |
873 |
ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs) |
874 |
if self.module == 'local': |
875 |
self.user.set_unusable_password()
|
876 |
self.user.save()
|
877 |
return ret
|
878 |
|
879 |
def __repr__(self): |
880 |
return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier) |
881 |
|
882 |
def __unicode__(self): |
883 |
if self.identifier: |
884 |
return "%s:%s" % (self.module, self.identifier) |
885 |
if self.auth_backend: |
886 |
return "%s:%s" % (self.module, self.auth_backend) |
887 |
return self.module |
888 |
|
889 |
def save(self, *args, **kwargs): |
890 |
self.info_data = json.dumps(self.info) |
891 |
return super(AstakosUserAuthProvider, self).save(*args, **kwargs) |
892 |
|
893 |
|
894 |
class Membership(models.Model): |
895 |
person = models.ForeignKey(AstakosUser) |
896 |
group = models.ForeignKey(AstakosGroup) |
897 |
date_requested = models.DateField(default=datetime.now(), blank=True)
|
898 |
date_joined = models.DateField(null=True, db_index=True, blank=True) |
899 |
|
900 |
class Meta: |
901 |
unique_together = ("person", "group") |
902 |
|
903 |
def save(self, *args, **kwargs): |
904 |
if not self.id: |
905 |
if not self.group.moderation_enabled: |
906 |
self.date_joined = datetime.now()
|
907 |
super(Membership, self).save(*args, **kwargs) |
908 |
|
909 |
@property
|
910 |
def is_approved(self): |
911 |
if self.date_joined: |
912 |
return True |
913 |
return False |
914 |
|
915 |
def approve(self): |
916 |
if self.is_approved: |
917 |
return
|
918 |
if self.group.max_participants: |
919 |
assert len(self.group.approved_members) + 1 <= self.group.max_participants, \ |
920 |
'Maximum participant number has been reached.'
|
921 |
self.date_joined = datetime.now()
|
922 |
self.save()
|
923 |
quota_disturbed.send(sender=self, users=(self.person,)) |
924 |
|
925 |
def disapprove(self): |
926 |
approved = self.is_approved()
|
927 |
self.delete()
|
928 |
if approved:
|
929 |
quota_disturbed.send(sender=self, users=(self.person,)) |
930 |
|
931 |
class ExtendedManager(models.Manager): |
932 |
def _update_or_create(self, **kwargs): |
933 |
assert kwargs, \
|
934 |
'update_or_create() must be passed at least one keyword argument'
|
935 |
obj, created = self.get_or_create(**kwargs)
|
936 |
defaults = kwargs.pop('defaults', {})
|
937 |
if created:
|
938 |
return obj, True, False |
939 |
else:
|
940 |
try:
|
941 |
params = dict(
|
942 |
[(k, v) for k, v in kwargs.items() if '__' not in k]) |
943 |
params.update(defaults) |
944 |
for attr, val in params.items(): |
945 |
if hasattr(obj, attr): |
946 |
setattr(obj, attr, val)
|
947 |
sid = transaction.savepoint() |
948 |
obj.save(force_update=True)
|
949 |
transaction.savepoint_commit(sid) |
950 |
return obj, False, True |
951 |
except IntegrityError, e:
|
952 |
transaction.savepoint_rollback(sid) |
953 |
try:
|
954 |
return self.get(**kwargs), False, False |
955 |
except self.model.DoesNotExist: |
956 |
raise e
|
957 |
|
958 |
update_or_create = _update_or_create |
959 |
|
960 |
class AstakosGroupQuota(models.Model): |
961 |
objects = ExtendedManager() |
962 |
limit = models.PositiveIntegerField(_('Limit'), null=True) # obsolete field |
963 |
uplimit = models.BigIntegerField(_('Up limit'), null=True) |
964 |
resource = models.ForeignKey(Resource) |
965 |
group = models.ForeignKey(AstakosGroup, blank=True)
|
966 |
|
967 |
class Meta: |
968 |
unique_together = ("resource", "group") |
969 |
|
970 |
class AstakosUserQuota(models.Model): |
971 |
objects = ExtendedManager() |
972 |
limit = models.PositiveIntegerField(_('Limit'), null=True) # obsolete field |
973 |
uplimit = models.BigIntegerField(_('Up limit'), null=True) |
974 |
resource = models.ForeignKey(Resource) |
975 |
user = models.ForeignKey(AstakosUser) |
976 |
|
977 |
class Meta: |
978 |
unique_together = ("resource", "user") |
979 |
|
980 |
|
981 |
class ApprovalTerms(models.Model): |
982 |
"""
|
983 |
Model for approval terms
|
984 |
"""
|
985 |
|
986 |
date = models.DateTimeField( |
987 |
_('Issue date'), db_index=True, default=datetime.now()) |
988 |
location = models.CharField(_('Terms location'), max_length=255) |
989 |
|
990 |
|
991 |
class Invitation(models.Model): |
992 |
"""
|
993 |
Model for registring invitations
|
994 |
"""
|
995 |
inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
|
996 |
null=True)
|
997 |
realname = models.CharField(_('Real name'), max_length=255) |
998 |
username = models.CharField(_('Unique ID'), max_length=255, unique=True) |
999 |
code = models.BigIntegerField(_('Invitation code'), db_index=True) |
1000 |
is_consumed = models.BooleanField(_('Consumed?'), default=False) |
1001 |
created = models.DateTimeField(_('Creation date'), auto_now_add=True) |
1002 |
consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True) |
1003 |
|
1004 |
def __init__(self, *args, **kwargs): |
1005 |
super(Invitation, self).__init__(*args, **kwargs) |
1006 |
if not self.id: |
1007 |
self.code = _generate_invitation_code()
|
1008 |
|
1009 |
def consume(self): |
1010 |
self.is_consumed = True |
1011 |
self.consumed = datetime.now()
|
1012 |
self.save()
|
1013 |
|
1014 |
def __unicode__(self): |
1015 |
return '%s -> %s [%d]' % (self.inviter, self.username, self.code) |
1016 |
|
1017 |
|
1018 |
class EmailChangeManager(models.Manager): |
1019 |
|
1020 |
@transaction.commit_on_success
|
1021 |
def change_email(self, activation_key): |
1022 |
"""
|
1023 |
Validate an activation key and change the corresponding
|
1024 |
``User`` if valid.
|
1025 |
|
1026 |
If the key is valid and has not expired, return the ``User``
|
1027 |
after activating.
|
1028 |
|
1029 |
If the key is not valid or has expired, return ``None``.
|
1030 |
|
1031 |
If the key is valid but the ``User`` is already active,
|
1032 |
return ``None``.
|
1033 |
|
1034 |
After successful email change the activation record is deleted.
|
1035 |
|
1036 |
Throws ValueError if there is already
|
1037 |
"""
|
1038 |
try:
|
1039 |
email_change = self.model.objects.get(
|
1040 |
activation_key=activation_key) |
1041 |
if email_change.activation_key_expired():
|
1042 |
email_change.delete() |
1043 |
raise EmailChange.DoesNotExist
|
1044 |
# is there an active user with this address?
|
1045 |
try:
|
1046 |
AstakosUser.objects.get(email__iexact=email_change.new_email_address) |
1047 |
except AstakosUser.DoesNotExist:
|
1048 |
pass
|
1049 |
else:
|
1050 |
raise ValueError(_('The new email address is reserved.')) |
1051 |
# update user
|
1052 |
user = AstakosUser.objects.get(pk=email_change.user_id) |
1053 |
old_email = user.email |
1054 |
user.email = email_change.new_email_address |
1055 |
user.save() |
1056 |
email_change.delete() |
1057 |
msg = "User %d changed email from %s to %s" % (user.pk, old_email,
|
1058 |
user.email) |
1059 |
logger.log(LOGGING_LEVEL, msg) |
1060 |
return user
|
1061 |
except EmailChange.DoesNotExist:
|
1062 |
raise ValueError(_('Invalid activation key.')) |
1063 |
|
1064 |
|
1065 |
class EmailChange(models.Model): |
1066 |
new_email_address = models.EmailField( |
1067 |
_(u'new e-mail address'),
|
1068 |
help_text=_('Your old email address will be used until you verify your new one.'))
|
1069 |
user = models.ForeignKey( |
1070 |
AstakosUser, unique=True, related_name='emailchanges') |
1071 |
requested_at = models.DateTimeField(default=datetime.now()) |
1072 |
activation_key = models.CharField( |
1073 |
max_length=40, unique=True, db_index=True) |
1074 |
|
1075 |
objects = EmailChangeManager() |
1076 |
|
1077 |
def get_url(self): |
1078 |
return reverse('email_change_confirm', |
1079 |
kwargs={'activation_key': self.activation_key}) |
1080 |
|
1081 |
def activation_key_expired(self): |
1082 |
expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS) |
1083 |
return self.requested_at + expiration_date < datetime.now() |
1084 |
|
1085 |
|
1086 |
class AdditionalMail(models.Model): |
1087 |
"""
|
1088 |
Model for registring invitations
|
1089 |
"""
|
1090 |
owner = models.ForeignKey(AstakosUser) |
1091 |
email = models.EmailField() |
1092 |
|
1093 |
|
1094 |
def _generate_invitation_code(): |
1095 |
while True: |
1096 |
code = randint(1, 2L ** 63 - 1) |
1097 |
try:
|
1098 |
Invitation.objects.get(code=code) |
1099 |
# An invitation with this code already exists, try again
|
1100 |
except Invitation.DoesNotExist:
|
1101 |
return code
|
1102 |
|
1103 |
|
1104 |
def get_latest_terms(): |
1105 |
try:
|
1106 |
term = ApprovalTerms.objects.order_by('-id')[0] |
1107 |
return term
|
1108 |
except IndexError: |
1109 |
pass
|
1110 |
return None |
1111 |
|
1112 |
class PendingThirdPartyUser(models.Model): |
1113 |
"""
|
1114 |
Model for registring successful third party user authentications
|
1115 |
"""
|
1116 |
third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True) |
1117 |
provider = models.CharField(_('Provider'), max_length=255, blank=True) |
1118 |
email = models.EmailField(_('e-mail address'), blank=True, null=True) |
1119 |
first_name = models.CharField(_('first name'), max_length=30, blank=True) |
1120 |
last_name = models.CharField(_('last name'), max_length=30, blank=True) |
1121 |
affiliation = models.CharField('Affiliation', max_length=255, blank=True) |
1122 |
username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters")) |
1123 |
token = models.CharField(_('Token'), max_length=255, null=True, blank=True) |
1124 |
created = models.DateTimeField(auto_now_add=True, null=True, blank=True) |
1125 |
info = models.TextField(default="", null=True, blank=True) |
1126 |
|
1127 |
class Meta: |
1128 |
unique_together = ("provider", "third_party_identifier") |
1129 |
|
1130 |
def get_user_instance(self): |
1131 |
d = self.__dict__
|
1132 |
d.pop('_state', None) |
1133 |
d.pop('id', None) |
1134 |
d.pop('token', None) |
1135 |
d.pop('created', None) |
1136 |
d.pop('info', None) |
1137 |
user = AstakosUser(**d) |
1138 |
|
1139 |
return user
|
1140 |
|
1141 |
@property
|
1142 |
def realname(self): |
1143 |
return '%s %s' %(self.first_name, self.last_name) |
1144 |
|
1145 |
@realname.setter
|
1146 |
def realname(self, value): |
1147 |
parts = value.split(' ')
|
1148 |
if len(parts) == 2: |
1149 |
self.first_name = parts[0] |
1150 |
self.last_name = parts[1] |
1151 |
else:
|
1152 |
self.last_name = parts[0] |
1153 |
|
1154 |
def save(self, **kwargs): |
1155 |
if not self.id: |
1156 |
# set username
|
1157 |
while not self.username: |
1158 |
username = uuid.uuid4().hex[:30]
|
1159 |
try:
|
1160 |
AstakosUser.objects.get(username = username) |
1161 |
except AstakosUser.DoesNotExist, e:
|
1162 |
self.username = username
|
1163 |
super(PendingThirdPartyUser, self).save(**kwargs) |
1164 |
|
1165 |
def generate_token(self): |
1166 |
self.password = self.third_party_identifier |
1167 |
self.last_login = datetime.now()
|
1168 |
self.token = default_token_generator.make_token(self) |
1169 |
|
1170 |
class SessionCatalog(models.Model): |
1171 |
session_key = models.CharField(_('session key'), max_length=40) |
1172 |
user = models.ForeignKey(AstakosUser, related_name='sessions', null=True) |
1173 |
|
1174 |
class MemberJoinPolicy(models.Model): |
1175 |
policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True) |
1176 |
description = models.CharField(_('Description'), max_length=80) |
1177 |
|
1178 |
def __str__(self): |
1179 |
return self.policy |
1180 |
|
1181 |
class MemberLeavePolicy(models.Model): |
1182 |
policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True) |
1183 |
description = models.CharField(_('Description'), max_length=80) |
1184 |
|
1185 |
def __str__(self): |
1186 |
return self.policy |
1187 |
|
1188 |
_auto_accept_join = False
|
1189 |
def get_auto_accept_join(): |
1190 |
global _auto_accept_join
|
1191 |
if _auto_accept_join is not False: |
1192 |
return _auto_accept_join
|
1193 |
try:
|
1194 |
auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
|
1195 |
except:
|
1196 |
auto_accept = None
|
1197 |
_auto_accept_join = auto_accept |
1198 |
return auto_accept
|
1199 |
|
1200 |
_closed_join = False
|
1201 |
def get_closed_join(): |
1202 |
global _closed_join
|
1203 |
if _closed_join is not False: |
1204 |
return _closed_join
|
1205 |
try:
|
1206 |
closed = MemberJoinPolicy.objects.get(policy='closed')
|
1207 |
except:
|
1208 |
closed = None
|
1209 |
_closed_join = closed |
1210 |
return closed
|
1211 |
|
1212 |
_auto_accept_leave = False
|
1213 |
def get_auto_accept_leave(): |
1214 |
global _auto_accept_leave
|
1215 |
if _auto_accept_leave is not False: |
1216 |
return _auto_accept_leave
|
1217 |
try:
|
1218 |
auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
|
1219 |
except:
|
1220 |
auto_accept = None
|
1221 |
_auto_accept_leave = auto_accept |
1222 |
return auto_accept
|
1223 |
|
1224 |
_closed_leave = False
|
1225 |
def get_closed_leave(): |
1226 |
global _closed_leave
|
1227 |
if _closed_leave is not False: |
1228 |
return _closed_leave
|
1229 |
try:
|
1230 |
closed = MemberLeavePolicy.objects.get(policy='closed')
|
1231 |
except:
|
1232 |
closed = None
|
1233 |
_closed_leave = closed |
1234 |
return closed
|
1235 |
|
1236 |
|
1237 |
### PROJECTS ###
|
1238 |
################
|
1239 |
|
1240 |
|
1241 |
def synced_model_metaclass(class_name, class_parents, class_attributes): |
1242 |
|
1243 |
new_attributes = {} |
1244 |
sync_attributes = {} |
1245 |
|
1246 |
for name, value in class_attributes.iteritems(): |
1247 |
sync, underscore, rest = name.partition('_')
|
1248 |
if sync == 'sync' and underscore == '_': |
1249 |
sync_attributes[rest] = value |
1250 |
else:
|
1251 |
new_attributes[name] = value |
1252 |
|
1253 |
if 'prefix' not in sync_attributes: |
1254 |
m = ("you did not specify a 'sync_prefix' attribute "
|
1255 |
"in class '%s'" % (class_name,))
|
1256 |
raise ValueError(m) |
1257 |
|
1258 |
prefix = sync_attributes.pop('prefix')
|
1259 |
class_name = sync_attributes.pop('classname', prefix + '_model') |
1260 |
|
1261 |
for name, value in sync_attributes.iteritems(): |
1262 |
newname = prefix + '_' + name
|
1263 |
if newname in new_attributes: |
1264 |
m = ("class '%s' was specified with prefix '%s' "
|
1265 |
"but it already has an attribute named '%s'"
|
1266 |
% (class_name, prefix, newname)) |
1267 |
raise ValueError(m) |
1268 |
|
1269 |
new_attributes[newname] = value |
1270 |
|
1271 |
newclass = type(class_name, class_parents, new_attributes)
|
1272 |
return newclass
|
1273 |
|
1274 |
|
1275 |
def make_synced(prefix='sync', name='SyncedState'): |
1276 |
|
1277 |
the_name = name |
1278 |
the_prefix = prefix |
1279 |
|
1280 |
class SyncedState(models.Model): |
1281 |
|
1282 |
sync_classname = the_name |
1283 |
sync_prefix = the_prefix |
1284 |
__metaclass__ = synced_model_metaclass |
1285 |
|
1286 |
sync_new_state = models.BigIntegerField(null=True)
|
1287 |
sync_synced_state = models.BigIntegerField(null=True)
|
1288 |
STATUS_SYNCED = 0
|
1289 |
STATUS_PENDING = 1
|
1290 |
sync_status = models.IntegerField(db_index=True)
|
1291 |
|
1292 |
class Meta: |
1293 |
abstract = True
|
1294 |
|
1295 |
class NotSynced(Exception): |
1296 |
pass
|
1297 |
|
1298 |
def sync_init_state(self, state): |
1299 |
self.sync_synced_state = state
|
1300 |
self.sync_new_state = state
|
1301 |
self.sync_status = self.STATUS_SYNCED |
1302 |
|
1303 |
def sync_get_status(self): |
1304 |
return self.sync_status |
1305 |
|
1306 |
def sync_set_status(self): |
1307 |
if self.sync_new_state != self.sync_synced_state: |
1308 |
self.sync_status = self.STATUS_PENDING |
1309 |
else:
|
1310 |
self.sync_status = self.STATUS_SYNCED |
1311 |
|
1312 |
def sync_set_synced(self): |
1313 |
self.sync_synced_state = self.sync_new_state |
1314 |
self.sync_status = self.STATUS_SYNCED |
1315 |
|
1316 |
def sync_get_synced_state(self): |
1317 |
return self.sync_synced_state |
1318 |
|
1319 |
def sync_set_new_state(self, new_state): |
1320 |
self.sync_new_state = new_state
|
1321 |
self.sync_set_status()
|
1322 |
|
1323 |
def sync_get_new_state(self): |
1324 |
return self.sync_new_state |
1325 |
|
1326 |
def sync_set_synced_state(self, synced_state): |
1327 |
self.sync_synced_state = synced_state
|
1328 |
self.sync_set_status()
|
1329 |
|
1330 |
def sync_get_pending_objects(self): |
1331 |
kw = dict((the_prefix + '_status', self.STATUS_PENDING)) |
1332 |
return self.objects.filter(**kw) |
1333 |
|
1334 |
def sync_get_synced_objects(self): |
1335 |
kw = dict((the_prefix + '_status', self.STATUS_SYNCED)) |
1336 |
return self.objects.filter(**kw) |
1337 |
|
1338 |
def sync_verify_get_synced_state(self): |
1339 |
status = self.sync_get_status()
|
1340 |
state = self.sync_get_synced_state()
|
1341 |
verified = (status == self.STATUS_SYNCED)
|
1342 |
return state, verified
|
1343 |
|
1344 |
def sync_is_synced(self): |
1345 |
state, verified = self.sync_verify_get_synced_state()
|
1346 |
return verified
|
1347 |
|
1348 |
return SyncedState
|
1349 |
|
1350 |
SyncedState = make_synced(prefix='sync', name='SyncedState') |
1351 |
|
1352 |
|
1353 |
class ProjectApplication(models.Model): |
1354 |
PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown' |
1355 |
applicant = models.ForeignKey( |
1356 |
AstakosUser, |
1357 |
related_name='projects_applied',
|
1358 |
db_index=True)
|
1359 |
|
1360 |
state = models.CharField(max_length=80,
|
1361 |
default=UNKNOWN) |
1362 |
|
1363 |
owner = models.ForeignKey( |
1364 |
AstakosUser, |
1365 |
related_name='projects_owned',
|
1366 |
db_index=True)
|
1367 |
|
1368 |
precursor_application = models.OneToOneField('ProjectApplication',
|
1369 |
null=True,
|
1370 |
blank=True,
|
1371 |
db_index=True)
|
1372 |
|
1373 |
name = models.CharField(max_length=80, help_text=" The Project's name should be in a domain format. The domain shouldn't neccessarily exist in the real world but is helpful to imply a structure. e.g.: myproject.mylab.ntua.gr or myservice.myteam.myorganization ",) |
1374 |
homepage = models.URLField(max_length=255, null=True, |
1375 |
blank=True,help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",) |
1376 |
description = models.TextField(null=True, blank=True,help_text= "Please provide a short but descriptive abstract of your Project, so that anyone searching can quickly understand what this Project is about. ") |
1377 |
start_date = models.DateTimeField(help_text= "Here you specify the date you want your Project to start granting its resources. Its members will get the resources coming from this Project on this exact date.")
|
1378 |
end_date = models.DateTimeField(help_text= "Here you specify the date you want your Project to cease. This means that after this date all members will no longer be able to allocate resources from this Project. ")
|
1379 |
member_join_policy = models.ForeignKey(MemberJoinPolicy) |
1380 |
member_leave_policy = models.ForeignKey(MemberLeavePolicy) |
1381 |
limit_on_members_number = models.PositiveIntegerField(null=True,
|
1382 |
blank=True,help_text= "Here you specify the number of members this Project is going to have. This means that this number of people will be granted the resources you will specify in the next step. This can be '1' if you are the only one wanting to get resources. ") |
1383 |
resource_grants = models.ManyToManyField( |
1384 |
Resource, |
1385 |
null=True,
|
1386 |
blank=True,
|
1387 |
through='ProjectResourceGrant')
|
1388 |
comments = models.TextField(null=True, blank=True) |
1389 |
issue_date = models.DateTimeField() |
1390 |
|
1391 |
def add_resource_policy(self, service, resource, uplimit): |
1392 |
"""Raises ObjectDoesNotExist, IntegrityError"""
|
1393 |
q = self.projectresourcegrant_set
|
1394 |
resource = Resource.objects.get(service__name=service, name=resource) |
1395 |
q.create(resource=resource, member_capacity=uplimit) |
1396 |
|
1397 |
|
1398 |
@property
|
1399 |
def grants(self): |
1400 |
return self.projectresourcegrant_set.values('member_capacity', 'resource__name', 'resource__service__name') |
1401 |
|
1402 |
@property
|
1403 |
def resource_policies(self): |
1404 |
return self.projectresourcegrant_set.all() |
1405 |
|
1406 |
@resource_policies.setter
|
1407 |
def resource_policies(self, policies): |
1408 |
for p in policies: |
1409 |
service = p.get('service', None) |
1410 |
resource = p.get('resource', None) |
1411 |
uplimit = p.get('uplimit', 0) |
1412 |
self.add_resource_policy(service, resource, uplimit)
|
1413 |
|
1414 |
@property
|
1415 |
def follower(self): |
1416 |
try:
|
1417 |
return ProjectApplication.objects.get(precursor_application=self) |
1418 |
except ProjectApplication.DoesNotExist:
|
1419 |
return
|
1420 |
|
1421 |
def submit(self, resource_policies, applicant, comments, |
1422 |
precursor_application=None):
|
1423 |
|
1424 |
if precursor_application:
|
1425 |
self.precursor_application = precursor_application
|
1426 |
self.owner = precursor_application.owner
|
1427 |
else:
|
1428 |
self.owner = applicant
|
1429 |
|
1430 |
self.id = None |
1431 |
self.applicant = applicant
|
1432 |
self.comments = comments
|
1433 |
self.issue_date = datetime.now()
|
1434 |
self.state = self.PENDING |
1435 |
self.save()
|
1436 |
self.resource_policies = resource_policies
|
1437 |
|
1438 |
def _get_project(self): |
1439 |
precursor = self
|
1440 |
while precursor:
|
1441 |
try:
|
1442 |
project = precursor.project |
1443 |
return project
|
1444 |
except Project.DoesNotExist:
|
1445 |
pass
|
1446 |
precursor = precursor.precursor_application |
1447 |
|
1448 |
return None |
1449 |
|
1450 |
def approve(self, approval_user=None): |
1451 |
"""
|
1452 |
If approval_user then during owner membership acceptance
|
1453 |
it is checked whether the request_user is eligible.
|
1454 |
|
1455 |
Raises:
|
1456 |
PermissionDenied
|
1457 |
"""
|
1458 |
|
1459 |
if not transaction.is_managed(): |
1460 |
raise AssertionError("NOPE") |
1461 |
|
1462 |
new_project_name = self.name
|
1463 |
if self.state != self.PENDING: |
1464 |
m = _("cannot approve: project '%s' in state '%s'") % (
|
1465 |
new_project_name, self.state)
|
1466 |
raise PermissionDenied(m) # invalid argument |
1467 |
|
1468 |
now = datetime.now() |
1469 |
project = self._get_project()
|
1470 |
|
1471 |
try:
|
1472 |
# needs SERIALIZABLE
|
1473 |
conflicting_project = Project.objects.get(name=new_project_name) |
1474 |
if (conflicting_project.is_alive and |
1475 |
conflicting_project != project): |
1476 |
m = (_("cannot approve: project with name '%s' "
|
1477 |
"already exists (serial: %s)") % (
|
1478 |
new_project_name, conflicting_project.id)) |
1479 |
raise PermissionDenied(m) # invalid argument |
1480 |
except Project.DoesNotExist:
|
1481 |
pass
|
1482 |
|
1483 |
if project is None: |
1484 |
project = Project(creation_date=now) |
1485 |
|
1486 |
project.name = new_project_name |
1487 |
project.application = self
|
1488 |
|
1489 |
# This will block while syncing,
|
1490 |
# but unblock before setting the membership state.
|
1491 |
# See ProjectMembership.set_sync()
|
1492 |
project.set_membership_pending_sync() |
1493 |
|
1494 |
project.last_approval_date = now |
1495 |
project.save() |
1496 |
#ProjectMembership.add_to_project(self)
|
1497 |
project.add_member(self.owner)
|
1498 |
|
1499 |
precursor = self.precursor_application
|
1500 |
while precursor:
|
1501 |
precursor.state = self.REPLACED
|
1502 |
precursor.save() |
1503 |
precursor = precursor.precursor_application |
1504 |
|
1505 |
self.state = self.APPROVED |
1506 |
self.save()
|
1507 |
|
1508 |
|
1509 |
class ProjectResourceGrant(models.Model): |
1510 |
|
1511 |
resource = models.ForeignKey(Resource) |
1512 |
project_application = models.ForeignKey(ProjectApplication, |
1513 |
null=True)
|
1514 |
project_capacity = models.BigIntegerField(null=True)
|
1515 |
project_import_limit = models.BigIntegerField(null=True)
|
1516 |
project_export_limit = models.BigIntegerField(null=True)
|
1517 |
member_capacity = models.BigIntegerField(null=True)
|
1518 |
member_import_limit = models.BigIntegerField(null=True)
|
1519 |
member_export_limit = models.BigIntegerField(null=True)
|
1520 |
|
1521 |
objects = ExtendedManager() |
1522 |
|
1523 |
class Meta: |
1524 |
unique_together = ("resource", "project_application") |
1525 |
|
1526 |
|
1527 |
class Project(models.Model): |
1528 |
|
1529 |
application = models.OneToOneField( |
1530 |
ProjectApplication, |
1531 |
related_name='project')
|
1532 |
last_approval_date = models.DateTimeField(null=True)
|
1533 |
|
1534 |
members = models.ManyToManyField( |
1535 |
AstakosUser, |
1536 |
through='ProjectMembership')
|
1537 |
|
1538 |
termination_start_date = models.DateTimeField(null=True)
|
1539 |
termination_date = models.DateTimeField(null=True)
|
1540 |
|
1541 |
creation_date = models.DateTimeField() |
1542 |
name = models.CharField( |
1543 |
max_length=80,
|
1544 |
db_index=True,
|
1545 |
unique=True)
|
1546 |
|
1547 |
@property
|
1548 |
def violated_resource_grants(self): |
1549 |
return False |
1550 |
|
1551 |
@property
|
1552 |
def violated_members_number_limit(self): |
1553 |
application = self.application
|
1554 |
return len(self.approved_members) > application.limit_on_members_number |
1555 |
|
1556 |
@property
|
1557 |
def is_terminated(self): |
1558 |
return bool(self.termination_date) |
1559 |
|
1560 |
@property
|
1561 |
def is_still_approved(self): |
1562 |
return bool(self.last_approval_date) |
1563 |
|
1564 |
@property
|
1565 |
def is_active(self): |
1566 |
if (self.is_terminated or |
1567 |
not self.is_still_approved or |
1568 |
self.violated_resource_grants):
|
1569 |
return False |
1570 |
# if self.violated_members_number_limit:
|
1571 |
# return False
|
1572 |
return True |
1573 |
|
1574 |
@property
|
1575 |
def is_suspended(self): |
1576 |
if (self.is_terminated or |
1577 |
self.is_still_approved or |
1578 |
not self.violated_resource_grants): |
1579 |
return False |
1580 |
# if not self.violated_members_number_limit:
|
1581 |
# return False
|
1582 |
return True |
1583 |
|
1584 |
@property
|
1585 |
def is_alive(self): |
1586 |
return self.is_active or self.is_suspended |
1587 |
|
1588 |
@property
|
1589 |
def is_inconsistent(self): |
1590 |
now = datetime.now() |
1591 |
if self.creation_date > now: |
1592 |
return True |
1593 |
if self.last_approval_date > now: |
1594 |
return True |
1595 |
if self.terminaton_date > now: |
1596 |
return True |
1597 |
return False |
1598 |
|
1599 |
@property
|
1600 |
def approved_memberships(self): |
1601 |
ACCEPTED = ProjectMembership.ACCEPTED |
1602 |
PENDING = ProjectMembership.PENDING |
1603 |
return self.projectmembership_set.filter( |
1604 |
Q(state=ACCEPTED) | Q(state=PENDING)) |
1605 |
|
1606 |
@property
|
1607 |
def approved_members(self): |
1608 |
return [m.person for m in self.approved_memberships] |
1609 |
|
1610 |
def set_membership_pending_sync(self): |
1611 |
ACCEPTED = ProjectMembership.ACCEPTED |
1612 |
PENDING = ProjectMembership.PENDING |
1613 |
sfu = self.projectmembership_set.select_for_update()
|
1614 |
members = sfu.filter(Q(state=ACCEPTED) | Q(state=PENDING)) |
1615 |
|
1616 |
for member in members: |
1617 |
member.state = member.PENDING |
1618 |
member.save() |
1619 |
|
1620 |
def add_member(self, user): |
1621 |
"""
|
1622 |
Raises:
|
1623 |
django.exceptions.PermissionDenied
|
1624 |
astakos.im.models.AstakosUser.DoesNotExist
|
1625 |
"""
|
1626 |
if isinstance(user, int): |
1627 |
user = AstakosUser.objects.get(user=user) |
1628 |
|
1629 |
m, created = ProjectMembership.objects.get_or_create( |
1630 |
person=user, project=self
|
1631 |
) |
1632 |
m.accept() |
1633 |
|
1634 |
def remove_member(self, user): |
1635 |
"""
|
1636 |
Raises:
|
1637 |
django.exceptions.PermissionDenied
|
1638 |
astakos.im.models.AstakosUser.DoesNotExist
|
1639 |
astakos.im.models.ProjectMembership.DoesNotExist
|
1640 |
"""
|
1641 |
if isinstance(user, int): |
1642 |
user = AstakosUser.objects.get(user=user) |
1643 |
|
1644 |
m = ProjectMembership.objects.get(person=user, project=self)
|
1645 |
m.remove() |
1646 |
|
1647 |
def terminate(self): |
1648 |
self.termination_start_date = datetime.now()
|
1649 |
self.terminaton_date = None |
1650 |
self.save()
|
1651 |
|
1652 |
rejected = self.sync()
|
1653 |
if not rejected: |
1654 |
self.termination_start_date = None |
1655 |
self.termination_date = datetime.now()
|
1656 |
self.save()
|
1657 |
|
1658 |
# try:
|
1659 |
# notification = build_notification(
|
1660 |
# settings.SERVER_EMAIL,
|
1661 |
# [self.application.owner.email],
|
1662 |
# _(PROJECT_TERMINATION_SUBJECT) % self.__dict__,
|
1663 |
# template='im/projects/project_termination_notification.txt',
|
1664 |
# dictionary={'object':self.application}
|
1665 |
# ).send()
|
1666 |
# except NotificationError, e:
|
1667 |
# logger.error(e.message)
|
1668 |
|
1669 |
def suspend(self): |
1670 |
self.last_approval_date = None |
1671 |
self.save()
|
1672 |
self.sync()
|
1673 |
|
1674 |
# try:
|
1675 |
# notification = build_notification(
|
1676 |
# settings.SERVER_EMAIL,
|
1677 |
# [self.application.owner.email],
|
1678 |
# _(PROJECT_SUSPENSION_SUBJECT) % self.__dict__,
|
1679 |
# template='im/projects/project_suspension_notification.txt',
|
1680 |
# dictionary={'object':self.application}
|
1681 |
# ).send()
|
1682 |
# except NotificationError, e:
|
1683 |
# logger.error(e.message)
|
1684 |
|
1685 |
|
1686 |
class ProjectMembership(models.Model): |
1687 |
|
1688 |
person = models.ForeignKey(AstakosUser) |
1689 |
request_date = models.DateField(default=datetime.now()) |
1690 |
project = models.ForeignKey(Project) |
1691 |
|
1692 |
state = models.IntegerField(default=0)
|
1693 |
application = models.ForeignKey( |
1694 |
ProjectApplication, |
1695 |
null=True,
|
1696 |
related_name='memberships')
|
1697 |
pending_application = models.ForeignKey( |
1698 |
ProjectApplication, |
1699 |
null=True,
|
1700 |
related_name='pending_memebrships')
|
1701 |
pending_serial = models.BigIntegerField(null=True, db_index=True) |
1702 |
|
1703 |
acceptance_date = models.DateField(null=True, db_index=True) |
1704 |
leave_request_date = models.DateField(null=True)
|
1705 |
|
1706 |
objects = ForUpdateManager() |
1707 |
|
1708 |
REQUESTED = 0
|
1709 |
PENDING = 1
|
1710 |
ACCEPTED = 2
|
1711 |
REMOVING = 3
|
1712 |
REMOVED = 4
|
1713 |
|
1714 |
class Meta: |
1715 |
unique_together = ("person", "project") |
1716 |
#index_together = [["project", "state"]]
|
1717 |
|
1718 |
def __str__(self): |
1719 |
return _("<'%s' membership in project '%s'>") % ( |
1720 |
self.person.username, self.project.application) |
1721 |
|
1722 |
__repr__ = __str__ |
1723 |
|
1724 |
def __init__(self, *args, **kwargs): |
1725 |
self.state = self.REQUESTED |
1726 |
super(ProjectMembership, self).__init__(*args, **kwargs) |
1727 |
|
1728 |
def _set_history_item(self, reason, date=None): |
1729 |
if isinstance(reason, basestring): |
1730 |
reason = ProjectMembershipHistory.reasons.get(reason, -1)
|
1731 |
|
1732 |
history_item = ProjectMembershipHistory( |
1733 |
serial=self.id,
|
1734 |
person=self.person,
|
1735 |
project=self.project,
|
1736 |
date=date or datetime.now(),
|
1737 |
reason=reason) |
1738 |
history_item.save() |
1739 |
serial = history_item.id |
1740 |
|
1741 |
def accept(self): |
1742 |
state = self.state
|
1743 |
if state != self.REQUESTED: |
1744 |
m = _("%s: attempt to accept in state [%s]") % (self, state) |
1745 |
raise AssertionError(m) |
1746 |
|
1747 |
now = datetime.now() |
1748 |
self.acceptance_date = now
|
1749 |
self._set_history_item(reason='ACCEPT', date=now) |
1750 |
self.state = self.PENDING |
1751 |
self.save()
|
1752 |
|
1753 |
def remove(self): |
1754 |
state = self.state
|
1755 |
if state != self.ACCEPTED: |
1756 |
m = _("%s: attempt to remove in state '%s'") % (self, state) |
1757 |
raise AssertionError(m) |
1758 |
|
1759 |
self._set_history_item(reason='REMOVE') |
1760 |
self.state = self.REMOVING |
1761 |
self.save()
|
1762 |
|
1763 |
def reject(self): |
1764 |
state = self.state
|
1765 |
if state != self.REQUESTED: |
1766 |
m = _("%s: attempt to remove in state '%s'") % (self, state) |
1767 |
raise AssertionError(m) |
1768 |
|
1769 |
# rejected requests don't need sync,
|
1770 |
# because they were never effected
|
1771 |
self._set_history_item(reason='REJECT') |
1772 |
self.delete()
|
1773 |
|
1774 |
def get_diff_quotas(self, sub_list=None, add_list=None, remove=False): |
1775 |
if sub_list is None: |
1776 |
sub_list = [] |
1777 |
|
1778 |
if add_list is None: |
1779 |
add_list = [] |
1780 |
|
1781 |
sub_append = sub_list.append |
1782 |
add_append = add_list.append |
1783 |
holder = self.person.uuid
|
1784 |
|
1785 |
synced_application = self.application
|
1786 |
if synced_application is not None: |
1787 |
cur_grants = synced_application.projectresourcegrant_set.all() |
1788 |
for grant in cur_grants: |
1789 |
sub_append(QuotaLimits( |
1790 |
holder = holder, |
1791 |
resource = str(grant.resource),
|
1792 |
capacity = grant.member_capacity, |
1793 |
import_limit = grant.member_import_limit, |
1794 |
export_limit = grant.member_export_limit)) |
1795 |
|
1796 |
if not remove: |
1797 |
new_grants = self.pending_application.projectresourcegrant_set.all()
|
1798 |
for new_grant in new_grants: |
1799 |
add_append(QuotaLimits( |
1800 |
holder = holder, |
1801 |
resource = str(new_grant.resource),
|
1802 |
capacity = new_grant.member_capacity, |
1803 |
import_limit = new_grant.member_import_limit, |
1804 |
export_limit = new_grant.member_export_limit)) |
1805 |
|
1806 |
return (sub_list, add_list)
|
1807 |
|
1808 |
def set_sync(self): |
1809 |
state = self.state
|
1810 |
if state == self.PENDING: |
1811 |
pending_application = self.pending_application
|
1812 |
if pending_application is None: |
1813 |
m = _("%s: attempt to sync an empty pending application") % (
|
1814 |
self, state)
|
1815 |
raise AssertionError(m) |
1816 |
self.application = pending_application
|
1817 |
self.pending_application = None |
1818 |
self.pending_serial = None |
1819 |
|
1820 |
# project.application may have changed in the meantime,
|
1821 |
# in which case we stay PENDING;
|
1822 |
# we are safe to check due to select_for_update
|
1823 |
if self.application == self.project.application: |
1824 |
self.state = self.ACCEPTED |
1825 |
self.save()
|
1826 |
elif state == self.REMOVING: |
1827 |
self.delete()
|
1828 |
else:
|
1829 |
m = _("%s: attempt to sync in state '%s'") % (self, state) |
1830 |
raise AssertionError(m) |
1831 |
|
1832 |
def reset_sync(self): |
1833 |
state = self.state
|
1834 |
if state in [self.PENDING, self.REMOVING]: |
1835 |
self.pending_application = None |
1836 |
self.pending_serial = None |
1837 |
self.save()
|
1838 |
else:
|
1839 |
m = _("%s: attempt to reset sync in state '%s'") % (self, state) |
1840 |
raise AssertionError(m) |
1841 |
|
1842 |
class Serial(models.Model): |
1843 |
serial = models.AutoField(primary_key=True)
|
1844 |
|
1845 |
def new_serial(): |
1846 |
s = Serial.objects.create() |
1847 |
serial = s.serial |
1848 |
s.delete() |
1849 |
return serial
|
1850 |
|
1851 |
def sync_finish_serials(serials_to_ack=None): |
1852 |
if serials_to_ack is None: |
1853 |
serials_to_ack = qh_query_serials([]) |
1854 |
|
1855 |
serials_to_ack = set(serials_to_ack)
|
1856 |
sfu = ProjectMembership.objects.select_for_update() |
1857 |
memberships = list(sfu.filter(pending_serial__isnull=False)) |
1858 |
|
1859 |
if memberships:
|
1860 |
for membership in memberships: |
1861 |
serial = membership.pending_serial |
1862 |
# just make sure the project row is selected for update
|
1863 |
project = membership.project |
1864 |
if serial in serials_to_ack: |
1865 |
membership.set_sync() |
1866 |
else:
|
1867 |
membership.reset_sync() |
1868 |
|
1869 |
transaction.commit() |
1870 |
|
1871 |
qh_ack_serials(list(serials_to_ack))
|
1872 |
return len(memberships) |
1873 |
|
1874 |
def sync_projects(): |
1875 |
sync_finish_serials() |
1876 |
|
1877 |
PENDING = ProjectMembership.PENDING |
1878 |
REMOVING = ProjectMembership.REMOVING |
1879 |
objects = ProjectMembership.objects.select_for_update() |
1880 |
|
1881 |
sub_quota, add_quota = [], [] |
1882 |
|
1883 |
serial = new_serial() |
1884 |
|
1885 |
pending = objects.filter(state=PENDING) |
1886 |
for membership in pending: |
1887 |
|
1888 |
if membership.pending_application:
|
1889 |
m = "%s: impossible: pending_application is not None (%s)" % (
|
1890 |
membership, membership.pending_application) |
1891 |
raise AssertionError(m) |
1892 |
if membership.pending_serial:
|
1893 |
m = "%s: impossible: pending_serial is not None (%s)" % (
|
1894 |
membership, membership.pending_serial) |
1895 |
raise AssertionError(m) |
1896 |
|
1897 |
membership.pending_application = membership.project.application |
1898 |
membership.pending_serial = serial |
1899 |
membership.get_diff_quotas(sub_quota, add_quota) |
1900 |
membership.save() |
1901 |
|
1902 |
removing = objects.filter(state=REMOVING) |
1903 |
for membership in removing: |
1904 |
|
1905 |
if membership.pending_application:
|
1906 |
m = ("%s: impossible: removing pending_application is not None (%s)"
|
1907 |
% (membership, membership.pending_application)) |
1908 |
raise AssertionError(m) |
1909 |
if membership.pending_serial:
|
1910 |
m = "%s: impossible: pending_serial is not None (%s)" % (
|
1911 |
membership, membership.pending_serial) |
1912 |
raise AssertionError(m) |
1913 |
|
1914 |
membership.pending_serial = serial |
1915 |
membership.get_diff_quotas(sub_quota, add_quota, remove=True)
|
1916 |
membership.save() |
1917 |
|
1918 |
transaction.commit() |
1919 |
# ProjectApplication.approve() unblocks here
|
1920 |
# and can set PENDING an already PENDING membership
|
1921 |
# which has been scheduled to sync with the old project.application
|
1922 |
# Need to check in ProjectMembership.set_sync()
|
1923 |
|
1924 |
r = qh_add_quota(serial, sub_quota, add_quota) |
1925 |
if r:
|
1926 |
m = "cannot sync serial: %d" % serial
|
1927 |
raise RuntimeError(m) |
1928 |
|
1929 |
sync_finish_serials([serial]) |
1930 |
|
1931 |
|
1932 |
def trigger_sync(retries=3, retry_wait=1.0): |
1933 |
cursor = connection.cursor() |
1934 |
locked = True
|
1935 |
try:
|
1936 |
while 1: |
1937 |
cursor.execute("SELECT pg_try_advisory_lock(1)")
|
1938 |
r = cursor.fetchone() |
1939 |
if r is None: |
1940 |
m = "Impossible"
|
1941 |
raise AssertionError(m) |
1942 |
locked = r[0]
|
1943 |
if locked:
|
1944 |
break
|
1945 |
|
1946 |
retries -= 1
|
1947 |
if retries <= 0: |
1948 |
return False |
1949 |
sleep(retry_wait) |
1950 |
|
1951 |
transaction.commit() |
1952 |
sync_projects() |
1953 |
return True |
1954 |
|
1955 |
finally:
|
1956 |
if locked:
|
1957 |
cursor.execute("SELECT pg_advisory_unlock(1)")
|
1958 |
cursor.fetchall() |
1959 |
|
1960 |
|
1961 |
class ProjectMembershipHistory(models.Model): |
1962 |
reasons_list = ['ACCEPT', 'REJECT', 'REMOVE'] |
1963 |
reasons = dict((k, v) for v, k in enumerate(reasons_list)) |
1964 |
|
1965 |
person = models.ForeignKey(AstakosUser) |
1966 |
project = models.ForeignKey(Project) |
1967 |
date = models.DateField(default=datetime.now) |
1968 |
reason = models.IntegerField() |
1969 |
serial = models.BigIntegerField() |
1970 |
|
1971 |
|
1972 |
def filter_queryset_by_property(q, property): |
1973 |
"""
|
1974 |
Incorporate list comprehension for filtering querysets by property
|
1975 |
since Queryset.filter() operates on the database level.
|
1976 |
"""
|
1977 |
return (p for p in q if getattr(p, property, False)) |
1978 |
|
1979 |
def get_alive_projects(): |
1980 |
return filter_queryset_by_property(
|
1981 |
Project.objects.all(), |
1982 |
'is_alive'
|
1983 |
) |
1984 |
|
1985 |
def get_active_projects(): |
1986 |
return filter_queryset_by_property(
|
1987 |
Project.objects.all(), |
1988 |
'is_active'
|
1989 |
) |
1990 |
|
1991 |
def _create_object(model, **kwargs): |
1992 |
o = model.objects.create(**kwargs) |
1993 |
o.save() |
1994 |
return o
|
1995 |
|
1996 |
|
1997 |
def create_astakos_user(u): |
1998 |
try:
|
1999 |
AstakosUser.objects.get(user_ptr=u.pk) |
2000 |
except AstakosUser.DoesNotExist:
|
2001 |
extended_user = AstakosUser(user_ptr_id=u.pk) |
2002 |
extended_user.__dict__.update(u.__dict__) |
2003 |
extended_user.save() |
2004 |
if not extended_user.has_auth_provider('local'): |
2005 |
extended_user.add_auth_provider('local')
|
2006 |
except BaseException, e: |
2007 |
logger.exception(e) |
2008 |
|
2009 |
|
2010 |
def fix_superusers(sender, **kwargs): |
2011 |
# Associate superusers with AstakosUser
|
2012 |
admins = User.objects.filter(is_superuser=True)
|
2013 |
for u in admins: |
2014 |
create_astakos_user(u) |
2015 |
post_syncdb.connect(fix_superusers) |
2016 |
|
2017 |
|
2018 |
def user_post_save(sender, instance, created, **kwargs): |
2019 |
if not created: |
2020 |
return
|
2021 |
create_astakos_user(instance) |
2022 |
post_save.connect(user_post_save, sender=User) |
2023 |
|
2024 |
|
2025 |
# def astakosuser_pre_save(sender, instance, **kwargs):
|
2026 |
# instance.aquarium_report = False
|
2027 |
# instance.new = False
|
2028 |
# try:
|
2029 |
# db_instance = AstakosUser.objects.get(id=instance.id)
|
2030 |
# except AstakosUser.DoesNotExist:
|
2031 |
# # create event
|
2032 |
# instance.aquarium_report = True
|
2033 |
# instance.new = True
|
2034 |
# else:
|
2035 |
# get = AstakosUser.__getattribute__
|
2036 |
# l = filter(lambda f: get(db_instance, f) != get(instance, f),
|
2037 |
# BILLING_FIELDS)
|
2038 |
# instance.aquarium_report = True if l else False
|
2039 |
# pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
|
2040 |
|
2041 |
# def set_default_group(user):
|
2042 |
# try:
|
2043 |
# default = AstakosGroup.objects.get(name='default')
|
2044 |
# Membership(
|
2045 |
# group=default, person=user, date_joined=datetime.now()).save()
|
2046 |
# except AstakosGroup.DoesNotExist, e:
|
2047 |
# logger.exception(e)
|
2048 |
|
2049 |
|
2050 |
def astakosuser_post_save(sender, instance, created, **kwargs): |
2051 |
# if instance.aquarium_report:
|
2052 |
# report_user_event(instance, create=instance.new)
|
2053 |
if not created: |
2054 |
return
|
2055 |
# set_default_group(instance)
|
2056 |
# TODO handle socket.error & IOError
|
2057 |
register_users((instance,)) |
2058 |
post_save.connect(astakosuser_post_save, sender=AstakosUser) |
2059 |
|
2060 |
|
2061 |
def resource_post_save(sender, instance, created, **kwargs): |
2062 |
if not created: |
2063 |
return
|
2064 |
register_resources((instance,)) |
2065 |
post_save.connect(resource_post_save, sender=Resource) |
2066 |
|
2067 |
|
2068 |
# def on_quota_disturbed(sender, users, **kwargs):
|
2069 |
# # print '>>>', locals()
|
2070 |
# if not users:
|
2071 |
# return
|
2072 |
# send_quota(users)
|
2073 |
#
|
2074 |
# quota_disturbed = Signal(providing_args=["users"])
|
2075 |
# quota_disturbed.connect(on_quota_disturbed)
|
2076 |
|
2077 |
|
2078 |
# def send_quota_disturbed(sender, instance, **kwargs):
|
2079 |
# users = []
|
2080 |
# extend = users.extend
|
2081 |
# if sender == Membership:
|
2082 |
# if not instance.group.is_enabled:
|
2083 |
# return
|
2084 |
# extend([instance.person])
|
2085 |
# elif sender == AstakosUserQuota:
|
2086 |
# extend([instance.user])
|
2087 |
# elif sender == AstakosGroupQuota:
|
2088 |
# if not instance.group.is_enabled:
|
2089 |
# return
|
2090 |
# extend(instance.group.astakosuser_set.all())
|
2091 |
# elif sender == AstakosGroup:
|
2092 |
# if not instance.is_enabled:
|
2093 |
# return
|
2094 |
# quota_disturbed.send(sender=sender, users=users)
|
2095 |
# post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
|
2096 |
# post_delete.connect(send_quota_disturbed, sender=Membership)
|
2097 |
# post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
|
2098 |
# post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
|
2099 |
# post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
|
2100 |
# post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
|
2101 |
|
2102 |
|
2103 |
def renew_token(sender, instance, **kwargs): |
2104 |
if not instance.auth_token: |
2105 |
instance.renew_token() |
2106 |
pre_save.connect(renew_token, sender=AstakosUser) |
2107 |
pre_save.connect(renew_token, sender=Service) |
2108 |
|