From: Sofia Papagiannaki Date: Mon, 22 Oct 2012 14:31:46 +0000 (+0300) Subject: Merge remote-tracking branch 'origin' into future X-Git-Url: https://code.grnet.gr/git/astakos/commitdiff_plain/485c713e0ff9565ef58d6e1506f54f5dcf37b8ba?hp=-c Merge remote-tracking branch 'origin' into future Conflicts: snf-astakos-app/README snf-astakos-app/astakos/im/forms.py snf-astakos-app/astakos/im/settings.py snf-astakos-app/astakos/im/static/im/js/forms.js snf-astakos-app/astakos/im/target/redirect.py snf-astakos-app/astakos/im/target/shibboleth.py snf-astakos-app/astakos/im/urls.py snf-astakos-app/astakos/im/util.py snf-astakos-app/astakos/im/views.py snf-astakos-app/conf/20-snf-astakos-app-settings.conf --- 485c713e0ff9565ef58d6e1506f54f5dcf37b8ba diff --combined snf-astakos-app/README index 4055f64,617ea32..f7c4c01 --- a/snf-astakos-app/README +++ b/snf-astakos-app/README @@@ -89,14 -89,8 +89,18 @@@ ASTAKOS_ADMIN_NOTIFICATION_EMAIL_SUBJEC ASTAKOS_HELPDESK_NOTIFICATION_EMAIL_SUBJECT '%s alpha2 testing account activated (%%(user)s)' % SITENAME Account activation helpdesk notification email subject ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT 'Email change on %s alpha2 testing' % SITENAME Email change subject ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT 'Password reset on %s alpha2 testing' % SITENAME Password change email subject ++ +ASTAKOS_QUOTA_HOLDER_URL '' The quota holder URI + e.g. ``http://localhost:8080/api/quotaholder/v`` +ASTAKOS_SERVICES {'cyclades': {'url':'https://node1.example.com/ui/', 'quota': {'vm': 2}}, Cloud service default url and quota + 'pithos+': {'url':'https://node2.example.com/ui/', 'quota': { + 'diskspace': 50 * 1024 * 1024 * 1024}}}) +ASTAKOS_AQUARIUM_URL '' The billing (aquarium) URI + e.g. ``http://localhost:8888/user`` +ASTAKOS_PAGINATE_BY 10 Number of object to be displayed per page ++ + ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN True Enforce token renewal on password change/reset. If set to False, user can optionally decide + whether to renew the token or not. =========================================== ============================================================================= =========================================================================================== Administrator functions @@@ -122,4 -116,11 +126,4 @@@ showuser Show user inf To update user credibility from the billing system (Aquarium), enable the queue, install snf-pithos-tools and use ``pithos-dispatcher``:: - pithos-dispatcher --exchange=aquarium --callback=astakos.im.queue.listener.on_creditevent - -Load groups: ------------- - -To set the initial user groups load the followind fixture: - - snf-manage loaddata groups + pithos-dispatcher --exchange=aquarium --callback=astakos.im.endpoints.aquarium.consumer.on_creditevent diff --combined snf-astakos-app/astakos/im/forms.py index 8ba1423,b3d2be1..878eb5e --- a/snf-astakos-app/astakos/im/forms.py +++ b/snf-astakos-app/astakos/im/forms.py @@@ -31,34 -31,32 +31,34 @@@ # interpreted as representing official policies, either expressed # or implied, of GRNET S.A. from urlparse import urljoin from django import forms from django.utils.translation import ugettext as _ -from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \ - PasswordResetForm, PasswordChangeForm, SetPasswordForm +from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, - PasswordResetForm, PasswordChangeForm - ) ++ PasswordResetForm, PasswordChangeForm, ++ SetPasswordForm) from django.core.mail import send_mail from django.contrib.auth.tokens import default_token_generator 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 astakos.im.models import AstakosUser, Invitation, get_latest_terms, EmailChange -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 +from django.forms.extras.widgets import SelectDateWidget +from django.conf import settings + +from astakos.im.models import (AstakosUser, EmailChange, AstakosGroup, + Invitation, Membership, GroupKind, Resource, + get_latest_terms) +from astakos.im.settings import (INVITATIONS_PER_LEVEL, BASEURL, SITENAME, + RECAPTCHA_PRIVATE_KEY, RECAPTCHA_ENABLED, + DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL, - PASSWORD_RESET_EMAIL_SUBJECT) - ++ PASSWORD_RESET_EMAIL_SUBJECT, ++ NEWPASSWD_INVALIDATE_TOKEN) from astakos.im.widgets import DummyWidget, RecaptchaWidget from astakos.im.functions import send_change_email -# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead -from astakos.im.util import reverse_lazy, reserved_email, get_query +from astakos.im.util import reserved_email, get_query import logging import hashlib @@@ -67,7 -65,6 +67,7 @@@ from random import rando logger = logging.getLogger(__name__) + class LocalUserCreationForm(UserCreationForm): """ Extends the built in UserCreationForm in several ways: @@@ -77,13 -74,11 +77,13 @@@ * User created is not active. """ recaptcha_challenge_field = forms.CharField(widget=DummyWidget) - recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='') + recaptcha_response_field = forms.CharField( + widget=RecaptchaWidget, label='') class Meta: model = AstakosUser - fields = ("email", "first_name", "last_name", "has_signed_terms", "has_signed_terms") + fields = ("email", "first_name", "last_name", + "has_signed_terms", "has_signed_terms") def __init__(self, *args, **kwargs): """ @@@ -101,7 -96,7 +101,7 @@@ if RECAPTCHA_ENABLED: self.fields.keyOrder.extend(['recaptcha_challenge_field', - 'recaptcha_response_field',]) + 'recaptcha_response_field', ]) if get_latest_terms(): self.fields.keyOrder.append('has_signed_terms') @@@ -109,9 -104,9 +109,9 @@@ # Overriding field label since we need to apply a link # to the terms within the label terms_link_html = '%s' \ - % (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'] @@@ -142,8 -137,7 +142,8 @@@ 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( + _('You have not entered the correct words')) def save(self, commit=True): """ @@@ -154,10 -148,9 +154,10 @@@ 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. @@@ -176,7 -169,8 +176,7 @@@ ro = ('email', 'username',) 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 @@@ -187,13 -181,11 +187,13 @@@ user.save() return user + class ThirdPartyUserCreationForm(forms.ModelForm): class Meta: model = AstakosUser - fields = ("email", "first_name", "last_name", "third_party_identifier", "has_signed_terms") - + fields = ("email", "first_name", "last_name", + "third_party_identifier", "has_signed_terms") + def __init__(self, *args, **kwargs): """ Changes the order of fields, and removes the username field. @@@ -202,8 -194,7 +202,8 @@@ if self.request: kwargs.pop('request') super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs) - self.fields.keyOrder = ['email', 'first_name', 'last_name', 'third_party_identifier'] + self.fields.keyOrder = ['email', 'first_name', 'last_name', + 'third_party_identifier'] if get_latest_terms(): self.fields.keyOrder.append('has_signed_terms') #set readonly form fields @@@ -215,10 -206,10 +215,10 @@@ # Overriding field label since we need to apply a link # to the terms within the label terms_link_html = '%s' \ - % (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'] if not email: @@@ -238,10 -229,9 +238,10 @@@ user.provider = get_query(self.request).get('provider') 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. @@@ -250,8 -240,7 +250,8 @@@ """ Changes the order of fields, and removes the username field. """ - super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs) + super( + InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs) #set readonly form fields ro = ('email',) @@@ -259,8 -248,7 +259,8 @@@ self.fields[f].widget.attrs['readonly'] = True def save(self, commit=True): - user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False) + user = super( + InvitedThirdPartyUserCreationForm, self).save(commit=False) level = user.invitation.inviter.level + 1 user.level = level user.invitations = INVITATIONS_PER_LEVEL.get(level, 0) @@@ -269,23 -257,20 +269,23 @@@ user.save() return user -class ShibbolethUserCreationForm(ThirdPartyUserCreationForm): - additional_email = forms.CharField(widget=forms.HiddenInput(), label='', required = False) +class ShibbolethUserCreationForm(ThirdPartyUserCreationForm): + additional_email = forms.CharField( + widget=forms.HiddenInput(), label='', required=False) + def __init__(self, *args, **kwargs): super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs) self.fields.keyOrder.append('additional_email') # copy email value to additional_mail in case user will change it name = 'email' field = self.fields[name] - self.initial['additional_email'] = self.initial.get(name, field.initial) - + self.initial['additional_email'] = self.initial.get(name, + field.initial) + def clean_email(self): email = self.cleaned_data['email'] - for user in AstakosUser.objects.filter(email = email): + for user in AstakosUser.objects.filter(email=email): if user.provider == 'shibboleth': raise forms.ValidationError(_("This email is already associated with another shibboleth account.")) elif not user.is_active: @@@ -294,17 -279,14 +294,17 @@@ super(ShibbolethUserCreationForm, self).clean_email() return email + class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm): pass + class LoginForm(AuthenticationForm): username = forms.EmailField(label=_("Email")) recaptcha_challenge_field = forms.CharField(widget=DummyWidget) - recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='') - + recaptcha_response_field = forms.CharField( + widget=RecaptchaWidget, label='') + def __init__(self, *args, **kwargs): was_limited = kwargs.get('was_limited', False) request = kwargs.get('request', None) @@@ -321,12 -303,8 +321,12 @@@ self.fields.keyOrder = ['username', 'password'] if was_limited and RECAPTCHA_ENABLED: self.fields.keyOrder.extend(['recaptcha_challenge_field', - 'recaptcha_response_field',]) - + 'recaptcha_response_field', ]) + + def clean_username(self): + if 'username' in self.cleaned_data: + return self.cleaned_data['username'].lower() + def clean_recaptcha_response_field(self): if 'recaptcha_challenge_field' in self.cleaned_data: self.validate_captcha() @@@ -342,16 -320,14 +342,16 @@@ 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( + _('You have not entered the correct words')) + def clean(self): super(LoginForm, self).clean() if self.user_cache and self.user_cache.provider not in ('local', ''): raise forms.ValidationError(_('Local login is not the current authentication method for this account.')) return self.cleaned_data + class ProfileForm(forms.ModelForm): """ Subclass of ``ModelForm`` for permiting user to edit his/her profile. @@@ -364,8 -340,7 +364,8 @@@ class Meta: model = AstakosUser - fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires') + fields = ('email', 'first_name', 'last_name', 'auth_token', + 'auth_token_expires') def __init__(self, *args, **kwargs): super(ProfileForm, self).__init__(*args, **kwargs) @@@ -384,7 -359,6 +384,7 @@@ user.save() return user + class FeedbackForm(forms.Form): """ Form for writing feedback. @@@ -393,16 -367,14 +393,16 @@@ feedback_data = forms.CharField(widget=forms.HiddenInput(), label='', required=False) + class SendInvitationForm(forms.Form): """ Form for sending an invitations """ - email = forms.EmailField(required = True, label = 'Email address') - first_name = forms.EmailField(label = 'First name') - last_name = forms.EmailField(label = 'Last name') + email = forms.EmailField(required=True, label='Email address') + first_name = forms.EmailField(label='First name') + last_name = forms.EmailField(label='Last name') + class ExtendedPasswordResetForm(PasswordResetForm): """ @@@ -417,24 -389,20 +417,24 @@@ try: user = AstakosUser.objects.get(email=email, is_active=True) if not user.has_usable_password(): - raise forms.ValidationError(_("This account has not a usable password.")) - except AstakosUser.DoesNotExist, e: + raise forms.ValidationError( + _("This account has not a usable password.")) + except AstakosUser.DoesNotExist: raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?')) 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. """ for user in self.users_cache: url = reverse('django.contrib.auth.views.password_reset_confirm', - kwargs={'uidb36':int_to_base36(user.id), - 'token':token_generator.make_token(user)}) + kwargs={'uidb36': int_to_base36(user.id), + 'token': token_generator.make_token(user) + } + ) url = urljoin(BASEURL, url) t = loader.get_template(email_template_name) c = { @@@ -445,10 -413,9 +445,10 @@@ 'baseurl': BASEURL, 'support': DEFAULT_CONTACT_EMAIL } - from_email = DEFAULT_FROM_EMAIL + from_email = settings.SERVER_EMAIL send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT), - t.render(Context(c)), from_email, [user.email]) + t.render(Context(c)), from_email, [user.email]) + class EmailChangeForm(forms.ModelForm): class Meta: @@@ -464,14 -431,12 +464,14 @@@ def save(self, email_template_name, request, commit=True): ec = super(EmailChangeForm, self).save(commit=False) ec.user = request.user - activation_key = hashlib.sha1(str(random()) + smart_str(ec.new_email_address)) - ec.activation_key=activation_key.hexdigest() + activation_key = hashlib.sha1( + str(random()) + smart_str(ec.new_email_address)) + ec.activation_key = activation_key.hexdigest() if commit: ec.save() send_change_email(ec, request, email_template_name=email_template_name) + class SignApprovalTermsForm(forms.ModelForm): class Meta: model = AstakosUser @@@ -486,7 -451,6 +486,7 @@@ raise forms.ValidationError(_('You have to agree with the terms')) return has_signed_terms + class InvitationForm(forms.ModelForm): username = forms.EmailField(label=_("Email")) @@@ -500,160 -464,55 +500,189 @@@ def clean_username(self): username = self.cleaned_data['username'] try: - Invitation.objects.get(username = username) - raise forms.ValidationError(_('There is already invitation for this email.')) + Invitation.objects.get(username=username) + raise forms.ValidationError( + _('There is already invitation for this email.')) except Invitation.DoesNotExist: pass return username + class ExtendedPasswordChangeForm(PasswordChangeForm): """ Extends PasswordChangeForm by enabling user to optionally renew also the token. """ - renew = forms.BooleanField(label='Renew token', required=False) + if not NEWPASSWD_INVALIDATE_TOKEN: + renew = forms.BooleanField(label='Renew token', required=False, + initial=True, + help_text='Unsetting this may result in security risk.') def __init__(self, user, *args, **kwargs): super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs) def save(self, commit=True): user = super(ExtendedPasswordChangeForm, self).save(commit=False) - if self.cleaned_data.get('renew'): + if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'): user.renew_token() if commit: user.save() return user + +class AstakosGroupCreationForm(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 + ) + + class Meta: + model = AstakosGroup + + def __init__(self, *args, **kwargs): + try: + resources = kwargs.pop('resources') + except KeyError: + resources = {} + super(AstakosGroupCreationForm, self).__init__(*args, **kwargs) + self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc', 'issue_date', + 'expiration_date', 'estimated_participants', + 'moderation_enabled'] + for id, r in resources.iteritems(): + self.fields['resource_%s' % id] = forms.IntegerField( + label=r, + required=False, + help_text=_('Leave it blank for no additional quota.') + ) + + def resources(self): + for name, value in self.cleaned_data.items(): + prefix, delimiter, suffix = name.partition('resource_') + if suffix: + # yield only those having a value + if not value: + continue + yield (suffix, value) + +class AstakosGroupUpdateForm(forms.ModelForm): + class Meta: + model = AstakosGroup + fields = ('homepage', 'desc') + +class AddGroupMembersForm(forms.Form): + q = forms.CharField(max_length=800, widget=forms.Textarea, label=_('Search users'), + help_text=_('Add comma separated user emails'), + 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( + _('Unknown users: %s' % ','.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 group') + +class TimelineForm(forms.Form): +# entity = forms.CharField( +# widget=forms.HiddenInput(), label='') + 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()" ++ resource.widget.attrs["onchange"]="this.form.submit()" ++ + class ExtendedSetPasswordForm(SetPasswordForm): + """ + Extends SetPasswordForm by enabling user + to optionally renew also the token. + """ + if not NEWPASSWD_INVALIDATE_TOKEN: + renew = forms.BooleanField(label='Renew token', required=False, + initial=True, + help_text='Unsetting this may result in security risk.') + + def __init__(self, user, *args, **kwargs): + super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs) + + def save(self, commit=True): + user = super(ExtendedSetPasswordForm, self).save(commit=False) + if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'): + try: + user = AstakosUser.objects.get(id=user.id) + except AstakosUser.DoesNotExist: + pass + else: + user.renew_token() + if commit: + user.save() + return user diff --combined snf-astakos-app/astakos/im/management/commands/user-modify.py index 33bb2fd,9138524..33bb2fd --- a/snf-astakos-app/astakos/im/management/commands/user-modify.py +++ b/snf-astakos-app/astakos/im/management/commands/user-modify.py @@@ -32,217 -32,173 +32,217 @@@ # or implied, of GRNET S.A. from optparse import make_option +from datetime import datetime from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import Group, Permission -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError +from django.db.utils import IntegrityError -from astakos.im.models import AstakosUser +from astakos.im.models import (AstakosUser, AstakosGroup, Membership, Resource, + AstakosUserQuota) +from astakos.im.endpoints.aquarium.producer import report_user_credits_event from ._common import remove_user_permission, add_user_permission + class Command(BaseCommand): args = "" help = "Modify a user's attributes" - - option_list = BaseCommand.option_list + ( + + option_list = list(BaseCommand.option_list) + [ make_option('--invitations', - dest='invitations', - metavar='NUM', - help="Update user's invitations"), + dest='invitations', + metavar='NUM', + help="Update user's invitations"), make_option('--level', - dest='level', - metavar='NUM', - help="Update user's level"), + dest='level', + metavar='NUM', + help="Update user's level"), make_option('--password', - dest='password', - metavar='PASSWORD', - help="Set user's password"), + dest='password', + metavar='PASSWORD', + help="Set user's password"), make_option('--provider', - dest='provider', - metavar='PROVIDER', - help="Set user's provider"), + dest='provider', + metavar='PROVIDER', + help="Set user's provider"), make_option('--renew-token', - action='store_true', - dest='renew_token', - default=False, - help="Renew the user's token"), + action='store_true', + dest='renew_token', + default=False, + help="Renew the user's token"), make_option('--renew-password', - action='store_true', - dest='renew_password', - default=False, - help="Renew the user's password"), + action='store_true', + dest='renew_password', + default=False, + help="Renew the user's password"), make_option('--set-admin', - action='store_true', - dest='admin', - default=False, - help="Give user admin rights"), + action='store_true', + dest='admin', + default=False, + help="Give user admin rights"), make_option('--set-noadmin', - action='store_true', - dest='noadmin', - default=False, - help="Revoke user's admin rights"), + action='store_true', + dest='noadmin', + default=False, + help="Revoke user's admin rights"), make_option('--set-active', - action='store_true', - dest='active', - default=False, - help="Change user's state to inactive"), + action='store_true', + dest='active', + default=False, + help="Change user's state to inactive"), make_option('--set-inactive', - action='store_true', - dest='inactive', - default=False, - help="Change user's state to inactive"), + action='store_true', + dest='inactive', + default=False, + help="Change user's state to inactive"), make_option('--add-group', - dest='add-group', - help="Add user group"), + dest='add-group', + help="Add user group"), make_option('--delete-group', - dest='delete-group', - help="Delete user group"), + dest='delete-group', + help="Delete user group"), make_option('--add-permission', - dest='add-permission', - help="Add user permission"), + dest='add-permission', + help="Add user permission"), make_option('--delete-permission', - dest='delete-permission', - help="Delete user permission"), - ) + dest='delete-permission', + help="Delete user permission"), + make_option('--refill-credits', + action='store_true', + dest='refill', + default=False, + help="Refill user credits"), + ] + resources = Resource.objects.select_related().all() + append = option_list.append + for r in resources: + append(make_option('--%s-set-quota' % r, + dest='%s-set-quota' % r, + metavar='QUANTITY', + help="Set resource quota")) def handle(self, *args, **options): if len(args) != 1: raise CommandError("Please provide a user ID") - + if args[0].isdigit(): - user = AstakosUser.objects.get(id=int( args[0])) + user = AstakosUser.objects.get(id=int(args[0])) else: raise CommandError("Invalid ID") - + if not user: raise CommandError("Unknown user") - + if options.get('admin'): user.is_superuser = True elif options.get('noadmin'): user.is_superuser = False - + if options.get('active'): user.is_active = True elif options.get('inactive'): user.is_active = False - + invitations = options.get('invitations') if invitations is not None: user.invitations = int(invitations) - + groupname = options.get('add-group') if groupname is not None: try: - group = Group.objects.get(name=groupname) - user.groups.add(group) - except Group.DoesNotExist, e: - self.stdout.write("Group named %s does not exist\n" % groupname) - + group = AstakosGroup.objects.get(name=groupname) + m = Membership( + person=user, group=group, date_joined=datetime.now()) + m.save() + except AstakosGroup.DoesNotExist, e: + self.stdout.write( + "Group named %s does not exist\n" % groupname) + except IntegrityError, e: + self.stdout.write("User is already member of %s\n" % groupname) + groupname = options.get('delete-group') if groupname is not None: try: - group = Group.objects.get(name=groupname) - user.groups.remove(group) - except Group.DoesNotExist, e: - self.stdout.write("Group named %s does not exist\n" % groupname) - + group = AstakosGroup.objects.get(name=groupname) + m = Membership.objects.get(person=user, group=group) + m.delete() + except AstakosGroup.DoesNotExist, e: + self.stdout.write( + "Group named %s does not exist\n" % groupname) + except Membership.DoesNotExist, e: + self.stdout.write("User is not a member of %s\n" % groupname) + pname = options.get('add-permission') if pname is not None: try: r, created = add_user_permission(user, pname) if created: - self.stdout.write('Permission: %s created successfully\n' % pname) + self.stdout.write( + 'Permission: %s created successfully\n' % pname) if r > 0: - self.stdout.write('Permission: %s added successfully\n' % pname) - elif r==0: - self.stdout.write('User has already permission: %s\n' % pname) + self.stdout.write( + 'Permission: %s added successfully\n' % pname) + elif r == 0: + self.stdout.write( + 'User has already permission: %s\n' % pname) except Exception, e: raise CommandError(e) - - pname = options.get('delete-permission') + + pname = options.get('delete-permission') if pname is not None and not user.has_perm(pname): try: r = remove_user_permission(user, pname) if r < 0: - self.stdout.write('Invalid permission codename: %s\n' % pname) + self.stdout.write( + 'Invalid permission codename: %s\n' % pname) elif r == 0: self.stdout.write('User has not permission: %s\n' % pname) elif r > 0: - self.stdout.write('Permission: %s removed successfully\n' % pname) + self.stdout.write( + 'Permission: %s removed successfully\n' % pname) except Exception, e: raise CommandError(e) - + level = options.get('level') if level is not None: user.level = int(level) - + password = options.get('password') if password is not None: user.set_password(password) - + provider = options.get('provider') if provider is not None: user.provider = provider - - + password = None if options['renew_password']: password = AstakosUser.objects.make_random_password() user.set_password(password) - + if options['renew_token']: user.renew_token() - + + if options['refill']: + report_user_credits_event(user) + try: user.save() except ValidationError, e: raise CommandError(e) - + if password: self.stdout.write('User\'s new password: %s\n' % password) + + for r in self.resources: + limit = options.get('%s-set-quota' % r) + if not limit: + continue + if not limit.isdigit(): + raise CommandError('Invalid limit') + + q = AstakosUserQuota.objects + q, created = q.get_or_create(resource=r, user=user, + defaults={'uplimit': limit}) + verb = 'set' if created else 'updated' + self.stdout.write('User\'s quota %s successfully\n' % verb) diff --combined snf-astakos-app/astakos/im/settings.py index 576466e,da60994..d0c758d --- a/snf-astakos-app/astakos/im/settings.py +++ b/snf-astakos-app/astakos/im/settings.py @@@ -11,16 -11,17 +11,16 @@@ TWITTER_SECRET = getattr(settings, 'AST DEFAULT_USER_LEVEL = getattr(settings, 'ASTAKOS_DEFAULT_USER_LEVEL', 4) INVITATIONS_PER_LEVEL = getattr(settings, 'ASTAKOS_INVITATIONS_PER_LEVEL', { - 0 : 100, - 1 : 2, - 2 : 0, - 3 : 0, - 4 : 0 + 0: 100, + 1: 2, + 2: 0, + 3: 0, + 4: 0 }) # Address to use for outgoing emails -DEFAULT_FROM_EMAIL = getattr(settings, 'ASTAKOS_DEFAULT_FROM_EMAIL', 'GRNET Cloud ') -DEFAULT_CONTACT_EMAIL = getattr(settings, 'ASTAKOS_DEFAULT_CONTACT_EMAIL', 'support@cloud.grnet.gr') -DEFAULT_ADMIN_EMAIL = getattr(settings, 'ASTAKOS_DEFAULT_ADMIN_EMAIL', 'support@cloud.grnet.gr') +DEFAULT_CONTACT_EMAIL = getattr( + settings, 'ASTAKOS_DEFAULT_CONTACT_EMAIL', 'support@cloud.grnet.gr') # Identity Management enabled modules IM_MODULES = getattr(settings, 'ASTAKOS_IM_MODULES', ['local', 'shibboleth']) @@@ -50,7 -51,7 +50,7 @@@ SITENAME = getattr(settings, 'ASTAKOS_S RECAPTCHA_PUBLIC_KEY = getattr(settings, 'ASTAKOS_RECAPTCHA_PUBLIC_KEY', '') RECAPTCHA_PRIVATE_KEY = getattr(settings, 'ASTAKOS_RECAPTCHA_PRIVATE_KEY', '') RECAPTCHA_OPTIONS = getattr(settings, 'ASTAKOS_RECAPTCHA_OPTIONS', - {'theme' : 'custom', 'custom_theme_widget': 'okeanos_recaptcha'}) + {'theme': 'custom', 'custom_theme_widget': 'okeanos_recaptcha'}) RECAPTCHA_USE_SSL = getattr(settings, 'ASTAKOS_RECAPTCHA_USE_SSL', True) RECAPTCHA_ENABLED = getattr(settings, 'ASTAKOS_RECAPTCHA_ENABLED', True) @@@ -58,14 -59,13 +58,14 @@@ BILLING_FIELDS = getattr(settings, 'ASTAKOS_BILLING_FIELDS', ['is_active']) # Queue for billing. -QUEUE_CONNECTION = getattr(settings, 'ASTAKOS_QUEUE_CONNECTION', None) # Example: 'rabbitmq://guest:guest@localhost:5672/astakos' +QUEUE_CONNECTION = getattr(settings, 'ASTAKOS_QUEUE_CONNECTION', None) # Example: 'rabbitmq://guest:guest@localhost:5672/astakos' # Set where the user should be redirected after logout LOGOUT_NEXT = getattr(settings, 'ASTAKOS_LOGOUT_NEXT', '') # Set user email patterns that are automatically activated -RE_USER_EMAIL_PATTERNS = getattr(settings, 'ASTAKOS_RE_USER_EMAIL_PATTERNS', []) +RE_USER_EMAIL_PATTERNS = getattr( + settings, 'ASTAKOS_RE_USER_EMAIL_PATTERNS', []) # Messages to display on login page header # e.g. {'warning': 'This warning message will be displayed on the top of login page'} @@@ -87,16 -87,14 +87,16 @@@ GLOBAL_MESSAGES = getattr(settings, 'AS # e.g. {'https://cms.okeanos.grnet.gr/': 'Back to ~okeanos'} PROFILE_EXTRA_LINKS = getattr(settings, 'ASTAKOS_PROFILE_EXTRA_LINKS', {}) -# The number of unsuccessful login requests per minute allowed for a specific email -RATELIMIT_RETRIES_ALLOWED = getattr(settings, 'ASTAKOS_RATELIMIT_RETRIES_ALLOWED', 3) +# The number of unsuccessful login requests per minute allowed for a specific user +RATELIMIT_RETRIES_ALLOWED = getattr( + settings, 'ASTAKOS_RATELIMIT_RETRIES_ALLOWED', 3) # If False the email change mechanism is disabled EMAILCHANGE_ENABLED = getattr(settings, 'ASTAKOS_EMAILCHANGE_ENABLED', False) # Set the expiration time (in days) of email change requests -EMAILCHANGE_ACTIVATION_DAYS = getattr(settings, 'ASTAKOS_EMAILCHANGE_ACTIVATION_DAYS', 10) +EMAILCHANGE_ACTIVATION_DAYS = getattr( + settings, 'ASTAKOS_EMAILCHANGE_ACTIVATION_DAYS', 10) # Set the astakos main functions logging severity (None to disable) from logging import INFO @@@ -120,36 -118,5 +120,21 @@@ EMAIL_CHANGE_EMAIL_SUBJECT = getattr(se PASSWORD_RESET_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT', 'Password reset on %s alpha2 testing' % SITENAME) - # Configurable email subjects - INVITATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_INVITATION_EMAIL_SUBJECT', - 'Invitation to %s alpha2 testing' % SITENAME) - GREETING_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_GREETING_EMAIL_SUBJECT', - 'Welcome to %s alpha2 testing' % SITENAME) - FEEDBACK_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_FEEDBACK_EMAIL_SUBJECT', - 'Feedback from %s alpha2 testing' % SITENAME) - VERIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_VERIFICATION_EMAIL_SUBJECT', - '%s alpha2 testing account activation is needed' % SITENAME) - ADMIN_NOTIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_ADMIN_NOTIFICATION_EMAIL_SUBJECT', - '%s alpha2 testing account created (%%(user)s)' % SITENAME) - HELPDESK_NOTIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_HELPDESK_NOTIFICATION_EMAIL_SUBJECT', - '%s alpha2 testing account activated (%%(user)s)' % SITENAME) - EMAIL_CHANGE_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT', - 'Email change on %s alpha2 testing' % SITENAME) - PASSWORD_RESET_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT', - 'Password reset on %s alpha2 testing' % SITENAME) - +# Set the quota holder component URI +QUOTA_HOLDER_URL = getattr(settings, 'ASTAKOS_QUOTA_HOLDER_URL', '') + +# Set the cloud service properties +SERVICES = getattr(settings, 'ASTAKOS_SERVICES', + {'cyclades': {'url':'https://node1.example.com/ui/', + 'quota': {'vm': 2}}, + 'pithos+': {'url':'https://node2.example.com/ui/', + 'quota': {'diskspace': 50 * 1024 * 1024 * 1024}}}) + +# Set the billing URI +AQUARIUM_URL = getattr(settings, 'ASTAKOS_AQUARIUM_URL', '') + +# Set how many objects should be displayed per page - PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 8) ++PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 8) ++ + # Enforce token renewal on password change/reset + NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True) diff --combined snf-astakos-app/astakos/im/target/local.py index fb3ddaf,25c02cd..df2a092 --- a/snf-astakos-app/astakos/im/target/local.py +++ b/snf-astakos-app/astakos/im/target/local.py @@@ -31,23 -31,27 +31,25 @@@ # interpreted as representing official policies, either expressed # or implied, of GRNET S.A. -from django.http import HttpResponseBadRequest from django.shortcuts import render_to_response from django.template import RequestContext from django.contrib import messages from django.utils.translation import ugettext as _ from django.views.decorators.csrf import csrf_exempt + from django.views.decorators.http import require_http_methods from astakos.im.util import prepare_response, get_query from astakos.im.views import requires_anonymous -from astakos.im.models import AstakosUser from astakos.im.forms import LoginForm from astakos.im.settings import RATELIMIT_RETRIES_ALLOWED from ratelimit.decorators import ratelimit -retries = RATELIMIT_RETRIES_ALLOWED-1 -rate = str(retries)+'/m' +retries = RATELIMIT_RETRIES_ALLOWED - 1 +rate = str(retries) + '/m' + + @require_http_methods(["GET", "POST"]) @csrf_exempt @requires_anonymous @ratelimit(field='username', method='POST', rate=rate) @@@ -56,27 -60,25 +58,27 @@@ def login(request, on_failure='im/login 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', '') if not form.is_valid(): return render_to_response(on_failure, - {'login_form':form, - 'next':next}, + {'login_form': form, + 'next': next}, context_instance=RequestContext(request)) # get the user from the cash user = form.user_cache - + message = None if not user: message = _('Cannot authenticate account') elif not user.is_active: message = _('Inactive account') if message: - messages.add_message(request, messages.ERROR, message) + messages.error(request, message) return render_to_response(on_failure, - {'form':form}, + {'form': form}, context_instance=RequestContext(request)) - + return prepare_response(request, user, next) diff --combined snf-astakos-app/astakos/im/target/redirect.py index ca31fac,08140bb..8e278bc --- a/snf-astakos-app/astakos/im/target/redirect.py +++ b/snf-astakos-app/astakos/im/target/redirect.py @@@ -32,13 -32,17 +32,14 @@@ # or implied, of GRNET S.A. from django.core.urlresolvers import reverse -from django.shortcuts import redirect from django.utils.translation import ugettext as _ -from django.contrib import messages from django.utils.http import urlencode from django.contrib.auth import authenticate from django.http import HttpResponse, HttpResponseBadRequest from django.core.exceptions import ValidationError + from django.views.decorators.http import require_http_methods -from urllib import quote -from urlparse import urlunsplit, urlsplit, urlparse, parse_qsl +from urlparse import urlunsplit, urlsplit, parse_qsl from astakos.im.settings import COOKIE_NAME, COOKIE_DOMAIN from astakos.im.util import set_cookie @@@ -48,7 -52,7 +49,8 @@@ import loggin logger = logging.getLogger(__name__) + + @require_http_methods(["GET", "POST"]) def login(request): """ If there is no ``next`` request parameter redirects to astakos index page @@@ -69,17 -73,17 +71,17 @@@ if request.user.is_authenticated(): # if user has not signed the approval terms # redirect to approval terms with next the request path - if not request.user.signed_terms(): + if not request.user.signed_terms: # first build next parameter parts = list(urlsplit(request.build_absolute_uri())) params = dict(parse_qsl(parts[3], keep_blank_values=True)) # delete force parameter parts[3] = urlencode(params) next = urlunsplit(parts) - + # build url location parts[2] = reverse('latest_terms') - params = {'next':next} + params = {'next': next} parts[3] = urlencode(params) url = urlunsplit(parts) response['Location'] = url @@@ -93,24 -97,19 +95,24 @@@ except ValidationError, e: return HttpResponseBadRequest(e) # authenticate before login - user = authenticate(email=request.user.email, auth_token=request.user.auth_token) + user = authenticate(email=request.user.email, + auth_token=request.user.auth_token + ) auth_login(request, user) set_cookie(response, user) logger.info('Token reset for %s' % request.user.email) parts = list(urlsplit(next)) - parts[3] = urlencode({'user': request.user.email, 'token': request.user.auth_token}) + parts[3] = urlencode({'user': request.user.email, + 'token': request.user.auth_token + } + ) url = urlunsplit(parts) response['Location'] = url response.status_code = 302 return response else: # redirect to login with next the request path - + # first build next parameter parts = list(urlsplit(request.build_absolute_uri())) params = dict(parse_qsl(parts[3], keep_blank_values=True)) @@@ -119,12 -118,12 +121,12 @@@ del params['force'] parts[3] = urlencode(params) next = urlunsplit(parts) - + # build url location - parts[2] = reverse('astakos.im.views.index') - params = {'next':next} + parts[2] = reverse('index') + params = {'next': next} parts[3] = urlencode(params) url = urlunsplit(parts) response['Location'] = url response.status_code = 302 - return response + return response diff --combined snf-astakos-app/astakos/im/target/shibboleth.py index ffa015f,f54b2de..0bb1466 --- a/snf-astakos-app/astakos/im/target/shibboleth.py +++ b/snf-astakos-app/astakos/im/target/shibboleth.py @@@ -1,18 -1,18 +1,18 @@@ # Copyright 2011-2012 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 @@@ -25,7 -25,7 +25,7 @@@ # 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 @@@ -35,17 -35,18 +35,18 @@@ from django.http import HttpResponseBad from django.utils.translation import ugettext as _ from django.contrib import messages from django.template import RequestContext + from django.views.decorators.http import require_http_methods -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 DEFAULT_USER_LEVEL -from astakos.im.models import AstakosUser, Invitation, AdditionalMail +from astakos.im.models import AstakosUser from astakos.im.forms import LoginForm from astakos.im.activation_backends import get_backend, SimpleBackend + class Tokens: # these are mapped by the Shibboleth SP software - SHIB_EPPN = "HTTP_EPPN" # eduPersonPrincipalName + SHIB_EPPN = "HTTP_EPPN" # eduPersonPrincipalName SHIB_NAME = "HTTP_SHIB_INETORGPERSON_GIVENNAME" SHIB_SURNAME = "HTTP_SHIB_PERSON_SURNAME" SHIB_CN = "HTTP_SHIB_PERSON_COMMONNAME" @@@ -54,19 -55,16 +55,20 @@@ SHIB_SESSION_ID = "HTTP_SHIB_SESSION_ID" SHIB_MAIL = "HTTP_SHIB_MAIL" + + @require_http_methods(["GET", "POST"]) @requires_anonymous -def login(request, backend=None, on_login_template='im/login.html', on_creation_template='im/third_party_registration.html', extra_context={}): +def login(request, backend=None, on_login_template='im/login.html', + on_creation_template='im/third_party_registration.html', + extra_context=None + ): tokens = request.META - + try: eppn = tokens[Tokens.SHIB_EPPN] except KeyError: return HttpResponseBadRequest("Missing unique token in request") - + if Tokens.SHIB_DISPLAYNAME in tokens: realname = tokens[Tokens.SHIB_DISPLAYNAME] elif Tokens.SHIB_CN in tokens: @@@ -75,14 -73,12 +77,13 @@@ realname = tokens[Tokens.SHIB_NAME] + ' ' + tokens[Tokens.SHIB_SURNAME] else: return HttpResponseBadRequest("Missing user name in request") - + affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '') email = tokens.get(Tokens.SHIB_MAIL, None) - + try: - user = AstakosUser.objects.get(provider='shibboleth', third_party_identifier=eppn) + user = AstakosUser.objects.get(provider='shibboleth', - third_party_identifier=eppn - ) ++ third_party_identifier=eppn) if user.is_active: return prepare_response(request, user, @@@ -90,9 -86,9 +91,9 @@@ 'renew' in request.GET) else: message = _('Inactive account') - messages.add_message(request, messages.ERROR, message) + messages.error(request, message) return render_response(on_login_template, - login_form = LoginForm(request=request), + login_form=LoginForm(request=request), context_instance=RequestContext(request)) except AstakosUser.DoesNotExist, e: user = AstakosUser(third_party_identifier=eppn, realname=realname, @@@ -101,19 -97,11 +102,15 @@@ try: if not backend: backend = get_backend(request) - form = backend.get_signup_form(provider='shibboleth', instance=user) + form = backend.get_signup_form( + provider='shibboleth', instance=user) except Exception, e: - form = SimpleBackend(request).get_signup_form(provider='shibboleth', instance=user) - messages.add_message(request, messages.ERROR, e) + form = SimpleBackend(request).get_signup_form( + provider='shibboleth', - instance=user - ) ++ instance=user) + messages.error(request, e) return render_response(on_creation_template, - signup_form = form, - provider = 'shibboleth', - context_instance=get_context(request, extra_context)) + signup_form=form, + provider='shibboleth', - context_instance=get_context( - request, - extra_context - ) - ) ++ context_instance=get_context(request, ++ extra_context)) diff --combined snf-astakos-app/astakos/im/urls.py index a62b71b,0bb3edf..715edac --- a/snf-astakos-app/astakos/im/urls.py +++ b/snf-astakos-app/astakos/im/urls.py @@@ -31,126 -31,82 +31,124 @@@ # interpreted as representing official policies, either expressed # or implied, of GRNET S.A. -from django.conf.urls.defaults import patterns, include, url -from django.contrib.auth.views import password_change +from django.conf.urls.defaults import patterns, url - from astakos.im.forms import ExtendedPasswordResetForm, ExtendedPasswordChangeForm, LoginForm + from astakos.im.forms import (ExtendedPasswordResetForm, + ExtendedPasswordChangeForm, + ExtendedSetPasswordForm, LoginForm) from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED, EMAILCHANGE_ENABLED urlpatterns = patterns('astakos.im.views', - url(r'^$', 'index', {}, name='index'), - url(r'^login/?$', 'index', {}, name='login'), - url(r'^profile/?$', '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'^approval_terms/?$', 'approval_terms', {}, name='latest_terms'), - url(r'^approval_terms/(?P\d+)/?$', 'approval_terms'), - url(r'^password/?$', 'change_password', {}, name='password_change'), -) + url(r'^$', 'index', {}, name='index'), + url(r'^login/?$', 'index', {}, name='login'), + 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\d+)/?$', + 'approval_terms'), + url(r'^password/?$', + 'change_password', {}, name='password_change'), + url(r'^resources/?$', + 'resource_list', {}, name='resource_list'), + url(r'^billing/?$', 'billing', {}, name='billing'), + url(r'^timeline/?$', 'timeline', {}, name='timeline'), + url(r'^group/add/(?P\w+)?$', + 'group_add', {}, name='group_add'), + url(r'^group/list/?$', + 'group_list', {}, name='group_list'), + url(r'^group/(?P\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\d+)/join/?$', 'group_join', + {}, name='group_join'), + url(r'^group/(?P\d+)/leave/?$', 'group_leave', + {}, name='group_leave'), + url(r'^group/(?P\d+)/(?P\d+)/approve/?$', + 'approve_member', {}, name='approve_member'), + url(r'^group/(?P\d+)/(?P\d+)/disapprove/?$', + 'disapprove_member', {}, name='disapprove_member'), + url(r'^group/create/?$', 'group_create_list', {}, + name='group_create_list'), + ) if EMAILCHANGE_ENABLED: urlpatterns += patterns('astakos.im.views', - url(r'^email_change/?$', 'change_email', {}, name='email_change'), - url(r'^email_change/confirm/(?P\w+)/', 'change_email', {}, - name='email_change_confirm') -) - + url(r'^email_change/?$', + 'change_email', {}, name='email_change'), + url( + r'^email_change/confirm/(?P\w+)/', 'change_email', {}, + name='email_change_confirm') + ) + urlpatterns += patterns('astakos.im.target', - url(r'^login/redirect/?$', 'redirect.login') -) + url(r'^login/redirect/?$', 'redirect.login') + ) if 'local' in IM_MODULES: urlpatterns += patterns('astakos.im.target', - url(r'^local/?$', 'local.login') - ) + url(r'^local/?$', 'local.login') + ) urlpatterns += patterns('django.contrib.auth.views', - url(r'^local/password_reset/?$', 'password_reset', - {'email_template_name': 'registration/password_email.txt', - 'password_reset_form': ExtendedPasswordResetForm}), - url(r'^local/password_reset_done/?$', - 'password_reset_done'), - url( - r'^local/reset/confirm/(?P[0-9A-Za-z]+)-(?P.+)/?$', - 'password_reset_confirm'), - url(r'^local/password/reset/complete/?$', - 'password_reset_complete'), - url( - r'^password_change/?$', 'password_change', {'post_change_redirect': 'profile', - 'password_change_form': ExtendedPasswordChangeForm}) - ) + url(r'^local/password_reset/?$', 'password_reset', + {'email_template_name':'registration/password_email.txt', + 'password_reset_form':ExtendedPasswordResetForm}), + url(r'^local/password_reset_done/?$', 'password_reset_done'), + url(r'^local/reset/confirm/(?P[0-9A-Za-z]+)-(?P.+)/?$', + 'password_reset_confirm', {'set_password_form':ExtendedSetPasswordForm}), + url(r'^local/password/reset/complete/?$', 'password_reset_complete'), + url(r'^password_change/?$', 'password_change', {'post_change_redirect':'profile', + 'password_change_form':ExtendedPasswordChangeForm}) + ) if INVITATIONS_ENABLED: urlpatterns += patterns('astakos.im.views', - url(r'^invite/?$', 'invite') - ) + url(r'^invite/?$', 'invite', {}, name='invite') + ) if 'shibboleth' in IM_MODULES: urlpatterns += patterns('astakos.im.target', - url(r'^login/shibboleth/?$', 'shibboleth.login') - ) + url(r'^login/shibboleth/?$', 'shibboleth.login') + ) if 'twitter' in IM_MODULES: urlpatterns += patterns('astakos.im.target', - url(r'^login/twitter/?$', 'twitter.login'), - url(r'^login/twitter/authenticated/?$', 'twitter.authenticated') - ) + url(r'^login/twitter/?$', 'twitter.login'), + url(r'^login/twitter/authenticated/?$', + 'twitter.authenticated') + ) + +urlpatterns += patterns('astakos.im.api', + url(r'^get_services/?$', 'get_services'), + url(r'^get_menu/?$', 'get_menu'), + ) urlpatterns += patterns('astakos.im.api.admin', - url(r'^authenticate/?$', 'authenticate_old'), - #url(r'^authenticate/v2/?$', 'authenticate'), - url(r'^get_services/?$', 'get_services'), - url(r'^get_menu/?$', 'get_menu'), - url(r'^admin/api/v2.0/users/?$', 'get_user_by_email'), - url(r'^admin/api/v2.0/users/(?P.+?)/?$', 'get_user_by_username'), -) + url(r'^authenticate/?$', 'authenticate_old'), + #url(r'^authenticate/v2/?$', 'authenticate'), + url(r'^admin/api/v2.0/users/?$', 'get_user_by_email'), + url(r'^admin/api/v2.0/users/(?P.+?)/?$', + 'get_user_by_username'), + ) urlpatterns += patterns('astakos.im.api.service', - #url(r'^service/api/v2.0/tokens/(?P.+?)/?$', 'validate_token'), - url(r'^service/api/v2.0/feedback/?$', 'send_feedback'), - url(r'^service/api/v2.0/users/?$', 'get_user_by_email'), - url(r'^service/api/v2.0/users/(?P.+?)/?$', 'get_user_by_username'), -) + #url(r'^service/api/v2.0/tokens/(?P.+?)/?$', 'validate_token'), + url(r'^service/api/v2.0/feedback/?$', 'send_feedback'), + url(r'^service/api/v2.0/users/?$', + 'get_user_by_email'), + url(r'^service/api/v2.0/users/(?P.+?)/?$', + 'get_user_by_username'), + ) diff --combined snf-astakos-app/astakos/im/util.py index 9ade1af,926ec3c..44333cb --- a/snf-astakos-app/astakos/im/util.py +++ b/snf-astakos-app/astakos/im/util.py @@@ -1,18 -1,18 +1,18 @@@ # Copyright 2011-2012 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 @@@ -25,7 -25,7 +25,7 @@@ # 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 @@@ -36,6 -36,7 +36,6 @@@ import datetim import time from urllib import quote -from urlparse import urlsplit, urlunsplit from datetime import tzinfo, timedelta from django.http import HttpResponse, HttpResponseBadRequest, urlencode @@@ -45,45 -46,41 +45,45 @@@ from django.contrib.auth import authent from django.core.urlresolvers import reverse from django.core.exceptions import ValidationError -from astakos.im.models import AstakosUser, Invitation, ApprovalTerms -from astakos.im.settings import INVITATIONS_PER_LEVEL, COOKIE_NAME, \ +from astakos.im.models import AstakosUser, Invitation +from astakos.im.settings import COOKIE_NAME, \ COOKIE_DOMAIN, COOKIE_SECURE, FORCE_PROFILE_UPDATE, LOGGING_LEVEL from astakos.im.functions import login logger = logging.getLogger(__name__) + class UTC(tzinfo): - def utcoffset(self, dt): - return timedelta(0) + def utcoffset(self, dt): + return timedelta(0) + + def tzname(self, dt): + return 'UTC' - def tzname(self, dt): - return 'UTC' + def dst(self, dt): + return timedelta(0) - def dst(self, dt): - return timedelta(0) def isoformat(d): - """Return an ISO8601 date string that includes a timezone.""" + """Return an ISO8601 date string that includes a timezone.""" + + return d.replace(tzinfo=UTC()).isoformat() - return d.replace(tzinfo=UTC()).isoformat() def epoch(datetime): - return int(time.mktime(datetime.timetuple())*1000) + return int(time.mktime(datetime.timetuple()) * 1000) -def get_context(request, extra_context={}, **kwargs): - if not extra_context: - extra_context = {} + +def get_context(request, extra_context=None, **kwargs): + extra_context = extra_context or {} extra_context.update(kwargs) return RequestContext(request, extra_context) + def get_invitation(request): """ Returns the invitation identified by the ``code``. - + Raises ValueError if the invitation is consumed or there is another account associated with this email. """ @@@ -92,91 -89,81 +92,91 @@@ code = request.POST.get('code') if not code: return - invitation = Invitation.objects.get(code = code) + invitation = Invitation.objects.get(code=code) if invitation.is_consumed: raise ValueError(_('Invitation is used')) if reserved_email(invitation.username): raise ValueError(_('Email: %s is reserved' % invitation.username)) return invitation + def prepare_response(request, user, next='', renew=False): """Return the unique username and the token as 'X-Auth-User' and 'X-Auth-Token' headers, or redirect to the URL provided in 'next' with the 'user' and 'token' as parameters. - + Reissue the token even if it has not yet expired, if the 'renew' parameter is present or user has not a valid token. """ renew = renew or (not user.auth_token) - renew = renew or (user.auth_token_expires and user.auth_token_expires < datetime.datetime.now()) + renew = renew or (user.auth_token_expires < datetime.datetime.now()) if renew: user.renew_token() try: user.save() except ValidationError, e: - return HttpResponseBadRequest(e) - + return HttpResponseBadRequest(e) + if FORCE_PROFILE_UPDATE and not user.is_verified and not user.is_superuser: params = '' if next: params = '?' + urlencode({'next': next}) - next = reverse('astakos.im.views.edit_profile') + params - + next = reverse('edit_profile') + params + response = HttpResponse() - + # authenticate before login user = authenticate(email=user.email, auth_token=user.auth_token) login(request, user) set_cookie(response, user) request.session.set_expiry(user.auth_token_expires) - + if not next: - next = reverse('astakos.im.views.index') - + next = reverse('index') + response['Location'] = next response.status_code = 302 return response + def set_cookie(response, user): expire_fmt = user.auth_token_expires.strftime('%a, %d-%b-%Y %H:%M:%S %Z') cookie_value = quote(user.email + '|' + user.auth_token) response.set_cookie(COOKIE_NAME, value=cookie_value, expires=expire_fmt, path='/', - domain=COOKIE_DOMAIN, secure=COOKIE_SECURE) - msg = 'Cookie [expiring %s] set for %s' % (user.auth_token_expires, user.email) - logger._log(LOGGING_LEVEL, msg, []) + domain=COOKIE_DOMAIN, secure=COOKIE_SECURE + ) + msg = 'Cookie [expiring %s] set for %s' % ( + user.auth_token_expires, + user.email + ) + logger.log(LOGGING_LEVEL, msg) + class lazy_string(object): def __init__(self, function, *args, **kwargs): - self.function=function - self.args=args - self.kwargs=kwargs - + self.function = function + self.args = args + self.kwargs = kwargs + def __str__(self): if not hasattr(self, 'str'): - self.str=self.function(*self.args, **self.kwargs) + self.str = self.function(*self.args, **self.kwargs) return self.str + def reverse_lazy(*args, **kwargs): return lazy_string(reverse, *args, **kwargs) + def reserved_email(email): - return AstakosUser.objects.filter(email = email).count() != 0 + return AstakosUser.objects.filter(email__iexact=email).count() != 0 + def get_query(request): try: return request.__getattribute__(request.method) except AttributeError: - return request.GET + return {} diff --combined snf-astakos-app/astakos/im/views.py index 5f78142,f12e356..b687373 --- a/snf-astakos-app/astakos/im/views.py +++ b/snf-astakos-app/astakos/im/views.py @@@ -32,65 -32,40 +32,68 @@@ # or implied, of GRNET S.A. import logging -import socket +import calendar -from smtplib import SMTPException from urllib import quote from functools import wraps +from datetime import datetime, timedelta +from collections import defaultdict -from django.core.mail import send_mail -from django.http import HttpResponse, HttpResponseBadRequest -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.db import transaction -from django.utils.http import urlencode -from django.http import HttpResponseRedirect, HttpResponseBadRequest -from django.db.utils import IntegrityError +from django.contrib.auth.decorators import login_required from django.contrib.auth.views import password_change -from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse +from django.db import transaction from django.db.models import Q ++<<<<<<< HEAD +from django.db.utils import IntegrityError +from django.forms.fields import URLField +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 (create_object, delete_object, + get_model_and_form_class) +from django.views.generic.list_detail import object_list, object_detail +from django.http import HttpResponseBadRequest +from django.core.xheaders import populate_xheaders + +from astakos.im.models import ( + AstakosUser, ApprovalTerms, AstakosGroup, Resource, + EmailChange, GroupKind, Membership, AstakosGroupQuota) + from django.views.decorators.http import require_http_methods + -from astakos.im.models import AstakosUser, Invitation, ApprovalTerms from astakos.im.activation_backends import get_backend, SimpleBackend from astakos.im.util import get_context, prepare_response, set_cookie, get_query -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, switch_account_to_shibboleth -from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL +from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm, + FeedbackForm, SignApprovalTermsForm, + ExtendedPasswordChangeForm, EmailChangeForm, + AstakosGroupCreationForm, AstakosGroupSearchForm, + AstakosGroupUpdateForm, AddGroupMembersForm, + AstakosGroupSortForm, MembersSortForm, + TimelineForm, PickResourceForm) +from astakos.im.functions import (send_feedback, SendMailError, + invite as invite_func, logout as auth_logout, + activate as activate_func, + switch_account_to_shibboleth, + send_admin_notification, + SendNotificationError) +from astakos.im.endpoints.quotaholder import timeline_charge +from astakos.im.settings import ( + COOKIE_NAME, COOKIE_DOMAIN, SITENAME, LOGOUT_NEXT, + LOGGING_LEVEL, PAGINATE_BY) +from astakos.im.tasks import request_billing logger = logging.getLogger(__name__) -def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs): + +DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''), + 'https://', '')""" + +def render_response(template, tab=None, status=200, reset_cookie=False, + context_instance=None, **kwargs): """ Calls ``django.template.loader.render_to_string`` with an additional ``tab`` keyword argument and returns an ``django.http.HttpResponse`` with the @@@ -99,8 -74,7 +102,8 @@@ if tab is None: tab = template.partition('_')[0].partition('.html')[0] kwargs.setdefault('tab', tab) - html = render_to_string(template, kwargs, context_instance=context_instance) + html = template_loader.render_to_string( + template, kwargs, context_instance=context_instance) response = HttpResponse(html, status=status) if reset_cookie: set_cookie(response, context_instance['request'].user) @@@ -121,7 -95,6 +124,7 @@@ def requires_anonymous(func) return func(request, *args) return wrapper + def signed_terms_required(func): """ Decorator checkes whether the request.user is Anonymous and in that case @@@ -129,17 -102,17 +132,18 @@@ """ @wraps(func) def wrapper(request, *args, **kwargs): - if request.user.is_authenticated() and not request.user.signed_terms(): + if request.user.is_authenticated() and not request.user.signed_terms: params = urlencode({'next': request.build_absolute_uri(), - 'show_form':''}) + 'show_form': ''}) terms_uri = reverse('latest_terms') + '?' + params return HttpResponseRedirect(terms_uri) return func(request, *args, **kwargs) return wrapper + + @require_http_methods(["GET", "POST"]) @signed_terms_required -def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}): +def index(request, login_template_name='im/login.html', extra_context=None): """ If there is logged on user renders the profile page otherwise renders login page. @@@ -163,16 -136,16 +167,17 @@@ """ template_name = login_template_name if request.user.is_authenticated(): - return HttpResponseRedirect(reverse('astakos.im.views.edit_profile')) + return HttpResponseRedirect(reverse('edit_profile')) return render_response(template_name, - login_form = LoginForm(request=request), - context_instance = get_context(request, extra_context)) + login_form=LoginForm(request=request), + context_instance=get_context(request, extra_context)) + + @require_http_methods(["GET", "POST"]) @login_required @signed_terms_required @transaction.commit_manually -def invite(request, template_name='im/invitations.html', extra_context={}): +def invite(request, template_name='im/invitations.html', extra_context=None): """ Allows a user to invite somebody else. @@@ -204,11 -177,12 +209,11 @@@ * LOGIN_URL: login uri * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email - * ASTAKOS_DEFAULT_FROM_EMAIL: from email """ status = None message = None form = InvitationForm() - + inviter = request.user if request.method == 'POST': form = InvitationForm(request.POST) @@@ -217,38 -191,39 +222,39 @@@ try: invitation = form.save() invite_func(invitation, inviter) - status = messages.SUCCESS message = _('Invitation sent to %s' % invitation.username) + messages.success(request, message) except SendMailError, e: - status = messages.ERROR message = e.message + messages.error(request, message) transaction.rollback() except BaseException, e: - status = messages.ERROR message = _('Something went wrong.') + messages.error(request, message) logger.exception(e) transaction.rollback() else: transaction.commit() else: - status = messages.ERROR message = _('No invitations left') - messages.add_message(request, status, message) + messages.error(request, message) sent = [{'email': inv.username, 'realname': inv.realname, 'is_consumed': inv.is_consumed} - for inv in request.user.invitations_sent.all()] + for inv in request.user.invitations_sent.all()] kwargs = {'inviter': inviter, - 'sent':sent} + 'sent': sent} context = get_context(request, extra_context, **kwargs) return render_response(template_name, - invitation_form = form, - context_instance = context) + invitation_form=form, + context_instance=context) + + @require_http_methods(["GET", "POST"]) @login_required @signed_terms_required -def edit_profile(request, template_name='im/profile.html', extra_context={}): +def edit_profile(request, template_name='im/profile.html', extra_context=None): """ Allows a user to edit his/her profile. @@@ -277,7 -252,6 +283,7 @@@ * LOGIN_URL: login uri """ + extra_context = extra_context or {} form = ProfileForm(instance=request.user) extra_context['next'] = request.GET.get('next') reset_cookie = False @@@ -292,23 -266,21 +298,24 @@@ next = request.POST.get('next') if next: return redirect(next) - msg = _('

