# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import re
import synnefo.util.date as date_util
from datetime import datetime
from django import forms
from django.utils.translation import ugettext as _
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \
PasswordResetForm, PasswordChangeForm, SetPasswordForm
from django.core.mail import send_mail, get_connection
from django.contrib.auth.tokens import default_token_generator
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from astakos.im import transaction
from django.core import validators
from synnefo.util import units
from synnefo_branding.utils import render_to_string
from synnefo.lib import join_urls
from astakos.im.fields import EmailField, InfiniteChoiceField
from astakos.im.models import AstakosUser, EmailChange, Invitation, Resource, \
PendingThirdPartyUser, get_latest_terms, ProjectApplication, Project
from astakos.im import presentation
from astakos.im.widgets import DummyWidget, RecaptchaWidget
from astakos.im.functions import submit_application, \
accept_membership_project_checks, ProjectError
from astakos.im.user_utils import change_user_email
from astakos.im.util import reserved_verified_email, model_to_dict
from astakos.im import auth_providers
from astakos.im import settings
from astakos.im import auth
from astakos.im.auth_backends import LDAPBackend
import astakos.im.messages as astakos_messages
import logging
import recaptcha.client.captcha as captcha
import re
logger = logging.getLogger(__name__)
BASE_PROJECT_NAME_REGEX = re.compile(
r'^system:[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-'
'[a-f0-9]{12}$')
DOMAIN_VALUE_REGEX = re.compile(
r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
re.IGNORECASE)
READ_ONLY_FIELD_MSG = ("This value is provided by your authentication provider"
" and cannot be changed.")
class LocalUserCreationForm(UserCreationForm):
"""
Extends the built in UserCreationForm in several ways:
* Adds email, first_name, last_name, recaptcha_challenge_field,
* recaptcha_response_field field.
* The username field isn't visible and it is assigned a generated id.
* User created is not active.
"""
recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
recaptcha_response_field = forms.CharField(
widget=RecaptchaWidget, label='')
email = EmailField()
class Meta:
model = AstakosUser
fields = ("email", "first_name", "last_name",
"has_signed_terms", "has_signed_terms")
def __init__(self, *args, **kwargs):
"""
Changes the order of fields, and removes the username field.
"""
request = kwargs.pop('request', None)
provider = kwargs.pop('provider', 'local')
# we only use LocalUserCreationForm for local provider
if not provider == 'local':
raise Exception('Invalid provider')
self.ip = None
if request:
self.ip = request.META.get('REMOTE_ADDR',
request.META.get('HTTP_X_REAL_IP',
None))
super(LocalUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email', 'first_name', 'last_name',
'password1', 'password2']
if settings.RECAPTCHA_ENABLED:
self.fields.keyOrder.extend(['recaptcha_challenge_field',
'recaptcha_response_field', ])
if get_latest_terms():
self.fields.keyOrder.append('has_signed_terms')
if 'has_signed_terms' in self.fields:
# Overriding field label since we need to apply a link
# to the terms within the label
terms_link_html = '%s' \
% (reverse('latest_terms'), _("the terms"))
self.fields['has_signed_terms'].label = \
mark_safe("I agree with %s" % terms_link_html)
def clean_email(self):
email = self.cleaned_data['email']
if not email:
raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
if reserved_verified_email(email):
raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
return email
def clean_has_signed_terms(self):
has_signed_terms = self.cleaned_data['has_signed_terms']
if not has_signed_terms:
raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
return has_signed_terms
def clean_recaptcha_response_field(self):
if 'recaptcha_challenge_field' in self.cleaned_data:
self.validate_captcha()
return self.cleaned_data['recaptcha_response_field']
def clean_recaptcha_challenge_field(self):
if 'recaptcha_response_field' in self.cleaned_data:
self.validate_captcha()
return self.cleaned_data['recaptcha_challenge_field']
def validate_captcha(self):
rcf = self.cleaned_data['recaptcha_challenge_field']
rrf = self.cleaned_data['recaptcha_response_field']
check = captcha.submit(
rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
if not check.is_valid:
raise forms.ValidationError(_(
astakos_messages.CAPTCHA_VALIDATION_ERR))
def create_user(self):
try:
data = self.cleaned_data
except AttributeError:
self.is_valid()
data = self.cleaned_data
user = auth.make_local_user(
email=data['email'], password=data['password1'],
first_name=data['first_name'], last_name=data['last_name'],
has_signed_terms=True)
return user
class ThirdPartyUserCreationForm(forms.ModelForm):
email = EmailField(
label='Contact email',
help_text='This is needed for contact purposes. '
'It doesn't need to be the same with the one you '
'provided to login previously. '
)
ro_fields = []
class Meta:
model = AstakosUser
fields = ['email', 'first_name', 'last_name', 'has_signed_terms']
def __init__(self, *args, **kwargs):
"""
Changes the order of fields, and removes the username field.
"""
self.provider = kwargs.pop('provider', None)
self.request = kwargs.pop('request', None)
if not self.provider or self.provider == 'local':
raise Exception('Invalid provider, %r' % self.provider)
# ThirdPartyUserCreationForm should always get instantiated with
# a third_party_token value
self.third_party_token = kwargs.pop('third_party_token', None)
if not self.third_party_token:
raise Exception('ThirdPartyUserCreationForm'
' requires third_party_token')
super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
if not get_latest_terms():
del self.fields['has_signed_terms']
if 'has_signed_terms' in self.fields:
# Overriding field label since we need to apply a link
# to the terms within the label
terms_link_html = '%s' \
% (reverse('latest_terms'), _("the terms"))
self.fields['has_signed_terms'].label = \
mark_safe("I agree with %s" % terms_link_html)
auth_provider = auth_providers.get_provider(self.provider)
user_attr_map = auth_provider.get_user_attr_map()
for field in ['email', 'first_name', 'last_name']:
if not user_attr_map[field][1]:
self.ro_fields.append(field)
self.fields[field].widget.attrs['readonly'] = True
self.fields[field].help_text = _(READ_ONLY_FIELD_MSG)
def clean_email(self):
email = self.cleaned_data['email']
if not email:
raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
if reserved_verified_email(email):
provider_id = self.provider
provider = auth_providers.get_provider(provider_id)
extra_message = provider.get_add_to_existing_account_msg
raise forms.ValidationError(mark_safe(
_(astakos_messages.EMAIL_USED) + ' ' + extra_message))
return email
def clean_first_name(self):
if 'first_name' in self.ro_fields:
return self.initial['first_name']
return self.cleaned_data['first_name']
def clean_last_name(self):
if 'last_name' in self.ro_fields:
return self.initial['last_name']
return self.cleaned_data['last_name']
def clean_has_signed_terms(self):
has_signed_terms = self.cleaned_data['has_signed_terms']
if not has_signed_terms:
raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
return has_signed_terms
def _get_pending_user(self):
return PendingThirdPartyUser.objects.get(token=self.third_party_token)
def create_user(self):
try:
data = self.cleaned_data
except AttributeError:
self.is_valid()
data = self.cleaned_data
user = auth.make_user(
email=data["email"],
first_name=data["first_name"], last_name=data["last_name"],
has_signed_terms=True)
pending = self._get_pending_user()
provider = pending.get_provider(user)
provider.add_to_user()
pending.delete()
return user
autofocus_widget = forms.TextInput(attrs={'autofocus': 'autofocus'})
class LoginForm(AuthenticationForm):
username = EmailField(label=_("Email"), widget=autofocus_widget)
recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
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)
if request:
self.ip = request.META.get(
'REMOTE_ADDR',
request.META.get('HTTP_X_REAL_IP', None))
t = ('request', 'was_limited')
for elem in t:
if elem in kwargs.keys():
kwargs.pop(elem)
super(LoginForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['username', 'password']
if was_limited and settings.RECAPTCHA_ENABLED:
self.fields.keyOrder.extend(['recaptcha_challenge_field',
'recaptcha_response_field', ])
def clean_username(self):
return self.cleaned_data['username'].lower()
def clean_recaptcha_response_field(self):
if 'recaptcha_challenge_field' in self.cleaned_data:
self.validate_captcha()
return self.cleaned_data['recaptcha_response_field']
def clean_recaptcha_challenge_field(self):
if 'recaptcha_response_field' in self.cleaned_data:
self.validate_captcha()
return self.cleaned_data['recaptcha_challenge_field']
def validate_captcha(self):
rcf = self.cleaned_data['recaptcha_challenge_field']
rrf = self.cleaned_data['recaptcha_response_field']
check = captcha.submit(
rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
if not check.is_valid:
raise forms.ValidationError(_(
astakos_messages.CAPTCHA_VALIDATION_ERR))
def clean(self):
"""
Override default behavior in order to check user's activation later
"""
username = self.cleaned_data.get('username')
if username:
try:
user = AstakosUser.objects.get_by_identifier(username)
if not user.has_auth_provider('local'):
provider = auth_providers.get_provider('local', user)
msg = provider.get_login_disabled_msg
raise forms.ValidationError(mark_safe(msg))
except AstakosUser.DoesNotExist:
pass
try:
super(LoginForm, self).clean()
except forms.ValidationError:
if self.user_cache is None:
raise
if not self.user_cache.is_active:
msg = self.user_cache.get_inactive_message('local')
raise forms.ValidationError(msg)
if self.request:
if not self.request.session.test_cookie_worked():
raise
return self.cleaned_data
class LDAPLoginForm(LoginForm):
"""Login form for LDAP Authentication Provider.
* Inherits from 'LoginForm' in order to inherit recaptcha handling.
* Overrides username to be an arbitraty string rather than an email.
* Overrides clean method
"""
username = forms.CharField(label=_('Identifier'))
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if username:
try:
user = AstakosUser.objects.get_by_identifier(username)
if not user.has_auth_provider('ldap'):
provider = auth_providers.get_provider('ldap', user)
msg = provider.get_login_disabled_msg
raise forms.ValidationError(mark_safe(msg))
except AstakosUser.DoesNotExist:
pass
# Set user cache to None, so that methods of 'AuthenticationForm'
# work
self.user_cache = None
if username and password:
self.ldap_user_cache = LDAPBackend().authenticate(username=username,
password=password)
if self.ldap_user_cache is None:
if self.request:
if not self.request.session.test_cookie_worked():
raise
raise forms.ValidationError(
self.error_messages['invalid_login'])
self.check_for_test_cookie()
return self.cleaned_data
def get_ldap_user_id(self):
if self.ldap_user_cache:
return self.ldap_user_cache.id
return None
def get_ldap_user(self):
return self.ldap_user_cache
class ProfileForm(forms.ModelForm):
"""
Subclass of ``ModelForm`` for permiting user to edit his/her profile.
Most of the fields are readonly since the user is not allowed to change
them.
The class defines a save method which sets ``is_verified`` to True so as
the user during the next login will not to be redirected to profile page.
"""
email = EmailField(label='E-mail address',
help_text='E-mail address')
renew = forms.BooleanField(label='Renew token', required=False)
ro_fields = ['email']
class Meta:
model = AstakosUser
fields = ('email', 'first_name', 'last_name')
def __init__(self, *args, **kwargs):
self.session_key = kwargs.pop('session_key', None)
super(ProfileForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.id:
if not instance.can_change_first_name():
self.ro_fields.append('first_name')
if not instance.can_change_last_name():
self.ro_fields.append('last_name')
for field in self.ro_fields:
self.fields[field].widget.attrs['readonly'] = True
self.fields[field].help_text = _(READ_ONLY_FIELD_MSG)
def clean_email(self):
return self.instance.email
def clean_first_name(self):
if 'first_name' in self.ro_fields:
return self.initial['first_name']
return self.cleaned_data['first_name']
def clean_last_name(self):
if 'last_name' in self.ro_fields:
return self.initial['last_name']
return self.cleaned_data['last_name']
def save(self, commit=True, **kwargs):
user = super(ProfileForm, self).save(commit=False, **kwargs)
user.is_verified = True
if self.cleaned_data.get('renew'):
user.renew_token(
flush_sessions=True,
current_key=self.session_key
)
if commit:
user.save(**kwargs)
return user
class FeedbackForm(forms.Form):
"""
Form for writing feedback.
"""
feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
required=False)
class SendInvitationForm(forms.Form):
"""
Form for sending an invitations
"""
email = EmailField(required=True, label='Email address')
first_name = EmailField(label='First name')
last_name = EmailField(label='Last name')
class ExtendedPasswordResetForm(PasswordResetForm):
"""
Extends PasswordResetForm by overriding
save method: to pass a custom from_email in send_mail.
clean_email: to handle local auth provider checks
"""
def clean_email(self):
# we override the default django auth clean_email to provide more
# detailed messages in case of inactive users
email = self.cleaned_data['email']
try:
user = AstakosUser.objects.get_by_identifier(email)
self.users_cache = [user]
if not user.is_active:
if not user.has_auth_provider('local', auth_backend='astakos'):
provider = auth_providers.get_provider('local', user)
msg = mark_safe(provider.get_unusable_password_msg)
raise forms.ValidationError(msg)
msg = mark_safe(user.get_inactive_message('local'))
raise forms.ValidationError(msg)
provider = auth_providers.get_provider('local', user)
if not user.has_usable_password():
msg = provider.get_unusable_password_msg
raise forms.ValidationError(mark_safe(msg))
if not user.can_change_password():
msg = provider.get_cannot_change_password_msg
raise forms.ValidationError(mark_safe(msg))
except AstakosUser.DoesNotExist:
raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
return email
def save(self, domain_override=None,
email_template_name='registration/password_reset_email.html',
use_https=False, token_generator=default_token_generator,
request=None, **kwargs):
"""
Generates a one-use only link for resetting password and sends to the
user.
"""
for user in self.users_cache:
url = user.astakosuser.get_password_reset_url(token_generator)
url = join_urls(settings.BASE_HOST, url)
c = {
'email': user.email,
'url': url,
'user': user,
'baseurl': settings.BASE_URL,
'support': settings.CONTACT_EMAIL
}
message = render_to_string(email_template_name, c)
from_email = settings.SERVER_EMAIL
send_mail(_(astakos_messages.PASSWORD_RESET_EMAIL_SUBJECT),
message,
from_email,
[user.email],
connection=get_connection())
class EmailChangeForm(forms.ModelForm):
new_email_address = EmailField()
class Meta:
model = EmailChange
fields = ('new_email_address',)
def clean_new_email_address(self):
addr = self.cleaned_data['new_email_address']
if reserved_verified_email(addr):
raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
return addr
def save(self):
raise NotImplementedError
class SignApprovalTermsForm(forms.ModelForm):
class Meta:
model = AstakosUser
fields = ("has_signed_terms",)
def __init__(self, *args, **kwargs):
super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
self.fields['has_signed_terms'].label = _("I agree with the terms")
def clean_has_signed_terms(self):
has_signed_terms = self.cleaned_data['has_signed_terms']
if not has_signed_terms:
raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
return has_signed_terms
def save(self, commit=True, **kwargs):
user = super(SignApprovalTermsForm, self).save(commit=commit, **kwargs)
user.date_signed_terms = datetime.now()
if commit:
user.save(**kwargs)
return user
class InvitationForm(forms.ModelForm):
username = EmailField(label=_("Email"))
def __init__(self, *args, **kwargs):
super(InvitationForm, self).__init__(*args, **kwargs)
class Meta:
model = Invitation
fields = ('username', 'realname')
def clean_username(self):
username = self.cleaned_data['username']
try:
Invitation.objects.get(username=username)
raise forms.ValidationError(
_(astakos_messages.INVITATION_EMAIL_EXISTS))
except Invitation.DoesNotExist:
pass
return username
class ExtendedPasswordChangeForm(PasswordChangeForm):
"""
Extends PasswordChangeForm by enabling user
to optionally renew also the token.
"""
if not settings.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):
self.session_key = kwargs.pop('session_key', None)
super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
def save(self, commit=True, **kwargs):
try:
if settings.NEWPASSWD_INVALIDATE_TOKEN or \
self.cleaned_data.get('renew'):
self.user.renew_token()
self.user.flush_sessions(current_key=self.session_key)
except AttributeError:
# if user model does has not such methods
pass
return super(ExtendedPasswordChangeForm, self).save(commit=commit,
**kwargs)
class ExtendedSetPasswordForm(SetPasswordForm):
"""
Extends SetPasswordForm by enabling user
to optionally renew also the token.
"""
if not settings.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)
@transaction.commit_on_success()
def save(self, commit=True, **kwargs):
try:
self.user = AstakosUser.objects.get(id=self.user.id)
if settings.NEWPASSWD_INVALIDATE_TOKEN or \
self.cleaned_data.get('renew'):
self.user.renew_token()
provider = auth_providers.get_provider('local', self.user)
if provider.get_add_policy:
provider.add_to_user()
except BaseException, e:
logger.exception(e)
return super(ExtendedSetPasswordForm, self).save(commit=commit,
**kwargs)
app_name_label = "Project name"
app_name_placeholder = _("myproject.mylab.ntua.gr")
app_name_validator = validators.RegexValidator(
DOMAIN_VALUE_REGEX,
_(astakos_messages.DOMAIN_VALUE_ERR),
'invalid')
base_app_name_validator = validators.RegexValidator(
BASE_PROJECT_NAME_REGEX,
_(astakos_messages.BASE_PROJECT_NAME_ERR),
'invalid')
app_name_help = _("""
The project's name should be in a domain format.
The domain shouldn't neccessarily exist in the real
world but is helpful to imply a structure.
e.g.: myproject.mylab.ntua.gr or
myservice.myteam.myorganization""")
app_name_widget = forms.TextInput(
attrs={'placeholder': app_name_placeholder})
app_home_label = "Homepage URL"
app_home_placeholder = 'myinstitution.org/myproject/'
app_home_help = _("""
URL pointing at your project's site.
e.g.: myinstitution.org/myproject/.
Leave blank if there is no website.""")
app_home_widget = forms.TextInput(
attrs={'placeholder': app_home_placeholder})
app_desc_label = _("Description")
app_desc_help = _("""
Please provide a short but descriptive abstract of your
project, so that anyone searching can quickly understand
what this project is about.""")
app_comment_label = _("Comments for review (private)")
app_comment_help = _("""
Write down any comments you may have for the reviewer
of this application (e.g. background and rationale to
support your request).
The comments are strictly for the review process
and will not be made public.""")
app_start_date_label = _("Start date")
app_start_date_help = _("""
Provide a date when your need your project to be created,
and members to be able to join and get resources.
This date is only a hint to help prioritize reviews.""")
app_end_date_label = _("Termination date")
app_end_date_help = _("""
At this date, the project will be automatically terminated
and its resource grants revoked from all members. If you are
not certain, it is best to start with a conservative estimation.
You can always re-apply for an extension, if you need.""")
join_policy_label = _("Joining policy")
app_member_join_policy_help = _("""
Select how new members are accepted into the project.""")
leave_policy_label = _("Leaving policy")
app_member_leave_policy_help = _("""
Select how new members can leave the project.""")
max_members_label = _("Max members")
max_members_help = _("""
Specify the maximum number of members this project may have,
including the owner. Beyond this number, no new members
may join the project and be granted the project resources.
If you are not certain, it is best to start with a conservative
limit. You can always request a raise when you need it.""")
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
class ProjectApplicationForm(forms.ModelForm):
name = forms.CharField(
label=app_name_label,
help_text=app_name_help,
widget=app_name_widget,
validators=[app_name_validator])
homepage = forms.URLField(
label=app_home_label,
help_text=app_home_help,
widget=app_home_widget,
required=False)
description = forms.CharField(
label=app_desc_label,
help_text=app_desc_help,
widget=forms.Textarea,
required=False)
comments = forms.CharField(
label=app_comment_label,
help_text=app_comment_help,
widget=forms.Textarea,
required=False)
start_date = forms.DateTimeField(
label=app_start_date_label,
help_text=app_start_date_help,
required=False)
end_date = forms.DateTimeField(
label=app_end_date_label,
help_text=app_end_date_help)
member_join_policy = forms.TypedChoiceField(
label=join_policy_label,
help_text=app_member_join_policy_help,
initial=2,
coerce=int,
choices=join_policies)
member_leave_policy = forms.TypedChoiceField(
label=leave_policy_label,
help_text=app_member_leave_policy_help,
coerce=int,
choices=leave_policies)
limit_on_members_number = InfiniteChoiceField(
choices=settings.PROJECT_MEMBERS_LIMIT_CHOICES,
label=max_members_label,
help_text=max_members_help,
initial="Unlimited",
required=True)
class Meta:
model = ProjectApplication
fields = ('name', 'homepage', 'description',
'start_date', 'end_date', 'comments',
'member_join_policy', 'member_leave_policy',
'limit_on_members_number')
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
super(ProjectApplicationForm, self).__init__(*args, **kwargs)
# in case of new application remove closed join policy
if not instance:
policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.copy()
policies.pop(3)
self.fields['member_join_policy'].choices = policies.iteritems()
else:
if instance.is_base:
name_field = self.fields['name']
name_field.validators = [base_app_name_validator]
if self.initial['limit_on_members_number'] == \
units.PRACTICALLY_INFINITE:
self.initial['limit_on_members_number'] = 'Unlimited'
def clean_limit_on_members_number(self):
value = self.cleaned_data.get('limit_on_members_number')
if value in ["inf", "Unlimited"]:
return units.PRACTICALLY_INFINITE
return value
def clean_start_date(self):
start_date = self.cleaned_data.get('start_date')
if not self.instance:
today = datetime.now()
today = datetime(today.year, today.month, today.day)
if start_date and (start_date - today).days < 0:
raise forms.ValidationError(
_(astakos_messages.INVALID_PROJECT_START_DATE))
return start_date
def clean_end_date(self):
start_date = self.cleaned_data.get('start_date')
end_date = self.cleaned_data.get('end_date')
today = datetime.now()
today = datetime(today.year, today.month, today.day)
if end_date and (end_date - today).days < 0:
raise forms.ValidationError(
_(astakos_messages.INVALID_PROJECT_END_DATE))
if start_date and (end_date - start_date).days <= 0:
raise forms.ValidationError(
_(astakos_messages.INCONSISTENT_PROJECT_DATES))
return end_date
def clean(self):
userid = self.data.get('user', None)
policies = self.resource_policies
self.user = None
if userid:
try:
self.user = AstakosUser.objects.get(id=userid)
except AstakosUser.DoesNotExist:
pass
if not self.user:
raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
cleaned_data = super(ProjectApplicationForm, self).clean()
return cleaned_data
def value_or_inf(self, value):
if value == 'inf' or value == 'Unlimited':
return units.PRACTICALLY_INFINITE
return value
@property
def resource_policies(self):
policies = []
append = policies.append
resource_indexes = {}
include_diffs = False
is_new = self.instance and self.instance.id is None
existing_policies = []
existing_data = {}
# normalize to single values dict
data = dict()
for key, value in self.data.iteritems():
data[key] = value
if not is_new:
# User may have emptied some fields. Empty values are not handled
# below. Fill data as if user typed "0" in field, but only
# for resources which exist in application project and have
# non-zero capacity (either for member or project).
include_diffs = True
existing_policies = self.instance.resource_set
append_groups = set()
for policy in existing_policies:
cap_set = max(policy.project_capacity, policy.member_capacity)
if not policy.resource.ui_visible:
continue
rname = policy.resource.name
group = policy.resource.group
existing_data["%s_p_uplimit" % rname] = "0"
existing_data["%s_m_uplimit" % rname] = "0"
append_groups.add(group)
for key, value in existing_data.iteritems():
if not key in data or data.get(key, '') == '':
data[key] = value
for group in append_groups:
data["is_selected_%s" % group] = "1"
for name, value in data.iteritems():
if not value:
continue
if name.endswith('_uplimit'):
is_project_limit = name.endswith('_p_uplimit')
suffix = '_p_uplimit' if is_project_limit else '_m_uplimit'
value = self.value_or_inf(value)
uplimit = value
prefix, _suffix = name.split(suffix)
try:
resource = Resource.objects.get(name=prefix)
except Resource.DoesNotExist:
raise forms.ValidationError("Resource %s does not exist" %
resource.name)
if is_project_limit:
_key = prefix + '_m_uplimit'
member_limit = self.value_or_inf(data.get(_key))
try:
pvalue = int(value)
mvalue = int(member_limit)
except:
raise forms.ValidationError("Invalid format")
else:
_key = prefix + '_p_uplimit'
project_limit = self.value_or_inf(data.get(_key))
try:
mvalue = int(value)
pvalue = int(project_limit)
except:
raise forms.ValidationError("Invalid format")
if mvalue > pvalue:
msg = "%s per member limit exceeds total limit"
raise forms.ValidationError(msg % resource.name)
# keep only resource limits for selected resource groups
if data.get('is_selected_%s' % \
resource.group, "0") == "1":
if not resource.ui_visible:
raise forms.ValidationError("Invalid resource %s" %
resource.name)
d = model_to_dict(resource)
try:
uplimit = long(uplimit)
except ValueError:
m = "Limit should be an integer"
raise forms.ValidationError(m)
display = units.show(uplimit, resource.unit)
if display == "inf":
display = "Unlimited"
handled = resource_indexes.get(prefix)
diff_data = None
if include_diffs:
try:
policy = existing_policies.get(resource=resource)
if is_project_limit:
pval = policy.project_capacity
else:
pval = policy.member_capacity
if pval != uplimit:
diff = pval - uplimit
diff_display = units.show(abs(diff),
resource.unit,
inf="Unlimited")
diff_is_inf = False
prev_is_inf = False
if uplimit == units.PRACTICALLY_INFINITE:
diff_display = "Unlimited"
diff_is_inf = True
if pval == units.PRACTICALLY_INFINITE:
diff_display = "Unlimited"
prev_is_inf = True
prev_display = units.show(pval, resource.unit,
inf="Unlimited")
diff_data = {
'prev': pval,
'prev_display': prev_display,
'diff': diff,
'diff_display': diff_display,
'increased': diff < 0,
'diff_is_inf': diff_is_inf,
'prev_is_inf': prev_is_inf,
'operator': '+' if diff < 0 else '-'
}
except:
pass
if is_project_limit:
d.update(dict(resource=prefix,
p_uplimit=uplimit,
display_p_uplimit=display))
if diff_data:
d.update(dict(resource=prefix, p_diff=diff_data))
if not handled:
d.update(dict(resource=prefix, m_uplimit=0,
display_m_uplimit=units.show(0,
resource.unit)))
else:
d.update(dict(resource=prefix, m_uplimit=uplimit,
display_m_uplimit=display))
if diff_data:
d.update(dict(resource=prefix, m_diff=diff_data))
if not handled:
d.update(dict(resource=prefix, p_uplimit=0,
display_p_uplimit=units.show(0,
resource.unit)))
if resource_indexes.get(prefix, None) is not None:
# already included in policies
handled.update(d)
else:
# keep track of resource dicts
append(d)
resource_indexes[prefix] = d
ordered_keys = presentation.RESOURCES['resources_order']
def resource_order(r):
if r['str_repr'] in ordered_keys:
return ordered_keys.index(r['str_repr'])
else:
return -1
policies = sorted(policies, key=resource_order)
return policies
def cleaned_resource_policies(self):
policies = {}
for d in self.resource_policies:
if self.instance.pk:
if not d.get('p_diff', None) and not d.get('m_diff', None):
continue
policies[d["name"]] = {
"project_capacity": d.get("p_uplimit", 0),
"member_capacity": d.get("m_uplimit", 0)
}
if len(policies.keys()) == 0:
return {}
return policies
def get_api_data(self):
data = dict(self.cleaned_data)
is_new = self.instance.id is None
if isinstance(self.instance, Project):
data['project_id'] = self.instance.id
else:
data['project_id'] = self.instance.chain.id if not is_new else None
owner_uuid = None
if self.instance.owner:
owner_uuid = self.instance.owner.uuid
user_uuid = self.user.uuid if is_new else owner_uuid
try:
object_owner = AstakosUser.objects.get(uuid=user_uuid)
data['owner'] = object_owner
except AstakosUser.DoesNotExist:
pass
exclude_keys = ['owner', 'comments', 'project_id', 'start_date',
'limit_on_members_number']
# is_valid changes instance attributes
instance = self.instance
if not is_new:
instance = Project.objects.get(pk=self.instance.pk)
for key in [dkey for dkey in data.keys() if not dkey in exclude_keys]:
if not is_new and \
(getattr(instance, key) == data.get(key)):
del data[key]
resources = self.cleaned_resource_policies()
if resources:
data['resources'] = resources
if data.get('start_date', None):
data['start_date'] = date_util.isoformat(data.get('start_date'))
else:
del data['start_date']
if data.get('end_date', None):
data['end_date'] = date_util.isoformat(data.get('end_date'))
limit = data.get('limit_on_members_number', None)
if limit:
data['max_members'] = data.get('limit_on_members_number')
else:
data['max_members'] = units.PRACTICALLY_INFINITE
data['request_user'] = self.user
if 'owner' in data:
data['owner'] = data['owner'].uuid
return data
def save(self, commit=True, **kwargs):
from astakos.api import projects as api
data = self.get_api_data()
return api.submit_new_project(data, self.user)
class ProjectModificationForm(ProjectApplicationForm):
class Meta:
model = Project
fields = ('name', 'homepage', 'description',
'end_date', 'comments', 'member_join_policy',
'member_leave_policy', 'limit_on_members_number')
def save(self, commit=True, **kwargs):
from astakos.api import projects as api
data = self.get_api_data()
return api.submit_modification(data, self.user, self.instance.uuid)
class ProjectSortForm(forms.Form):
sorting = forms.ChoiceField(
label='Sort by',
choices=(('name', 'Sort by Name'),
('issue_date', 'Sort by Issue date'),
('start_date', 'Sort by Start Date'),
('end_date', 'Sort by End Date'),
# ('approved_members_num', 'Sort by Participants'),
('state', 'Sort by Status'),
('member_join_policy__description',
'Sort by Member Join Policy'),
('member_leave_policy__description',
'Sort by Member Leave Policy'),
('-name', 'Sort by Name'),
('-issue_date', 'Sort by Issue date'),
('-start_date', 'Sort by Start Date'),
('-end_date', 'Sort by End Date'),
# ('-approved_members_num', 'Sort by Participants'),
('-state', 'Sort by Status'),
('-member_join_policy__description',
'Sort by Member Join Policy'),
('-member_leave_policy__description',
'Sort by Member Leave Policy')
),
required=True
)
class AddProjectMembersForm(forms.Form):
q = forms.CharField(
widget=forms.Textarea(
attrs={
'placeholder':
astakos_messages.ADD_PROJECT_MEMBERS_Q_PLACEHOLDER}),
label=_('Add members'),
help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
required=True,)
def __init__(self, *args, **kwargs):
project_id = kwargs.pop('project_id', None)
if project_id:
self.project = Project.objects.get(id=project_id)
self.request_user = kwargs.pop('request_user', None)
super(AddProjectMembersForm, self).__init__(*args, **kwargs)
def clean(self):
try:
accept_membership_project_checks(self.project, self.request_user)
except ProjectError as e:
raise forms.ValidationError(e)
q = self.cleaned_data.get('q') or ''
users = re.split("\r\n|\n|,", q)
users = list(u.strip() for u in users if u)
# Notice that deactivated users are reported as 'unknown' here, too.
db_entries = AstakosUser.objects.accepted().filter(
email__in=users, is_active=True)
unknown = list(set(users) - set(u.email for u in db_entries))
if unknown:
raise forms.ValidationError(
_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
self.valid_users = db_entries
return self.cleaned_data
def get_valid_users(self):
"""Should be called after form cleaning"""
return self.valid_users
class ProjectMembersSortForm(forms.Form):
sorting = forms.ChoiceField(
label='Sort by',
choices=(('person__email', 'User Id'),
('person__first_name', 'Name'),
('acceptance_date', 'Acceptance date')
),
required=True
)
class ProjectSearchForm(forms.Form):
q = forms.CharField(max_length=200, label='Search project', required=False)
class ExtendedProfileForm(ProfileForm):
"""
Profile form that combines `email change` and `password change` user
actions by propagating submited data to internal EmailChangeForm
and ExtendedPasswordChangeForm objects.
"""
password_change_form = None
email_change_form = None
password_change = False
email_change = False
extra_forms_fields = {
'email': ['new_email_address'],
'password': ['old_password', 'new_password1', 'new_password2']
}
fields = ('email')
change_password = forms.BooleanField(initial=False, required=False)
change_email = forms.BooleanField(initial=False, required=False)
email_changed = False
password_changed = False
def __init__(self, *args, **kwargs):
session_key = kwargs.get('session_key', None)
self.fields_list = [
'email',
'new_email_address',
'first_name',
'last_name',
'old_password',
'new_password1',
'new_password2',
'change_email',
'change_password',
]
super(ExtendedProfileForm, self).__init__(*args, **kwargs)
self.session_key = session_key
if self.instance.can_change_password():
self.password_change = True
else:
self.fields_list.remove('old_password')
self.fields_list.remove('new_password1')
self.fields_list.remove('new_password2')
self.fields_list.remove('change_password')
del self.fields['change_password']
if settings.EMAILCHANGE_ENABLED and self.instance.can_change_email():
self.email_change = True
else:
self.fields_list.remove('new_email_address')
self.fields_list.remove('change_email')
del self.fields['change_email']
self._init_extra_forms()
self.save_extra_forms = []
self.success_messages = []
self.fields.keyOrder = self.fields_list
def _init_extra_form_fields(self):
if self.email_change:
self.fields.update(self.email_change_form.fields)
self.fields['new_email_address'].required = False
self.fields['email'].help_text = _(
'Change the email associated with '
'your account. This email will '
'remain active until you verify '
'your new one.')
if self.password_change:
self.fields.update(self.password_change_form.fields)
self.fields['old_password'].required = False
self.fields['old_password'].label = _('Password')
self.fields['old_password'].help_text = _('Change your password.')
self.fields['old_password'].initial = 'password'
self.fields['old_password'].widget.render_value = True
self.fields['new_password1'].required = False
self.fields['new_password2'].required = False
def _update_extra_form_errors(self):
if self.cleaned_data.get('change_password'):
self.errors.update(self.password_change_form.errors)
if self.cleaned_data.get('change_email'):
self.errors.update(self.email_change_form.errors)
def _init_extra_forms(self):
self.email_change_form = EmailChangeForm(self.data)
self.password_change_form = ExtendedPasswordChangeForm(
user=self.instance,
data=self.data, session_key=self.session_key)
self._init_extra_form_fields()
def is_valid(self):
password, email = True, True
profile = super(ExtendedProfileForm, self).is_valid()
if profile and self.cleaned_data.get('change_password', None):
self.password_change_form.fields['new_password1'].required = True
self.password_change_form.fields['new_password2'].required = True
password = self.password_change_form.is_valid()
self.save_extra_forms.append('password')
if profile and self.cleaned_data.get('change_email'):
self.fields['new_email_address'].required = True
email = self.email_change_form.is_valid()
self.save_extra_forms.append('email')
if not password or not email:
self._update_extra_form_errors()
return all([profile, password, email])
def save(self, request, *args, **kwargs):
if 'email' in self.save_extra_forms:
change_user_email(
self.instance,
self.email_change_form.cleaned_data['new_email_address']
)
self.email_changed = True
if 'password' in self.save_extra_forms:
self.password_change_form.save(*args, **kwargs)
self.password_changed = True
return super(ExtendedProfileForm, self).save(*args, **kwargs)