Commit c2e00580b00093df5558ce50ed30500fb8113fd2
Exists in
master
and in
28 other branches
Merge commit 'refs/merge-requests/231' of git://gitorious.org/noosfero/noosfero …
…into merge-requests/231
Showing
52 changed files
with
3197 additions
and
20 deletions
Show diff stats
app/helpers/application_helper.rb
| ... | ... | @@ -265,9 +265,9 @@ module ApplicationHelper |
| 265 | 265 | |
| 266 | 266 | VIEW_EXTENSIONS = %w[.rhtml .html.erb] |
| 267 | 267 | |
| 268 | - def partial_for_class_in_view_path(klass, view_path) | |
| 268 | + def partial_for_class_in_view_path(klass, view_path, suffix = nil) | |
| 269 | 269 | return nil if klass.nil? |
| 270 | - name = klass.name.underscore | |
| 270 | + name = [klass.name.underscore, suffix].compact.map(&:to_s).join('_') | |
| 271 | 271 | |
| 272 | 272 | search_name = String.new(name) |
| 273 | 273 | if search_name.include?("/") |
| ... | ... | @@ -285,28 +285,17 @@ module ApplicationHelper |
| 285 | 285 | partial_for_class_in_view_path(klass.superclass, view_path) |
| 286 | 286 | end |
| 287 | 287 | |
| 288 | - def partial_for_class(klass) | |
| 288 | + def partial_for_class(klass, suffix=nil) | |
| 289 | 289 | raise ArgumentError, 'No partial for object. Is there a partial for any class in the inheritance hierarchy?' if klass.nil? |
| 290 | 290 | name = klass.name.underscore |
| 291 | 291 | @controller.view_paths.each do |view_path| |
| 292 | - partial = partial_for_class_in_view_path(klass, view_path) | |
| 292 | + partial = partial_for_class_in_view_path(klass, view_path, suffix) | |
| 293 | 293 | return partial if partial |
| 294 | 294 | end |
| 295 | 295 | |
| 296 | 296 | raise ArgumentError, 'No partial for object. Is there a partial for any class in the inheritance hierarchy?' |
| 297 | 297 | end |
| 298 | 298 | |
| 299 | - def partial_for_task_class(klass, action) | |
| 300 | - raise ArgumentError, 'No partial for object. Is there a partial for any class in the inheritance hierarchy?' if klass.nil? | |
| 301 | - | |
| 302 | - name = "#{klass.name.underscore}_#{action.to_s}" | |
| 303 | - VIEW_EXTENSIONS.each do |ext| | |
| 304 | - return name if File.exists?(File.join(RAILS_ROOT, 'app', 'views', params[:controller], '_'+name+ext)) | |
| 305 | - end | |
| 306 | - | |
| 307 | - partial_for_task_class(klass.superclass, action) | |
| 308 | - end | |
| 309 | - | |
| 310 | 299 | def view_for_profile_actions(klass) |
| 311 | 300 | raise ArgumentError, 'No profile actions view for this class.' if klass.nil? |
| 312 | 301 | ... | ... |
app/helpers/forms_helper.rb
| ... | ... | @@ -123,6 +123,119 @@ module FormsHelper |
| 123 | 123 | options_for_select.join("\n") |
| 124 | 124 | end |
| 125 | 125 | |
| 126 | + def date_field(name, value, format = '%Y-%m-%d', datepicker_options = {}, html_options = {}) | |
| 127 | + datepicker_options[:disabled] ||= false | |
| 128 | + datepicker_options[:alt_field] ||= '' | |
| 129 | + datepicker_options[:alt_format] ||= '' | |
| 130 | + datepicker_options[:append_text] ||= '' | |
| 131 | + datepicker_options[:auto_size] ||= false | |
| 132 | + datepicker_options[:button_image] ||= '' | |
| 133 | + datepicker_options[:button_image_only] ||= false | |
| 134 | + datepicker_options[:button_text] ||= '...' | |
| 135 | + datepicker_options[:calculate_week] ||= 'jQuery.datepicker.iso8601Week' | |
| 136 | + datepicker_options[:change_month] ||= false | |
| 137 | + datepicker_options[:change_year] ||= false | |
| 138 | + datepicker_options[:close_text] ||= _('Done') | |
| 139 | + datepicker_options[:constrain_input] ||= true | |
| 140 | + datepicker_options[:current_text] ||= _('Today') | |
| 141 | + datepicker_options[:date_format] ||= 'mm/dd/yy' | |
| 142 | + datepicker_options[:day_names] ||= [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')] | |
| 143 | + datepicker_options[:day_names_min] ||= [_('Su'), _('Mo'), _('Tu'), _('We'), _('Th'), _('Fr'), _('Sa')] | |
| 144 | + datepicker_options[:day_names_short] ||= [_('Sun'), _('Mon'), _('Tue'), _('Wed'), _('Thu'), _('Fri'), _('Sat')] | |
| 145 | + datepicker_options[:default_date] ||= nil | |
| 146 | + datepicker_options[:duration] ||= 'normal' | |
| 147 | + datepicker_options[:first_day] ||= 0 | |
| 148 | + datepicker_options[:goto_current] ||= false | |
| 149 | + datepicker_options[:hide_if_no_prev_next] ||= false | |
| 150 | + datepicker_options[:is_rtl] ||= false | |
| 151 | + datepicker_options[:max_date] ||= nil | |
| 152 | + datepicker_options[:min_date] ||= nil | |
| 153 | + datepicker_options[:month_names] ||= [_('January'), _('February'), _('March'), _('April'), _('May'), _('June'), _('July'), _('August'), _('September'), _('October'), _('November'), _('December')] | |
| 154 | + datepicker_options[:month_names_short] ||= [_('Jan'), _('Feb'), _('Mar'), _('Apr'), _('May'), _('Jun'), _('Jul'), _('Aug'), _('Sep'), _('Oct'), _('Nov'), _('Dec')] | |
| 155 | + datepicker_options[:navigation_as_date_format] ||= false | |
| 156 | + datepicker_options[:next_text] ||= _('Next') | |
| 157 | + datepicker_options[:number_of_months] ||= 1 | |
| 158 | + datepicker_options[:prev_text] ||= _('Prev') | |
| 159 | + datepicker_options[:select_other_months] ||= false | |
| 160 | + datepicker_options[:short_year_cutoff] ||= '+10' | |
| 161 | + datepicker_options[:show_button_panel] ||= false | |
| 162 | + datepicker_options[:show_current_at_pos] ||= 0 | |
| 163 | + datepicker_options[:show_month_after_year] ||= false | |
| 164 | + datepicker_options[:show_on] ||= 'focus' | |
| 165 | + datepicker_options[:show_options] ||= {} | |
| 166 | + datepicker_options[:show_other_months] ||= false | |
| 167 | + datepicker_options[:show_week] ||= false | |
| 168 | + datepicker_options[:step_months] ||= 1 | |
| 169 | + datepicker_options[:week_header] ||= _('Wk') | |
| 170 | + datepicker_options[:year_range] ||= 'c-10:c+10' | |
| 171 | + datepicker_options[:year_suffix] ||= '' | |
| 172 | + | |
| 173 | + element_id = html_options[:id] || 'datepicker-date' | |
| 174 | + value = value.strftime(format) if value.present? | |
| 175 | + method = datepicker_options[:time] ? 'datetimepicker' : 'datepicker' | |
| 176 | + result = text_field_tag(name, value, html_options) | |
| 177 | + result += | |
| 178 | + " | |
| 179 | + <script type='text/javascript'> | |
| 180 | + jQuery('##{element_id}').#{method}({ | |
| 181 | + disabled: #{datepicker_options[:disabled].to_json}, | |
| 182 | + altField: #{datepicker_options[:alt_field].to_json}, | |
| 183 | + altFormat: #{datepicker_options[:alt_format].to_json}, | |
| 184 | + appendText: #{datepicker_options[:append_text].to_json}, | |
| 185 | + autoSize: #{datepicker_options[:auto_size].to_json}, | |
| 186 | + buttonImage: #{datepicker_options[:button_image].to_json}, | |
| 187 | + buttonImageOnly: #{datepicker_options[:button_image_only].to_json}, | |
| 188 | + buttonText: #{datepicker_options[:button_text].to_json}, | |
| 189 | + calculateWeek: #{datepicker_options[:calculate_week].to_json}, | |
| 190 | + changeMonth: #{datepicker_options[:change_month].to_json}, | |
| 191 | + changeYear: #{datepicker_options[:change_year].to_json}, | |
| 192 | + closeText: #{datepicker_options[:close_text].to_json}, | |
| 193 | + constrainInput: #{datepicker_options[:constrain_input].to_json}, | |
| 194 | + currentText: #{datepicker_options[:current_text].to_json}, | |
| 195 | + dateFormat: #{datepicker_options[:date_format].to_json}, | |
| 196 | + dayNames: #{datepicker_options[:day_names].to_json}, | |
| 197 | + dayNamesMin: #{datepicker_options[:day_names_min].to_json}, | |
| 198 | + dayNamesShort: #{datepicker_options[:day_names_short].to_json}, | |
| 199 | + defaultDate: #{datepicker_options[:default_date].to_json}, | |
| 200 | + duration: #{datepicker_options[:duration].to_json}, | |
| 201 | + firstDay: #{datepicker_options[:first_day].to_json}, | |
| 202 | + gotoCurrent: #{datepicker_options[:goto_current].to_json}, | |
| 203 | + hideIfNoPrevNext: #{datepicker_options[:hide_if_no_prev_next].to_json}, | |
| 204 | + isRTL: #{datepicker_options[:is_rtl].to_json}, | |
| 205 | + maxDate: #{datepicker_options[:max_date].to_json}, | |
| 206 | + minDate: #{datepicker_options[:min_date].to_json}, | |
| 207 | + monthNames: #{datepicker_options[:month_names].to_json}, | |
| 208 | + monthNamesShort: #{datepicker_options[:month_names_short].to_json}, | |
| 209 | + navigationAsDateFormat: #{datepicker_options[:navigation_as_date_format].to_json}, | |
| 210 | + nextText: #{datepicker_options[:next_text].to_json}, | |
| 211 | + numberOfMonths: #{datepicker_options[:number_of_months].to_json}, | |
| 212 | + prevText: #{datepicker_options[:prev_text].to_json}, | |
| 213 | + selectOtherMonths: #{datepicker_options[:select_other_months].to_json}, | |
| 214 | + shortYearCutoff: #{datepicker_options[:short_year_cutoff].to_json}, | |
| 215 | + showButtonPanel: #{datepicker_options[:show_button_panel].to_json}, | |
| 216 | + showCurrentAtPos: #{datepicker_options[:show_current_at_pos].to_json}, | |
| 217 | + showMonthAfterYear: #{datepicker_options[:show_month_after_year].to_json}, | |
| 218 | + showOn: #{datepicker_options[:show_on].to_json}, | |
| 219 | + showOptions: #{datepicker_options[:show_options].to_json}, | |
| 220 | + showOtherMonths: #{datepicker_options[:show_other_months].to_json}, | |
| 221 | + showWeek: #{datepicker_options[:show_week].to_json}, | |
| 222 | + stepMonths: #{datepicker_options[:step_months].to_json}, | |
| 223 | + weekHeader: #{datepicker_options[:week_header].to_json}, | |
| 224 | + yearRange: #{datepicker_options[:year_range].to_json}, | |
| 225 | + yearSuffix: #{datepicker_options[:year_suffix].to_json} | |
| 226 | + }) | |
| 227 | + </script> | |
| 228 | + " | |
| 229 | + result | |
| 230 | + end | |
| 231 | + | |
| 232 | + def date_range_field(from_name, to_name, from_value, to_value, format = '%Y-%m-%d', datepicker_options = {}, html_options = {}) | |
| 233 | + from_id = html_options[:from_id] || 'datepicker-from-date' | |
| 234 | + to_id = html_options[:to_id] || 'datepicker-to-date' | |
| 235 | + return _('From') +' '+ date_field(from_name, from_value, format, datepicker_options, html_options.merge({:id => from_id})) + | |
| 236 | + ' ' + _('until') +' '+ date_field(to_name, to_value, format, datepicker_options, html_options.merge({:id => to_id})) | |
| 237 | + end | |
| 238 | + | |
| 126 | 239 | protected |
| 127 | 240 | def self.next_id_number |
| 128 | 241 | if defined? @@id_num | ... | ... |
app/models/task.rb
| ... | ... | @@ -31,7 +31,7 @@ class Task < ActiveRecord::Base |
| 31 | 31 | end |
| 32 | 32 | end |
| 33 | 33 | |
| 34 | - belongs_to :requestor, :class_name => 'Person', :foreign_key => :requestor_id | |
| 34 | + belongs_to :requestor, :class_name => 'Profile', :foreign_key => :requestor_id | |
| 35 | 35 | belongs_to :target, :foreign_key => :target_id, :polymorphic => true |
| 36 | 36 | |
| 37 | 37 | validates_uniqueness_of :code, :on => :create | ... | ... |
app/views/layouts/_javascript.rhtml
| ... | ... | @@ -2,7 +2,8 @@ |
| 2 | 2 | 'jquery.noconflict.js', 'jquery.cycle.all.min.js', 'thickbox.js', 'lightbox', 'colorbox', |
| 3 | 3 | 'jquery-ui-1.8.2.custom.min', 'jquery.scrollTo', 'jquery.form.js', 'jquery-validation/jquery.validate', |
| 4 | 4 | 'jquery.cookie', 'jquery.ba-bbq.min.js', 'reflection', 'jquery.tokeninput', |
| 5 | -'add-and-join', 'report-abuse', 'catalog', 'manage-products', :cache => 'cache-general' %> | |
| 5 | +'add-and-join', 'report-abuse', 'catalog', 'manage-products', | |
| 6 | +'jquery-ui-timepicker-addon', :cache => 'cache-general' %> | |
| 6 | 7 | |
| 7 | 8 | <% language = FastGettext.locale %> |
| 8 | 9 | <% %w{messages methods}.each do |type| %> | ... | ... |
app/views/tasks/_task.rhtml
| ... | ... | @@ -50,13 +50,13 @@ |
| 50 | 50 | <% fields_for "tasks[#{task.id}][task]", task do |f| %> |
| 51 | 51 | <% if task.accept_details %> |
| 52 | 52 | <div id="on-accept-information-<%=task.id%>" style="display: none"> |
| 53 | - <%= render :partial => partial_for_task_class(task.class, :accept_details), :locals => {:task => task, :f => f} %> | |
| 53 | + <%= render :partial => partial_for_class(task.class, :accept_details), :locals => {:task => task, :f => f} %> | |
| 54 | 54 | </div> |
| 55 | 55 | <% end %> |
| 56 | 56 | |
| 57 | 57 | <% if task.reject_details %> |
| 58 | 58 | <div id="on-reject-information-<%=task.id%>" style="display: none"> |
| 59 | - <%= render :partial => partial_for_task_class(task.class, :reject_details), :locals => {:task => task, :f => f} %> | |
| 59 | + <%= render :partial => partial_for_class(task.class, :reject_details), :locals => {:task => task, :f => f} %> | |
| 60 | 60 | </div> |
| 61 | 61 | <% end %> |
| 62 | 62 | <% end %> | ... | ... |
plugins/custom_forms/controllers/custom_forms_plugin_myprofile_controller.rb
0 → 100644
| ... | ... | @@ -0,0 +1,145 @@ |
| 1 | +class CustomFormsPluginMyprofileController < MyProfileController | |
| 2 | + def index | |
| 3 | + @forms = CustomFormsPlugin::Form.from(profile) | |
| 4 | + end | |
| 5 | + | |
| 6 | + def create | |
| 7 | + @form = CustomFormsPlugin::Form.new(:profile => profile) | |
| 8 | + @fields = [] | |
| 9 | + @empty_field = CustomFormsPlugin::Field.new | |
| 10 | + if request.post? | |
| 11 | + begin | |
| 12 | + @form.update_attributes!(params[:form]) | |
| 13 | + params[:fields] = format_kind(params[:fields]) | |
| 14 | + params[:fields] = format_choices(params[:fields]) | |
| 15 | + params[:fields] = set_form_id(params[:fields], @form.id) | |
| 16 | + create_fields(new_fields(params)) | |
| 17 | + session['notice'] = _('Form created') | |
| 18 | + redirect_to :action => 'index' | |
| 19 | + rescue Exception => exception | |
| 20 | + logger.error(exception.to_s) | |
| 21 | + session['notice'] = _('Form could not be created') | |
| 22 | + end | |
| 23 | + end | |
| 24 | + end | |
| 25 | + | |
| 26 | + def edit | |
| 27 | + @form = CustomFormsPlugin::Form.find(params[:id]) | |
| 28 | + @fields = @form.fields | |
| 29 | + @empty_field = CustomFormsPlugin::TextField.new | |
| 30 | + if request.post? | |
| 31 | + begin | |
| 32 | + @form.update_attributes!(params[:form]) | |
| 33 | + params[:fields] = format_kind(params[:fields]) | |
| 34 | + params[:fields] = format_choices(params[:fields]) | |
| 35 | + remove_fields(params, @form) | |
| 36 | + create_fields(new_fields(params)) | |
| 37 | + update_fields(edited_fields(params)) | |
| 38 | + session['notice'] = _('Form updated') | |
| 39 | + redirect_to :action => 'index' | |
| 40 | + rescue Exception => exception | |
| 41 | + logger.error(exception.to_s) | |
| 42 | + session['notice'] = _('Form could not be updated') | |
| 43 | + end | |
| 44 | + end | |
| 45 | + end | |
| 46 | + | |
| 47 | + def remove | |
| 48 | + @form = CustomFormsPlugin::Form.find(params[:id]) | |
| 49 | + begin | |
| 50 | + @form.destroy | |
| 51 | + session[:notice] = _('Form removed') | |
| 52 | + rescue | |
| 53 | + session[:notice] = _('Form could not be removed') | |
| 54 | + end | |
| 55 | + redirect_to :action => 'index' | |
| 56 | + end | |
| 57 | + | |
| 58 | + def submissions | |
| 59 | + @form = CustomFormsPlugin::Form.find(params[:id]) | |
| 60 | + @submissions = @form.submissions | |
| 61 | + end | |
| 62 | + | |
| 63 | + def show_submission | |
| 64 | + @submission = CustomFormsPlugin::Submission.find(params[:id]) | |
| 65 | + @form = @submission.form | |
| 66 | + end | |
| 67 | + | |
| 68 | + private | |
| 69 | + | |
| 70 | + def new_fields(params) | |
| 71 | + result = params[:fields].map {|id, hash| hash.has_key?(:real_id) ? nil : hash}.compact | |
| 72 | + result.delete_if {|field| field[:name].blank?} | |
| 73 | + result | |
| 74 | + end | |
| 75 | + | |
| 76 | + def edited_fields(params) | |
| 77 | + params[:fields].map {|id, hash| hash.has_key?(:real_id) ? hash : nil}.compact | |
| 78 | + end | |
| 79 | + | |
| 80 | + def create_fields(fields) | |
| 81 | + fields.each do |field| | |
| 82 | + case field[:type] | |
| 83 | + when 'text_field' | |
| 84 | + CustomFormsPlugin::TextField.create!(field) | |
| 85 | + when 'select_field' | |
| 86 | + CustomFormsPlugin::SelectField.create!(field) | |
| 87 | + else | |
| 88 | + CustomFormsPlugin::Field.create!(field) | |
| 89 | + end | |
| 90 | + end | |
| 91 | + end | |
| 92 | + | |
| 93 | + def update_fields(fields) | |
| 94 | + fields.each do |field_attrs| | |
| 95 | + field = CustomFormsPlugin::Field.find(field_attrs.delete(:real_id)) | |
| 96 | + field.attributes = field_attrs | |
| 97 | + field.save! if field.changed? | |
| 98 | + end | |
| 99 | + end | |
| 100 | + | |
| 101 | + def format_kind(fields) | |
| 102 | + fields.each do |id, field| | |
| 103 | + next if field[:kind].blank? | |
| 104 | + kind = field.delete(:kind) | |
| 105 | + case kind | |
| 106 | + when 'radio' | |
| 107 | + field[:list] = false | |
| 108 | + field[:multiple] = false | |
| 109 | + when 'check_box' | |
| 110 | + field[:list] = false | |
| 111 | + field[:multiple] = true | |
| 112 | + when 'select' | |
| 113 | + field[:list] = true | |
| 114 | + field[:multiple] = false | |
| 115 | + when 'multiple_select' | |
| 116 | + field[:list] = true | |
| 117 | + field[:multiple] = true | |
| 118 | + end | |
| 119 | + end | |
| 120 | + fields | |
| 121 | + end | |
| 122 | + | |
| 123 | + def format_choices(fields) | |
| 124 | + fields.each do |id, field| | |
| 125 | + next if !field.has_key?(:choices) | |
| 126 | + field[:choices] = field[:choices].map {|key, value| value}.inject({}) do |result, choice| | |
| 127 | + hash = (choice[:name].blank? || choice[:value].blank?) ? {} : {choice[:name] => choice[:value]} | |
| 128 | + result.merge!(hash) | |
| 129 | + end | |
| 130 | + end | |
| 131 | + fields | |
| 132 | + end | |
| 133 | + | |
| 134 | + def remove_fields(params, form) | |
| 135 | + present_fields = params[:fields].map{|id, value| value}.collect {|field| field[:real_id]}.compact | |
| 136 | + form.fields.each {|field| field.destroy if !present_fields.include?(field.id.to_s) } | |
| 137 | + end | |
| 138 | + | |
| 139 | + def set_form_id(fields, form_id) | |
| 140 | + fields.each do |id, field| | |
| 141 | + field[:form_id] = form_id | |
| 142 | + end | |
| 143 | + fields | |
| 144 | + end | |
| 145 | +end | ... | ... |
plugins/custom_forms/controllers/custom_forms_plugin_profile_controller.rb
0 → 100644
| ... | ... | @@ -0,0 +1,45 @@ |
| 1 | +class CustomFormsPluginProfileController < ProfileController | |
| 2 | + | |
| 3 | + before_filter :has_access, :show | |
| 4 | + | |
| 5 | + def show | |
| 6 | + @form = CustomFormsPlugin::Form.find(params[:id]) | |
| 7 | + if user | |
| 8 | + @submission ||= CustomFormsPlugin::Submission.find_by_form_id_and_profile_id(@form.id,user.id) | |
| 9 | + @submission ||= CustomFormsPlugin::Submission.new(:form_id => @form.id, :profile_id => user.id) | |
| 10 | + else | |
| 11 | + @submission ||= CustomFormsPlugin::Submission.new(:form_id => @form.id) | |
| 12 | + end | |
| 13 | + if request.post? | |
| 14 | + begin | |
| 15 | + extend(CustomFormsPlugin::Helper) | |
| 16 | + answers = build_answers(params[:submission], @form) | |
| 17 | + failed_answers = answers.select {|answer| !answer.valid? } | |
| 18 | + if failed_answers.empty? | |
| 19 | + if !user | |
| 20 | + @submission.author_name = params[:author_name] | |
| 21 | + @submission.author_email = params[:author_email] | |
| 22 | + end | |
| 23 | + @submission.save! | |
| 24 | + answers.map {|answer| answer.submission = @submission; answer.save!} | |
| 25 | + else | |
| 26 | + @submission.valid? | |
| 27 | + failed_answers.each do |answer| | |
| 28 | + @submission.errors.add(answer.field.name.to_sym, answer.errors[answer.field.slug.to_sym]) | |
| 29 | + end | |
| 30 | + end | |
| 31 | + session[:notice] = _('Submission saved') | |
| 32 | + redirect_to :action => 'show' | |
| 33 | + rescue | |
| 34 | + session[:notice] = _('Submission could not be saved') | |
| 35 | + end | |
| 36 | + end | |
| 37 | + end | |
| 38 | + | |
| 39 | + private | |
| 40 | + | |
| 41 | + def has_access | |
| 42 | + form = CustomFormsPlugin::Form.find(params[:id]) | |
| 43 | + render_access_denied if !form.accessible_to(user) | |
| 44 | + end | |
| 45 | +end | ... | ... |
plugins/custom_forms/db/migrate/20120727162444_create_custom_forms_plugin_forms.rb
0 → 100644
| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | +class CreateCustomFormsPluginForms < ActiveRecord::Migration | |
| 2 | + def self.up | |
| 3 | + create_table :custom_forms_plugin_forms do |t| | |
| 4 | + t.string :name | |
| 5 | + t.string :slug | |
| 6 | + t.text :description | |
| 7 | + t.references :profile | |
| 8 | + t.datetime :begining | |
| 9 | + t.datetime :ending | |
| 10 | + t.boolean :report_submissions, :default => false | |
| 11 | + t.boolean :on_membership, :default => false | |
| 12 | + t.string :access | |
| 13 | + t.timestamps | |
| 14 | + end | |
| 15 | + end | |
| 16 | + | |
| 17 | + def self.down | |
| 18 | + drop_table :custom_forms_plugin_forms | |
| 19 | + end | |
| 20 | +end | ... | ... |
plugins/custom_forms/db/migrate/20120727174506_create_custom_forms_plugin_fields.rb
0 → 100644
| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | +class CreateCustomFormsPluginFields < ActiveRecord::Migration | |
| 2 | + def self.up | |
| 3 | + create_table :custom_forms_plugin_fields do |t| | |
| 4 | + t.string :name | |
| 5 | + t.string :slug | |
| 6 | + t.string :type | |
| 7 | + t.string :default_value | |
| 8 | + t.string :choices | |
| 9 | + t.float :minimum | |
| 10 | + t.float :maximum | |
| 11 | + t.references :form | |
| 12 | + t.boolean :mandatory, :default => false | |
| 13 | + t.boolean :multiple | |
| 14 | + t.boolean :list | |
| 15 | + end | |
| 16 | + end | |
| 17 | + | |
| 18 | + def self.down | |
| 19 | + drop_table :custom_forms_plugin_fields | |
| 20 | + end | |
| 21 | +end | ... | ... |
plugins/custom_forms/db/migrate/20120727175250_create_custom_forms_plugin_answers.rb
0 → 100644
| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | +class CreateCustomFormsPluginAnswers < ActiveRecord::Migration | |
| 2 | + def self.up | |
| 3 | + create_table :custom_forms_plugin_answers do |t| | |
| 4 | + t.text :value | |
| 5 | + t.references :field | |
| 6 | + t.references :submission | |
| 7 | + end | |
| 8 | + end | |
| 9 | + | |
| 10 | + def self.down | |
| 11 | + drop_table :custom_forms_plugin_answers | |
| 12 | + end | |
| 13 | +end | ... | ... |
plugins/custom_forms/db/migrate/20120727180512_create_custom_forms_plugin_submissions.rb
0 → 100644
| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | +class CreateCustomFormsPluginSubmissions < ActiveRecord::Migration | |
| 2 | + def self.up | |
| 3 | + create_table :custom_forms_plugin_submissions do |t| | |
| 4 | + t.string :author_name | |
| 5 | + t.string :author_email | |
| 6 | + t.references :profile | |
| 7 | + t.references :form | |
| 8 | + t.timestamps | |
| 9 | + end | |
| 10 | + end | |
| 11 | + | |
| 12 | + def self.down | |
| 13 | + drop_table :custom_forms_plugin_submissions | |
| 14 | + end | |
| 15 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | +require 'ext/role_assignment_trigger' | |
| 2 | + | |
| 3 | +class CustomFormsPlugin < Noosfero::Plugin | |
| 4 | + | |
| 5 | + def self.plugin_name | |
| 6 | + "Custom Forms" | |
| 7 | + end | |
| 8 | + | |
| 9 | + def self.plugin_description | |
| 10 | + _("Enables the creation of forms.") | |
| 11 | + end | |
| 12 | + | |
| 13 | + def stylesheet? | |
| 14 | + true | |
| 15 | + end | |
| 16 | + | |
| 17 | + def control_panel_buttons | |
| 18 | + {:title => _('Manage Forms'), :icon => 'custom-forms', :url => {:controller => 'custom_forms_plugin_myprofile'}} | |
| 19 | + end | |
| 20 | + | |
| 21 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | +class CustomFormsPlugin::Answer < Noosfero::Plugin::ActiveRecord | |
| 2 | + belongs_to :field, :class_name => 'CustomFormsPlugin::Field' | |
| 3 | + belongs_to :submission, :class_name => 'CustomFormsPlugin::Submission' | |
| 4 | + | |
| 5 | + validates_presence_of :field | |
| 6 | + validate :value_mandatory, :if => 'field.present?' | |
| 7 | + | |
| 8 | + def value_mandatory | |
| 9 | + if field.mandatory && value.blank? | |
| 10 | + errors.add(field.slug.to_sym, _("is mandatory.").fix_i18n) | |
| 11 | + end | |
| 12 | + end | |
| 13 | +end | |
| 14 | + | ... | ... |
| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | +class CustomFormsPlugin::Field < ActiveRecord::Base | |
| 2 | + set_table_name :custom_forms_plugin_fields | |
| 3 | + | |
| 4 | + validates_presence_of :form, :name | |
| 5 | + validates_uniqueness_of :slug, :scope => :form_id | |
| 6 | + | |
| 7 | + belongs_to :form, :class_name => 'CustomFormsPlugin::Form', :dependent => :destroy | |
| 8 | + has_many :answers, :class_name => 'CustomFormsPlugin::Answer' | |
| 9 | + | |
| 10 | + serialize :choices, Hash | |
| 11 | + | |
| 12 | + before_validation do |field| | |
| 13 | + field.slug = field.name.to_slug if field.name.present? | |
| 14 | + end | |
| 15 | +end | |
| 16 | + | ... | ... |
| ... | ... | @@ -0,0 +1,70 @@ |
| 1 | +class CustomFormsPlugin::Form < Noosfero::Plugin::ActiveRecord | |
| 2 | + belongs_to :profile | |
| 3 | + | |
| 4 | + has_many :fields, :class_name => 'CustomFormsPlugin::Field' | |
| 5 | + has_many :submissions, :class_name => 'CustomFormsPlugin::Submission' | |
| 6 | + | |
| 7 | + serialize :access | |
| 8 | + | |
| 9 | + validates_presence_of :profile, :name | |
| 10 | + validates_uniqueness_of :slug, :scope => :profile_id | |
| 11 | + validate :access_format | |
| 12 | + | |
| 13 | + before_validation do |form| | |
| 14 | + form.slug = form.name.to_slug if form.name.present? | |
| 15 | + form.access = nil if form.access.blank? | |
| 16 | + end | |
| 17 | + | |
| 18 | + named_scope :from, lambda {|profile| {:conditions => {:profile_id => profile.id}}} | |
| 19 | + named_scope :on_memberships, {:conditions => {:on_membership => true}} | |
| 20 | +=begin | |
| 21 | + named_scope :accessible_to lambda do |profile| | |
| 22 | + #TODO should verify is profile is associated with the form owner | |
| 23 | + profile_associated = ??? | |
| 24 | + {:conditions => [" | |
| 25 | + access IS NULL OR | |
| 26 | + (access='logged' AND :profile_present) OR | |
| 27 | + (access='associated' AND :profile_associated) OR | |
| 28 | + :profile_id in access | |
| 29 | + ", {:profile_present => profile.present?, :profile_associated => ???, :profile_id => profile.id}]} | |
| 30 | + end | |
| 31 | +=end | |
| 32 | + | |
| 33 | + def expired? | |
| 34 | + (begining.present? && Time.now < begining) || (ending.present? && Time.now > ending) | |
| 35 | + end | |
| 36 | + | |
| 37 | + def accessible_to(target) | |
| 38 | + return true if access.nil? || target == profile | |
| 39 | + return false if target.nil? | |
| 40 | + return true if access == 'logged' | |
| 41 | + return true if access == 'associated' && ((profile.organization? && profile.members.include?(target)) || (profile.person? && profile.friends.include?(target))) | |
| 42 | + return true if access.kind_of?(Integer) && target.id == access | |
| 43 | + return true if access.kind_of?(Array) && access.include?(target.id) | |
| 44 | + end | |
| 45 | + | |
| 46 | + private | |
| 47 | + | |
| 48 | + def access_format | |
| 49 | + if access.present? | |
| 50 | + if access.kind_of?(String) | |
| 51 | + if access != 'logged' && access != 'associated' | |
| 52 | + errors.add(:access, _('Invalid string format of access.')) | |
| 53 | + end | |
| 54 | + elsif access.kind_of?(Integer) | |
| 55 | + if !Profile.exists?(access) | |
| 56 | + errors.add(:access, _('There is no profile with the provided id.')) | |
| 57 | + end | |
| 58 | + elsif access.kind_of?(Array) | |
| 59 | + access.each do |value| | |
| 60 | + if !value.kind_of?(Integer) || !Profile.exists?(value) | |
| 61 | + errors.add(:access, _('There is no profile with the provided id.')) | |
| 62 | + break | |
| 63 | + end | |
| 64 | + end | |
| 65 | + else | |
| 66 | + errors.add(:access, _('Invalid type format of access.')) | |
| 67 | + end | |
| 68 | + end | |
| 69 | + end | |
| 70 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,119 @@ |
| 1 | +module CustomFormsPlugin::Helper | |
| 2 | + def access_text(form) | |
| 3 | + return _('Public') if form.access.nil? | |
| 4 | + return _('Logged users') if form.access == 'logged' | |
| 5 | + if form.access == 'associated' | |
| 6 | + return _('Members') if form.profile.organization? | |
| 7 | + return _('Friends') if form.profile.person? | |
| 8 | + end | |
| 9 | + return _('Custom') | |
| 10 | + end | |
| 11 | + | |
| 12 | + def period_range(form) | |
| 13 | + if form.begining.blank? && form.ending.blank? | |
| 14 | + _('Always') | |
| 15 | + elsif form.begining.present? && form.ending.blank? | |
| 16 | + ('From %s') % time_format(form.begining) | |
| 17 | + elsif form.begining.blank? && form.ending.present? | |
| 18 | + _('Until %s') % time_format(form.ending) | |
| 19 | + elsif form.begining.present? && form.ending.present? | |
| 20 | + _('From %s until %s') % [time_format(form.begining), time_format(form.ending)] | |
| 21 | + end | |
| 22 | + end | |
| 23 | + | |
| 24 | + def time_format(time) | |
| 25 | + minutes = (time.min == 0) ? '' : ':%M' | |
| 26 | + hour = (time.hour == 0 && minutes.blank?) ? '' : ' %H' | |
| 27 | + h = hour.blank? ? '' : 'h' | |
| 28 | + time.strftime("%Y-%m-%d#{hour+minutes+h}") | |
| 29 | + end | |
| 30 | + | |
| 31 | + # TODO add the custom option that should offer the user the hability to | |
| 32 | + # choose the profiles one by one, using something like tokeninput | |
| 33 | + def access_options(profile) | |
| 34 | + associated = profile.organization? ? _('Members') : _('Friends') | |
| 35 | + [ | |
| 36 | + [_('Public'), nil ], | |
| 37 | + [_('Logged users'), 'logged' ], | |
| 38 | + [ associated, 'associated'], | |
| 39 | + ] | |
| 40 | + end | |
| 41 | + | |
| 42 | + def type_options | |
| 43 | + [ | |
| 44 | + [_('Text'), 'text_field' ], | |
| 45 | + [_('Select'), 'select_field'] | |
| 46 | + ] | |
| 47 | + end | |
| 48 | + | |
| 49 | + def type_to_label(type) | |
| 50 | + map = { | |
| 51 | + 'text_field' => _('Text'), | |
| 52 | + 'select_field' => _('Select') | |
| 53 | + } | |
| 54 | + map[type_for_options(type)] | |
| 55 | + end | |
| 56 | + | |
| 57 | + def type_for_options(type) | |
| 58 | + type.to_s.split(':').last.underscore | |
| 59 | + end | |
| 60 | + | |
| 61 | + def display_custom_field(field, submission, form) | |
| 62 | + answer = submission.answers.select{|answer| answer.field == field}.first | |
| 63 | + field_tag = send("display_#{type_for_options(field.class)}",field, answer, form) | |
| 64 | + if field.mandatory? && !radio_button?(field) && !check_box?(field) && submission.id.nil? | |
| 65 | + required(labelled_form_field(field.name, field_tag)) | |
| 66 | + else | |
| 67 | + labelled_form_field(field.name, field_tag) | |
| 68 | + end | |
| 69 | + end | |
| 70 | + | |
| 71 | + def display_text_field(field, answer, form) | |
| 72 | + value = answer.present? ? answer.value : field.default_value | |
| 73 | + text_field(form, field.name.to_slug, :value => value, :disabled => answer.present?) | |
| 74 | + end | |
| 75 | + | |
| 76 | + def display_select_field(field, answer, form) | |
| 77 | + if field.list && field.multiple | |
| 78 | + selected = answer.present? ? answer.value.split(',') : [] | |
| 79 | + select_tag "#{form}[#{field.name.to_slug}]", options_for_select(field.choices.to_a, selected), :multiple => true, :size => field.choices.size, :disabled => answer.present? | |
| 80 | + elsif !field.list && field.multiple | |
| 81 | + field.choices.map do |name, value| | |
| 82 | + default = answer.present? ? answer.value.split(',').include?(value) : false | |
| 83 | + labelled_check_box name, "#{form}[#{field.name.to_slug}][#{value}]", '1', default, :disabled => answer.present? | |
| 84 | + end.join("\n") | |
| 85 | + elsif field.list && !field.multiple | |
| 86 | + selected = answer.present? ? answer.value.split(',') : [] | |
| 87 | + select_tag "#{form}[#{field.name.to_slug}]", options_for_select([['','']] + field.choices.to_a, selected), :disabled => answer.present? | |
| 88 | + elsif !field.list && !field.multiple | |
| 89 | + field.choices.map do |name, value| | |
| 90 | + default = answer.present? ? answer.value == value : true | |
| 91 | + labelled_radio_button name, "#{form}[#{field.name.to_slug}]", value, default, :disabled => answer.present? | |
| 92 | + end.join("\n") | |
| 93 | + end | |
| 94 | + end | |
| 95 | + | |
| 96 | + def radio_button?(field) | |
| 97 | + type_for_options(field.class) == 'select_field' && !field.list && !field.multiple | |
| 98 | + end | |
| 99 | + | |
| 100 | + def check_box?(field) | |
| 101 | + type_for_options(field.class) == 'select_field' && !field.list && field.multiple | |
| 102 | + end | |
| 103 | + | |
| 104 | + def build_answers(submission, form) | |
| 105 | + answers = [] | |
| 106 | + submission.each do |slug, value| | |
| 107 | + field = form.fields.select {|field| field.slug==slug}.first | |
| 108 | + if value.kind_of?(String) | |
| 109 | + final_value = value | |
| 110 | + elsif value.kind_of?(Array) | |
| 111 | + final_value = value.join(',') | |
| 112 | + elsif value.kind_of?(Hash) | |
| 113 | + final_value = value.map {|option, present| present == '1' ? option : nil}.compact.join(',') | |
| 114 | + end | |
| 115 | + answers << CustomFormsPlugin::Answer.new(:field => field, :value => final_value) | |
| 116 | + end | |
| 117 | + answers | |
| 118 | + end | |
| 119 | +end | ... | ... |
plugins/custom_forms/lib/custom_forms_plugin/membership_survey.rb
0 → 100644
| ... | ... | @@ -0,0 +1,47 @@ |
| 1 | +class CustomFormsPlugin::MembershipSurvey < Task | |
| 2 | + | |
| 3 | + settings_items :form_id, :submission | |
| 4 | + validates_presence_of :form_id | |
| 5 | + | |
| 6 | + include CustomFormsPlugin::Helper | |
| 7 | + | |
| 8 | + def perform | |
| 9 | + form = CustomFormsPlugin::Form.find(form_id) | |
| 10 | + answers = build_answers(submission, form) | |
| 11 | + s = CustomFormsPlugin::Submission.create!(:form => form, :profile => target) | |
| 12 | + answers.map {|answer| answer.submission = s; answer.save!} | |
| 13 | + end | |
| 14 | + | |
| 15 | + def title | |
| 16 | + _("Membership survey") | |
| 17 | + end | |
| 18 | + | |
| 19 | + def subject | |
| 20 | + nil | |
| 21 | + end | |
| 22 | + | |
| 23 | + def linked_subject | |
| 24 | + nil | |
| 25 | + end | |
| 26 | + | |
| 27 | + def information | |
| 28 | + {:message => _('%{requestor} wants you to fill in some information.')} | |
| 29 | + end | |
| 30 | + | |
| 31 | + def accept_details | |
| 32 | + true | |
| 33 | + end | |
| 34 | + | |
| 35 | + def icon | |
| 36 | + {:type => :profile_image, :profile => requestor, :url => requestor.url} | |
| 37 | + end | |
| 38 | + | |
| 39 | + def target_notification_message | |
| 40 | + _('After joining %{requestor}, the administrators of this organization | |
| 41 | + wants you to fill in some further information.') % {:requestor => requestor.name} | |
| 42 | + end | |
| 43 | + | |
| 44 | + def target_notification_description | |
| 45 | + _('%{requestor} wants to fill in some further information.') % {:requestor => requestor.name} | |
| 46 | + end | |
| 47 | +end | ... | ... |
plugins/custom_forms/lib/custom_forms_plugin/select_field.rb
0 → 100644
plugins/custom_forms/lib/custom_forms_plugin/submission.rb
0 → 100644
| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | +class CustomFormsPlugin::Submission < Noosfero::Plugin::ActiveRecord | |
| 2 | + belongs_to :form, :class_name => 'CustomFormsPlugin::Form' | |
| 3 | + belongs_to :profile | |
| 4 | + | |
| 5 | + has_many :answers, :class_name => 'CustomFormsPlugin::Answer' | |
| 6 | + | |
| 7 | + validates_presence_of :form | |
| 8 | + validates_presence_of :author_name, :author_email, :if => lambda {|submission| submission.profile.nil?} | |
| 9 | + validates_uniqueness_of :author_email, :scope => :form_id, :allow_nil => true | |
| 10 | + validates_format_of :author_email, :with => Noosfero::Constants::EMAIL_FORMAT, :if => (lambda {|submission| !submission.author_email.blank?}) | |
| 11 | +end | |
| 12 | + | ... | ... |
plugins/custom_forms/lib/custom_forms_plugin/text_field.rb
0 → 100644
| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | +module RoleAssignmentTrigger | |
| 2 | + def self.included(base) | |
| 3 | + base.class_eval do | |
| 4 | + before_create do |ra| | |
| 5 | + profile = ra.resource | |
| 6 | + person = ra.accessor | |
| 7 | + ok = !profile.nil? && !person.nil? && profile.environment.present? | |
| 8 | + if ok && profile.environment.plugin_enabled?(CustomFormsPlugin) && !person.is_member_of?(profile) | |
| 9 | + CustomFormsPlugin::Form.from(profile).on_memberships.each do |form| | |
| 10 | + CustomFormsPlugin::MembershipSurvey.create!(:requestor => profile, :target => person, :form_id => form.id) | |
| 11 | + end | |
| 12 | + end | |
| 13 | + end | |
| 14 | + | |
| 15 | + after_destroy do |ra| | |
| 16 | + profile = ra.resource | |
| 17 | + person = ra.accessor | |
| 18 | + ok = !profile.nil? && !person.nil? && profile.environment.present? | |
| 19 | + if ok && profile.environment.plugin_enabled?(CustomFormsPlugin) && !person.is_member_of?(profile) | |
| 20 | + CustomFormsPlugin::Form.from(profile).on_memberships.each do |form| | |
| 21 | + task = person.tasks.pending.select {|task| task.kind_of?(CustomFormsPlugin::MembershipSurvey) && task.form_id == form.id}.first | |
| 22 | + task.cancel if task | |
| 23 | + end | |
| 24 | + end | |
| 25 | + end | |
| 26 | + end | |
| 27 | + end | |
| 28 | +end | |
| 29 | + | |
| 30 | +RoleAssignment.send :include, RoleAssignmentTrigger | ... | ... |
| ... | ... | @@ -0,0 +1,141 @@ |
| 1 | +jQuery('.icon-edit').live('click', function() { | |
| 2 | + elem = this; | |
| 3 | + jQuery.fn.colorbox({ | |
| 4 | + overlayClose: false, | |
| 5 | + escKey: false, | |
| 6 | + inline: true, | |
| 7 | + href: function(){ | |
| 8 | + id = jQuery(elem).attr('field_id'); | |
| 9 | + type = jQuery('#fields_'+id+'_type').val().split('_')[0]; | |
| 10 | + selector = '#edit-'+type+'-'+id | |
| 11 | + jQuery(selector).show(); | |
| 12 | + return selector | |
| 13 | + } | |
| 14 | + }); | |
| 15 | + return false; | |
| 16 | +}); | |
| 17 | + | |
| 18 | +jQuery('.remove-field').live('click', function(){ | |
| 19 | + id = jQuery(this).attr('field_id'); | |
| 20 | + jQuery('#field-'+id).slideDown(function(){ | |
| 21 | + jQuery('#field-'+id).remove(); | |
| 22 | + }); | |
| 23 | + return false | |
| 24 | +}); | |
| 25 | + | |
| 26 | +jQuery('.remove-option').live('click', function(){ | |
| 27 | + field_id = jQuery(this).attr('field_id'); | |
| 28 | + option_id = jQuery(this).attr('option_id'); | |
| 29 | + selector = '#field-'+field_id+'-option-'+option_id | |
| 30 | + jQuery(selector).slideDown(function(){ | |
| 31 | + jQuery(selector).remove(); | |
| 32 | + jQuery.colorbox.resize(); | |
| 33 | + }); | |
| 34 | + return false | |
| 35 | +}); | |
| 36 | + | |
| 37 | +function updateEditText(id){ | |
| 38 | + new_id = id+1 | |
| 39 | + jQuery('#edit-text-'+id).attr('id', 'edit-text-'+new_id); | |
| 40 | + input = jQuery('#edit-text-'+new_id+' input'); | |
| 41 | + jQuery('#edit-text-'+new_id+' .colorbox-ok-button').attr('div_id', 'edit-text-'+new_id); | |
| 42 | + input.attr('id', input.attr('id').replace(id,new_id)); | |
| 43 | + input.attr('name', input.attr('name').replace(id,new_id)); | |
| 44 | + label = jQuery('#edit-text-'+new_id+' label'); | |
| 45 | + label.attr('for', label.attr('for').replace(id,new_id)); | |
| 46 | +} | |
| 47 | + | |
| 48 | +function updateEditSelect(id){ | |
| 49 | + new_id = id+1 | |
| 50 | + jQuery('#edit-select-'+id).attr('id', 'edit-select-'+new_id); | |
| 51 | + jQuery('#edit-select-'+new_id+' .colorbox-ok-button').attr('div_id', 'edit-select-'+new_id); | |
| 52 | + jQuery('tr[id^=field-'+id+'-option').each(function(id, element){ | |
| 53 | + jQuery(element).attr('id', jQuery(element).attr('id').replace('field-'+id,'field-'+new_id)); | |
| 54 | + }); | |
| 55 | + jQuery('#edit-select-'+new_id+' label').each(function(index, element){ | |
| 56 | + label = jQuery(element); | |
| 57 | + label.attr('for', label.attr('for').replace(id,new_id)); | |
| 58 | + }); | |
| 59 | + jQuery('#edit-select-'+new_id+' input').each(function(index, element){ | |
| 60 | + input = jQuery(element); | |
| 61 | + input.attr('id', input.attr('id').replace(id,new_id)); | |
| 62 | + input.attr('name', input.attr('name').replace(id,new_id)); | |
| 63 | + }); | |
| 64 | + jQuery('#edit-select-'+new_id+' .remove-option').each(function(index, element){ | |
| 65 | + jQuery(element).attr('field_id',new_id); | |
| 66 | + }); | |
| 67 | + jQuery('#edit-select-'+new_id+' .new-option').attr('field_id',new_id); | |
| 68 | + jQuery('#edit-select-'+new_id+' #empty-option-'+id).attr('id','empty-option-'+new_id); | |
| 69 | +} | |
| 70 | + | |
| 71 | +function updateEmptyField(id){ | |
| 72 | + id = parseInt(id); | |
| 73 | + empty_field = jQuery('#empty-field'); | |
| 74 | + empty_field.attr('last_id', (id + 1).toString()); | |
| 75 | + jQuery('#empty-field input').each(function(index, element){ | |
| 76 | + new_id = jQuery(element).attr('id').replace(id,id+1); | |
| 77 | + jQuery(element).attr('id', new_id); | |
| 78 | + new_name = jQuery(element).attr('name').replace(id,id+1); | |
| 79 | + jQuery(element).attr('name', new_name); | |
| 80 | + }); | |
| 81 | + jQuery('#empty-field select').each(function(index, element){ | |
| 82 | + new_id = jQuery(element).attr('id').replace(id,id+1); | |
| 83 | + jQuery(element).attr('id', new_id); | |
| 84 | + new_name = jQuery(element).attr('name').replace(id,id+1); | |
| 85 | + jQuery(element).attr('name', new_name); | |
| 86 | + }); | |
| 87 | + jQuery('#empty-field a').each(function(index, element){ | |
| 88 | + jQuery(element).attr('field_id', id+1); | |
| 89 | + }); | |
| 90 | + updateEditText(id); | |
| 91 | + updateEditSelect(id); | |
| 92 | +} | |
| 93 | + | |
| 94 | +function updateEmptyOption(field_id, option_id){ | |
| 95 | + field_id = parseInt(field_id); | |
| 96 | + option_id = parseInt(option_id); | |
| 97 | + new_option_id = option_id+1; | |
| 98 | + empty_option = jQuery('#empty-option-'+field_id); | |
| 99 | + empty_option.attr('option_id',new_option_id); | |
| 100 | + jQuery('#empty-option-'+field_id+' .remove-option').attr('option_id', new_option_id); | |
| 101 | + | |
| 102 | + name_id = ' #fields_'+field_id+'_choices_'+option_id+'_name'; | |
| 103 | + jQuery('#empty-option-'+field_id+name_id).attr('name', 'fields['+field_id+'][choices]['+new_option_id+'][name]'); | |
| 104 | + jQuery('#empty-option-'+field_id+name_id).attr('id', 'fields_'+field_id+'_choices_'+new_option_id+'_name'); | |
| 105 | + | |
| 106 | + value_id = ' #fields_'+field_id+'_choices_'+option_id+'_value'; | |
| 107 | + jQuery('#empty-option-'+field_id+value_id).attr('name', 'fields['+field_id+'][choices]['+new_option_id+'][value]'); | |
| 108 | + jQuery('#empty-option-'+field_id+value_id).attr('id', 'fields_'+field_id+'_choices_'+new_option_id+'_value'); | |
| 109 | +} | |
| 110 | + | |
| 111 | +jQuery('#new-field').live('click', function(){ | |
| 112 | + empty_field = jQuery('#empty-field'); | |
| 113 | + id = empty_field.attr('last_id'); | |
| 114 | + edit_text = jQuery('#edit-text-'+id); | |
| 115 | + edit_select = jQuery('#edit-select-'+id); | |
| 116 | + new_field = empty_field.clone(); | |
| 117 | + new_field.attr('id','field-'+id); | |
| 118 | + new_field.insertBefore(empty_field).slideDown(); | |
| 119 | + edit_text.clone().insertAfter(edit_text); | |
| 120 | + edit_select.clone().insertAfter(edit_select); | |
| 121 | + updateEmptyField(id); | |
| 122 | + return false | |
| 123 | +}); | |
| 124 | + | |
| 125 | +jQuery('.new-option').live('click', function(){ | |
| 126 | + field_id = jQuery(this).attr('field_id'); | |
| 127 | + empty_option = jQuery('#empty-option-'+field_id); | |
| 128 | + option_id = empty_option.attr('option_id'); | |
| 129 | + new_option = empty_option.clone(); | |
| 130 | + new_option.attr('id','field-'+field_id+'-option-'+option_id); | |
| 131 | + new_option.insertBefore(empty_option).slideDown(); | |
| 132 | + jQuery.colorbox.resize(); | |
| 133 | + updateEmptyOption(field_id, option_id); | |
| 134 | + return false | |
| 135 | +}); | |
| 136 | + | |
| 137 | +jQuery('.colorbox-ok-button').live('click', function(){ | |
| 138 | + jQuery('#'+jQuery(this).attr('div_id')).hide(); | |
| 139 | + jQuery.colorbox.close(); | |
| 140 | + return false | |
| 141 | +}); | ... | ... |
4.05 KB
| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | +.controller-profile_editor a.control-panel-custom-forms, | |
| 2 | +.controller-profile_editor .msie6 a.control-panel-custom-forms { | |
| 3 | + background-image: url(/plugins/custom_forms/icons/custom-forms.png) | |
| 4 | +} | |
| 5 | + | |
| 6 | +.action-table { | |
| 7 | + width: 100%; | |
| 8 | + overflow: hidden; | |
| 9 | +} | |
| 10 | + | |
| 11 | +.action-table th, | |
| 12 | +.action-table td{ | |
| 13 | + text-align: center; | |
| 14 | +} | |
| 15 | + | |
| 16 | +.action-table td{ | |
| 17 | + cursor: move; | |
| 18 | +} | |
| 19 | + | |
| 20 | +.action-table .actions{ | |
| 21 | + white-space: nowrap; | |
| 22 | +} | |
| 23 | + | |
| 24 | +.action-table .new-item{ | |
| 25 | + background-color: #EEE; | |
| 26 | +} | |
| 27 | + | |
| 28 | +.edit-information { | |
| 29 | + display: none; | |
| 30 | +} | ... | ... |
plugins/custom_forms/test/functional/custom_forms_plugin_myprofile_controller_test.rb
0 → 100644
| ... | ... | @@ -0,0 +1,114 @@ |
| 1 | +require File.dirname(__FILE__) + '/../../../../test/test_helper' | |
| 2 | +require File.dirname(__FILE__) + '/../../controllers/custom_forms_plugin_myprofile_controller' | |
| 3 | + | |
| 4 | +# Re-raise errors caught by the controller. | |
| 5 | +class CustomFormsPluginMyprofileController; def rescue_action(e) raise e end; end | |
| 6 | + | |
| 7 | +class CustomFormsPluginMyprofileControllerTest < ActionController::TestCase | |
| 8 | + def setup | |
| 9 | + @controller = CustomFormsPluginMyprofileController.new | |
| 10 | + @request = ActionController::TestRequest.new | |
| 11 | + @response = ActionController::TestResponse.new | |
| 12 | + @profile = create_user('profile').person | |
| 13 | + login_as(@profile.identifier) | |
| 14 | + environment = Environment.default | |
| 15 | + environment.enable_plugin(CustomFormsPlugin) | |
| 16 | + end | |
| 17 | + | |
| 18 | + attr_reader :profile | |
| 19 | + | |
| 20 | + should 'list forms associated with profile' do | |
| 21 | + another_profile = fast_create(Profile) | |
| 22 | + f1 = CustomFormsPlugin::Form.create!(:profile => profile, :name => 'Free Software') | |
| 23 | + f2 = CustomFormsPlugin::Form.create!(:profile => profile, :name => 'Open Source') | |
| 24 | + f3 = CustomFormsPlugin::Form.create!(:profile => another_profile, :name => 'Open Source') | |
| 25 | + | |
| 26 | + get :index, :profile => profile.identifier | |
| 27 | + | |
| 28 | + assert_includes assigns(:forms), f1 | |
| 29 | + assert_includes assigns(:forms), f2 | |
| 30 | + assert_not_includes assigns(:forms), f3 | |
| 31 | + end | |
| 32 | + | |
| 33 | + should 'destroy form' do | |
| 34 | + form = CustomFormsPlugin::Form.create!(:profile => profile, :name => 'Free Software') | |
| 35 | + assert CustomFormsPlugin::Form.exists?(form.id) | |
| 36 | + post :remove, :profile => profile.identifier, :id => form.id | |
| 37 | + assert !CustomFormsPlugin::Form.exists?(form.id) | |
| 38 | + end | |
| 39 | + | |
| 40 | + should 'create a form' do | |
| 41 | + format = '%Y-%m-%d %H:%M' | |
| 42 | + begining = Time.now.strftime(format) | |
| 43 | + ending = (Time.now + 1.day).strftime('%Y-%m-%d %H:%M') | |
| 44 | + assert_difference CustomFormsPlugin::Form, :count, 1 do | |
| 45 | + post :create, :profile => profile.identifier, | |
| 46 | + :form => { | |
| 47 | + :name => 'My Form', | |
| 48 | + :access => 'logged', | |
| 49 | + :begining => begining, | |
| 50 | + :ending => ending, | |
| 51 | + :description => 'Cool form'}, | |
| 52 | + :fields => { | |
| 53 | + 1 => { | |
| 54 | + :name => 'Name', | |
| 55 | + :default_value => 'Jack', | |
| 56 | + :type => 'text_field' | |
| 57 | + }, | |
| 58 | + 2 => { | |
| 59 | + :name => 'Color', | |
| 60 | + :list => '1', | |
| 61 | + :type => 'select_field', | |
| 62 | + :choices => { | |
| 63 | + 1 => {:name => 'Red', :value => 'red'}, | |
| 64 | + 2 => {:name => 'Blue', :value => 'blue'}, | |
| 65 | + 3 => {:name => 'Black', :value => 'black'} | |
| 66 | + } | |
| 67 | + } | |
| 68 | + } | |
| 69 | + end | |
| 70 | + | |
| 71 | + form = CustomFormsPlugin::Form.find_by_name('My Form') | |
| 72 | + assert_equal 'logged', form.access | |
| 73 | + assert_equal begining, form.begining.strftime(format) | |
| 74 | + assert_equal ending, form.ending.strftime(format) | |
| 75 | + assert_equal 'Cool form', form.description | |
| 76 | + assert_equal 2, form.fields.count | |
| 77 | + | |
| 78 | + f1 = form.fields.first | |
| 79 | + f2 = form.fields.last | |
| 80 | + | |
| 81 | + assert_equal 'Name', f1.name | |
| 82 | + assert_equal 'Jack', f1.default_value | |
| 83 | + assert f1.kind_of?(CustomFormsPlugin::TextField) | |
| 84 | + | |
| 85 | + assert_equal 'Color', f2.name | |
| 86 | + assert_equal 'red', f2.choices['Red'] | |
| 87 | + assert_equal 'blue', f2.choices['Blue'] | |
| 88 | + assert_equal 'black', f2.choices['Black'] | |
| 89 | + assert f2.list | |
| 90 | + assert f2.kind_of?(CustomFormsPlugin::SelectField) | |
| 91 | + end | |
| 92 | + | |
| 93 | + should 'edit a form' do | |
| 94 | + form = CustomFormsPlugin::Form.create!(:profile => profile, :name => 'Free Software') | |
| 95 | + field = CustomFormsPlugin::TextField.create!(:form => form, :name => 'License') | |
| 96 | + format = '%Y-%m-%d %H:%M' | |
| 97 | + begining = Time.now.strftime(format) | |
| 98 | + ending = (Time.now + 1.day).strftime('%Y-%m-%d %H:%M') | |
| 99 | + | |
| 100 | + post :edit, :profile => profile.identifier, :id => form.id, | |
| 101 | + :form => {:name => 'My Form', :access => 'logged', :begining => begining, :ending => ending, :description => 'Cool form'}, | |
| 102 | + :fields => {1 => {:real_id => field.id.to_s, :name => 'Source'}} | |
| 103 | + | |
| 104 | + form.reload | |
| 105 | + field.reload | |
| 106 | + | |
| 107 | + assert_equal 'logged', form.access | |
| 108 | + assert_equal begining, form.begining.strftime(format) | |
| 109 | + assert_equal ending, form.ending.strftime(format) | |
| 110 | + assert_equal 'Cool form', form.description | |
| 111 | + assert_equal 'Source', field.name | |
| 112 | + end | |
| 113 | +end | |
| 114 | + | ... | ... |
plugins/custom_forms/test/unit/custom_forms_plugin/answer_test.rb
0 → 100644
| ... | ... | @@ -0,0 +1,38 @@ |
| 1 | +require File.dirname(__FILE__) + '/../../../../../test/test_helper' | |
| 2 | + | |
| 3 | +class CustomFormsPlugin::AnswerTest < ActiveSupport::TestCase | |
| 4 | + should 'validates presence of field' do | |
| 5 | + answer = CustomFormsPlugin::Answer.new | |
| 6 | + answer.valid? | |
| 7 | + assert answer.errors.invalid?(:field) | |
| 8 | + | |
| 9 | + form = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => fast_create(Profile)) | |
| 10 | + field = CustomFormsPlugin::Field.create!(:name => 'License', :form => form) | |
| 11 | + answer.field = field | |
| 12 | + answer.valid? | |
| 13 | + assert !answer.errors.invalid?(:field) | |
| 14 | + end | |
| 15 | + | |
| 16 | + should 'belong to a submission' do | |
| 17 | + form = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => fast_create(Profile)) | |
| 18 | + submission = CustomFormsPlugin::Submission.create!(:form => form, :profile => fast_create(Profile)) | |
| 19 | + answer = CustomFormsPlugin::Answer.new | |
| 20 | + answer.submission = submission | |
| 21 | + | |
| 22 | + assert_equal submission, answer.submission | |
| 23 | + end | |
| 24 | + | |
| 25 | + should 'require presence of value if field is mandatory' do | |
| 26 | + form = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => fast_create(Profile)) | |
| 27 | + field = CustomFormsPlugin::Field.create!(:name => 'License', :form => form, :mandatory => true) | |
| 28 | + answer = CustomFormsPlugin::Answer.new(:field => field) | |
| 29 | + answer.valid? | |
| 30 | + assert answer.errors.invalid?(field.slug.to_sym) | |
| 31 | + | |
| 32 | + answer.value = "GPL" | |
| 33 | + answer.valid? | |
| 34 | + assert !answer.errors.invalid?(field.slug.to_sym) | |
| 35 | + end | |
| 36 | + | |
| 37 | +end | |
| 38 | + | ... | ... |
plugins/custom_forms/test/unit/custom_forms_plugin/field_test.rb
0 → 100644
| ... | ... | @@ -0,0 +1,64 @@ |
| 1 | +require File.dirname(__FILE__) + '/../../../../../test/test_helper' | |
| 2 | + | |
| 3 | +class CustomFormsPlugin::FieldTest < ActiveSupport::TestCase | |
| 4 | + should 'validate presence of form' do | |
| 5 | + field = CustomFormsPlugin::Field.new | |
| 6 | + field.valid? | |
| 7 | + assert field.errors.invalid?(:form) | |
| 8 | + assert field.errors.invalid?(:name) | |
| 9 | + | |
| 10 | + form = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => fast_create(Profile)) | |
| 11 | + field.form = form | |
| 12 | + field.name = 'License' | |
| 13 | + field.valid? | |
| 14 | + assert !field.errors.invalid?(:form) | |
| 15 | + assert !field.errors.invalid?(:name) | |
| 16 | + end | |
| 17 | + | |
| 18 | + should 'set slug before validation based on name' do | |
| 19 | + field = CustomFormsPlugin::Field.new(:name => 'Name') | |
| 20 | + field.valid? | |
| 21 | + assert_equal field.name.to_slug, field.slug | |
| 22 | + end | |
| 23 | + | |
| 24 | + should 'validate uniqueness of slug scoped on the form' do | |
| 25 | + form1 = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => fast_create(Profile)) | |
| 26 | + form2 = CustomFormsPlugin::Form.create!(:name => 'Open Source', :profile => fast_create(Profile)) | |
| 27 | + f1 = CustomFormsPlugin::Field.create!(:name => 'License', :form => form1) | |
| 28 | + f2 = CustomFormsPlugin::Field.new(:name => 'License', :form => form1) | |
| 29 | + f3 = CustomFormsPlugin::Field.new(:name => 'License', :form => form2) | |
| 30 | + | |
| 31 | + f2.valid? | |
| 32 | + f3.valid? | |
| 33 | + | |
| 34 | + assert f2.errors.invalid?(:slug) | |
| 35 | + assert !f3.errors.invalid?(:slug) | |
| 36 | + end | |
| 37 | + | |
| 38 | + should 'set mandatory field as false by default' do | |
| 39 | + field = CustomFormsPlugin::Field.new | |
| 40 | + assert !field.mandatory | |
| 41 | + end | |
| 42 | + | |
| 43 | + should 'have answers' do | |
| 44 | + form = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => fast_create(Profile)) | |
| 45 | + field = CustomFormsPlugin::Field.create!(:name => 'License', :form => form) | |
| 46 | + a1 = CustomFormsPlugin::Answer.create!(:field => field) | |
| 47 | + a2 = CustomFormsPlugin::Answer.create!(:field => field) | |
| 48 | + | |
| 49 | + assert_includes field.answers, a1 | |
| 50 | + assert_includes field.answers, a2 | |
| 51 | + end | |
| 52 | + | |
| 53 | + should 'serialize choices into a hash' do | |
| 54 | + form = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => fast_create(Profile)) | |
| 55 | + field = CustomFormsPlugin::Field.create!(:name => 'License', :form => form) | |
| 56 | + field.choices = {'First' => 1, 'Second' => 2, 'Third' => 3} | |
| 57 | + field.save! | |
| 58 | + | |
| 59 | + assert_equal 1, field.choices['First'] | |
| 60 | + assert_equal 2, field.choices['Second'] | |
| 61 | + assert_equal 3, field.choices['Third'] | |
| 62 | + end | |
| 63 | +end | |
| 64 | + | ... | ... |
plugins/custom_forms/test/unit/custom_forms_plugin/form_test.rb
0 → 100644
| ... | ... | @@ -0,0 +1,172 @@ |
| 1 | +require File.dirname(__FILE__) + '/../../../../../test/test_helper' | |
| 2 | + | |
| 3 | +class CustomFormsPlugin::FormTest < ActiveSupport::TestCase | |
| 4 | + should 'validates presence of a profile and a name' do | |
| 5 | + form = CustomFormsPlugin::Form.new | |
| 6 | + form.valid? | |
| 7 | + assert form.errors.invalid?(:profile) | |
| 8 | + assert form.errors.invalid?(:name) | |
| 9 | + | |
| 10 | + form.profile = fast_create(Profile) | |
| 11 | + form.name = 'Free Software' | |
| 12 | + form.valid? | |
| 13 | + assert !form.errors.invalid?(:profile) | |
| 14 | + assert !form.errors.invalid?(:name) | |
| 15 | + end | |
| 16 | + | |
| 17 | + should 'have many fields including fields subclasses' do | |
| 18 | + form = CustomFormsPlugin::Form.create!(:profile => fast_create(Profile), :name => 'Free Software') | |
| 19 | + f1 = CustomFormsPlugin::Field.create!(:form => form, :name => 'License') | |
| 20 | + f2 = CustomFormsPlugin::Field.create!(:form => form, :name => 'Code') | |
| 21 | + f3 = CustomFormsPlugin::TextField.create!(:form => form, :name => 'Developer') | |
| 22 | + | |
| 23 | + assert_includes form.fields, f1 | |
| 24 | + assert_includes form.fields, f2 | |
| 25 | + assert_includes form.fields, f3 | |
| 26 | + end | |
| 27 | + | |
| 28 | + should 'have many submissions' do | |
| 29 | + form = CustomFormsPlugin::Form.create!(:profile => fast_create(Profile), :name => 'Free Software') | |
| 30 | + s1 = CustomFormsPlugin::Submission.create!(:form => form, :profile => fast_create(Profile)) | |
| 31 | + s2 = CustomFormsPlugin::Submission.create!(:form => form, :profile => fast_create(Profile)) | |
| 32 | + | |
| 33 | + assert_includes form.submissions, s1 | |
| 34 | + assert_includes form.submissions, s2 | |
| 35 | + end | |
| 36 | + | |
| 37 | + should 'set slug before validation based on name' do | |
| 38 | + form = CustomFormsPlugin::Form.new(:name => 'Name') | |
| 39 | + form.valid? | |
| 40 | + assert_equal form.name.to_slug, form.slug | |
| 41 | + end | |
| 42 | + | |
| 43 | + should 'validates uniqueness of slug scoped on profile' do | |
| 44 | + profile = fast_create(Profile) | |
| 45 | + another_profile = fast_create(Profile) | |
| 46 | + CustomFormsPlugin::Form.create!(:profile => profile, :name => 'Free Software') | |
| 47 | + form = CustomFormsPlugin::Form.new(:profile => profile, :name => 'Free Software') | |
| 48 | + form.valid? | |
| 49 | + assert form.errors.invalid?(:slug) | |
| 50 | + | |
| 51 | + form.profile = another_profile | |
| 52 | + form.valid? | |
| 53 | + assert !form.errors.invalid?(:slug) | |
| 54 | + end | |
| 55 | + | |
| 56 | + should 'define form expiration' do | |
| 57 | + form = CustomFormsPlugin::Form.new | |
| 58 | + assert !form.expired? | |
| 59 | + | |
| 60 | + form.begining = Time.now + 1.day | |
| 61 | + assert form.expired? | |
| 62 | + | |
| 63 | + form.begining = Time.now - 1.day | |
| 64 | + assert !form.expired? | |
| 65 | + | |
| 66 | + form.begining = nil | |
| 67 | + form.ending = Time.now + 1.day | |
| 68 | + assert !form.expired? | |
| 69 | + | |
| 70 | + form.ending = Time.now - 1.day | |
| 71 | + assert form.expired? | |
| 72 | + | |
| 73 | + form.begining = Time.now - 1.day | |
| 74 | + form.ending = Time.now + 1.day | |
| 75 | + assert !form.expired? | |
| 76 | + end | |
| 77 | + | |
| 78 | + should 'validates format of access' do | |
| 79 | + form = CustomFormsPlugin::Form.new | |
| 80 | + form.valid? | |
| 81 | + assert !form.errors.invalid?(:access) | |
| 82 | + | |
| 83 | + form.access = 'bli' | |
| 84 | + form.valid? | |
| 85 | + assert form.errors.invalid?(:access) | |
| 86 | + | |
| 87 | + form.access = 'logged' | |
| 88 | + form.valid? | |
| 89 | + assert !form.errors.invalid?(:access) | |
| 90 | + | |
| 91 | + form.access = 'associated' | |
| 92 | + form.valid? | |
| 93 | + assert !form.errors.invalid?(:access) | |
| 94 | + | |
| 95 | + form.access = {:bli => 1} | |
| 96 | + form.valid? | |
| 97 | + assert form.errors.invalid?(:access) | |
| 98 | + | |
| 99 | + form.access = 999 | |
| 100 | + form.valid? | |
| 101 | + assert form.errors.invalid?(:access) | |
| 102 | + | |
| 103 | + p1 = fast_create(Profile) | |
| 104 | + form.access = p1.id | |
| 105 | + form.valid? | |
| 106 | + assert !form.errors.invalid?(:access) | |
| 107 | + | |
| 108 | + p2 = fast_create(Profile) | |
| 109 | + p3 = fast_create(Profile) | |
| 110 | + form.access = [p1,p2,p3].map(&:id) | |
| 111 | + form.valid? | |
| 112 | + assert !form.errors.invalid?(:access) | |
| 113 | + end | |
| 114 | + | |
| 115 | + should 'defines who is able to access the form' do | |
| 116 | + owner = fast_create(Community) | |
| 117 | + form = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => owner) | |
| 118 | + assert form.accessible_to(nil) | |
| 119 | + | |
| 120 | + form.access = 'logged' | |
| 121 | + assert !form.accessible_to(nil) | |
| 122 | + person = fast_create(Person) | |
| 123 | + assert form.accessible_to(person) | |
| 124 | + | |
| 125 | + form.access = 'associated' | |
| 126 | + assert !form.accessible_to(person) | |
| 127 | + owner.add_member(person) | |
| 128 | + assert form.accessible_to(person) | |
| 129 | + | |
| 130 | + p1 = fast_create(Profile) | |
| 131 | + form.access = p1.id | |
| 132 | + assert !form.accessible_to(person) | |
| 133 | + assert form.accessible_to(p1) | |
| 134 | + | |
| 135 | + p2 = fast_create(Profile) | |
| 136 | + form.access = [person.id, p1.id] | |
| 137 | + assert form.accessible_to(person) | |
| 138 | + assert form.accessible_to(p1) | |
| 139 | + assert !form.accessible_to(p2) | |
| 140 | + form.access << p2.id | |
| 141 | + assert form.accessible_to(p2) | |
| 142 | + | |
| 143 | + assert form.accessible_to(owner) | |
| 144 | + end | |
| 145 | + | |
| 146 | + should 'have a named_scope that retrieve forms from a profile' do | |
| 147 | + profile = fast_create(Profile) | |
| 148 | + another_profile = fast_create(Profile) | |
| 149 | + f1 = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => profile) | |
| 150 | + f2 = CustomFormsPlugin::Form.create!(:name => 'Open Source', :profile => profile) | |
| 151 | + f3 = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => another_profile) | |
| 152 | + scope = CustomFormsPlugin::Form.from(profile) | |
| 153 | + | |
| 154 | + assert_equal ActiveRecord::NamedScope::Scope, scope.class | |
| 155 | + assert_includes scope, f1 | |
| 156 | + assert_includes scope, f2 | |
| 157 | + assert_not_includes scope, f3 | |
| 158 | + end | |
| 159 | + | |
| 160 | + should 'have a named_scope that retrieves all forms that are triggered on membership' do | |
| 161 | + profile = fast_create(Profile) | |
| 162 | + f1 = CustomFormsPlugin::Form.create!(:name => 'On membership 1', :profile => profile, :on_membership => true) | |
| 163 | + f2 = CustomFormsPlugin::Form.create!(:name => 'On membership 2', :profile => profile, :on_membership => true) | |
| 164 | + f3 = CustomFormsPlugin::Form.create!(:name => 'Not on memberhsip', :profile => profile, :on_membership => false) | |
| 165 | + scope = CustomFormsPlugin::Form.from(profile).on_memberships | |
| 166 | + | |
| 167 | + assert_equal ActiveRecord::NamedScope::Scope, scope.class | |
| 168 | + assert_includes scope, f1 | |
| 169 | + assert_includes scope, f2 | |
| 170 | + assert_not_includes scope, f3 | |
| 171 | + end | |
| 172 | +end | ... | ... |
plugins/custom_forms/test/unit/custom_forms_plugin/membership_survey_test.rb
0 → 100644
| ... | ... | @@ -0,0 +1,31 @@ |
| 1 | +require File.dirname(__FILE__) + '/../../../../../test/test_helper' | |
| 2 | + | |
| 3 | +class CustomFormsPlugin::MembershipSurveyTest < ActiveSupport::TestCase | |
| 4 | + should 'validates presence of form_id' do | |
| 5 | + task = CustomFormsPlugin::MembershipSurvey.new | |
| 6 | + task.valid? | |
| 7 | + assert task.errors.invalid?(:form_id) | |
| 8 | + | |
| 9 | + task.form_id = 1 | |
| 10 | + task.valid? | |
| 11 | + assert !task.errors.invalid?(:form_id) | |
| 12 | + end | |
| 13 | + | |
| 14 | + should 'create submission with answers on perform' do | |
| 15 | + profile = fast_create(Profile) | |
| 16 | + person = fast_create(Person) | |
| 17 | + form = CustomFormsPlugin::Form.create!(:name => 'Simple Form', :profile => profile) | |
| 18 | + field = CustomFormsPlugin::Field.create!(:name => 'Name', :form => form) | |
| 19 | + task = CustomFormsPlugin::MembershipSurvey.create!(:form_id => form.id, :submission => {'name' => 'Jack'}, :target => person, :requestor => profile) | |
| 20 | + | |
| 21 | + assert_difference CustomFormsPlugin::Submission, :count, 1 do | |
| 22 | + task.finish | |
| 23 | + end | |
| 24 | + | |
| 25 | + submission = CustomFormsPlugin::Submission.last | |
| 26 | + assert_equal submission.answers.count, 1 | |
| 27 | + | |
| 28 | + answer = submission.answers.first | |
| 29 | + assert_equal answer.value, 'Jack' | |
| 30 | + end | |
| 31 | +end | ... | ... |
plugins/custom_forms/test/unit/custom_forms_plugin/select_field_test.rb
0 → 100644
| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | +require File.dirname(__FILE__) + '/../../../../../test/test_helper' | |
| 2 | + | |
| 3 | +class CustomFormsPlugin::SelectFieldTest < ActiveSupport::TestCase | |
| 4 | + should 'validate presence of choices, multiple and list' do | |
| 5 | + select = CustomFormsPlugin::SelectField.new | |
| 6 | + select.valid? | |
| 7 | + assert select.errors.invalid?(:choices) | |
| 8 | + | |
| 9 | + select.choices = {'label' => 'value'} | |
| 10 | + select.valid? | |
| 11 | + assert !select.errors.invalid?(:choices) | |
| 12 | + end | |
| 13 | +end | ... | ... |
plugins/custom_forms/test/unit/custom_forms_plugin/submission_test.rb
0 → 100644
| ... | ... | @@ -0,0 +1,46 @@ |
| 1 | +require File.dirname(__FILE__) + '/../../../../../test/test_helper' | |
| 2 | + | |
| 3 | +class CustomFormsPlugin::SubmissionTest < ActiveSupport::TestCase | |
| 4 | + should 'validates presence of form' do | |
| 5 | + submission = CustomFormsPlugin::Submission.new | |
| 6 | + submission.valid? | |
| 7 | + assert submission.errors.invalid?(:form) | |
| 8 | + | |
| 9 | + form = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => fast_create(Profile)) | |
| 10 | + submission.form = form | |
| 11 | + submission.valid? | |
| 12 | + assert !submission.errors.invalid?(:form) | |
| 13 | + end | |
| 14 | + | |
| 15 | + should 'belong to a profile' do | |
| 16 | + profile = fast_create(Profile) | |
| 17 | + submission = CustomFormsPlugin::Submission.new | |
| 18 | + submission.profile = profile | |
| 19 | + assert_equal profile, submission.profile | |
| 20 | + end | |
| 21 | + | |
| 22 | + should 'require presence of author name and email if profile is nil' do | |
| 23 | + submission = CustomFormsPlugin::Submission.new | |
| 24 | + submission.valid? | |
| 25 | + assert submission.errors.invalid?(:author_name) | |
| 26 | + assert submission.errors.invalid?(:author_email) | |
| 27 | + | |
| 28 | + submission.author_name = 'Jack Sparrow' | |
| 29 | + submission.author_email = 'jack@black-pearl.com' | |
| 30 | + submission.valid? | |
| 31 | + assert !submission.errors.invalid?(:author_name) | |
| 32 | + assert !submission.errors.invalid?(:author_email) | |
| 33 | + end | |
| 34 | + | |
| 35 | + should 'have answers' do | |
| 36 | + form = CustomFormsPlugin::Form.create!(:name => 'Free Software', :profile => fast_create(Profile)) | |
| 37 | + field = CustomFormsPlugin::Field.create!(:name => 'License', :form => form) | |
| 38 | + submission = CustomFormsPlugin::Submission.create!(:form => form, :profile => fast_create(Profile)) | |
| 39 | + a1 = CustomFormsPlugin::Answer.create!(:field => field, :submission => submission) | |
| 40 | + a2 = CustomFormsPlugin::Answer.create!(:field => field, :submission => submission) | |
| 41 | + | |
| 42 | + assert_includes submission.answers, a1 | |
| 43 | + assert_includes submission.answers, a2 | |
| 44 | + end | |
| 45 | +end | |
| 46 | + | ... | ... |
plugins/custom_forms/test/unit/ext/role_assingment_test.rb
0 → 100644
| ... | ... | @@ -0,0 +1,51 @@ |
| 1 | +require File.dirname(__FILE__) + '/../../../../../test/test_helper' | |
| 2 | + | |
| 3 | +class RoleAssignmentsTest < ActiveSupport::TestCase | |
| 4 | + should 'create membership_surveys on membership creation' do | |
| 5 | + environment = Environment.default | |
| 6 | + environment.enable_plugin(CustomFormsPlugin) | |
| 7 | + organization = fast_create(Organization) | |
| 8 | + person = fast_create(Person) | |
| 9 | + f1 = CustomFormsPlugin::Form.create!(:profile => organization, :name => 'Form 1', :on_membership => true) | |
| 10 | + f2 = CustomFormsPlugin::Form.create!(:profile => organization, :name => 'Form 2', :on_membership => true) | |
| 11 | + f3 = CustomFormsPlugin::Form.create!(:profile => organization, :name => 'Form 3', :on_membership => false) | |
| 12 | + | |
| 13 | + assert_difference CustomFormsPlugin::MembershipSurvey, :count, 2 do | |
| 14 | + organization.add_member(person) | |
| 15 | + end | |
| 16 | + end | |
| 17 | + | |
| 18 | + should 'create membership_survey on membership creation with form accessible to members only' do | |
| 19 | + environment = Environment.default | |
| 20 | + environment.enable_plugin(CustomFormsPlugin) | |
| 21 | + organization = fast_create(Organization) | |
| 22 | + person = fast_create(Person) | |
| 23 | + form = CustomFormsPlugin::Form.create!(:profile => organization, :name => 'Form', :on_membership => true, :access => 'associated') | |
| 24 | + | |
| 25 | + assert_difference CustomFormsPlugin::MembershipSurvey, :count, 1 do | |
| 26 | + organization.add_member(person) | |
| 27 | + end | |
| 28 | + end | |
| 29 | + | |
| 30 | + should 'cancel membership_surveys if membership is undone and task is active' do | |
| 31 | + environment = Environment.default | |
| 32 | + environment.enable_plugin(CustomFormsPlugin) | |
| 33 | + organization = fast_create(Organization) | |
| 34 | + person = fast_create(Person) | |
| 35 | + form = CustomFormsPlugin::Form.create!(:profile => organization, :name => 'Form', :on_membership => true) | |
| 36 | + organization.add_member(person) | |
| 37 | + | |
| 38 | + assert_difference CustomFormsPlugin::MembershipSurvey.pending, :count, -1 do | |
| 39 | + organization.remove_member(person) | |
| 40 | + end | |
| 41 | + | |
| 42 | + organization.add_member(person) | |
| 43 | + task = CustomFormsPlugin::MembershipSurvey.last | |
| 44 | + task.status = Task::Status::FINISHED | |
| 45 | + task.save! | |
| 46 | + assert_no_difference CustomFormsPlugin::MembershipSurvey.finished, :count do | |
| 47 | + organization.remove_member(person) | |
| 48 | + end | |
| 49 | + end | |
| 50 | +end | |
| 51 | + | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_myprofile/_edit_select.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,33 @@ |
| 1 | +<% elem_id = "edit-select-#{counter}" %> | |
| 2 | +<div id=<%= elem_id %> class='edit-information'> | |
| 3 | + <h1><%= _('Options') %></h1> | |
| 4 | + <table class='action-table' style='width: 420px'> | |
| 5 | + <tr> | |
| 6 | + <th style='width: 40%'><%= _('Name') %></th> | |
| 7 | + <th style='width: 40%'><%= _('Value') %></th> | |
| 8 | + <th style='width: 20%'><%= _('Actions') %></th> | |
| 9 | + </tr> | |
| 10 | + <% option_counter = 1 %> | |
| 11 | + <% (field.choices || {}).each do |name, value| %> | |
| 12 | + <%= render :partial => 'option', :locals => {:name => name, :value => value, :counter => counter, :option_counter => option_counter} %> | |
| 13 | + <% option_counter += 1 %> | |
| 14 | + <% end %> | |
| 15 | + <%= render :partial => 'empty_option', :locals => {:counter => counter, :option_counter => option_counter} %> | |
| 16 | + <tr class='new-item'> | |
| 17 | + <td colspan='3'><strong><%= link_to(_('NEW OPTION'), '#', :class => 'new-option', :field_id => counter)%></strong></td> | |
| 18 | + </tr> | |
| 19 | + </table> | |
| 20 | + | |
| 21 | + <h2><%= _('Type') %></h2> | |
| 22 | + <%#= labelled_check_box _('List'), "fields[#{counter}][list]", true, field.list %> | |
| 23 | + <%#= labelled_check_box _('Multiple'), "fields[#{counter}][multiple]", true, field.multiple %> | |
| 24 | + <%= labelled_radio_button 'Radio', "fields[#{counter}][kind]", 'radio', !field.multiple && !field.list %><br /> | |
| 25 | + <%= labelled_radio_button 'Check Box', "fields[#{counter}][kind]", 'check_box', field.multiple && !field.list %><br /> | |
| 26 | + <%= labelled_radio_button 'Select', "fields[#{counter}][kind]", 'select', !field.multiple && field.list %><br /> | |
| 27 | + <%= labelled_radio_button 'Multiple Select', "fields[#{counter}][kind]", 'multiple_select', field.multiple && field.list %><br /> | |
| 28 | + | |
| 29 | + <% button_bar do %> | |
| 30 | + <%= button :ok, _('Ok'), '#', :class => 'colorbox-ok-button', :div_id => elem_id %> | |
| 31 | + <% end %> | |
| 32 | +</div> | |
| 33 | + | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_myprofile/_edit_text.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | +<% elem_id = "edit-text-#{counter}" %> | |
| 2 | +<div id=<%= elem_id %> class='edit-information'> | |
| 3 | + <%= labelled_form_field _('Default value'), text_field("fields[#{counter}]", :default_value, :value => field.default_value) %> | |
| 4 | + | |
| 5 | + <% button_bar do %> | |
| 6 | + <%= button :ok, _('Ok'), '#', :class => 'colorbox-ok-button', :div_id => elem_id %> | |
| 7 | + <% end %> | |
| 8 | +</div> | |
| 9 | + | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_myprofile/_empty_field.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | +<tr id="empty-field" style='display: none' last_id=<%= counter %>> | |
| 2 | + <td style="text-align: left"><%= text_field "fields[#{counter}]", :name %></td> | |
| 3 | + <td><%= select "fields[#{counter}]", :type, type_options, :selected => type_for_options(field.class) %></td> | |
| 4 | + <td><%= check_box "fields[#{counter}]", :mandatory %></td> | |
| 5 | + <%= hidden_field "fields[#{counter}]", :form_id, :value => @form.id %> | |
| 6 | + <td class='actions'> | |
| 7 | + <%= button_without_text :edit, _('Edit'), '', :field_id => counter %> | |
| 8 | + <%= button_without_text :remove, _('Remove'), '#', :class => 'remove-field', :field_id => counter, :confirm => _('Are you sure you want to remove this field?') %> | |
| 9 | + </td> | |
| 10 | +</tr> | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_myprofile/_empty_option.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | +<tr id=<%= "empty-option-#{counter}" %> option_id=<%= option_counter %> style="display: none;"> | |
| 2 | + <td><%= text_field_tag("fields[#{counter}][choices][#{option_counter}][name]") %></td> | |
| 3 | + <td><%= text_field_tag("fields[#{counter}][choices][#{option_counter}][value]") %></td> | |
| 4 | + <td class='actions'> | |
| 5 | + <%= button_without_text :remove, _('Remove'), '#', :class => 'remove-option', :field_id => counter, :option_id => option_counter, :confirm => _('Are you sure you want to remove this option?') %> | |
| 6 | + </td> | |
| 7 | +</tr> | |
| 8 | + | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_myprofile/_field.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | +<tr id=<%= "field-#{counter}" %>> | |
| 2 | + <td style="text-align: left"><%= text_field "fields[#{counter}]", :name, :value => field.name %></td> | |
| 3 | + <td><%= type_to_label(field.type) %></td> | |
| 4 | + <%= hidden_field "fields[#{counter}]", :type, :value => type_for_options(field.class) %> | |
| 5 | + <td><%= check_box "fields[#{counter}]", :mandatory, :checked => field.mandatory %></td> | |
| 6 | + <%= hidden_field "fields[#{counter}]", :real_id, :value => field.id %> | |
| 7 | + <%= hidden_field "fields[#{counter}]", :form_id, :value => @form.id %> | |
| 8 | + <td class='actions'> | |
| 9 | + <%= button_without_text :edit, _('Edit'), '#', :field_id => counter %> | |
| 10 | + <%= button_without_text :remove, _('Remove'), '#', :class => 'remove-field', :field_id => counter, :confirm => _('Are you sure you want to remove this field?') %> | |
| 11 | + </td> | |
| 12 | +</tr> | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_myprofile/_form.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,54 @@ |
| 1 | +<% self.extend(CustomFormsPlugin::Helper) %> | |
| 2 | + | |
| 3 | +<%= error_messages_for :form %> | |
| 4 | + | |
| 5 | +<% form_for :form, @form do |f| %> | |
| 6 | + <%= required labelled_form_field _('Name'), f.text_field(:name) %> | |
| 7 | + <%= labelled_form_field(_('Período'), ( | |
| 8 | + date_range_field('form[begining]', 'form[ending]', @form.begining, @form.ending, | |
| 9 | + '%Y-%m-%d %H:%M', | |
| 10 | + { :time => true, :change_month => true, :change_year => true, | |
| 11 | + :date_format => 'yy-mm-dd', :time_format => 'hh:mm' }, | |
| 12 | + { :size => 14 }) | |
| 13 | + )) %> | |
| 14 | + <%= labelled_form_field _('Access'), f.select(:access, access_options(profile))%> | |
| 15 | + <% if profile.organization? %> | |
| 16 | + <%= labelled_form_field _('Triggered on membership'), f.check_box(:on_membership) %> | |
| 17 | + <% end %> | |
| 18 | + <%= labelled_form_field _('Description'), f.text_area(:description, :style => 'width: 100%') %> | |
| 19 | + | |
| 20 | + <h2><%= _('Fields') %></h2> | |
| 21 | + <table class="action-table" id='fields-table'> | |
| 22 | + <tr> | |
| 23 | + <th style='width: 40%'><%= _('Name') %></th> | |
| 24 | + <th style='width: 30%'><%= _('Type') %></th> | |
| 25 | + <th style='width: 10%'><%= _('Mandatory') %></th> | |
| 26 | + <th style='width: 20%'><%= _('Actions') %></th> | |
| 27 | + </tr> | |
| 28 | + <% counter = 1 %> | |
| 29 | + <% @fields.each do |field| %> | |
| 30 | + <%= render :partial => 'field', :locals => {:field => field, :counter => counter} %> | |
| 31 | + <% counter += 1 %> | |
| 32 | + <% end %> | |
| 33 | + <%= render :partial => 'empty_field', :locals => {:field => @empty_field, :counter => counter} %> | |
| 34 | + <tr class='new-item'> | |
| 35 | + <td colspan='5'><strong><%= link_to(_('NEW FIELD'), '#', :id => 'new-field')%></strong></td> | |
| 36 | + </tr> | |
| 37 | + </table> | |
| 38 | + | |
| 39 | + <% counter = 1 %> | |
| 40 | + <% @fields.each do |field| %> | |
| 41 | + <%= render :partial => 'edit_text', :locals => {:field => field, :counter => counter} %> | |
| 42 | + <%= render :partial => 'edit_select', :locals => {:field => field, :counter => counter} %> | |
| 43 | + <% counter += 1 %> | |
| 44 | + <% end %> | |
| 45 | + | |
| 46 | + <%= render :partial => 'edit_text', :locals => {:field => @empty_field, :counter => counter} %> | |
| 47 | + <%= render :partial => 'edit_select', :locals => {:field => @empty_field, :counter => counter} %> | |
| 48 | + | |
| 49 | + <% button_bar do %> | |
| 50 | + <%= submit_button :save, _('Save'), :cancel => {:action => 'index'}%> | |
| 51 | + <% end %> | |
| 52 | +<% end %> | |
| 53 | + | |
| 54 | +<%= javascript_include_tag '../plugins/custom_forms/field' %> | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_myprofile/_option.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | +<tr id=<%= "field-#{counter}-option-#{option_counter}" %> style="display: auto;"> | |
| 2 | + <td><%= text_field_tag("fields[#{counter}][choices][#{option_counter}][name]", name) %></td> | |
| 3 | + <td><%= text_field_tag("fields[#{counter}][choices][#{option_counter}][value]", value) %></td> | |
| 4 | + <td class='actions'> | |
| 5 | + <%= button_without_text :remove, _('Remove'), '#', :class => 'remove-option', :field_id => counter, :option_id => option_counter, :confirm => _('Are you sure you want to remove this option?') %> | |
| 6 | + </td> | |
| 7 | +</tr> | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_myprofile/create.html.erb
0 → 100644
plugins/custom_forms/views/custom_forms_plugin_myprofile/edit.html.erb
0 → 100644
plugins/custom_forms/views/custom_forms_plugin_myprofile/index.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,26 @@ |
| 1 | +<% self.extend CustomFormsPlugin::Helper %> | |
| 2 | + | |
| 3 | +<h1><%= _('Manage forms') %></h1> | |
| 4 | +<table class="action-table"> | |
| 5 | + <tr> | |
| 6 | + <th style='width: 40%'><%= _('Name') %></th> | |
| 7 | + <th style='width: 30%'><%= _('Period') %></th> | |
| 8 | + <th style='width: 10%'><%= _('Submissions') %></th> | |
| 9 | + <th style='width: 10%'><%= _('Access') %></th> | |
| 10 | + <th style='width: 10%'><%= _('Actions') %></th> | |
| 11 | + </tr> | |
| 12 | + <% @forms.each do |form| %> | |
| 13 | + <tr> | |
| 14 | + <td><%= form.name %></td> | |
| 15 | + <td><%= period_range(form) %></td> | |
| 16 | + <td><%= form.submissions.count > 0 ? link_to(form.submissions.count, {:action => 'submissions', :id => form.id}) : 0 %></td> | |
| 17 | + <td><%= access_text(form) %></td> | |
| 18 | + <td class="actions"> | |
| 19 | + <%= button_without_text :edit, _('Edit'), :action => 'edit', :id => form.id %> | |
| 20 | + <%= button_without_text :remove, _('Remove'), {:action => 'remove', :id => form.id}, :confirm => _('Are you sure you want to remove this form?') %></td> | |
| 21 | + </tr> | |
| 22 | + <% end %> | |
| 23 | + <tr id="new-item"> | |
| 24 | + <td colspan='5'><strong><%= link_to(_('NEW FORM'), :action => 'create')%></strong></td> | |
| 25 | + </tr> | |
| 26 | +</table> | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_myprofile/show_submission.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | +<h1><%= @form.name %></h1> | |
| 2 | +<p><%= @form.description %></p> | |
| 3 | + | |
| 4 | +<% fields_for :submission, @submission do |f| %> | |
| 5 | + <%= render :partial => 'shared/form_submission', :locals => {:f => f} %> | |
| 6 | +<% end %> | |
| 7 | + | |
| 8 | +<% button_bar do %> | |
| 9 | + <%= button :back, _('Back to submissions'), :action => 'submissions', :id => @form.id %> | |
| 10 | +<% end %> | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_myprofile/submissions.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | +<% self.extend CustomFormsPlugin::Helper %> | |
| 2 | + | |
| 3 | +<h1><%= _('Submissions for %s') % @form.name %></h1> | |
| 4 | + | |
| 5 | +<% if @form.submissions.empty? %> | |
| 6 | + <%= _('There are no submissions for this form.') %> | |
| 7 | +<% else %> | |
| 8 | + <table class="action-table"> | |
| 9 | + <tr> | |
| 10 | + <th style='width: 50%'><%= _('Author') %></th> | |
| 11 | + <th style='width: 50%'><%= _('Time') %></th> | |
| 12 | + </tr> | |
| 13 | + <% @submissions.each do |submission| %> | |
| 14 | + <tr> | |
| 15 | + <% author = submission.profile.present? ? submission.profile.name : submission.author_name %> | |
| 16 | + <td><%= link_to(author, {:action => 'show_submission', :id => submission.id}) %></td> | |
| 17 | + <td><%= time_format(submission.created_at) %></td> | |
| 18 | + </tr> | |
| 19 | + <% end %> | |
| 20 | + </table> | |
| 21 | +<% end %> | |
| 22 | + | |
| 23 | +<% button_bar do %> | |
| 24 | + <%= button :back, _('Back to forms'), :action => 'index' %> | |
| 25 | +<% end %> | ... | ... |
plugins/custom_forms/views/custom_forms_plugin_profile/show.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | +<h1><%= @form.name %></h1> | |
| 2 | +<p><%= @form.description %></p> | |
| 3 | + | |
| 4 | +<% if @submission.id.nil? %> | |
| 5 | + <%= error_messages_for :submission %> | |
| 6 | + | |
| 7 | + <% form_for :submission, @submission do |f| %> | |
| 8 | + <% if !user %> | |
| 9 | + <%= required labelled_form_field _('Author name'), text_field_tag(:author_name, @submission.author_name) %> | |
| 10 | + <%= required labelled_form_field _('Author email'), text_field_tag(:author_email, @submission.author_email) %> | |
| 11 | + <% end %> | |
| 12 | + | |
| 13 | + <%= render :partial => 'shared/form_submission', :locals => {:f => f} %> | |
| 14 | + | |
| 15 | + <% button_bar do %> | |
| 16 | + <%= submit_button :save, _('Save'), :cancel => {:action => 'index'} %> | |
| 17 | + <% end %> | |
| 18 | + <% end %> | |
| 19 | +<% else %> | |
| 20 | + <% fields_for :submission, @submission do |f| %> | |
| 21 | + <%= render :partial => 'shared/form_submission', :locals => {:f => f} %> | |
| 22 | + <% end %> | |
| 23 | +<% end %> | ... | ... |
plugins/custom_forms/views/shared/_form_submission.html.erb
0 → 100644
plugins/custom_forms/views/tasks/_membership_survey_accept_details.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1 @@ |
| 1 | +<%= f.object_name.to_s %> | ... | ... |
plugins/custom_forms/views/tasks/custom_forms_plugin/_membership_survey_accept_details.html.erb
0 → 100644
| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | +<% @form = CustomFormsPlugin::Form.find(task.form_id) %> | |
| 2 | +<% @submission = CustomFormsPlugin::Submission.new(:form_id => @form.id, :profile_id => user.id) %> | |
| 3 | + | |
| 4 | +<h2><%= @form.name %></h2> | |
| 5 | +<p><%= @form.description %></p> | |
| 6 | + | |
| 7 | +<% f.fields_for :submission do |fi| %> | |
| 8 | + <%#= fi.error_messages_for :submission %> | |
| 9 | + <%= render :partial => 'shared/form_submission', :locals => {:f => fi} %> | |
| 10 | +<% end %> | ... | ... |
| ... | ... | @@ -0,0 +1,1530 @@ |
| 1 | +/* | |
| 2 | +* jQuery timepicker addon | |
| 3 | +* By: Trent Richardson [http://trentrichardson.com] | |
| 4 | +* Version 1.0.1 | |
| 5 | +* Last Modified: 07/01/2012 | |
| 6 | +* | |
| 7 | +* Copyright 2012 Trent Richardson | |
| 8 | +* You may use this project under MIT or GPL licenses. | |
| 9 | +* http://trentrichardson.com/Impromptu/GPL-LICENSE.txt | |
| 10 | +* http://trentrichardson.com/Impromptu/MIT-LICENSE.txt | |
| 11 | +* | |
| 12 | +* HERES THE CSS: | |
| 13 | +* .ui-timepicker-div .ui-widget-header { margin-bottom: 8px; } | |
| 14 | +* .ui-timepicker-div dl { text-align: left; } | |
| 15 | +* .ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; } | |
| 16 | +* .ui-timepicker-div dl dd { margin: 0 10px 10px 65px; } | |
| 17 | +* .ui-timepicker-div td { font-size: 90%; } | |
| 18 | +* .ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; } | |
| 19 | +*/ | |
| 20 | + | |
| 21 | +/*jslint evil: true, maxlen: 300, white: false, undef: false, nomen: false, onevar: false */ | |
| 22 | + | |
| 23 | +(function($) { | |
| 24 | + | |
| 25 | +// Prevent "Uncaught RangeError: Maximum call stack size exceeded" | |
| 26 | +$.ui.timepicker = $.ui.timepicker || {}; | |
| 27 | +if ($.ui.timepicker.version) { | |
| 28 | + return; | |
| 29 | +} | |
| 30 | + | |
| 31 | +$.extend($.ui, { timepicker: { version: "1.0.1" } }); | |
| 32 | + | |
| 33 | +/* Time picker manager. | |
| 34 | + Use the singleton instance of this class, $.timepicker, to interact with the time picker. | |
| 35 | + Settings for (groups of) time pickers are maintained in an instance object, | |
| 36 | + allowing multiple different settings on the same page. */ | |
| 37 | + | |
| 38 | +function Timepicker() { | |
| 39 | + this.regional = []; // Available regional settings, indexed by language code | |
| 40 | + this.regional[''] = { // Default regional settings | |
| 41 | + currentText: 'Now', | |
| 42 | + closeText: 'Done', | |
| 43 | + ampm: false, | |
| 44 | + amNames: ['AM', 'A'], | |
| 45 | + pmNames: ['PM', 'P'], | |
| 46 | + timeFormat: 'hh:mm tt', | |
| 47 | + timeSuffix: '', | |
| 48 | + timeOnlyTitle: 'Choose Time', | |
| 49 | + timeText: 'Time', | |
| 50 | + hourText: 'Hour', | |
| 51 | + minuteText: 'Minute', | |
| 52 | + secondText: 'Second', | |
| 53 | + millisecText: 'Millisecond', | |
| 54 | + timezoneText: 'Time Zone' | |
| 55 | + }; | |
| 56 | + this._defaults = { // Global defaults for all the datetime picker instances | |
| 57 | + showButtonPanel: true, | |
| 58 | + timeOnly: false, | |
| 59 | + showHour: true, | |
| 60 | + showMinute: true, | |
| 61 | + showSecond: false, | |
| 62 | + showMillisec: false, | |
| 63 | + showTimezone: false, | |
| 64 | + showTime: true, | |
| 65 | + stepHour: 1, | |
| 66 | + stepMinute: 1, | |
| 67 | + stepSecond: 1, | |
| 68 | + stepMillisec: 1, | |
| 69 | + hour: 0, | |
| 70 | + minute: 0, | |
| 71 | + second: 0, | |
| 72 | + millisec: 0, | |
| 73 | + timezone: null, | |
| 74 | + useLocalTimezone: false, | |
| 75 | + defaultTimezone: "+0000", | |
| 76 | + hourMin: 0, | |
| 77 | + minuteMin: 0, | |
| 78 | + secondMin: 0, | |
| 79 | + millisecMin: 0, | |
| 80 | + hourMax: 23, | |
| 81 | + minuteMax: 59, | |
| 82 | + secondMax: 59, | |
| 83 | + millisecMax: 999, | |
| 84 | + minDateTime: null, | |
| 85 | + maxDateTime: null, | |
| 86 | + onSelect: null, | |
| 87 | + hourGrid: 0, | |
| 88 | + minuteGrid: 0, | |
| 89 | + secondGrid: 0, | |
| 90 | + millisecGrid: 0, | |
| 91 | + alwaysSetTime: true, | |
| 92 | + separator: ' ', | |
| 93 | + altFieldTimeOnly: true, | |
| 94 | + showTimepicker: true, | |
| 95 | + timezoneIso8601: false, | |
| 96 | + timezoneList: null, | |
| 97 | + addSliderAccess: false, | |
| 98 | + sliderAccessArgs: null | |
| 99 | + }; | |
| 100 | + $.extend(this._defaults, this.regional['']); | |
| 101 | +} | |
| 102 | + | |
| 103 | +$.extend(Timepicker.prototype, { | |
| 104 | + $input: null, | |
| 105 | + $altInput: null, | |
| 106 | + $timeObj: null, | |
| 107 | + inst: null, | |
| 108 | + hour_slider: null, | |
| 109 | + minute_slider: null, | |
| 110 | + second_slider: null, | |
| 111 | + millisec_slider: null, | |
| 112 | + timezone_select: null, | |
| 113 | + hour: 0, | |
| 114 | + minute: 0, | |
| 115 | + second: 0, | |
| 116 | + millisec: 0, | |
| 117 | + timezone: null, | |
| 118 | + defaultTimezone: "+0000", | |
| 119 | + hourMinOriginal: null, | |
| 120 | + minuteMinOriginal: null, | |
| 121 | + secondMinOriginal: null, | |
| 122 | + millisecMinOriginal: null, | |
| 123 | + hourMaxOriginal: null, | |
| 124 | + minuteMaxOriginal: null, | |
| 125 | + secondMaxOriginal: null, | |
| 126 | + millisecMaxOriginal: null, | |
| 127 | + ampm: '', | |
| 128 | + formattedDate: '', | |
| 129 | + formattedTime: '', | |
| 130 | + formattedDateTime: '', | |
| 131 | + timezoneList: null, | |
| 132 | + | |
| 133 | + /* Override the default settings for all instances of the time picker. | |
| 134 | + @param settings object - the new settings to use as defaults (anonymous object) | |
| 135 | + @return the manager object */ | |
| 136 | + setDefaults: function(settings) { | |
| 137 | + extendRemove(this._defaults, settings || {}); | |
| 138 | + return this; | |
| 139 | + }, | |
| 140 | + | |
| 141 | + //######################################################################## | |
| 142 | + // Create a new Timepicker instance | |
| 143 | + //######################################################################## | |
| 144 | + _newInst: function($input, o) { | |
| 145 | + var tp_inst = new Timepicker(), | |
| 146 | + inlineSettings = {}; | |
| 147 | + | |
| 148 | + for (var attrName in this._defaults) { | |
| 149 | + var attrValue = $input.attr('time:' + attrName); | |
| 150 | + if (attrValue) { | |
| 151 | + try { | |
| 152 | + inlineSettings[attrName] = eval(attrValue); | |
| 153 | + } catch (err) { | |
| 154 | + inlineSettings[attrName] = attrValue; | |
| 155 | + } | |
| 156 | + } | |
| 157 | + } | |
| 158 | + tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, o, { | |
| 159 | + beforeShow: function(input, dp_inst) { | |
| 160 | + if ($.isFunction(o.beforeShow)) { | |
| 161 | + return o.beforeShow(input, dp_inst, tp_inst); | |
| 162 | + } | |
| 163 | + }, | |
| 164 | + onChangeMonthYear: function(year, month, dp_inst) { | |
| 165 | + // Update the time as well : this prevents the time from disappearing from the $input field. | |
| 166 | + tp_inst._updateDateTime(dp_inst); | |
| 167 | + if ($.isFunction(o.onChangeMonthYear)) { | |
| 168 | + o.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst); | |
| 169 | + } | |
| 170 | + }, | |
| 171 | + onClose: function(dateText, dp_inst) { | |
| 172 | + if (tp_inst.timeDefined === true && $input.val() !== '') { | |
| 173 | + tp_inst._updateDateTime(dp_inst); | |
| 174 | + } | |
| 175 | + if ($.isFunction(o.onClose)) { | |
| 176 | + o.onClose.call($input[0], dateText, dp_inst, tp_inst); | |
| 177 | + } | |
| 178 | + }, | |
| 179 | + timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker'); | |
| 180 | + }); | |
| 181 | + tp_inst.amNames = $.map(tp_inst._defaults.amNames, function(val) { return val.toUpperCase(); }); | |
| 182 | + tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function(val) { return val.toUpperCase(); }); | |
| 183 | + | |
| 184 | + if (tp_inst._defaults.timezoneList === null) { | |
| 185 | + var timezoneList = []; | |
| 186 | + for (var i = -11; i <= 12; i++) { | |
| 187 | + timezoneList.push((i >= 0 ? '+' : '-') + ('0' + Math.abs(i).toString()).slice(-2) + '00'); | |
| 188 | + } | |
| 189 | + if (tp_inst._defaults.timezoneIso8601) { | |
| 190 | + timezoneList = $.map(timezoneList, function(val) { | |
| 191 | + return val == '+0000' ? 'Z' : (val.substring(0, 3) + ':' + val.substring(3)); | |
| 192 | + }); | |
| 193 | + } | |
| 194 | + tp_inst._defaults.timezoneList = timezoneList; | |
| 195 | + } | |
| 196 | + | |
| 197 | + tp_inst.timezone = tp_inst._defaults.timezone; | |
| 198 | + tp_inst.hour = tp_inst._defaults.hour; | |
| 199 | + tp_inst.minute = tp_inst._defaults.minute; | |
| 200 | + tp_inst.second = tp_inst._defaults.second; | |
| 201 | + tp_inst.millisec = tp_inst._defaults.millisec; | |
| 202 | + tp_inst.ampm = ''; | |
| 203 | + tp_inst.$input = $input; | |
| 204 | + | |
| 205 | + if (o.altField) { | |
| 206 | + tp_inst.$altInput = $(o.altField) | |
| 207 | + .css({ cursor: 'pointer' }) | |
| 208 | + .focus(function(){ $input.trigger("focus"); }); | |
| 209 | + } | |
| 210 | + | |
| 211 | + if(tp_inst._defaults.minDate===0 || tp_inst._defaults.minDateTime===0) | |
| 212 | + { | |
| 213 | + tp_inst._defaults.minDate=new Date(); | |
| 214 | + } | |
| 215 | + if(tp_inst._defaults.maxDate===0 || tp_inst._defaults.maxDateTime===0) | |
| 216 | + { | |
| 217 | + tp_inst._defaults.maxDate=new Date(); | |
| 218 | + } | |
| 219 | + | |
| 220 | + // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime.. | |
| 221 | + if(tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date) { | |
| 222 | + tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime()); | |
| 223 | + } | |
| 224 | + if(tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date) { | |
| 225 | + tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime()); | |
| 226 | + } | |
| 227 | + if(tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date) { | |
| 228 | + tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime()); | |
| 229 | + } | |
| 230 | + if(tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date) { | |
| 231 | + tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime()); | |
| 232 | + } | |
| 233 | + return tp_inst; | |
| 234 | + }, | |
| 235 | + | |
| 236 | + //######################################################################## | |
| 237 | + // add our sliders to the calendar | |
| 238 | + //######################################################################## | |
| 239 | + _addTimePicker: function(dp_inst) { | |
| 240 | + var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ? | |
| 241 | + this.$input.val() + ' ' + this.$altInput.val() : | |
| 242 | + this.$input.val(); | |
| 243 | + | |
| 244 | + this.timeDefined = this._parseTime(currDT); | |
| 245 | + this._limitMinMaxDateTime(dp_inst, false); | |
| 246 | + this._injectTimePicker(); | |
| 247 | + }, | |
| 248 | + | |
| 249 | + //######################################################################## | |
| 250 | + // parse the time string from input value or _setTime | |
| 251 | + //######################################################################## | |
| 252 | + _parseTime: function(timeString, withDate) { | |
| 253 | + if (!this.inst) { | |
| 254 | + this.inst = $.datepicker._getInst(this.$input[0]); | |
| 255 | + } | |
| 256 | + | |
| 257 | + if (withDate || !this._defaults.timeOnly) | |
| 258 | + { | |
| 259 | + var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat'); | |
| 260 | + try { | |
| 261 | + var parseRes = parseDateTimeInternal(dp_dateFormat, this._defaults.timeFormat, timeString, $.datepicker._getFormatConfig(this.inst), this._defaults); | |
| 262 | + if (!parseRes.timeObj) { return false; } | |
| 263 | + $.extend(this, parseRes.timeObj); | |
| 264 | + } catch (err) | |
| 265 | + { | |
| 266 | + return false; | |
| 267 | + } | |
| 268 | + return true; | |
| 269 | + } | |
| 270 | + else | |
| 271 | + { | |
| 272 | + var timeObj = $.datepicker.parseTime(this._defaults.timeFormat, timeString, this._defaults); | |
| 273 | + if(!timeObj) { return false; } | |
| 274 | + $.extend(this, timeObj); | |
| 275 | + return true; | |
| 276 | + } | |
| 277 | + }, | |
| 278 | + | |
| 279 | + //######################################################################## | |
| 280 | + // generate and inject html for timepicker into ui datepicker | |
| 281 | + //######################################################################## | |
| 282 | + _injectTimePicker: function() { | |
| 283 | + var $dp = this.inst.dpDiv, | |
| 284 | + o = this._defaults, | |
| 285 | + tp_inst = this, | |
| 286 | + // Added by Peter Medeiros: | |
| 287 | + // - Figure out what the hour/minute/second max should be based on the step values. | |
| 288 | + // - Example: if stepMinute is 15, then minMax is 45. | |
| 289 | + hourMax = parseInt((o.hourMax - ((o.hourMax - o.hourMin) % o.stepHour)) ,10), | |
| 290 | + minMax = parseInt((o.minuteMax - ((o.minuteMax - o.minuteMin) % o.stepMinute)) ,10), | |
| 291 | + secMax = parseInt((o.secondMax - ((o.secondMax - o.secondMin) % o.stepSecond)) ,10), | |
| 292 | + millisecMax = parseInt((o.millisecMax - ((o.millisecMax - o.millisecMin) % o.stepMillisec)) ,10), | |
| 293 | + dp_id = this.inst.id.toString().replace(/([^A-Za-z0-9_])/g, ''); | |
| 294 | + | |
| 295 | + // Prevent displaying twice | |
| 296 | + //if ($dp.find("div#ui-timepicker-div-"+ dp_id).length === 0) { | |
| 297 | + if ($dp.find("div#ui-timepicker-div-"+ dp_id).length === 0 && o.showTimepicker) { | |
| 298 | + var noDisplay = ' style="display:none;"', | |
| 299 | + html = '<div class="ui-timepicker-div" id="ui-timepicker-div-' + dp_id + '"><dl>' + | |
| 300 | + '<dt class="ui_tpicker_time_label" id="ui_tpicker_time_label_' + dp_id + '"' + | |
| 301 | + ((o.showTime) ? '' : noDisplay) + '>' + o.timeText + '</dt>' + | |
| 302 | + '<dd class="ui_tpicker_time" id="ui_tpicker_time_' + dp_id + '"' + | |
| 303 | + ((o.showTime) ? '' : noDisplay) + '></dd>' + | |
| 304 | + '<dt class="ui_tpicker_hour_label" id="ui_tpicker_hour_label_' + dp_id + '"' + | |
| 305 | + ((o.showHour) ? '' : noDisplay) + '>' + o.hourText + '</dt>', | |
| 306 | + hourGridSize = 0, | |
| 307 | + minuteGridSize = 0, | |
| 308 | + secondGridSize = 0, | |
| 309 | + millisecGridSize = 0, | |
| 310 | + size = null; | |
| 311 | + | |
| 312 | + // Hours | |
| 313 | + html += '<dd class="ui_tpicker_hour"><div id="ui_tpicker_hour_' + dp_id + '"' + | |
| 314 | + ((o.showHour) ? '' : noDisplay) + '></div>'; | |
| 315 | + if (o.showHour && o.hourGrid > 0) { | |
| 316 | + html += '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>'; | |
| 317 | + | |
| 318 | + for (var h = o.hourMin; h <= hourMax; h += parseInt(o.hourGrid,10)) { | |
| 319 | + hourGridSize++; | |
| 320 | + var tmph = (o.ampm && h > 12) ? h-12 : h; | |
| 321 | + if (tmph < 10) { tmph = '0' + tmph; } | |
| 322 | + if (o.ampm) { | |
| 323 | + if (h === 0) { | |
| 324 | + tmph = 12 +'a'; | |
| 325 | + } else { | |
| 326 | + if (h < 12) { tmph += 'a'; } | |
| 327 | + else { tmph += 'p'; } | |
| 328 | + } | |
| 329 | + } | |
| 330 | + html += '<td>' + tmph + '</td>'; | |
| 331 | + } | |
| 332 | + | |
| 333 | + html += '</tr></table></div>'; | |
| 334 | + } | |
| 335 | + html += '</dd>'; | |
| 336 | + | |
| 337 | + // Minutes | |
| 338 | + html += '<dt class="ui_tpicker_minute_label" id="ui_tpicker_minute_label_' + dp_id + '"' + | |
| 339 | + ((o.showMinute) ? '' : noDisplay) + '>' + o.minuteText + '</dt>'+ | |
| 340 | + '<dd class="ui_tpicker_minute"><div id="ui_tpicker_minute_' + dp_id + '"' + | |
| 341 | + ((o.showMinute) ? '' : noDisplay) + '></div>'; | |
| 342 | + | |
| 343 | + if (o.showMinute && o.minuteGrid > 0) { | |
| 344 | + html += '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>'; | |
| 345 | + | |
| 346 | + for (var m = o.minuteMin; m <= minMax; m += parseInt(o.minuteGrid,10)) { | |
| 347 | + minuteGridSize++; | |
| 348 | + html += '<td>' + ((m < 10) ? '0' : '') + m + '</td>'; | |
| 349 | + } | |
| 350 | + | |
| 351 | + html += '</tr></table></div>'; | |
| 352 | + } | |
| 353 | + html += '</dd>'; | |
| 354 | + | |
| 355 | + // Seconds | |
| 356 | + html += '<dt class="ui_tpicker_second_label" id="ui_tpicker_second_label_' + dp_id + '"' + | |
| 357 | + ((o.showSecond) ? '' : noDisplay) + '>' + o.secondText + '</dt>'+ | |
| 358 | + '<dd class="ui_tpicker_second"><div id="ui_tpicker_second_' + dp_id + '"'+ | |
| 359 | + ((o.showSecond) ? '' : noDisplay) + '></div>'; | |
| 360 | + | |
| 361 | + if (o.showSecond && o.secondGrid > 0) { | |
| 362 | + html += '<div style="padding-left: 1px"><table><tr>'; | |
| 363 | + | |
| 364 | + for (var s = o.secondMin; s <= secMax; s += parseInt(o.secondGrid,10)) { | |
| 365 | + secondGridSize++; | |
| 366 | + html += '<td>' + ((s < 10) ? '0' : '') + s + '</td>'; | |
| 367 | + } | |
| 368 | + | |
| 369 | + html += '</tr></table></div>'; | |
| 370 | + } | |
| 371 | + html += '</dd>'; | |
| 372 | + | |
| 373 | + // Milliseconds | |
| 374 | + html += '<dt class="ui_tpicker_millisec_label" id="ui_tpicker_millisec_label_' + dp_id + '"' + | |
| 375 | + ((o.showMillisec) ? '' : noDisplay) + '>' + o.millisecText + '</dt>'+ | |
| 376 | + '<dd class="ui_tpicker_millisec"><div id="ui_tpicker_millisec_' + dp_id + '"'+ | |
| 377 | + ((o.showMillisec) ? '' : noDisplay) + '></div>'; | |
| 378 | + | |
| 379 | + if (o.showMillisec && o.millisecGrid > 0) { | |
| 380 | + html += '<div style="padding-left: 1px"><table><tr>'; | |
| 381 | + | |
| 382 | + for (var l = o.millisecMin; l <= millisecMax; l += parseInt(o.millisecGrid,10)) { | |
| 383 | + millisecGridSize++; | |
| 384 | + html += '<td>' + ((l < 10) ? '0' : '') + l + '</td>'; | |
| 385 | + } | |
| 386 | + | |
| 387 | + html += '</tr></table></div>'; | |
| 388 | + } | |
| 389 | + html += '</dd>'; | |
| 390 | + | |
| 391 | + // Timezone | |
| 392 | + html += '<dt class="ui_tpicker_timezone_label" id="ui_tpicker_timezone_label_' + dp_id + '"' + | |
| 393 | + ((o.showTimezone) ? '' : noDisplay) + '>' + o.timezoneText + '</dt>'; | |
| 394 | + html += '<dd class="ui_tpicker_timezone" id="ui_tpicker_timezone_' + dp_id + '"' + | |
| 395 | + ((o.showTimezone) ? '' : noDisplay) + '></dd>'; | |
| 396 | + | |
| 397 | + html += '</dl></div>'; | |
| 398 | + var $tp = $(html); | |
| 399 | + | |
| 400 | + // if we only want time picker... | |
| 401 | + if (o.timeOnly === true) { | |
| 402 | + $tp.prepend( | |
| 403 | + '<div class="ui-widget-header ui-helper-clearfix ui-corner-all">' + | |
| 404 | + '<div class="ui-datepicker-title">' + o.timeOnlyTitle + '</div>' + | |
| 405 | + '</div>'); | |
| 406 | + $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide(); | |
| 407 | + } | |
| 408 | + | |
| 409 | + this.hour_slider = $tp.find('#ui_tpicker_hour_'+ dp_id).slider({ | |
| 410 | + orientation: "horizontal", | |
| 411 | + value: this.hour, | |
| 412 | + min: o.hourMin, | |
| 413 | + max: hourMax, | |
| 414 | + step: o.stepHour, | |
| 415 | + slide: function(event, ui) { | |
| 416 | + tp_inst.hour_slider.slider( "option", "value", ui.value); | |
| 417 | + tp_inst._onTimeChange(); | |
| 418 | + } | |
| 419 | + }); | |
| 420 | + | |
| 421 | + | |
| 422 | + // Updated by Peter Medeiros: | |
| 423 | + // - Pass in Event and UI instance into slide function | |
| 424 | + this.minute_slider = $tp.find('#ui_tpicker_minute_'+ dp_id).slider({ | |
| 425 | + orientation: "horizontal", | |
| 426 | + value: this.minute, | |
| 427 | + min: o.minuteMin, | |
| 428 | + max: minMax, | |
| 429 | + step: o.stepMinute, | |
| 430 | + slide: function(event, ui) { | |
| 431 | + tp_inst.minute_slider.slider( "option", "value", ui.value); | |
| 432 | + tp_inst._onTimeChange(); | |
| 433 | + } | |
| 434 | + }); | |
| 435 | + | |
| 436 | + this.second_slider = $tp.find('#ui_tpicker_second_'+ dp_id).slider({ | |
| 437 | + orientation: "horizontal", | |
| 438 | + value: this.second, | |
| 439 | + min: o.secondMin, | |
| 440 | + max: secMax, | |
| 441 | + step: o.stepSecond, | |
| 442 | + slide: function(event, ui) { | |
| 443 | + tp_inst.second_slider.slider( "option", "value", ui.value); | |
| 444 | + tp_inst._onTimeChange(); | |
| 445 | + } | |
| 446 | + }); | |
| 447 | + | |
| 448 | + this.millisec_slider = $tp.find('#ui_tpicker_millisec_'+ dp_id).slider({ | |
| 449 | + orientation: "horizontal", | |
| 450 | + value: this.millisec, | |
| 451 | + min: o.millisecMin, | |
| 452 | + max: millisecMax, | |
| 453 | + step: o.stepMillisec, | |
| 454 | + slide: function(event, ui) { | |
| 455 | + tp_inst.millisec_slider.slider( "option", "value", ui.value); | |
| 456 | + tp_inst._onTimeChange(); | |
| 457 | + } | |
| 458 | + }); | |
| 459 | + | |
| 460 | + this.timezone_select = $tp.find('#ui_tpicker_timezone_'+ dp_id).append('<select></select>').find("select"); | |
| 461 | + $.fn.append.apply(this.timezone_select, | |
| 462 | + $.map(o.timezoneList, function(val, idx) { | |
| 463 | + return $("<option />") | |
| 464 | + .val(typeof val == "object" ? val.value : val) | |
| 465 | + .text(typeof val == "object" ? val.label : val); | |
| 466 | + }) | |
| 467 | + ); | |
| 468 | + if (typeof(this.timezone) != "undefined" && this.timezone !== null && this.timezone !== "") { | |
| 469 | + var local_date = new Date(this.inst.selectedYear, this.inst.selectedMonth, this.inst.selectedDay, 12); | |
| 470 | + var local_timezone = timeZoneString(local_date); | |
| 471 | + if (local_timezone == this.timezone) { | |
| 472 | + selectLocalTimeZone(tp_inst); | |
| 473 | + } else { | |
| 474 | + this.timezone_select.val(this.timezone); | |
| 475 | + } | |
| 476 | + } else { | |
| 477 | + if (typeof(this.hour) != "undefined" && this.hour !== null && this.hour !== "") { | |
| 478 | + this.timezone_select.val(o.defaultTimezone); | |
| 479 | + } else { | |
| 480 | + selectLocalTimeZone(tp_inst); | |
| 481 | + } | |
| 482 | + } | |
| 483 | + this.timezone_select.change(function() { | |
| 484 | + tp_inst._defaults.useLocalTimezone = false; | |
| 485 | + tp_inst._onTimeChange(); | |
| 486 | + }); | |
| 487 | + | |
| 488 | + // Add grid functionality | |
| 489 | + if (o.showHour && o.hourGrid > 0) { | |
| 490 | + size = 100 * hourGridSize * o.hourGrid / (hourMax - o.hourMin); | |
| 491 | + | |
| 492 | + $tp.find(".ui_tpicker_hour table").css({ | |
| 493 | + width: size + "%", | |
| 494 | + marginLeft: (size / (-2 * hourGridSize)) + "%", | |
| 495 | + borderCollapse: 'collapse' | |
| 496 | + }).find("td").each( function(index) { | |
| 497 | + $(this).click(function() { | |
| 498 | + var h = $(this).html(); | |
| 499 | + if(o.ampm) { | |
| 500 | + var ap = h.substring(2).toLowerCase(), | |
| 501 | + aph = parseInt(h.substring(0,2), 10); | |
| 502 | + if (ap == 'a') { | |
| 503 | + if (aph == 12) { h = 0; } | |
| 504 | + else { h = aph; } | |
| 505 | + } else if (aph == 12) { h = 12; } | |
| 506 | + else { h = aph + 12; } | |
| 507 | + } | |
| 508 | + tp_inst.hour_slider.slider("option", "value", h); | |
| 509 | + tp_inst._onTimeChange(); | |
| 510 | + tp_inst._onSelectHandler(); | |
| 511 | + }).css({ | |
| 512 | + cursor: 'pointer', | |
| 513 | + width: (100 / hourGridSize) + '%', | |
| 514 | + textAlign: 'center', | |
| 515 | + overflow: 'hidden' | |
| 516 | + }); | |
| 517 | + }); | |
| 518 | + } | |
| 519 | + | |
| 520 | + if (o.showMinute && o.minuteGrid > 0) { | |
| 521 | + size = 100 * minuteGridSize * o.minuteGrid / (minMax - o.minuteMin); | |
| 522 | + $tp.find(".ui_tpicker_minute table").css({ | |
| 523 | + width: size + "%", | |
| 524 | + marginLeft: (size / (-2 * minuteGridSize)) + "%", | |
| 525 | + borderCollapse: 'collapse' | |
| 526 | + }).find("td").each(function(index) { | |
| 527 | + $(this).click(function() { | |
| 528 | + tp_inst.minute_slider.slider("option", "value", $(this).html()); | |
| 529 | + tp_inst._onTimeChange(); | |
| 530 | + tp_inst._onSelectHandler(); | |
| 531 | + }).css({ | |
| 532 | + cursor: 'pointer', | |
| 533 | + width: (100 / minuteGridSize) + '%', | |
| 534 | + textAlign: 'center', | |
| 535 | + overflow: 'hidden' | |
| 536 | + }); | |
| 537 | + }); | |
| 538 | + } | |
| 539 | + | |
| 540 | + if (o.showSecond && o.secondGrid > 0) { | |
| 541 | + $tp.find(".ui_tpicker_second table").css({ | |
| 542 | + width: size + "%", | |
| 543 | + marginLeft: (size / (-2 * secondGridSize)) + "%", | |
| 544 | + borderCollapse: 'collapse' | |
| 545 | + }).find("td").each(function(index) { | |
| 546 | + $(this).click(function() { | |
| 547 | + tp_inst.second_slider.slider("option", "value", $(this).html()); | |
| 548 | + tp_inst._onTimeChange(); | |
| 549 | + tp_inst._onSelectHandler(); | |
| 550 | + }).css({ | |
| 551 | + cursor: 'pointer', | |
| 552 | + width: (100 / secondGridSize) + '%', | |
| 553 | + textAlign: 'center', | |
| 554 | + overflow: 'hidden' | |
| 555 | + }); | |
| 556 | + }); | |
| 557 | + } | |
| 558 | + | |
| 559 | + if (o.showMillisec && o.millisecGrid > 0) { | |
| 560 | + $tp.find(".ui_tpicker_millisec table").css({ | |
| 561 | + width: size + "%", | |
| 562 | + marginLeft: (size / (-2 * millisecGridSize)) + "%", | |
| 563 | + borderCollapse: 'collapse' | |
| 564 | + }).find("td").each(function(index) { | |
| 565 | + $(this).click(function() { | |
| 566 | + tp_inst.millisec_slider.slider("option", "value", $(this).html()); | |
| 567 | + tp_inst._onTimeChange(); | |
| 568 | + tp_inst._onSelectHandler(); | |
| 569 | + }).css({ | |
| 570 | + cursor: 'pointer', | |
| 571 | + width: (100 / millisecGridSize) + '%', | |
| 572 | + textAlign: 'center', | |
| 573 | + overflow: 'hidden' | |
| 574 | + }); | |
| 575 | + }); | |
| 576 | + } | |
| 577 | + | |
| 578 | + var $buttonPanel = $dp.find('.ui-datepicker-buttonpane'); | |
| 579 | + if ($buttonPanel.length) { $buttonPanel.before($tp); } | |
| 580 | + else { $dp.append($tp); } | |
| 581 | + | |
| 582 | + this.$timeObj = $tp.find('#ui_tpicker_time_'+ dp_id); | |
| 583 | + | |
| 584 | + if (this.inst !== null) { | |
| 585 | + var timeDefined = this.timeDefined; | |
| 586 | + this._onTimeChange(); | |
| 587 | + this.timeDefined = timeDefined; | |
| 588 | + } | |
| 589 | + | |
| 590 | + //Emulate datepicker onSelect behavior. Call on slidestop. | |
| 591 | + var onSelectDelegate = function() { | |
| 592 | + tp_inst._onSelectHandler(); | |
| 593 | + }; | |
| 594 | + this.hour_slider.bind('slidestop',onSelectDelegate); | |
| 595 | + this.minute_slider.bind('slidestop',onSelectDelegate); | |
| 596 | + this.second_slider.bind('slidestop',onSelectDelegate); | |
| 597 | + this.millisec_slider.bind('slidestop',onSelectDelegate); | |
| 598 | + | |
| 599 | + // slideAccess integration: http://trentrichardson.com/2011/11/11/jquery-ui-sliders-and-touch-accessibility/ | |
| 600 | + if (this._defaults.addSliderAccess){ | |
| 601 | + var sliderAccessArgs = this._defaults.sliderAccessArgs; | |
| 602 | + setTimeout(function(){ // fix for inline mode | |
| 603 | + if($tp.find('.ui-slider-access').length === 0){ | |
| 604 | + $tp.find('.ui-slider:visible').sliderAccess(sliderAccessArgs); | |
| 605 | + | |
| 606 | + // fix any grids since sliders are shorter | |
| 607 | + var sliderAccessWidth = $tp.find('.ui-slider-access:eq(0)').outerWidth(true); | |
| 608 | + if(sliderAccessWidth){ | |
| 609 | + $tp.find('table:visible').each(function(){ | |
| 610 | + var $g = $(this), | |
| 611 | + oldWidth = $g.outerWidth(), | |
| 612 | + oldMarginLeft = $g.css('marginLeft').toString().replace('%',''), | |
| 613 | + newWidth = oldWidth - sliderAccessWidth, | |
| 614 | + newMarginLeft = ((oldMarginLeft * newWidth)/oldWidth) + '%'; | |
| 615 | + | |
| 616 | + $g.css({ width: newWidth, marginLeft: newMarginLeft }); | |
| 617 | + }); | |
| 618 | + } | |
| 619 | + } | |
| 620 | + },0); | |
| 621 | + } | |
| 622 | + // end slideAccess integration | |
| 623 | + | |
| 624 | + } | |
| 625 | + }, | |
| 626 | + | |
| 627 | + //######################################################################## | |
| 628 | + // This function tries to limit the ability to go outside the | |
| 629 | + // min/max date range | |
| 630 | + //######################################################################## | |
| 631 | + _limitMinMaxDateTime: function(dp_inst, adjustSliders){ | |
| 632 | + var o = this._defaults, | |
| 633 | + dp_date = new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay); | |
| 634 | + | |
| 635 | + if(!this._defaults.showTimepicker) { return; } // No time so nothing to check here | |
| 636 | + | |
| 637 | + if($.datepicker._get(dp_inst, 'minDateTime') !== null && $.datepicker._get(dp_inst, 'minDateTime') !== undefined && dp_date){ | |
| 638 | + var minDateTime = $.datepicker._get(dp_inst, 'minDateTime'), | |
| 639 | + minDateTimeDate = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), 0, 0, 0, 0); | |
| 640 | + | |
| 641 | + if(this.hourMinOriginal === null || this.minuteMinOriginal === null || this.secondMinOriginal === null || this.millisecMinOriginal === null){ | |
| 642 | + this.hourMinOriginal = o.hourMin; | |
| 643 | + this.minuteMinOriginal = o.minuteMin; | |
| 644 | + this.secondMinOriginal = o.secondMin; | |
| 645 | + this.millisecMinOriginal = o.millisecMin; | |
| 646 | + } | |
| 647 | + | |
| 648 | + if(dp_inst.settings.timeOnly || minDateTimeDate.getTime() == dp_date.getTime()) { | |
| 649 | + this._defaults.hourMin = minDateTime.getHours(); | |
| 650 | + if (this.hour <= this._defaults.hourMin) { | |
| 651 | + this.hour = this._defaults.hourMin; | |
| 652 | + this._defaults.minuteMin = minDateTime.getMinutes(); | |
| 653 | + if (this.minute <= this._defaults.minuteMin) { | |
| 654 | + this.minute = this._defaults.minuteMin; | |
| 655 | + this._defaults.secondMin = minDateTime.getSeconds(); | |
| 656 | + } else if (this.second <= this._defaults.secondMin){ | |
| 657 | + this.second = this._defaults.secondMin; | |
| 658 | + this._defaults.millisecMin = minDateTime.getMilliseconds(); | |
| 659 | + } else { | |
| 660 | + if(this.millisec < this._defaults.millisecMin) { | |
| 661 | + this.millisec = this._defaults.millisecMin; | |
| 662 | + } | |
| 663 | + this._defaults.millisecMin = this.millisecMinOriginal; | |
| 664 | + } | |
| 665 | + } else { | |
| 666 | + this._defaults.minuteMin = this.minuteMinOriginal; | |
| 667 | + this._defaults.secondMin = this.secondMinOriginal; | |
| 668 | + this._defaults.millisecMin = this.millisecMinOriginal; | |
| 669 | + } | |
| 670 | + }else{ | |
| 671 | + this._defaults.hourMin = this.hourMinOriginal; | |
| 672 | + this._defaults.minuteMin = this.minuteMinOriginal; | |
| 673 | + this._defaults.secondMin = this.secondMinOriginal; | |
| 674 | + this._defaults.millisecMin = this.millisecMinOriginal; | |
| 675 | + } | |
| 676 | + } | |
| 677 | + | |
| 678 | + if($.datepicker._get(dp_inst, 'maxDateTime') !== null && $.datepicker._get(dp_inst, 'maxDateTime') !== undefined && dp_date){ | |
| 679 | + var maxDateTime = $.datepicker._get(dp_inst, 'maxDateTime'), | |
| 680 | + maxDateTimeDate = new Date(maxDateTime.getFullYear(), maxDateTime.getMonth(), maxDateTime.getDate(), 0, 0, 0, 0); | |
| 681 | + | |
| 682 | + if(this.hourMaxOriginal === null || this.minuteMaxOriginal === null || this.secondMaxOriginal === null){ | |
| 683 | + this.hourMaxOriginal = o.hourMax; | |
| 684 | + this.minuteMaxOriginal = o.minuteMax; | |
| 685 | + this.secondMaxOriginal = o.secondMax; | |
| 686 | + this.millisecMaxOriginal = o.millisecMax; | |
| 687 | + } | |
| 688 | + | |
| 689 | + if(dp_inst.settings.timeOnly || maxDateTimeDate.getTime() == dp_date.getTime()){ | |
| 690 | + this._defaults.hourMax = maxDateTime.getHours(); | |
| 691 | + if (this.hour >= this._defaults.hourMax) { | |
| 692 | + this.hour = this._defaults.hourMax; | |
| 693 | + this._defaults.minuteMax = maxDateTime.getMinutes(); | |
| 694 | + if (this.minute >= this._defaults.minuteMax) { | |
| 695 | + this.minute = this._defaults.minuteMax; | |
| 696 | + this._defaults.secondMax = maxDateTime.getSeconds(); | |
| 697 | + } else if (this.second >= this._defaults.secondMax) { | |
| 698 | + this.second = this._defaults.secondMax; | |
| 699 | + this._defaults.millisecMax = maxDateTime.getMilliseconds(); | |
| 700 | + } else { | |
| 701 | + if(this.millisec > this._defaults.millisecMax) { this.millisec = this._defaults.millisecMax; } | |
| 702 | + this._defaults.millisecMax = this.millisecMaxOriginal; | |
| 703 | + } | |
| 704 | + } else { | |
| 705 | + this._defaults.minuteMax = this.minuteMaxOriginal; | |
| 706 | + this._defaults.secondMax = this.secondMaxOriginal; | |
| 707 | + this._defaults.millisecMax = this.millisecMaxOriginal; | |
| 708 | + } | |
| 709 | + }else{ | |
| 710 | + this._defaults.hourMax = this.hourMaxOriginal; | |
| 711 | + this._defaults.minuteMax = this.minuteMaxOriginal; | |
| 712 | + this._defaults.secondMax = this.secondMaxOriginal; | |
| 713 | + this._defaults.millisecMax = this.millisecMaxOriginal; | |
| 714 | + } | |
| 715 | + } | |
| 716 | + | |
| 717 | + if(adjustSliders !== undefined && adjustSliders === true){ | |
| 718 | + var hourMax = parseInt((this._defaults.hourMax - ((this._defaults.hourMax - this._defaults.hourMin) % this._defaults.stepHour)) ,10), | |
| 719 | + minMax = parseInt((this._defaults.minuteMax - ((this._defaults.minuteMax - this._defaults.minuteMin) % this._defaults.stepMinute)) ,10), | |
| 720 | + secMax = parseInt((this._defaults.secondMax - ((this._defaults.secondMax - this._defaults.secondMin) % this._defaults.stepSecond)) ,10), | |
| 721 | + millisecMax = parseInt((this._defaults.millisecMax - ((this._defaults.millisecMax - this._defaults.millisecMin) % this._defaults.stepMillisec)) ,10); | |
| 722 | + | |
| 723 | + if(this.hour_slider) { | |
| 724 | + this.hour_slider.slider("option", { min: this._defaults.hourMin, max: hourMax }).slider('value', this.hour); | |
| 725 | + } | |
| 726 | + if(this.minute_slider) { | |
| 727 | + this.minute_slider.slider("option", { min: this._defaults.minuteMin, max: minMax }).slider('value', this.minute); | |
| 728 | + } | |
| 729 | + if(this.second_slider){ | |
| 730 | + this.second_slider.slider("option", { min: this._defaults.secondMin, max: secMax }).slider('value', this.second); | |
| 731 | + } | |
| 732 | + if(this.millisec_slider) { | |
| 733 | + this.millisec_slider.slider("option", { min: this._defaults.millisecMin, max: millisecMax }).slider('value', this.millisec); | |
| 734 | + } | |
| 735 | + } | |
| 736 | + | |
| 737 | + }, | |
| 738 | + | |
| 739 | + | |
| 740 | + //######################################################################## | |
| 741 | + // when a slider moves, set the internal time... | |
| 742 | + // on time change is also called when the time is updated in the text field | |
| 743 | + //######################################################################## | |
| 744 | + _onTimeChange: function() { | |
| 745 | + var hour = (this.hour_slider) ? this.hour_slider.slider('value') : false, | |
| 746 | + minute = (this.minute_slider) ? this.minute_slider.slider('value') : false, | |
| 747 | + second = (this.second_slider) ? this.second_slider.slider('value') : false, | |
| 748 | + millisec = (this.millisec_slider) ? this.millisec_slider.slider('value') : false, | |
| 749 | + timezone = (this.timezone_select) ? this.timezone_select.val() : false, | |
| 750 | + o = this._defaults; | |
| 751 | + | |
| 752 | + if (typeof(hour) == 'object') { hour = false; } | |
| 753 | + if (typeof(minute) == 'object') { minute = false; } | |
| 754 | + if (typeof(second) == 'object') { second = false; } | |
| 755 | + if (typeof(millisec) == 'object') { millisec = false; } | |
| 756 | + if (typeof(timezone) == 'object') { timezone = false; } | |
| 757 | + | |
| 758 | + if (hour !== false) { hour = parseInt(hour,10); } | |
| 759 | + if (minute !== false) { minute = parseInt(minute,10); } | |
| 760 | + if (second !== false) { second = parseInt(second,10); } | |
| 761 | + if (millisec !== false) { millisec = parseInt(millisec,10); } | |
| 762 | + | |
| 763 | + var ampm = o[hour < 12 ? 'amNames' : 'pmNames'][0]; | |
| 764 | + | |
| 765 | + // If the update was done in the input field, the input field should not be updated. | |
| 766 | + // If the update was done using the sliders, update the input field. | |
| 767 | + var hasChanged = (hour != this.hour || minute != this.minute || | |
| 768 | + second != this.second || millisec != this.millisec || | |
| 769 | + (this.ampm.length > 0 && | |
| 770 | + (hour < 12) != ($.inArray(this.ampm.toUpperCase(), this.amNames) !== -1)) || | |
| 771 | + timezone != this.timezone); | |
| 772 | + | |
| 773 | + if (hasChanged) { | |
| 774 | + | |
| 775 | + if (hour !== false) { this.hour = hour; } | |
| 776 | + if (minute !== false) { this.minute = minute; } | |
| 777 | + if (second !== false) { this.second = second; } | |
| 778 | + if (millisec !== false) { this.millisec = millisec; } | |
| 779 | + if (timezone !== false) { this.timezone = timezone; } | |
| 780 | + | |
| 781 | + if (!this.inst) { this.inst = $.datepicker._getInst(this.$input[0]); } | |
| 782 | + | |
| 783 | + this._limitMinMaxDateTime(this.inst, true); | |
| 784 | + } | |
| 785 | + if (o.ampm) { this.ampm = ampm; } | |
| 786 | + | |
| 787 | + //this._formatTime(); | |
| 788 | + this.formattedTime = $.datepicker.formatTime(this._defaults.timeFormat, this, this._defaults); | |
| 789 | + if (this.$timeObj) { this.$timeObj.text(this.formattedTime + o.timeSuffix); } | |
| 790 | + this.timeDefined = true; | |
| 791 | + if (hasChanged) { this._updateDateTime(); } | |
| 792 | + }, | |
| 793 | + | |
| 794 | + //######################################################################## | |
| 795 | + // call custom onSelect. | |
| 796 | + // bind to sliders slidestop, and grid click. | |
| 797 | + //######################################################################## | |
| 798 | + _onSelectHandler: function() { | |
| 799 | + var onSelect = this._defaults.onSelect; | |
| 800 | + var inputEl = this.$input ? this.$input[0] : null; | |
| 801 | + if (onSelect && inputEl) { | |
| 802 | + onSelect.apply(inputEl, [this.formattedDateTime, this]); | |
| 803 | + } | |
| 804 | + }, | |
| 805 | + | |
| 806 | + //######################################################################## | |
| 807 | + // left for any backwards compatibility | |
| 808 | + //######################################################################## | |
| 809 | + _formatTime: function(time, format) { | |
| 810 | + time = time || { hour: this.hour, minute: this.minute, second: this.second, millisec: this.millisec, ampm: this.ampm, timezone: this.timezone }; | |
| 811 | + var tmptime = (format || this._defaults.timeFormat).toString(); | |
| 812 | + | |
| 813 | + tmptime = $.datepicker.formatTime(tmptime, time, this._defaults); | |
| 814 | + | |
| 815 | + if (arguments.length) { return tmptime; } | |
| 816 | + else { this.formattedTime = tmptime; } | |
| 817 | + }, | |
| 818 | + | |
| 819 | + //######################################################################## | |
| 820 | + // update our input with the new date time.. | |
| 821 | + //######################################################################## | |
| 822 | + _updateDateTime: function(dp_inst) { | |
| 823 | + dp_inst = this.inst || dp_inst; | |
| 824 | + var dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)), | |
| 825 | + dateFmt = $.datepicker._get(dp_inst, 'dateFormat'), | |
| 826 | + formatCfg = $.datepicker._getFormatConfig(dp_inst), | |
| 827 | + timeAvailable = dt !== null && this.timeDefined; | |
| 828 | + this.formattedDate = $.datepicker.formatDate(dateFmt, (dt === null ? new Date() : dt), formatCfg); | |
| 829 | + var formattedDateTime = this.formattedDate; | |
| 830 | + // remove following lines to force every changes in date picker to change the input value | |
| 831 | + // Bug descriptions: when an input field has a default value, and click on the field to pop up the date picker. | |
| 832 | + // If the user manually empty the value in the input field, the date picker will never change selected value. | |
| 833 | + //if (dp_inst.lastVal !== undefined && (dp_inst.lastVal.length > 0 && this.$input.val().length === 0)) { | |
| 834 | + // return; | |
| 835 | + //} | |
| 836 | + | |
| 837 | + if (this._defaults.timeOnly === true) { | |
| 838 | + formattedDateTime = this.formattedTime; | |
| 839 | + } else if (this._defaults.timeOnly !== true && (this._defaults.alwaysSetTime || timeAvailable)) { | |
| 840 | + formattedDateTime += this._defaults.separator + this.formattedTime + this._defaults.timeSuffix; | |
| 841 | + } | |
| 842 | + | |
| 843 | + this.formattedDateTime = formattedDateTime; | |
| 844 | + | |
| 845 | + if(!this._defaults.showTimepicker) { | |
| 846 | + this.$input.val(this.formattedDate); | |
| 847 | + } else if (this.$altInput && this._defaults.altFieldTimeOnly === true) { | |
| 848 | + this.$altInput.val(this.formattedTime); | |
| 849 | + this.$input.val(this.formattedDate); | |
| 850 | + } else if(this.$altInput) { | |
| 851 | + this.$altInput.val(formattedDateTime); | |
| 852 | + this.$input.val(formattedDateTime); | |
| 853 | + } else { | |
| 854 | + this.$input.val(formattedDateTime); | |
| 855 | + } | |
| 856 | + | |
| 857 | + this.$input.trigger("change"); | |
| 858 | + } | |
| 859 | + | |
| 860 | +}); | |
| 861 | + | |
| 862 | +$.fn.extend({ | |
| 863 | + //######################################################################## | |
| 864 | + // shorthand just to use timepicker.. | |
| 865 | + //######################################################################## | |
| 866 | + timepicker: function(o) { | |
| 867 | + o = o || {}; | |
| 868 | + var tmp_args = arguments; | |
| 869 | + | |
| 870 | + if (typeof o == 'object') { tmp_args[0] = $.extend(o, { timeOnly: true }); } | |
| 871 | + | |
| 872 | + return $(this).each(function() { | |
| 873 | + $.fn.datetimepicker.apply($(this), tmp_args); | |
| 874 | + }); | |
| 875 | + }, | |
| 876 | + | |
| 877 | + //######################################################################## | |
| 878 | + // extend timepicker to datepicker | |
| 879 | + //######################################################################## | |
| 880 | + datetimepicker: function(o) { | |
| 881 | + o = o || {}; | |
| 882 | + var tmp_args = arguments; | |
| 883 | + | |
| 884 | + if (typeof(o) == 'string'){ | |
| 885 | + if(o == 'getDate') { | |
| 886 | + return $.fn.datepicker.apply($(this[0]), tmp_args); | |
| 887 | + } | |
| 888 | + else { | |
| 889 | + return this.each(function() { | |
| 890 | + var $t = $(this); | |
| 891 | + $t.datepicker.apply($t, tmp_args); | |
| 892 | + }); | |
| 893 | + } | |
| 894 | + } | |
| 895 | + else { | |
| 896 | + return this.each(function() { | |
| 897 | + var $t = $(this); | |
| 898 | + $t.datepicker($.timepicker._newInst($t, o)._defaults); | |
| 899 | + }); | |
| 900 | + } | |
| 901 | + } | |
| 902 | +}); | |
| 903 | + | |
| 904 | +$.datepicker.parseDateTime = function(dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) { | |
| 905 | + var parseRes = parseDateTimeInternal(dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings); | |
| 906 | + if (parseRes.timeObj) | |
| 907 | + { | |
| 908 | + var t = parseRes.timeObj; | |
| 909 | + parseRes.date.setHours(t.hour, t.minute, t.second, t.millisec); | |
| 910 | + } | |
| 911 | + | |
| 912 | + return parseRes.date; | |
| 913 | +}; | |
| 914 | + | |
| 915 | +$.datepicker.parseTime = function(timeFormat, timeString, options) { | |
| 916 | + | |
| 917 | + //######################################################################## | |
| 918 | + // pattern for standard and localized AM/PM markers | |
| 919 | + //######################################################################## | |
| 920 | + var getPatternAmpm = function(amNames, pmNames) { | |
| 921 | + var markers = []; | |
| 922 | + if (amNames) { | |
| 923 | + $.merge(markers, amNames); | |
| 924 | + } | |
| 925 | + if (pmNames) { | |
| 926 | + $.merge(markers, pmNames); | |
| 927 | + } | |
| 928 | + markers = $.map(markers, function(val) { return val.replace(/[.*+?|()\[\]{}\\]/g, '\\$&'); }); | |
| 929 | + return '(' + markers.join('|') + ')?'; | |
| 930 | + }; | |
| 931 | + | |
| 932 | + //######################################################################## | |
| 933 | + // figure out position of time elements.. cause js cant do named captures | |
| 934 | + //######################################################################## | |
| 935 | + var getFormatPositions = function( timeFormat ) { | |
| 936 | + var finds = timeFormat.toLowerCase().match(/(h{1,2}|m{1,2}|s{1,2}|l{1}|t{1,2}|z)/g), | |
| 937 | + orders = { h: -1, m: -1, s: -1, l: -1, t: -1, z: -1 }; | |
| 938 | + | |
| 939 | + if (finds) { | |
| 940 | + for (var i = 0; i < finds.length; i++) { | |
| 941 | + if (orders[finds[i].toString().charAt(0)] == -1) { | |
| 942 | + orders[finds[i].toString().charAt(0)] = i + 1; | |
| 943 | + } | |
| 944 | + } | |
| 945 | + } | |
| 946 | + return orders; | |
| 947 | + }; | |
| 948 | + | |
| 949 | + var o = extendRemove(extendRemove({}, $.timepicker._defaults), options || {}); | |
| 950 | + | |
| 951 | + var regstr = '^' + timeFormat.toString() | |
| 952 | + .replace(/h{1,2}/ig, '(\\d?\\d)') | |
| 953 | + .replace(/m{1,2}/ig, '(\\d?\\d)') | |
| 954 | + .replace(/s{1,2}/ig, '(\\d?\\d)') | |
| 955 | + .replace(/l{1}/ig, '(\\d?\\d?\\d)') | |
| 956 | + .replace(/t{1,2}/ig, getPatternAmpm(o.amNames, o.pmNames)) | |
| 957 | + .replace(/z{1}/ig, '(z|[-+]\\d\\d:?\\d\\d)?') | |
| 958 | + .replace(/\s/g, '\\s?') + o.timeSuffix + '$', | |
| 959 | + order = getFormatPositions(timeFormat), | |
| 960 | + ampm = '', | |
| 961 | + treg; | |
| 962 | + | |
| 963 | + treg = timeString.match(new RegExp(regstr, 'i')); | |
| 964 | + | |
| 965 | + var resTime = {hour: 0, minute: 0, second: 0, millisec: 0}; | |
| 966 | + | |
| 967 | + if (treg) { | |
| 968 | + if (order.t !== -1) { | |
| 969 | + if (treg[order.t] === undefined || treg[order.t].length === 0) { | |
| 970 | + ampm = ''; | |
| 971 | + resTime.ampm = ''; | |
| 972 | + } else { | |
| 973 | + ampm = $.inArray(treg[order.t], o.amNames) !== -1 ? 'AM' : 'PM'; | |
| 974 | + resTime.ampm = o[ampm == 'AM' ? 'amNames' : 'pmNames'][0]; | |
| 975 | + } | |
| 976 | + } | |
| 977 | + | |
| 978 | + if (order.h !== -1) { | |
| 979 | + if (ampm == 'AM' && treg[order.h] == '12') { | |
| 980 | + resTime.hour = 0; // 12am = 0 hour | |
| 981 | + } else { | |
| 982 | + if (ampm == 'PM' && treg[order.h] != '12') { | |
| 983 | + resTime.hour = parseInt(treg[order.h],10) + 12; // 12pm = 12 hour, any other pm = hour + 12 | |
| 984 | + } | |
| 985 | + else { resTime.hour = Number(treg[order.h]); } | |
| 986 | + } | |
| 987 | + } | |
| 988 | + | |
| 989 | + if (order.m !== -1) { resTime.minute = Number(treg[order.m]); } | |
| 990 | + if (order.s !== -1) { resTime.second = Number(treg[order.s]); } | |
| 991 | + if (order.l !== -1) { resTime.millisec = Number(treg[order.l]); } | |
| 992 | + if (order.z !== -1 && treg[order.z] !== undefined) { | |
| 993 | + var tz = treg[order.z].toUpperCase(); | |
| 994 | + switch (tz.length) { | |
| 995 | + case 1: // Z | |
| 996 | + tz = o.timezoneIso8601 ? 'Z' : '+0000'; | |
| 997 | + break; | |
| 998 | + case 5: // +hhmm | |
| 999 | + if (o.timezoneIso8601) { | |
| 1000 | + tz = tz.substring(1) == '0000' ? | |
| 1001 | + 'Z' : | |
| 1002 | + tz.substring(0, 3) + ':' + tz.substring(3); | |
| 1003 | + } | |
| 1004 | + break; | |
| 1005 | + case 6: // +hh:mm | |
| 1006 | + if (!o.timezoneIso8601) { | |
| 1007 | + tz = tz == 'Z' || tz.substring(1) == '00:00' ? | |
| 1008 | + '+0000' : | |
| 1009 | + tz.replace(/:/, ''); | |
| 1010 | + } else { | |
| 1011 | + if (tz.substring(1) == '00:00') { | |
| 1012 | + tz = 'Z'; | |
| 1013 | + } | |
| 1014 | + } | |
| 1015 | + break; | |
| 1016 | + } | |
| 1017 | + resTime.timezone = tz; | |
| 1018 | + } | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + return resTime; | |
| 1022 | + } | |
| 1023 | + | |
| 1024 | + return false; | |
| 1025 | +}; | |
| 1026 | + | |
| 1027 | +//######################################################################## | |
| 1028 | +// format the time all pretty... | |
| 1029 | +// format = string format of the time | |
| 1030 | +// time = a {}, not a Date() for timezones | |
| 1031 | +// options = essentially the regional[].. amNames, pmNames, ampm | |
| 1032 | +//######################################################################## | |
| 1033 | +$.datepicker.formatTime = function(format, time, options) { | |
| 1034 | + options = options || {}; | |
| 1035 | + options = $.extend($.timepicker._defaults, options); | |
| 1036 | + time = $.extend({hour:0, minute:0, second:0, millisec:0, timezone:'+0000'}, time); | |
| 1037 | + | |
| 1038 | + var tmptime = format; | |
| 1039 | + var ampmName = options.amNames[0]; | |
| 1040 | + | |
| 1041 | + var hour = parseInt(time.hour, 10); | |
| 1042 | + if (options.ampm) { | |
| 1043 | + if (hour > 11){ | |
| 1044 | + ampmName = options.pmNames[0]; | |
| 1045 | + if(hour > 12) { | |
| 1046 | + hour = hour % 12; | |
| 1047 | + } | |
| 1048 | + } | |
| 1049 | + if (hour === 0) { | |
| 1050 | + hour = 12; | |
| 1051 | + } | |
| 1052 | + } | |
| 1053 | + tmptime = tmptime.replace(/(?:hh?|mm?|ss?|[tT]{1,2}|[lz])/g, function(match) { | |
| 1054 | + switch (match.toLowerCase()) { | |
| 1055 | + case 'hh': return ('0' + hour).slice(-2); | |
| 1056 | + case 'h': return hour; | |
| 1057 | + case 'mm': return ('0' + time.minute).slice(-2); | |
| 1058 | + case 'm': return time.minute; | |
| 1059 | + case 'ss': return ('0' + time.second).slice(-2); | |
| 1060 | + case 's': return time.second; | |
| 1061 | + case 'l': return ('00' + time.millisec).slice(-3); | |
| 1062 | + case 'z': return time.timezone; | |
| 1063 | + case 't': case 'tt': | |
| 1064 | + if (options.ampm) { | |
| 1065 | + if (match.length == 1) { | |
| 1066 | + ampmName = ampmName.charAt(0); | |
| 1067 | + } | |
| 1068 | + return match.charAt(0) == 'T' ? ampmName.toUpperCase() : ampmName.toLowerCase(); | |
| 1069 | + } | |
| 1070 | + return ''; | |
| 1071 | + } | |
| 1072 | + }); | |
| 1073 | + | |
| 1074 | + tmptime = $.trim(tmptime); | |
| 1075 | + return tmptime; | |
| 1076 | +}; | |
| 1077 | + | |
| 1078 | +//######################################################################## | |
| 1079 | +// the bad hack :/ override datepicker so it doesnt close on select | |
| 1080 | +// inspired: http://stackoverflow.com/questions/1252512/jquery-datepicker-prevent-closing-picker-when-clicking-a-date/1762378#1762378 | |
| 1081 | +//######################################################################## | |
| 1082 | +$.datepicker._base_selectDate = $.datepicker._selectDate; | |
| 1083 | +$.datepicker._selectDate = function (id, dateStr) { | |
| 1084 | + var inst = this._getInst($(id)[0]), | |
| 1085 | + tp_inst = this._get(inst, 'timepicker'); | |
| 1086 | + | |
| 1087 | + if (tp_inst) { | |
| 1088 | + tp_inst._limitMinMaxDateTime(inst, true); | |
| 1089 | + inst.inline = inst.stay_open = true; | |
| 1090 | + //This way the onSelect handler called from calendarpicker get the full dateTime | |
| 1091 | + this._base_selectDate(id, dateStr); | |
| 1092 | + inst.inline = inst.stay_open = false; | |
| 1093 | + this._notifyChange(inst); | |
| 1094 | + this._updateDatepicker(inst); | |
| 1095 | + } | |
| 1096 | + else { this._base_selectDate(id, dateStr); } | |
| 1097 | +}; | |
| 1098 | + | |
| 1099 | +//############################################################################################# | |
| 1100 | +// second bad hack :/ override datepicker so it triggers an event when changing the input field | |
| 1101 | +// and does not redraw the datepicker on every selectDate event | |
| 1102 | +//############################################################################################# | |
| 1103 | +$.datepicker._base_updateDatepicker = $.datepicker._updateDatepicker; | |
| 1104 | +$.datepicker._updateDatepicker = function(inst) { | |
| 1105 | + | |
| 1106 | + // don't popup the datepicker if there is another instance already opened | |
| 1107 | + var input = inst.input[0]; | |
| 1108 | + if($.datepicker._curInst && | |
| 1109 | + $.datepicker._curInst != inst && | |
| 1110 | + $.datepicker._datepickerShowing && | |
| 1111 | + $.datepicker._lastInput != input) { | |
| 1112 | + return; | |
| 1113 | + } | |
| 1114 | + | |
| 1115 | + if (typeof(inst.stay_open) !== 'boolean' || inst.stay_open === false) { | |
| 1116 | + | |
| 1117 | + this._base_updateDatepicker(inst); | |
| 1118 | + | |
| 1119 | + // Reload the time control when changing something in the input text field. | |
| 1120 | + var tp_inst = this._get(inst, 'timepicker'); | |
| 1121 | + if(tp_inst) { | |
| 1122 | + tp_inst._addTimePicker(inst); | |
| 1123 | + | |
| 1124 | + if (tp_inst._defaults.useLocalTimezone) { //checks daylight saving with the new date. | |
| 1125 | + var date = new Date(inst.selectedYear, inst.selectedMonth, inst.selectedDay, 12); | |
| 1126 | + selectLocalTimeZone(tp_inst, date); | |
| 1127 | + tp_inst._onTimeChange(); | |
| 1128 | + } | |
| 1129 | + } | |
| 1130 | + } | |
| 1131 | +}; | |
| 1132 | + | |
| 1133 | +//####################################################################################### | |
| 1134 | +// third bad hack :/ override datepicker so it allows spaces and colon in the input field | |
| 1135 | +//####################################################################################### | |
| 1136 | +$.datepicker._base_doKeyPress = $.datepicker._doKeyPress; | |
| 1137 | +$.datepicker._doKeyPress = function(event) { | |
| 1138 | + var inst = $.datepicker._getInst(event.target), | |
| 1139 | + tp_inst = $.datepicker._get(inst, 'timepicker'); | |
| 1140 | + | |
| 1141 | + if (tp_inst) { | |
| 1142 | + if ($.datepicker._get(inst, 'constrainInput')) { | |
| 1143 | + var ampm = tp_inst._defaults.ampm, | |
| 1144 | + dateChars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat')), | |
| 1145 | + datetimeChars = tp_inst._defaults.timeFormat.toString() | |
| 1146 | + .replace(/[hms]/g, '') | |
| 1147 | + .replace(/TT/g, ampm ? 'APM' : '') | |
| 1148 | + .replace(/Tt/g, ampm ? 'AaPpMm' : '') | |
| 1149 | + .replace(/tT/g, ampm ? 'AaPpMm' : '') | |
| 1150 | + .replace(/T/g, ampm ? 'AP' : '') | |
| 1151 | + .replace(/tt/g, ampm ? 'apm' : '') | |
| 1152 | + .replace(/t/g, ampm ? 'ap' : '') + | |
| 1153 | + " " + | |
| 1154 | + tp_inst._defaults.separator + | |
| 1155 | + tp_inst._defaults.timeSuffix + | |
| 1156 | + (tp_inst._defaults.showTimezone ? tp_inst._defaults.timezoneList.join('') : '') + | |
| 1157 | + (tp_inst._defaults.amNames.join('')) + | |
| 1158 | + (tp_inst._defaults.pmNames.join('')) + | |
| 1159 | + dateChars, | |
| 1160 | + chr = String.fromCharCode(event.charCode === undefined ? event.keyCode : event.charCode); | |
| 1161 | + return event.ctrlKey || (chr < ' ' || !dateChars || datetimeChars.indexOf(chr) > -1); | |
| 1162 | + } | |
| 1163 | + } | |
| 1164 | + | |
| 1165 | + return $.datepicker._base_doKeyPress(event); | |
| 1166 | +}; | |
| 1167 | + | |
| 1168 | +//####################################################################################### | |
| 1169 | +// Override key up event to sync manual input changes. | |
| 1170 | +//####################################################################################### | |
| 1171 | +$.datepicker._base_doKeyUp = $.datepicker._doKeyUp; | |
| 1172 | +$.datepicker._doKeyUp = function (event) { | |
| 1173 | + var inst = $.datepicker._getInst(event.target), | |
| 1174 | + tp_inst = $.datepicker._get(inst, 'timepicker'); | |
| 1175 | + | |
| 1176 | + if (tp_inst) { | |
| 1177 | + if (tp_inst._defaults.timeOnly && (inst.input.val() != inst.lastVal)) { | |
| 1178 | + try { | |
| 1179 | + $.datepicker._updateDatepicker(inst); | |
| 1180 | + } | |
| 1181 | + catch (err) { | |
| 1182 | + $.datepicker.log(err); | |
| 1183 | + } | |
| 1184 | + } | |
| 1185 | + } | |
| 1186 | + | |
| 1187 | + return $.datepicker._base_doKeyUp(event); | |
| 1188 | +}; | |
| 1189 | + | |
| 1190 | +//####################################################################################### | |
| 1191 | +// override "Today" button to also grab the time. | |
| 1192 | +//####################################################################################### | |
| 1193 | +$.datepicker._base_gotoToday = $.datepicker._gotoToday; | |
| 1194 | +$.datepicker._gotoToday = function(id) { | |
| 1195 | + var inst = this._getInst($(id)[0]), | |
| 1196 | + $dp = inst.dpDiv; | |
| 1197 | + this._base_gotoToday(id); | |
| 1198 | + var tp_inst = this._get(inst, 'timepicker'); | |
| 1199 | + selectLocalTimeZone(tp_inst); | |
| 1200 | + var now = new Date(); | |
| 1201 | + this._setTime(inst, now); | |
| 1202 | + $( '.ui-datepicker-today', $dp).click(); | |
| 1203 | +}; | |
| 1204 | + | |
| 1205 | +//####################################################################################### | |
| 1206 | +// Disable & enable the Time in the datetimepicker | |
| 1207 | +//####################################################################################### | |
| 1208 | +$.datepicker._disableTimepickerDatepicker = function(target) { | |
| 1209 | + var inst = this._getInst(target); | |
| 1210 | + if (!inst) { return; } | |
| 1211 | + | |
| 1212 | + var tp_inst = this._get(inst, 'timepicker'); | |
| 1213 | + $(target).datepicker('getDate'); // Init selected[Year|Month|Day] | |
| 1214 | + if (tp_inst) { | |
| 1215 | + tp_inst._defaults.showTimepicker = false; | |
| 1216 | + tp_inst._updateDateTime(inst); | |
| 1217 | + } | |
| 1218 | +}; | |
| 1219 | + | |
| 1220 | +$.datepicker._enableTimepickerDatepicker = function(target) { | |
| 1221 | + var inst = this._getInst(target); | |
| 1222 | + if (!inst) { return; } | |
| 1223 | + | |
| 1224 | + var tp_inst = this._get(inst, 'timepicker'); | |
| 1225 | + $(target).datepicker('getDate'); // Init selected[Year|Month|Day] | |
| 1226 | + if (tp_inst) { | |
| 1227 | + tp_inst._defaults.showTimepicker = true; | |
| 1228 | + tp_inst._addTimePicker(inst); // Could be disabled on page load | |
| 1229 | + tp_inst._updateDateTime(inst); | |
| 1230 | + } | |
| 1231 | +}; | |
| 1232 | + | |
| 1233 | +//####################################################################################### | |
| 1234 | +// Create our own set time function | |
| 1235 | +//####################################################################################### | |
| 1236 | +$.datepicker._setTime = function(inst, date) { | |
| 1237 | + var tp_inst = this._get(inst, 'timepicker'); | |
| 1238 | + if (tp_inst) { | |
| 1239 | + var defaults = tp_inst._defaults, | |
| 1240 | + // calling _setTime with no date sets time to defaults | |
| 1241 | + hour = date ? date.getHours() : defaults.hour, | |
| 1242 | + minute = date ? date.getMinutes() : defaults.minute, | |
| 1243 | + second = date ? date.getSeconds() : defaults.second, | |
| 1244 | + millisec = date ? date.getMilliseconds() : defaults.millisec; | |
| 1245 | + //check if within min/max times.. | |
| 1246 | + // correct check if within min/max times. | |
| 1247 | + // Rewritten by Scott A. Woodward | |
| 1248 | + var hourEq = hour === defaults.hourMin, | |
| 1249 | + minuteEq = minute === defaults.minuteMin, | |
| 1250 | + secondEq = second === defaults.secondMin; | |
| 1251 | + var reset = false; | |
| 1252 | + if(hour < defaults.hourMin || hour > defaults.hourMax) | |
| 1253 | + reset = true; | |
| 1254 | + else if( (minute < defaults.minuteMin || minute > defaults.minuteMax) && hourEq) | |
| 1255 | + reset = true; | |
| 1256 | + else if( (second < defaults.secondMin || second > defaults.secondMax ) && hourEq && minuteEq) | |
| 1257 | + reset = true; | |
| 1258 | + else if( (millisec < defaults.millisecMin || millisec > defaults.millisecMax) && hourEq && minuteEq && secondEq) | |
| 1259 | + reset = true; | |
| 1260 | + if(reset) { | |
| 1261 | + hour = defaults.hourMin; | |
| 1262 | + minute = defaults.minuteMin; | |
| 1263 | + second = defaults.secondMin; | |
| 1264 | + millisec = defaults.millisecMin; | |
| 1265 | + } | |
| 1266 | + tp_inst.hour = hour; | |
| 1267 | + tp_inst.minute = minute; | |
| 1268 | + tp_inst.second = second; | |
| 1269 | + tp_inst.millisec = millisec; | |
| 1270 | + if (tp_inst.hour_slider) tp_inst.hour_slider.slider('value', hour); | |
| 1271 | + if (tp_inst.minute_slider) tp_inst.minute_slider.slider('value', minute); | |
| 1272 | + if (tp_inst.second_slider) tp_inst.second_slider.slider('value', second); | |
| 1273 | + if (tp_inst.millisec_slider) tp_inst.millisec_slider.slider('value', millisec); | |
| 1274 | + | |
| 1275 | + tp_inst._onTimeChange(); | |
| 1276 | + tp_inst._updateDateTime(inst); | |
| 1277 | + } | |
| 1278 | +}; | |
| 1279 | + | |
| 1280 | +//####################################################################################### | |
| 1281 | +// Create new public method to set only time, callable as $().datepicker('setTime', date) | |
| 1282 | +//####################################################################################### | |
| 1283 | +$.datepicker._setTimeDatepicker = function(target, date, withDate) { | |
| 1284 | + var inst = this._getInst(target); | |
| 1285 | + if (!inst) { return; } | |
| 1286 | + | |
| 1287 | + var tp_inst = this._get(inst, 'timepicker'); | |
| 1288 | + | |
| 1289 | + if (tp_inst) { | |
| 1290 | + this._setDateFromField(inst); | |
| 1291 | + var tp_date; | |
| 1292 | + if (date) { | |
| 1293 | + if (typeof date == "string") { | |
| 1294 | + tp_inst._parseTime(date, withDate); | |
| 1295 | + tp_date = new Date(); | |
| 1296 | + tp_date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec); | |
| 1297 | + } | |
| 1298 | + else { tp_date = new Date(date.getTime()); } | |
| 1299 | + if (tp_date.toString() == 'Invalid Date') { tp_date = undefined; } | |
| 1300 | + this._setTime(inst, tp_date); | |
| 1301 | + } | |
| 1302 | + } | |
| 1303 | + | |
| 1304 | +}; | |
| 1305 | + | |
| 1306 | +//####################################################################################### | |
| 1307 | +// override setDate() to allow setting time too within Date object | |
| 1308 | +//####################################################################################### | |
| 1309 | +$.datepicker._base_setDateDatepicker = $.datepicker._setDateDatepicker; | |
| 1310 | +$.datepicker._setDateDatepicker = function(target, date) { | |
| 1311 | + var inst = this._getInst(target); | |
| 1312 | + if (!inst) { return; } | |
| 1313 | + | |
| 1314 | + var tp_date = (date instanceof Date) ? new Date(date.getTime()) : date; | |
| 1315 | + | |
| 1316 | + this._updateDatepicker(inst); | |
| 1317 | + this._base_setDateDatepicker.apply(this, arguments); | |
| 1318 | + this._setTimeDatepicker(target, tp_date, true); | |
| 1319 | +}; | |
| 1320 | + | |
| 1321 | +//####################################################################################### | |
| 1322 | +// override getDate() to allow getting time too within Date object | |
| 1323 | +//####################################################################################### | |
| 1324 | +$.datepicker._base_getDateDatepicker = $.datepicker._getDateDatepicker; | |
| 1325 | +$.datepicker._getDateDatepicker = function(target, noDefault) { | |
| 1326 | + var inst = this._getInst(target); | |
| 1327 | + if (!inst) { return; } | |
| 1328 | + | |
| 1329 | + var tp_inst = this._get(inst, 'timepicker'); | |
| 1330 | + | |
| 1331 | + if (tp_inst) { | |
| 1332 | + this._setDateFromField(inst, noDefault); | |
| 1333 | + var date = this._getDate(inst); | |
| 1334 | + if (date && tp_inst._parseTime($(target).val(), tp_inst.timeOnly)) { date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec); } | |
| 1335 | + return date; | |
| 1336 | + } | |
| 1337 | + return this._base_getDateDatepicker(target, noDefault); | |
| 1338 | +}; | |
| 1339 | + | |
| 1340 | +//####################################################################################### | |
| 1341 | +// override parseDate() because UI 1.8.14 throws an error about "Extra characters" | |
| 1342 | +// An option in datapicker to ignore extra format characters would be nicer. | |
| 1343 | +//####################################################################################### | |
| 1344 | +$.datepicker._base_parseDate = $.datepicker.parseDate; | |
| 1345 | +$.datepicker.parseDate = function(format, value, settings) { | |
| 1346 | + var splitRes = splitDateTime(format, value, settings); | |
| 1347 | + return $.datepicker._base_parseDate(format, splitRes[0], settings); | |
| 1348 | +}; | |
| 1349 | + | |
| 1350 | +//####################################################################################### | |
| 1351 | +// override formatDate to set date with time to the input | |
| 1352 | +//####################################################################################### | |
| 1353 | +$.datepicker._base_formatDate = $.datepicker._formatDate; | |
| 1354 | +$.datepicker._formatDate = function(inst, day, month, year){ | |
| 1355 | + var tp_inst = this._get(inst, 'timepicker'); | |
| 1356 | + if(tp_inst) { | |
| 1357 | + tp_inst._updateDateTime(inst); | |
| 1358 | + return tp_inst.$input.val(); | |
| 1359 | + } | |
| 1360 | + return this._base_formatDate(inst); | |
| 1361 | +}; | |
| 1362 | + | |
| 1363 | +//####################################################################################### | |
| 1364 | +// override options setter to add time to maxDate(Time) and minDate(Time). MaxDate | |
| 1365 | +//####################################################################################### | |
| 1366 | +$.datepicker._base_optionDatepicker = $.datepicker._optionDatepicker; | |
| 1367 | +$.datepicker._optionDatepicker = function(target, name, value) { | |
| 1368 | + var inst = this._getInst(target); | |
| 1369 | + if (!inst) { return null; } | |
| 1370 | + | |
| 1371 | + var tp_inst = this._get(inst, 'timepicker'); | |
| 1372 | + if (tp_inst) { | |
| 1373 | + var min = null, max = null, onselect = null; | |
| 1374 | + if (typeof name == 'string') { // if min/max was set with the string | |
| 1375 | + if (name === 'minDate' || name === 'minDateTime' ) { | |
| 1376 | + min = value; | |
| 1377 | + } | |
| 1378 | + else { | |
| 1379 | + if (name === 'maxDate' || name === 'maxDateTime') { | |
| 1380 | + max = value; | |
| 1381 | + } | |
| 1382 | + else { | |
| 1383 | + if (name === 'onSelect') { | |
| 1384 | + onselect = value; | |
| 1385 | + } | |
| 1386 | + } | |
| 1387 | + } | |
| 1388 | + } else { | |
| 1389 | + if (typeof name == 'object') { //if min/max was set with the JSON | |
| 1390 | + if (name.minDate) { | |
| 1391 | + min = name.minDate; | |
| 1392 | + } else { | |
| 1393 | + if (name.minDateTime) { | |
| 1394 | + min = name.minDateTime; | |
| 1395 | + } else { | |
| 1396 | + if (name.maxDate) { | |
| 1397 | + max = name.maxDate; | |
| 1398 | + } else { | |
| 1399 | + if (name.maxDateTime) { | |
| 1400 | + max = name.maxDateTime; | |
| 1401 | + } | |
| 1402 | + } | |
| 1403 | + } | |
| 1404 | + } | |
| 1405 | + } | |
| 1406 | + } | |
| 1407 | + if(min) { //if min was set | |
| 1408 | + if (min === 0) { | |
| 1409 | + min = new Date(); | |
| 1410 | + } else { | |
| 1411 | + min = new Date(min); | |
| 1412 | + } | |
| 1413 | + | |
| 1414 | + tp_inst._defaults.minDate = min; | |
| 1415 | + tp_inst._defaults.minDateTime = min; | |
| 1416 | + } else if (max) { //if max was set | |
| 1417 | + if(max===0) { | |
| 1418 | + max=new Date(); | |
| 1419 | + } else { | |
| 1420 | + max= new Date(max); | |
| 1421 | + } | |
| 1422 | + tp_inst._defaults.maxDate = max; | |
| 1423 | + tp_inst._defaults.maxDateTime = max; | |
| 1424 | + } else if (onselect) { | |
| 1425 | + tp_inst._defaults.onSelect = onselect; | |
| 1426 | + } | |
| 1427 | + } | |
| 1428 | + if (value === undefined) { | |
| 1429 | + return this._base_optionDatepicker(target, name); | |
| 1430 | + } | |
| 1431 | + return this._base_optionDatepicker(target, name, value); | |
| 1432 | +}; | |
| 1433 | + | |
| 1434 | +//####################################################################################### | |
| 1435 | +// jQuery extend now ignores nulls! | |
| 1436 | +//####################################################################################### | |
| 1437 | +function extendRemove(target, props) { | |
| 1438 | + $.extend(target, props); | |
| 1439 | + for (var name in props) { | |
| 1440 | + if (props[name] === null || props[name] === undefined) { | |
| 1441 | + target[name] = props[name]; | |
| 1442 | + } | |
| 1443 | + } | |
| 1444 | + return target; | |
| 1445 | +} | |
| 1446 | + | |
| 1447 | +//####################################################################################### | |
| 1448 | +// Splits datetime string into date ans time substrings. | |
| 1449 | +// Throws exception when date can't be parsed | |
| 1450 | +// If only date is present, time substring eill be '' | |
| 1451 | +//####################################################################################### | |
| 1452 | +var splitDateTime = function(dateFormat, dateTimeString, dateSettings) | |
| 1453 | +{ | |
| 1454 | + try { | |
| 1455 | + var date = $.datepicker._base_parseDate(dateFormat, dateTimeString, dateSettings); | |
| 1456 | + } catch (err) { | |
| 1457 | + if (err.indexOf(":") >= 0) { | |
| 1458 | + // Hack! The error message ends with a colon, a space, and | |
| 1459 | + // the "extra" characters. We rely on that instead of | |
| 1460 | + // attempting to perfectly reproduce the parsing algorithm. | |
| 1461 | + var dateStringLength = dateTimeString.length-(err.length-err.indexOf(':')-2); | |
| 1462 | + var timeString = dateTimeString.substring(dateStringLength); | |
| 1463 | + | |
| 1464 | + return [dateTimeString.substring(0, dateStringLength), dateTimeString.substring(dateStringLength)]; | |
| 1465 | + | |
| 1466 | + } else { | |
| 1467 | + throw err; | |
| 1468 | + } | |
| 1469 | + } | |
| 1470 | + return [dateTimeString, '']; | |
| 1471 | +}; | |
| 1472 | + | |
| 1473 | +//####################################################################################### | |
| 1474 | +// Internal function to parse datetime interval | |
| 1475 | +// Returns: {date: Date, timeObj: Object}, where | |
| 1476 | +// date - parsed date without time (type Date) | |
| 1477 | +// timeObj = {hour: , minute: , second: , millisec: } - parsed time. Optional | |
| 1478 | +//####################################################################################### | |
| 1479 | +var parseDateTimeInternal = function(dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) | |
| 1480 | +{ | |
| 1481 | + var date; | |
| 1482 | + var splitRes = splitDateTime(dateFormat, dateTimeString, dateSettings); | |
| 1483 | + date = $.datepicker._base_parseDate(dateFormat, splitRes[0], dateSettings); | |
| 1484 | + if (splitRes[1] !== '') | |
| 1485 | + { | |
| 1486 | + var timeString = splitRes[1]; | |
| 1487 | + var separator = timeSettings && timeSettings.separator ? timeSettings.separator : $.timepicker._defaults.separator; | |
| 1488 | + if ( timeString.indexOf(separator) !== 0) { | |
| 1489 | + throw 'Missing time separator'; | |
| 1490 | + } | |
| 1491 | + timeString = timeString.substring(separator.length); | |
| 1492 | + var parsedTime = $.datepicker.parseTime(timeFormat, timeString, timeSettings); | |
| 1493 | + if (parsedTime === null) { | |
| 1494 | + throw 'Wrong time format'; | |
| 1495 | + } | |
| 1496 | + return {date: date, timeObj: parsedTime}; | |
| 1497 | + } else { | |
| 1498 | + return {date: date}; | |
| 1499 | + } | |
| 1500 | +}; | |
| 1501 | + | |
| 1502 | +//####################################################################################### | |
| 1503 | +// Internal function to set timezone_select to the local timezone | |
| 1504 | +//####################################################################################### | |
| 1505 | +var selectLocalTimeZone = function(tp_inst, date) | |
| 1506 | +{ | |
| 1507 | + if (tp_inst && tp_inst.timezone_select) { | |
| 1508 | + tp_inst._defaults.useLocalTimezone = true; | |
| 1509 | + var now = typeof date !== 'undefined' ? date : new Date(); | |
| 1510 | + var tzoffset = timeZoneString(now); | |
| 1511 | + if (tp_inst._defaults.timezoneIso8601) { | |
| 1512 | + tzoffset = tzoffset.substring(0, 3) + ':' + tzoffset.substring(3); | |
| 1513 | + } | |
| 1514 | + tp_inst.timezone_select.val(tzoffset); | |
| 1515 | + } | |
| 1516 | +}; | |
| 1517 | + | |
| 1518 | +// Input: Date Object | |
| 1519 | +// Output: String with timezone offset, e.g. '+0100' | |
| 1520 | +var timeZoneString = function(date) | |
| 1521 | +{ | |
| 1522 | + var off = date.getTimezoneOffset() * -10100 / 60; | |
| 1523 | + var timezone = (off >= 0 ? '+' : '-') + Math.abs(off).toString().substr(1); | |
| 1524 | + return timezone; | |
| 1525 | +}; | |
| 1526 | + | |
| 1527 | +$.timepicker = new Timepicker(); // singleton instance | |
| 1528 | +$.timepicker.version = "1.0.1"; | |
| 1529 | + | |
| 1530 | +})(jQuery); | ... | ... |
public/stylesheets/application.css
| ... | ... | @@ -4340,6 +4340,14 @@ h1#agenda-title { |
| 4340 | 4340 | margin: 5px 0; |
| 4341 | 4341 | padding: 5px; |
| 4342 | 4342 | } |
| 4343 | + | |
| 4344 | +.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; } | |
| 4345 | +.ui-timepicker-div dl { text-align: left; } | |
| 4346 | +.ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; } | |
| 4347 | +.ui-timepicker-div dl dd { margin: 0 10px 10px 65px; } | |
| 4348 | +.ui-timepicker-div td { font-size: 90%; } | |
| 4349 | +.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; } | |
| 4350 | + | |
| 4343 | 4351 | /* Categories block stuff */ |
| 4344 | 4352 | |
| 4345 | 4353 | .categories-block ul { | ... | ... |