# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
- from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED, IM_STATIC_URL, \
- COOKIE_NAME, LOGIN_MESSAGES, SIGNUP_MESSAGES, PROFILE_MESSAGES, \
- GLOBAL_MESSAGES, PROFILE_EXTRA_LINKS
- from astakos.im.api.admin import get_menu
+ from astakos.im.settings import (
+ IM_MODULES, INVITATIONS_ENABLED, IM_STATIC_URL,
+ LOGIN_MESSAGES, SIGNUP_MESSAGES, PROFILE_MESSAGES,
+ GLOBAL_MESSAGES, PROFILE_EXTRA_LINKS
+ )
+ from astakos.im.api import get_menu
from astakos.im.util import get_query
+ from astakos.im.models import GroupKind
+from astakos.im.auth_providers import PROVIDERS as AUTH_PROVIDERS
- from django.conf import settings
- from django.core.urlresolvers import reverse
from django.utils import simplejson as json
+
def im_modules(request):
return {'im_modules': IM_MODULES}
+def auth_providers(request):
+ return {'auth_providers': filter(lambda p:p.module_enabled,
+ AUTH_PROVIDERS.itervalues())}
def next(request):
- return {'next' : get_query(request).get('next', '')}
+ return {'next': get_query(request).get('next', '')}
+
def code(request):
- return {'code' : request.GET.get('code', '')}
+ return {'code': request.GET.get('code', '')}
+
def invitations(request):
- return {'invitations_enabled' :INVITATIONS_ENABLED}
+ return {'invitations_enabled': INVITATIONS_ENABLED}
+
def media(request):
- return {'IM_STATIC_URL' : IM_STATIC_URL}
+ return {'IM_STATIC_URL': IM_STATIC_URL}
+
def custom_messages(request):
global GLOBAL_MESSAGES, SIGNUP_MESSAGES, LOGIN_MESSAGES, PROFILE_MESSAGES
from django.template import Context, loader
from django.utils.http import int_to_base36
from django.core.urlresolvers import reverse
- from django.utils.functional import lazy
from django.utils.safestring import mark_safe
- from django.contrib import messages
from django.utils.encoding import smart_str
+ from django.conf import settings
+from django.forms.models import fields_for_model
+from django.db import transaction
from astakos.im.models import (
- AstakosUser, Invitation, get_latest_terms,
- EmailChange, PendingThirdPartyUser
+ AstakosUser, EmailChange, AstakosGroup, Invitation, GroupKind,
+ Resource, PendingThirdPartyUser, get_latest_terms, RESOURCE_SEPARATOR
)
- from astakos.im.settings import (INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL,
- BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL,
- RECAPTCHA_ENABLED, LOGGING_LEVEL, PASSWORD_RESET_EMAIL_SUBJECT,
- NEWPASSWD_INVALIDATE_TOKEN, MODERATION_ENABLED
+ from astakos.im.settings import (
+ INVITATIONS_PER_LEVEL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY,
+ RECAPTCHA_ENABLED, DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
- PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN
++ PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN,
++ MODERATION_ENABLED
)
- from astakos.im import settings
from astakos.im.widgets import DummyWidget, RecaptchaWidget
from astakos.im.functions import send_change_email
# Overriding field label since we need to apply a link
# to the terms within the label
terms_link_html = '<a href="%s" target="_blank">%s</a>' \
- % (reverse('latest_terms'), _("the terms"))
+ % (reverse('latest_terms'), _("the terms"))
self.fields['has_signed_terms'].label = \
- mark_safe("I agree with %s" % terms_link_html)
+ mark_safe("I agree with %s" % terms_link_html)
def clean_email(self):
- email = self.cleaned_data['email']
+ email = self.cleaned_data['email'].lower()
if not email:
- raise forms.ValidationError(_("This field is required"))
+ raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
if reserved_email(email):
- raise forms.ValidationError(_("This email is already used"))
+ raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
return email
def clean_has_signed_terms(self):
rrf = self.cleaned_data['recaptcha_response_field']
check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
if not check.is_valid:
- raise forms.ValidationError(_('You have not entered the correct words'))
+ raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
+ def post_store_user(self, user, request):
+ """
+ Interface method for descendant backends to be able to do stuff within
+ the transaction enabled by store_user.
+ """
+ user.add_auth_provider('local', auth_backend='astakos')
+ user.set_password(self.cleaned_data['password1'])
+
def save(self, commit=True):
"""
Saves the email, first_name and last_name properties, after the normal
save behavior is complete.
"""
user = super(LocalUserCreationForm, self).save(commit=False)
+ user.renew_token()
if commit:
user.save()
- logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, [])
+ logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
return user
+
class InvitedLocalUserCreationForm(LocalUserCreationForm):
"""
Extends the LocalUserCreationForm: email is readonly.
for f in ro:
self.fields[f].widget.attrs['readonly'] = True
-
def save(self, commit=True):
user = super(InvitedLocalUserCreationForm, self).save(commit=False)
- level = user.invitation.inviter.level + 1
- user.level = level
- user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
+ user.update_invitations_level()
user.email_verified = True
if commit:
user.save()
self.request = kwargs.get('request', None)
if self.request:
kwargs.pop('request')
--
++
latest_terms = get_latest_terms()
if latest_terms:
self._meta.fields.append('has_signed_terms')
--
++
super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
--
++
if latest_terms:
self.fields.keyOrder.append('has_signed_terms')
--
++
if 'has_signed_terms' in self.fields:
# Overriding field label since we need to apply a link
# to the terms within the label
terms_link_html = '<a href="%s" target="_blank">%s</a>' \
- % (reverse('latest_terms'), _("the terms"))
+ % (reverse('latest_terms'), _("the terms"))
self.fields['has_signed_terms'].label = \
mark_safe("I agree with %s" % terms_link_html)
--
++
def clean_email(self):
- email = self.cleaned_data['email']
+ email = self.cleaned_data['email'].lower()
if not email:
- raise forms.ValidationError(_("This field is required"))
+ raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
return email
def clean_has_signed_terms(self):
has_signed_terms = self.cleaned_data['has_signed_terms']
if not has_signed_terms:
- raise forms.ValidationError(_('You have to agree with the terms'))
+ raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
return has_signed_terms
+ def post_store_user(self, user, request):
+ pending = PendingThirdPartyUser.objects.get(
+ token=request.POST.get('third_party_token'),
+ third_party_identifier= \
+ self.cleaned_data.get('third_party_identifier'))
+ return user.add_pending_auth_provider(pending)
+
+
def save(self, commit=True):
user = super(ThirdPartyUserCreationForm, self).save(commit=False)
user.set_unusable_password()
- user.provider = get_query(self.request).get('provider')
+ user.is_local = False
+ user.renew_token()
if commit:
user.save()
- logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, [])
+ logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
return user
+
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
"""
Extends the ThirdPartyUserCreationForm: email is readonly.
raise forms.ValidationError(_("This email is already used"))
super(ShibbolethUserCreationForm, self).clean_email()
return email
-
- def save(self, commit=True):
- user = super(ShibbolethUserCreationForm, self).save(commit=False)
- try:
- p = PendingThirdPartyUser.objects.get(
- provider=user.provider,
- third_party_identifier=user.third_party_identifier
- )
- except:
- pass
- else:
- p.delete()
- return user
- class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
+ class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
+ InvitedThirdPartyUserCreationForm):
pass
+
class LoginForm(AuthenticationForm):
username = forms.EmailField(label=_("Email"))
recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
rrf = self.cleaned_data['recaptcha_response_field']
check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
if not check.is_valid:
- raise forms.ValidationError(_('You have not entered the correct words'))
-
+ raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
-
++
def clean(self):
"""
Override default behavior in order to check user's activation later
def clean_email(self):
email = super(ExtendedPasswordResetForm, self).clean_email()
try:
- user = AstakosUser.objects.get(email=email, is_active=True)
+ user = AstakosUser.objects.get(email__iexact=email, is_active=True)
if not user.has_usable_password():
- raise forms.ValidationError(_("This account has not a usable password."))
+ raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
- except AstakosUser.DoesNotExist:
+
+ if not user.can_change_password():
+ raise forms.ValidationError(_('Password change for this account'
+ ' is not supported.'))
+
+ except AstakosUser.DoesNotExist, e:
- raise forms.ValidationError(_('That e-mail address doesn\'t have an'
- ' associated user account. Are you sure'
- ' you\'ve registered?'))
+ raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
return email
- def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
- use_https=False, token_generator=default_token_generator, request=None):
+ def save(
+ self, domain_override=None, email_template_name='registration/password_reset_email.html',
+ use_https=False, token_generator=default_token_generator, request=None):
"""
Generates a one-use only link for resetting password and sends to the user.
"""
pass
return super(ExtendedPasswordChangeForm, self).save(commit=commit)
+
+ class AstakosGroupCreationForm(forms.ModelForm):
+ kind = forms.ModelChoiceField(
+ queryset=GroupKind.objects.all(),
+ label="",
+ widget=forms.HiddenInput()
+ )
+ name = forms.URLField(widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}), help_text="Name should be in the form of dns",)
+ moderation_enabled = forms.BooleanField(
+ help_text="Check if you want to approve members participation manually",
+ required=False,
+ initial=True
+ )
+ max_participants = forms.IntegerField(
+ required=True, min_value=1
+ )
+
+ class Meta:
+ model = AstakosGroup
+
+ def __init__(self, *args, **kwargs):
+ #update QueryDict
+ args = list(args)
+ qd = args.pop(0).copy()
+ members_unlimited = qd.pop('members_unlimited', False)
+ members_uplimit = qd.pop('members_uplimit', None)
+ # max_participants = None if members_unlimited else members_uplimit
+ # qd['max_participants']= max_participants.pop(0) if max_participants else None
-
++
+ #substitue QueryDict
+ args.insert(0, qd)
-
++
+ super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
+ self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
+ 'issue_date', 'expiration_date',
+ 'moderation_enabled', 'max_participants']
+ def add_fields((k, v)):
+ k = k.partition('_proxy')[0]
+ self.fields[k] = forms.IntegerField(
+ required=False,
+ widget=forms.HiddenInput(),
+ min_value=1
+ )
+ map(add_fields,
+ ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
+ )
-
++
+ def add_fields((k, v)):
+ self.fields[k] = forms.BooleanField(
+ required=False,
+ #widget=forms.HiddenInput()
+ )
+ map(add_fields,
+ ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
+ )
-
++
+ def policies(self):
+ self.clean()
+ policies = []
+ append = policies.append
+ for name, uplimit in self.cleaned_data.iteritems():
-
++
+ subs = name.split('_uplimit')
+ if len(subs) == 2:
+ prefix, suffix = subs
+ s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
+ resource = Resource.objects.get(service__name=s, name=r)
-
++
+ # keep only resource limits for selected resource groups
+ if self.cleaned_data.get(
+ 'is_selected_%s' % resource.group, False
+ ):
+ append(dict(service=s, resource=r, uplimit=uplimit))
+ return policies
+
+ class AstakosGroupCreationSummaryForm(forms.ModelForm):
+ kind = forms.ModelChoiceField(
+ queryset=GroupKind.objects.all(),
+ label="",
+ widget=forms.HiddenInput()
+ )
+ name = forms.URLField()
+ moderation_enabled = forms.BooleanField(
+ help_text="Check if you want to approve members participation manually",
+ required=False,
+ initial=True
+ )
+ max_participants = forms.IntegerField(
+ required=False, min_value=1
+ )
+
+ class Meta:
+ model = AstakosGroup
+
+ def __init__(self, *args, **kwargs):
+ #update QueryDict
+ args = list(args)
+ qd = args.pop(0).copy()
+ members_unlimited = qd.pop('members_unlimited', False)
+ members_uplimit = qd.pop('members_uplimit', None)
+ # max_participants = None if members_unlimited else members_uplimit
+ # qd['max_participants']= max_participants.pop(0) if max_participants else None
-
++
+ #substitue QueryDict
+ args.insert(0, qd)
-
++
+ super(AstakosGroupCreationSummaryForm, self).__init__(*args, **kwargs)
+ self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
+ 'issue_date', 'expiration_date',
+ 'moderation_enabled', 'max_participants']
+ def add_fields((k, v)):
+ self.fields[k] = forms.IntegerField(
+ required=False,
+ widget=forms.TextInput(),
+ min_value=1
+ )
+ map(add_fields,
+ ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
+ )
-
++
+ def add_fields((k, v)):
+ self.fields[k] = forms.BooleanField(
+ required=False,
+ widget=forms.HiddenInput()
+ )
+ map(add_fields,
+ ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
+ )
+ for f in self.fields.values():
+ f.widget = forms.HiddenInput()
+
+ def clean(self):
+ super(AstakosGroupCreationSummaryForm, self).clean()
+ self.cleaned_data['policies'] = []
+ append = self.cleaned_data['policies'].append
+ #tbd = [f for f in self.fields if (f.startswith('is_selected_') and (not f.endswith('_proxy')))]
+ tbd = [f for f in self.fields if f.startswith('is_selected_')]
+ for name, uplimit in self.cleaned_data.iteritems():
+ subs = name.split('_uplimit')
+ if len(subs) == 2:
+ tbd.append(name)
+ prefix, suffix = subs
+ s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
+ resource = Resource.objects.get(service__name=s, name=r)
-
++
+ # keep only resource limits for selected resource groups
+ if self.cleaned_data.get(
+ 'is_selected_%s' % resource.group, False
+ ):
+ append(dict(service=s, resource=r, uplimit=uplimit))
+ for name in tbd:
+ self.cleaned_data.pop(name, None)
+ return self.cleaned_data
+
+ class AstakosGroupUpdateForm(forms.ModelForm):
+ class Meta:
+ model = AstakosGroup
+ fields = ( 'desc','homepage')
+
+
+ class AddGroupMembersForm(forms.Form):
+ q = forms.CharField(
+ max_length=800, widget=forms.Textarea, label=_('Add members'),
+ help_text=_(astakos_messages.ADD_GROUP_MEMBERS_Q_HELP),
+ required=True)
+
+ def clean(self):
+ q = self.cleaned_data.get('q') or ''
+ users = q.split(',')
+ users = list(u.strip() for u in users if u)
+ db_entries = AstakosUser.objects.filter(email__in=users)
+ unknown = list(set(users) - set(u.email for u in db_entries))
+ if unknown:
+ raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
+ self.valid_users = db_entries
+ return self.cleaned_data
+
+ def get_valid_users(self):
+ """Should be called after form cleaning"""
+ try:
+ return self.valid_users
+ except:
+ return ()
+
+
+ class AstakosGroupSearchForm(forms.Form):
+ q = forms.CharField(max_length=200, label='Search project')
+
+
+ class TimelineForm(forms.Form):
+ entity = forms.ModelChoiceField(
+ queryset=AstakosUser.objects.filter(is_active=True)
+ )
+ resource = forms.ModelChoiceField(
+ queryset=Resource.objects.all()
+ )
+ start_date = forms.DateTimeField()
+ end_date = forms.DateTimeField()
+ details = forms.BooleanField(required=False, label="Detailed Listing")
+ operation = forms.ChoiceField(
+ label='Charge Method',
+ choices=(('', '-------------'),
+ ('charge_usage', 'Charge Usage'),
+ ('charge_traffic', 'Charge Traffic'), )
+ )
+
+ def clean(self):
+ super(TimelineForm, self).clean()
+ d = self.cleaned_data
+ if 'resource' in d:
+ d['resource'] = str(d['resource'])
+ if 'start_date' in d:
+ d['start_date'] = d['start_date'].strftime(
+ "%Y-%m-%dT%H:%M:%S.%f")[:24]
+ if 'end_date' in d:
+ d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
+ if 'entity' in d:
+ d['entity'] = d['entity'].email
+ return d
+
+
+ class AstakosGroupSortForm(forms.Form):
+ sort_by = forms.ChoiceField(label='Sort by',
+ choices=(('groupname', 'Name'),
+ ('kindname', 'Type'),
+ ('issue_date', 'Issue Date'),
+ ('expiration_date',
+ 'Expiration Date'),
+ ('approved_members_num',
+ 'Participants'),
+ ('is_enabled', 'Status'),
+ ('moderation_enabled', 'Moderation'),
+ ('membership_status',
+ 'Enrollment Status')
+ ),
+ required=False)
+
+
+ class MembersSortForm(forms.Form):
+ sort_by = forms.ChoiceField(label='Sort by',
+ choices=(('person__email', 'User Id'),
+ ('person__first_name', 'Name'),
+ ('date_joined', 'Status')
+ ),
+ required=False)
+
+
+ class PickResourceForm(forms.Form):
+ resource = forms.ModelChoiceField(
+ queryset=Resource.objects.select_related().all()
+ )
+ resource.widget.attrs["onchange"] = "this.form.submit()"
+
+
class ExtendedSetPasswordForm(SetPasswordForm):
"""
Extends SetPasswordForm by enabling user
initial=True,
help_text='Unsetting this may result in security risk.'
)
--
++
def __init__(self, user, *args, **kwargs):
super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
self.user = AstakosUser.objects.get(id=self.user.id)
if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
self.user.renew_token()
- self.user.flush_sessions()
+ #self.user.flush_sessions()
+ if not self.user.has_auth_provider('local'):
+ self.user.add_auth_provider('local', auth_backend='astakos')
+
except BaseException, e:
logger.exception(e)
- pass
return super(ExtendedSetPasswordForm, self).save(commit=commit)
from time import asctime
from datetime import datetime, timedelta
from base64 import b64encode
+from urlparse import urlparse
+from urllib import quote
from random import randint
+ from collections import defaultdict
from django.db import models, IntegrityError
- from django.contrib.auth.models import User, UserManager, Group
+ from django.contrib.auth.models import User, UserManager, Group, Permission
from django.utils.translation import ugettext as _
- from django.core.exceptions import ValidationError
- from django.template.loader import render_to_string
- from django.core.mail import send_mail
from django.db import transaction
- from django.db.models.signals import post_save, pre_save, post_syncdb
+ from django.core.exceptions import ValidationError
+ from django.db.models.signals import (
+ pre_save, post_save, post_syncdb, post_delete
+ )
+ from django.contrib.contenttypes.models import ContentType
+
+ from django.dispatch import Signal
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.core.validators import email_re
- from astakos.im.settings import (
- DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
- AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME,
- EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
+ from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
+ AUTH_TOKEN_DURATION, BILLING_FIELDS,
+ EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
+ from astakos.im.endpoints.qh import (
+ register_users, send_quota, register_resources
)
+from astakos.im import auth_providers
+ from astakos.im.endpoints.aquarium.producer import report_user_event
+ from astakos.im.functions import send_invitation
+ from astakos.im.tasks import propagate_groupmembers_quota
- QUEUE_CLIENT_ID = 3 # Astakos.
+ import astakos.im.messages as astakos_messages
logger = logging.getLogger(__name__)
+ DEFAULT_CONTENT_TYPE = None
+ try:
+ content_type = ContentType.objects.get(app_label='im', model='astakosuser')
+ except:
+ content_type = DEFAULT_CONTENT_TYPE
+
+ RESOURCE_SEPARATOR = '.'
+
+ inf = float('inf')
+
+ class Service(models.Model):
+ name = models.CharField('Name', max_length=255, unique=True, db_index=True)
+ url = models.FilePathField()
+ icon = models.FilePathField(blank=True)
+ auth_token = models.CharField('Authentication Token', max_length=32,
+ null=True, blank=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):
+ md5 = hashlib.md5()
+ md5.update(self.name.encode('ascii', 'ignore'))
+ md5.update(self.url.encode('ascii', 'ignore'))
+ md5.update(asctime())
+
+ self.auth_token = b64encode(md5.digest())
+ self.auth_token_created = datetime.now()
+ self.auth_token_expires = self.auth_token_created + \
+ timedelta(hours=AUTH_TOKEN_DURATION)
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def resources(self):
+ return self.resource_set.all()
+
+ @resources.setter
+ def resources(self, resources):
+ for s in resources:
+ self.resource_set.create(**s)
-
++
+ def add_resource(self, service, resource, uplimit, update=True):
+ """Raises ObjectDoesNotExist, IntegrityError"""
+ resource = Resource.objects.get(service__name=service, name=resource)
+ if update:
+ AstakosUserQuota.objects.update_or_create(user=self,
+ resource=resource,
+ defaults={'uplimit': uplimit})
+ else:
+ q = self.astakosuserquota_set
+ q.create(resource=resource, uplimit=uplimit)
+
+
+ class ResourceMetadata(models.Model):
+ key = models.CharField('Name', max_length=255, unique=True, db_index=True)
+ value = models.CharField('Value', max_length=255)
+
+
+ class Resource(models.Model):
+ name = models.CharField('Name', max_length=255, unique=True, db_index=True)
+ meta = models.ManyToManyField(ResourceMetadata)
+ service = models.ForeignKey(Service)
+ desc = models.TextField('Description', null=True)
+ unit = models.CharField('Name', null=True, max_length=255)
+ group = models.CharField('Group', null=True, max_length=255)
+
+ def __str__(self):
+ return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
+
+
+ class GroupKind(models.Model):
+ name = models.CharField('Name', max_length=255, unique=True, db_index=True)
+
+ def __str__(self):
+ return self.name
+
+
+ class AstakosGroup(Group):
+ kind = models.ForeignKey(GroupKind)
+ homepage = models.URLField(
+ 'Homepage Url', max_length=255, null=True, blank=True)
+ desc = models.TextField('Description', null=True)
+ policy = models.ManyToManyField(
+ Resource,
+ null=True,
+ blank=True,
+ through='AstakosGroupQuota'
+ )
+ creation_date = models.DateTimeField(
+ 'Creation date',
+ default=datetime.now()
+ )
+ issue_date = models.DateTimeField('Issue date', null=True)
+ expiration_date = models.DateTimeField(
+ 'Expiration date',
+ null=True
+ )
+ moderation_enabled = models.BooleanField(
+ 'Moderated membership?',
+ default=True
+ )
+ approval_date = models.DateTimeField(
+ 'Activation date',
+ null=True,
+ blank=True
+ )
+ estimated_participants = models.PositiveIntegerField(
+ 'Estimated #members',
+ null=True,
+ blank=True,
+ )
+ max_participants = models.PositiveIntegerField(
+ 'Maximum numder of participants',
+ null=True,
+ blank=True
+ )
-
++
+ @property
+ def is_disabled(self):
+ if not self.approval_date:
+ return True
+ return False
+
+ @property
+ def is_enabled(self):
+ if self.is_disabled:
+ return False
+ if not self.issue_date:
+ return False
+ if not self.expiration_date:
+ return True
+ now = datetime.now()
+ if self.issue_date > now:
+ return False
+ if now >= self.expiration_date:
+ return False
+ return True
+
+ def enable(self):
+ if self.is_enabled:
+ return
+ self.approval_date = datetime.now()
+ self.save()
+ quota_disturbed.send(sender=self, users=self.approved_members)
+ propagate_groupmembers_quota.apply_async(
+ args=[self], eta=self.issue_date)
+ propagate_groupmembers_quota.apply_async(
+ args=[self], eta=self.expiration_date)
+
+ def disable(self):
+ if self.is_disabled:
+ return
+ self.approval_date = None
+ self.save()
+ quota_disturbed.send(sender=self, users=self.approved_members)
+
+ @transaction.commit_manually
+ def approve_member(self, person):
+ m, created = self.membership_set.get_or_create(person=person)
+ try:
+ m.approve()
+ except:
+ transaction.rollback()
+ raise
+ else:
+ transaction.commit()
+
+ # def disapprove_member(self, person):
+ # self.membership_set.remove(person=person)
+
+ @property
+ def members(self):
+ q = self.membership_set.select_related().all()
+ return [m.person for m in q]
-
++
+ @property
+ def approved_members(self):
+ q = self.membership_set.select_related().all()
+ return [m.person for m in q if m.is_approved]
+
+ @property
+ def quota(self):
+ d = defaultdict(int)
+ for q in self.astakosgroupquota_set.select_related().all():
+ d[q.resource] += q.uplimit or inf
+ return d
-
++
+ def add_policy(self, service, resource, uplimit, update=True):
+ """Raises ObjectDoesNotExist, IntegrityError"""
+ resource = Resource.objects.get(service__name=service, name=resource)
+ if update:
+ AstakosGroupQuota.objects.update_or_create(
+ group=self,
+ resource=resource,
+ defaults={'uplimit': uplimit}
+ )
+ else:
+ q = self.astakosgroupquota_set
+ q.create(resource=resource, uplimit=uplimit)
-
++
+ @property
+ def policies(self):
+ return self.astakosgroupquota_set.select_related().all()
+
+ @policies.setter
+ def policies(self, policies):
+ for p in policies:
+ service = p.get('service', None)
+ resource = p.get('resource', None)
+ uplimit = p.get('uplimit', 0)
+ update = p.get('update', True)
+ self.add_policy(service, resource, uplimit, update)
-
++
+ @property
+ def owners(self):
+ return self.owner.all()
+
+ @property
+ def owner_details(self):
+ return self.owner.select_related().all()
+
+ @owners.setter
+ def owners(self, l):
+ self.owner = l
+ map(self.approve_member, l)
+
+
+
+class AstakosUserManager(models.Manager):
+
+ 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)
+
-
class AstakosUser(User):
"""
Extends ``django.contrib.auth.models.User`` by defining additional fields.
email_verified = models.BooleanField('Email verified?', default=False)
has_credits = models.BooleanField('Has credits?', default=False)
- has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
- date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
-
- activation_sent = models.DateTimeField('Activation sent data', null=True, 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)
+
+ activation_sent = models.DateTimeField(
+ 'Activation sent data', null=True, blank=True)
+
+ policy = models.ManyToManyField(
+ Resource, null=True, through='AstakosUserQuota')
+
+ astakos_groups = models.ManyToManyField(
+ AstakosGroup, verbose_name=_('agroups'), blank=True,
+ help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
+ through='Membership')
+
__has_signed_terms = False
- __groupnames = []
+ disturbed_quota = models.BooleanField('Needs quotaholder syncing',
+ default=False, db_index=True)
+ objects = AstakosUserManager()
+ owner = models.ManyToManyField(
+ AstakosGroup, related_name='owner', null=True)
+
+ class Meta:
+ unique_together = ("provider", "third_party_identifier")
def __init__(self, *args, **kwargs):
super(AstakosUser, self).__init__(*args, **kwargs)
if not self.id:
# set username
while not self.username:
- username = uuid.uuid4().hex[:30]
+ username = self.email
try:
- AstakosUser.objects.get(username = username)
- except AstakosUser.DoesNotExist, e:
+ AstakosUser.objects.get(username=username)
+ except AstakosUser.DoesNotExist:
self.username = username
- if not self.provider:
- self.provider = 'local'
- self.email = self.email.lower()
+
- report_user_event(self)
self.validate_unique_email_isactive()
if self.is_active and self.activation_sent:
# reset the activation sent
self.activation_sent = None
+
super(AstakosUser, self).save(**kwargs)
-
- # set default group if does not exist
- groupname = 'default'
- if groupname not in self.__groupnames:
- try:
- group = Group.objects.get(name = groupname)
- self.groups.add(group)
- except Group.DoesNotExist, e:
- logger.exception(e)
--
++
def renew_token(self, flush_sessions=False, current_key=None):
md5 = hashlib.md5()
md5.update(settings.SECRET_KEY)
md5.update(self.username)
md5.update(self.realname.encode('ascii', 'ignore'))
md5.update(asctime())
--
++
self.auth_token = b64encode(md5.digest())
self.auth_token_created = datetime.now()
self.auth_token_expires = self.auth_token_created + \
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' % ','.join(keys)
return False
return True
- def store_disturbed_quota(self, set=True):
- self.disturbed_quota = set
- self.save()
+ def set_invitations_level(self):
+ """
+ Update user invitation level
+ """
+ level = self.invitation.inviter.level + 1
+ self.level = level
+ self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
+
+ def can_login_with_auth_provider(self, provider):
+ if not self.has_auth_provider(provider):
+ return False
+ else:
+ return auth_providers.get_provider(provider).is_available_for_login()
+
+ def can_add_provider(self, provider, **kwargs):
+ provider_settings = auth_providers.get_provider(provider)
+ if not provider_settings.is_available_for_login():
+ return False
+ if self.has_auth_provider(provider) and \
+ provider_settings.one_per_user:
+ return False
+ return True
+
+ def can_remove_auth_provider(self, provider):
+ if len(self.get_active_auth_providers()) <= 1:
+ return False
+ return True
+
+ def can_change_password(self):
+ return self.has_auth_provider('local', auth_backend='astakos')
+
+ def has_auth_provider(self, provider, **kwargs):
+ return bool(self.auth_providers.filter(module=provider,
+ **kwargs).count())
+
+ def add_auth_provider(self, provider, **kwargs):
+ self.auth_providers.create(module=provider, active=True, **kwargs)
+
+ def add_pending_auth_provider(self, pending):
+ """
+ Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
+ the current user.
+ """
+ if not isinstance(pending, PendingThirdPartyUser):
+ pending = PendingThirdPartyUser.objects.get(token=pending)
+
+ provider = self.add_auth_provider(pending.provider,
+ identifier=pending.third_party_identifier)
+
+ if email_re.match(pending.email) and pending.email != self.email:
+ self.additionalmail_set.get_or_create(email=pending.email)
+
+ pending.delete()
+ return provider
+
+ def remove_auth_provider(self, provider, **kwargs):
+ self.auth_providers.get(module=provider, **kwargs).delete()
+
+ # user urls
+ def get_resend_activation_url(self):
+ return reverse('send_activation', {'user_id': self.pk})
+
+ def get_activation_url(self, nxt=False):
+ url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
+ quote(self.auth_token))
+ if nxt:
+ url += "&next=%s" % quote(nxt)
+ return url
+
+ def get_password_reset_url(self, token_generator=default_token_generator):
+ return reverse('django.contrib.auth.views.password_reset_confirm',
+ kwargs={'uidb36':int_to_base36(self.id),
+ 'token':token_generator.make_token(self)})
+
+ def get_auth_providers(self):
+ return self.auth_providers.all()
+
+ def get_available_auth_providers(self):
+ """
+ Returns a list of providers available for user to connect to.
+ """
+ providers = []
+ for module, provider_settings in auth_providers.PROVIDERS.iteritems():
+ if self.can_add_provider(module):
+ providers.append(provider_settings(self))
+
+ return providers
+
+ def get_active_auth_providers(self):
+ providers = []
+ for provider in self.auth_providers.active():
+ if auth_providers.get_provider(provider.module).is_available_for_login():
+ providers.append(provider)
+ return providers
+
+
+class AstakosUserAuthProviderManager(models.Manager):
+
+ def active(self):
+ return self.filter(active=True)
+
+
+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')
+
+ objects = AstakosUserAuthProviderManager()
+
+ class Meta:
+ unique_together = (('identifier', 'module', 'user'), )
+
+ @property
+ def settings(self):
+ return auth_providers.get_provider(self.module)
+
+ @property
+ def details_display(self):
- print self.settings.details_tpl
+ return self.settings.details_tpl % self.__dict__
+
+ def can_remove(self):
+ return self.user.can_remove_auth_provider(self.module)
+
+ def delete(self, *args, **kwargs):
+ ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
+ self.user.set_unusable_password()
+ self.user.save()
+ return ret
+ class Membership(models.Model):
+ person = models.ForeignKey(AstakosUser)
+ group = models.ForeignKey(AstakosGroup)
+ date_requested = models.DateField(default=datetime.now(), blank=True)
+ date_joined = models.DateField(null=True, db_index=True, blank=True)
+
+ class Meta:
+ unique_together = ("person", "group")
+
+ def save(self, *args, **kwargs):
+ if not self.id:
+ if not self.group.moderation_enabled:
+ self.date_joined = datetime.now()
+ super(Membership, self).save(*args, **kwargs)
+
+ @property
+ def is_approved(self):
+ if self.date_joined:
+ return True
+ return False
+
+ def approve(self):
+ if self.is_approved:
+ return
+ if self.group.max_participants:
+ assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
+ 'Maximum participant number has been reached.'
+ self.date_joined = datetime.now()
+ self.save()
+ quota_disturbed.send(sender=self, users=(self.person,))
+
+ def disapprove(self):
+ self.delete()
+ quota_disturbed.send(sender=self, users=(self.person,))
+
+ class AstakosQuotaManager(models.Manager):
+ def _update_or_create(self, **kwargs):
+ assert kwargs, \
+ 'update_or_create() must be passed at least one keyword argument'
+ obj, created = self.get_or_create(**kwargs)
+ defaults = kwargs.pop('defaults', {})
+ if created:
+ return obj, True, False
+ else:
+ try:
+ params = dict(
+ [(k, v) for k, v in kwargs.items() if '__' not in k])
+ params.update(defaults)
+ for attr, val in params.items():
+ if hasattr(obj, attr):
+ setattr(obj, attr, val)
+ sid = transaction.savepoint()
+ obj.save(force_update=True)
+ transaction.savepoint_commit(sid)
+ return obj, False, True
+ except IntegrityError, e:
+ transaction.savepoint_rollback(sid)
+ try:
+ return self.get(**kwargs), False, False
+ except self.model.DoesNotExist:
+ raise e
+
+ update_or_create = _update_or_create
+
+ class AstakosGroupQuota(models.Model):
+ objects = AstakosQuotaManager()
+ limit = models.PositiveIntegerField('Limit', null=True) # obsolete field
+ uplimit = models.BigIntegerField('Up limit', null=True)
+ resource = models.ForeignKey(Resource)
+ group = models.ForeignKey(AstakosGroup, blank=True)
+
+ class Meta:
+ unique_together = ("resource", "group")
+
+ class AstakosUserQuota(models.Model):
+ objects = AstakosQuotaManager()
+ limit = models.PositiveIntegerField('Limit', null=True) # obsolete field
+ uplimit = models.BigIntegerField('Up limit', null=True)
+ resource = models.ForeignKey(Resource)
+ user = models.ForeignKey(AstakosUser)
+
+ class Meta:
+ unique_together = ("resource", "user")
+
+
class ApprovalTerms(models.Model):
"""
Model for approval terms
self.last_name = parts[1]
else:
self.last_name = parts[0]
--
++
def save(self, **kwargs):
if not self.id:
# set username
if not instance.id:
instance.renew_token()
+ post_syncdb.connect(fix_superusers)
+ post_save.connect(user_post_save, sender=User)
+ pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
+ post_save.connect(astakosuser_post_save, sender=AstakosUser)
+ post_save.connect(resource_post_save, sender=Resource)
+
+ quota_disturbed = Signal(providing_args=["users"])
+ quota_disturbed.connect(on_quota_disturbed)
+
+ post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
+ post_delete.connect(send_quota_disturbed, sender=Membership)
+ post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
+ post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
+ post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
+ post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
+
pre_save.connect(renew_token, sender=AstakosUser)
--pre_save.connect(renew_token, sender=Service)
++pre_save.connect(renew_token, sender=Service)
from django.contrib.auth.decorators import login_required
from astakos.im.util import prepare_response, get_query
- from astakos.im.views import requires_anonymous, signed_terms_required, \
- requires_auth_provider
- from astakos.im.models import AstakosUser, PendingThirdPartyUser
- from astakos.im.forms import LoginForm, ExtendedPasswordChangeForm, \
- ExtendedSetPasswordForm
+ from astakos.im.views import requires_anonymous, signed_terms_required
+ from astakos.im.models import PendingThirdPartyUser
+ from astakos.im.forms import LoginForm, ExtendedPasswordChangeForm
-from astakos.im.settings import RATELIMIT_RETRIES_ALLOWED
-from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION
-
+from astakos.im.settings import (RATELIMIT_RETRIES_ALLOWED,
+ ENABLE_LOCAL_ACCOUNT_MIGRATION)
+ import astakos.im.messages as astakos_messages
++from astakos.im.views import requires_auth_provider
+from astakos.im import settings
from ratelimit.decorators import ratelimit
- retries = RATELIMIT_RETRIES_ALLOWED-1
- rate = str(retries)+'/m'
+ retries = RATELIMIT_RETRIES_ALLOWED - 1
+ rate = str(retries) + '/m'
+
+@requires_auth_provider('local', login=True)
@require_http_methods(["GET", "POST"])
@csrf_exempt
@requires_anonymous
on_failure: the template name to render on login failure
"""
was_limited = getattr(request, 'limited', False)
- form = LoginForm(data=request.POST, was_limited=was_limited, request=request)
+ form = LoginForm(data=request.POST,
+ was_limited=was_limited,
+ request=request)
next = get_query(request).get('next', '')
- username = get_query(request).get('key')
-
+ third_party_token = get_query(request).get('key', False)
+
if not form.is_valid():
return render_to_response(
on_failure,
)
# get the user from the cash
user = form.user_cache
-
+
message = None
if not user:
- message = _('Cannot authenticate account')
+ message = _(astakos_messages.ACCOUNT_AUTHENTICATION_FAILED)
elif not user.is_active:
if not user.activation_sent:
- message = _('Your request is pending activation')
+ message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
else:
- send_activation_url = reverse('send_activation', kwargs={'user_id':user.id})
- message = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION) % locals()
- elif user.provider not in ('local', ''):
++ # TODO: USE astakos_messages
+ url = reverse('send_activation', kwargs={'user_id':user.id})
+ msg = _('You have not followed the activation link.')
+ if settings.MODERATION_ENABLED:
+ msg_extra = ' ' + _('Please contact support.')
+ else:
+ msg_extra = _('<a href="%s">Resend activation email?</a>') % url
+
+ message = msg + msg_extra
+ elif not user.can_login_with_auth_provider('local'):
- message = _(
- 'Local login is not the current authentication method for this account.'
- )
+ message = _(astakos_messages.NO_LOCAL_AUTH)
-
+
if message:
messages.error(request, message)
return render_to_response(on_failure,
- {'login_form':form},
+ {'login_form': form},
context_instance=RequestContext(request))
-
- # hook for switching account to use third party authentication
- if ENABLE_LOCAL_ACCOUNT_MIGRATION and username:
+
+ response = prepare_response(request, user, next)
+ if third_party_token:
+ # use requests to assign the account he just authenticated with with
+ # a third party provider account
++ # TODO: USE astakos_messages
try:
- new = PendingThirdPartyUser.objects.get(
- username=username)
- except:
- messages.error(
- request,
- _(astakos_messages.SWITCH_ACCOUNT_FAILURE)
- )
- return render_to_response(
- on_failure,
- {'login_form':form,
- 'next':next},
- context_instance=RequestContext(request)
- )
- else:
- user.provider = new.provider
- user.third_party_identifier = new.third_party_identifier
- user.save()
- new.delete()
- messages.success(
- request,
- _(astakos_messages.SWITCH_ACCOUNT_SUCCESS_WITH_PROVIDER) % user.__dict__
- )
- return prepare_response(request, user, next)
+ request.user.add_pending_auth_provider(third_party_token)
+ messages.success(request, _('Your new login method has been added'))
+ except PendingThirdPartyUser.DoesNotExist:
+ messages.error(request, _('Account method assignment failed'))
+
+ return response
@require_http_methods(["GET", "POST"])
@signed_terms_required
from django.contrib import messages
from django.template import RequestContext
from django.views.decorators.http import require_http_methods
- from django.db.models import Q
- from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
- from django.utils.http import urlencode
+ from django.core.exceptions import ImproperlyConfigured
+from django.shortcuts import get_object_or_404
+
+from urlparse import urlunsplit, urlsplit
- from astakos.im.util import prepare_response, get_context, get_invitation
+ from astakos.im.util import prepare_response, get_context
-from astakos.im.views import requires_anonymous, render_response
-from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION
+from astakos.im.views import requires_anonymous, render_response, \
+ requires_auth_provider
+from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
-
from astakos.im.models import AstakosUser, PendingThirdPartyUser
from astakos.im.forms import LoginForm
-from astakos.im.activation_backends import get_backend
+from astakos.im.activation_backends import get_backend, SimpleBackend
+from astakos.im import settings
+ import astakos.im.messages as astakos_messages
+
import logging
logger = logging.getLogger(__name__)
SHIB_SESSION_ID = "HTTP_SHIB_SESSION_ID"
SHIB_MAIL = "HTTP_SHIB_MAIL"
-
+@requires_auth_provider('local', login=True)
@require_http_methods(["GET", "POST"])
+ @requires_anonymous
def login(
request,
- login_template='im/login.html',
- signup_template='im/third_party_check_local.html',
+ template='im/third_party_check_local.html',
extra_context=None
):
extra_context = extra_context or {}
elif Tokens.SHIB_NAME in tokens and Tokens.SHIB_SURNAME in tokens:
realname = tokens[Tokens.SHIB_NAME] + ' ' + tokens[Tokens.SHIB_SURNAME]
else:
- raise KeyError(_('Missing provider user information'))
+ raise KeyError(_(astakos_messages.SHIBBOLETH_MISSING_NAME))
except KeyError, e:
- extra_context['login_form'] = LoginForm(request=request)
- messages.error(request, e)
- return render_response(
- login_template,
- context_instance=get_context(request, extra_context)
- )
-
+ # invalid shibboleth headers, redirect to login, display message
- messages.error(request, e)
++ messages.error(request, e.message)
+ return HttpResponseRedirect(reverse('login'))
+
affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '')
email = tokens.get(Tokens.SHIB_MAIL, '')
-
+
+ # an existing user accessed the view
+ if request.user.is_authenticated():
+ if request.user.has_auth_provider('shibboleth', identifier=eppn):
+ return HttpResponseRedirect(reverse('edit_profile'))
+
+ # automatically add eppn provider to user
+ user = request.user
+ user.add_provider('shibboleth', identifier=eppn)
+ return HttpResponseRedirect('edit_profile')
+
try:
- user = AstakosUser.objects.get(
- provider='shibboleth',
- third_party_identifier=eppn
+ # astakos user exists ?
+ user = AstakosUser.objects.get_auth_provider_user(
+ 'shibboleth',
+ identifier=eppn
)
if user.is_active:
+ # authenticate user
return prepare_response(request,
user,
request.GET.get('next'),
'renew' in request.GET)
elif not user.activation_sent:
- message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
+ message = _('Your request is pending activation')
++ #TODO: use astakos_messages
+ if not settings.MODERATION_ENABLED:
+ url = user.get_resend_activation_url()
+ msg_extra = _('<a href="%s">Resend activation email?</a>') % url
+ message = message + u' ' + msg_extra
+
messages.error(request, message)
+ return HttpResponseRedirect(reverse('login'))
+
else:
- urls = {}
- urls['send_activation_url'] = reverse(
- 'send_activation',
- kwargs={'user_id':user.id}
- )
- urls['signup_url'] = reverse(
- 'shibboleth_signup',
- args= [user.username]
- )
- message = _(astakos_messages.INACTIVE_ACCOUNT_CHANGE_EMAIL) % urls
++ #TODO: use astakos_messages
+ message = _(u'Account disabled. Please contact support')
messages.error(request, message)
- return render_response(login_template,
- login_form = LoginForm(request=request),
- context_instance=RequestContext(request))
+ return HttpResponseRedirect(reverse('login'))
+
except AstakosUser.DoesNotExist, e:
- # First time
- try:
- user, created = PendingThirdPartyUser.objects.get_or_create(
- third_party_identifier=eppn,
- provider='shibboleth',
- defaults=dict(
- realname=realname,
- affiliation=affiliation,
- email=email
- )
- )
- user.save()
- except BaseException, e:
- logger.exception(e)
- template = login_template
- extra_context['login_form'] = LoginForm(request=request)
- messages.error(request, _(astakos_messages.GENERIC_ERROR))
- else:
- if not ENABLE_LOCAL_ACCOUNT_MIGRATION:
- url = reverse(
- 'shibboleth_signup',
- args= [user.username]
- )
- return HttpResponseRedirect(url)
- else:
- template = signup_template
- extra_context['username'] = user.username
-
- extra_context['provider']='shibboleth'
++ #TODO: use astakos_messages
+ # eppn not stored in astakos models, create pending profile
+ user, created = PendingThirdPartyUser.objects.get_or_create(
+ third_party_identifier=eppn,
+ provider='shibboleth',
+ )
+ # update pending user
+ user.realname = realname
+ user.affiliation = affiliation
+ user.email = email
+ user.generate_token()
+ user.save()
+
+ extra_context['provider'] = 'shibboleth'
+ extra_context['token'] = user.token
+
return render_response(
template,
context_instance=get_context(request, extra_context)
@requires_anonymous
def signup(
request,
- username,
+ token,
backend=None,
on_creation_template='im/third_party_registration.html',
- extra_context=None
-):
+ extra_context=None):
+
extra_context = extra_context or {}
- if not username:
- return HttpResponseBadRequest(_(astakos_messages.MISSING_KEY_PARAMETER))
- try:
- pending = PendingThirdPartyUser.objects.get(username=username)
- except PendingThirdPartyUser.DoesNotExist:
- try:
- user = AstakosUser.objects.get(username=username)
- except AstakosUser.DoesNotExist:
- return HttpResponseBadRequest(_(astakos_messages.INVALID_KEY_PARAMETER))
- else:
- d = pending.__dict__
- d.pop('_state', None)
- d.pop('id', None)
- user = AstakosUser(**d)
+ if not token:
++ #TODO: use astakos_messages
+ return HttpResponseBadRequest(_('Missing key parameter.'))
+
+ pending = get_object_or_404(PendingThirdPartyUser, token=token)
+ d = pending.__dict__
+ d.pop('_state', None)
+ d.pop('id', None)
+ d.pop('token', None)
+ d.pop('created', None)
+ user = AstakosUser(**d)
+
try:
backend = backend or get_backend(request)
except ImproperlyConfigured, e:
<input type="submit" class="submit altcol" value="UPDATE" />
</div>
+ <div class="auth_methods">
+ <br /><br />
+ <div class="assigned">
+ <h4>Authentication methods</h4>
+ <p>You can login to your account using the following methods</p>
+ <ul class="auth_providers">
+ {% for provider in user_providers %}
+ <li>
+ <h2>
+ {{ provider.settings.title }}
+ <span class="actions" style="margin-left: 40px">
+ {% for name, url in provider.settings.extra_actions %}
+ <a href="{{ url }}" title="{{ name }}">{{ name }}</a>
+ {% endfor %}
+ {% if provider.can_remove %}
+ <a href="{% url remove_auth_provider provider.pk %}" title="disble">Remove</a>
+ {% endif %}
+ </span>
+ </h2>
+ <p>{{ provider.details_display }}</p>
+ <br />
+ </li>
+ {% empty %}
+ <li>No available authentication methods</li>
+ {% endfor %}
+ </ul>
+ </div>
+ <div class="notassigned">
+ <p>You can add the following authentication methods to your account </p>
+ <ul class="auth_providers">
+ {% for provider in user_available_providers %}
+ <li>
+ <h2><a href="{{ provider.add_url }}">{{ provider.title }}</a></h2>
+ <p>{{ provider.add_description }}</p>
+ <br />
+ </li>
+ {% empty %}
+ No available providers.
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+
</form>
+
+ <div class="two-cols-links">
+ <p><a href="{% url password_change %}">Change Password</a></p>
+ <p>
+ <a href="https://okeanos.grnet.gr/home/">Back to ~okeanos</a>
+ <a href="https://cyclades.okeanos.grnet.gr/ui/">Take me to cyclades</a>
+ <a href="https://pithos.okeanos.grnet.gr/ui/">Take me to pithos+</a>
+ </p>
+ </div>
{% endblock body %}
--- /dev/null
+# Copyright 2011 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 datetime
+
+from django.test import TestCase, Client
+from django.conf import settings
+from django.core import mail
+
+from astakos.im.target.shibboleth import Tokens as ShibbolethTokens
+from astakos.im.models import *
+from astakos.im import functions
+from astakos.im import settings as astakos_settings
+
+from urllib import quote
+
++from astakos.im import messages
++
+class ShibbolethClient(Client):
+ """
+ A shibboleth agnostic client.
+ """
+ VALID_TOKENS = filter(lambda x: not x.startswith("_"), dir(ShibbolethTokens))
+
+ def __init__(self, *args, **kwargs):
+ self.tokens = kwargs.pop('tokens', {})
+ super(ShibbolethClient, self).__init__(*args, **kwargs)
+
+ def set_tokens(self, **kwargs):
+ for key, value in kwargs.iteritems():
+ key = 'SHIB_%s' % key.upper()
+ if not key in self.VALID_TOKENS:
+ raise Exception('Invalid shibboleth token')
+
+ self.tokens[key] = value
+
+ def unset_tokens(self, *keys):
+ for key in keys:
+ key = 'SHIB_%s' % param.upper()
+ if key in self.tokens:
+ del self.tokens[key]
+
+ def reset_tokens(self):
+ self.tokens = {}
+
+ def get_http_token(self, key):
+ http_header = getattr(ShibbolethTokens, key)
+ return http_header
+
+ def request(self, **request):
+ """
+ Transform valid shibboleth tokens to http headers
+ """
+ for token, value in self.tokens.iteritems():
+ request[self.get_http_token(token)] = value
+
+ for param in request.keys():
+ key = 'SHIB_%s' % param.upper()
+ if key in self.VALID_TOKENS:
+ request[self.get_http_token(key)] = request[param]
+ del request[param]
+
+ return super(ShibbolethClient, self).request(**request)
+
+
+def get_local_user(username, **kwargs):
+ try:
+ return AstakosUser.objects.get(email=username)
+ except:
+ user_params = {
+ 'username': username,
+ 'email': username,
+ 'is_active': True,
+ 'activation_sent': datetime.now(),
+ 'email_verified': True,
+ 'provider': 'local'
+ }
+ user_params.update(kwargs)
+ user = AstakosUser(**user_params)
+ user.set_password(kwargs.get('password', 'password'))
+ user.save()
+ user.add_auth_provider('local', auth_backend='astakos')
+ if kwargs.get('is_active', True):
+ user.is_active = True
+ else:
+ user.is_active = False
+ user.save()
+ return user
+
+
+def get_mailbox(email):
+ mails = []
+ for sent_email in mail.outbox:
+ for recipient in sent_email.recipients():
+ if email in recipient:
+ mails.append(sent_email)
+ return mails
+
+
+class ShibbolethTests(TestCase):
+ """
+ Testing shibboleth authentication.
+ """
+
+ fixtures = ['groups']
+
+ def setUp(self):
+ self.client = ShibbolethClient()
+ settings.ASTAKOS_IM_MODULES = ['local', 'shibboleth']
+
+ def test_create_account(self):
+ client = ShibbolethClient()
+
+ # shibboleth views validation
+ # eepn required
+ r = client.get('/im/login/shibboleth?', follow=True)
- self.assertContains(r, 'Missing provider token')
++ self.assertContains(r, messages.SHIBBOLETH_MISSING_EPPN)
+ client.set_tokens(eppn="kpapeppn")
+ # shibboleth user info required
+ r = client.get('/im/login/shibboleth?', follow=True)
- self.assertContains(r, 'Missing provider user information')
++ self.assertContains(r, messages.SHIBBOLETH_MISSING_NAME)
+
+ # shibboleth logged us in
+ client.set_tokens(mail="kpap@grnet.gr", eppn="kpapeppn", cn="1", )
+ r = client.get('/im/login/shibboleth?')
+
+ # astakos asks if we want to add shibboleth
+ self.assertContains(r, "Already have an account?")
+
+ # a new pending user created
+ pending_user = PendingThirdPartyUser.objects.get(
+ third_party_identifier="kpapeppn")
+ self.assertEqual(PendingThirdPartyUser.objects.count(), 1)
+ token = pending_user.token
+ # from now on no shibboleth headers are sent to the server
+ client.reset_tokens()
+
+ # we choose to signup as a new user
+ r = client.get('/im/shibboleth/signup/%s' % pending_user.username)
+ self.assertEqual(r.status_code, 404)
+
+ r = client.get('/im/shibboleth/signup/%s' % token)
+ form = r.context['form']
+ post_data = {'email': 'kpap@grnet.gr',
+ 'third_party_identifier': pending_user.third_party_identifier,
+ 'first_name': 'Kostas',
+ 'third_party_token': token,
+ 'last_name': 'Mitroglou',
+ 'additional_email': 'kpap@grnet.gr',
+ 'provider': 'shibboleth'
+ }
+ r = client.post('/im/signup', post_data)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(AstakosUser.objects.count(), 1)
+ self.assertEqual(PendingThirdPartyUser.objects.count(), 0)
+ self.assertEqual(AstakosUserAuthProvider.objects.count(), 1)
+
+
+ client.set_tokens(mail="kpap@grnet.gr", eppn="kpapeppn", cn="1", )
+ r = client.get("/im/login/shibboleth?", follow=True)
+ self.assertContains(r, "Your request is pending activation")
+ r = client.get("/im/profile", follow=True)
+ self.assertRedirects(r, 'http://testserver/im/?next=%2Fim%2Fprofile')
+
+ u = AstakosUser.objects.get()
+ functions.activate(u)
+ self.assertEqual(u.is_active, True)
+
+ r = client.get("/im/login/shibboleth?")
+ self.assertRedirects(r, '/im/profile')
+
+ def test_existing(self):
+ existing_user = get_local_user('kpap@grnet.gr')
+
+ client = ShibbolethClient()
+ # shibboleth logged us in, notice that we use different email
+ client.set_tokens(mail="kpap@shibboleth.gr", eppn="kpapeppn", cn="1", )
+ r = client.get("/im/login/shibboleth?")
+ # astakos asks if we want to switch a local account to shibboleth
+ self.assertContains(r, "Already have an account?")
+
+ # a new pending user created
+ pending_user = PendingThirdPartyUser.objects.get()
+ self.assertEqual(PendingThirdPartyUser.objects.count(), 1)
+ pending_key = pending_user.token
+ client.reset_tokens()
+
+ # we choose to add shibboleth to an our existing account
+ # we get redirected to login page with the pending token set
+ r = client.get('/im/login?key=%s' % pending_key)
+ post_data = {'password': 'password',
+ 'username': 'kpap@grnet.gr',
+ 'key': pending_key}
+ r = client.post('/im/local', post_data, follow=True)
+ self.assertContains(r, "Your new login method has been added")
+
+ user = AstakosUser.objects.get(username="kpap@grnet.gr",
+ email="kpap@grnet.gr")
+ self.assertTrue(user.has_auth_provider('shibboleth'))
+ self.assertTrue(user.has_auth_provider('local', auth_backend='astakos'))
+ client.logout()
+
+ # again ???? show her a message
+ r = client.get('/im/login?key=%s' % pending_key)
+ post_data = {'password': 'password',
+ 'username': 'kpap@grnet.gr',
+ 'key': pending_key}
+ r = self.client.post('/im/local', post_data, follow=True)
+ self.assertContains(r, "Account method assignment failed")
+ self.client.logout()
+ client.logout()
+
+ # look Ma, i can login with both my shibboleth and local account
+ client.set_tokens(mail="kpap@shibboleth.gr", eppn="kpapeppn", cn="1")
+ r = client.get("/im/login/shibboleth?", follow=True)
+ self.assertTrue(r.context['request'].user.is_authenticated())
+ self.assertTrue(r.context['request'].user.email == "kpap@grnet.gr")
+ r = client.get("/im/profile")
+ self.assertEquals(r.status_code,200)
+ client.logout()
+ client.reset_tokens()
+ r = client.get("/im/profile", follow=True)
+ self.assertFalse(r.context['request'].user.is_authenticated())
+
+ post_data = {'password': 'password',
+ 'username': 'kpap@grnet.gr'}
+ r = self.client.post('/im/local', post_data, follow=True)
+ self.assertTrue(r.context['request'].user.is_authenticated())
+ r = self.client.get("/im/profile")
+ self.assertEquals(r.status_code,200)
+
+ r = client.post('/im/local', post_data, follow=True)
+ client.set_tokens(mail="secondary@shibboleth.gr", eppn="kpapeppn", cn="1", )
+ r = client.get("/im/login/shibboleth?", follow=True)
+ client.reset_tokens()
+
+ client.logout()
+ client.set_tokens(mail="kpap@grnet.gr", eppn="kpapeppninvalid", cn="1")
+ r = client.get("/im/login/shibboleth?", follow=True)
+ self.assertFalse(r.context['request'].user.is_authenticated())
+
++ user2 = get_local_user('kpap@grnet.gr')
++
+
+class LocalUserTests(TestCase):
+
+ fixtures = ['groups']
+
++ def setUp(self):
++ from django.conf import settings
++ settings.ADMINS = (('admin', 'support@cloud.grnet.gr'),)
++ settings.SERVER_EMAIL = 'no-reply@grnet.gr'
++
+ def test_invitations(self):
+ return
+
+ def test_local_provider(self):
+ r = self.client.get("/im/signup")
+ self.assertEqual(r.status_code, 200)
+
+ data = {'email':'kpap@grnet.gr', 'password1':'password',
+ 'password2':'password', 'first_name': 'Kostas',
+ 'last_name': 'Mitroglou', 'provider': 'local'}
+ r = self.client.post("/im/signup", data)
+ self.assertEqual(AstakosUser.objects.count(), 1)
+ user = AstakosUser.objects.get(username="kpap@grnet.gr",
+ email="kpap@grnet.gr")
+ self.assertEqual(user.username, 'kpap@grnet.gr')
+ self.assertEqual(user.has_auth_provider('local'), True)
+ self.assertFalse(user.is_active)
+
+ # admin gets notified
+ self.assertEqual(len(get_mailbox('support@cloud.grnet.gr')), 1)
+ # and sends user activation email
+ functions.send_activation(user)
+
+ # user activation fields updated
+ user = AstakosUser.objects.get(pk=user.pk)
+ self.assertTrue(user.activation_sent)
+ self.assertFalse(user.email_verified)
+ # email sent to user
+ self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 1)
+
+ # user forgot she got registered and tries to submit registration
+ # form. Notice the upper case in email
+ data = {'email':'KPAP@grnet.gr', 'password1':'password',
+ 'password2':'password', 'first_name': 'Kostas',
+ 'last_name': 'Mitroglou', 'provider': 'local'}
+ r = self.client.post("/im/signup", data)
- self.assertContains(r, "This email is already used")
++ self.assertContains(r, messages.EMAIL_USED)
+
+ # hmmm, email exists; lets get the password
+ r = self.client.get('/im/local/password_reset')
+ self.assertEqual(r.status_code, 200)
+ r = self.client.post('/im/local/password_reset', {'email':
+ 'kpap@grnet.gr'})
+ # she can't because account is not active yet
+ self.assertContains(r, "doesn't have an associated user account")
+
+ # moderation is enabled so no automatic activation can be send
+ r = self.client.get('/im/send/activation/%d' % user.pk)
+ self.assertEqual(r.status_code, 403)
+ self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 1)
+ # also she cannot login
+ r = self.client.post('/im/local', {'username': 'kpap@grnet.gr',
+ 'password': 'password'})
+ self.assertContains(r, 'You have not followed the activation link')
+ self.assertNotContains(r, 'Resend activation')
+ self.assertFalse(r.context['request'].user.is_authenticated())
+ self.assertFalse('_pithos2_a' in self.client.cookies)
+
+ # lets disable moderation
+ astakos_settings.MODERATION_ENABLED = False
+ r = self.client.post('/im/local/password_reset', {'email':
+ 'kpap@grnet.gr'})
+ self.assertContains(r, "doesn't have an associated user account")
+ r = self.client.post('/im/local', {'username': 'kpap@grnet.gr',
+ 'password': 'password'})
+ self.assertContains(r, 'You have not followed the activation link')
+ self.assertContains(r, 'Resend activation')
+ self.assertFalse(r.context['request'].user.is_authenticated())
+ self.assertFalse('_pithos2_a' in self.client.cookies)
+ # user sees the message and resends activation
+ r = self.client.get('/im/send/activation/%d' % user.pk)
+ # email sent
+ self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 2)
+
+ # switch back moderation setting
+ astakos_settings.MODERATION_ENABLED = True
+ # lets activate the user
+ r = self.client.get(user.get_activation_url(), follow=True)
+ self.assertRedirects(r, "/im/profile")
+ self.assertContains(r, "kpap@grnet.gr")
+ self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 3)
+
+ user = AstakosUser.objects.get(pk=user.pk)
+ # user activated and logged in, token cookie set
+ self.assertTrue(r.context['request'].user.is_authenticated())
+ self.assertTrue('_pithos2_a' in self.client.cookies)
+ cookies = self.client.cookies
+ self.assertTrue(quote(user.auth_token) in cookies.get('_pithos2_a').value)
+ r = self.client.get('/im/logout', follow=True)
+ r = self.client.get('/im/')
+ # user logged out, token cookie removed
+ self.assertFalse(r.context['request'].user.is_authenticated())
+ self.assertFalse(self.client.cookies.get('_pithos2_a').value)
+ # https://docs.djangoproject.com/en/dev/topics/testing/#persistent-state
+ del self.client.cookies['_pithos2_a']
+
+ # user can login
+ r = self.client.post('/im/local', {'username': 'kpap@grnet.gr',
+ 'password': 'password'},
+ follow=True)
+ self.assertTrue(r.context['request'].user.is_authenticated())
+ self.assertTrue('_pithos2_a' in self.client.cookies)
+ cookies = self.client.cookies
+ self.assertTrue(quote(user.auth_token) in cookies.get('_pithos2_a').value)
+ self.client.get('/im/logout', follow=True)
+
+ # user forgot password
+ old_pass = user.password
+ r = self.client.get('/im/local/password_reset')
+ self.assertEqual(r.status_code, 200)
+ r = self.client.post('/im/local/password_reset', {'email':
+ 'kpap@grnet.gr'})
+ self.assertEqual(r.status_code, 302)
+ # email sent
+ self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 4)
+
+ # user visits change password link
+ r = self.client.get(user.get_password_reset_url())
+ r = self.client.post(user.get_password_reset_url(),
+ {'new_password1':'newpass',
+ 'new_password2':'newpass'})
+
+ user = AstakosUser.objects.get(pk=user.pk)
+ self.assertNotEqual(old_pass, user.password)
+
+ # old pass is not usable
+ r = self.client.post('/im/local', {'username': 'kpap@grnet.gr',
+ 'password': 'password'})
+ self.assertContains(r, 'Please enter a correct username and password')
+ r = self.client.post('/im/local', {'username': 'kpap@grnet.gr',
+ 'password': 'newpass'},
+ follow=True)
+ self.assertTrue(r.context['request'].user.is_authenticated())
+ self.client.logout()
+
+ # tests of special local backends
+ user = AstakosUser.objects.get(pk=user.pk)
+ user.auth_providers.filter(module='local').update(auth_backend='ldap')
+ user.save()
+
+ # non astakos local backends do not support password reset
+ r = self.client.get('/im/local/password_reset')
+ self.assertEqual(r.status_code, 200)
+ r = self.client.post('/im/local/password_reset', {'email':
+ 'kpap@grnet.gr'})
+ # she can't because account is not active yet
+ self.assertContains(r, "Password change for this account is not"
+ " supported")
+
urlpatterns = patterns('astakos.im.views',
url(r'^$', 'index', {}, name='index'),
url(r'^login/?$', 'index', {}, name='login'),
- url(r'^profile/?$', 'edit_profile', name='edit_profile'),
- url(r'^feedback/?$', 'feedback'),
- url(r'^signup/?$', 'signup', {'on_success':'im/login.html', 'extra_context':{'login_form':LoginForm()}}),
- url(r'^logout/?$', 'logout', {'template':'im/login.html', 'extra_context':{'login_form':LoginForm()}}),
- url(r'^activate/?$', 'activate'),
+ url(r'^profile/?$','edit_profile', {}, name='edit_profile'),
+ url(r'^feedback/?$', 'feedback', {}, name='feedback'),
+ url(r'^signup/?$', 'signup', {'on_success': 'im/login.html', 'extra_context': {'login_form': LoginForm()}}, name='signup'),
+ url(r'^logout/?$', 'logout', {'template': 'im/login.html', 'extra_context': {'login_form': LoginForm()}}, name='logout'),
+ url(r'^activate/?$', 'activate', {}, name='activate'),
url(r'^approval_terms/?$', 'approval_terms', {}, name='latest_terms'),
url(r'^approval_terms/(?P<term_id>\d+)/?$', 'approval_terms'),
- url(r'^send/activation/(?P<user_id>\d+)/?$', 'send_activation', {},
- name='send_activation'),
+ url(r'^send/activation/(?P<user_id>\d+)/?$', 'send_activation', {}, name='send_activation'),
+ url(r'^resources/?$', 'resource_list', {}, name='resource_list'),
+ url(r'^billing/?$', 'billing', {}, name='billing'),
+ url(r'^timeline/?$', 'timeline', {}, name='timeline'),
+ url(r'^group/add/complete/?$', 'group_add_complete', {}, name='group_add_complete'),
+ url(r'^group/add/(?P<kind_name>\w+)?$', 'group_add', {}, name='group_add'),
+ url(r'^group/list/?$', 'group_list', {}, name='group_list'),
+ url(r'^group/(?P<group_id>\d+)/?$', 'group_detail', {}, name='group_detail'),
+ url(r'^group/search/?$', 'group_search', {}, name='group_search'),
+ url(r'^group/all/?$', 'group_all', {}, name='group_all'),
+ url(r'^group/(?P<group_id>\d+)/join/?$', 'group_join', {}, name='group_join'),
+ url(r'^group/(?P<group_id>\d+)/leave/?$', 'group_leave', {}, name='group_leave'),
+ url(r'^group/(?P<group_id>\d+)/(?P<user_id>\d+)/approve/?$', 'approve_member', {}, name='approve_member'),
+ url(r'^group/(?P<group_id>\d+)/(?P<user_id>\d+)/disapprove/?$', 'disapprove_member', {}, name='disapprove_member'),
- url(r'^group/create/?$', 'group_create_list', {}, name='group_create_list')
++ url(r'^group/create/?$', 'group_create_list', {}, name='group_create_list'),
+ url(r'^remove_auth_provider/(?P<pk>\d+)?$', 'remove_auth_provider', {},
+ name='remove_auth_provider')
)
+
if EMAILCHANGE_ENABLED:
urlpatterns += patterns('astakos.im.views',
url(r'^email_change/?$', 'change_email', {}, name='email_change'),
# or implied, of GRNET S.A.
import logging
- import socket
+ import calendar
+ import inflect
+
+ engine = inflect.engine()
- from smtplib import SMTPException
from urllib import quote
from functools import wraps
+ from datetime import datetime
- from django.core.mail import send_mail
- from django.http import (
- HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
- )
- from django.shortcuts import redirect
- from django.template.loader import render_to_string
- from django.utils.translation import ugettext as _
- from django.core.urlresolvers import reverse
- from django.contrib.auth.decorators import login_required
from django.contrib import messages
+ from django.contrib.auth.decorators import login_required
+ from django.core.urlresolvers import reverse
from django.db import transaction
- from django.utils.http import urlencode
from django.db.utils import IntegrityError
- from django.contrib.auth.views import password_change
+ from django.http import (HttpResponse, HttpResponseBadRequest,
+ HttpResponseForbidden, HttpResponseRedirect,
+ HttpResponseBadRequest, Http404)
+ from django.shortcuts import redirect
+ from django.template import RequestContext, loader as template_loader
+ from django.utils.http import urlencode
+ from django.utils.translation import ugettext as _
+ from django.views.generic.create_update import (delete_object,
+ get_model_and_form_class)
+ from django.views.generic.list_detail import object_list
+ from django.core.xheaders import populate_xheaders
+from django.core.exceptions import ValidationError, PermissionDenied
- from django.views.decorators.http import require_http_methods
- from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
+ from django.template.loader import render_to_string
+ from django.views.decorators.http import require_http_methods
from astakos.im.activation_backends import get_backend, SimpleBackend
- from astakos.im.util import (
- get_context, prepare_response, get_query, restrict_next
- )
- from astakos.im.forms import *
- from astakos.im.functions import (send_greeting, send_feedback, SendMailError,
- invite as invite_func, logout as auth_logout, activate as activate_func,
- send_activation as send_activation_func
- )
- from astakos.im.settings import (
- DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_DOMAIN, IM_MODULES,
- SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
- )
+
+ from astakos.im.models import (AstakosUser, ApprovalTerms, AstakosGroup,
+ EmailChange, GroupKind, Membership,
+ RESOURCE_SEPARATOR)
+ from astakos.im.util import get_context, prepare_response, get_query, restrict_next
+ from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
+ FeedbackForm, SignApprovalTermsForm,
+ EmailChangeForm,
+ AstakosGroupCreationForm, AstakosGroupSearchForm,
+ AstakosGroupUpdateForm, AddGroupMembersForm,
+ MembersSortForm,
+ TimelineForm, PickResourceForm,
+ AstakosGroupCreationSummaryForm)
+ from astakos.im.functions import (send_feedback, SendMailError,
+ logout as auth_logout,
+ activate as activate_func,
+ send_activation as send_activation_func,
+ send_group_creation_notification,
+ SendNotificationError)
+ from astakos.im.endpoints.qh import timeline_charge
+ from astakos.im.settings import (COOKIE_DOMAIN, LOGOUT_NEXT,
+ LOGGING_LEVEL, PAGINATE_BY, RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL)
+ from astakos.im.tasks import request_billing
+ from astakos.im.api.callpoint import AstakosCallpoint
+
+ import astakos.im.messages as astakos_messages
+from astakos.im import settings
+from astakos.im import auth_providers
logger = logging.getLogger(__name__)
template_name = login_template_name
if request.user.is_authenticated():
return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
--
++
return render_response(
template_name,
login_form = LoginForm(request=request),
)
if next:
return redirect(next)
- msg = _('<p>Profile has been updated successfully</p>')
- messages.add_message(request, messages.SUCCESS, msg)
+ msg = _(astakos_messages.PROFILE_UPDATED)
+ messages.success(request, msg)
except ValueError, ve:
- messages.add_message(request, messages.ERROR, ve)
+ messages.success(request, ve)
elif request.method == "GET":
- if not request.user.is_verified:
- request.user.is_verified = True
- request.user.save()
+ request.user.is_verified = True
+ request.user.save()
+
+ # existing providers
+ user_providers = request.user.get_active_auth_providers()
+
+ # providers that user can add
+ user_available_providers = request.user.get_available_auth_providers()
+
return render_response(template_name,
profile_form = form,
+ user_providers = user_providers,
+ user_available_providers = user_available_providers,
context_instance = get_context(request,
extra_context))
"""
extra_context = extra_context or {}
if request.user.is_authenticated():
- return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
-
+ return HttpResponseRedirect(reverse('edit_profile'))
+
provider = get_query(request).get('provider', 'local')
+ if not auth_providers.get_provider(provider).is_available_for_create():
+ raise PermissionDenied
+
id = get_query(request).get('id')
try:
instance = AstakosUser.objects.get(id=id) if id else None
)
)
+ class ResourcePresentation():
-
++
+ def __init__(self, data):
+ self.data = data
-
++
+ def update_from_result(self, result):
+ if result.is_success:
+ for r in result.data:
+ rname = '%s%s%s' % (r.get('service'), RESOURCE_SEPARATOR, r.get('name'))
+ if not rname in self.data['resources']:
+ self.data['resources'][rname] = {}
-
++
+ self.data['resources'][rname].update(r)
+ self.data['resources'][rname]['id'] = rname
+ group = r.get('group')
+ if not group in self.data['groups']:
+ self.data['groups'][group] = {}
-
++
+ self.data['groups'][r.get('group')].update({'name': r.get('group')})
-
++
+ def test(self, quota_dict):
+ for k, v in quota_dict.iteritems():
+ rname = k
+ value = v
+ if not rname in self.data['resources']:
+ self.data['resources'][rname] = {}
-
-
++
++
+ self.data['resources'][rname]['value'] = value
-
-
++
++
+ def update_from_result_report(self, result):
+ if result.is_success:
+ for r in result.data:
+ rname = r.get('name')
+ if not rname in self.data['resources']:
+ self.data['resources'][rname] = {}
-
++
+ self.data['resources'][rname].update(r)
+ self.data['resources'][rname]['id'] = rname
+ group = r.get('group')
+ if not group in self.data['groups']:
+ self.data['groups'][group] = {}
-
++
+ self.data['groups'][r.get('group')].update({'name': r.get('group')})
-
++
+ def get_group_resources(self, group):
+ return dict(filter(lambda t: t[1].get('group') == group, self.data['resources'].iteritems()))
-
++
+ def get_groups_resources(self):
+ for g in self.data['groups']:
+ yield g, self.get_group_resources(g)
-
++
+ def get_quota(self, group_quotas):
+ for r, v in group_quotas.iteritems():
+ rname = str(r)
+ quota = self.data['resources'].get(rname)
+ quota['value'] = v
+ yield quota
-
-
++
++
+ def get_policies(self, policies_data):
+ for policy in policies_data:
+ rname = '%s%s%s' % (policy.get('service'), RESOURCE_SEPARATOR, policy.get('resource'))
+ policy.update(self.data['resources'].get(rname))
+ yield policy
-
++
+ def __repr__(self):
+ return self.data.__repr__()
-
++
+ def __iter__(self, *args, **kwargs):
+ return self.data.__iter__(*args, **kwargs)
-
++
+ def __getitem__(self, *args, **kwargs):
+ return self.data.__getitem__(*args, **kwargs)
-
++
+ def get(self, *args, **kwargs):
+ return self.data.get(*args, **kwargs)
-
-
++
++
+
+ @require_http_methods(["GET", "POST"])
+ @signed_terms_required
+ @login_required
+ def group_add(request, kind_name='default'):
-
++
+ result = callpoint.list_resources()
+ resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
+ resource_catalog.update_from_result(result)
-
++
+ if not result.is_success:
+ messages.error(
+ request,
+ 'Unable to retrieve system resources: %s' % result.reason
+ )
-
++
+ try:
+ kind = GroupKind.objects.get(name=kind_name)
+ except:
+ return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
-
-
++
++
+
+ post_save_redirect = '/im/group/%(id)s/'
+ context_processors = None
+ model, form_class = get_model_and_form_class(
+ model=None,
+ form_class=AstakosGroupCreationForm
+ )
-
++
+ if request.method == 'POST':
+ form = form_class(request.POST, request.FILES)
+ if form.is_valid():
+ return render_response(
+ template='im/astakosgroup_form_summary.html',
+ context_instance=get_context(request),
+ form = AstakosGroupCreationSummaryForm(form.cleaned_data),
+ policies = resource_catalog.get_policies(form.policies()),
+ resource_catalog= resource_catalog,
+ )
-
++
+ else:
+ now = datetime.now()
+ data = {
+ 'kind': kind,
+ }
+ for group, resources in resource_catalog.get_groups_resources():
+ data['is_selected_%s' % group] = False
+ for resource in resources:
+ data['%s_uplimit' % resource] = ''
-
++
+ form = form_class(data)
+
+ # Create the template, context, response
+ template_name = "%s/%s_form.html" % (
+ model._meta.app_label,
+ model._meta.object_name.lower()
+ )
+ t = template_loader.get_template(template_name)
+ c = RequestContext(request, {
+ 'form': form,
+ 'kind': kind,
+ 'resource_catalog':resource_catalog,
+ }, context_processors)
+ return HttpResponse(t.render(c))
+
+
+ #@require_http_methods(["POST"])
+ @require_http_methods(["GET", "POST"])
+ @signed_terms_required
+ @login_required
+ def group_add_complete(request):
+ model = AstakosGroup
+ form = AstakosGroupCreationSummaryForm(request.POST)
+ if form.is_valid():
+ d = form.cleaned_data
+ d['owners'] = [request.user]
+ result = callpoint.create_groups((d,)).next()
+ if result.is_success:
+ new_object = result.data[0]
+ msg = _(astakos_messages.OBJECT_CREATED) %\
+ {"verbose_name": model._meta.verbose_name}
+ messages.success(request, msg, fail_silently=True)
-
++
+ # send notification
+ try:
+ send_group_creation_notification(
+ template_name='im/group_creation_notification.txt',
+ dictionary={
+ 'group': new_object,
+ 'owner': request.user,
+ 'policies': d.get('policies', [])
+ }
+ )
+ except SendNotificationError, e:
+ messages.error(request, e, fail_silently=True)
+ post_save_redirect = '/im/group/%(id)s/'
+ return HttpResponseRedirect(post_save_redirect % new_object)
+ else:
+ d = {"verbose_name": model._meta.verbose_name,
+ "reason":result.reason}
- msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
++ msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
+ messages.error(request, msg, fail_silently=True)
+ return render_response(
+ template='im/astakosgroup_form_summary.html',
+ context_instance=get_context(request),
+ form=form)
+
+
+ #@require_http_methods(["GET"])
+ @require_http_methods(["GET", "POST"])
+ @signed_terms_required
+ @login_required
+ def group_list(request):
+ none = request.user.astakos_groups.none()
+ sorting = request.GET.get('sorting')
+ query = """
+ SELECT auth_group.id,
+ %s AS groupname,
+ im_groupkind.name AS kindname,
+ im_astakosgroup.*,
+ owner.email AS groupowner,
+ (SELECT COUNT(*) FROM im_membership
+ WHERE group_id = im_astakosgroup.group_ptr_id
+ AND date_joined IS NOT NULL) AS approved_members_num,
+ (SELECT CASE WHEN(
+ SELECT date_joined FROM im_membership
+ WHERE group_id = im_astakosgroup.group_ptr_id
+ AND person_id = %s) IS NULL
+ THEN 0 ELSE 1 END) AS membership_status
+ FROM im_astakosgroup
+ INNER JOIN im_membership ON (
+ im_astakosgroup.group_ptr_id = im_membership.group_id)
+ INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
+ INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
+ LEFT JOIN im_astakosuser_owner ON (
+ im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
+ LEFT JOIN auth_user as owner ON (
+ im_astakosuser_owner.astakosuser_id = owner.id)
- WHERE im_membership.person_id = %s
++ WHERE im_membership.person_id = %s
+ """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id)
-
++
+ if sorting:
- query = query+" ORDER BY %s ASC" %sorting
++ query = query+" ORDER BY %s ASC" %sorting
+ else:
- query = query+" ORDER BY groupname ASC"
++ query = query+" ORDER BY groupname ASC"
+ q = AstakosGroup.objects.raw(query)
+
-
-
++
++
+ # Create the template, context, response
+ template_name = "%s/%s_list.html" % (
+ q.model._meta.app_label,
+ q.model._meta.object_name.lower()
+ )
+ extra_context = dict(
+ is_search=False,
+ q=q,
+ sorting=request.GET.get('sorting'),
+ page=request.GET.get('page', 1)
+ )
+ return render_response(template_name,
+ context_instance=get_context(request, extra_context)
+ )
+
+
+ @require_http_methods(["GET", "POST"])
+ @signed_terms_required
+ @login_required
+ def group_detail(request, group_id):
+ q = AstakosGroup.objects.select_related().filter(pk=group_id)
+ q = q.extra(select={
+ 'is_member': """SELECT CASE WHEN EXISTS(
+ SELECT id FROM im_membership
+ WHERE group_id = im_astakosgroup.group_ptr_id
+ AND person_id = %s)
+ THEN 1 ELSE 0 END""" % request.user.id,
+ 'is_owner': """SELECT CASE WHEN EXISTS(
+ SELECT id FROM im_astakosuser_owner
+ WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
+ AND astakosuser_id = %s)
+ THEN 1 ELSE 0 END""" % request.user.id,
+ 'kindname': """SELECT name FROM im_groupkind
+ WHERE id = im_astakosgroup.kind_id"""})
+
+ model = q.model
+ context_processors = None
+ mimetype = None
+ try:
+ obj = q.get()
+ except AstakosGroup.DoesNotExist:
+ raise Http404("No %s found matching the query" % (
+ model._meta.verbose_name))
+
+ update_form = AstakosGroupUpdateForm(instance=obj)
+ addmembers_form = AddGroupMembersForm()
+ if request.method == 'POST':
+ update_data = {}
+ addmembers_data = {}
+ for k, v in request.POST.iteritems():
+ if k in update_form.fields:
+ update_data[k] = v
+ if k in addmembers_form.fields:
+ addmembers_data[k] = v
+ update_data = update_data or None
+ addmembers_data = addmembers_data or None
+ update_form = AstakosGroupUpdateForm(update_data, instance=obj)
+ addmembers_form = AddGroupMembersForm(addmembers_data)
+ if update_form.is_valid():
+ update_form.save()
+ if addmembers_form.is_valid():
+ try:
+ map(obj.approve_member, addmembers_form.valid_users)
+ except AssertionError:
+ msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
+ messages.error(request, msg)
+ addmembers_form = AddGroupMembersForm()
+
+ template_name = "%s/%s_detail.html" % (
+ model._meta.app_label, model._meta.object_name.lower())
+ t = template_loader.get_template(template_name)
+ c = RequestContext(request, {
+ 'object': obj,
+ }, context_processors)
+
+ # validate sorting
+ sorting = request.GET.get('sorting')
+ if sorting:
+ form = MembersSortForm({'sort_by': sorting})
+ if form.is_valid():
+ sorting = form.cleaned_data.get('sort_by')
-
++
+ else:
+ form = MembersSortForm({'sort_by': 'person_first_name'})
-
++
+ result = callpoint.list_resources()
+ resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
+ resource_catalog.update_from_result(result)
+
-
++
+ if not result.is_success:
+ messages.error(
+ request,
+ 'Unable to retrieve system resources: %s' % result.reason
+ )
-
++
+ extra_context = {'update_form': update_form,
+ 'addmembers_form': addmembers_form,
+ 'page': request.GET.get('page', 1),
+ 'sorting': sorting,
+ 'resource_catalog':resource_catalog,
+ 'quota':resource_catalog.get_quota(obj.quota)}
+ for key, value in extra_context.items():
+ if callable(value):
+ c[key] = value()
+ else:
+ c[key] = value
+ response = HttpResponse(t.render(c), mimetype=mimetype)
+ populate_xheaders(
+ request, response, model, getattr(obj, obj._meta.pk.name))
+ return response
+
+
+ @require_http_methods(["GET", "POST"])
+ @signed_terms_required
+ @login_required
+ def group_search(request, extra_context=None, **kwargs):
+ q = request.GET.get('q')
+ sorting = request.GET.get('sorting')
+ if request.method == 'GET':
+ form = AstakosGroupSearchForm({'q': q} if q else None)
+ else:
+ form = AstakosGroupSearchForm(get_query(request))
+ if form.is_valid():
+ q = form.cleaned_data['q'].strip()
+ if q:
+ queryset = AstakosGroup.objects.select_related()
+ queryset = queryset.filter(name__contains=q)
+ queryset = queryset.filter(approval_date__isnull=False)
+ queryset = queryset.extra(select={
+ 'groupname': DB_REPLACE_GROUP_SCHEME,
+ 'kindname': "im_groupkind.name",
+ 'approved_members_num': """
+ SELECT COUNT(*) FROM im_membership
+ WHERE group_id = im_astakosgroup.group_ptr_id
+ AND date_joined IS NOT NULL""",
+ 'membership_approval_date': """
+ SELECT date_joined FROM im_membership
+ WHERE group_id = im_astakosgroup.group_ptr_id
+ AND person_id = %s""" % request.user.id,
+ 'is_member': """
+ SELECT CASE WHEN EXISTS(
+ SELECT date_joined FROM im_membership
+ WHERE group_id = im_astakosgroup.group_ptr_id
+ AND person_id = %s)
+ THEN 1 ELSE 0 END""" % request.user.id,
+ 'is_owner': """
+ SELECT CASE WHEN EXISTS(
+ SELECT id FROM im_astakosuser_owner
+ WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
+ AND astakosuser_id = %s)
+ THEN 1 ELSE 0 END""" % request.user.id,
+ 'is_owner': """SELECT CASE WHEN EXISTS(
+ SELECT id FROM im_astakosuser_owner
+ WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
+ AND astakosuser_id = %s)
- THEN 1 ELSE 0 END""" % request.user.id,
++ THEN 1 ELSE 0 END""" % request.user.id,
+ })
+ if sorting:
+ # TODO check sorting value
+ queryset = queryset.order_by(sorting)
+ else:
+ queryset = queryset.order_by("groupname")
+
+ else:
+ queryset = AstakosGroup.objects.none()
+ return object_list(
+ request,
+ queryset,
+ paginate_by=PAGINATE_BY_ALL,
+ page=request.GET.get('page') or 1,
+ template_name='im/astakosgroup_list.html',
+ extra_context=dict(form=form,
+ is_search=True,
+ q=q,
+ sorting=sorting))
+
+
+ @require_http_methods(["GET", "POST"])
+ @signed_terms_required
+ @login_required
+ def group_all(request, extra_context=None, **kwargs):
+ q = AstakosGroup.objects.select_related()
+ q = q.filter(approval_date__isnull=False)
+ q = q.extra(select={
+ 'groupname': DB_REPLACE_GROUP_SCHEME,
+ 'kindname': "im_groupkind.name",
+ 'approved_members_num': """
+ SELECT COUNT(*) FROM im_membership
+ WHERE group_id = im_astakosgroup.group_ptr_id
+ AND date_joined IS NOT NULL""",
+ 'membership_approval_date': """
+ SELECT date_joined FROM im_membership
+ WHERE group_id = im_astakosgroup.group_ptr_id
+ AND person_id = %s""" % request.user.id,
+ 'is_member': """
+ SELECT CASE WHEN EXISTS(
+ SELECT date_joined FROM im_membership
+ WHERE group_id = im_astakosgroup.group_ptr_id
+ AND person_id = %s)
+ THEN 1 ELSE 0 END""" % request.user.id,
+ 'is_owner': """SELECT CASE WHEN EXISTS(
+ SELECT id FROM im_astakosuser_owner
+ WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
+ AND astakosuser_id = %s)
+ THEN 1 ELSE 0 END""" % request.user.id, })
+ sorting = request.GET.get('sorting')
+ if sorting:
+ # TODO check sorting value
+ q = q.order_by(sorting)
+ else:
+ q = q.order_by("groupname")
-
++
+ return object_list(
+ request,
+ q,
+ paginate_by=PAGINATE_BY_ALL,
+ page=request.GET.get('page') or 1,
+ template_name='im/astakosgroup_list.html',
+ extra_context=dict(form=AstakosGroupSearchForm(),
+ is_search=True,
+ sorting=sorting))
+
+
+ #@require_http_methods(["POST"])
+ @require_http_methods(["POST", "GET"])
+ @signed_terms_required
+ @login_required
+ def group_join(request, group_id):
+ m = Membership(group_id=group_id,
+ person=request.user,
+ date_requested=datetime.now())
+ try:
+ m.save()
+ post_save_redirect = reverse(
+ 'group_detail',
+ kwargs=dict(group_id=group_id))
+ return HttpResponseRedirect(post_save_redirect)
+ except IntegrityError, e:
+ logger.exception(e)
+ msg = _(astakos_messages.GROUP_JOIN_FAILURE)
+ messages.error(request, msg)
+ return group_search(request)
+
+
+ @require_http_methods(["POST"])
+ @signed_terms_required
+ @login_required
+ def group_leave(request, group_id):
+ try:
+ m = Membership.objects.select_related().get(
+ group__id=group_id,
+ person=request.user)
+ except Membership.DoesNotExist:
+ return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
+ if request.user in m.group.owner.all():
+ return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_GROUP))
+ return delete_object(
+ request,
+ model=Membership,
+ object_id=m.id,
+ template_name='im/astakosgroup_list.html',
+ post_delete_redirect=reverse(
+ 'group_detail',
+ kwargs=dict(group_id=group_id)))
+
+
+ def handle_membership(func):
+ @wraps(func)
+ def wrapper(request, group_id, user_id):
+ try:
+ m = Membership.objects.select_related().get(
+ group__id=group_id,
+ person__id=user_id)
+ except Membership.DoesNotExist:
+ return HttpResponseBadRequest(_(astakos_messages.NOT_MEMBER))
+ else:
+ if request.user not in m.group.owner.all():
+ return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
+ func(request, m)
+ return group_detail(request, group_id)
+ return wrapper
+
+
+ #@require_http_methods(["POST"])
+ @require_http_methods(["POST", "GET"])
+ @signed_terms_required
+ @login_required
+ @handle_membership
+ def approve_member(request, membership):
+ try:
+ membership.approve()
+ realname = membership.person.realname
+ msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
+ messages.success(request, msg)
+ except AssertionError:
+ msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
+ messages.error(request, msg)
+ except BaseException, e:
+ logger.exception(e)
+ realname = membership.person.realname
+ msg = _(astakos_messages.GENERIC_ERROR)
+ messages.error(request, msg)
+
+
+ @signed_terms_required
+ @login_required
+ @handle_membership
+ def disapprove_member(request, membership):
+ try:
+ membership.disapprove()
+ realname = membership.person.realname
+ msg = astakos_messages.MEMBER_REMOVED % realname
+ messages.success(request, msg)
+ except BaseException, e:
+ logger.exception(e)
+ msg = _(astakos_messages.GENERIC_ERROR)
+ messages.error(request, msg)
+
+
+ #@require_http_methods(["GET"])
+ @require_http_methods(["POST", "GET"])
+ @signed_terms_required
+ @login_required
+ def resource_list(request):
+ def with_class(entry):
+ entry['load_class'] = 'red'
+ max_value = float(entry['maxValue'])
+ curr_value = float(entry['currValue'])
+ if max_value > 0 :
+ entry['ratio'] = (curr_value / max_value) * 100
+ else:
- entry['ratio'] = 0
++ entry['ratio'] = 0
+ if entry['ratio'] < 66:
+ entry['load_class'] = 'yellow'
+ if entry['ratio'] < 33:
+ entry['load_class'] = 'green'
+ return entry
+
+ def pluralize(entry):
+ entry['plural'] = engine.plural(entry.get('name'))
+ return entry
+
+ result = callpoint.get_user_status(request.user.id)
+ if result.is_success:
+ backenddata = map(with_class, result.data)
+ data = map(pluralize, result.data)
+ else:
+ data = None
+ messages.error(request, result.reason)
+ resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
+ resource_catalog.update_from_result_report(result)
-
+
-
++
++
+ return render_response('im/resource_list.html',
+ data=data,
+ context_instance=get_context(request),
+ resource_catalog=resource_catalog,
+ result=result)
+
+
+ def group_create_list(request):
+ form = PickResourceForm()
+ return render_response(
+ template='im/astakosgroup_create_list.html',
+ context_instance=get_context(request),)
+
+
+ #@require_http_methods(["GET"])
+ @require_http_methods(["POST", "GET"])
+ @signed_terms_required
+ @login_required
+ def billing(request):
+
+ today = datetime.today()
+ month_last_day = calendar.monthrange(today.year, today.month)[1]
+ start = request.POST.get('datefrom', None)
+ if start:
+ today = datetime.fromtimestamp(int(start))
+ month_last_day = calendar.monthrange(today.year, today.month)[1]
+
+ start = datetime(today.year, today.month, 1).strftime("%s")
+ end = datetime(today.year, today.month, month_last_day).strftime("%s")
+ r = request_billing.apply(args=('pgerakios@grnet.gr',
+ int(start) * 1000,
+ int(end) * 1000))
+ data = {}
+
+ try:
+ status, data = r.result
+ data = _clear_billing_data(data)
+ if status != 200:
+ messages.error(request, _(astakos_messages.BILLING_ERROR) % status)
+ except:
+ messages.error(request, r.result)
+
+ return render_response(
+ template='im/billing.html',
+ context_instance=get_context(request),
+ data=data,
+ zerodate=datetime(month=1, year=1970, day=1),
+ today=today,
+ start=int(start),
+ month_last_day=month_last_day)
+
+
+ def _clear_billing_data(data):
+
+ # remove addcredits entries
+ def isnotcredit(e):
+ return e['serviceName'] != "addcredits"
+
+ # separate services
+ def servicefilter(service_name):
+ service = service_name
+
+ def fltr(e):
+ return e['serviceName'] == service
+ return fltr
+
+ data['bill_nocredits'] = filter(isnotcredit, data['bill'])
+ data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
+ data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
+ data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
+
+ return data
-
-
++
++
+ #@require_http_methods(["GET"])
+ @require_http_methods(["POST", "GET"])
+ @signed_terms_required
+ @login_required
+ def timeline(request):
+ # data = {'entity':request.user.email}
+ timeline_body = ()
+ timeline_header = ()
+ # form = TimelineForm(data)
+ form = TimelineForm()
+ if request.method == 'POST':
+ data = request.POST
+ form = TimelineForm(data)
+ if form.is_valid():
+ data = form.cleaned_data
+ timeline_header = ('entity', 'resource',
+ 'event name', 'event date',
+ 'incremental cost', 'total cost')
+ timeline_body = timeline_charge(
+ data['entity'], data['resource'],
+ data['start_date'], data['end_date'],
+ data['details'], data['operation'])
+
+ return render_response(template='im/timeline.html',
+ context_instance=get_context(request),
+ form=form,
+ timeline_header=timeline_header,
+ timeline_body=timeline_body)
+ return data
++
+@require_http_methods(["GET", "POST"])
+@login_required
+@signed_terms_required
+def remove_auth_provider(request, pk):
+ provider = request.user.auth_providers.get(pk=pk)
- print provider
+ if provider.can_remove():
+ provider.delete()
+ return HttpResponseRedirect(reverse('edit_profile'))
+ else:
+ messages.error(_('Authentication method cannot be removed'))
+ return HttpResponseRedirect(reverse('edit_profile'))
-