Form could be useful for creating fields like Education interval or Work Experience interval. You could see such fields on facebook and linkedin.
Using: Flask, WTForms, SQLAlchemy, jQuery, Bootstrap
| function toggleDateType($togglerDateType) { | |
| var $choiceDateWrapper = $togglerDateType.closest('.date').find('.choice-date'); | |
| var $currentDateWrapper = $togglerDateType.closest('.date').find('.current-date'); | |
| if ($togglerDateType.is(':checked')) { | |
| $choiceDateWrapper.addClass('hidden'); | |
| $currentDateWrapper.removeClass('hidden'); | |
| } | |
| else { | |
| $choiceDateWrapper.removeClass('hidden'); | |
| $currentDateWrapper.addClass('hidden'); | |
| } | |
| } | |
| function initMonthYearPeriod($wrapper) { | |
| var $periodWrapper = $wrapper.find('.month-year-period'); | |
| var $currentDateToogler = $periodWrapper.find('.current-date-toggler'); | |
| if ($periodWrapper.length && $currentDateToogler) { | |
| $.each($currentDateToogler, function () { | |
| toggleDateType($(this)); | |
| }); | |
| } | |
| } | |
| $wrapper = $('form'); // put here you wrapper | |
| initMonthYearPeriod($wrapper); | |
| $wrapper.on('change', '.current-date-toggler', function (e) { | |
| toggleDateType($(this)); | |
| }); |
| .month-year-period { | |
| .clearfix; | |
| .date { | |
| float: left; | |
| width: 46%; | |
| } | |
| .devider { | |
| float: left; | |
| width: 8%; | |
| text-align: center; | |
| font-size: 120%; | |
| line-height: @input-height-base; | |
| } | |
| .choice-date { | |
| .clearfix; | |
| .month, .year { | |
| float: left; | |
| width: 49%; | |
| } | |
| .month { | |
| margin-right: 2%; | |
| } | |
| } | |
| .current-date { | |
| line-height: @input-height-base; | |
| } | |
| } |
| {% from 'macros.html' import form_field %} | |
| <form method="post"> | |
| <div class="month-year-period"> | |
| <div class="date start-date"> | |
| <div class="choice-date"> | |
| <div class="month"> | |
| {{ form_field(form, form['start_date_month'], class_='position-start-date-month') }} | |
| </div> | |
| <div class="year"> | |
| {{ form_field(form, form['start_date_year'], has_label=false, class_='position-start-date-year', maxlength=4) }} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="devider">–</div> | |
| <div class="date end-date"> | |
| <div class="choice-date"> | |
| <div class="month"> | |
| {{ form_field(form, form['end_date_month'], class_='position-end-date-month') }} | |
| </div> | |
| <div class="year"> | |
| {{ form_field(form, form['end_date_year'], has_label=false, class_='position-end-date-year', maxlength=4) }} | |
| </div> | |
| </div> | |
| <div class="form-group current-date hidden"> | |
| Current date | |
| </div> | |
| <div class="pull-right"> | |
| {{ form_field(form, form['end_date_is_current'], class_='position-end-date-is-current current-date-toggler') }} | |
| </div> | |
| </div> | |
| </div> | |
| </form> |
| from flask.ext.wtf import Form | |
| from wtforms import (StringField, TextAreaField, BooleanField, SelectField, IntegerField, SubmitField) | |
| from wtforms.validators import DataRequired, Length, Optional | |
| class EducationRecordForm(Form): | |
| start_date_month = SelectField('', [Optional(), valid_month_year_period], choices=[(0, '')] + get_month_list_shortname(), coerce=int) | |
| start_date_year = IntegerField('from', [valid_month_year_period]) | |
| end_date_month = SelectField('', [Optional(), valid_month_year_period], choices=[(0, '')] + get_month_list_shortname(), coerce=int) | |
| end_date_year = IntegerField('to', [valid_month_year_period]) | |
| end_date_is_current = BooleanField('To the current date') | |
| sudmit = SubmitField('Submit') | |
| def get_month_list_shortname(): | |
| return [(1, 'Jan'), (2, 'Feb'), (3, 'Mar'), (4, 'Apr'), | |
| (5, 'May'), (6, 'Jun'), (7, 'Jul'), (8, 'Aug'), | |
| (9, 'Sep'), (10, 'Oct'), (11, 'Nov'), (12, 'Dec')] | |
| def valid_month_year_period(form, field, minyear=1900): | |
| # Validation logic | |
| # | |
| # Form is optional if all fields are not set. | |
| # Otherwise use next rules: | |
| # | |
| # start_date_year: | |
| # - is required | |
| # - should be number from minyear to current year | |
| # | |
| # start_date_month: | |
| # - is not required | |
| # - if it's set and start_date_year == current year, start_date_month should be not greater then current month | |
| # | |
| # end_date_is_current: | |
| # - is not required | |
| # - if it's set, end_date_year and end_date_month should be unset | |
| # | |
| # end_date_year: | |
| # - if end_date_is_current is not set, end_date_year is required | |
| # - if required, should be number from start_date_year to current year | |
| # | |
| # end_date_month: | |
| # - is not required | |
| # - if it's set and end_date_year == current year, end_date_month should be not greater then current month | |
| # - if it's set and end_date_year == start_date_year, end_date_month should be bigger then start_date_month | |
| if not form.start_date_month.data and not form.start_date_year.data and\ | |
| not form.end_date_month.data and not form.end_date_year.data and\ | |
| not form.end_date_is_current.data: | |
| field.errors[:] = [] | |
| raise StopValidation() | |
| current_year = date.today().year | |
| current_month = date.today().month | |
| if 'start_date_year' in field.name: | |
| if not field.data: | |
| raise ValidationError(messages.start_date_year_required) | |
| if field.data < minyear: | |
| raise ValidationError(messages.start_date_year_min_value.format(min=minyear)) | |
| if field.data > current_year: | |
| raise ValidationError(messages.start_date_year_max_value.format(max=current_year)) | |
| if 'start_date_month' in field.name: | |
| if field.data: | |
| if form.start_date_year.data == current_year and field.data > current_month: | |
| raise ValidationError(messages.start_date_month_max_value.format(max=dict(get_month_list_shortname()).get(current_month))) | |
| if form.end_date_is_current.data: | |
| form.end_date_year.data = None | |
| form.end_date_month.data = 0 | |
| else: | |
| if 'end_date_year' in field.name: | |
| if not field.data: | |
| raise ValidationError(messages.end_date_year_required) | |
| if field.data < form.start_date_year.data: | |
| raise ValidationError(messages.end_date_year_min_value.format(min=form.start_date_year.data)) | |
| if field.data > current_year: | |
| raise ValidationError(messages.end_date_year_max_value.format(max=current_year)) | |
| if 'end_date_month' in field.name: | |
| if field.data: | |
| if form.end_date_year.data == current_year and field.data > current_month: | |
| raise ValidationError(messages.end_date_month_max_value.format(max=dict(get_month_list_shortname()).get(current_month))) | |
| if form.end_date_year.data == form.start_date_year.data and field.data < form.start_date_month.data: | |
| raise ValidationError(messages.end_date_month_min_value.format(min=dict(get_month_list_shortname()).get(form.start_date_month.data))) | |
| {% macro form_field(form, field, print_status=True) -%} | |
| {% set has_label = kwargs.pop('has_label', True) %} | |
| {% set placeholder = '' %} | |
| {% if not has_label %} | |
| {% set placeholder = field.label.text %} | |
| {% endif %} | |
| {% set field_status = '' %} | |
| {% if form.errors and (form.submitted or (form.is_submitted() and form.submit.data)) %} | |
| {# form.submit.data for support multiple forms on page #} | |
| {# form.submitted - manual control for form without button (ajax) #} | |
| {# If form has error, password field would be empty in repeat form | |
| and user need to retype this value. | |
| That is why we need to mark password field always as `error`, | |
| to draw user attention | |
| #} | |
| {# Simulate error for password field (because if form has errors this fields is empty after reloading page) #} | |
| {# Can reset this simulations with field.hide_error = True (need to sing in form) #} | |
| {% if field.type == 'PasswordField' and not field.hide_error %} | |
| {% set field_status = 'error' %} | |
| {% set field_errors = ['Please retype password'] %} | |
| {% else %} | |
| {% if field.errors %} | |
| {% set field_status = 'error' %} | |
| {% else %} | |
| {% set field_status = 'success' %} | |
| {% endif %} | |
| {% endif %} | |
| {% endif %} | |
| <div class="form-group {{ field_status }}"> | |
| {% if has_label and field.label.text and 'BooleanField' not in field.type %} | |
| <label for="{{ field.id }}" class="control-label"> | |
| {{ field.label.text }}{% if field.flags.required %} <span class="required">*</span>{% endif %} | |
| </label> | |
| {% endif %} | |
| <div class="controls"> | |
| {% set class_ = kwargs.pop('class_', '') %} | |
| {% if 'BooleanField' in field.type %} | |
| <div class="checkbox"> | |
| <label for="{{ field.id }}"> | |
| {{ field(class_=class_, **kwargs) }} | |
| {{ field.label.text | safe }} | |
| </label> | |
| </div> | |
| {% elif 'RadioField' in field.type %} | |
| {% for value, label, checked in field.iter_choices() %} | |
| <div class="radio"> | |
| {% if checked %} | |
| <label> | |
| <input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}" | |
| checked="True">{{ label }} | |
| </label> | |
| {% else %} | |
| <label> | |
| <input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}">{{ label }} | |
| </label> | |
| {% endif %} | |
| </div> | |
| {% endfor %} | |
| {% else %} | |
| {% set class_ = class_ ~ ' form-control' %} | |
| {% set prepend = kwargs.pop('prepend', None) %} | |
| {% set append = kwargs.pop('append', None) %} | |
| {% if append or prepend %} | |
| {% if append and prepend %} | |
| <div class="input-group input-group-prepend input-group-append"> | |
| <span class="input-group-addon">{{ prepend | safe }}</span> | |
| {{ field(class_=class_, placeholder=placeholder, **kwargs) }} | |
| <span class="input-group-addon">{{ append | safe }}</span> | |
| </div> | |
| {% elif prepend %} | |
| <div class="input-group input-group-prepend"> | |
| <span class="input-group-addon">{{ prepend | safe }}</span> | |
| {{ field(class_=class_, placeholder=placeholder, **kwargs) }} | |
| </div> | |
| {% elif append %} | |
| <div class="input-group input-group-append"> | |
| {{ field(class_=class_, placeholder=placeholder, **kwargs) }} | |
| <span class="input-group-addon">{{ append | safe }}</span> | |
| </div> | |
| {% endif %} | |
| {% else %} | |
| {{ field(class_=class_, placeholder=placeholder, **kwargs) }} | |
| {% endif %} | |
| {% endif %} | |
| {% if field.description %} | |
| <p class="help-block">{{ field.description|safe }}</p> | |
| {% endif %} | |
| {% if print_status %} | |
| {% if field_status == 'error' %} | |
| <p class="error">{{ field.errors|join('<br/>') or field_errors|join('<br/>') }}</p> | |
| {% elif field_status == 'success' %} | |
| <p class="success">Field is ok</p> | |
| {% endif %} | |
| {% endif %} | |
| </div> | |
| </div> | |
| {%- endmacro %} |
| class EducationRecord(db.Model): | |
| __tablename__ = 'education_records' | |
| id = db.Column(db.Integer, primary_key=True) | |
| start_date_month = db.Column(db.SmallInteger) | |
| start_date_year = db.Column(db.SmallInteger) | |
| end_date_month = db.Column(db.SmallInteger) | |
| end_date_year = db.Column(db.SmallInteger) | |
| end_date_is_current = db.Column(db.Boolean, default=False) |
| @bp.route('/edit/<int:uid>', methods=['GET', 'POST']) | |
| def edit(uid): | |
| model = EducationRecord.query.get_or_404(uid) | |
| form = EducationRecordForm(obj=model) | |
| if form.validate_on_submit(): | |
| form.populate_obj(model) | |
| db.session.commit() | |
| flash(messages.object_updated, 'success') | |
| return render_template('education/form.html', model=model, form=form) |