Source code for annotations.forms
from django.contrib.auth.forms import UserChangeForm
from annotations.models import *
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django import forms
from django.forms import widgets, BaseFormSet
from crispy_forms.helper import FormHelper
from django.db.models import Count
from django.db.utils import ProgrammingError
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
import autocomplete_light
import networkx as nx
[docs]class RegistrationForm(forms.Form):
"""
Gives user form of signup and validates it.
"""
full_name = forms.CharField(required=True, max_length=30, label=_("Full name"))
username = forms.RegexField(regex=r'^\w+$', widget=forms.TextInput(attrs=dict(required=True, max_length=30)),
label=_("Username"),
error_messages={ 'invalid': _("This value must contain only letters, "
"numbers and underscores.") })
email = forms.EmailField(widget=forms.TextInput(attrs=dict(required=True, max_length=30)), label=_("Email address"))
password1 = forms.CharField(widget=forms.PasswordInput(attrs=dict(required=True, max_length=30,
render_value=False)), label=_("Password"))
password2 = forms.CharField(widget=forms.PasswordInput(attrs=dict(required=True, max_length=30,
render_value=False)), label=_("Password (again)"))
affiliation = forms.CharField(required=True, max_length=30, label=_("Affliation"))
location = forms.CharField(required=True, max_length=30, label=_("Location"))
link = forms.URLField(required=True, max_length=500, label=_("Link"))
[docs] def clean_username(self):
"""
Validates username.
"""
try:
user = VogonUser.objects.get(username__iexact=self.cleaned_data['username'])
except VogonUser.DoesNotExist:
return self.cleaned_data['username']
raise forms.ValidationError(_("The username already exists. Please try another one."))
[docs] def clean(self):
"""
Validates the values inserted in Password and Confirm Password field are same.
"""
if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data:
if self.cleaned_data['password1'] != self.cleaned_data['password2']:
raise forms.ValidationError(_("The two password fields did not match."))
[docs]def validatefiletype(file):
"""
Validates type of uploaded file.
Parameters
----------
file : file
The file that is uploaded.
Raises
------
ValidationError
Raises this exception if uploaded file is neither plain text nor PDF
"""
# TODO: add PDF back in once we have a better system in place for processing
# large uploads.
if file.content_type != 'text/plain': # file.content_type != 'application/pdf' and
raise ValidationError('Please choose a plain text file')
[docs]class UploadFileForm(forms.Form):
title = forms.CharField(max_length=255, required=True,
help_text='The title of the original resource.')
uri = forms.CharField(label='URI', max_length=255, required=True,
help_text='"URI" stands for Uniform Resource Identifier. This can be a DOI, a permalink to a digital archive (e.g. JSTOR), or any other unique identifier.')
ispublic = forms.BooleanField(label='Make this text public',
required=False,
help_text='By checking this box you affirm that you have the right to make this file publicly available.')
filetoupload = forms.FileField(label='Choose a plain text file:',
required=True,
validators=[validatefiletype],
help_text="Soon you'll be able to upload images, PDFs, and HTML documents!")
datecreated = forms.DateField(label='Date created:',
required=False,
widget=forms.TextInput(attrs={'class':'datepicker'}),
help_text='The date that the original resource was published.')
project = forms.ModelChoiceField(queryset=TextCollection.objects.all(),
required=False,
label='Add to project',
help_text='You can add this text to a project that you own.')
[docs]class UserCreationForm(forms.ModelForm):
password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
[docs] class Meta:
model = VogonUser
fields = ('full_name', 'email', 'affiliation', 'location', 'link', 'imagefile')
[docs] def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError("Passwords don't match")
return password2
[docs] def save(self, commit=True):
# Save the provided password in hashed format
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
[docs]class UserChangeForm(forms.ModelForm):
[docs] class Meta:
model = VogonUser
fields = ('full_name', 'email', 'affiliation', 'location', 'imagefile',
'link')
[docs] def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial.get("password")
[docs]class AutocompleteWidget(widgets.TextInput):
def _format_value(self, value):
if self.is_localized:
return formats.localize_input(value)
return value
[docs] def render(self, name, value, attrs=None):
if value is None:
value = ''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = widgets.force_text(self._format_value(value))
classes = 'autocomplete'
if 'class' in final_attrs:
classes += ' ' + final_attrs['class']
return widgets.format_html('<input class="' + classes + '"{} />', widgets.flatatt(final_attrs))
[docs]class ConceptField(forms.CharField):
queryset = Concept.objects.all()
[docs] def label_from_instance(self, obj):
"""
The ``_concept`` field should be populated with the :class:`.Concept`\s
id.
"""
return obj.id
[docs] def to_python(self, value):
if value in self.empty_values:
return None
try:
key = 'pk'
value = self.queryset.get(**{key: value})
except (ValueError, TypeError, self.queryset.model.DoesNotExist):
raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
return value
[docs]class TemplateChoiceField(forms.ChoiceField):
[docs] def label_from_instance(self, obj):
"""
The ``_concept`` field should be populated with the :class:`.Concept`\s
id.
"""
return obj.id
[docs]class ChoiceIntegerField(forms.IntegerField):
"""
An :class:`.IntegerField` that plays well with the :class:`.Select` widget.
"""
[docs] def to_python(self, value):
"""
Validates that int() can be called on the input. If not, return -1
(default value for ..._relationtemplate_internal_id fields).
"""
try:
value = value.split(':')[-1]
int(value)
return super(ChoiceIntegerField, self).to_python(value)
except ValueError:
return -1
[docs]class RelationTemplateForm(forms.ModelForm):
name = forms.CharField(widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'What is this relation called?'
}))
description = forms.CharField(widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 2,
'placeholder': 'Please describe this relation.',
}))
expression = forms.CharField(widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 2,
'placeholder': 'Enter an expression pattern for this relation.'
}))
[docs]class UberCheckboxInput(forms.CheckboxInput):
[docs] def value_from_datadict(self, data, files, name):
"""
For some stupid reason, some checked checkboxes are getting passed as
'', and :class:`forms.CheckboxInput` stupidly calls these values False.
So, we fix that. If the field is present in the POST data, then it is
True. Grrrr.
"""
if name not in data:
# A missing value means False because HTML form submission does not
# send results for unselected checkboxes.
return False
return True
[docs]class RelationTemplatePartForm(forms.ModelForm):
"""
TODO: make sure that there are no self-loops in inter-part references.
"""
source_concept = ConceptField(widget=widgets.HiddenInput(), required=False)
source_node_type = forms.ChoiceField(choices=[('', 'Select a node type')] + list(RelationTemplatePart.NODE_CHOICES), required=True,
widget=widgets.Select(attrs={'class': 'form-control node_type_field', 'part': 'source'}))
source_concept_text = forms.CharField(widget=AutocompleteWidget(attrs={'target': 'source_concept'}), required=False)
source_prompt_text = forms.BooleanField(required=False, initial=True, widget=UberCheckboxInput())
predicate_concept = ConceptField(widget=widgets.HiddenInput(), required=False)
predicate_node_type = forms.ChoiceField(choices=[('', 'Select a node type')] + list(RelationTemplatePart.PRED_CHOICES), required=True,
widget=widgets.Select(attrs={'class': 'form-control node_type_field', 'part': 'predicate'}))
predicate_concept_text = forms.CharField(widget=AutocompleteWidget(attrs={'target': 'predicate_concept'}), required=False)
predicate_prompt_text = forms.BooleanField(required=False, initial=True, widget=UberCheckboxInput())
object_concept = ConceptField(widget=widgets.HiddenInput(), required=False)
object_node_type = forms.ChoiceField(choices=[('', 'Select a node type')] + list(RelationTemplatePart.NODE_CHOICES), required=True,
widget=widgets.Select(attrs={'class': 'form-control node_type_field', 'part': 'object'}))
object_concept_text= forms.CharField(widget=AutocompleteWidget(attrs={'target': 'object_concept'}), required=False)
object_prompt_text = forms.BooleanField(required=False, initial=True, widget=UberCheckboxInput())
source_relationtemplate_internal_id = ChoiceIntegerField(required=False)
object_relationtemplate_internal_id = ChoiceIntegerField(required=False)
internal_id = forms.IntegerField(widget=widgets.HiddenInput())
[docs] class Media:
js = ('annotations/js/autocomplete.js',)
css = {
'all': ['annotations/css/autocomplete.css']
}
[docs] class Meta:
model = RelationTemplatePart
exclude = [
'source_relationtemplate',
'object_relationtemplate',
'part_of',
]
autocomplete_fields = ( # TODO: do we need this?
'source_concept',
'predicate_concept',
'object_concept',
)
def __init__(self, *args, **kwargs):
super(RelationTemplatePartForm, self).__init__(*args, **kwargs)
# We need a bit of set-up for angular data bindings to work properly
# on the form.
# Angular can't handle hyphens, so we use underscores.
print self.prefix
self.safe_prefix = self.prefix.replace('-', '_')
self.ident = self.prefix.split('-')[-1]
self.fields['internal_id'].initial = self.ident
for fname, field in self.fields.iteritems():
if 'prompt_text' in fname:
continue
if 'class' not in field.widget.attrs:
field.widget.attrs.update({'class': 'form-control'})
if 'target' in field.widget.attrs:
field.widget.attrs['target'] = 'id_{0}-'.format(self.prefix) + field.widget.attrs['target']
[docs] def clean(self, *args, **kwargs):
super(RelationTemplatePartForm, self).clean(*args, **kwargs)
for field in ['source', 'object']:
selected_node_type = self.cleaned_data.get('%s_node_type' % field)
# If the user has selected the "Concept type" field, then they must
# also provide a specific concept type.
if selected_node_type == 'TP':
if not self.cleaned_data.get('%s_type' % field, None):
self.add_error('%s_type' % field, 'Must select a concept type')
[docs]class RelationTemplatePartFormSet(BaseFormSet):
"""
Ensure that the structure of the links among relation template parts is
coherent: it must be acyclic, and the parts must all be connected.
"""
[docs] def clean(self):
if any(self.errors):
return
formGraph = nx.DiGraph() # Dependency graph among parts.
for form in self.forms:
formGraph.add_node(form.cleaned_data['internal_id'])
for field in ['source', 'object']:
fieldname = '%s_relationtemplate_internal_id' % field
target = form.cleaned_data[fieldname]
if target > -1:
formGraph.add_edge(form.cleaned_data['internal_id'], target)
if not nx.is_directed_acyclic_graph(formGraph):
for form in self.forms:
form.add_error(None, 'Circular reference among relation parts.')
if not nx.is_connected(formGraph.to_undirected()):
for form in self.forms:
form.add_error(None, 'At least one relation part is disconnected from the rest of the relation.')
# print self.cleaned_data
# def __init__(self, *args, **kwargs):
# super(ProjectForm, self).__init__(*args, **kwargs)
# self.fields["participants"].widget = forms.CheckboxSelectMultiple()
# self.fields["participants"].queryset = VogonUser.objects.order_by('username')
[docs]class MySplitDateTimeWidget(forms.widgets.SplitDateTimeWidget):
"""
Allow addition of separate CSS classes for the Date and Time widgets.
"""
def __init__(self, attrs=None, date_format=None, time_format=None):
date_class = attrs.get('date_class', '')
date_placeholder = attrs.get('date_placeholder', '')
if date_class:
del attrs['date_class']
if date_placeholder:
del attrs['date_placeholder']
time_class = attrs.get('time_class', '')
time_placeholder = attrs.get('time_placeholder', '')
if time_class:
del attrs['time_class']
if time_placeholder:
del attrs['time_placeholder']
widgets = (forms.widgets.DateInput(attrs={'class' : date_class, 'placeholder': date_placeholder}, format=date_format),
forms.widgets.TimeInput(attrs={'class' : time_class, 'placeholder': time_placeholder}, format=time_format))
super(forms.widgets.SplitDateTimeWidget, self).__init__(widgets, attrs)
# The database annotations in this Form cause call kinds of problems when
# testing. TODO: we should find a more elegant solution than just eating
# these exceptions.
try:
[docs] class RelationSetFilterForm(forms.Form):
text = forms.MultipleChoiceField(choices=Text.objects.annotate(num_appellations=Count('appellation')).filter(num_appellations__gte=1).values_list('id', 'title'),
required=False,
widget=forms.widgets.SelectMultiple(attrs={
'class': 'form-control ymultiselect'
}))
project = forms.MultipleChoiceField(choices=TextCollection.objects.all().values_list('id', 'name'),
required=False,
widget=forms.widgets.SelectMultiple(attrs={'class': 'form-control ymultiselect'}))
text_published_from = forms.DateField(required=False,
widget=forms.widgets.DateInput(attrs={
'class': 'datepicker form-control',
'placeholder': 'YYYY-MM-DD'
}))
text_published_through = forms.DateField(required=False,
widget=forms.widgets.DateInput(attrs={
'class': 'datepicker form-control',
'placeholder': 'YYYY-MM-DD'
}))
user = forms.MultipleChoiceField(choices=VogonUser.objects.annotate(num_appellations=Count('appellation')).filter(num_appellations__gte=1).values_list('id', 'username'),
required=False,
widget=forms.widgets.SelectMultiple(attrs={'class': 'form-control ymultiselect'}))
created_from = forms.SplitDateTimeField(required=False,
widget=MySplitDateTimeWidget(attrs={
'date_class': 'datepicker form-control',
'placeholder': 'YYYY-MM-DD',
'time_class': 'form-control',
'time_placeholder': 'HH:MM:SS'
}))
created_through = forms.SplitDateTimeField(required=False,
widget=MySplitDateTimeWidget(attrs={
'date_class': 'datepicker form-control',
'placeholder': 'YYYY-MM-DD',
'time_class': 'form-control',
'time_placeholder': 'HH:MM:SS'
}))
node_types = forms.MultipleChoiceField(choices=Type.objects.all().values_list('id', 'label'), required=False,
widget=forms.widgets.SelectMultiple(attrs={
'class': 'form-control ymultiselect'
}))
exclusive = forms.BooleanField(required=False)
except ProgrammingError:
pass
[docs]class RepositorySearchForm(forms.Form):
query = forms.CharField(max_length=255, widget=widgets.TextInput(attrs={'class': 'form-control'}))