diff --git a/Rakefile b/Rakefile index 8c89bf145..e411068ec 100644 --- a/Rakefile +++ b/Rakefile @@ -20,13 +20,19 @@ Bundler::GemHelper.install_tasks require 'rake/testtask' -Rake::TestTask.new('test:units' => ['project:ensure_db_exists', 'app:test:prepare']) do |t| +Rake::TestTask.new('units') do |t| t.libs << 'lib' t.libs << 'test' t.pattern = 'test/unit/**/*_test.rb' t.verbose = false end +Rake::TestTask.new('spec') do |t| + t.libs << 'lib' + t.libs << 'spec' + t.pattern = "spec/**/*_spec.rb" +end + Rake::TestTask.new('test:functionals' => ['project:ensure_db_exists', 'app:test:prepare']) do |t| t.libs << 'lib' t.libs << 'test' @@ -46,11 +52,11 @@ require 'cucumber' require 'cucumber/rake/task' Cucumber::Rake::Task.new(:features => ['project:ensure_db_exists', 'app:test:prepare']) do |t| - t.cucumber_opts = "features --format progress" + t.cucumber_opts = "launch_on_failure=false features --format progress" end Cucumber::Rake::Task.new('features:fast' => ['project:ensure_db_exists', 'app:test:prepare']) do |t| - t.cucumber_opts = "features --format progress --tags ~@cli" + t.cucumber_opts = "launch_on_failure=false features --format progress --tags ~@cli" end Cucumber::Rake::Task.new('features:cli' => ['project:ensure_db_exists', 'app:test:prepare']) do |t| @@ -59,17 +65,26 @@ end desc "Run everything but the command line (slow) tests" -task 'test:fast' => %w{test:units test:functionals test:integration features:fast} +task 'test:fast' => %w{app:test:prepare test:units test:functionals test:integration features:fast} -desc 'Runs all the tests' +desc "Runs all unit level tests" +task 'test:units' => ['app:test:prepare'] do + run_tests ["units", "spec"] +end + +desc 'Runs all the tests, specs and scenarios.' task :test => ['project:ensure_db_exists', 'app:test:prepare'] do - tests_to_run = ENV['TEST'] ? ["test:single"] : %w(test:units test:functionals test:integration features) + tests_to_run = ENV['TEST'] ? ["test:single"] : %w(test:units spec test:functionals test:integration features) + run_tests(tests_to_run) +end + +def run_tests(tests_to_run) errors = tests_to_run.collect do |task| begin Rake::Task[task].invoke nil rescue => e - { :task => task, :exception => e } + {:task => task, :exception => e} end end.compact diff --git a/app/assets/javascripts/cms/ajax.js b/app/assets/javascripts/cms/ajax.js index ae22e9696..2eead4abb 100644 --- a/app/assets/javascripts/cms/ajax.js +++ b/app/assets/javascripts/cms/ajax.js @@ -52,9 +52,9 @@ jQuery(function ($) { // Defaults for AJAX requests $.ajaxSetup({ - error:function (x, status, error) { - alert("A " + x.status + " error occurred: " + error); - }, beforeSend: $.cms_ajax.asJSON() }); + $(document).ajaxError(function (x, status, error) { + alert("A " + x.status + " error occurred: " + error); + }); }); diff --git a/app/assets/javascripts/cms/application.js b/app/assets/javascripts/cms/application.js index 25f12eed4..14428b215 100644 --- a/app/assets/javascripts/cms/application.js +++ b/app/assets/javascripts/cms/application.js @@ -8,6 +8,7 @@ //= require jquery.taglist //= require cms/core_library //= require cms/attachment_manager +//= require cms/form_builder //= require bootstrap // diff --git a/app/assets/javascripts/cms/form_builder.js b/app/assets/javascripts/cms/form_builder.js new file mode 100644 index 000000000..bfa9b5332 --- /dev/null +++ b/app/assets/javascripts/cms/form_builder.js @@ -0,0 +1,253 @@ +//= require cms/ajax +//= require underscore + +/** + * The UI for dynamically creating custom forms via the UI. + * @constructor + * + */ + +// Determine if an element exists. +// i.e. if($('.some-class').exists()){ // do something } +jQuery.fn.exists = function() { + return this.length > 0; +}; + +var FormBuilder = function() { +}; + +// Add a new field to the form +// (Implementation: Clone existing hidden form elements rather than build new ones via HTML). +FormBuilder.prototype.newField = function(field_type) { + this.hideNewFormInstruction(); + this.addPreviewFieldToForm(field_type); + +}; + +FormBuilder.prototype.addPreviewFieldToForm = function(field_type) { + $("#placeHolder").load($('#placeHolder').data('new-path') + '?field_type=' + field_type + ' .control-group', function() { + var newField = $("#placeHolder").find('.control-group'); + newField.insertBefore('#placeHolder'); + formBuilder.enableFieldButtons(); + formBuilder.resetAddFieldButton(); + }); +}; + +FormBuilder.prototype.resetAddFieldButton = function() { + $("#form_new_entry_new_field").val('1'); +}; + +FormBuilder.prototype.removeCurrentField = function() { + this.field_being_editted.remove(); + this.field_being_editted = null; +}; + +// Function that triggers when users click the 'Delete' field button. +FormBuilder.prototype.confirmDeleteFormField = function() { + formBuilder.field_being_editted = $(this).parents('.control-group'); + + var path = $(this).attr('data-path'); + if (path == "") { + formBuilder.removeCurrentField(); + } else { + $('#modal-confirm-delete-field').modal({ + show: true + }); + } +}; + +// Function that triggers when users click the 'Edit' field button. +FormBuilder.prototype.editFormField = function() { + // This is the overall container for the entire field. + formBuilder.field_being_editted = $(this).parents('.control-group'); + $('#modal-edit-field').removeData('modal').modal({ + show: true, + remote: $(this).attr('data-edit-path') + }); + +}; + + +FormBuilder.prototype.hideNewFormInstruction = function() { + var no_fields = $("#no-field-instructions"); + if (no_fields.exists()) { + no_fields.hide(); + } +}; + +// Add handler to any edit field buttons. +FormBuilder.prototype.enableFieldButtons = function() { + $('.edit_form_button').unbind('click').on('click', formBuilder.editFormField); + $('.delete_field_button').unbind('click').on('click', formBuilder.confirmDeleteFormField); +}; + +FormBuilder.prototype.newFormField = function() { + return $('#ajax_form_field'); +}; + +// Delete field from form, then remove it from the field +FormBuilder.prototype.deleteFormField = function() { + var element = formBuilder.field_being_editted.find('.delete_field_button'); + var url = element.attr('data-path'); + $.cms_ajax.delete({ + url: url, + success: function(field) { + formBuilder.removeCurrentField(); + formBuilder.removeFieldId(field.id); + } + }); +}; + +// @param [Number] value The id of the field that is to be removed from the form. +FormBuilder.prototype.removeFieldId = function(value) { + var field_ids = $('#field_ids').val().split(" "); + field_ids.splice($.inArray(value.toString(), field_ids), 1); + formBuilder.setFieldIds(field_ids); +}; + +// @param [Array] value +FormBuilder.prototype.setFieldIds = function(value) { + var spaced_string = value.join(" "); + $('#field_ids').val(spaced_string); +}; + +FormBuilder.prototype.addFieldIdToList = function(new_value) { + $('#field_ids').val($('#field_ids').val() + " " + new_value); +}; + +// Save a new Field to the database for the current form. +FormBuilder.prototype.createField = function() { + var form = formBuilder.newFormField(); + var data = form.serialize(); + var url = form.attr('action'); + + $.ajax({ + type: "POST", + url: url, + data: data, + global: false, + datatype: $.cms_ajax.asJSON() + }).done( + function(field) { + formBuilder.clearFieldErrorsOnCurrentField(); + + formBuilder.addFieldIdToList(field.id); + formBuilder.field_being_editted.find('input').attr('data-id', field.id); + formBuilder.field_being_editted.find('label').html(field.label); + formBuilder.field_being_editted.find('a').attr('data-edit-path', field.edit_path); + formBuilder.field_being_editted.find('a.delete_field_button').attr('data-path', field.delete_path); + formBuilder.field_being_editted.find('.help-block').html(field.instructions); + + } + ).fail(function(xhr, textStatus, errorThrown) { + formBuilder.displayErrorOnField(formBuilder.field_being_editted, xhr.responseJSON); + }); + +}; + +FormBuilder.prototype.clearFieldErrorsOnCurrentField = function() { + var field = formBuilder.field_being_editted; + field.removeClass("error"); + field.find('.help-inline').remove(); +}; + +FormBuilder.prototype.displayErrorOnField = function(field, json) { + var error_message = json.errors[0]; +// console.log(error_message); + field.addClass("error"); + var input_field = field.find('.input-append'); + input_field.after('' + error_message + ''); +}; + +// Attaches behavior to the proper element. +FormBuilder.prototype.setup = function() { + var select_box = $('.add-new-field'); + if (select_box.exists()) { + select_box.change(function() { + formBuilder.newField($(this).val()); + }); + + this.enableFieldButtons(); + $("#create_field").on('click', formBuilder.createField); + $("#delete_field").on('click', formBuilder.deleteFormField); + + // Edit Field should handle Enter by submitting the form via AJAX. + // Enter within textareas should still add endlines as normal. + $('#modal-edit-field').on('shown', function() { + formBuilder.newFormField().on("keypress", function(e) { + if (e.which == 13 && e.target.tagName != 'TEXTAREA') { + formBuilder.createField(); + e.preventDefault(); + $('#modal-edit-field').modal('hide'); + return false; + } + }); + }); + + // Allow fields to be sorted. + $('#form-preview').sortable({ + axis: 'y', + delay: 250, + + // When form element is moved + update: function(event, ui) { + var field_id = ui.item.find('input').attr('data-id'); + var new_position = ui.item.index() + 1; + formBuilder.moveFieldTo(field_id, new_position); + } + }); + this.setupConfirmationBehavior(); + this.enableFormCleanup(); + } +}; + +// Since we create a form for the #new action, we need to delete it if the user doesn't save it explicitly. +FormBuilder.prototype.enableFormCleanup = function() { + var cleanup_element = $('#cleanup-before-abandoning'); + if (cleanup_element.exists()) { + var cleanup_on_leave = true; + $(":submit").on('click', function() { + cleanup_on_leave = false; + }); + $(window).bind('beforeunload', function() { + if (cleanup_on_leave) { + var path = cleanup_element.attr('data-path'); + $.cms_ajax.delete({url: path, async: false}); + } + }); + } +}; + +// Updates the server with the new position for a given field. +FormBuilder.prototype.moveFieldTo = function(field_id, position) { + var url = '/cms/form_fields/' + field_id + '/insert_at/' + position; + + var success = function(data) { + console.log("Success:", data); + }; + console.log('For', field_id, 'to', position); + $.post(url, success); +}; + +FormBuilder.prototype.setupConfirmationBehavior = function() { + // Confirmation Behavior + $("#form_confirmation_behavior_show_text").on('click', function() { + $(".form_confirmation_text").show(); + $(".form_confirmation_redirect").hide(); + }); + $("#form_confirmation_behavior_redirect").on('click', function() { + $(".form_confirmation_redirect").show(); + $(".form_confirmation_text").hide(); + }); + $("#form_confirmation_behavior_show_text").trigger('click'); +}; +var formBuilder = new FormBuilder(); + +// Register FormBuilder handlers on page load. +$(function() { + formBuilder.setup(); + + + // Include a text field to start (For easier testing) +// formBuilder.newField('text_field'); +}); diff --git a/app/assets/stylesheets/cms/bootstrap-customizations.css.scss b/app/assets/stylesheets/cms/bootstrap-customizations.css.scss index bdd90d09c..be441cb5a 100644 --- a/app/assets/stylesheets/cms/bootstrap-customizations.css.scss +++ b/app/assets/stylesheets/cms/bootstrap-customizations.css.scss @@ -1,6 +1,17 @@ @import "bootstrap"; @import "bootstrap-responsive"; +#form-preview { + + // Add hover borders when over an element. + .control-group { + border: 1px solid transparent; + } + .control-group:hover { + border: 1px dashed #3B699F !important; + } +} + // For classes that need to explicitly extend bootstrap classes. // They must go in this file. @@ -95,4 +106,10 @@ textarea, #asset_add_uploader { display: none; +} + +// simpleform f.error generates this class for model wide attributes. +#base-errors .help-inline { + @extend .alert-error; + @extend .alert; } \ No newline at end of file diff --git a/app/assets/stylesheets/cms/default-forms.css.scss b/app/assets/stylesheets/cms/default-forms.css.scss new file mode 100644 index 000000000..31970c7c1 --- /dev/null +++ b/app/assets/stylesheets/cms/default-forms.css.scss @@ -0,0 +1,4 @@ +// Default styles for public CMS Forms (built via the forms module. + +@import "bootstrap"; +@import "bootstrap-responsive"; \ No newline at end of file diff --git a/app/controllers/cms/base_controller.rb b/app/controllers/cms/base_controller.rb index ed238ff6d..2de84f641 100644 --- a/app/controllers/cms/base_controller.rb +++ b/app/controllers/cms/base_controller.rb @@ -7,5 +7,15 @@ class BaseController < Cms::ApplicationController layout 'cms/application' + + # Disables the default security level for actions, meaning they will be available for guests to access. + # Users will not need to login prior to accessing these methods. + # + # @param [Array] methods List of methods to disable security for. + def self.allow_guests_to(methods) + skip_before_action :login_required, only: methods + skip_before_action :cms_access_required, only: methods + end + end end \ No newline at end of file diff --git a/app/controllers/cms/category_types_controller.rb b/app/controllers/cms/category_types_controller.rb index 399be1dd1..6f1e048dc 100644 --- a/app/controllers/cms/category_types_controller.rb +++ b/app/controllers/cms/category_types_controller.rb @@ -1,7 +1,12 @@ module Cms class CategoryTypesController < Cms::ContentBlockController def show - redirect_to category_types_path + redirect_to edit_category_type_path(id: params[:id]) + end + + def create + params[:_redirect_to] = category_types_path + super end end end \ No newline at end of file diff --git a/app/controllers/cms/content_block_controller.rb b/app/controllers/cms/content_block_controller.rb index 68dd2865e..b69943f00 100644 --- a/app/controllers/cms/content_block_controller.rb +++ b/app/controllers/cms/content_block_controller.rb @@ -6,12 +6,11 @@ class ContentBlockController < Cms::BaseController include Cms::ContentRenderingSupport layout 'cms/content_library' - skip_filter :cms_access_required, :login_required - before_filter :login_required, except: [:show_via_slug] - before_filter :cms_access_required, except: [:show_via_slug] + + allow_guests_to [:show_via_slug] before_filter :set_toolbar_tab - helper_method :block_form, :new_block_path, :block_path, :blocks_path, :content_type + helper_method :block_form, :content_type helper Cms::RenderingHelper def index @@ -76,7 +75,7 @@ def update def destroy do_command("deleted") { @block.destroy } respond_to do |format| - format.html { redirect_to_first params[:_redirect_to], blocks_path } + format.html { redirect_to_first params[:_redirect_to], engine_aware_path(@block.class) } format.json { render :json => {:success => true} } end @@ -86,14 +85,14 @@ def destroy def publish do_command("published") { @block.publish! } - redirect_to_first params[:_redirect_to], block_path(@block) + redirect_to_first params[:_redirect_to], engine_aware_path(@block, nil) end def revert_to do_command("reverted to version #{params[:version]}") do revert_block(params[:version]) end - redirect_to_first params[:_redirect_to], block_path(@block) + redirect_to_first params[:_redirect_to], engine_aware_path(@block, nil) end def version @@ -118,28 +117,11 @@ def usages end def new_button_path - cms_new_path_for(content_type) + new_engine_aware_path(content_type) end protected - def assign_parent_if_specified - if params[:parent] - @block.parent_id = params[:parent] - elsif @block.class.addressable? - parent = Cms::Section.with_path(@block.class.path).first - unless parent - logger.warn "Creating default section for #{@block.display_name} in #{@block.class.path}." - parent = Cms::Section.create(:name => @block.class.name.demodulize.pluralize, - :parent => Cms::Section.root.first, - :path => @block.class.path, - :hidden => true, - allow_groups: :all) - end - @block.parent_id = parent.id - end - end - def content_type_name self.class.name.sub(/Controller/, '').singularize end @@ -153,7 +135,7 @@ def model_class end def model_form_name - content_type.model_class_form_name + content_type.param_key end # methods for loading one or a collection of blocks @@ -189,14 +171,6 @@ def load_block_draft # path related methods - available in the view as helpers - def new_block_path(block, options={}) - cms_new_path_for(block, options) - end - - def blocks_path(options={}) - cms_index_path_for(@content_type.model_class, options) - end - # This is the partial that will be used in the form def block_form @content_type.form @@ -221,7 +195,6 @@ def set_default_category def create_block build_block - assign_parent_if_specified @block.save end @@ -231,7 +204,7 @@ def after_create_on_success if @block.class.connectable? && @block.connected_page redirect_to @block.connected_page.path else - redirect_to_first params[:_redirect_to], block_path(@block) + redirect_to_first params[:_redirect_to], engine_aware_path(@block) end end @@ -266,7 +239,7 @@ def model_params def after_update_on_success flash[:notice] = "#{content_type_name.demodulize.titleize} '#{@block.name}' was updated" - redirect_to_first params[:_redirect_to], block_path(@block) + redirect_to_first params[:_redirect_to], engine_aware_path(@block) end def after_update_on_failure @@ -279,18 +252,17 @@ def after_update_on_edit_conflict end - # methods for other actions - # A "command" is when you want to perform an action on a content block # You pass a ruby block to this method, this calls it # and then sets a flash message based on success or failure def do_command(result) load_block if yield - flash[:notice] = "#{content_type_name.demodulize.titleize} '#{@block.name}' was #{result}" + flash[:notice] = "#{content_type_name.demodulize.titleize} '#{@block.name}' was #{result}" unless request.xhr? else - flash[:error] = "#{content_type_name.demodulize.titleize} '#{@block.name}' could not be #{result}" + flash[:error] = "#{content_type_name.demodulize.titleize} '#{@block.name}' could not be #{result}" unless request.xhr? end + end def revert_block(to_version) @@ -330,8 +302,7 @@ def set_toolbar_tab def render_block_in_main_container ensure_current_user_can_view(@block) - @page = @block # page templates expect a @page attribute - @content_block = @block # render.html.erb's expect a @content_block attribute + show_content_as_page(@block) render 'render_block_in_main_container', layout: @block.class.layout end diff --git a/app/controllers/cms/dynamic_views_controller.rb b/app/controllers/cms/dynamic_views_controller.rb index 251469bdc..90aab2c69 100644 --- a/app/controllers/cms/dynamic_views_controller.rb +++ b/app/controllers/cms/dynamic_views_controller.rb @@ -20,7 +20,7 @@ def create @view = dynamic_view_type.new(dynamic_view_params) if @view.save flash[:notice] = "#{dynamic_view_type} '#{@view.name}' was created" - redirect_to cms_index_path_for(dynamic_view_type) + redirect_to engine_aware_path(dynamic_view_type) else render :action => "new" end @@ -31,9 +31,9 @@ def show end def update - if @view.update_attributes(dynamic_view_params) + if @view.update(dynamic_view_params) flash[:notice] = "#{dynamic_view_type} '#{@view.name}' was updated" - redirect_to cms_index_path_for(dynamic_view_type) + redirect_to engine_aware_path(dynamic_view_type) else render :action => "edit" end @@ -42,7 +42,7 @@ def update def destroy @view.destroy flash[:notice] = "#{dynamic_view_type} '#{@view.name}' was deleted" - redirect_to cms_index_path_for(dynamic_view_type) + redirect_to engine_aware_path(dynamic_view_type) end protected @@ -52,7 +52,7 @@ def dynamic_view_params end def view_param_name - dynamic_view_type.resource_collection_name + dynamic_view_type.model_name.param_key end def dynamic_view_type diff --git a/app/controllers/cms/form_entries_controller.rb b/app/controllers/cms/form_entries_controller.rb new file mode 100644 index 000000000..cda6814fd --- /dev/null +++ b/app/controllers/cms/form_entries_controller.rb @@ -0,0 +1,115 @@ +module Cms + class FormEntriesController < Cms::BaseController + + include ContentRenderingSupport + + helper_method :content_type + helper Cms::ContentBlockHelper + + allow_guests_to [:submit] + + # Handles public submission of a form. + def submit + find_form_and_populate_entry + if @entry.save + if @form.show_text? + show_content_as_page(@form) + render layout: Cms::Form.layout + else + redirect_to @form.confirmation_redirect + end + unless @form.notification_email.blank? + Cms::EmailMessage.create!( + :recipients => @form.notification_email, + :subject => "[CMS Form] A new entry has been created", + :body => "A visitor has filled out the #{@form.name} form. The entry can be found here: + #{Cms::EmailMessage.absolute_cms_url(cms.form_entry_path(@entry)) }" + ) + end + else + show_content_as_page(@form) + render 'error', layout: Cms::Form.layout + end + end + + # Same behavior as ContentBlockController#index + def index + form = Cms::Form.where(id: params[:id]).first + @blocks = Cms::FormEntry.where(form_id: params[:id]).paginate({page: params[:page], order: params[:order]}) + #Shim for buttonbar + @content_type = FauxContentType.new(form) + @entry = Cms::FormEntry.for(form) + + render 'cms/content_block/index' + end + + def edit + @entry = Cms::FormEntry.find(params[:id]) + end + + def update + @entry = Cms::FormEntry.find(params[:id]).enable_validations + if @entry.update(entry_params(@entry)) + redirect_to form_entry_path(@entry) + else + render :edit + end + end + + def show + @entry = Cms::FormEntry.find(params[:id]) + end + + def new + @entry = Cms::FormEntry.for(Form.find(params[:form_id])) + end + + def create + find_form_and_populate_entry + if @entry.save + redirect_to entries_path(@form) + else + save_entry_failure + end + end + + def save_entry_failure + render :new + end + + protected + + def find_form_and_populate_entry + @form = Cms::Form.find(params[:form_id]) + @entry = Cms::FormEntry.for(@form) + @entry.attributes = entry_params(@entry) + end + + def entry_params(entry) + params.require(:form_entry).permit(entry.permitted_params) + end + + # Allows Entries to be displayed using same view as Content Blocks. + class FauxContentType < Cms::ContentType + def initialize(form) + @form = form + self.name = 'Cms::FormEntry' + end + + def display_name + 'Entry' + end + + def columns_for_index + cols = @form.fields.collect do |field| + {:label => field.label, :method => field.name} + end + cols + end + end + + def content_type + @content_type + end + end +end \ No newline at end of file diff --git a/app/controllers/cms/form_fields_controller.rb b/app/controllers/cms/form_fields_controller.rb new file mode 100644 index 000000000..f61e68789 --- /dev/null +++ b/app/controllers/cms/form_fields_controller.rb @@ -0,0 +1,74 @@ +module Cms + class FormFieldsController < Cms::BaseController + + layout false + + def new + @field = Cms::FormField.new(label: 'Untitled', field_type: params[:field_type], form_id: params[:form_id]) + end + + def preview + @form = Cms::Form.find(params[:id]) + @field = Cms::FormField.new(label: 'Untitled', name: :untitled, field_type: params[:field_type], form: @form) + end + + def create + form = Cms::Form.find(params[:form_field].delete(:form_id)) + field = FormField.new(form_field_params) + field.form = form + if field.save + include_edit_path_in_json(field) + include_delete_path_in_json(field) + render json: field + else + render json: { + errors: field.errors.full_messages + }, + success: false, + status: :unprocessable_entity + end + end + + def edit + @field = FormField.find(params[:id]) + render :new + end + + def update + field = FormField.find(params[:id]) + if field.update form_field_params + include_edit_path_in_json(field) + render json: field + else + render text: "Fail", status: 500 + end + end + + def destroy + field = FormField.find(params[:id]) + field.destroy + render json: field, success: true + end + + def insert_at + field = FormField.find(params[:id]) + field.insert_at(params[:position]) + render json: field + end + + protected + + # For UI to update for subsequent editing. + def include_edit_path_in_json(field) + field.edit_path = cms.edit_form_field_path(field) + end + + def include_delete_path_in_json(field) + field.delete_path = cms.form_field_path(field) + end + + def form_field_params() + params.require(:form_field).permit(FormField.permitted_params) + end + end +end \ No newline at end of file diff --git a/app/controllers/cms/forms_controller.rb b/app/controllers/cms/forms_controller.rb new file mode 100644 index 000000000..95b30879d --- /dev/null +++ b/app/controllers/cms/forms_controller.rb @@ -0,0 +1,33 @@ +class Cms::FormsController < Cms::ContentBlockController + + before_filter :associate_form_fields, only: [:create, :update] + before_filter :strip_new_entry_params, only: [:create, :update] + + helper do + # For new forms, if the user doesn't complete and save them, we need to delete them from the database. + def cleanup_before_abandoning + ["new", "create"].include? action_name + end + end + + def new + super + @block.confirmation_text = "Thanks for filling out this form." + @block.save! + end + + protected + + # Split the space separated list of ids into an actual array of ids. + # Rails might have a more conventional way to do this, but I couldn't figure it out.' + def associate_form_fields + field_ids = params[:field_ids].split(" ") + params[:form][:field_ids] = field_ids + end + + + # params[:form][:new_entry] is just a garbage parameter that exists to make displaying forms work. We want to ignore anything submitted here + def strip_new_entry_params + params[:form].delete(:new_entry) + end +end diff --git a/app/controllers/cms/portlets_controller.rb b/app/controllers/cms/portlets_controller.rb index ceb78ad67..841980b0e 100644 --- a/app/controllers/cms/portlets_controller.rb +++ b/app/controllers/cms/portlets_controller.rb @@ -12,29 +12,19 @@ def build_block if params[:type].blank? @block = model_class.new else - @block = params[:type].classify.constantize.new(params[params[:type]]) + @block = params[:type].classify.constantize.new(params[:portlet]) end + end def update_block load_block - @block.update_attributes(params[@block.class.name.underscore]) + @block.update(params[:portlet]) end def block_form "portlets/portlets/form" end - - def new_block_path(block) - new_portlet_path - end - - def block_path(block, action=nil) - send("#{action ? "#{action}_" : ""}portlet_path", block) - end - def blocks_path - portlets_path - end end end \ No newline at end of file diff --git a/app/controllers/cms/resource_controller.rb b/app/controllers/cms/resource_controller.rb index c27d6b68e..d3a55d746 100644 --- a/app/controllers/cms/resource_controller.rb +++ b/app/controllers/cms/resource_controller.rb @@ -36,7 +36,7 @@ def edit def update @object = resource.find(params[:id]) - if @object.update_attributes(resource_params()) + if @object.update(resource_params()) flash[:notice] = "#{resource_name.singularize.titleize} '#{object_name}' was updated" redirect_to after_update_url else @@ -95,7 +95,7 @@ def object_name end def index_url - cms_index_url_for(resource_name) + engine_aware_path(resource) end def after_create_url diff --git a/app/helpers/cms/application_helper.rb b/app/helpers/cms/application_helper.rb index 0e4695d03..5f4e78548 100644 --- a/app/helpers/cms/application_helper.rb +++ b/app/helpers/cms/application_helper.rb @@ -146,11 +146,11 @@ def render_pagination(collection, content_type, options={}) model_class = content_type.instance_of?(Class) ? content_type : content_type.model_class render :partial => "cms/shared/pagination", :locals => { :collection => collection, - :first_page_path => cms_connectable_path(model_class, {:page => 1}.merge(options)), - :previous_page_path => cms_connectable_path(model_class, {:page => collection.previous_page ? collection.previous_page : 1}.merge(options)), - :current_page_path => cms_connectable_path(model_class, options), - :next_page_path => cms_connectable_path(model_class, {:page => collection.next_page ? collection.next_page : collection.current_page}.merge(options)), - :last_page_path => cms_connectable_path(model_class, {:page => collection.total_pages}.merge(options)) + :first_page_path => polymorphic_path(engine_aware_path(model_class), {:page => 1}.merge(options)), + :previous_page_path => polymorphic_path(engine_aware_path(model_class), {:page => collection.previous_page ? collection.previous_page : 1}.merge(options)), + :current_page_path => polymorphic_path(engine_aware_path(model_class), options), + :next_page_path => polymorphic_path(engine_aware_path(model_class), {:page => collection.next_page ? collection.next_page : collection.current_page}.merge(options)), + :last_page_path => polymorphic_path(engine_aware_path(model_class), {:page => collection.total_pages}.merge(options)) } end end diff --git a/app/helpers/cms/content_block_helper.rb b/app/helpers/cms/content_block_helper.rb index 16169803e..84c0c26e5 100644 --- a/app/helpers/cms/content_block_helper.rb +++ b/app/helpers/cms/content_block_helper.rb @@ -38,13 +38,13 @@ def content_block_tr_tag(block) options[:class] = [cname] options[:class] << 'non-editable' unless can_modify && current_user.able_to?(:edit_content) options[:class] << 'non-publishable' unless can_modify && current_user.able_to?(:publish_content) - options['data-new_path'] = url_for(new_block_path(block)) - options['data-view_path'] = url_for(block_path(block)) - options['data-edit_path'] = url_for(block_path(block, :edit)) + options['data-new_path'] = url_for(new_engine_aware_path(block)) + options['data-view_path'] = url_for(engine_aware_path(block, nil)) + options['data-edit_path'] = url_for(edit_engine_aware_path(block)) options['data-preview_path'] = block.path if block.class.addressable? - options['data-versions_path'] = url_for(block_path(block, :versions)) if block.class.versioned? - options['data-delete_path'] = url_for(block_path(block)) - options['data-publish_path'] = url_for(block_path(block, :publish)) if block.class.publishable? + options['data-versions_path'] = engine(block).polymorphic_path(block, action: :versions) if block.class.versioned? + options['data-delete_path'] = url_for(engine_aware_path(block)) + options['data-publish_path'] = engine(block).polymorphic_path([:publish, block]) if block.class.publishable? tag "tr", options, true end diff --git a/app/helpers/cms/form_tag_helper.rb b/app/helpers/cms/form_tag_helper.rb index 1e955e43c..6827cdf04 100644 --- a/app/helpers/cms/form_tag_helper.rb +++ b/app/helpers/cms/form_tag_helper.rb @@ -8,7 +8,7 @@ module FormTagHelper # support. def content_block_form_for(object, *args, &block) options = args.extract_options! - simple_form_for(object, *(args << options.merge(builder: Cms::FormBuilder::ContentBlockFormBuilder)), &block) + simple_form_for(engine_aware_path(object), *(args << options.merge(builder: Cms::FormBuilder::ContentBlockFormBuilder)), &block) end diff --git a/app/helpers/cms/page_helper.rb b/app/helpers/cms/page_helper.rb index 7b8b661f0..4bc9ccc31 100644 --- a/app/helpers/cms/page_helper.rb +++ b/app/helpers/cms/page_helper.rb @@ -44,7 +44,13 @@ def page_title(*args) if args.first deprecated_set_page_title_usage(args) else - @page_title ? @page_title : current_page.page_title + if @page_title + @page_title + elsif current_page + current_page.page_title + else + "Untitled" + end end end diff --git a/app/helpers/cms/path_helper.rb b/app/helpers/cms/path_helper.rb index cc6284831..6f5afb2b7 100644 --- a/app/helpers/cms/path_helper.rb +++ b/app/helpers/cms/path_helper.rb @@ -20,54 +20,12 @@ def attachment_path_for(attachment) # # @param [Cms::ContentType] content_type # @param [String] column_to_sort The name of the column to sort on. - def cms_sortable_column_path(content_type, column_to_sort) + def sortable_column_path(content_type, column_to_sort) filtered_params = params.clone filtered_params.delete(:action) filtered_params.delete(:controller) filtered_params.merge!(:order => determine_order(filtered_params[:order], column_to_sort)) - cms_connectable_path(content_type.model_class, filtered_params) - end - - # @deprecated Use cms_connectable_path instead. - def cms_index_path_for(resource, options={}) - polymorphic_path(build_path_for(resource), options) - end - - # @deprecated Remove all usages of this in favor of cms_index_path_for () - def cms_index_url_for(resource, options={}) - send("#{resource_collection_name(resource).underscore.pluralize.gsub('/', '_')}_url", options) - end - - def cms_new_path_for(resource, options={}) - new_polymorphic_path(build_path_for(resource), options) - end - - # @deprecated Remove all usages of this in favor of cms_new_path_for () - def cms_new_url_for(resource, options={}) - send("new_#{resource_collection_name(resource).underscore.gsub('/', '_')}_url", options) - end - - # @param [Class, String] connectable The model class (i.e. HtmlBlock) or plural collection name (html_blocks) to link to - # @param [Hash] options Passed to polymorphic_path - # - # @return [String] path suitable to give to link_to - def cms_connectable_path(connectable, options={}) - if connectable.is_a?(Class) && connectable < Portlet - cms.portlet_path(connectable) - else - polymorphic_path(build_path_for(connectable), options) - end - end - - # @todo Really needs to be renamed to match conventions for Engines. - # In CMS::Engine, should be edit_connectable_path - # From app, should be cms.edit_connectable_path - def edit_cms_connectable_path(connectable, options={}) - if Portlet === connectable - edit_portlet_path(connectable, options) - else - edit_polymorphic_path(build_path_for(connectable), options) - end + polymorphic_path(engine_aware_path(content_type.model_class), filtered_params) end def link_to_usages(block) @@ -80,7 +38,7 @@ def link_to_usages(block) p = [] p << engine_for(block) p << :usages - p.concat path_elements_for(block) + p << block p end link_to count, path, :id => block.id, :block_type => block.content_block_type @@ -89,65 +47,58 @@ def link_to_usages(block) end end - # Return the path for a given block. Similar to polymorphic_path but handles resources from different engines. + + # Returns the route proxy (aka engine) for a given resource, which can then have named paths called on it. + # I.e. engine(@block).polymorphic_path([@block, :preview]) + # + # @param [ActiveRecord::Base] resource + # @return [ActionDispatch::Routing::RoutesProxy] + def engine(resource) + name = EngineAwarePathBuilder.new(resource).engine_name + send(name) + end + + # @deprecated + alias :engine_for :engine + + # Return the path for a given resource. Determines the relevant engine, and the result can be passed to polymporhic_path # - # @param [Object] block A content block + # @param [Object] model_or_class_or_content_type A content block, class or content type. # @param [String] action (Optional) i.e. :edit # @return [Array] An array of argument suitable to be passed to url_for or link_to helpers. This will be something like: - # [main_app, :cms, :products, @block, :edit] + # [main_app, :dummy_products, @block, :edit] # or [cms, :html_blocks, @block] # # This will work whether the block is: - # 1. A custom unnamespaced block in a project (i.e. Product) + # 1. A block in a project (namespaced to the project) (i.e. Dummy::Product) # 2. A core CMS block (i.e. Cms::Portlet) # 3. A block in a module (i.e. BcmsNews::NewsArticle) # e.g. - # block_path(Product.find(1)) => /cms/products/1 - # block_path(Cms::HtmlBlock.find(1)) => /cms/html_blocks/1 - # block_path(BcmsNews::NewsArticle.find(1)) => /bcms_news/news_articles/1 + # engine_aware_path(Dummy::Product.find(1)) => /dummy/products/1 + # engine_aware_path(Cms::HtmlBlock.find(1)) => /cms/html_blocks/1 + # engine_aware_path(BcmsNews::NewsArticle.find(1)) => /bcms_news/news_articles/1 # - def block_path(block, action=nil) - path = [] - path << engine_for(block) - path << action if action - path.concat path_elements_for(block) - path + def engine_aware_path(model_or_class_or_content_type, action = false) + elements = build_path_for(model_or_class_or_content_type) + elements << action if action + elements end - # Returns the Engine Proxy that this resource is from. - def engine_for(resource) - EngineHelper.decorate(resource) - send(resource.engine_name) + # Wrappers edit_polymorphic_path to be engine aware. + def edit_engine_aware_path(model_or_class_or_content_type, options={}) + edit_polymorphic_path(build_path_for(model_or_class_or_content_type), options) end - def path_elements_for(resource) - EngineHelper.decorate(resource) - resource.path_elements + # Wrappers new_polymorphic_path to be engine aware. + def new_engine_aware_path(subject, options={}) + new_polymorphic_path(build_path_for(subject), options) end private - def build_path_for(model_or_class_or_content_type) Cms::EngineAwarePathBuilder.new(model_or_class_or_content_type).build(self) end - # Returns the name of the collection that this resource belongs to - # the resource can be a ContentType, ActiveRecord::Base instance - # or just a string or symbol - def resource_collection_name(resource) - if resource.respond_to?(:resource_collection_name) - return resource.resource_collection_name - end - case resource - when ContentType then - resource.route_name - when ActiveRecord::Base then - resource.class.model_name.demodulize - else - resource.to_s - end - end - end end diff --git a/app/helpers/cms/ui_elements_helper.rb b/app/helpers/cms/ui_elements_helper.rb index 603b8f2a8..0196277c4 100644 --- a/app/helpers/cms/ui_elements_helper.rb +++ b/app/helpers/cms/ui_elements_helper.rb @@ -27,7 +27,7 @@ def publish_menu_button(content_item) options = {class: ["btn", "btn-primary", "http_put"], id: "publish_button"} path = "#" if current_user.able_to?(:publish_content) && !content_item.new_record? && content_item.respond_to?(:live?) && !content_item.live? - path = block_path(@block, :publish) + path = engine(@block).polymorphic_path([:publish, @block]) else options[:class] << "disabled" end @@ -37,7 +37,7 @@ def publish_menu_button(content_item) def edit_content_menu_button(content_item) path = "#" unless content_item.new_record? - path = block_path(content_item, :edit) + path = edit_engine_aware_path(content_item) end link_to "Edit Content", path, class: "btn btn-primary", id: "edit_button" end @@ -45,7 +45,7 @@ def edit_content_menu_button(content_item) def view_content_menu_button(content_item) path = "#" unless content_item.new_record? - path = block_path(content_item) + path = engine_aware_path(content_item, nil) end link_to "View Content", path, class: "btn btn-primary", id: "view_button" end @@ -79,7 +79,7 @@ def versions_menu_button(content_item) options = {class: ["btn", "btn-primary"], id: "revisions_button"} path = "#" if !content_item.new_record? && content_item.class.versioned? - path = block_path(content_item, :versions) + path = engine(content_item).polymorphic_path([:versions, content_item]) else options[:class] << "disabled" end @@ -106,7 +106,7 @@ def delete_menu_button(content_item=nil, opts={class: []}) classes << 'disabled' else options[:title] = "Are you sure you want to delete '#{content_item.name}'?" - link_to_path = block_path(content_item) + link_to_path = engine_aware_path(content_item, nil) end if opts[:title] options[:title] = opts[:title] @@ -115,7 +115,7 @@ def delete_menu_button(content_item=nil, opts={class: []}) end def select_content_type_tag(type, &block) - options = {:rel => "select-#{type.key}"} + options = {:rel => "select-#{type.param_key}"} if (defined?(content_type) && content_type == type) options[:class] = "on" end diff --git a/app/inputs/cms_text_field_input.rb b/app/inputs/cms_text_field_input.rb index 4214e72f2..d6517a3c8 100644 --- a/app/inputs/cms_text_field_input.rb +++ b/app/inputs/cms_text_field_input.rb @@ -23,7 +23,7 @@ def input protected def should_autogenerate_slug?(method) - content_requires_slug_field?(method) && object.new_record? + content_requires_slug_field?(method) && (object.new_record? || (object.name.blank? && object.slug.blank?)) end def content_requires_slug_field?(method) diff --git a/app/models/cms/attachment.rb b/app/models/cms/attachment.rb index 015d9a46c..36d5b8ae6 100644 --- a/app/models/cms/attachment.rb +++ b/app/models/cms/attachment.rb @@ -261,14 +261,5 @@ def set_cardinality end end - - # Forces this record to be changed, even if nothing has changed - # This is necessary if just the section.id has changed, for example - # test if this is necessary now that the attributes are in the - # model itself. - def dirty! - # Seems like a hack, is there a better way? - self.updated_at = Time.now - end end end diff --git a/app/models/cms/content_type.rb b/app/models/cms/content_type.rb index 5098ddc3c..8b327bdc0 100644 --- a/app/models/cms/content_type.rb +++ b/app/models/cms/content_type.rb @@ -5,8 +5,12 @@ class ContentType def initialize(options) self.name = options[:name] + @path_builder = EngineAwarePathBuilder.new(model_class) end + attr_accessor :path_builder + delegate :main_app_model?, :engine_name, :engine_class, to: :path_builder + DEFAULT_CONTENT_TYPE_NAME = 'Cms::HtmlBlock' class << self @@ -110,18 +114,9 @@ def module_name model_class.content_module end - # Returns URL friendly 'key' which is used to identify this - def key - model_class_form_name - end - # Returns the partial used to render the form fields for a given block. def form - f = model_class.respond_to?(:form) ? model_class.form : "#{name.underscore.pluralize}/form" - if main_app_model? - f = "cms/#{f}" - end - f + model_class.respond_to?(:form) ? model_class.form : "#{name.underscore.pluralize}/form" end def display_name @@ -136,25 +131,6 @@ def model_class name.constantize end - # @deprecated Should be removed eventually - def route_name - if model_class.name.starts_with?("Cms") - model_class_form_name - else - "main_app.cms_#{model_class_form_name}" - end - end - - include EngineHelper - - def target_class - model_class - end - - def path_subject - model_class - end - # Determines if the content can be connected to other pages. def connectable? model_class.connectable? @@ -162,8 +138,8 @@ def connectable? # Cms::HtmlBlock -> html_block # ThingBlock -> thing_block - def model_class_form_name - model_class.model_name.element + def param_key + model_class.model_name.param_key end # Allows models to show additional columns when being shown in a list. diff --git a/app/models/cms/dynamic_view.rb b/app/models/cms/dynamic_view.rb index f4f6308bf..31e05b3db 100644 --- a/app/models/cms/dynamic_view.rb +++ b/app/models/cms/dynamic_view.rb @@ -57,10 +57,6 @@ def self.base_path File.join(Rails.root, "tmp", "views") end - def self.form_name - ::ActiveModel::Naming.singular(self) - end - def file_name "#{name}.#{format}.#{handler}" end @@ -94,23 +90,6 @@ def set_publish_on_save self.publish_on_save = true end - # Get the plural symbol for a particular resource. - # i.e. Cms::PageTemplate -> :page_templates - def self.resource_name - resource_collection_name.pluralize - end - - # Default implementation - def self.resource_collection_name - model_name.plural - end - - # So that route lookup works for these resources. - # See PathHelper#cms_index_path_for - def self.engine - "cms" - end - def set_path self.path = self.class.relative_path + '/' + name end diff --git a/app/models/cms/email_message.rb b/app/models/cms/email_message.rb index a61f4b228..5b909af7b 100644 --- a/app/models/cms/email_message.rb +++ b/app/models/cms/email_message.rb @@ -2,8 +2,9 @@ module Cms class EmailMessage < ActiveRecord::Base extend DefaultAccessible + extend Cms::DomainSupport - scope :undelivered, -> {where("delivered_at is null")} + scope :undelivered, -> { where("delivered_at is null") } validates_presence_of :recipients after_create :deliver_now @@ -18,6 +19,34 @@ def self.deliver! end end + # Returns a clean (non-www prefixed) domain name + def self.normalize_domain(domain) + normalized = domain =~ /^www/ ? domain.sub(/^www\./, "") : domain + # Strip the port + URI.parse("http://#{normalized}").host + end + + # Converts a relative path to a path in the CMS. Used for creating a links to internal pages in the body of emails. + # + # @param [String] path A relative path (i.e. /cms/your-page) + # @return [String] (i.e http://cms.example.com/cms/your-page) + def self.absolute_cms_url(path) + host = normalize_domain(Rails.configuration.cms.site_domain) + "http://#{cms_domain_prefix}.#{host}#{path}" + end + + # Returns a default address that mail will be sent from. (i.e. mailbot@example.com) + def self.mailbot_address + address = Rails.configuration.cms.mailbot + if address == :default + host = normalize_domain(Rails.configuration.cms.site_domain) + "mailbot@#{host}" + else + address + end + + end + #TODO: Take this out when we have an email queue processor def deliver_now deliver! @@ -25,6 +54,7 @@ def deliver_now def deliver! return false if delivered? + self.sender = self.class.mailbot_address if self.sender.blank? Cms::EmailMessageMailer.email_message(self).deliver update_attributes(:delivered_at => Time.now) end diff --git a/app/models/cms/form.rb b/app/models/cms/form.rb new file mode 100644 index 000000000..e9f1dee1c --- /dev/null +++ b/app/models/cms/form.rb @@ -0,0 +1,47 @@ +module Cms + class Form < ActiveRecord::Base + acts_as_content_block + content_module :forms + is_addressable path: '/forms', template: 'default' + + has_many :fields, -> {order(:position)}, class_name: 'Cms::FormField' + has_many :entries, class_name: 'Cms::FormEntry' + + validates_format_of :notification_email, :with => /@/, unless: lambda { |form| form.notification_email.blank? } + + # Copy any field related errors into the model :base so they get displayed at the top of the form. + after_validation do + unless errors[:fields].empty? + fields.each do |field| + field.errors.each do |attribute, error| + errors[:base] << field.errors.full_message(attribute, error) + end + + end + end + end + + def field(name) + fields.select {|f| f.name == name}.first + end + + def required?(name) + field = field(name) + field ? field.required? : false + end + + def field_names + fields.collect { |f| f.name } + end + + def show_text? + confirmation_behavior.to_sym == :show_text + end + + # Provides a sample Entry for the form. + # This allows us to use SimpleForm to layout out elements but ignore the input when the form submits. + def new_entry + Cms::Entry.new(form: self) + end + end +end diff --git a/app/models/cms/form_entry.rb b/app/models/cms/form_entry.rb new file mode 100644 index 000000000..0369912c4 --- /dev/null +++ b/app/models/cms/form_entry.rb @@ -0,0 +1,71 @@ +module Cms + class FormEntry < ActiveRecord::Base + + store :data_columns + belongs_to :form, class_name: 'Cms::Form' + + after_initialize :add_field_accessors + + def permitted_params + form.field_names + end + + # Returns a copy of the persisted object. This is required so that existing records fetched from the db can + # have validation during update operations. + # + # @return [Cms::FormEntry] A copy of this record with validations enabled on it. + def enable_validations + entry = FormEntry.for(form) + entry.attributes = self.attributes + entry.instance_variable_set(:@new_record, false) + entry + end + + class << self + + # Create an Entry for a specific Form. It will have validation and accessors based on the fields of the form. + # + # @param [Cms::Form] form + def for(form) + entry = FormEntry.ish(form: form) { + form.field_names.each do |field_name| + if form.required?(field_name) + validates field_name, presence: true + end + end + } + entry + end + + # Create an instance of a FormEntry with the given methods. + def ish(*args, &block) + dup(&block).new(*args) + end + + # Creates a faux class with singleton methods. Solves this problem: + # https://github.com/rails/rails/issues/5449 + def dup(&block) + super.tap do |dup| + def dup.name() + FormEntry.name + end + + dup.class_eval(&block) if block + end + end + end + private + + # Add a single field accessor to the current instance of the object. (I.e. not shared with others) + def add_store_accessor(field_name) + singleton_class.class_eval { store_accessor :data_columns, field_name } + end + + def add_field_accessors + return unless form + form.field_names.each do |field_name| + add_store_accessor(field_name) + end + end + end +end \ No newline at end of file diff --git a/app/models/cms/form_field.rb b/app/models/cms/form_field.rb new file mode 100644 index 000000000..393f28d11 --- /dev/null +++ b/app/models/cms/form_field.rb @@ -0,0 +1,78 @@ +module Cms + class FormField < ActiveRecord::Base + extend DefaultAccessible + + belongs_to :form + acts_as_list scope: :form + + attr_accessor :edit_path, :delete_path + + # Name is assigned when the Field is created, and should not be changed afterwords. + # Otherwise existing entries will not display their data correctly. + # + # FormField#name is used for input fields. I.e. <%= f.input field.name %> + before_validation(on: :create) do + self.name = label.parameterize.underscore.to_sym if label + end + + validates :name, :uniqueness => {:scope => :form_id, message: "can only be used once per form."} + + def as_json(options={}) + super(:methods => [:edit_path, :delete_path]) + end + + + # Return the form widget that should be used to render this field. + # + # @return [Symbol] A SimpleForm input mapping (i.e. :string, :text) + def as + case field_type + when "text_field" + :string + when "text_area" + :text + else + field_type.to_sym + end + end + + # Return options to be passed to a SimpleForm input. + # @return [Hash] + # @param [Hash] config + # @option config [Boolean] :disabled If the field should be disabled. + # @option config [FormEntry] :entry + def options(config={}) + opts = {label: label} + if field_type != "text_field" + opts[:as] = self.as + end + if config[:disabled] + opts[:disabled] = true + opts[:readonly] = 'readonly' + end + opts[:required] = !!required + opts[:hint] = instructions if instructions + unless choices.blank? + opts[:collection] = parse_choices + opts[:prompt] = !self.required? + end + unless config[:entry] && config[:entry].send(name) + opts[:input_html] = {value: default_value} + end + opts + end + + + # Don't allow name to be set via UI. + def self.permitted_params + super - [:name] + end + + private + + # Choices is a single text area where choices are divided by endlines. + def parse_choices + choices.split( /\r?\n/ ) + end + end +end \ No newline at end of file diff --git a/app/models/cms/page_partial.rb b/app/models/cms/page_partial.rb index b224d5473..435827717 100644 --- a/app/models/cms/page_partial.rb +++ b/app/models/cms/page_partial.rb @@ -18,14 +18,6 @@ def self.display_name(file_name) "#{name.sub(/^_/, '').titleize} (#{format}/#{handler})" end - def self.resource_collection_name - "page_partial" - end - - def self.path_elements - [Cms::PagePartial] - end - def prepend_underscore if !name.blank? && name[0, 1] != '_' self.name = "_#{name}" diff --git a/app/models/cms/page_template.rb b/app/models/cms/page_template.rb index 8a891e533..ae4903bec 100644 --- a/app/models/cms/page_template.rb +++ b/app/models/cms/page_template.rb @@ -20,14 +20,6 @@ def self.display_name(file_name) "#{name.titleize} (#{format}/#{handler})" end - def self.resource_collection_name - "page_template" - end - - def self.path_elements - [Cms::PageTemplate] - end - # This is a combination of file system page templates # and database page templates def self.options diff --git a/app/models/cms/portlet.rb b/app/models/cms/portlet.rb index 0777ea361..80c9568e1 100644 --- a/app/models/cms/portlet.rb +++ b/app/models/cms/portlet.rb @@ -31,6 +31,7 @@ def self.inherited(subclass) super if defined? super ensure subclass.class_eval do + extend Cms::PolymorphicSingleTableInheritance # Using the table prefix here is NOT tested, since unloading classes is hard during tests. has_dynamic_attributes :class_name => "CmsPortletAttribute", diff --git a/app/models/cms/task.rb b/app/models/cms/task.rb index 6a40bc718..796ddecb2 100644 --- a/app/models/cms/task.rb +++ b/app/models/cms/task.rb @@ -9,7 +9,6 @@ class Task < ActiveRecord::Base belongs_to :page, :class_name => 'Cms::Page' extend DefaultAccessible - #attr_accessible :assigned_by, :assigned_to, :page after_create :mark_other_tasks_for_the_same_page_as_complete after_create :send_email @@ -47,24 +46,13 @@ def mark_other_tasks_for_the_same_page_as_complete end def send_email - #Hmm... what if the assign_by or assign_to don't have email addresses? - #For now we'll say just don't send an email and log that as a warning - if assigned_by.email.blank? - logger.warn "Can't send email for task because assigned by user #{assigned_by.login}:#{assigned_by.id} has no email address" - elsif assigned_to.email.blank? + if assigned_to.email.blank? logger.warn "Can't send email for task because assigned to user #{assigned_to.login}:#{assigned_to.id} has no email address" else - domain = Rails.configuration.cms.site_domain - if domain =~ /^www/ - host = domain.sub(/^www\./, "#{cms_domain_prefix}.") - else - host = "#{cms_domain_prefix}.#{domain}" - end - email = Cms::EmailMessage.create( - :sender => assigned_by.email, + Cms::EmailMessage.create( :recipients => assigned_to.email, :subject => "Page '#{page.name}' has been assigned to you", - :body => "http://#{host}#{page.path}\n\n#{comment}" + :body => "#{Cms::EmailMessage.absolute_cms_url(page.path)}\n\n#{comment}" ) end true #don't accidently return false and halt the chain diff --git a/app/views/cms/content_block/_buttonbar.html.erb b/app/views/cms/content_block/_buttonbar.html.erb index 9424d9270..b9ebaa29b 100644 --- a/app/views/cms/content_block/_buttonbar.html.erb +++ b/app/views/cms/content_block/_buttonbar.html.erb @@ -9,7 +9,7 @@ - If block is already published. %> - <%= menu_button("Back to #{@block.class.display_name_plural}", cms_connectable_path(@block.class)) unless @block.new_record? %> + <%= menu_button("Back to #{@block.class.content_type.display_name_plural}", engine_aware_path(@block.class)) unless @block.new_record? %> <%= publish_menu_button(@block) %> <% if @block.class.addressable? %> <%= link_to("Preview", @block.path, id: "preview_button", target: "_blank", class: "btn btn-primary") %> @@ -20,7 +20,7 @@ <%= versions_menu_button(@block) %> <%= delete_menu_button(@block) %> <% if @block.persisted? %> - <% if !@block.live? %> + <% if @block.publishable? && !@block.live? %> DRAFT <% else %> Published diff --git a/app/views/cms/content_block/_form_errors.html.erb b/app/views/cms/content_block/_form_errors.html.erb new file mode 100644 index 000000000..637c8cd38 --- /dev/null +++ b/app/views/cms/content_block/_form_errors.html.erb @@ -0,0 +1,4 @@ +<%= form_errors.error_notification %> + +<%= form_errors.error :base %> + \ No newline at end of file diff --git a/app/views/cms/content_block/edit.html.erb b/app/views/cms/content_block/edit.html.erb index 6c9b02cbf..1dddf0509 100644 --- a/app/views/cms/content_block/edit.html.erb +++ b/app/views/cms/content_block/edit.html.erb @@ -5,8 +5,8 @@ <% if @block.respond_to?(:deleted) && @block.deleted %>
This <%= @block.class.name %> has been deleted.
<% else %> -<%= content_block_form_for(@block, :url => block_path(@block), :html => {:multipart => true}) do |f| %> - <%= f.error_notification %> +<%= content_block_form_for(@block, :html => {:multipart => true}) do |f| %> + <%= render partial: 'form_errors', object: f %> <%= render :partial => 'cms/shared/exception', :object => @exception if @exception %> <% if @other_version %> <%= render :partial => 'cms/shared/version_conflict_error', :locals => {:other_version => @other_version, :your_version => @block} %> diff --git a/app/views/cms/content_block/index.html.erb b/app/views/cms/content_block/index.html.erb index 4c17ea7d4..1d8c0ac84 100644 --- a/app/views/cms/content_block/index.html.erb +++ b/app/views/cms/content_block/index.html.erb @@ -22,7 +22,7 @@ >
<% if column[:order] %> - <%= link_to column[:label], cms_sortable_column_path(content_type, column[:order])%> + <%= link_to column[:label], sortable_column_path(content_type, column[:order])%> <% else %> <%= column[:label] %> <% end %> diff --git a/app/views/cms/content_block/new.html.erb b/app/views/cms/content_block/new.html.erb index f831cc4e2..2cbac0ecd 100644 --- a/app/views/cms/content_block/new.html.erb +++ b/app/views/cms/content_block/new.html.erb @@ -1,8 +1,8 @@ <% use_page_title "Content Library / Add New #{content_type.display_name}" %> <% @toolbar_title = "Add New #{content_type.display_name}" %> <%= content_for :button_bar, render('buttonbar') %> -<%= content_block_form_for(@block, :url => blocks_path, :html => {:multipart => true}) do |f| %> - <%= f.error_notification %> +<%= content_block_form_for(@block, :html => {:multipart => true}) do |f| %> + <%= render partial: 'form_errors', object: f %> <%= render :partial => 'cms/shared/exception', :object => @exception if @exception %> <%= render :partial => 'hidden_fields', :locals => {:f => f} %> <%= render :partial => block_form, :locals => {:f => f} %> diff --git a/app/views/cms/content_block/show.html.erb b/app/views/cms/content_block/show.html.erb index a86ce66ec..0cadc29ba 100644 --- a/app/views/cms/content_block/show.html.erb +++ b/app/views/cms/content_block/show.html.erb @@ -1,4 +1,4 @@ <%= content_for :button_bar do %> <%= render 'buttonbar' %> <% end %> - + diff --git a/app/views/cms/content_block/usages.html.erb b/app/views/cms/content_block/usages.html.erb index d8b27e182..53177a6e2 100644 --- a/app/views/cms/content_block/usages.html.erb +++ b/app/views/cms/content_block/usages.html.erb @@ -1,16 +1,5 @@ <% use_page_title "Content Library / View Usages" %> <% @toolbar_title = "Pages Using #{content_type.display_name} '#{ @block.name }'".html_safe %> -<%= content_for :html_head do %> - <%= javascript_tag do %> - jQuery(function($) { - $('#functions .button').addClass('disabled').attr('href','#'); - $('#view_button').removeClass('disabled') - .attr('href', '<%= cms_connectable_path(@block)%>') - $('#edit_button').removeClass('disabled') - .attr('href', '<%= edit_cms_connectable_path(@block)%>') - }) - <% end %> -<% end %> <%= content_for :button_bar, render(:partial => 'buttonbar') %>
diff --git a/app/views/cms/content_block/version.html.erb b/app/views/cms/content_block/version.html.erb index ce4f0027a..7647b92fe 100644 --- a/app/views/cms/content_block/version.html.erb +++ b/app/views/cms/content_block/version.html.erb @@ -13,7 +13,7 @@

Used on: <%= link_to_usages(@block) %> <%= "page".pluralize_unless_one(@block.connected_pages.count)%>

<% end -%> <% if @block.respond_to?(:draft_version?) && !@block.draft_version? %> -

Version: v. <%= @block.version %> ( <%= link_to "Current", block_path(@block) %> )

+

Version: v. <%= @block.version %> ( <%= link_to "Current", engine_aware_path(@block) %> )

<% end -%> <% if @block.class.renderable? %>

Block Contents:

diff --git a/app/views/cms/dynamic_views/_toolbar.html.erb b/app/views/cms/dynamic_views/_toolbar.html.erb index f6d19801d..9d40c035e 100644 --- a/app/views/cms/dynamic_views/_toolbar.html.erb +++ b/app/views/cms/dynamic_views/_toolbar.html.erb @@ -1,8 +1,8 @@ <%= content_for :button_bar do %> <% if @view %> - <%= link_to "Back to #{dynamic_view_type.title.pluralize}", cms_index_path_for(dynamic_view_type), class: 'btn btn-primary pull-left' %> + <%= link_to "Back to #{dynamic_view_type.title.pluralize}", engine_aware_path(dynamic_view_type), class: 'btn btn-primary pull-left' %> <% else %> - <%= add_button cms_new_path_for(dynamic_view_type), bootstrap: true %> + <%= add_button new_engine_aware_path(dynamic_view_type), bootstrap: true %> <%= edit_button(bootstrap: true) %> <%= delete_menu_button nil, :title => "Are you sure you want to delete this #{dynamic_view_type.title}?", class: ["pull-left"] %> <% end %> diff --git a/app/views/cms/dynamic_views/index.html.erb b/app/views/cms/dynamic_views/index.html.erb index 2ac3b7722..ef6da2406 100644 --- a/app/views/cms/dynamic_views/index.html.erb +++ b/app/views/cms/dynamic_views/index.html.erb @@ -30,7 +30,7 @@ <% @views.each do |view| %> - +
<%=h view.display_name %>
<%=h view.path %>
<%= view.updated_at && view.updated_at.to_s(:date) %>
diff --git a/app/views/cms/form_entries/_buttonbar.html.erb b/app/views/cms/form_entries/_buttonbar.html.erb new file mode 100644 index 000000000..7a08e56c3 --- /dev/null +++ b/app/views/cms/form_entries/_buttonbar.html.erb @@ -0,0 +1,8 @@ +<% view = :index if local_assigns[:view].nil? %> +<%= menu_button("Back to Entries", cms.entries_path(@entry.form)) unless view == :index %> +<%= link_to("Form", form_path(@entry.form), class: "btn btn-primary pull-left ") %> + + <%= view_content_menu_button(@entry) %> + <%= edit_content_menu_button(@entry) unless view == :edit %> + +<%= link_to("New Entry", new_form_entry_path(form_id: @entry.form), class: "btn btn-primary pull-right") %> diff --git a/app/views/cms/form_entries/_form.html.erb b/app/views/cms/form_entries/_form.html.erb new file mode 100644 index 000000000..797d46970 --- /dev/null +++ b/app/views/cms/form_entries/_form.html.erb @@ -0,0 +1,7 @@ +<%= simple_form_for(model, as: :form_entry, url: url) do |f| %> + <%= hidden_field_tag :form_id, model.form.id %> + <% model.form.fields.each do |field| %> + <%= f.input field.name, field.options(entry: model) %> + <% end %> + <%= f.button :submit, "Submit" %> +<% end %> \ No newline at end of file diff --git a/app/views/cms/form_entries/edit.html.erb b/app/views/cms/form_entries/edit.html.erb new file mode 100644 index 000000000..e0164497e --- /dev/null +++ b/app/views/cms/form_entries/edit.html.erb @@ -0,0 +1,6 @@ +<% use_page_title "Edit Entry" %> +<%= content_for :button_bar, render(partial: 'buttonbar', locals: {view: :edit}) %> +<%= render partial: 'cms/form_entries/form', + locals: {model: @entry, + url: cms.form_entry_path(@entry)} +%> diff --git a/app/views/cms/form_entries/error.html.erb b/app/views/cms/form_entries/error.html.erb new file mode 100644 index 000000000..3af4a2aa8 --- /dev/null +++ b/app/views/cms/form_entries/error.html.erb @@ -0,0 +1,3 @@ +<%= content_for :main do %> + <%= render file: 'cms/forms/render' %> +<% end %> \ No newline at end of file diff --git a/app/views/cms/form_entries/new.html.erb b/app/views/cms/form_entries/new.html.erb new file mode 100644 index 000000000..011f835d2 --- /dev/null +++ b/app/views/cms/form_entries/new.html.erb @@ -0,0 +1,6 @@ +<% use_page_title "New Entry for #{@entry.form.name}" %> +<%= content_for :button_bar, render(partial: 'buttonbar', locals: {view: :new}) %> +<%= render partial: 'cms/form_entries/form', + locals: {model: @entry, + url: form_entries_path(@entry)} +%> diff --git a/app/views/cms/form_entries/show.html.erb b/app/views/cms/form_entries/show.html.erb new file mode 100644 index 000000000..847447f55 --- /dev/null +++ b/app/views/cms/form_entries/show.html.erb @@ -0,0 +1,6 @@ +<% use_page_title "View Entry" %> +<%= content_for :button_bar, render(partial: 'buttonbar', locals: {view: :show})%> + +<% @entry.form.fields.each do |field| %> + <%= field.label %>: <%= @entry.data_columns[field.name] %>
+<% end %> \ No newline at end of file diff --git a/app/views/cms/form_entries/submit.html.erb b/app/views/cms/form_entries/submit.html.erb new file mode 100644 index 000000000..d00f3bbbd --- /dev/null +++ b/app/views/cms/form_entries/submit.html.erb @@ -0,0 +1 @@ +<%= content_for :main , @form.confirmation_text.html_safe %> \ No newline at end of file diff --git a/app/views/cms/form_fields/_form.html.erb b/app/views/cms/form_fields/_form.html.erb new file mode 100644 index 000000000..e6c760a3b --- /dev/null +++ b/app/views/cms/form_fields/_form.html.erb @@ -0,0 +1,8 @@ +<%= f.input field.name, + label: field.label, + hint: field.instructions, + wrapper: :append do %> + <%= f.input_field field.name, field.options(disabled: true).merge({data: {id: field.id}}) %> + <%= link_to("Edit", "#", data: {edit_path: edit_path}, class: "btn edit_form_button") %> + <%= link_to("Delete", "#", data: {path: delete_path}, class: "btn delete_field_button") %> +<% end %> diff --git a/app/views/cms/form_fields/_select.html.erb b/app/views/cms/form_fields/_select.html.erb new file mode 100644 index 000000000..a11012312 --- /dev/null +++ b/app/views/cms/form_fields/_select.html.erb @@ -0,0 +1,3 @@ +<%= f.input :choices, hint: "Enter each choice on a separate line." %> +<%= f.input :required, hint: "Required fields will show the first option a preselected." %> +<%= f.input :instructions, as: :text %> diff --git a/app/views/cms/form_fields/_text_area.html.erb b/app/views/cms/form_fields/_text_area.html.erb new file mode 100644 index 000000000..5a7444617 --- /dev/null +++ b/app/views/cms/form_fields/_text_area.html.erb @@ -0,0 +1,3 @@ +<%= f.input :required %> +<%= f.input :instructions, as: :text %> +<%= f.input :default_value %> \ No newline at end of file diff --git a/app/views/cms/form_fields/_text_field.html.erb b/app/views/cms/form_fields/_text_field.html.erb new file mode 100644 index 000000000..5a7444617 --- /dev/null +++ b/app/views/cms/form_fields/_text_field.html.erb @@ -0,0 +1,3 @@ +<%= f.input :required %> +<%= f.input :instructions, as: :text %> +<%= f.input :default_value %> \ No newline at end of file diff --git a/app/views/cms/form_fields/edit.html.erb b/app/views/cms/form_fields/edit.html.erb new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/cms/form_fields/new.html.erb b/app/views/cms/form_fields/new.html.erb new file mode 100644 index 000000000..792dab106 --- /dev/null +++ b/app/views/cms/form_fields/new.html.erb @@ -0,0 +1,7 @@ +<%= simple_form_for(@field, html: {id: 'ajax_form_field'}) do |f| %> + <%= f.hidden_field :form_id %> + <%= f.hidden_field :field_type %> + <%= f.input :label, hint: @field.new_record? ? "" : "Will not change the database name (i.e. '#{@field.name}') for new or existing entries. " %> + <%= render partial: @field.field_type, locals: {f: f, field: @field} %> + <%= f.button :submit, "Save", style: 'display: none' %> +<% end %> \ No newline at end of file diff --git a/app/views/cms/form_fields/preview.html.erb b/app/views/cms/form_fields/preview.html.erb new file mode 100644 index 000000000..2401f7fcd --- /dev/null +++ b/app/views/cms/form_fields/preview.html.erb @@ -0,0 +1,16 @@ +<%= simple_form_for(@form) do |f| %> + <%= f.simple_fields_for :new_entry do |e| %> + <% + locals = {f: e, + field: @field, + edit_path: new_form_field_path(form_id: @form.id, field_type: @field.field_type) + } + if @field.persisted? + locals[:delete_path] = form_field_path(@field.id) + else + locals[:delete_path] = "" + end + %> + <%= render partial: 'cms/form_fields/form', locals: locals %> + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/cms/forms/_buttonbar.html.erb b/app/views/cms/forms/_buttonbar.html.erb new file mode 100644 index 000000000..27701cdc0 --- /dev/null +++ b/app/views/cms/forms/_buttonbar.html.erb @@ -0,0 +1,8 @@ +<% # Override core content blocks menu to add additional buttons. %> +<% if cleanup_before_abandoning %> + <%= link_to("Forms", cms.forms_path, class: 'btn btn-primary') %> +<% else %> +<%= link_to("Entries", cms.entries_path(@block), class: "btn btn-primary pull-left") if @block %> +<%= render 'cms/content_block/buttonbar' %> +<% end %> + diff --git a/app/views/cms/forms/_form.html.erb b/app/views/cms/forms/_form.html.erb new file mode 100644 index 000000000..87279320b --- /dev/null +++ b/app/views/cms/forms/_form.html.erb @@ -0,0 +1,71 @@ +<% if cleanup_before_abandoning %> + +<% end %> + +

Form Settings

+<%= f.input :name, as: :cms_text_field %> +<%= f.input :description %> +<%= hidden_field_tag "field_ids", @block.field_ids %> +<%= f.input :confirmation_behavior, label: "On Confirmation", + collection: [["Show Text", :show_text], ['Redirect to Website', :redirect]], + as: :radio_buttons %> +<%= f.input :confirmation_text, label: false %> +<%= f.input :confirmation_redirect, label: false, hint: "A complete URL (i.e. http://www.yoursite.com/registration) or relative path (/thanks-for-registering)." %> +<%= f.input :notification_email, placeholder: 'email@example.com', label: "Notification Email", hint: "Notify this email address when an entry is created. (Leave blank for no notification)" %> + +

Preview

+
+ <% if @block.new_record? %> +
+ No fields yet! This live preview of your form will show you fields as you add them. Choose a field type + below. +
+ <% end %> + <%= f.simple_fields_for :new_entry do |e| %> + <% @block.fields.each do |field| %> + <%= render partial: 'cms/form_fields/form', + locals: {f: e, field: field, edit_path: edit_form_field_path(field.id), delete_path: form_field_path(field.id)} %> + <% end %> + <% end %> + +
+ +<%= f.simple_fields_for :new_entry do |e| %> + <%= e.input :new_field, + label: "Add New Field", + prompt: "Select a field type:", + collection: [["Text Field", :text_field], ["Text Box", :text_area], ["Dropdown", :select]], + input_html: {class: 'add-new-field'} + %> +<% end %> +<%# Can't have a form inside a form, so this must go into the head. %> +<%= content_for :html_head do %> + + + +<% end %> + diff --git a/app/views/cms/forms/render.html.erb b/app/views/cms/forms/render.html.erb new file mode 100644 index 000000000..e07f74b37 --- /dev/null +++ b/app/views/cms/forms/render.html.erb @@ -0,0 +1,15 @@ +<% content_for :html_head do %> + <%= stylesheet_link_tag Rails.application.config.cms.form_builder_css %> +<% end %> +

<%= @content_block.name %>

+

+ <%= @content_block.description %> +

+<% + unless @entry + @entry = Cms::FormEntry.for(@content_block) + end +%> +<%= render partial: 'cms/form_entries/form', locals: {model: @entry, url: cms.submit_form_entries_path, form: @content_block} %> + + diff --git a/app/views/cms/page_components/new.html.erb b/app/views/cms/page_components/new.html.erb index 7c38b4952..27ab99d8b 100644 --- a/app/views/cms/page_components/new.html.erb +++ b/app/views/cms/page_components/new.html.erb @@ -1,14 +1,14 @@ -

<%= link_to "Add #{@default_type.display_name}", cms_new_path_for(@default_type, - "#{@default_type.model_class_form_name}[connect_to_page_id]" => params[:connect_to_page_id], - "#{@default_type.model_class_form_name}[connect_to_container]" => params[:connect_to_container] +

<%= link_to "Add #{@default_type.display_name}", new_engine_aware_path((@default_type), + "#{@default_type.param_key}[connect_to_page_id]" => params[:connect_to_page_id], + "#{@default_type.param_key}[connect_to_container]" => params[:connect_to_container] ), :class=>"btn btn-primary" %>

Or choose another content types to add:

    <% @content_types.each_with_index do |type, i| %> -
  • <%= link_to h(type.display_name), cms_new_path_for(type, - "#{type.model_class_form_name}[connect_to_page_id]" => params[:connect_to_page_id], +
  • <%= link_to h(type.display_name), new_engine_aware_path(type, + "#{type.param_key}[connect_to_page_id]" => params[:connect_to_page_id], "#{type.model_class_form_name}[connect_to_container]" => params[:connect_to_container] ), :target=> "_top" %>
  • <% end %> diff --git a/app/views/cms/pages/_edit_content.html.erb b/app/views/cms/pages/_edit_content.html.erb index ff37a0a34..9b54f57c1 100644 --- a/app/views/cms/pages/_edit_content.html.erb +++ b/app/views/cms/pages/_edit_content.html.erb @@ -2,7 +2,7 @@ data: {move_up: cms.move_up_connector_path(connector, format: :json), move_down: cms.move_down_connector_path(connector, format: :json), remove: cms.connector_path(connector, format: :json), - edit_path: edit_cms_connectable_path(connectable, :_redirect_to => @page.path) + edit_path: edit_engine_aware_path(connectable, :_redirect_to => @page.path) } do %> <% if (!content_supports_inline_editing?(connector)) %> <%= link_to(image_tag('cms/arrow-up.png', class: 'cms-move-up-content-link'), @@ -22,7 +22,7 @@ %> <% end %> <%= link_to(image_tag('cms/pencil.png', class: 'cms-edit-content-link'), - edit_cms_connectable_path(connectable, :_redirect_to => @page.path), + edit_engine_aware_path(connectable, :_redirect_to => @page.path), title: "Edit this content.") %> <%= render_connectable connectable %> <% end %> \ No newline at end of file diff --git a/app/views/cms/section_nodes/_addressable_content_block.html.erb b/app/views/cms/section_nodes/_addressable_content_block.html.erb index 1a88580da..653d7b81a 100644 --- a/app/views/cms/section_nodes/_addressable_content_block.html.erb +++ b/app/views/cms/section_nodes/_addressable_content_block.html.erb @@ -1,7 +1,7 @@ <% - data = {edit_path: url_for(block_path(object)), - configure_path: url_for(block_path(object, :edit)), - delete_path: url_for(block_path(object)), + data = {edit_path: url_for(engine_aware_path(object)), + configure_path: url_for(edit_engine_aware_path(object)), + delete_path: url_for(engine_aware_path(object)), type: object.class.name, node_id: key.id, } diff --git a/app/views/layouts/cms/_thin_toolbar.html.erb b/app/views/layouts/cms/_thin_toolbar.html.erb index e32ac1461..bafcef2f6 100644 --- a/app/views/layouts/cms/_thin_toolbar.html.erb +++ b/app/views/layouts/cms/_thin_toolbar.html.erb @@ -38,7 +38,7 @@ modules.keys.sort.each_with_index do |module_name, i| %> <%= divider_tag(i) %> <% modules[module_name].each do |type| %> - <%= nav_link_to h(type.display_name), cms_index_path_for(type) %> + <%= nav_link_to h(type.display_name), engine_aware_path(type) %> <% end -%> <% end %>
@@ -94,7 +94,7 @@ modules.keys.sort.each_with_index do |module_name, i| %> <%= divider_tag(i) %> <% modules[module_name].each do |type| %> - <%= nav_link_to h(type.display_name), cms_new_path_for(type), id: "create_new_#{type.key}" %> + <%= nav_link_to h(type.display_name), new_engine_aware_path(type), id: "create_new_#{type.param_key}" %> <% end -%> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 14e3ff4ce..29a1cecad 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -7,3 +7,5 @@ en: attachments: one: The attached file other: An attached file + cms/form_field: + name: Labels diff --git a/config/routes.rb b/config/routes.rb index fabdea519..e1cd1e4ea 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,6 @@ # There are other routes that will be added at the root of the site (i.e. /) which can # be found in lib/cms/route_extensions.rb Cms::Engine.routes.draw do - get 'fakemap', to: 'section_nodes#fake' get '/content/:id/edit', :to=>"content#edit", :as=>'edit_content' get '/dashboard', :to=>"dashboard#index", :as=>'dashboard' @@ -64,6 +63,24 @@ resources :attachments, :only=>[:show, :create, :destroy] content_blocks :html_blocks + content_blocks :forms + resources :form_fields do + member do + get :confirm_delete + end + end + post "form_fields/:id/insert_at/:position" => 'form_fields#insert_at' + get "/forms/:id/fields/preview" => 'form_fields#preview', as: 'preview_form_field' + + resources :form_entries do + collection do + post :submit + end + end + + # Faux nested resource for forms (not sure if #content_blocks allows for it.) + get 'forms/:id/entries' => 'form_entries#index', as: 'entries' + content_blocks :portlets post '/portlet/:id/:handler', :to=>"portlet#execute_handler", :as=>"portlet_handler" @@ -94,5 +111,6 @@ get "/routes", :to => "routes#index", :as=>'routes' + add_routes_for_addressable_content_blocks end diff --git a/db/migrate/20111130221145_browsercms340.rb b/db/migrate/20111130221145_browsercms340.rb index 32cc71ce2..b41a62ed9 100644 --- a/db/migrate/20111130221145_browsercms340.rb +++ b/db/migrate/20111130221145_browsercms340.rb @@ -6,8 +6,8 @@ class Browsercms340 < ActiveRecord::Migration def change # Namespace class_names where they are not namespaced. %w[HtmlBlock Category CategoryType Portlet FileBlock ImageBlock Tag].each do |content_type| - update_content_types(content_type) - update_connectors_table(content_type) + update_content_types(namespace_model(content_type)) + update_connectors_table(namespace_model(content_type)) end update_sitemap diff --git a/db/migrate/20130925170117_create_forms.rb b/db/migrate/20130925170117_create_forms.rb new file mode 100644 index 000000000..3f1593f37 --- /dev/null +++ b/db/migrate/20130925170117_create_forms.rb @@ -0,0 +1,12 @@ +class CreateForms < ActiveRecord::Migration + def change + create_content_table :forms do |t| + t.string :name + t.text :description + t.string :confirmation_behavior + t.text :confirmation_text + t.string :confirmation_redirect + t.string :notification_email + end + end +end diff --git a/db/migrate/20130925174919_create_form_fields.rb b/db/migrate/20130925174919_create_form_fields.rb new file mode 100644 index 000000000..352ee0541 --- /dev/null +++ b/db/migrate/20130925174919_create_form_fields.rb @@ -0,0 +1,18 @@ +class CreateFormFields < ActiveRecord::Migration + def change + create_table :cms_form_fields do |t| + t.integer :form_id + t.string :label + t.string :name + t.string :field_type + t.boolean :required + t.integer :position + t.text :instructions + t.text :default_value + t.timestamps + end + + # Field names should be unique per form + add_index :cms_form_fields, [:form_id, :name], :unique => true + end +end diff --git a/db/migrate/20131004200959_create_form_entries.rb b/db/migrate/20131004200959_create_form_entries.rb new file mode 100644 index 000000000..a16aa6a40 --- /dev/null +++ b/db/migrate/20131004200959_create_form_entries.rb @@ -0,0 +1,9 @@ +class CreateFormEntries < ActiveRecord::Migration + def change + create_table :cms_form_entries do |t| + t.text :data_columns + t.integer :form_id + t.timestamps + end + end +end diff --git a/doc/features/form_builder.md b/doc/features/form_builder.md new file mode 100644 index 000000000..7098ef0b8 --- /dev/null +++ b/doc/features/form_builder.md @@ -0,0 +1,42 @@ +# Form Builder + +Allow content editors to create new form via the interface. + +## Remain issues/features + +* [MINOR] Unique icon for forms (i.e. custom) for sitemap +* [Improvement] Captcha? +* [FEATURE] Allow sorting/reporting. +* [BUG] Preview for dropdowns is inaccurate (doesn't change after updates). To fix, would need to refactor to have HTML returned rather than JSON. +* [BUG] 500 errors when posting forms within CMS shows errors within double nested toolbar. +* [Improvement] Addressable content types need a way to select templates (or change via configuration) +* [BUG] Slugs for Addressable Content types are not unique +* [Improvement] Default Forms just uses bootstrap, which is probably not a valid option. Need a way to pare it down to avoid changing styles for existing designes. +* [BUG] After adding a field, it clears but does not correctly show the 'blank' value. +* [BUG] Addressable content types (product, forms, etc) do not/cannot appear in menus without manually creating a link. + +## Documentation + +## Forms + +* Can notify staff when a form is submitted. +* Email mailbot can be configured in application.rb + +### Configuration + +In config/application.rb, to change which CSS stylesheet is applied to Form pages, update the following: + +``` + # Default is 'cms/default-forms' + config.cms.form_builder_css = 'my-forms' # Returns /assets/my-forms.css +``` + +# New Namespacing + +When upgrading, Content Models should be moved under application namespace. Basis steps: + + Custom models should have the following added to them: + Create a directory in the project named app/models/cms + Move any Content Blocks into that directory. + Rename from Widget to *Cms::*Widget + Add self.table_name = :widgets (or migrate the table from widgets to cms_widgets) diff --git a/doc/release_notes.md b/doc/release_notes.md index 8ec6ac1ec..e00eeec93 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -5,6 +5,7 @@ This release includes the following features: * User Interface Redesign - Complete UI rework to use Bootstrap. * True In Context Editing - Editors can directly edit Html content and page titles using CKEditor's inline capability. +* Form Builder - Editors can create forms to collect information from site visitors. Forms are addressable content pages. * Addressable Content Blocks - Custom content blocks (i.e. Product, Event, News Articles) can be created directly as pages in the sitemap. * Refined Content API - Make content blocks closer in behavior to ActiveRecord. * Improved Template Storage - Templates stored in the database no longer need to be written out to the file system. This should make it easier to deploy CMS apps to Heroku. diff --git a/features/content_blocks/form_controls.feature b/features/content_blocks/form_controls.feature index 69884588d..951f289eb 100644 --- a/features/content_blocks/form_controls.feature +++ b/features/content_blocks/form_controls.feature @@ -22,7 +22,7 @@ Feature: Form Controls Then I should see two file uploads Scenario: Edit content with multiple attachments - Given a block exists which configured to have two attachments + Given a block exists with two uploaded attachments When I edit that block Then I should see two file uploads diff --git a/features/content_blocks/forms.feature b/features/content_blocks/forms.feature new file mode 100644 index 000000000..f4e32ff0d --- /dev/null +++ b/features/content_blocks/forms.feature @@ -0,0 +1,27 @@ +Feature: Forms + + Editors should be able to create and manage forms to collect information from site visitors. They should be able + to build these forms dyamically through the UI. + + Background: + Given I am logged in as a Content Editor + + Scenario: List Forms + Given I had created a form named "Contact Us" + When I select forms from the content library + Then I should see the list of forms + And I should see the "Contact Us" form in the list + + Scenario: Add Form + When I am adding new form + And I enter the required form fields + Then after saving I should be redirect to the form page + + Scenario: Edit Form + Given I had created a form named "Contact Us" + When I edit that form + And I make changes to the form + Then I should see the form with the updated fields + + + diff --git a/features/content_blocks/manage_custom_blocks.feature b/features/content_blocks/manage_custom_blocks.feature index 5da8d16b5..d30f505cc 100644 --- a/features/content_blocks/manage_custom_blocks.feature +++ b/features/content_blocks/manage_custom_blocks.feature @@ -4,11 +4,10 @@ Feature: Manage Content Blocks This blocks will be generated as Rails resources, with a controller and views. Background: - Given a Content Type named "Product" is registered - And I am logged in as a Content Editor + Given I am logged in as a Content Editor Scenario: List Content Blocks - When I request /cms/products + When I view products in the content library Then I should see "List Products" Scenario: List Content Blocks @@ -24,10 +23,10 @@ Feature: Manage Content Blocks | Tag | Scenario: Create a new block - When I request /cms/products/new + When I add a new product Then I should see "Add New Product" When I fill in "Name" with "iPhone" - And I fill in "product_slug" with "/iphone" + And I fill in "Slug" with "/iphone" And I click on "Save" Then a new product should be created @@ -37,11 +36,11 @@ Feature: Manage Content Blocks | 1 | iPhone | 400 | | 2 | Kindle Fire | 200 | When I delete "Kindle Fire" - Then I should be redirected to /cms/products + Then I should be returned to the view products page in the content library Scenario: Multiple Pages Given there are multiple pages of products in the Content Library - When I request /cms/products + When I view products in the content library Then I should see the paging controls And I click on "next_page_link" Then I should see the second page of content diff --git a/features/generators/attachments.feature b/features/generators/attachments.feature index cd1778140..d4f6ffe7b 100644 --- a/features/generators/attachments.feature +++ b/features/generators/attachments.feature @@ -8,84 +8,64 @@ Feature: Generate Attachments Scenario: Single Named Attachment When I run `rails g cms:content_block Product photo:attachment` - Then the file "app/models/product.rb" should contain: + Then the file "app/models/petstore/product.rb" should contain: """ - class Product < ActiveRecord::Base - acts_as_content_block - content_module :products - has_attachment :photo - end + has_attachment :photo """ - And a migration named "create_products.rb" should not contain: + And a migration named "create_petstore_products.rb" should not contain: """ t.attachment :photo """ - And the file "app/views/cms/products/render.html.erb" should contain: + And the file "app/views/petstore/products/render.html.erb" should contain: """

Photo: <%= link_to "Photo", attachment_path_for(@content_block.photo) %>

""" - And the file "app/views/cms/products/_form.html.erb" should contain: + And the file "app/views/petstore/products/_form.html.erb" should contain: """ <%= f.input :photo, as: :file_picker %> """ Scenario: Two Named Attachment When I run `rails g cms:content_block Product photo:attachment cover:attachment` - Then the file "app/models/product.rb" should contain: - """ - class Product < ActiveRecord::Base - acts_as_content_block - content_module :products - has_attachment :photo - has_attachment :cover - end - """ + Then the file "app/models/petstore/product.rb" should contain the following content: + | has_attachment :photo | + | has_attachment :cover | Scenario: Multiple Attachments When I run `rails g cms:content_block Product photos:attachments` - Then the file "app/models/product.rb" should contain: + Then the file "app/models/petstore/product.rb" should contain: """ - class Product < ActiveRecord::Base - acts_as_content_block - content_module :products - has_many_attachments :photos - end + has_many_attachments :photos """ - And a migration named "create_products.rb" should not contain: + And a migration named "create_petstore_products.rb" should not contain: """ t.attachments :photos """ - And the file "app/views/cms/products/render.html.erb" should contain: + And the file "app/views/petstore/products/render.html.erb" should contain: """

Attachments: <%= attachment_viewer @content_block %>

""" - And the file "app/views/cms/products/_form.html.erb" should contain: + And the file "app/views/petstore/products/_form.html.erb" should contain: """ <%= f.cms_attachment_manager %> """ Scenario: Multiple Attachments with different names When I run `rails g cms:content_block Product photos:attachments documents:attachments` - Then the file "app/models/product.rb" should contain: - """ - class Product < ActiveRecord::Base - acts_as_content_block - content_module :products - has_many_attachments :photos - has_many_attachments :documents - end - """ - And a migration named "create_products.rb" should not contain: + Then the file "app/models/petstore/product.rb" should contain the following content: + | has_many_attachments :photos | + | has_many_attachments :documents | + And a migration named "create_petstore_products.rb" should not contain: """ t.attachments :photos t.attachments :documents """ - And the file "app/views/cms/products/render.html.erb" should not contain: + And the file "app/views/petstore/products/render.html.erb" should not contain: """

Attachments: <%= attachment_viewer @content_block %>

Attachments: <%= attachment_viewer @content_block %>

""" - And the file "app/views/cms/products/_form.html.erb" should not contain: + And the file "app/views/petstore/products/_form.html.erb" should not contain: """ <%= f.cms_attachment_manager %> <%= f.cms_attachment_manager %> diff --git a/features/generators/content_blocks_for_projects.feature b/features/generators/content_blocks_for_projects.feature index 22039202a..87630fa9f 100644 --- a/features/generators/content_blocks_for_projects.feature +++ b/features/generators/content_blocks_for_projects.feature @@ -8,29 +8,33 @@ Feature: Scenario: Create an content block for a project When I run `rails g cms:content_block product name:string price:string` - Then the file "app/models/product.rb" should contain: + Then the file "app/models/petstore/product.rb" should contain: """ - class Product < ActiveRecord::Base - acts_as_content_block - content_module :products + module Petstore + class Product < ActiveRecord::Base + acts_as_content_block + content_module :products + end end """ - And a file named "test/models/product_test.rb" should exist - And the file "app/controllers/cms/products_controller.rb" should contain: + And a file named "test/models/petstore/product_test.rb" should exist + And the file "app/controllers/petstore/products_controller.rb" should contain: """ - class Cms::ProductsController < Cms::ContentBlockController + module Petstore + class ProductsController < Cms::ContentBlockController + end end """ - And the file "app/views/cms/products/render.html.erb" should contain: + And the file "app/views/petstore/products/render.html.erb" should contain: """

Name: <%= @content_block.name %>

Price: <%= @content_block.price %>

""" - And a migration named "create_products.rb" should contain: + And a migration named "create_petstore_products.rb" should contain: """ - class CreateProducts < ActiveRecord::Migration + class CreatePetstoreProducts < ActiveRecord::Migration def change - create_content_table :products, :prefix=>false do |t| + create_content_table :petstore_products, :prefix=>false do |t| t.string :name t.string :price @@ -45,7 +49,7 @@ Feature: """ And the file "config/routes.rb" should contain: """ - namespace :cms do content_blocks :products end + namespace :petstore do content_blocks :products end """ And the file "config/routes.rb" should contain: """ @@ -54,18 +58,18 @@ Feature: Scenario: Generated Dates When I run `rails g cms:content_block product fresh_until:date` - Then the file "app/views/cms/products/_form.html.erb" should contain: + Then the file "app/views/petstore/products/_form.html.erb" should contain: """ <%= f.input :fresh_until, as: :date_picker %> """ Scenario: Html Content When I run `rails g cms:content_block product content:html` - Then the file "app/views/cms/products/_form.html.erb" should contain: + Then the file "app/views/petstore/products/_form.html.erb" should contain: """ <%= f.input :name, as: :cms_text_field %> """ - And the file "app/views/cms/products/_form.html.erb" should contain: + And the file "app/views/petstore/products/_form.html.erb" should contain: """ <%= f.input :content, as: :text_editor %> """ @@ -73,61 +77,53 @@ Feature: # Date times should just be standard Rails widget Scenario: Date Time When I run `rails g cms:content_block sale runs_til:datetime` - Then the file "app/views/cms/sales/_form.html.erb" should contain: + Then the file "app/views/petstore/sales/_form.html.erb" should contain: """ <%= f.input :runs_til %> """ Scenario: With Belongs To When I run `rails g cms:content_block product size:belongs_to` - Then the file "app/models/product.rb" should contain: + Then the file "app/models/petstore/product.rb" should contain: """ - class Product < ActiveRecord::Base - acts_as_content_block - content_module :products - belongs_to :size - end + belongs_to :size """ - And a migration named "create_products.rb" should contain: + And a migration named "create_petstore_products.rb" should contain: """ t.belongs_to :size """ - Then the file "app/views/cms/products/_form.html.erb" should contain: + Then the file "app/views/petstore/products/_form.html.erb" should contain: """ <%= f.input :size %> """ Scenario: With Categories When I run `rails g cms:content_block product category:category` - Then the file "app/models/product.rb" should contain: + Then the file "app/models/petstore/product.rb" should contain: """ - class Product < ActiveRecord::Base - acts_as_content_block - content_module :products - belongs_to_category - end + belongs_to_category """ - And a migration named "create_products.rb" should contain: + And a migration named "create_petstore_products.rb" should contain: """ t.belongs_to :category """ - And the file "app/views/cms/products/_form.html.erb" should contain: + And the file "app/views/petstore/products/_form.html.erb" should contain: """ <%= f.association :category, collection: categories_for('Product') %> """ Scenario: With Html attributes When I run `rails g cms:content_block product content:html` - Then a migration named "create_products.rb" should contain the following: + Then a migration named "create_petstore_products.rb" should contain the following: | t.text :content, :size => (64.kilobytes + 1) | Scenario: Block names starting with 'do' should work When I run `rails g cms:content_block dog` - And a migration named "create_dogs.rb" should contain: + And a migration named "create_petstore_dogs.rb" should contain: """ - class CreateDogs < ActiveRecord::Migration + class CreatePetstoreDogs < ActiveRecord::Migration def change - create_content_table :dogs, :prefix=>false do |t| + create_content_table :petstore_dogs, :prefix=>false do |t| t.timestamps end diff --git a/features/manage_page_routes.feature b/features/manage_page_routes.feature index 3d629de4f..9a79cf5f1 100644 --- a/features/manage_page_routes.feature +++ b/features/manage_page_routes.feature @@ -12,9 +12,9 @@ Feature: Manage Page Routes Scenario: Match a path to a Controller When I request /cms/routes - And I search for a path including "/cms/sample_blocks/1" + And I search for a path including "/dummy/sample_blocks/1" Then I should see the following content: - | {:action=>"show", :controller=>"cms/sample_blocks", :id=>"1"} | + | {:action=>"show", :controller=>"dummy/sample_blocks", :id=>"1"} | Scenario: Create Page Route When I request /cms/page_routes diff --git a/features/step_definitions/command_line_steps.rb b/features/step_definitions/command_line_steps.rb index 994c7bdbb..cb276eb69 100644 --- a/features/step_definitions/command_line_steps.rb +++ b/features/step_definitions/command_line_steps.rb @@ -110,6 +110,12 @@ # This output is ugly, but it verifies that seed data completely runs end +Then /^the file "([^"]*)" should contain the following content:$/ do |file, table| + table.rows.each do |row| + check_file_content(file, row[0], true) + end +end + # Opposite of aruba step 'the file "x" should contain:' When /^the file "([^"]*)" should not contain:$/ do |file, partial_content| check_file_content(file, partial_content, false) diff --git a/features/step_definitions/content_pages_steps.rb b/features/step_definitions/content_pages_steps.rb index 4ae861f8a..6ac7855fa 100644 --- a/features/step_definitions/content_pages_steps.rb +++ b/features/step_definitions/content_pages_steps.rb @@ -280,4 +280,50 @@ Given /^I am adding a new tag$/ do visit cms.new_tag_path +end + +Given /^I had created a form named "([^"]*)"$/ do |arg| + create(:form, name: arg) +end + +When /^I select forms from the content library$/ do + visit cms.forms_path +end + +Then(/^I should see the list of forms$/) do + should_be_successful + should_see_a_page_titled('List Forms') +end + +Then(/^I should see the "([^"]*)" form in the list$/) do |form_name| + page_should_have_content(form_name) +end + +When /^I am adding new form$/ do + visit cms.new_form_path +end + +When(/^I enter the required form fields$/) do + fill_in "Name", with: "Contact Us" + click_on 'Save And Publish' +end + +Then(/^after saving I should be redirect to the form page$/) do + should_be_successful + should_see_a_page_titled "Contact Us" +end + +When(/^I edit that form$/) do + @form = Cms::Form.where(name: "Contact Us").first + visit cms.edit_form_path(@form) +end + +When(/^I make changes to the form$/) do + fill_in "Name", with: "Updated Name" + click_on "Save And Publish" +end + +Then(/^I should see the form with with the updated fields$/) do + should_be_successful + should_see_a_page_titled "Updated Name" end \ No newline at end of file diff --git a/features/step_definitions/deprecated.rb b/features/step_definitions/deprecated.rb index 7361194b6..561574b1c 100644 --- a/features/step_definitions/deprecated.rb +++ b/features/step_definitions/deprecated.rb @@ -5,7 +5,7 @@ Given /^I'm creating content which uses deprecated input fields$/ do # Avoids printing all the deprecation warnings visiting the following page will result in. ActiveSupport::Deprecation.silence do - visit new_cms_deprecated_input_path + visit new_dummy_deprecated_input_path end end @@ -23,7 +23,7 @@ Then /^a new deprecated content block should be created$/ do should_be_successful - last_block = DeprecatedInput.last + last_block = Dummy::DeprecatedInput.last assert_not_nil last_block, "Content should have been created." assert_equal @expect_name, last_block.name assert_equal @expect_content, last_block.content diff --git a/features/step_definitions/form_control_steps.rb b/features/step_definitions/form_control_steps.rb index aa87109e4..61a392d08 100644 --- a/features/step_definitions/form_control_steps.rb +++ b/features/step_definitions/form_control_steps.rb @@ -13,8 +13,7 @@ end Given /^I am creating a new block which has two attachments$/ do - register_content_type("Product") - visit '/cms/products/new' + visit new_dummy_product_path end Then /^I should see two file uploads$/ do @@ -24,7 +23,7 @@ module MultipleAttachments def create_new_product(upload_both_files=true) - visit '/cms/products/new' + visit new_dummy_product_path fill_in "Name", with: "Have two attachments" attach_file "Photo 1", "test/fixtures/giraffe.jpeg" attach_file "Photo 2", "test/fixtures/hurricane.jpeg" if upload_both_files @@ -33,35 +32,29 @@ def create_new_product(upload_both_files=true) end World(MultipleAttachments) -Given /^a block exists which configured to have two attachments$/ do - register_content_type("Product") - create_new_product - @block = Product.last -end - When /^I upload an image named "([^"]*)"$/ do |path| attach_file("Image", File.expand_path(path)) end When /^I edit that block$/ do - visit "/cms/#{@block.class.path_name}/#{@block.id}/edit" + # Not engine aware, but these models should be in main_app anyway. + visit edit_polymorphic_path(@block) end Given /^a block exists with two uploaded attachments$/ do - register_content_type("Product") create_new_product - @block = Product.last + @block = Dummy::Product.last end When /^I replace both attachments$/ do - visit edit_cms_product_path(@block) + visit edit_dummy_product_path(@block) attach_file "Photo 1", "test/fixtures/multipart/version1.txt" attach_file "Photo 2", "test/fixtures/multipart/version2.txt" click_button "Save" end Then /^I should see the new attachments when I view the block$/ do - visit inline_cms_product_path(@block) + visit dummy_product_inline_path(@block) get_image("img[data-type=photo-1]") assert page.has_content?("v1"), "Check the contents of the image to make sure its the correct one." @@ -71,11 +64,11 @@ def create_new_product(upload_both_files=true) When /^I upload a single attachment$/ do create_new_product(false) - @block = Product.last + @block = Dummy::Product.last end When /^I am created a new block$/ do - visit "/cms/#{@content_type.model_class.path_name}/new" + visit new_polymorphic_path(@content_type.model_class) end Then /^I should see the attachment manager widget displayed$/ do @@ -89,8 +82,8 @@ def create_new_product(upload_both_files=true) end Given /^a block exists with a single image$/ do - @block = Catalog.create!(:name => "Hello") - @block.attachments << create(:attachment_document, :attachment_name => "photos", :attachable_type => "Catalog", :attachable_version => @block.version) + @block = Dummy::Catalog.create!(:name => "Hello") + @block.attachments << create(:attachment_document, :attachment_name => "photos", :attachable_type => "Dummy::Catalog", :attachable_version => @block.version) @block.save! a = @block.attachments.first @@ -101,16 +94,12 @@ def create_new_product(upload_both_files=true) When /^I view that block$/ do if @block.class.addressable? # Can't load iframes with current capybara drivers, so must test inline content for addressable blocks. - visit "/cms/#{@block.class.path_name}/#{@block.id}/inline" + visit "/dummy/#{@block.class.path_name}/#{@block.id}/inline" else visit cms.polymorphic_path(@block) end end -#When /^I view that block inline$/ do -# visit "/cms/#{@block.class.path_name}/#{@block.id}/inline" -#end - Then /^I should see that block's image$/ do assert page.has_css?("img[data-purpose=attachment]") end @@ -122,13 +111,13 @@ def create_new_product(upload_both_files=true) end When /^there is block which allows many attachments$/ do - @content_type = Catalog.content_type + @content_type = Dummy::Catalog.content_type end Given /^an attachment exists in a protected section$/ do @protected_section = create(:protected_section) - @block = Catalog.create!(:name => "In Protected Section", :publish_on_save => true) - @block.attachments << create(:attachment_document, :attachment_name => "photos", :attachable_type => "Catalog", :parent => @protected_section) + @block = Dummy::Catalog.create!(:name => "In Protected Section", :publish_on_save => true) + @block.attachments << create(:attachment_document, :attachment_name => "photos", :attachable_type => "Dummy::Catalog", :parent => @protected_section) @block.save! end @@ -137,7 +126,7 @@ def create_new_product(upload_both_files=true) end Given /^an attachment exists in a public section$/ do - @block = Catalog.create!(:name => "In Public Section", :publish_on_save => true) + @block = Dummy::Catalog.create!(:name => "In Public Section", :publish_on_save => true) @block.attachments << create(:catalog_attachment) @block.save! end diff --git a/features/step_definitions/manage_content_blocks_steps.rb b/features/step_definitions/manage_content_blocks_steps.rb index dc54c29dc..c55dcf80e 100644 --- a/features/step_definitions/manage_content_blocks_steps.rb +++ b/features/step_definitions/manage_content_blocks_steps.rb @@ -7,20 +7,21 @@ def register_content_type(type) World(CustomBlockHelpers) -When /^a Content Type named "Product" is registered$/ do - register_content_type("Product") -end - Given /^the following products exist:$/ do |table| # table is a | 1 | iPhone | 400 | table.hashes.each do |row| - Product.create!(:id => row['id'], :name => row['name'], :price => row['price']) + Dummy::Product.create!(:id => row['id'], :name => row['name'], :price => row['price']) end end When /^I delete "([^"]*)"$/ do |product_name| - p = Product.find_by_name(product_name) - page.driver.delete "/cms/products/#{p.id}" + p = Dummy::Product.find_by_name(product_name) + page.driver.delete dummy_product_path(p) +end + +Then /^I should be returned to the view products page in the content library$/ do + assert_equal dummy_products_url, page.response_headers["Location"] end + Then /^I should be redirected to ([^"]*)$/ do |path| assert_equal "http://www.example.com#{path}", page.response_headers["Location"] end @@ -53,10 +54,11 @@ def register_content_type(type) end end + Given /^there are multiple pages of products in the Content Library$/ do per_page = WillPaginate.per_page (per_page * 2).times do |i| - Product.create(:name => "Product #{i}") + Dummy::Product.create(:name => "Product #{i}") end end @@ -94,4 +96,7 @@ def register_content_type(type) within("#page-status-label") do assert page.has_content?('DRAFT') end -end \ No newline at end of file +end +When /^I add a new product$/ do + visit new_dummy_product_path +end diff --git a/features/step_definitions/more_custom_block_steps.rb b/features/step_definitions/more_custom_block_steps.rb index d2d86846b..c93f016b4 100644 --- a/features/step_definitions/more_custom_block_steps.rb +++ b/features/step_definitions/more_custom_block_steps.rb @@ -50,9 +50,12 @@ assert page.has_content?("A Widget") end Then /^a new product should be created$/ do - assert_equal 1, Product.count + assert_equal 1, Dummy::Product.count end Given /^no product with a slug "([^"]*)" exists$/ do |slug| - assert_nil Product.with_slug(slug) + assert_nil Dummy::Product.with_slug(slug) +end +When /^I view products in the content library$/ do + visit dummy_products_path end \ No newline at end of file diff --git a/features/support/open_on_first_failure.rb b/features/support/open_on_first_failure.rb index 342f5dcd6..4849ffe19 100644 --- a/features/support/open_on_first_failure.rb +++ b/features/support/open_on_first_failure.rb @@ -18,7 +18,7 @@ def failed_tests? end After('~@cli')do |scenario| - if scenario.failed? && !LaunchOnFirstFailure.failed_tests? + if scenario.failed? && !LaunchOnFirstFailure.failed_tests? && ENV['launch_on_failure'] != 'false' LaunchOnFirstFailure.failure_occurred save_and_open_page end diff --git a/features/taxonomy/add_content_with_category.feature b/features/taxonomy/add_content_with_category.feature index 475000e7c..dea003ac3 100644 --- a/features/taxonomy/add_content_with_category.feature +++ b/features/taxonomy/add_content_with_category.feature @@ -1,18 +1,17 @@ Feature: Add Content with categories Background: - Given a Content Type named "Product" is registered - And I am logged in as a Content Editor + Given I am logged in as a Content Editor Scenario: With no Category Type defined yet - When I visit /cms/products/new + When I add a new product Then I should see "You must first create a 'Category Type' named 'Product'" Scenario: With No Categories Given the following Category Types exist: | name | | Product | - When I request /cms/products/new + When I add a new product Then I should see "You must first create a Category with a Category Type of 'Product'." Scenario: With Categories @@ -23,7 +22,7 @@ Feature: Add Content with categories | name | | T-shirts | | Hoodies | - When I visit /cms/products/new + When I add a new product Then I should see the following content: | T-shirts | | Hoodies | diff --git a/lib/browsercms.rb b/lib/browsercms.rb index 414621d40..6e2c1b2ef 100644 --- a/lib/browsercms.rb +++ b/lib/browsercms.rb @@ -19,6 +19,7 @@ require 'cms/default_accessible' require 'cms/admin_tab' require 'cms/content_filter' +require 'cms/polymorphic_single_table_inheritance' require 'cms/form_builder/default_input' require 'cms/form_builder/content_block_form_builder' diff --git a/lib/cms/behaviors/rendering.rb b/lib/cms/behaviors/rendering.rb index 28d43597a..372456067 100644 --- a/lib/cms/behaviors/rendering.rb +++ b/lib/cms/behaviors/rendering.rb @@ -37,7 +37,6 @@ def is_renderable(options={}) @instance_variable_name_for_view = options[:instance_variable_name_for_view] extend ClassMethods - extend EngineHelper include InstanceMethods # I'm not pleased with the need to include all of the these rails helpers onto every 'renderable' content item @@ -82,11 +81,7 @@ def helper_class # of the renderable, so if you have an Article that is renderable, # the template will be "articles/render" def template_path - path = "#{name.underscore.pluralize}/render" - if main_app_model? - path = "cms/#{path}" - end - path + "#{name.underscore.pluralize}/render" end # Instance variables that will not be copied from the renderable to the view diff --git a/lib/cms/behaviors/versioning.rb b/lib/cms/behaviors/versioning.rb index c034b6653..e305a4044 100644 --- a/lib/cms/behaviors/versioning.rb +++ b/lib/cms/behaviors/versioning.rb @@ -86,7 +86,6 @@ def is_versioned(options={}) before_validation :initialize_version before_save :build_new_version attr_accessor :skip_callbacks - #attr_accessible :version_comment #Define the version class #puts "is_version called for #{self}" diff --git a/lib/cms/concerns/can_be_addressable.rb b/lib/cms/concerns/can_be_addressable.rb index c02af2f74..dc5fa7f79 100644 --- a/lib/cms/concerns/can_be_addressable.rb +++ b/lib/cms/concerns/can_be_addressable.rb @@ -8,7 +8,7 @@ module CanBeAddressable # @params [Hash] options # @option options [String] :path The base path where instances will be placed. # @option options [String] :no_dynamic_path Set as true if the Record has a :path attribute managed as a column in the db. (Default: false) - # @option options [Symbol] :destroy_if Name of a custom method used to determine when this object should be destrotyed. Rather than dependant: destroy to determine if the section node should be destroyed when this object is. + # @option options [Symbol] :destroy_if Name of a custom method used to determine when this object should be destroyed. Rather than dependant: destroy to determine if the section node should be destroyed when this object is. def is_addressable(options={}) has_one_options = {as: :node, inverse_of: :node, class_name: 'Cms::SectionNode'} unless options[:destroy_if] @@ -20,9 +20,17 @@ def is_addressable(options={}) has_one :section_node, has_one_options + # For reasons that aren't clear, just using :autosave doesn't work. + after_save do + if section_node && section_node.changed? + section_node.save + end + end + include Cms::Concerns::Addressable extend Cms::Concerns::Addressable::ClassMethods include Cms::Concerns::Addressable::NodeAccessors + include Cms::Concerns::Addressable::MarkAsDirty if options[:path] @path = options[:path] @@ -47,9 +55,13 @@ def requires_slug? end end - # Implements behavior for displaying content blocks that should appear in the - # sitemap (as opposed to pages/sections/links) + # Implements behavior for displaying content blocks that should appear in the sitemap. + # module GenericSitemapBehavior + def self.included(klass) + klass.before_validation :assign_parent_if_needed + end + def partial_for "addressable_content_block" end @@ -58,6 +70,25 @@ def hidden? false end + def assign_parent_if_needed + unless parent || parent_id + new_parent = Cms::Section.with_path(self.class.path).first + + unless new_parent + logger.warn "Creating default section for #{self.try(:display_name)} in #{self.class.path}." + section_attributes = {:name => self.class.name.demodulize.pluralize, + :path => self.class.path, + :hidden => true, + allow_groups: :all} + section_parent = Cms::Section.root.first + if section_parent + section_attributes[:parent] = section_parent + end + new_parent = Cms::Section.create!(section_attributes) + end + self.parent_id = new_parent.id + end + end end module Addressable @@ -130,17 +161,10 @@ def slug=(slug) else @slug = slug # Store temporarily until there is a section_node created. end - - end - - def self.included(klass) - #klass.attr_accessible :slug + dirty! end end - def self.included(model_class) - #model_class.attr_accessible :parent, :parent_id - end # Returns all classes which need a custom route to show themselves. def self.classes_that_require_custom_routes @@ -202,6 +226,10 @@ def cache_parent(section) @parent = section end + def parent_id + parent.try(:id) + end + def parent_id=(id) self.parent = Cms::Section.find(id) # Handles slug being set before there is a parent @@ -273,6 +301,19 @@ def section=(sec) end end + + # Allows records to be marked as being changed, which can be called to trigger updates when + # associated objects may have been changed. + module MarkAsDirty + # Forces this record to be changed, even if nothing has changed + # This is necessary if just the section.id has changed, for example + # test if this is necessary now that the attributes are in the + # model itself. + def dirty! + # Seems like a hack, is there a better way? + self.updated_at = Time.now + end + end end diff --git a/lib/cms/content_rendering_support.rb b/lib/cms/content_rendering_support.rb index f70d56b09..d9bbce838 100644 --- a/lib/cms/content_rendering_support.rb +++ b/lib/cms/content_rendering_support.rb @@ -15,6 +15,11 @@ def self.included(base) end + def show_content_as_page(content) + @page = content # page templates expect a @page attribute + @content_block = content # render.html.erb's expect a @content_block attribute + end + def handle_draft_not_found(exception) logger.warn "Draft Content Not Found" render(:layout => 'cms/application', diff --git a/lib/cms/engine.rb b/lib/cms/engine.rb index 509b41763..1476a8417 100644 --- a/lib/cms/engine.rb +++ b/lib/cms/engine.rb @@ -36,6 +36,7 @@ class Engine < Rails::Engine # Define configuration for the CKEditor config.cms.ckeditor = ActiveSupport::OrderedOptions.new + # Make sure we use our rails model template (rather then its default) when `rails g cms:content_block` is run. config.app_generators do |g| path = File::expand_path('../../templates', __FILE__) @@ -72,6 +73,10 @@ class Engine < Rails::Engine # config.cms.site_domain = "www.browsercms.org" app.config.cms.site_domain = "localhost:3000" + # Determines what email sender will be applied to messages generated by the CMS. + # By default, this is based on the site_domain, i.e. mailbot@example.com + app.config.cms.mailbot = :default + # Determines which ckeditor file will be used to configure all instances. # There should be at most ONE of these, so use manifest files which require the below one to augement it. app.config.cms.ckeditor.configuration_file = 'bcms/ckeditor_standard_config.js' @@ -81,6 +86,16 @@ class Engine < Rails::Engine require 'cms/configure_simple_form' require 'cms/configure_simple_form_bootstrap' + + # Sets the default .css file that will be added to forms created via the Forms module. + # Projects can override this as needed. + app.config.cms.form_builder_css = 'cms/default-forms' + end + + # Needed to ensure routes added to the main app by the Engine are available. (Since engine draws its routes after the main app) + # Borrrow from Spree as documenented here: https://github.com/rails/rails/issues/11895 + config.after_initialize do + Rails.application.routes_reloader.reload! end initializer 'browsercms.add_core_routes', :after => 'action_dispatch.prepare_dispatcher' do |app| @@ -88,6 +103,7 @@ class Engine < Rails::Engine end initializer 'browsercms.add_load_paths', :after => 'action_controller.deprecated_routes' do |app| + ActiveSupport::Dependencies.autoload_paths += %W( #{self.root}/vendor #{self.root}/app/mailers #{self.root}/app/helpers) ActiveSupport::Dependencies.autoload_paths += %W( #{self.root}/app/controllers #{self.root}/app/models #{self.root}/app/portlets) ActiveSupport::Dependencies.autoload_paths += %W( #{Rails.root}/app/portlets ) diff --git a/lib/cms/engine_helper.rb b/lib/cms/engine_helper.rb index c8811179b..18c5497c6 100644 --- a/lib/cms/engine_helper.rb +++ b/lib/cms/engine_helper.rb @@ -2,36 +2,34 @@ module Cms class EngineAwarePathBuilder - def initialize(model_class_or_content_type_or_model) - # ContentType - if model_class_or_content_type_or_model.respond_to? :model_class - @path_subject = model_class_or_content_type_or_model.model_class - else # Class or Model - @path_subject = model_class_or_content_type_or_model - end - end - # The object that will be added to the constructed path. - def path_subject - @path_subject + attr_reader :path_subject + + def initialize(model_class_or_content_type_or_model) + normalize_subject_class(model_class_or_content_type_or_model) end def subject_class - if @path_subject.instance_of?(Class) - @path_subject + if path_subject.instance_of?(Class) + path_subject else - @path_subject.class + path_subject.class end end - def build(view) + def build_preview(view) path = [] path << engine_name - path << "cms" if main_app_model? path << path_subject + path + end - path[0] = view.send(path[0]) # Replace the engine name with an actual lookup of the proper Engine routeset + def build(view) + path = [] + path << engine(view) + path << path_subject path + end def main_app_model? @@ -40,7 +38,8 @@ def main_app_model? # Determine which 'Engine' this model is from based on the class def engine_name - name = EngineHelper.module_name(subject_class) + model_class = subject_class.model_name + name = EngineAware.module_name(model_class) return "main_app" unless name begin @@ -51,52 +50,43 @@ def engine_name end engine.engine_name end - end - module EngineHelper - def main_app_model? - engine_name == "main_app" + def engine_class + if main_app_model? + Rails.application + else + guess_engine_class(subject_class) + end end - def engine_exists? - !main_app_model? - end + private - def engine_name - name = EngineHelper.module_name(target_class) - return "main_app" unless name + # Will raise NameError if klass::Engine doesn't exist. + def guess_engine_class(klass) + name = EngineAware.module_name(klass) + "#{name}::Engine".constantize + end - begin - engine = "#{name}::Engine".constantize - rescue NameError - # This means there is no Engine for this model, so its from the main Rails App. - return "main_app" + # Allows ContentType, Class, or model to be passed. + def normalize_subject_class(model_class_or_content_type_or_model) + if model_class_or_content_type_or_model.respond_to? :model_class # i.e. ContentType + @path_subject = model_class_or_content_type_or_model.model_class + else # Class or Model + @path_subject = model_class_or_content_type_or_model end - engine.engine_name end - def path_elements - path = [] - path << "cms" if main_app_model? - path << path_subject + # Loads the actual engine (which contains the RouteSet.) + # See http://api.rubyonrails.org/classes/ActionDispatch/Routing/PolymorphicRoutes.html + def engine(view) + view.send(engine_name) end - # Subclasses can override this as necessary - def target_class - return self.class unless self.instance_of?(Class) - self - end - # Subclasses can override this as necessary - def path_subject - self - end + end - # Add this module if its not already. - def self.decorate(instance) - instance.extend EngineHelper unless instance.respond_to?(:engine_name) - end + module EngineAware # Finds the top level module for a given class. # Cms::Thing -> Cms diff --git a/lib/cms/module.rb b/lib/cms/module.rb index f3a5451bc..83c4ff933 100644 --- a/lib/cms/module.rb +++ b/lib/cms/module.rb @@ -29,7 +29,7 @@ def self.included(base) # # And have it correctly find the right namespaced class model (i.e. BcmsZoo::Bear) def routes - Module.current_namespace = ::Cms::EngineHelper.module_name(self.class) + Module.current_namespace = ::Cms::EngineAware.module_name(self.class) super end end diff --git a/lib/cms/polymorphic_single_table_inheritance.rb b/lib/cms/polymorphic_single_table_inheritance.rb new file mode 100644 index 000000000..abcb0340d --- /dev/null +++ b/lib/cms/polymorphic_single_table_inheritance.rb @@ -0,0 +1,19 @@ +module Cms + # Allows STI classes to be handled by a single resource controller + # i.e. LoginPortlet can be handled by the Cms::PortletController as a resource. + # + # + # This is a simpler version of this solution: https://gist.github.com/sj26/5843855 + module PolymorphicSingleTableInheritance + + # Override model_name to return base_class Name. + def model_name + if self == base_class + super + else + base_class.model_name + end + end + + end +end \ No newline at end of file diff --git a/lib/cms/route_extensions.rb b/lib/cms/route_extensions.rb index c20fe2395..a6d08858d 100644 --- a/lib/cms/route_extensions.rb +++ b/lib/cms/route_extensions.rb @@ -4,27 +4,23 @@ module Cms::RouteExtensions # Adds all necessary routes to manage a new content type. Works very similar to the Rails _resources_ method, adding basic CRUD routes, as well as additional ones # for CMS specific routes (like versioning) # - # @param [Symbol] content_block_name - The plural name of a new Content Type. Should match the name of the content_block, like :dogs or :donation_statuses + # @param [Symbol] content_block_name - The plural name of a new Content Type. Should match the name of the model_class, like :dogs or :donation_statuses def content_blocks(content_block_name, options={}, & block) - content_name = content_block_name.to_s.classify - begin - content_block = "#{Cms::Module.current_namespace}::#{content_name}".constantize - rescue NameError - content_block = content_name.constantize - end + model_class = guess_model_class(content_block_name) resources content_block_name do member do - put :publish if content_block.publishable? - get :versions if content_block.versioned? - get :usages if content_block.connectable? + put :publish if model_class.publishable? + get :versions if model_class.versioned? + get :usages if model_class.connectable? end end - if content_block.versioned? - send("get", "/#{content_block_name}/:id/version/:version", :to => "#{content_block_name}#version", :as => "version_cms_#{content_block_name}".to_sym) - send("put", "/#{content_block_name}/:id/revert_to/:version", :to => "#{content_block_name}#revert_to", :as => "revert_to_cms_#{content_block_name}".to_sym) + if model_class.versioned? + get "/#{content_block_name}/:id/version/:version", :to => "#{content_block_name}#version", :as => "#{content_block_name}_version" + put "/#{content_block_name}/:id/revert_to/:version", :to => "#{content_block_name}#revert_to", :as => "revert_to_cms_#{content_block_name}" end + end # Adds the routes required for BrowserCMS to function to a routes.rb file. Should be the last route in the file, as @@ -37,9 +33,9 @@ def content_blocks(content_block_name, options={}, & block) # end # def mount_browsercms + mount Cms::Engine => "/cms", :as => "cms" - add_routes_for_addressable_content_blocks add_page_routes_defined_in_database # Handle 'stock' attachments @@ -57,22 +53,77 @@ def mount_browsercms private - # Creates a default GET route for every addressable content block class. - # This will be use a :slug and the module path, like: - # /products/:slug + def guess_model_class(content_block_name) + content_name = content_block_name.to_s.classify + prefix = determine_model_prefix + begin + namespaced_model_name = "#{Cms::Module.current_namespace}::#{content_name}" + model_class = namespaced_model_name.constantize + rescue NameError + model_class = "#{prefix}#{content_name}".constantize + end + model_class + end + + def determine_model_prefix + if @scope && @scope[:module] + "#{@scope[:module].camelize}::" + else + "" + end + end + + + # Define additional routes (in the main_app) that addressable content types need. def add_routes_for_addressable_content_blocks classes = Cms::Concerns::Addressable.classes_that_require_custom_routes classes.each do |klass| - path = "#{klass.path}/:slug" - controller_name = klass.name.demodulize.pluralize.underscore - get path, to: "cms/#{controller_name}#show_via_slug" - route_name = "inline_cms_#{klass.name.demodulize.underscore}" - unless route_exists?(route_name) - get "cms/#{klass.path}/:id/inline", to: "cms/#{controller_name}#inline", as: route_name + base_route_name = klass.name.demodulize.underscore.gsub("/", "_") + add_show_via_slug_route(base_route_name, klass) + add_inline_content_route(base_route_name, klass) + end + end + + # I.e. /cms/forms/:id/inline + def add_inline_content_route(base_route_name, klass) + denamespaced_controller = klass.name.demodulize.pluralize.underscore + module_name = klass.name.deconstantize.underscore + inline_route_name = "#{base_route_name}_inline" + unless route_exists?(inline_route_name) + klass.content_type.engine_class.routes.prepend do + if klass.content_type.main_app_model? + namespace module_name do + inline_route(denamespaced_controller, inline_route_name, klass) + end + else + inline_route(denamespaced_controller, inline_route_name, klass) + end end + end + end + def inline_route(denamespaced_controller, inline_route_name, klass) + begin + get "#{klass.path}/:id/inline", to: "#{denamespaced_controller}#inline", as: inline_route_name + rescue ArgumentError + # Because when you prepend a route, you can't easily determine if it has already been defined. + # This avoids the error when Cms::PageRoute.reload_routes is called. + Rails.logger.debug "Skipping readding existing route (probably during a route reload): get \"#{klass.path}/:id/inline\", to: \"#{denamespaced_controller}#inline\", as: #{inline_route_name}" end + end + # I.e. /forms/:slug + def add_show_via_slug_route(base_route_name, klass) + slug_path = "#{klass.path}/:slug" + namespaced_controller = klass.name.underscore.pluralize + slug_path_name = "#{base_route_name}_slug" + # Add route to main application (By doing this here, we ensure all ContentBlock constants have already been load) + # Engines don't process their routes until after the main routes are created. + unless route_exists?(slug_path_name) + Rails.application.routes.prepend do + get slug_path, to: "#{namespaced_controller}#show_via_slug", as: slug_path_name + end + end end # Determine if a named route already exists, since Rails 4 will object if a duplicate named route is defined now. diff --git a/lib/generators/cms/content_block/content_block_generator.rb b/lib/generators/cms/content_block/content_block_generator.rb index 7c592da12..4f0bfae31 100644 --- a/lib/generators/cms/content_block/content_block_generator.rb +++ b/lib/generators/cms/content_block/content_block_generator.rb @@ -12,6 +12,15 @@ class ContentBlockGenerator < Rails::Generators::NamedBase include Rails::Generators::Migration include Rails::Generators::ResourceHelpers + def set_classpath + @in_core_application = false + unless namespaced? + cms_engine_module_name = Cms::EngineAware.module_name(Rails.application.class).constantize + Rails::Generators.namespace = cms_engine_module_name + @in_core_application = true + end + end + hook_for :orm, :in => :rails, :required => true, :as => :model def alter_the_model @@ -43,21 +52,22 @@ def alter_the_migration end end - hook_for :resource_controller, :in => :rails, :as => :controller, :required => true do |controller| - invoke controller, [namespaced_controller_name, options[:actions]] + hook_for :resource_controller, :in => :rails, :as => :controller, :required => true do |instance, controller| + instance.invoke controller, [instance.name.pluralize] end + def create_controller_and_views - gsub_file File.join('app/controllers', cms_or_class_path, "#{file_name.pluralize}_controller.rb"), /ApplicationController/, "Cms::ContentBlockController" - template '_form.html.erb', File.join('app/views', cms_or_class_path, file_name.pluralize, "_form.html.erb") - template 'render.html.erb', File.join('app/views', cms_or_class_path, file_name.pluralize, "render.html.erb") + gsub_file File.join('app/controllers', class_path, "#{file_name.pluralize}_controller.rb"), /ApplicationController/, "Cms::ContentBlockController" + template '_form.html.erb', File.join('app/views', class_path, file_name.pluralize, "_form.html.erb") + template 'render.html.erb', File.join('app/views', class_path, file_name.pluralize, "render.html.erb") end def create_routes - if namespaced? + if namespaced? && !@in_core_application route "content_blocks :#{file_name.pluralize}" else - route "namespace :cms do content_blocks :#{file_name.pluralize} end" + route "namespace :#{namespace.name.underscore} do content_blocks :#{file_name.pluralize} end" end end @@ -70,36 +80,6 @@ def model_has_attachment? def attachment_attributes self.attributes.select { |attr| attr.type == :attachment } end - - - def group_name - if namespaced? - class_name.split("::").first - else - class_name - end - end - - def namespaced_controller_name - unless namespaced? - "cms/#{@controller_name}" - else - @controller_name - end - end - - # Modules want to put classes under their namespace folders, i.e - # - app/controllers/bcms_widgets/widget_controller - # - # while projects want to put it under cms - # - app/controllers/cms/widget_controller - def cms_or_class_path - if namespaced? - class_path - else - ["cms"] - end - end end end end diff --git a/test/unit/models/content_type_mini_test.rb b/spec/cms/content_type_spec.rb similarity index 98% rename from test/unit/models/content_type_mini_test.rb rename to spec/cms/content_type_spec.rb index 4cbd50132..41283de68 100644 --- a/test/unit/models/content_type_mini_test.rb +++ b/spec/cms/content_type_spec.rb @@ -64,7 +64,7 @@ class AAAFirstBlock < ActiveRecord::Base describe '#connectable?' do it "should mark content_types as connectable to pages" do Cms::HtmlBlock.content_type.connectable?.must_equal true - Product.content_type.connectable?.must_equal true + Dummy::Product.content_type.connectable?.must_equal true [Cms::Category, Cms::Tag, Cms::CategoryType].each do |unconnectable| unconnectable.content_type.connectable?.must_equal false diff --git a/spec/cms/email_message_spec.rb b/spec/cms/email_message_spec.rb new file mode 100644 index 000000000..1fb77f2c4 --- /dev/null +++ b/spec/cms/email_message_spec.rb @@ -0,0 +1,55 @@ +require "minitest_helper" + +describe Cms::EmailMessage do + + describe ".deliver!" do + it "should assign mailbot if no sender was specified" do + m = Cms::EmailMessage.create!(recipients: "test@example.com", subject: 'Test', body: 'Test') #Deliver! is side effect + m.sender.must_equal Cms::EmailMessage.mailbot_address + end + end + + describe "#absolute_cms_url" do + it "should return a url which points to the cms admin version of a page" do + Rails.configuration.cms.expects(:site_domain).returns('www.example.com') + url = Cms::EmailMessage.absolute_cms_url("/some-path") + url.must_equal "http://cms.example.com/some-path" + end + + it "should handle domains without www. subdomain" do + Rails.configuration.cms.expects(:site_domain).returns('example.com') + url = Cms::EmailMessage.absolute_cms_url("/some-path") + url.must_equal "http://cms.example.com/some-path" + end + end + + describe "#normalize_domain" do + it "should remove ports" do + Cms::EmailMessage.normalize_domain("www.example.com:80").must_equal "example.com" + end + + it "should strip the www. off domains" do + Cms::EmailMessage.normalize_domain("www.example.com").must_equal "example.com" + end + + it "should not alter other subdomains" do + Cms::EmailMessage.normalize_domain("assets.example.com").must_equal "assets.example.com" + end + + it "should not alter with no subdomain" do + Cms::EmailMessage.normalize_domain("example.com").must_equal "example.com" + end + end + describe "#mailbot_address" do + it "should return a default address based on the site domain" do + Rails.configuration.cms.expects(:mailbot).returns(:default) + Rails.configuration.cms.expects(:site_domain).returns('www.example.com') + Cms::EmailMessage.mailbot_address.must_equal 'mailbot@example.com' + end + + it "should use mailbot in configuration if specified by project" do + Rails.configuration.cms.expects(:mailbot).returns('staff@example.com') + Cms::EmailMessage.mailbot_address.must_equal 'staff@example.com' + end + end +end \ No newline at end of file diff --git a/spec/cms/form_entry_spec.rb b/spec/cms/form_entry_spec.rb new file mode 100644 index 000000000..e7f2db907 --- /dev/null +++ b/spec/cms/form_entry_spec.rb @@ -0,0 +1,153 @@ +require "minitest_helper" + +def expect_nonrequired_fields(form) + mock_field = mock() + mock_field.expects(:required?).returns(false).at_least_once + form.expects(:field).returns(mock_field).at_least_once +end + +describe Cms::FormEntry do + let(:entry) { Cms::FormEntry.new } + + def contact_form + return @form if @form + @form = Cms::Form.new + @form.expects(:field_names).returns([:name, :email]).at_least_once + @form + end + + let(:contact_form_entry) { Cms::FormEntry.for(contact_form) } + + describe '.ish' do + it "should create object with class_eval'd methods'" do + entry = Cms::FormEntry.ish { + def hello + end + } + entry.respond_to?(:hello).must_equal true + end + end + + describe '.new' do + it "should create a entry with no accessors" do + entry.wont_be_nil + end + end + + def form_with_fields(fields) + form = Cms::Form.new + fields.each do |f| + form.fields << Cms::FormField.create!(f) + end + form.save! + form + end + + describe '#valid?' do + it "should return false if required fields are not filled in" do + form = form_with_fields([{label: "Name", required: true}]) + + form_entry = Cms::FormEntry.for(form) + form_entry.valid?.must_equal false + end + end + + describe '#data_columns' do + it 'should return nil for unset attributes' do + entry.data_columns['name'].must_be_nil + end + + it 'should allow arbitrary access' do + entry.data_columns['name'] = 'Stan' + entry.save! + entry.reload.data_columns['name'].must_equal 'Stan' + end + + it "should not share accessors across forms" do + [:name, :email].each do |field| + contact_form_entry.respond_to?(field).must_equal true + end + registration_form = Cms::Form.new + registration_form.expects(:field_names).returns([:first_name, :last_name]).at_least_once + expect_nonrequired_fields(registration_form) + registration_entry = Cms::FormEntry.for(registration_form) + + [:first_name, :last_name].each do |field| + registration_entry.respond_to?(field).must_equal true + end + [:name, :email].each do |field| + registration_entry.respond_to?(field).must_equal false + end + end + + it "should coerce fields to their proper type" do + entry.data_columns[:age] = 1 + entry.save! + + entry.reload.data_columns[:age].must_be_instance_of Fixnum + end + end + + describe '#prepare' do + it "should return a copy of a FormEntry with the same data and accessors" do + form = Cms::Form.create!(name: "Contact") + form.fields << Cms::FormField.create!(label: 'Name', required: true) + form.save! + + entry = Cms::FormEntry.for(form) + entry.name = "Filled in" + entry.save! + + e = entry.enable_validations + e.id.must_equal entry.id + e.new_record?.must_equal false + e.name.must_equal "Filled in" + e.name = "" + e.valid?.must_equal false + end + end + + describe '.for' do + it "should create FormEntry with accessors for backing form" do + form = Cms::Form.new + form.expects(:field_names).returns([:name, :email]).at_least_once + entry = Cms::FormEntry.for(form) + + entry.name = "Hello" + entry.name.must_equal 'Hello' + entry.data_columns[:name].must_equal 'Hello' + entry.respond_to? :name + entry.respond_to? :email + end + + it "should connect form to entry" do + form = Cms::Form.new + entry = Cms::FormEntry.for(form) + + entry.form.must_equal form + end + end + + describe '#form' do + + it "should be a belongs_to association" do + form = Cms::Form.create!(name: "Contact") + contact_form_entry = Cms::FormEntry.new(form: form) + contact_form_entry.save! + + contact_form_entry = Cms::FormEntry.find(contact_form_entry.id) + contact_form_entry.form.must_equal form + end + + end + + describe '#permitted_params' do + it "should respond" do + contact_form_entry.respond_to?(:name).must_equal true + end + + it "should return all the fields for the specific form" do + contact_form_entry.permitted_params.must_equal [:name, :email] + end + end +end diff --git a/spec/cms/form_fields_spec.rb b/spec/cms/form_fields_spec.rb new file mode 100644 index 000000000..dd504b59b --- /dev/null +++ b/spec/cms/form_fields_spec.rb @@ -0,0 +1,143 @@ +require "minitest_helper" + +describe Cms::FormField do + + describe '.permitted_params' do + it 'should return an array of fields' do + [:form_id, :label, :field_type, :required, :position, :instructions, :default_value].each do |field| + Cms::FormField.permitted_params.must_include field + end + end + end + + describe '#form' do + it "should be belongs_to" do + form = Cms::Form.create!(name: "Testing") + field = Cms::FormField.new(label: "Name") + field.form = form + field.save! + field.form.wont_be_nil + + field.form_id.must_equal field.form.id + end + + it "should trigger validation with duplicate names" do + form = Cms::Form.create!(name: "Testing") + field = Cms::FormField.new(label: "Name", form_id: form.id) + field.save.must_equal true + + duplicate_field = Cms::FormField.new(label: "Name", form_id: form.id) + duplicate_field.save.must_equal false + end + end + + describe '#name' do + it "should return a symbol that can be used as the name for inputs" do + field = Cms::FormField.create!(label: 'Name') + field.name.must_equal :name + end + + it "should underscore names" do + field = Cms::FormField.create!(label: 'Full Name') + field.name.must_equal :full_name + end + + it "should not change after being saved even when the label is changed" do + field = Cms::FormField.create!(label: 'Name') + field.update(label: 'Full Name') + field.name.must_equal :name + end + + + end + + describe "#options" do + let(:field) { Cms::FormField.new(label: 'Title', field_type: 'text_field') } + + it "can disable the input" do + field.options(disabled: true)[:disabled].must_equal(true) + field.options(disabled: true)[:readonly].must_equal('readonly') + end + it "should provide as: for default cases" do + field.options[:label].must_equal('Title') + end + + it "includes as: for text_areas" do + field.field_type = 'text_area' + field.options[:as].must_equal(:text) + end + + it "should include collection for :select" do + field.choices = "A\nB\nC" + field.options[:collection].must_equal ["A", "B", "C"] + field.options[:prompt].must_equal true + end + + it "set prompt to false for required selects" do + field.choices = "A\nB\nC" + field.required = true + field.options[:collection].must_equal ["A", "B", "C"] + field.options[:prompt].must_equal false + end + + it "should return required false by default" do + field.options[:required].must_equal false + end + it "should handle required fields" do + field.required = true + field.options[:required].must_equal true + end + + it "should return hints" do + field.instructions = "Fill this in" + field.options[:hint].must_equal "Fill this in" + end + + it "should return default value" do + field.default_value = "My Default" + field.options[:input_html][:value].must_equal "My Default" + end + + + it "should not return default value if the model has a value for the field" do + field.valid? # Ensure name is set + field.default_value = "My Default" + entry = mock() + entry.expects(:title).returns("some-value").at_least(0) + field.options({entry: entry}).wont_include(:input_html) + end + + + end + + describe "#as" do + it "should handle text_fields" do + field = Cms::FormField.new(field_type: 'text_field') + field.as.must_equal :string + end + + it "should handle text_areas" do + field = Cms::FormField.new(field_type: 'text_area') + field.as.must_equal :text + end + + it "should handle other random types" do + field = Cms::FormField.new(field_type: 'random') + field.as.must_equal :random + end + + it "should handle select" do + field = Cms::FormField.new(field_type: 'select') + field.as.must_equal :select + end + end + + describe "#as_json" do + let(:field) { Cms::FormField.new(label: 'Name') } + it "should include #edit_path when being serialized" do + field.edit_path = "/cms/form_fields/1/edit" + json = JSON.parse(field.to_json) + json["edit_path"].must_equal "/cms/form_fields/1/edit" + end + end +end diff --git a/spec/cms/form_spec.rb b/spec/cms/form_spec.rb new file mode 100644 index 000000000..96dc92cae --- /dev/null +++ b/spec/cms/form_spec.rb @@ -0,0 +1,130 @@ +require "minitest_helper" + +describe Cms::Form do + + let(:form) { Cms::Form.new(name: 'Contact Us') } + describe '.create!' do + it "should create a new instance" do + form.save! + form.name.must_equal "Contact Us" + form.persisted?.must_equal true + end + + it "should create a slug when created " do + form.slug = "/contact-us" + form.save! + form.reload.section_node.wont_be_nil + form.section_node.slug.must_equal "/contact-us" + end + + it "should assign parent with created" do + form.save! + form.parent.wont_be_nil + form.section_node.slug.must_equal + end + + end + + describe '#update' do + let(:saved_form) { Cms::Form.create! } + it "should update slug" do + saved_form.update({name: 'New', slug: '/about-us'}).must_equal true + saved_form.reload.section_node.slug.must_equal '/about-us' + end + end + + describe '#show_text?' do + it "should return true with the :show_text confirmation behavior" do + form.confirmation_behavior = :show_text + form.save! + form.reload.show_text?.must_equal true + end + end + + describe "#valid?" do + it "should not allow improperly formatted notification emails (requires @)" do + form.notification_email = "valid@example.com" + form.must_be :valid? + + form.notification_email = "not-valid-example.com" + form.wont_be :valid? + + form.notification_email = "" + form.must_be :valid? + end + end + + describe '#fields' do + it "should save a list of fields" do + field = Cms::FormField.new(label: "Event Name", field_type: :string) + form.fields << field + form.save! + + form.reload.fields.size.must_equal 1 + field.persisted?.must_equal true + end + + it "should set error on :base when there are duplicate fields" do + form.fields << Cms::FormField.create(label: 'Name') + form.fields << Cms::FormField.new(label: 'Name') + form.valid?.must_equal false + form.errors[:base].size.must_equal 1 + form.errors[:base].must_include "Labels can only be used once per form." + end + + describe "ordering" do + def form_with_fields(field_labels=[]) + return @form_with_fields if @form_with_fields + @form_with_fields = Cms::Form.new + field_labels.each do |label| + @form_with_fields.fields << Cms::FormField.create(label: label) + end + @form_with_fields.save! + @form_with_fields + end + + it "should add fields in position order" do + f = form_with_fields(['Name', 'Address']) + f.fields.first.position.must_equal 1 + f.fields.last.position.must_equal 2 + end + + it "are orderable" do + form = form_with_fields(['Name', 'Address']) + form.fields.last.move_to_top + form.reload + form.fields.first.label.must_equal "Address" + end + end + + end + + describe '#field_names' do + + it "should return a list of the field names as symbols" do + form = Cms::Form.new + form.fields << Cms::FormField.new(label: 'Name') + form.fields << Cms::FormField.new(label: 'Email') + form.save! + form.field_names.must_equal [:name, :email] + end + end + + describe '#required?' do + + def form_with_name_field + form = Cms::Form.new + form.fields << Cms::FormField.new(label: 'Name', required: true) + form.fields << Cms::FormField.new(label: 'Email') + form.save! + form + end + + it "should return true for required fields" do + form_with_name_field.required?(:name).must_equal(true) + end + it "should returns nil for missing fields" do + form_with_name_field.required?(:email).must_equal(false) + end + end +end diff --git a/test/unit/concerns/addressable_test.rb b/spec/concerns/addressable_spec.rb similarity index 75% rename from test/unit/concerns/addressable_test.rb rename to spec/concerns/addressable_spec.rb index 5bf03eaf7..0f0083a0a 100644 --- a/test/unit/concerns/addressable_test.rb +++ b/spec/concerns/addressable_spec.rb @@ -7,9 +7,13 @@ class WannabeAddressable class CouldBeAddressable < ActiveRecord::Base; end +# Mimics Attachments +class ManuallySetPath < ActiveRecord::Base + is_addressable +end + class HasSelfDefinedPath < ActiveRecord::Base is_addressable(no_dynamic_path: true) - #attr_accessible :path end class IsAddressable < ActiveRecord::Base; @@ -26,6 +30,9 @@ def create_testing_table(name, &block) ActiveRecord::Base.connection.instance_eval do drop_table(name) if table_exists?(name) create_table(name, &block) + change_table name do |t| + t.timestamps + end end end @@ -34,6 +41,10 @@ def create_testing_table(name, &block) t.string :name end create_testing_table :another_addressables + create_testing_table :manually_set_paths do |t| + t.string :name + t.string :path + end create_testing_table :has_self_defined_paths do |t| t.string :path end @@ -43,6 +54,7 @@ def create_testing_table(name, &block) describe '#is_addressable' do it "should have parent relationship" do WannabeAddressable.expects(:has_one) + WannabeAddressable.expects(:after_save) WannabeAddressable.is_addressable WannabeAddressable.new.must_respond_to :parent end @@ -113,6 +125,11 @@ class UsingDefaultTemplate < ActiveRecord::Base it "should be nil for new objects" do addressable.slug.must_be_nil end + + it "setting it should mark the record as changed, so it will persist" do + addressable.slug = 'new' + addressable.changed?.must_equal true + end end describe "#descendants" do @@ -153,6 +170,13 @@ class UsingDefaultTemplate < ActiveRecord::Base end end + describe "#update" do + let(:saved_content) { IsAddressable.create! name: "Test" } + it "should autosave changes to section_node" do + saved_content.update(slug: "/change") + saved_content.reload.section_node.slug.must_equal "/change" + end + end describe "#create" do it "should allow for both parent and slug to be saved" do f = IsAddressable.create!(parent_id: root_section.id, slug: "slug") @@ -169,6 +193,41 @@ class UsingDefaultTemplate < ActiveRecord::Base f = IsAddressable.create(slug: "slug", parent_id: root_section.id) f.section_node.slug.must_equal "slug" end + + describe "without assigning parent" do + + it "should create parent section if it doesn't exist'" do + find_or_create_root_section + content = IsAddressable.create! name: 'Hello' + content.section_node.wont_be_nil + end + + it "should create new section (with no parent) if root doesn't existing'" do + content = IsAddressable.create! name: 'Hello' + content.section_node.wont_be_nil + content.parent.parent.must_be_nil + content.parent.root?.must_equal false + end + end + + describe "with a manully set path" do + it "should assign a parent if the class doesn't define a default path" do + content = ManuallySetPath.create! path: "/must-be-set", parent: root_section + content.parent.must_equal root_section + end + + it "should not create a parent if the class doesn't define a default path" do + content = ManuallySetPath.create! path: "/must-be-set" + content.parent.must_be_nil + end + end + end + + describe "#parent_id" do + it "should return parent id" do + addressable.parent = root_section + addressable.parent_id.must_equal root_section.id + end end describe "#calculate_path" do @@ -197,12 +256,4 @@ class UsingDefaultTemplate < ActiveRecord::Base end end - describe "#ancestors" do - describe "without a parent" do - it "should have no ancestors" do - content = IsAddressable.create - content.ancestors.must_equal [] - end - end - end end diff --git a/test/unit/models/product_mini_test.rb b/spec/dummy/product_mini_test.rb similarity index 84% rename from test/unit/models/product_mini_test.rb rename to spec/dummy/product_mini_test.rb index c4f1a04fb..6d789f892 100644 --- a/test/unit/models/product_mini_test.rb +++ b/spec/dummy/product_mini_test.rb @@ -1,6 +1,6 @@ require "minitest_helper" -describe Product do +describe Dummy::Product do describe 'Factory' do it 'should ' do p = FactoryGirl.create(:product) diff --git a/spec/inputs/cms_text_field_input_spec.rb b/spec/inputs/cms_text_field_input_spec.rb new file mode 100644 index 000000000..a9733d529 --- /dev/null +++ b/spec/inputs/cms_text_field_input_spec.rb @@ -0,0 +1,27 @@ +require "minitest_helper" + +describe CmsTextFieldInput do + + def text_input(attribute_name, object) + form_builder = mock() + form_builder.expects(:object).returns(object).at_least_once + CmsTextFieldInput.new(form_builder, attribute_name, attribute_name, :string) + end + + describe 'should_autogenerate_slug?' do + it 'should generate slug when object is new' do + input = text_input(:name, Cms::Form.new) + input.send(:should_autogenerate_slug?, :name).must_equal true + end + + it 'should not generate slug for saved object with a name/slug' do + input = text_input(:name, Cms::Form.create!(name: "Name", slug: "/name")) + input.send(:should_autogenerate_slug?, :name).must_equal false + end + + it 'should generate slug when object has blank name and slug' do + input = text_input(:name, Cms::Form.create!(name: "", slug: "")) + input.send(:should_autogenerate_slug?, :name).must_equal true + end + end +end diff --git a/spec/minitest_helper.rb b/spec/minitest_helper.rb new file mode 100644 index 000000000..01eca4e0c --- /dev/null +++ b/spec/minitest_helper.rb @@ -0,0 +1,23 @@ +ENV["RAILS_ENV"] = "test" +require File.expand_path("../../test/dummy/config/environment.rb", __FILE__) +require "rails/test_help" +require "minitest/spec" +require "minitest/unit" + +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } +require File.expand_path("../../test/factories/factories", __FILE__) +require File.expand_path("../../test/factories/attachable_factories", __FILE__) + +require 'minitest/reporters' +MiniTest::Reporters.use! + +require 'database_cleaner' +DatabaseCleaner.strategy = :truncation + +class Minitest::Spec + after :each do + DatabaseCleaner.clean + end + include FactoryGirl::Syntax::Methods + include FactoryHelpers +end diff --git a/test/dummy/app/assets/stylesheets/cms/catalogs.css b/test/dummy/app/assets/stylesheets/cms/catalogs.css deleted file mode 100644 index afad32db0..000000000 --- a/test/dummy/app/assets/stylesheets/cms/catalogs.css +++ /dev/null @@ -1,4 +0,0 @@ -/* - Place all the styles related to the matching controller here. - They will automatically be included in application.css. -*/ diff --git a/test/dummy/app/assets/stylesheets/custom-forms.css.scss b/test/dummy/app/assets/stylesheets/custom-forms.css.scss new file mode 100644 index 000000000..169098dce --- /dev/null +++ b/test/dummy/app/assets/stylesheets/custom-forms.css.scss @@ -0,0 +1,2 @@ +@import 'bootstrap'; +@import "bootstrap-responsive"; diff --git a/test/dummy/app/controllers/cms/catalogs_controller.rb b/test/dummy/app/controllers/cms/catalogs_controller.rb deleted file mode 100644 index 5f0dd8842..000000000 --- a/test/dummy/app/controllers/cms/catalogs_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Cms::CatalogsController < Cms::ContentBlockController -end diff --git a/test/dummy/app/controllers/cms/deprecated_inputs_controller.rb b/test/dummy/app/controllers/cms/deprecated_inputs_controller.rb deleted file mode 100644 index 7df9ad404..000000000 --- a/test/dummy/app/controllers/cms/deprecated_inputs_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Cms::DeprecatedInputsController < Cms::ContentBlockController -end diff --git a/test/dummy/app/controllers/cms/sample_blocks_controller.rb b/test/dummy/app/controllers/cms/sample_blocks_controller.rb deleted file mode 100644 index 069e83f84..000000000 --- a/test/dummy/app/controllers/cms/sample_blocks_controller.rb +++ /dev/null @@ -1,3 +0,0 @@ -# This exists only for testing -class Cms::SampleBlocksController < Cms::ContentBlockController -end \ No newline at end of file diff --git a/test/dummy/app/controllers/dummy/catalogs_controller.rb b/test/dummy/app/controllers/dummy/catalogs_controller.rb new file mode 100644 index 000000000..684c62a40 --- /dev/null +++ b/test/dummy/app/controllers/dummy/catalogs_controller.rb @@ -0,0 +1,2 @@ +class Dummy::CatalogsController < Cms::ContentBlockController +end diff --git a/test/dummy/app/controllers/dummy/deprecated_inputs_controller.rb b/test/dummy/app/controllers/dummy/deprecated_inputs_controller.rb new file mode 100644 index 000000000..dc9b7015c --- /dev/null +++ b/test/dummy/app/controllers/dummy/deprecated_inputs_controller.rb @@ -0,0 +1,2 @@ +class Dummy::DeprecatedInputsController < Cms::ContentBlockController +end diff --git a/test/dummy/app/controllers/cms/products_controller.rb b/test/dummy/app/controllers/dummy/products_controller.rb similarity index 84% rename from test/dummy/app/controllers/cms/products_controller.rb rename to test/dummy/app/controllers/dummy/products_controller.rb index cf35fd94f..0a2e0b094 100644 --- a/test/dummy/app/controllers/cms/products_controller.rb +++ b/test/dummy/app/controllers/dummy/products_controller.rb @@ -1,4 +1,4 @@ -module Cms +module Dummy class ProductsController < Cms::ContentBlockController diff --git a/test/dummy/app/controllers/dummy/sample_blocks_controller.rb b/test/dummy/app/controllers/dummy/sample_blocks_controller.rb new file mode 100644 index 000000000..a9634cbcd --- /dev/null +++ b/test/dummy/app/controllers/dummy/sample_blocks_controller.rb @@ -0,0 +1,3 @@ +# This exists only for testing +class Dummy::SampleBlocksController < Cms::ContentBlockController +end \ No newline at end of file diff --git a/test/dummy/app/models/catalog.rb b/test/dummy/app/models/dummy/catalog.rb similarity index 68% rename from test/dummy/app/models/catalog.rb rename to test/dummy/app/models/dummy/catalog.rb index c93d771b2..19a188a87 100644 --- a/test/dummy/app/models/catalog.rb +++ b/test/dummy/app/models/dummy/catalog.rb @@ -1,6 +1,7 @@ -class Catalog < ActiveRecord::Base +class Dummy::Catalog < ActiveRecord::Base acts_as_content_block content_module :testing + self.table_name = :catalogs is_addressable path: "/catalogs" has_many_attachments :photos, :styles => { :thumbnail => "50x50" } diff --git a/test/dummy/app/models/deprecated_input.rb b/test/dummy/app/models/dummy/deprecated_input.rb similarity index 58% rename from test/dummy/app/models/deprecated_input.rb rename to test/dummy/app/models/dummy/deprecated_input.rb index 65db078c5..4568fa448 100644 --- a/test/dummy/app/models/deprecated_input.rb +++ b/test/dummy/app/models/dummy/deprecated_input.rb @@ -1,10 +1,14 @@ -class DeprecatedInput < ActiveRecord::Base +# This model uses syntax for BrowserCMS 3.5.x that has been deprecated. +# It's used to verify that deprecated methods work but generate warnings. +# +class Dummy::DeprecatedInput < ActiveRecord::Base acts_as_content_block taggable: true content_module :deprecated_inputs belongs_to_category has_attachment :cover_photo has_many_attachments :photos + self.table_name = :deprecated_inputs # For testing template_editor input def self.render_inline true diff --git a/test/dummy/app/models/product.rb b/test/dummy/app/models/dummy/product.rb similarity index 79% rename from test/dummy/app/models/product.rb rename to test/dummy/app/models/dummy/product.rb index abb22da36..ac99aea87 100644 --- a/test/dummy/app/models/product.rb +++ b/test/dummy/app/models/dummy/product.rb @@ -1,7 +1,8 @@ # This is a sample content type that mimics how content blocks are generated with project status. -class Product < ActiveRecord::Base +class Dummy::Product < ActiveRecord::Base acts_as_content_block content_module :testing + self.table_name = :products belongs_to_category diff --git a/test/dummy/app/models/cms/sample_block.rb b/test/dummy/app/models/dummy/sample_block.rb similarity index 95% rename from test/dummy/app/models/cms/sample_block.rb rename to test/dummy/app/models/dummy/sample_block.rb index b9dfd165e..7d30a6ce8 100644 --- a/test/dummy/app/models/cms/sample_block.rb +++ b/test/dummy/app/models/dummy/sample_block.rb @@ -1,4 +1,4 @@ -module Cms +module Dummy class SampleBlock extend ::ActiveModel::Naming diff --git a/test/dummy/app/views/cms/catalogs/_form.html.erb b/test/dummy/app/views/dummy/catalogs/_form.html.erb similarity index 100% rename from test/dummy/app/views/cms/catalogs/_form.html.erb rename to test/dummy/app/views/dummy/catalogs/_form.html.erb diff --git a/test/dummy/app/views/cms/catalogs/render.html.erb b/test/dummy/app/views/dummy/catalogs/render.html.erb similarity index 100% rename from test/dummy/app/views/cms/catalogs/render.html.erb rename to test/dummy/app/views/dummy/catalogs/render.html.erb diff --git a/test/dummy/app/views/cms/deprecated_inputs/_form.html.erb b/test/dummy/app/views/dummy/deprecated_inputs/_form.html.erb similarity index 100% rename from test/dummy/app/views/cms/deprecated_inputs/_form.html.erb rename to test/dummy/app/views/dummy/deprecated_inputs/_form.html.erb diff --git a/test/dummy/app/views/cms/deprecated_inputs/render.html.erb b/test/dummy/app/views/dummy/deprecated_inputs/render.html.erb similarity index 100% rename from test/dummy/app/views/cms/deprecated_inputs/render.html.erb rename to test/dummy/app/views/dummy/deprecated_inputs/render.html.erb diff --git a/test/dummy/app/views/cms/products/_form.html.erb b/test/dummy/app/views/dummy/products/_form.html.erb similarity index 100% rename from test/dummy/app/views/cms/products/_form.html.erb rename to test/dummy/app/views/dummy/products/_form.html.erb diff --git a/test/dummy/app/views/cms/products/render.html.erb b/test/dummy/app/views/dummy/products/render.html.erb similarity index 100% rename from test/dummy/app/views/cms/products/render.html.erb rename to test/dummy/app/views/dummy/products/render.html.erb diff --git a/test/dummy/app/views/cms/products/view.html.erb b/test/dummy/app/views/dummy/products/view.html.erb similarity index 100% rename from test/dummy/app/views/cms/products/view.html.erb rename to test/dummy/app/views/dummy/products/view.html.erb diff --git a/test/dummy/app/views/portlets/product_catalog/render.html.erb b/test/dummy/app/views/portlets/product_catalog/render.html.erb index a5857eb1b..747aeee60 100644 --- a/test/dummy/app/views/portlets/product_catalog/render.html.erb +++ b/test/dummy/app/views/portlets/product_catalog/render.html.erb @@ -1,6 +1,6 @@ Product Catalog -We have: <%= Product.all.size %> products.
-<% Product.all.each do |p| %> +We have: <%= Dummy::Product.all.size %> products.
+<% Dummy::Product.all.each do |p| %> <%= link_to "View #{p.name}", product_path(:id => p.id) %>
<% end %> \ No newline at end of file diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index 6936a26b6..2c7a8deb7 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -11,6 +11,7 @@ module Dummy class Application < Rails::Application + config.cms.form_builder_css = 'custom-forms' # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index a9b86ec63..9833037cf 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -1,11 +1,11 @@ Dummy::Application.routes.draw do - namespace :cms do content_blocks :deprecated_inputs end + namespace :dummy do content_blocks :products end + namespace :dummy do content_blocks :deprecated_inputs end get "content-page", :to=>"content_page#index" get "custom-page", :to=>"content_page#custom_page" - namespace :cms do content_blocks :catalogs end - namespace :cms do content_blocks :products end - namespace :cms do content_blocks :sample_blocks end + namespace :dummy do content_blocks :catalogs end + namespace :dummy do content_blocks :sample_blocks end # For testing Acts::As::Page get "/__test__", :to => "cms/content#show_page_route" diff --git a/test/dummy/db/migrate/20131106193042_add_choices.rb b/test/dummy/db/migrate/20131106193042_add_choices.rb new file mode 100644 index 000000000..56777947d --- /dev/null +++ b/test/dummy/db/migrate/20131106193042_add_choices.rb @@ -0,0 +1,5 @@ +class AddChoices < ActiveRecord::Migration + def change + add_column prefix(:form_fields), :choices, :text + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 4ce04701e..935ce9c69 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20130924162315) do +ActiveRecord::Schema.define(version: 20131106193042) do create_table "catalog_versions", force: true do |t| t.string "name" @@ -213,6 +213,66 @@ add_index "cms_file_blocks", ["deleted"], name: "index_cms_file_blocks_on_deleted", using: :btree add_index "cms_file_blocks", ["type"], name: "index_cms_file_blocks_on_type", using: :btree + create_table "cms_form_entries", force: true do |t| + t.text "data_columns" + t.integer "form_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + create_table "cms_form_fields", force: true do |t| + t.integer "form_id" + t.string "label" + t.string "name" + t.string "field_type" + t.boolean "required" + t.integer "position" + t.text "instructions" + t.text "default_value" + t.datetime "created_at" + t.datetime "updated_at" + t.text "choices" + end + + add_index "cms_form_fields", ["form_id", "name"], name: "index_cms_form_fields_on_form_id_and_name", unique: true, using: :btree + + create_table "cms_form_versions", force: true do |t| + t.string "name" + t.text "description" + t.string "confirmation_behavior" + t.text "confirmation_text" + t.string "confirmation_redirect" + t.string "notification_email" + t.integer "original_record_id" + t.integer "version" + t.boolean "published", default: false + t.boolean "deleted", default: false + t.boolean "archived", default: false + t.string "version_comment" + t.integer "created_by_id" + t.integer "updated_by_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + create_table "cms_forms", force: true do |t| + t.string "name" + t.text "description" + t.string "confirmation_behavior" + t.text "confirmation_text" + t.string "confirmation_redirect" + t.string "notification_email" + t.integer "version" + t.integer "lock_version", default: 0 + t.boolean "published", default: false + t.boolean "deleted", default: false + t.boolean "archived", default: false + t.integer "created_by_id" + t.integer "updated_by_id" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "cms_group_permissions", force: true do |t| t.integer "group_id" t.integer "permission_id" diff --git a/test/dummy/db/seeds.rb b/test/dummy/db/seeds.rb index c5f8984c4..493f2b0e8 100644 --- a/test/dummy/db/seeds.rb +++ b/test/dummy/db/seeds.rb @@ -1,5 +1,3 @@ load File.expand_path('../../../../db/browsercms.seeds.rb', __FILE__) -create_content_type(:catalog, name: "Catalog", group_name: "Testing") -create_content_type(:product, name: "Product", group_name: "Testing") diff --git a/test/dummy/test/models/deprecated_input_test.rb b/test/dummy/test/models/deprecated_input_test.rb index c38f7899e..f0d5b972b 100644 --- a/test/dummy/test/models/deprecated_input_test.rb +++ b/test/dummy/test/models/deprecated_input_test.rb @@ -1,7 +1,10 @@ require "test_helper" -class DeprecatedInputTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end +module Dummy + class DeprecatedInputTest < ActiveSupport::TestCase + + test "#model_form_name should use full model name" do + assert_equal "dummy_deprecated_input", DeprecatedInput.content_type.param_key + end + end end diff --git a/test/factories/attachable_factories.rb b/test/factories/attachable_factories.rb index 7e615a03a..aaf995091 100644 --- a/test/factories/attachable_factories.rb +++ b/test/factories/attachable_factories.rb @@ -79,7 +79,7 @@ class HasThumbnail < ActiveRecord::Base factory :catalog_attachment, :class => Cms::Attachment do |m| m.attachment_name "photos" - m.attachable_type "Catalog" + m.attachable_type "Dummy::Catalog" m.data { mock_text_file } m.parent { find_or_create_root_section } m.publish_on_save true diff --git a/test/factories/factories.rb b/test/factories/factories.rb index af8307ff7..ebd3e8958 100644 --- a/test/factories/factories.rb +++ b/test/factories/factories.rb @@ -259,8 +259,13 @@ end end - factory :product, :class => Product do |product| + factory :product, :class => Dummy::Product do |product| product.name "Product" product.sequence(:slug) { |n| "/product-#{n}" } end + + factory :form, :class => Cms::Form do |form| + form.name "Form" + form.sequence(:slug) { |n| "/form-#{n}" } + end end diff --git a/test/functional/cms/content_block_controller_test.rb b/test/functional/cms/content_block_controller_test.rb index fdbe3a6d5..096a8df00 100644 --- a/test/functional/cms/content_block_controller_test.rb +++ b/test/functional/cms/content_block_controller_test.rb @@ -1,11 +1,11 @@ require 'test_helper' -class Cms::SampleBlocksController < Cms::ContentBlockController +class Dummy::SampleBlocksController < Cms::ContentBlockController end class PermissionsForContentBlockControllerTest < ActionController::TestCase include Cms::ControllerTestHelper - tests Cms::SampleBlocksController + tests Dummy::SampleBlocksController # We're stubbing a lot because we *just* want to isolate the behaviour for checking permissions def setup @@ -14,21 +14,20 @@ def setup @user = Cms::User.first @controller.stubs(:current_user).returns(@user) @controller.stubs(:render) - @controller.stubs(:model_class).returns(Cms::SampleBlock) + @controller.stubs(:model_class).returns(Dummy::SampleBlock) @controller.stubs(:set_default_category) - @controller.stubs(:blocks_path).returns("/cms/sample_block") - @controller.stubs(:block_path).returns("/cms/sample_block") + @controller.stubs(:engine_aware_path).returns("/cms/sample_block") @controller.stubs(:redirect_to_first).returns("/cms/sample_block") @block = stub_everything("block") - @block.stubs(:class).returns(Cms::SampleBlock) + @block.stubs(:class).returns(Dummy::SampleBlock) @block.stubs(:as_of_draft_version).returns(@block) @block.stubs(:as_of_version).returns(@block) @block.stubs(:connected_pages).returns(stub(:all => stub)) - Cms::SampleBlock.stubs(:find).returns(@block) - Cms::SampleBlock.stubs(:new).returns(@block) - Cms::SampleBlock.stubs(:paginate) + Dummy::SampleBlock.stubs(:find).returns(@block) + Dummy::SampleBlock.stubs(:new).returns(@block) + Dummy::SampleBlock.stubs(:paginate) end def expect_access_denied diff --git a/test/unit/helpers/path_helper_test.rb b/test/unit/helpers/path_helper_test.rb deleted file mode 100644 index d0bf9ba3f..000000000 --- a/test/unit/helpers/path_helper_test.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'test_helper' - -class Cms::PathHelperTest < ActionController::TestCase - - include Cms::PathHelper - include ActionDispatch::Routing::UrlFor - #include Rails.application.routes.url_helpers - #default_url_options[:host] = 'www.example.com' - - def setup - end - - def teardown - end - - # These tests are probably redundant now since we have Scenario coverage. - # However, it will be easier to merge forward with an editted file rather than a deleted one. - def test_edit_cms_connectable_path_for_portlets - portlet = DynamicPortlet.create(:name => "Testing Route generation") - expected_path = "/cms/portlets/#{portlet.id}/edit" - self.expects(:edit_portlet_path).with(portlet, {}).returns(expected_path) - - path = edit_cms_connectable_path(portlet) - - assert_equal(expected_path, path) - end - - def test_edit_cms_connectable_path_includes_options_for_portlets - portlet = DynamicPortlet.create(:name => "Testing Route generation") - expected_path = "/cms/portlets/#{portlet.id}/edit?_redirect_to=some_path" - self.expects(:edit_portlet_path).with(portlet, {:_redirect_to => "/some_path"}).returns(expected_path) - - path = edit_cms_connectable_path(portlet, :_redirect_to => "/some_path") - - assert_equal(expected_path, path) - end - -end diff --git a/test/unit/lib/cms/engine_helper_test.rb b/test/unit/lib/cms/engine_helper_test.rb index e2bb68a22..d8f3a231b 100644 --- a/test/unit/lib/cms/engine_helper_test.rb +++ b/test/unit/lib/cms/engine_helper_test.rb @@ -9,15 +9,21 @@ def self.connectable? false end - include EngineHelper + extend ::ActiveModel::Naming end end -class MainAppThing - include Cms::EngineHelper +module Dummy + class MainAppThing + extend ::ActiveModel::Naming + end +end + +class UnnamespacedBlock + extend ::ActiveModel::Naming end -class NewThing +class PortletSubclass < Cms::Portlet end @@ -25,17 +31,18 @@ module BcmsWidgets class Engine < Rails::Engine end class ContentBlock - include Cms::EngineHelper + extend ::ActiveModel::Naming end end class BcmsParts class ContentThing - include Cms::EngineHelper + extend ::ActiveModel::Naming end end module ExpectedMockViews + def view_for_cms_engine mock_view.expects(:cms).returns(:cms_engine) mock_view @@ -56,118 +63,79 @@ def mock_view @view = stub() end end + module Cms class EngineHelperTest < ActiveSupport::TestCase - def setup - @cms_block = Cms::CoreContentBlock.new - @main_app_block = MainAppThing.new + test "#module_name" do + assert_equal "Cms", EngineAware.module_name(Cms::CoreContentBlock) + assert_nil EngineAware.module_name(UnnamespacedBlock) + assert_equal "BcmsWidgets", EngineAware.module_name(BcmsWidgets::ContentBlock) end - test "main_app?" do - assert_equal true, @main_app_block.main_app_model? - assert_equal false, @cms_block.main_app_model? - assert_equal false, BcmsWidgets::ContentBlock.new.main_app_model? - end + end - test "If there is no Engine, engine_name should be the main app." do - assert_equal "main_app", BcmsParts::ContentThing.new.engine_name - end - test "Module Name" do - assert_equal "Cms", EngineHelper.module_name(Cms::CoreContentBlock) - assert_nil EngineHelper.module_name(NewThing) - assert_equal "BcmsWidgets", EngineHelper.module_name(BcmsWidgets::ContentBlock) - end + class ContentBlocksEnginePathsTest < ActiveSupport::TestCase + include ExpectedMockViews - test "path_for_widget" do - name = BcmsWidgets::ContentBlock.new.engine_name - assert_not_nil name - assert_equal BcmsWidgets::Engine.engine_name, name + def setup + @pathbuilder = EngineAwarePathBuilder.new(Cms::CoreContentBlock) end - test "Decorate" do - n = NewThing.new - EngineHelper.decorate(n) - assert_equal true, n.respond_to?(:engine_name) + test "subject_class" do + assert_equal Cms::CoreContentBlock, @pathbuilder.subject_class end - test "Decorate class" do - EngineHelper.decorate(NewThing) - assert_equal true, NewThing.respond_to?(:engine_name) + test "#engine for applications" do + assert_equal Rails.application, path_builder(Dummy::MainAppThing).engine_class end - test "Don't decorate twice'" do - class DecorateOnce - include EngineHelper - - def engine_name - "Original" - end - end - subject = DecorateOnce.new - EngineHelper.decorate(subject) - assert_equal "Original", subject.engine_name - - end - test "Engine Name" do - assert_equal "cms", Cms::Engine.engine_name + test "#engine for core Cms types" do + assert_equal Cms::Engine, path_builder(Cms::CoreContentBlock).engine_class end - test "calculate engine_name" do - assert_equal "cms", Cms::CoreContentBlock.new.engine_name + test "#engine for Engines" do + assert_equal BcmsWidgets::Engine, path_builder(BcmsWidgets::ContentBlock).engine_class end - test "Blocks without namespace should be in main app" do - assert_equal "main_app", MainAppThing.new.engine_name + test "#engine_name" do + assert_equal "cms", path_builder(Cms::CoreContentBlock).engine_name end - test "path_elements for an instance of a class in an application" do - assert_equal ["cms", @main_app_block], @main_app_block.path_elements + test "#engine_name for models with no matching engine" do + assert_equal "main_app", path_builder(BcmsParts::ContentThing).engine_name end - test "path_elements for a class in an application" do - MainAppThing.extend EngineHelper - assert_equal ["cms", MainAppThing], MainAppThing.path_elements + test "#engine_name for models from engine engine" do + assert_equal BcmsWidgets::Engine.engine_name, path_builder(BcmsWidgets::ContentBlock).engine_name end - test "path_elements for an instance of in Cms namespace" do - assert_equal [@cms_block], @cms_block.path_elements + test "#engine_name for classes that support PolymorphicSTI" do + assert_equal "cms", path_builder(PortletSubclass).engine_name end - test "path_elements for a class in Cms namespace" do - Cms::CoreContentBlock.extend EngineHelper - assert_equal [Cms::CoreContentBlock], Cms::CoreContentBlock.path_elements + test "#engine_name for unnamespaced blocks" do + assert_equal "main_app", path_builder(UnnamespacedBlock).engine_name end - test "path_elements for a class in a module" do - BcmsWidgets::ContentBlock.extend EngineHelper - assert_equal [BcmsWidgets::ContentBlock], BcmsWidgets::ContentBlock.path_elements + test "#main_app?" do + assert_equal true, path_builder(Dummy::MainAppThing).main_app_model? + assert_equal false, path_builder(Cms::CoreContentBlock).main_app_model? + assert_equal false, path_builder(BcmsWidgets::ContentBlock).main_app_model? end - end - - - class ContentBlocksEnginePathsTest < ActiveSupport::TestCase - include ExpectedMockViews - - def setup - @pathbuilder = EngineAwarePathBuilder.new(Cms::CoreContentBlock) + test "build for Core Cms class" do + assert_equal [:cms_engine, Cms::CoreContentBlock], @pathbuilder.build(view_for_cms_engine) end - test "subject_class" do - assert_equal Cms::CoreContentBlock, @pathbuilder.subject_class - end - test "engine_name" do - assert_equal "cms", @pathbuilder.engine_name - end + private - test "build for Core Cms class" do - assert_equal [:cms_engine, Cms::CoreContentBlock], @pathbuilder.build(view_for_cms_engine) + def path_builder(klass) + EngineAwarePathBuilder.new(klass) end - end class PathsWithContentTypeTest < ActiveSupport::TestCase @@ -186,8 +154,8 @@ def setup class EngineAwarePathsTest < ActiveSupport::TestCase include ExpectedMockViews test "build for custom block class" do - pathbuilder = EngineAwarePathBuilder.new(MainAppThing) - assert_equal [:main_app, "cms", MainAppThing], pathbuilder.build(view_for_main_app) + pathbuilder = EngineAwarePathBuilder.new(Dummy::MainAppThing) + assert_equal [:main_app, Dummy::MainAppThing], pathbuilder.build(view_for_main_app) end test "paths for block from a module" do diff --git a/test/unit/lib/routes_test.rb b/test/unit/lib/routes_extensions_test.rb similarity index 95% rename from test/unit/lib/routes_test.rb rename to test/unit/lib/routes_extensions_test.rb index 2f2c26a61..c1e8d1b40 100644 --- a/test/unit/lib/routes_test.rb +++ b/test/unit/lib/routes_extensions_test.rb @@ -41,7 +41,7 @@ def setup # Expect rb.expects(:resources).with(:bears) - rb.expects(:get).with('/bears/:id/version/:version', {:to => 'bears#version', :as => :version_cms_bears}) + rb.expects(:get).with('/bears/:id/version/:version', {:to => 'bears#version', :as => 'bears_version'}) rb.expects(:put) rb.content_blocks :bears @@ -52,7 +52,7 @@ def setup test "model names with s at the end behave identically (since content_blocks expects plural symbols)" do rb = RouteBuilder.new rb.expects(:resources).with(:kindnesses) - rb.expects(:get).with('/kindnesses/:id/version/:version', {:to => 'kindnesses#version', :as => :version_cms_kindnesses}) + rb.expects(:get).with('/kindnesses/:id/version/:version', {:to => 'kindnesses#version', :as => 'kindnesses_version'}) rb.expects(:put) rb.content_blocks :kindnesses diff --git a/test/unit/models/content_type_test.rb b/test/unit/models/content_type_test.rb index 21ed16c84..ed70ae078 100644 --- a/test/unit/models/content_type_test.rb +++ b/test/unit/models/content_type_test.rb @@ -27,6 +27,12 @@ class Unnamespaced < ActiveRecord::Base acts_as_content_block end +module Dummy + class Widget < ActiveRecord::Base + acts_as_content_block + end +end + class Widget < ActiveRecord::Base acts_as_content_block end @@ -55,19 +61,14 @@ class ContentTypeTest < ActiveSupport::TestCase assert_equal AudioTour, Cms::ContentType.find_by_key('AudioTour').model_class end - test "#find_by_key searches Cms:: then non Cms:: namespace class" do - assert_equal Product, Cms::ContentType.find_by_key('Cms::Product').model_class + test "#find_by_key finds namespaced models" do + assert_equal Dummy::Product, Cms::ContentType.find_by_key('Dummy::Product').model_class end test "#find_by_key using key" do assert_equal Cms::HtmlBlock, Cms::ContentType.find_by_key('html_block').model_class end - - test "#key" do - assert_equal "really_long_name_class", long_name_content_type.key - end - test "#display_name for blocks from modules" do assert_equal "Widget", Cms::ContentType.new(:name => "BcmsStore::Widget").display_name assert_equal "Widget", BcmsStore::Widget.display_name @@ -77,13 +78,13 @@ class ContentTypeTest < ActiveSupport::TestCase assert_equal "String", Cms::ContentType.new(:name => "String").display_name end - test "#form for unnamespaced blocks" do - widget_type = Cms::ContentType.new(:name => "Widget") - assert_equal "cms/widgets/form", widget_type.form + test "#form for Core modules" do + widget_type = Cms::ContentType.new(:name => "Dummy::Widget") + assert_equal "dummy/widgets/form", widget_type.form end test "template_path" do - assert_equal "cms/widgets/render", Widget.template_path + assert_equal "dummy/widgets/render", Dummy::Widget.template_path end test "template_path for modules" do @@ -94,36 +95,8 @@ class ContentTypeTest < ActiveSupport::TestCase assert_equal Unnamespaced, Cms::ContentType.find_by_key("Unnamespaced").model_class end - test "model_resource_name" do - assert_equal "really_long_name_class", long_name_content_type().model_class_form_name - end - - test "Project specific routes should be still be namespaced under cms_" do - assert_equal "main_app.cms_unnamespaced", Unnamespaced.content_type.route_name - end - - test "route_name removes cms_ as prefix (no longer needed for engines)" do - content_type = Cms::NamespacedBlock.content_type - assert_equal "namespaced_block", content_type.route_name - end - - test "engine_for using Class" do - EngineHelper.decorate(Unnamespaced) - assert_equal "main_app", Unnamespaced.engine_name - end - - test "engine_name for Cms engine" do - cms_namespace = Cms::ContentType.new(:name => "Cms::NamespacedBlock") - assert_equal "cms", cms_namespace.engine_name - end - - test "path_elements for Cms engine" do - cms_namespace = Cms::ContentType.new(:name => "Cms::NamespacedBlock") - assert_equal [Cms::NamespacedBlock], cms_namespace.path_elements - end - - test "path_elements for an app ContentType" do - assert_equal ["cms", Unnamespaced], unnamespaced_type().path_elements + test "#param_key" do + assert_equal "really_long_name_class", long_name_content_type.param_key end def test_model_class @@ -156,7 +129,6 @@ def test_content_block_type test "Form for Blocks with Engines" do engine_type = Cms::ContentType.new(:name => "BcmsStore::Widget") - assert_equal true, engine_type.engine_exists? assert_equal "bcms_store/widgets/form", engine_type.form end @@ -173,6 +145,41 @@ def unnamespaced_type Unnamespaced.content_type end end + + class EngineAwareMethodsTest < ActiveSupport::TestCase + + + test "#path_builder" do + assert_equal EngineAwarePathBuilder, path_builder.class + assert_equal Dummy::Widget, path_builder.subject_class + end + + test "#engine_class" do + path_builder.expects(:engine_class).returns(:expected_value) + assert_equal :expected_value, content_type.engine_class + end + + test "#engine_name" do + path_builder.expects(:engine_name).returns(:expected_value) + assert_equal :expected_value, content_type.engine_name + end + + test "#main_app_model?" do + path_builder.expects(:main_app_model?).returns(:expected_value) + assert_equal :expected_value, content_type.main_app_model? + end + + private + + def path_builder + @path_builder ||= content_type.path_builder + end + + def content_type + @content_type ||= Dummy::Widget.content_type + end + + end end # For testing find_by_key with pluralization diff --git a/test/unit/models/dynamic_views_test.rb b/test/unit/models/dynamic_views_test.rb index e4c937090..5cfdea0f7 100644 --- a/test/unit/models/dynamic_views_test.rb +++ b/test/unit/models/dynamic_views_test.rb @@ -15,27 +15,23 @@ def teardown end test "#form_name" do - assert_equal "cms_page_template", Cms::PageTemplate.form_name + assert_equal "cms_page_template", Cms::PageTemplate.model_name.singular end test "version_foreign_key" do assert_equal :original_record_id, Cms::PageTemplate.version_foreign_key end - test "resource_name works for non-namespaced templates" do - assert_equal "basic_templates", BasicTemplate.resource_name - end + test "#route_key" do + assert_equal "basic_templates", BasicTemplate.model_name.route_key + assert_equal "page_templates", Cms::PageTemplate.model_name.route_key + assert_equal "page_partials", Cms::PagePartial.model_name.route_key - test "Engine" do - assert_equal "cms", Cms::DynamicView.engine - assert_equal "cms", Cms::PageTemplate.engine - assert_equal "cms", Cms::PagePartial.engine end - test "path_elements" do - assert_equal [Cms::PageTemplate], Cms::PageTemplate.path_elements - assert_equal [Cms::PagePartial], Cms::PagePartial.path_elements - + test "#resource_collection_name" do + assert_equal "page_template", Cms::PageTemplate.model_name.param_key + assert_equal "page_partial", Cms::PagePartial.model_name.param_key end test "Display Name" do diff --git a/test/unit/models/html_block_test.rb b/test/unit/models/html_block_test.rb index b5b0008a0..b8114cef3 100644 --- a/test/unit/models/html_block_test.rb +++ b/test/unit/models/html_block_test.rb @@ -2,6 +2,9 @@ class HtmlBlockTest < ActiveSupport::TestCase + test "#model_form_name" do + assert_equal "html_block", Cms::HtmlBlock.content_type.param_key + end test "#paginate" do red = create(:html_block, name: 'red') blue = create(:html_block, name: 'blue') diff --git a/test/unit/models/page_partial_test.rb b/test/unit/models/page_partial_test.rb index bf6dded73..1809fec63 100644 --- a/test/unit/models/page_partial_test.rb +++ b/test/unit/models/page_partial_test.rb @@ -19,13 +19,6 @@ def teardown Cms::PagePartial.new(:name=>"A", :format=>"B", :handler=>"C", :body=>"D") Cms::PageTemplate.new(:name=>"A", :format=>"B", :handler=>"C", :body=>"D") end - test "Name used to build the form" do - assert_equal "page_partial", Cms::PagePartial.resource_collection_name - end - - test "resource_name works for namespaced templates" do - assert_equal "page_partials", Cms::PagePartial.resource_name - end test "create" do @page_partial.save! diff --git a/test/unit/models/page_route_test.rb b/test/unit/models/page_route_test.rb index 4e50c7894..0f768fca2 100644 --- a/test/unit/models/page_route_test.rb +++ b/test/unit/models/page_route_test.rb @@ -17,6 +17,8 @@ def test_create route.add_requirement(:day, "\\d{2,}") route.add_condition(:method, "get") + route.expects(:reload_routes) # Avoid actual route reloading + assert route.save! assert_equal "/things/:year/:month/:day", route.pattern assert_equal({ diff --git a/test/unit/models/page_template_test.rb b/test/unit/models/page_template_test.rb index d4418c3fb..a6fd70e6e 100644 --- a/test/unit/models/page_template_test.rb +++ b/test/unit/models/page_template_test.rb @@ -10,16 +10,6 @@ def teardown File.delete(@page_template.file_path) if File.exists?(@page_template.file_path) end - - test "Name used to build the form" do - assert_equal "page_template", Cms::PageTemplate.resource_collection_name - end - - test "resource_name works for namespaced templates" do - assert_equal "page_templates", Cms::PageTemplate.resource_name - - end - def test_create_and_destroy assert !File.exists?(@page_template.file_path), "template file already exists" assert_valid @page_template diff --git a/test/unit/models/portlet_test.rb b/test/unit/models/portlet_test.rb index bc4bb17a8..0a874a4bd 100644 --- a/test/unit/models/portlet_test.rb +++ b/test/unit/models/portlet_test.rb @@ -22,6 +22,29 @@ class EditablePortlet < Cms::Portlet end +class PortletPolymorphismTest < ActiveSupport::TestCase + + test ".route_key for Portlet base class" do + assert_equal "portlets", Cms::Portlet.model_name.route_key + end + + test ".route_key" do + assert_equal "portlets", InlinePortlet.model_name.route_key + end + + test ".singular" do + assert_equal "cms_portlet", InlinePortlet.model_name.singular + end + + test ".plural" do + assert_equal "cms_portlets", InlinePortlet.model_name.plural + end + + test ".model_name should return Cms::Portlet" do + assert_equal Cms::Portlet.model_name, InlinePortlet.model_name + end +end + class PortletTest < ActiveSupport::TestCase def setup @@ -86,8 +109,8 @@ def test_portlets_consistently_load_the_same_number_of_types list = Cms::Portlet.types assert list.size > 0 - DynamicPortlet.create!(:name=>"test 1") - DynamicPortlet.create!(:name=>"test 2") + DynamicPortlet.create!(:name => "test 1") + DynamicPortlet.create!(:name => "test 2") assert_equal list.size, Cms::Portlet.types.size end diff --git a/test/unit/models/sections_test.rb b/test/unit/models/sections_test.rb index 4afce62eb..6764ef9de 100644 --- a/test/unit/models/sections_test.rb +++ b/test/unit/models/sections_test.rb @@ -281,7 +281,7 @@ def setup end test "#sitemap should include addressable content blocks" do - product = Product.create!(name: "Hello", parent: root_section) + product = Dummy::Product.create!(name: "Hello", parent: root_section) assert child_nodes_in(root_section).include?(product), "Verify product is in root section" assert content_in_root_section.include?(product), "Verify it doesn't get filtered out when returned by sitemap'" end diff --git a/test/unit/models/task_test.rb b/test/unit/models/task_test.rb index e7969fe0e..b83a81283 100644 --- a/test/unit/models/task_test.rb +++ b/test/unit/models/task_test.rb @@ -32,12 +32,12 @@ def test_create_task end test "Assign task sends email" do - Rails.configuration.cms.expects(:site_domain).returns("www.browsercms.org") + Rails.configuration.cms.expects(:site_domain).returns("www.browsercms.org").at_least_once create_the_task! email = Cms::EmailMessage.order('id asc').first - assert_equal @editor_a.email, email.sender + assert_equal Cms::EmailMessage.mailbot_address, email.sender assert_equal @editor_b.email, email.recipients assert_equal "Page '#{@page.name}' has been assigned to you", email.subject assert_equal "http://cms.browsercms.org#{@page.path}\n\n#{@task.comment}", email.body