Profile has been updated successfully

') - messages.add_message(request, messages.SUCCESS, msg) + msg = _('Profile has been updated successfully') + messages.success(request, msg) except ValueError, ve: - messages.add_message(request, messages.ERROR, ve) + messages.success(request, ve) elif request.method == "GET": - request.user.is_verified = True - request.user.save() + if not request.user.is_verified: + request.user.is_verified = True + request.user.save() return render_response(template_name, - reset_cookie = reset_cookie, - profile_form = form, - context_instance = get_context(request, - extra_context)) + reset_cookie=reset_cookie, + profile_form=form, + context_instance=get_context(request, + extra_context)) + +@transaction.commit_manually + @require_http_methods(["GET", "POST"]) -def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None): +def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None): """ Allows a user to create a local account. @@@ -319,14 -291,14 +326,14 @@@ if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend`` if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not (see activation_backends); - + Upon successful user creation, if ``next`` url parameter is present the user is redirected there otherwise renders the same page with a success message. - + On unsuccessful creation, renders ``template_name`` with an error message. - + **Arguments** - + ``template_name`` A custom template to render. This is optional; if not specified, this will default to ``im/signup.html``. @@@ -339,13 -311,13 +346,13 @@@ An dictionary of variables to add to the template context. **Template:** - + im/signup.html or ``template_name`` keyword argument. - im/signup_complete.html or ``on_success`` keyword argument. + im/signup_complete.html or ``on_success`` keyword argument. """ 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') try: if not backend: @@@ -353,7 -325,7 +360,7 @@@ form = backend.get_signup_form(provider) except Exception, e: form = SimpleBackend(request).get_signup_form(provider) - messages.add_message(request, messages.ERROR, e) + messages.error(request, e) if request.method == 'POST': if form.is_valid(): user = form.save(commit=False) @@@ -366,35 -338,32 +373,36 @@@ additional_email = form.cleaned_data['additional_email'] if additional_email != user.email: user.additionalmail_set.create(email=additional_email) - msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email) - logger._log(LOGGING_LEVEL, msg, []) + msg = 'Additional email: %s saved for user %s.' % ( + additional_email, user.email) + logger.log(LOGGING_LEVEL, msg) if user and user.is_active: next = request.POST.get('next', '') + transaction.commit() return prepare_response(request, user, next=next) messages.add_message(request, status, message) + transaction.commit() return render_response(on_success, context_instance=get_context(request, extra_context)) except SendMailError, e: - status = messages.ERROR message = e.message - messages.add_message(request, status, message) + messages.error(request, message) + transaction.rollback() except BaseException, e: - status = messages.ERROR message = _('Something went wrong.') - messages.add_message(request, status, message) + messages.error(request, message) logger.exception(e) + transaction.rollback() return render_response(template_name, - signup_form = form, - provider = provider, + signup_form=form, + provider=provider, context_instance=get_context(request, extra_context)) + + @require_http_methods(["GET", "POST"]) @login_required @signed_terms_required -def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}): +def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None): """ Allows a user to send feedback. @@@ -434,17 -403,18 +442,18 @@@ try: send_feedback(msg, data, request.user, email_template_name) except SendMailError, e: - message = e.message - status = messages.ERROR + messages.error(request, message) else: message = _('Feedback successfully sent') - status = messages.SUCCESS - messages.add_message(request, status, message) + messages.success(request, message) return render_response(template_name, - feedback_form = form, - context_instance = get_context(request, extra_context)) + feedback_form=form, + context_instance=get_context(request, extra_context)) + + @require_http_methods(["GET", "POST"]) -def logout(request, template='registration/logged_out.html', extra_context={}): +@signed_terms_required +def logout(request, template='registration/logged_out.html', extra_context=None): """ Wraps `django.contrib.auth.logout` and delete the cookie. """ @@@ -454,7 -424,7 +463,7 @@@ auth_logout(request) response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN) msg = 'Cookie deleted for %s' % email - logger._log(LOGGING_LEVEL, msg, []) + logger.log(LOGGING_LEVEL, msg) next = request.GET.get('next') if next: response['Location'] = next @@@ -464,15 -434,14 +473,16 @@@ response['Location'] = LOGOUT_NEXT response.status_code = 301 return response - messages.add_message(request, messages.SUCCESS, _('

You have successfully logged out.

')) + messages.success(request, _('You have successfully logged out.')) context = get_context(request, extra_context) - response.write(render_to_string(template, context_instance=context)) + response.write(template_loader.render_to_string(template, context_instance=context)) return response + + @require_http_methods(["GET", "POST"]) @transaction.commit_manually -def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'): +def activate(request, greeting_email_template_name='im/welcome_email.txt', + helpdesk_email_template_name='im/helpdesk_notification.txt'): """ Activates the user identified by the ``auth`` request parameter, sends a welcome email and renews the user token. @@@ -486,64 -455,53 +496,65 @@@ user = AstakosUser.objects.get(auth_token=token) except AstakosUser.DoesNotExist: return HttpResponseBadRequest(_('No such user')) - + if user.is_active: message = _('Account already active.') - messages.add_message(request, messages.ERROR, message) + messages.error(request, message) return index(request) - + try: - local_user = AstakosUser.objects.get(~Q(id = user.id), email=user.email, is_active=True) + local_user = AstakosUser.objects.get( + ~Q(id=user.id), + email=user.email, + is_active=True + ) except AstakosUser.DoesNotExist: try: - activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True) + activate_func( + user, + greeting_email_template_name, + helpdesk_email_template_name, + verify_email=True + ) response = prepare_response(request, user, next, renew=True) transaction.commit() return response except SendMailError, e: message = e.message - messages.add_message(request, messages.ERROR, message) + messages.error(request, message) transaction.rollback() return index(request) except BaseException, e: - status = messages.ERROR message = _('Something went wrong.') - messages.add_message(request, messages.ERROR, message) + messages.error(request, message) logger.exception(e) transaction.rollback() return index(request) else: try: - user = switch_account_to_shibboleth(user, local_user, greeting_email_template_name) + user = switch_account_to_shibboleth( + user, + local_user, + greeting_email_template_name + ) response = prepare_response(request, user, next, renew=True) transaction.commit() return response except SendMailError, e: message = e.message - messages.add_message(request, messages.ERROR, message) + messages.error(request, message) transaction.rollback() return index(request) except BaseException, e: - status = messages.ERROR message = _('Something went wrong.') - messages.add_message(request, messages.ERROR, message) + messages.error(request, message) logger.exception(e) transaction.rollback() return index(request) + + @require_http_methods(["GET", "POST"]) -def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}): +def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None): term = None terms = None if not term_id: @@@ -553,594 -511,91 +564,595 @@@ pass else: try: - term = ApprovalTerms.objects.get(id=term_id) - except ApprovalTermDoesNotExist, e: + term = ApprovalTerms.objects.get(id=term_id) + except ApprovalTerms.DoesNotExist, e: pass if not term: - return HttpResponseRedirect(reverse('astakos.im.views.index')) + return HttpResponseRedirect(reverse('index')) f = open(term.location, 'r') terms = f.read() if request.method == 'POST': next = request.POST.get('next') if not next: - next = reverse('astakos.im.views.index') + next = reverse('index') form = SignApprovalTermsForm(request.POST, instance=request.user) if not form.is_valid(): return render_response(template_name, - terms = terms, - approval_terms_form = form, - context_instance = get_context(request, extra_context)) + terms=terms, + approval_terms_form=form, + context_instance=get_context(request, extra_context)) user = form.save() return HttpResponseRedirect(next) else: form = None - if request.user.is_authenticated() and not request.user.signed_terms(): + if request.user.is_authenticated() and not request.user.signed_terms: form = SignApprovalTermsForm(instance=request.user) return render_response(template_name, - terms = terms, - approval_terms_form = form, - context_instance = get_context(request, extra_context)) + terms=terms, + approval_terms_form=form, + context_instance=get_context(request, extra_context)) + + @require_http_methods(["GET", "POST"]) @signed_terms_required def change_password(request): return password_change(request, - post_change_redirect=reverse('astakos.im.views.edit_profile'), - password_change_form=ExtendedPasswordChangeForm) + post_change_redirect=reverse('edit_profile'), + password_change_form=ExtendedPasswordChangeForm) - - @signed_terms_required + @require_http_methods(["GET", "POST"]) @login_required + @signed_terms_required @transaction.commit_manually def change_email(request, activation_key=None, email_template_name='registration/email_change_email.txt', form_template_name='registration/email_change_form.html', confirm_template_name='registration/email_change_done.html', - extra_context={}): + extra_context=None): if activation_key: try: user = EmailChange.objects.change_email(activation_key) if request.user.is_authenticated() and request.user == user: msg = _('Email changed successfully.') - messages.add_message(request, messages.SUCCESS, msg) + messages.success(request, msg) auth_logout(request) response = prepare_response(request, user) transaction.commit() return response except ValueError, e: - messages.add_message(request, messages.ERROR, e) + messages.error(request, e) return render_response(confirm_template_name, - modified_user = user if 'user' in locals() else None, - context_instance = get_context(request, - extra_context)) - + modified_user=user if 'user' in locals( + ) else None, + context_instance=get_context(request, + extra_context)) + if not request.user.is_authenticated(): path = quote(request.get_full_path()) - url = request.build_absolute_uri(reverse('astakos.im.views.index')) + url = request.build_absolute_uri(reverse('index')) return HttpResponseRedirect(url + '?next=' + path) form = EmailChangeForm(request.POST or None) if request.method == 'POST' and form.is_valid(): try: ec = form.save(email_template_name, request) except SendMailError, e: - status = messages.ERROR msg = e + messages.error(request, msg) transaction.rollback() except IntegrityError, e: - status = messages.ERROR msg = _('There is already a pending change email request.') + messages.error(request, msg) else: - status = messages.SUCCESS msg = _('Change email request has been registered succefully.\ You are going to receive a verification email in the new address.') + messages.success(request, msg) transaction.commit() - messages.add_message(request, status, msg) return render_response(form_template_name, - form = form, - context_instance = get_context(request, - extra_context)) + form=form, + context_instance=get_context(request, + extra_context)) + + +@signed_terms_required +@login_required +def group_add(request, kind_name='default'): + try: + kind = GroupKind.objects.get(name=kind_name) + except: + return HttpResponseBadRequest(_('No such group kind')) + + post_save_redirect = '/im/group/%(id)s/' + context_processors = None + model, form_class = get_model_and_form_class( + model=None, + form_class=AstakosGroupCreationForm + ) + resources = dict( + (str(r.id), r) for r in Resource.objects.select_related().all()) + policies = [] + if request.method == 'POST': + form = form_class(request.POST, request.FILES, resources=resources) + if form.is_valid(): + new_object = form.save() + + # save owner + new_object.owners = [request.user] + + # save quota policies + for (rid, uplimit) in form.resources(): + try: + r = resources[rid] + except KeyError, e: + logger.exception(e) + # TODO Should I stay or should I go??? + continue + else: + new_object.astakosgroupquota_set.create( + resource=r, + uplimit=uplimit + ) + policies.append('%s %d' % (r, uplimit)) + msg = _("The %(verbose_name)s was created successfully.") %\ + {"verbose_name": model._meta.verbose_name} + messages.success(request, msg, fail_silently=True) + + # send notification + try: + send_admin_notification( + template_name='im/group_creation_notification.txt', + dictionary={ + 'group': new_object, + 'owner': request.user, + 'policies': policies, + }, + subject='%s alpha2 testing group creation notification' % SITENAME + ) + except SendNotificationError, e: + messages.error(request, e, fail_silently=True) + return HttpResponseRedirect(post_save_redirect % new_object.__dict__) + else: + now = datetime.now() + data = { + 'kind': kind + } + form = form_class(data, resources=resources) + + # 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, + }, context_processors) + return HttpResponse(t.render(c)) + + +@signed_terms_required +@login_required +def group_list(request): + none = request.user.astakos_groups.none() + q = AstakosGroup.objects.raw(""" + 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 + """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id)) + d = defaultdict(list) + for g in q: + if request.user.email == g.groupowner: + d['own'].append(g) + else: + d['other'].append(g) + + # validate sorting + fields = ('own', 'other') + for f in fields: + v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f) + if v: + form = AstakosGroupSortForm({'sort_by': v}) + if not form.is_valid(): + globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by') + return object_list(request, queryset=none, + extra_context={'is_search':False, + 'mine': d['own'], + 'other': d['other'], + 'own_sorting': own_sorting, + 'other_sorting': other_sorting, + 'own_page': request.GET.get('own_page', 1), + 'other_page': request.GET.get('other_page', 1) + }) + + +@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(): + map(obj.approve_member, addmembers_form.valid_users) + 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') + + extra_context = {'update_form': update_form, + 'addmembers_form': addmembers_form, + 'page': request.GET.get('page', 1), + 'sorting': sorting} + 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 + + +@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}) + if sorting: + # TODO check sorting value + queryset = queryset.order_by(sorting) + else: + queryset = AstakosGroup.objects.none() + return object_list( + request, + queryset, + paginate_by=PAGINATE_BY, + 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)) + +@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}) + sorting = request.GET.get('sorting') + if sorting: + # TODO check sorting value + q = q.order_by(sorting) + return object_list( + request, + q, + paginate_by=PAGINATE_BY, + page=request.GET.get('page') or 1, + template_name='im/astakosgroup_list.html', + extra_context=dict(form=AstakosGroupSearchForm(), + is_search=True, + sorting=sorting)) + + +@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 = _('Failed to join group.') + messages.error(request, msg) + return group_search(request) + + +@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(_('Invalid membership.')) + if request.user in m.group.owner.all(): + return HttpResponseForbidden(_('Owner can not leave the 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(_('Invalid membership.')) + else: + if request.user not in m.group.owner.all(): + return HttpResponseForbidden(_('User is not a group owner.')) + func(request, m) + return group_detail(request, group_id) + return wrapper + + +@signed_terms_required +@login_required +@handle_membership +def approve_member(request, membership): + try: + membership.approve() + realname = membership.person.realname + msg = _('%s has been successfully joined the group.' % realname) + messages.success(request, msg) + except BaseException, e: + logger.exception(e) + realname = membership.person.realname + msg = _('Something went wrong during %s\'s approval.' % realname) + messages.error(request, msg) + + +@signed_terms_required +@login_required +@handle_membership +def disapprove_member(request, membership): + try: + membership.disapprove() + realname = membership.person.realname + msg = _('%s has been successfully removed from the group.' % realname) + messages.success(request, msg) + except BaseException, e: + logger.exception(e) + msg = _('Something went wrong during %s\'s disapproval.' % realname) + messages.error(request, msg) + + +@signed_terms_required +@login_required +def resource_list(request): + if request.method == 'POST': + form = PickResourceForm(request.POST) + if form.is_valid(): + r = form.cleaned_data.get('resource') + if r: + groups = request.user.membership_set.only('group').filter( + date_joined__isnull=False) + groups = [g.group_id for g in groups] + q = AstakosGroupQuota.objects.select_related().filter( + resource=r, group__in=groups) + else: + form = PickResourceForm() + q = AstakosGroupQuota.objects.none() + return object_list(request, q, + template_name='im/astakosuserquota_list.html', + extra_context={'form': form}) + + +def group_create_list(request): + form = PickResourceForm() + return render_response( + template='im/astakosgroup_create_list.html', + context_instance=get_context(request),) + + +@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, _('Service response status: %d' % status)) + except: + messages.error(request, r.result) + + print type(start) + + 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 + +@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 diff --combined snf-astakos-app/conf/20-snf-astakos-app-settings.conf index 3e9bc1e,b0f2291..be71cf7 --- a/snf-astakos-app/conf/20-snf-astakos-app-settings.conf +++ b/snf-astakos-app/conf/20-snf-astakos-app-settings.conf @@@ -100,7 -100,7 +100,7 @@@ #from logging import INFO #ASTAKOS_LOGGING_LEVEL = INFO -# Email subjects configuration. For admin/helper notification emails %(user)s +# Email subjects configuration. For admin/helper notification emails %(user)s # maps to registered/activated user email. #ASTAKOS_INVITATION_EMAIL_SUBJECT = 'Invitation to %s alpha2 testing' % SITENAME #ASTAKOS_GREETING_EMAIL_SUBJECT = 'Welcome to %s alpha2 testing' % SITENAME @@@ -111,18 -111,5 +111,21 @@@ #ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT = 'Email change on %s alpha2 testing' % SITENAME #ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT = 'Password reset on %s alpha2 testing' % SITENAME +# Set the quota holder component URI +#ASTAKOS_QUOTA_HOLDER_URL = '' + +# Set the cloud service properties +# SERVICES = getattr(settings, 'ASTAKOS_SERVICES', +# {'cyclades': {'url':'https://node1.example.com/ui/', +# 'quota': {'vm': 2}}, +# 'pithos+': {'url':'https://node2.example.com/ui/', +# 'quota': {'diskspace': 50 * 1024 * 1024 * 1024}}}) + +# Set the billing URI +#ASTAKOS_AQUARIUM_URL = '' + +# Set how many objects should be displayed per page - #PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 10) ++#PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 10) ++ + # Enforce token renewal on password change/reset + NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)