# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 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.
#
# 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.
#
# 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 uuid
import logging
import json
import copy
from datetime import datetime, timedelta
import base64
from urllib import quote
from random import randint
import os
from django.db import models, transaction
from django.contrib.auth.models import User, UserManager, Group, Permission
from django.utils.translation import ugettext as _
from django.db.models.signals import pre_save, post_save
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.core.urlresolvers import reverse
from django.utils.http import int_to_base36
from django.contrib.auth.tokens import default_token_generator
from django.conf import settings
from django.utils.importlib import import_module
from django.utils.safestring import mark_safe
from synnefo.lib.utils import dict_merge
from astakos.im import settings as astakos_settings
from astakos.im import auth_providers as auth
import astakos.im.messages as astakos_messages
from synnefo.lib.ordereddict import OrderedDict
from synnefo.util.text import uenc, udec
from synnefo.util import units
from astakos.im import presentation
logger = logging.getLogger(__name__)
DEFAULT_CONTENT_TYPE = None
_content_type = None
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
inf = float('inf')
def generate_token():
s = os.urandom(32)
return base64.urlsafe_b64encode(s).rstrip('=')
def _partition_by(f, l):
d = {}
for x in l:
group = f(x)
group_l = d.get(group, [])
group_l.append(x)
d[group] = group_l
return d
def first_of_group(f, l):
Nothing = type("Nothing", (), {})
last_group = Nothing
d = {}
for x in l:
group = f(x)
if group != last_group:
last_group = group
d[group] = x
return d
class Component(models.Model):
name = models.CharField(_('Name'), max_length=255, unique=True,
db_index=True)
url = models.CharField(_('Component url'), max_length=1024, null=True,
help_text=_("URL the component is accessible from"))
base_url = models.CharField(max_length=1024, null=True)
auth_token = models.CharField(_('Authentication Token'), max_length=64,
null=True, blank=True, unique=True)
auth_token_created = models.DateTimeField(_('Token creation date'),
null=True)
auth_token_expires = models.DateTimeField(_('Token expiration date'),
null=True)
def renew_token(self, expiration_date=None):
for i in range(10):
new_token = generate_token()
count = Component.objects.filter(auth_token=new_token).count()
if count == 0:
break
continue
else:
raise ValueError('Could not generate a token')
self.auth_token = new_token
self.auth_token_created = datetime.now()
if expiration_date:
self.auth_token_expires = expiration_date
else:
self.auth_token_expires = None
msg = 'Token renewed for component %s'
logger.log(astakos_settings.LOGGING_LEVEL, msg, self.name)
def __str__(self):
return self.name
@classmethod
def catalog(cls, orderfor=None):
catalog = {}
components = list(cls.objects.all())
default_metadata = presentation.COMPONENTS
metadata = {}
for component in components:
d = {'url': component.url,
'name': component.name}
if component.name in default_metadata:
metadata[component.name] = default_metadata.get(component.name)
metadata[component.name].update(d)
else:
metadata[component.name] = d
def component_by_order(s):
return s[1].get('order')
def component_by_dashboard_order(s):
return s[1].get('dashboard').get('order')
metadata = dict_merge(metadata,
astakos_settings.COMPONENTS_META)
for component, info in metadata.iteritems():
default_meta = presentation.component_defaults(component)
base_meta = metadata.get(component, {})
settings_meta = astakos_settings.COMPONENTS_META.get(component, {})
component_meta = dict_merge(default_meta, base_meta)
meta = dict_merge(component_meta, settings_meta)
catalog[component] = meta
order_key = component_by_order
if orderfor == 'dashboard':
order_key = component_by_dashboard_order
ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
key=order_key))
return ordered_catalog
_presentation_data = {}
def get_presentation(resource):
global _presentation_data
resource_presentation = _presentation_data.get(resource, {})
if not resource_presentation:
resources_presentation = presentation.RESOURCES.get('resources', {})
resource_presentation = resources_presentation.get(resource, {})
_presentation_data[resource] = resource_presentation
return resource_presentation
class Service(models.Model):
component = models.ForeignKey(Component)
name = models.CharField(max_length=255, unique=True)
type = models.CharField(max_length=255)
class Endpoint(models.Model):
service = models.ForeignKey(Service, related_name='endpoints')
class EndpointData(models.Model):
endpoint = models.ForeignKey(Endpoint, related_name='data')
key = models.CharField(max_length=255)
value = models.CharField(max_length=1024)
class Meta:
unique_together = (('endpoint', 'key'),)
class Resource(models.Model):
name = models.CharField(_('Name'), max_length=255, unique=True)
desc = models.TextField(_('Description'), null=True)
service_type = models.CharField(_('Type'), max_length=255)
service_origin = models.CharField(max_length=255, db_index=True)
unit = models.CharField(_('Unit'), null=True, max_length=255)
uplimit = models.BigIntegerField(default=0)
ui_visible = models.BooleanField(default=True)
api_visible = models.BooleanField(default=True)
def __str__(self):
return self.name
def full_name(self):
return str(self)
def get_info(self):
return {'service': self.service_origin,
'description': self.desc,
'unit': self.unit,
'ui_visible': self.ui_visible,
'api_visible': self.api_visible,
}
@property
def group(self):
default = self.name
return get_presentation(str(self)).get('group', default)
@property
def help_text(self):
default = "%s resource" % self.name
return get_presentation(str(self)).get('help_text', default)
@property
def help_text_input_each(self):
default = "%s resource" % self.name
return get_presentation(str(self)).get('help_text_input_each', default)
@property
def is_abbreviation(self):
return get_presentation(str(self)).get('is_abbreviation', False)
@property
def report_desc(self):
default = "%s resource" % self.name
return get_presentation(str(self)).get('report_desc', default)
@property
def placeholder(self):
return get_presentation(str(self)).get('placeholder', self.unit)
@property
def verbose_name(self):
return get_presentation(str(self)).get('verbose_name', self.name)
@property
def display_name(self):
name = self.verbose_name
if self.is_abbreviation:
name = name.upper()
return name
@property
def pluralized_display_name(self):
if not self.unit:
return '%ss' % self.display_name
return self.display_name
def get_resource_names():
_RESOURCE_NAMES = []
resources = Resource.objects.select_related('service').all()
_RESOURCE_NAMES = [resource.full_name() for resource in resources]
return _RESOURCE_NAMES
def split_realname(value):
parts = value.split(' ')
if len(parts) == 2:
return parts
else:
return ('', value)
class AstakosUserManager(UserManager):
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)
def get_by_email(self, email):
return self.get(email=email)
def get_by_identifier(self, email_or_username, **kwargs):
try:
return self.get(email__iexact=email_or_username, **kwargs)
except AstakosUser.DoesNotExist:
return self.get(username__iexact=email_or_username, **kwargs)
def user_exists(self, email_or_username, **kwargs):
qemail = Q(email__iexact=email_or_username)
qusername = Q(username__iexact=email_or_username)
qextra = Q(**kwargs)
return self.filter((qemail | qusername) & qextra).exists()
def unverified_namesakes(self, email_or_username):
q = Q(email__iexact=email_or_username)
q |= Q(username__iexact=email_or_username)
return self.filter(q & Q(email_verified=False))
def verified_user_exists(self, email_or_username):
return self.user_exists(email_or_username, email_verified=True)
def verified(self):
return self.filter(email_verified=True)
def accepted(self):
return self.filter(moderated=True, is_rejected=False)
def uuid_catalog(self, l=None):
"""
Returns a uuid to username mapping for the uuids appearing in l.
If l is None returns the mapping for all existing users.
"""
q = self.filter(uuid__in=l) if l is not None else self
return dict(q.values_list('uuid', 'username'))
def displayname_catalog(self, l=None):
"""
Returns a username to uuid mapping for the usernames appearing in l.
If l is None returns the mapping for all existing users.
"""
if l is not None:
lmap = dict((x.lower(), x) for x in l)
q = self.filter(username__in=lmap.keys())
values = ((lmap[n], u)
for n, u in q.values_list('username', 'uuid'))
else:
q = self
values = self.values_list('username', 'uuid')
return dict(values)
class AstakosUser(User):
"""
Extends ``django.contrib.auth.models.User`` by defining additional fields.
"""
affiliation = models.CharField(_('Affiliation'), max_length=255,
blank=True, null=True)
#for invitations
user_level = astakos_settings.DEFAULT_USER_LEVEL
level = models.IntegerField(_('Inviter level'), default=user_level)
invitations = models.IntegerField(
_('Invitations left'),
default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
auth_token = models.CharField(
_('Authentication Token'),
max_length=64,
unique=True,
null=True,
blank=True,
help_text=_('Renew your authentication '
'token. Make sure to set the new '
'token in any client you may be '
'using, to preserve its '
'functionality.'))
auth_token_created = models.DateTimeField(_('Token creation date'),
null=True)
auth_token_expires = models.DateTimeField(
_('Token expiration date'), null=True)
updated = models.DateTimeField(_('Update date'))
# Arbitrary text to identify the reason user got deactivated.
# To be used as a reference from administrators.
deactivated_reason = models.TextField(
_('Reason the user was disabled for'),
default=None, null=True)
deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
blank=True)
has_credits = models.BooleanField(_('Has credits?'), default=False)
# this is set to True when user profile gets updated for the first time
is_verified = models.BooleanField(_('Is verified?'), default=False)
# user email is verified
email_verified = models.BooleanField(_('Email verified?'), default=False)
# unique string used in user email verification url
verification_code = models.CharField(max_length=255, null=True,
blank=False, unique=True)
# date user email verified
verified_at = models.DateTimeField(_('User verified email at'), null=True,
blank=True)
# email verification notice was sent to the user at this time
activation_sent = models.DateTimeField(_('Activation sent date'),
null=True, blank=True)
# user got rejected during moderation process
is_rejected = models.BooleanField(_('Account rejected'),
default=False)
# reason user got rejected
rejected_reason = models.TextField(_('User rejected reason'), null=True,
blank=True)
# moderation status
moderated = models.BooleanField(_('User moderated'), default=False)
# date user moderated (either accepted or rejected)
moderated_at = models.DateTimeField(_('Date moderated'), default=None,
blank=True, null=True)
# a snapshot of user instance the time got moderated
moderated_data = models.TextField(null=True, default=None, blank=True)
# a string which identifies how the user got moderated
accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
default=None, null=True, blank=True)
# the email used to accept the user
accepted_email = models.EmailField(null=True, default=None, blank=True)
has_signed_terms = models.BooleanField(_('I agree with the terms'),
default=False)
date_signed_terms = models.DateTimeField(_('Signed terms date'),
null=True, blank=True)
# permanent unique user identifier
uuid = models.CharField(max_length=255, null=False, blank=False,
unique=True)
policy = models.ManyToManyField(
Resource, null=True, through='AstakosUserQuota')
disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
default=False, db_index=True)
objects = AstakosUserManager()
@property
def realname(self):
return '%s %s' % (self.first_name, self.last_name)
@property
def log_display(self):
"""
Should be used in all logger.* calls that refer to a user so that
user display is consistent across log entries.
"""
return '%s::%s' % (self.uuid, self.email)
@realname.setter
def realname(self, value):
first, last = split_realname(value)
self.first_name = first
self.last_name = last
def add_permission(self, pname):
if self.has_perm(pname):
return
p, created = Permission.objects.get_or_create(
codename=pname,
name=pname.capitalize(),
content_type=get_content_type())
self.user_permissions.add(p)
def remove_permission(self, pname):
if self.has_perm(pname):
return
p = Permission.objects.get(codename=pname,
content_type=get_content_type())
self.user_permissions.remove(p)
def add_group(self, gname):
group, _ = Group.objects.get_or_create(name=gname)
self.groups.add(group)
def is_accepted(self):
return self.moderated and not self.is_rejected
def is_project_admin(self, application_id=None):
return self.uuid in astakos_settings.PROJECT_ADMINS
@property
def invitation(self):
try:
return Invitation.objects.get(username=self.email)
except Invitation.DoesNotExist:
return None
@property
def policies(self):
return self.astakosuserquota_set.select_related().all()
def get_resource_policy(self, resource):
return AstakosUserQuota.objects.select_related("resource").\
get(user=self, resource__name=resource)
def fix_username(self):
self.username = self.email.lower()
def set_email(self, email):
self.email = email
self.fix_username()
def save(self, update_timestamps=True, **kwargs):
if update_timestamps:
self.updated = datetime.now()
super(AstakosUser, self).save(**kwargs)
def renew_verification_code(self):
self.verification_code = str(uuid.uuid4())
logger.info("Verification code renewed for %s" % self.log_display)
def renew_token(self, flush_sessions=False, current_key=None):
for i in range(10):
new_token = generate_token()
count = AstakosUser.objects.filter(auth_token=new_token).count()
if count == 0:
break
continue
else:
raise ValueError('Could not generate a token')
self.auth_token = new_token
self.auth_token_created = datetime.now()
self.auth_token_expires = self.auth_token_created + \
timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
if flush_sessions:
self.flush_sessions(current_key)
msg = 'Token renewed for %s'
logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
def token_expired(self):
return self.auth_token_expires < datetime.now()
def flush_sessions(self, current_key=None):
q = self.sessions
if current_key:
q = q.exclude(session_key=current_key)
keys = q.values_list('session_key', flat=True)
if keys:
msg = 'Flushing sessions: %s'
logger.log(astakos_settings.LOGGING_LEVEL, msg, ','.join(keys))
engine = import_module(settings.SESSION_ENGINE)
for k in keys:
s = engine.SessionStore(k)
s.flush()
def __unicode__(self):
return '%s (%s)' % (self.realname, self.email)
def conflicting_email(self):
q = AstakosUser.objects.exclude(username=self.username)
q = q.filter(email__iexact=self.email)
if q.count() != 0:
return True
return False
def email_change_is_pending(self):
return self.emailchanges.count() > 0
@property
def status_display(self):
msg = ""
if self.is_active:
msg = "Accepted/Active"
if self.is_rejected:
msg = "Rejected"
if self.rejected_reason:
msg += " (%s)" % self.rejected_reason
if not self.email_verified:
msg = "Pending email verification"
if not self.moderated:
msg = "Pending moderation"
if not self.is_active and self.email_verified:
msg = "Accepted/Inactive"
if self.deactivated_reason:
msg += " (%s)" % (self.deactivated_reason)
if self.moderated and not self.is_rejected:
if self.accepted_policy == 'manual':
msg += " (manually accepted)"
else:
msg += " (accepted policy: %s)" % \
self.accepted_policy
return msg
@property
def signed_terms(self):
return self.has_signed_terms
def set_invitations_level(self):
"""
Update user invitation level
"""
level = self.invitation.inviter.level + 1
self.level = level
self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
def can_change_password(self):
return self.has_auth_provider('local', auth_backend='astakos')
def can_change_email(self):
if not self.has_auth_provider('local'):
return True
local = self.get_auth_provider('local')._instance
return local.auth_backend == 'astakos'
# Auth providers related methods
def get_auth_provider(self, module=None, identifier=None, **filters):
if not module:
return self.auth_providers.active()[0].settings
params = {'module': module}
if identifier:
params['identifier'] = identifier
params.update(filters)
return self.auth_providers.active().get(**params).settings
def has_auth_provider(self, provider, **kwargs):
return bool(self.auth_providers.active().filter(module=provider,
**kwargs).count())
def get_required_providers(self, **kwargs):
return auth.REQUIRED_PROVIDERS.keys()
def missing_required_providers(self):
required = self.get_required_providers()
missing = []
for provider in required:
if not self.has_auth_provider(provider):
missing.append(auth.get_provider(provider, self))
return missing
def get_available_auth_providers(self, **filters):
"""
Returns a list of providers available for add by the user.
"""
modules = astakos_settings.IM_MODULES
providers = []
for p in modules:
providers.append(auth.get_provider(p, self))
available = []
for p in providers:
if p.get_add_policy:
available.append(p)
return available
def get_disabled_auth_providers(self, **filters):
providers = self.get_auth_providers(**filters)
disabled = []
for p in providers:
if not p.get_login_policy:
disabled.append(p)
return disabled
def get_enabled_auth_providers(self, **filters):
providers = self.get_auth_providers(**filters)
enabled = []
for p in providers:
if p.get_login_policy:
enabled.append(p)
return enabled
def get_auth_providers(self, **filters):
providers = []
for provider in self.auth_providers.active(**filters):
if provider.settings.module_enabled:
providers.append(provider.settings)
modules = astakos_settings.IM_MODULES
def key(p):
if not p.module in modules:
return 100
return modules.index(p.module)
providers = sorted(providers, key=key)
return providers
# URL methods
@property
def auth_providers_display(self):
return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
self.get_enabled_auth_providers()])
def add_auth_provider(self, module='local', identifier=None, **params):
provider = auth.get_provider(module, self, identifier, **params)
provider.add_to_user()
def get_resend_activation_url(self):
return reverse('send_activation', kwargs={'user_id': self.pk})
def get_activation_url(self, nxt=False):
url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
quote(self.verification_code))
if nxt:
url += "&next=%s" % quote(nxt)
return url
def get_password_reset_url(self, token_generator=default_token_generator):
return reverse('astakos.im.views.target.local.password_reset_confirm',
kwargs={'uidb36': int_to_base36(self.id),
'token': token_generator.make_token(self)})
def get_inactive_message(self, provider_module, identifier=None):
provider = self.get_auth_provider(provider_module, identifier)
msg_extra = ''
message = ''
msg_inactive = provider.get_account_inactive_msg
msg_pending = provider.get_pending_activation_msg
msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
#msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
msg_pending_mod = provider.get_pending_moderation_msg
msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
if not self.email_verified:
message = msg_pending
url = self.get_resend_activation_url()
msg_extra = msg_pending_help + \
u' ' + \
'%s?' % (url, msg_resend)
else:
if not self.moderated:
message = msg_pending_mod
else:
if self.is_rejected:
message = msg_rejected
else:
message = msg_inactive
return mark_safe(message + u' ' + msg_extra)
def owns_application(self, application):
return application.owner == self
def owns_project(self, project):
return project.application.owner == self
def is_associated(self, project):
try:
m = ProjectMembership.objects.get(person=self, project=project)
return m.state in ProjectMembership.ASSOCIATED_STATES
except ProjectMembership.DoesNotExist:
return False
def get_membership(self, project):
try:
return ProjectMembership.objects.get(
project=project,
person=self)
except ProjectMembership.DoesNotExist:
return None
def membership_display(self, project):
m = self.get_membership(project)
if m is None:
return _('Not a member')
else:
return m.user_friendly_state_display()
def non_owner_can_view(self, maybe_project):
if self.is_project_admin():
return True
if maybe_project is None:
return False
project = maybe_project
if self.is_associated(project):
return True
if project.is_deactivated():
return False
return True
class AstakosUserAuthProviderManager(models.Manager):
def active(self, **filters):
return self.filter(active=True, **filters)
def remove_unverified_providers(self, provider, **filters):
try:
existing = self.filter(module=provider, user__email_verified=False,
**filters)
for p in existing:
p.user.delete()
except:
pass
def unverified(self, provider, **filters):
try:
return self.get(module=provider, user__email_verified=False,
**filters).settings
except AstakosUserAuthProvider.DoesNotExist:
return None
def verified(self, provider, **filters):
try:
return self.get(module=provider, user__email_verified=True,
**filters).settings
except AstakosUserAuthProvider.DoesNotExist:
return None
class AuthProviderPolicyProfileManager(models.Manager):
def active(self):
return self.filter(active=True)
def for_user(self, user, provider):
policies = {}
exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
exclusive_q = exclusive_q1 | exclusive_q2
for profile in user.authpolicy_profiles.active().filter(exclusive_q):
policies.update(profile.policies)
user_groups = user.groups.all().values('pk')
for profile in self.active().filter(groups__in=user_groups).filter(
exclusive_q):
policies.update(profile.policies)
return policies
def add_policy(self, name, provider, group_or_user, exclusive=False,
**policies):
is_group = isinstance(group_or_user, Group)
profile, created = self.get_or_create(name=name, provider=provider,
is_exclusive=exclusive)
profile.is_exclusive = exclusive
profile.save()
if is_group:
profile.groups.add(group_or_user)
else:
profile.users.add(group_or_user)
profile.set_policies(policies)
profile.save()
return profile
class AuthProviderPolicyProfile(models.Model):
name = models.CharField(_('Name'), max_length=255, blank=False,
null=False, db_index=True)
provider = models.CharField(_('Provider'), max_length=255, blank=False,
null=False)
# apply policies to all providers excluding the one set in provider field
is_exclusive = models.BooleanField(default=False)
policy_add = models.NullBooleanField(null=True, default=None)
policy_remove = models.NullBooleanField(null=True, default=None)
policy_create = models.NullBooleanField(null=True, default=None)
policy_login = models.NullBooleanField(null=True, default=None)
policy_limit = models.IntegerField(null=True, default=None)
policy_required = models.NullBooleanField(null=True, default=None)
policy_automoderate = models.NullBooleanField(null=True, default=None)
policy_switch = models.NullBooleanField(null=True, default=None)
POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
'automoderate')
priority = models.IntegerField(null=False, default=1)
groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
users = models.ManyToManyField(AstakosUser,
related_name='authpolicy_profiles')
active = models.BooleanField(default=True)
objects = AuthProviderPolicyProfileManager()
class Meta:
ordering = ['priority']
@property
def policies(self):
policies = {}
for pkey in self.POLICY_FIELDS:
value = getattr(self, 'policy_%s' % pkey, None)
if value is None:
continue
policies[pkey] = value
return policies
def set_policies(self, policies_dict):
for key, value in policies_dict.iteritems():
if key in self.POLICY_FIELDS:
setattr(self, 'policy_%s' % key, value)
return self.policies
class AstakosUserAuthProvider(models.Model):
"""
Available user authentication methods.
"""
affiliation = models.CharField(_('Affiliation'), max_length=255,
blank=True, null=True, default=None)
user = models.ForeignKey(AstakosUser, related_name='auth_providers')
module = models.CharField(_('Provider'), max_length=255, blank=False,
default='local')
identifier = models.CharField(_('Third-party identifier'),
max_length=255, null=True,
blank=True)
active = models.BooleanField(default=True)
auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
default='astakos')
info_data = models.TextField(default="", null=True, blank=True)
created = models.DateTimeField('Creation date', auto_now_add=True)
objects = AstakosUserAuthProviderManager()
class Meta:
unique_together = (('identifier', 'module', 'user'), )
ordering = ('module', 'created')
def __init__(self, *args, **kwargs):
super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
try:
self.info = json.loads(self.info_data)
if not self.info:
self.info = {}
except Exception:
self.info = {}
for key, value in self.info.iteritems():
setattr(self, 'info_%s' % key, value)
@property
def settings(self):
extra_data = {}
info_data = {}
if self.info_data:
info_data = json.loads(self.info_data)
extra_data['info'] = info_data
for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
extra_data[key] = getattr(self, key)
extra_data['instance'] = self
return auth.get_provider(self.module, self.user,
self.identifier, **extra_data)
def __repr__(self):
return '' % (
self.module, self.identifier)
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
def save(self, *args, **kwargs):
self.info_data = json.dumps(self.info)
return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
class AstakosUserQuota(models.Model):
capacity = models.BigIntegerField()
resource = models.ForeignKey(Resource)
user = models.ForeignKey(AstakosUser)
class Meta:
unique_together = ("resource", "user")
class ApprovalTerms(models.Model):
"""
Model for approval terms
"""
date = models.DateTimeField(
_('Issue date'), db_index=True, auto_now_add=True)
location = models.CharField(_('Terms location'), max_length=255)
class Invitation(models.Model):
"""
Model for registring invitations
"""
inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
null=True)
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)
def __init__(self, *args, **kwargs):
super(Invitation, self).__init__(*args, **kwargs)
if not self.id:
self.code = _generate_invitation_code()
def consume(self):
self.is_consumed = True
self.consumed = datetime.now()
self.save()
def __unicode__(self):
return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
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:
email_change = self.model.objects.get(
activation_key=activation_key)
if email_change.activation_key_expired():
email_change.delete()
raise EmailChange.DoesNotExist
# is there an active user with this address?
try:
AstakosUser.objects.get(
email__iexact=email_change.new_email_address)
except AstakosUser.DoesNotExist:
pass
else:
raise ValueError(_('The new email address is reserved.'))
# update user
user = AstakosUser.objects.select_for_update().\
get(pk=email_change.user_id)
old_email = user.email
user.set_email(email_change.new_email_address)
user.save()
email_change.delete()
msg = "User %s changed email from %s to %s"
logger.log(astakos_settings.LOGGING_LEVEL, msg, user.log_display,
old_email, user.email)
return user
except EmailChange.DoesNotExist:
raise ValueError(_('Invalid activation key.'))
class EmailChange(models.Model):
new_email_address = models.EmailField(
_(u'new e-mail address'),
help_text=_('Provide a new email address. Until you verify the new '
'address by following the activation link that will be '
'sent to it, your old email address will remain active.'))
user = models.ForeignKey(
AstakosUser, unique=True, related_name='emailchanges')
requested_at = models.DateTimeField(auto_now_add=True)
activation_key = models.CharField(
max_length=40, unique=True, db_index=True)
objects = EmailChangeManager()
def get_url(self):
return reverse('email_change_confirm',
kwargs={'activation_key': self.activation_key})
def activation_key_expired(self):
expiration_date = timedelta(
days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
return self.requested_at + expiration_date < datetime.now()
class AdditionalMail(models.Model):
"""
Model for registring invitations
"""
owner = models.ForeignKey(AstakosUser)
email = models.EmailField()
def _generate_invitation_code():
while True:
code = randint(1, 2L ** 63 - 1)
try:
Invitation.objects.get(code=code)
# An invitation with this code already exists, try again
except Invitation.DoesNotExist:
return code
def get_latest_terms():
try:
term = ApprovalTerms.objects.order_by('-id')[0]
return term
except IndexError:
pass
return None
class PendingThirdPartyUser(models.Model):
"""
Model for registring successful third party user authentications
"""
third_party_identifier = models.CharField(
_('Third-party identifier'), max_length=255, null=True, blank=True)
provider = models.CharField(_('Provider'), max_length=255, blank=True)
email = models.EmailField(_('e-mail address'), blank=True, null=True)
first_name = models.CharField(_('first name'), max_length=30, blank=True,
null=True)
last_name = models.CharField(_('last name'), max_length=30, blank=True,
null=True)
affiliation = models.CharField('Affiliation', max_length=255, blank=True,
null=True)
username = models.CharField(
_('username'), max_length=30, unique=True,
help_text=_("Required. 30 characters or fewer. "
"Letters, numbers and @/./+/-/_ characters"))
token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
info = models.TextField(default="", null=True, blank=True)
class Meta:
unique_together = ("provider", "third_party_identifier")
def get_user_instance(self):
"""
Create a new AstakosUser instance based on details provided when user
initially signed up.
"""
d = copy.copy(self.__dict__)
d.pop('_state', None)
d.pop('id', None)
d.pop('token', None)
d.pop('created', None)
d.pop('info', None)
d.pop('affiliation', None)
d.pop('provider', None)
d.pop('third_party_identifier', None)
user = AstakosUser(**d)
return user
@property
def realname(self):
return '%s %s' % (self.first_name, self.last_name)
@realname.setter
def realname(self, value):
first, last = split_realname(value)
self.first_name = first
self.last_name = last
def save(self, *args, **kwargs):
if not self.id:
# set username
while not self.username:
username = uuid.uuid4().hex[:30]
try:
AstakosUser.objects.get(username=username)
except AstakosUser.DoesNotExist:
self.username = username
super(PendingThirdPartyUser, self).save(*args, **kwargs)
def generate_token(self):
self.password = self.third_party_identifier
self.last_login = datetime.now()
self.token = default_token_generator.make_token(self)
def existing_user(self):
return AstakosUser.objects.filter(
auth_providers__module=self.provider,
auth_providers__identifier=self.third_party_identifier)
def get_provider(self, user):
params = {
'info_data': self.info,
'affiliation': self.affiliation
}
return auth.get_provider(self.provider, user,
self.third_party_identifier, **params)
class SessionCatalog(models.Model):
session_key = models.CharField(_('session key'), max_length=40)
user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
class UserSetting(models.Model):
user = models.ForeignKey(AstakosUser)
setting = models.CharField(max_length=255)
value = models.IntegerField()
class Meta:
unique_together = ("user", "setting")
### PROJECTS ###
################
class Chain(models.Model):
chain = models.AutoField(primary_key=True)
def __str__(self):
return "%s" % (self.chain,)
def new_chain():
c = Chain.objects.create()
return c
class ProjectApplicationManager(models.Manager):
def pending_per_project(self, projects):
apps = self.filter(state=self.model.PENDING,
chain__in=projects).order_by('chain', '-id')
checked_chain = None
projs = {}
for app in apps:
chain = app.chain_id
if chain != checked_chain:
checked_chain = chain
projs[chain] = app
return projs
class ProjectApplication(models.Model):
applicant = models.ForeignKey(
AstakosUser,
related_name='projects_applied',
db_index=True)
PENDING = 0
APPROVED = 1
REPLACED = 2
DENIED = 3
DISMISSED = 4
CANCELLED = 5
state = models.IntegerField(default=PENDING,
db_index=True)
owner = models.ForeignKey(
AstakosUser,
related_name='projects_owned',
db_index=True)
chain = models.ForeignKey('Project',
related_name='chained_apps',
db_column='chain')
name = models.CharField(max_length=80)
homepage = models.URLField(max_length=255, null=True,
verify_exists=False)
description = models.TextField(null=True, blank=True)
start_date = models.DateTimeField(null=True, blank=True)
end_date = models.DateTimeField()
member_join_policy = models.IntegerField()
member_leave_policy = models.IntegerField()
limit_on_members_number = models.PositiveIntegerField(null=True)
resource_grants = models.ManyToManyField(
Resource,
null=True,
blank=True,
through='ProjectResourceGrant')
comments = models.TextField(null=True, blank=True)
issue_date = models.DateTimeField(auto_now_add=True)
response_date = models.DateTimeField(null=True, blank=True)
response = models.TextField(null=True, blank=True)
response_actor = models.ForeignKey(AstakosUser, null=True,
related_name='responded_apps')
waive_date = models.DateTimeField(null=True, blank=True)
waive_reason = models.TextField(null=True, blank=True)
waive_actor = models.ForeignKey(AstakosUser, null=True,
related_name='waived_apps')
objects = ProjectApplicationManager()
# Compiled queries
Q_PENDING = Q(state=PENDING)
Q_APPROVED = Q(state=APPROVED)
Q_DENIED = Q(state=DENIED)
class Meta:
unique_together = ("chain", "id")
def __unicode__(self):
return "%s applied by %s" % (self.name, self.applicant)
# TODO: Move to a more suitable place
APPLICATION_STATE_DISPLAY = {
PENDING: _('Pending review'),
APPROVED: _('Approved'),
REPLACED: _('Replaced'),
DENIED: _('Denied'),
DISMISSED: _('Dismissed'),
CANCELLED: _('Cancelled')
}
@property
def log_display(self):
return "application %s (%s) for project %s" % (
self.id, self.name, self.chain)
def state_display(self):
return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
@property
def grants(self):
return self.projectresourcegrant_set.values('member_capacity',
'resource__name')
@property
def resource_policies(self):
return [str(rp) for rp in self.projectresourcegrant_set.all()]
def is_modification(self):
# if self.state != self.PENDING:
# return False
parents = self.chained_applications().filter(id__lt=self.id)
parents = parents.filter(state__in=[self.APPROVED])
return parents.count() > 0
def chained_applications(self):
return ProjectApplication.objects.filter(chain=self.chain)
def denied_modifications(self):
q = self.chained_applications()
q = q.filter(Q(state=self.DENIED))
q = q.filter(~Q(id=self.id))
return q
def last_denied(self):
try:
return self.denied_modifications().order_by('-id')[0]
except IndexError:
return None
def has_denied_modifications(self):
return bool(self.last_denied())
def can_cancel(self):
return self.state == self.PENDING
def cancel(self, actor=None, reason=None):
if not self.can_cancel():
m = _("cannot cancel: application '%s' in state '%s'") % (
self.id, self.state)
raise AssertionError(m)
self.state = self.CANCELLED
self.waive_date = datetime.now()
self.waive_reason = reason
self.waive_actor = actor
self.save()
def can_dismiss(self):
return self.state == self.DENIED
def dismiss(self, actor=None, reason=None):
if not self.can_dismiss():
m = _("cannot dismiss: application '%s' in state '%s'") % (
self.id, self.state)
raise AssertionError(m)
self.state = self.DISMISSED
self.waive_date = datetime.now()
self.waive_reason = reason
self.waive_actor = actor
self.save()
def can_deny(self):
return self.state == self.PENDING
def deny(self, actor=None, reason=None):
if not self.can_deny():
m = _("cannot deny: application '%s' in state '%s'") % (
self.id, self.state)
raise AssertionError(m)
self.state = self.DENIED
self.response_date = datetime.now()
self.response = reason
self.response_actor = actor
self.save()
def can_approve(self):
return self.state == self.PENDING
def approve(self, actor=None, reason=None):
if not self.can_approve():
m = _("cannot approve: project '%s' in state '%s'") % (
self.name, self.state)
raise AssertionError(m) # invalid argument
now = datetime.now()
self.state = self.APPROVED
self.response_date = now
self.response = reason
self.response_actor = actor
self.save()
@property
def member_join_policy_display(self):
policy = self.member_join_policy
return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
@property
def member_leave_policy_display(self):
policy = self.member_leave_policy
return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
class ProjectResourceGrantManager(models.Manager):
def grants_per_app(self, applications):
app_ids = [app.id for app in applications]
grants = self.filter(
project_application__in=app_ids).select_related("resource")
return _partition_by(lambda g: g.project_application_id, grants)
class ProjectResourceGrant(models.Model):
resource = models.ForeignKey(Resource)
project_application = models.ForeignKey(ProjectApplication,
null=True)
project_capacity = models.BigIntegerField(null=True)
member_capacity = models.BigIntegerField(default=0)
objects = ProjectResourceGrantManager()
class Meta:
unique_together = ("resource", "project_application")
def display_member_capacity(self):
return units.show(self.member_capacity, self.resource.unit)
def __str__(self):
return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
self.display_member_capacity())
def _distinct(f, l):
d = {}
last = None
for x in l:
group = f(x)
if group == last:
continue
last = group
d[group] = x
return d
def invert_dict(d):
return dict((v, k) for k, v in d.iteritems())
class ProjectManager(models.Manager):
def all_with_pending(self, flt=None):
flt = Q() if flt is None else flt
projects = list(self.select_related(
'application', 'application__owner').filter(flt))
objs = ProjectApplication.objects.select_related('owner')
apps = objs.filter(state=ProjectApplication.PENDING,
chain__in=projects).order_by('chain', '-id')
app_d = _distinct(lambda app: app.chain_id, apps)
return [(project, app_d.get(project.pk)) for project in projects]
def expired_projects(self):
model = self.model
q = ((model.o_state_q(model.O_ACTIVE) |
model.o_state_q(model.O_SUSPENDED)) &
Q(application__end_date__lt=datetime.now()))
return self.filter(q)
def user_accessible_projects(self, user):
"""
Return projects accessible by specified user.
"""
model = self.model
if user.is_project_admin():
flt = Q()
else:
membs = user.projectmembership_set.associated()
memb_projects = membs.values_list("project", flat=True)
flt = (Q(application__owner=user) |
Q(application__applicant=user) |
Q(id__in=memb_projects))
relevant = model.o_states_q(model.RELEVANT_STATES)
return self.filter(flt, relevant).order_by(
'application__issue_date').select_related(
'application', 'application__owner', 'application__applicant')
def search_by_name(self, *search_strings):
q = Q()
for s in search_strings:
q = q | Q(name__icontains=s)
return self.filter(q)
class Project(models.Model):
id = models.BigIntegerField(db_column='id', primary_key=True)
application = models.OneToOneField(
ProjectApplication,
related_name='project')
members = models.ManyToManyField(
AstakosUser,
through='ProjectMembership')
creation_date = models.DateTimeField(auto_now_add=True)
name = models.CharField(
max_length=80,
null=True,
db_index=True,
unique=True)
NORMAL = 1
SUSPENDED = 10
TERMINATED = 100
DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
state = models.IntegerField(default=NORMAL,
db_index=True)
objects = ProjectManager()
def __str__(self):
return uenc(_("") %
(self.id, udec(self.application.name)))
__repr__ = __str__
def __unicode__(self):
return _("") % (self.id, self.application.name)
O_PENDING = 0
O_ACTIVE = 1
O_DENIED = 3
O_DISMISSED = 4
O_CANCELLED = 5
O_SUSPENDED = 10
O_TERMINATED = 100
O_STATE_DISPLAY = {
O_PENDING: _("Pending"),
O_ACTIVE: _("Active"),
O_DENIED: _("Denied"),
O_DISMISSED: _("Dismissed"),
O_CANCELLED: _("Cancelled"),
O_SUSPENDED: _("Suspended"),
O_TERMINATED: _("Terminated"),
}
OVERALL_STATE = {
(NORMAL, ProjectApplication.PENDING): O_PENDING,
(NORMAL, ProjectApplication.APPROVED): O_ACTIVE,
(NORMAL, ProjectApplication.DENIED): O_DENIED,
(NORMAL, ProjectApplication.DISMISSED): O_DISMISSED,
(NORMAL, ProjectApplication.CANCELLED): O_CANCELLED,
(SUSPENDED, ProjectApplication.APPROVED): O_SUSPENDED,
(TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
}
OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
@classmethod
def o_state_q(cls, o_state):
p_state, a_state = cls.OVERALL_STATE_INV[o_state]
return Q(state=p_state, application__state=a_state)
@classmethod
def o_states_q(cls, o_states):
return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
INITIALIZED_STATES = [O_ACTIVE,
O_SUSPENDED,
O_TERMINATED,
]
RELEVANT_STATES = [O_PENDING,
O_DENIED,
O_ACTIVE,
O_SUSPENDED,
O_TERMINATED,
]
SKIP_STATES = [O_DISMISSED,
O_CANCELLED,
O_TERMINATED,
]
@classmethod
def _overall_state(cls, project_state, app_state):
return cls.OVERALL_STATE.get((project_state, app_state), None)
def overall_state(self):
return self._overall_state(self.state, self.application.state)
def last_pending_application(self):
apps = self.chained_apps.filter(
state=ProjectApplication.PENDING).order_by('-id')
if apps:
return apps[0]
return None
def last_pending_modification(self):
last_pending = self.last_pending_application()
if last_pending == self.application:
return None
return last_pending
def state_display(self):
return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
def expiration_info(self):
return (str(self.id), self.name, self.state_display(),
str(self.application.end_date))
def last_deactivation(self):
objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
ls = objs.order_by("-date")
if not ls:
return None
return ls[0]
def is_deactivated(self, reason=None):
if reason is not None:
return self.state == reason
return self.state != self.NORMAL
def is_active(self):
return self.overall_state() == self.O_ACTIVE
def is_initialized(self):
return self.overall_state() in self.INITIALIZED_STATES
### Deactivation calls
def _log_create(self, from_state, to_state, actor=None, reason=None,
comments=None):
now = datetime.now()
self.log.create(from_state=from_state, to_state=to_state, date=now,
actor=actor, reason=reason, comments=comments)
def set_state(self, to_state, actor=None, reason=None, comments=None):
self._log_create(self.state, to_state, actor=actor, reason=reason,
comments=comments)
self.state = to_state
self.save()
def terminate(self, actor=None, reason=None):
self.set_state(self.TERMINATED, actor=actor, reason=reason)
self.name = None
self.save()
def suspend(self, actor=None, reason=None):
self.set_state(self.SUSPENDED, actor=actor, reason=reason)
def resume(self, actor=None, reason=None):
self.set_state(self.NORMAL, actor=actor, reason=reason)
if self.name is None:
self.name = self.application.name
self.save()
### Logical checks
@property
def is_alive(self):
return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
@property
def is_terminated(self):
return self.is_deactivated(self.TERMINATED)
@property
def is_suspended(self):
return self.is_deactivated(self.SUSPENDED)
def violates_members_limit(self, adding=0):
application = self.application
limit = application.limit_on_members_number
if limit is None:
return False
return (len(self.approved_members) + adding > limit)
### Other
def count_pending_memberships(self):
return self.projectmembership_set.requested().count()
def members_count(self):
return self.approved_memberships.count()
@property
def approved_memberships(self):
query = ProjectMembership.Q_ACCEPTED_STATES
return self.projectmembership_set.filter(query)
@property
def approved_members(self):
return [m.person for m in self.approved_memberships]
class ProjectLogManager(models.Manager):
def last_deactivations(self, projects):
logs = self.filter(
project__in=projects,
to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
return first_of_group(lambda l: l.project_id, logs)
class ProjectLog(models.Model):
project = models.ForeignKey(Project, related_name="log")
from_state = models.IntegerField(null=True)
to_state = models.IntegerField()
date = models.DateTimeField()
actor = models.ForeignKey(AstakosUser, null=True)
reason = models.TextField(null=True)
comments = models.TextField(null=True)
objects = ProjectLogManager()
class ProjectLock(models.Model):
pass
class ProjectMembershipManager(models.Manager):
def any_accepted(self):
q = self.model.Q_ACCEPTED_STATES
return self.filter(q)
def actually_accepted(self):
q = self.model.Q_ACTUALLY_ACCEPTED
return self.filter(q)
def requested(self):
return self.filter(state=ProjectMembership.REQUESTED)
def suspended(self):
return self.filter(state=ProjectMembership.USER_SUSPENDED)
def associated(self):
return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
def any_accepted_per_project(self, projects):
ms = self.any_accepted().filter(project__in=projects)
return _partition_by(lambda m: m.project_id, ms)
def requested_per_project(self, projects):
ms = self.requested().filter(project__in=projects)
return _partition_by(lambda m: m.project_id, ms)
def one_per_project(self):
ms = self.all().select_related(
'project', 'project__application',
'project__application__owner', 'project_application__applicant',
'person')
m_per_p = {}
for m in ms:
m_per_p[m.project_id] = m
return m_per_p
class ProjectMembership(models.Model):
person = models.ForeignKey(AstakosUser)
project = models.ForeignKey(Project)
REQUESTED = 0
ACCEPTED = 1
LEAVE_REQUESTED = 5
# User deactivation
USER_SUSPENDED = 10
REJECTED = 100
CANCELLED = 101
REMOVED = 200
ASSOCIATED_STATES = set([REQUESTED,
ACCEPTED,
LEAVE_REQUESTED,
USER_SUSPENDED,
])
ACCEPTED_STATES = set([ACCEPTED,
LEAVE_REQUESTED,
USER_SUSPENDED,
])
ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
state = models.IntegerField(default=REQUESTED,
db_index=True)
objects = ProjectMembershipManager()
# Compiled queries
Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
MEMBERSHIP_STATE_DISPLAY = {
REQUESTED: _('Requested'),
ACCEPTED: _('Accepted'),
LEAVE_REQUESTED: _('Leave Requested'),
USER_SUSPENDED: _('Suspended'),
REJECTED: _('Rejected'),
CANCELLED: _('Cancelled'),
REMOVED: _('Removed'),
}
USER_FRIENDLY_STATE_DISPLAY = {
REQUESTED: _('Join requested'),
ACCEPTED: _('Accepted member'),
LEAVE_REQUESTED: _('Requested to leave'),
USER_SUSPENDED: _('Suspended member'),
REJECTED: _('Request rejected'),
CANCELLED: _('Request cancelled'),
REMOVED: _('Removed member'),
}
def state_display(self):
return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
def user_friendly_state_display(self):
return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
class Meta:
unique_together = ("person", "project")
#index_together = [["project", "state"]]
def __str__(self):
return uenc(_("<'%s' membership in '%s'>") %
(self.person.username, self.project))
__repr__ = __str__
def latest_log(self):
logs = self.log.all()
logs_d = _partition_by(lambda l: l.to_state, logs)
for s, s_logs in logs_d.iteritems():
logs_d[s] = max(s_logs, key=(lambda l: l.date))
return logs_d
def _log_create(self, from_state, to_state, actor=None, reason=None,
comments=None):
now = datetime.now()
self.log.create(from_state=from_state, to_state=to_state, date=now,
actor=actor, reason=reason, comments=comments)
def set_state(self, to_state, actor=None, reason=None, comments=None):
self._log_create(self.state, to_state, actor=actor, reason=reason,
comments=comments)
self.state = to_state
self.save()
ACTION_CHECKS = {
"join": lambda m: m.state not in m.ASSOCIATED_STATES,
"accept": lambda m: m.state == m.REQUESTED,
"enroll": lambda m: m.state not in m.ACCEPTED_STATES,
"leave": lambda m: m.state in m.ACCEPTED_STATES,
"leave_request": lambda m: m.state in m.ACCEPTED_STATES,
"deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
"cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
"remove": lambda m: m.state in m.ACCEPTED_STATES,
"reject": lambda m: m.state == m.REQUESTED,
"cancel": lambda m: m.state == m.REQUESTED,
}
ACTION_STATES = {
"join": REQUESTED,
"accept": ACCEPTED,
"enroll": ACCEPTED,
"leave_request": LEAVE_REQUESTED,
"deny_leave": ACCEPTED,
"cancel_leave": ACCEPTED,
"remove": REMOVED,
"reject": REJECTED,
"cancel": CANCELLED,
}
def check_action(self, action):
try:
check = self.ACTION_CHECKS[action]
except KeyError:
raise ValueError("No check found for action '%s'" % action)
return check(self)
def perform_action(self, action, actor=None, reason=None):
if not self.check_action(action):
m = _("%s: attempted action '%s' in state '%s'") % (
self, action, self.state)
raise AssertionError(m)
try:
s = self.ACTION_STATES[action]
except KeyError:
raise ValueError("No such action '%s'" % action)
return self.set_state(s, actor=actor, reason=reason)
class ProjectMembershipLogManager(models.Manager):
def last_logs(self, memberships):
logs = self.filter(membership__in=memberships).order_by("-date")
logs = _partition_by(lambda l: l.membership_id, logs)
for memb_id, m_logs in logs.iteritems():
logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
return logs
class ProjectMembershipLog(models.Model):
membership = models.ForeignKey(ProjectMembership, related_name="log")
from_state = models.IntegerField(null=True)
to_state = models.IntegerField()
date = models.DateTimeField()
actor = models.ForeignKey(AstakosUser, null=True)
reason = models.TextField(null=True)
comments = models.TextField(null=True)
objects = ProjectMembershipLogManager()
### SIGNALS ###
################
def resource_post_save(sender, instance, created, **kwargs):
pass
post_save.connect(resource_post_save, sender=Resource)
def renew_token(sender, instance, **kwargs):
if not instance.auth_token:
instance.renew_token()
pre_save.connect(renew_token, sender=Component)