diff --git a/app/controllers/admin/content_types_controller.rb b/app/controllers/admin/content_types_controller.rb index 5ed3eabd..5b6f7ac5 100644 --- a/app/controllers/admin/content_types_controller.rb +++ b/app/controllers/admin/content_types_controller.rb @@ -34,7 +34,7 @@ module Admin render :action => "edit" end end - + def destroy @content_type = current_site.content_types.find(params[:id]) diff --git a/app/controllers/admin/contents_controller.rb b/app/controllers/admin/contents_controller.rb index cb2b99fa..c4e805cf 100644 --- a/app/controllers/admin/contents_controller.rb +++ b/app/controllers/admin/contents_controller.rb @@ -18,7 +18,7 @@ module Admin end def create - @content = @content_type.contents.build(params[:content]) + @content = @content_type.contents.build(params[:content_instance]) if @content.save flash_success! @@ -32,7 +32,7 @@ module Admin def update @content = @content_type.contents.find(params[:id]) - if @content.update_attributes(params[:content]) + if @content.update_attributes(params[:content_instance]) flash_success! redirect_to edit_admin_content_url(@content_type.slug, @content) else @@ -40,6 +40,10 @@ module Admin render :action => "edit" end end + + def sort + + end def destroy @content = @content_type.contents.find(params[:id]) diff --git a/app/helpers/admin/custom_fields_helper.rb b/app/helpers/admin/custom_fields_helper.rb index 5c27646e..240405ee 100644 --- a/app/helpers/admin/custom_fields_helper.rb +++ b/app/helpers/admin/custom_fields_helper.rb @@ -1,13 +1,5 @@ module Admin::CustomFieldsHelper - - # def options_for_field_kind(selected = nil) - # # %w{String Text Boolean Email File Date} - # options = %w{String Text}.map do |kind| - # [t("admin.custom_fields.kind.#{kind.downcase}"), kind] - # end - # options_for_select(options, selected) - # end - + def options_for_field_kind(selected = nil) # %w{String Text Boolean Email File Date} options = %w{String Text}.map do |kind| @@ -15,4 +7,18 @@ module Admin::CustomFieldsHelper end end + def options_for_order_by(content_type, collection_name) + options = %w{updated_at position}.map do |type| + [t("admin.content_types.form.order_by.#{type.gsub(/^_/, '')}"), type] + end + options + options_for_highlighted_field(content_type, collection_name) + end + + def options_for_highlighted_field(content_type, collection_name) + custom_fields_collection_name = "ordered_#{collection_name.singularize}_custom_fields".to_sym + collection = content_type.send(custom_fields_collection_name) + collection.delete_if { |f| f.label == 'field name' } + collection.map { |field| [field.label, field._name] } + end + end \ No newline at end of file diff --git a/app/models/content_instance.rb b/app/models/content_instance.rb index baf4e682..c6c5472b 100644 --- a/app/models/content_instance.rb +++ b/app/models/content_instance.rb @@ -2,7 +2,24 @@ class ContentInstance include Mongoid::Document include Mongoid::Timestamps - # fields ## - field :name + ## fields (dynamic fields) ## + field :position, :type => Integer, :default => 0 + + ## validations ## + validate :require_highlighted_field + + ## associations ## + embedded_in :content_type, :inverse_of => :contents + + ## methods ## + + protected + + def require_highlighted_field + _alias = self.content_type.highlighted_field._alias.to_sym + if self.send(_alias).blank? + self.errors.add(_alias, :blank) + end + end end \ No newline at end of file diff --git a/app/models/content_type.rb b/app/models/content_type.rb index 0b9189eb..946361e5 100644 --- a/app/models/content_type.rb +++ b/app/models/content_type.rb @@ -1,13 +1,14 @@ class ContentType include Mongoid::Document include Mongoid::Timestamps - # include Mongoid::CustomFields + include Mongoid::CustomFields ## fields ## field :name field :description field :slug field :order_by + field :highlighted_field_name ## associations ## belongs_to_related :site @@ -21,10 +22,26 @@ class ContentType validates_uniqueness_of :slug, :scope => :site ## behaviours ## - # custom_fields_for :contents + custom_fields_for :contents ## methods ## + def ordered_contents + self.contents.sort { |a, b| (a.position || 0) <=> (b.position || 0) } + end + + def contents_order + self.ordered_contents.collect(&:id).join(',') + end + + def contents_order=(order) + @contents_order = order + end + + def highlighted_field + self.content_custom_fields.detect { |f| f._name == self.highlighted_field_name } + end + protected def normalize_slug diff --git a/app/views/admin/asset_collections/edit.html.haml b/app/views/admin/asset_collections/edit.html.haml index 032ffb98..cb145112 100644 --- a/app/views/admin/asset_collections/edit.html.haml +++ b/app/views/admin/asset_collections/edit.html.haml @@ -1,7 +1,7 @@ - title link_to(@collection.name.blank? ? @collection.name_was : @collection.name, '#', :rel => 'asset_collection_name', :title => t('.ask_for_name'), :class => 'editable') - content_for :head do - = javascript_include_tag 'admin/asset_collections.js', 'admin/custom_fields' + = javascript_include_tag 'admin/asset_collections', 'admin/custom_fields' - content_for :submenu do = render 'admin/shared/menu/assets' @@ -25,6 +25,6 @@ = f.input :name = f.input :slug, :required => false - = render 'custom_fields', :f => f + = render 'admin/shared/custom_fields', :f => f, :collection_name => 'assets' = render 'admin/shared/form_actions', :delete_button => link_to(content_tag(:span, t('.destroy')), admin_asset_collection_url(@collection), :confirm => t('admin.messages.confirm'), :method => :delete, :class => 'button small remove'), :button_label => :update \ No newline at end of file diff --git a/app/views/admin/content_types/_form.html.haml b/app/views/admin/content_types/_form.html.haml index 7c62ece7..ddf2b629 100644 --- a/app/views/admin/content_types/_form.html.haml +++ b/app/views/admin/content_types/_form.html.haml @@ -1,4 +1,13 @@ +- content_for :head do + = javascript_include_tag 'admin/custom_fields' + = f.inputs :name => :information do = f.input :name - = f.input :slug, :required => false - = f.input :description, :as => 'text' \ No newline at end of file + = f.input :slug + = f.input :description + += render 'admin/shared/custom_fields', :f => f, :collection_name => 'contents' + += f.foldable_inputs :name => :options, :class => 'switchable' do + = f.input :highlighted_field_name, :as => :select, :collection => options_for_highlighted_field(f.object, 'contents'), :include_blank => false + = f.input :order_by, :as => :select, :collection => options_for_order_by(f.object, 'contents'), :include_blank => false \ No newline at end of file diff --git a/app/views/admin/contents/_form.html.haml b/app/views/admin/contents/_form.html.haml new file mode 100644 index 00000000..1015c6f0 --- /dev/null +++ b/app/views/admin/contents/_form.html.haml @@ -0,0 +1,10 @@ +- highlighted_field_name = @content.content_type.highlighted_field_name + += f.inputs :name => :other_fields do + - @content.custom_fields.each do |field| + - required = highlighted_field_name == field._name + + - if field.string? + = f.input field._alias.to_sym, :label => field.label, :required => required + - elsif field.text? + = f.input field._alias.to_sym, :label => field.label, :as => :text, :required => required \ No newline at end of file diff --git a/app/views/admin/contents/edit.html.haml b/app/views/admin/contents/edit.html.haml new file mode 100644 index 00000000..c0526753 --- /dev/null +++ b/app/views/admin/contents/edit.html.haml @@ -0,0 +1,15 @@ +- title t('.title', :type => @content_type.name.capitalize) + +- content_for :submenu do + = render 'admin/shared/menu/contents' + +- content_for :buttons do + = admin_button_tag t('admin.contents.index.edit'), edit_admin_content_type_url(@content_type), :class => 'edit' + +%p= @content_type.description + += semantic_form_for @content, :url => admin_content_url(@content_type.slug, @content), :html => { :multipart => true } do |form| + + = render 'form', :f => form + + = render 'admin/shared/form_actions', :back_url => admin_contents_url(@content_type.slug), :button_label => :update \ No newline at end of file diff --git a/app/views/admin/contents/index.html.haml b/app/views/admin/contents/index.html.haml index aca89109..be2f2c84 100644 --- a/app/views/admin/contents/index.html.haml +++ b/app/views/admin/contents/index.html.haml @@ -1,8 +1,11 @@ -- title t('.title', :type => @content_type.name) +- title t('.title', :type => @content_type.name.capitalize) - content_for :submenu do = render 'admin/shared/menu/contents' - + +- content_for :head do + = javascript_include_tag 'admin/contents' + - content_for :buttons do = admin_button_tag :edit, edit_admin_content_type_url(@content_type), :class => 'edit' = admin_button_tag :download, '#', :class => 'download' @@ -14,4 +17,21 @@ - if @contents.empty? %p.no-items= t('.no_items', :url => new_admin_content_url(@content_type.slug)) - else - foo bar \ No newline at end of file + %ul{ :id => 'contents-list', :class => "list #{'sortable' if @content_type.order_by == 'position'}" } + - @contents.each do |content| + %li.content{ :id => "content-#{content._id}" } + - if @content_type.order_by == 'position' + %em + %strong + = link_to content.send(@content_type.highlighted_field_name), edit_admin_content_path(@content_type.slug, content) + .more + %span + = t('admin.contents.index.updated_at') + = l content.updated_at, :format => :short rescue 'n/a' + + = link_to image_tag('admin/list/icons/trash.png'), admin_content_path(@content_type.slug, content), :class => 'remove', :confirm => t('admin.messages.confirm'), :method => :delete + += form_tag sort_admin_contents_path(@content_type.slug), :method => :put, :class => 'formtastic' do + = hidden_field_tag :order + + = render 'admin/shared/form_actions', :delete_button => link_to(content_tag(:span, t('.destroy')), admin_content_type_url(@content_type), :confirm => t('admin.messages.confirm'), :method => :delete, :class => 'button small remove'), :button_label => :update \ No newline at end of file diff --git a/app/views/admin/contents/new.html.haml b/app/views/admin/contents/new.html.haml new file mode 100644 index 00000000..5a6980ce --- /dev/null +++ b/app/views/admin/contents/new.html.haml @@ -0,0 +1,15 @@ +- title t('.title', :type => @content_type.name.capitalize) + +- content_for :submenu do + = render 'admin/shared/menu/contents' + +- content_for :buttons do + = admin_button_tag t('admin.contents.index.edit'), edit_admin_content_type_url(@content_type), :class => 'edit' + +%p= @content_type.description + += semantic_form_for @content, :url => admin_contents_url(@content_type.slug), :html => { :multipart => true } do |form| + + = render 'form', :f => form + + = render 'admin/shared/form_actions', :back_url => admin_contents_url(@content_type.slug), :button_label => :create \ No newline at end of file diff --git a/app/views/admin/asset_collections/_custom_fields.html.haml b/app/views/admin/shared/_custom_fields.html.haml similarity index 71% rename from app/views/admin/asset_collections/_custom_fields.html.haml rename to app/views/admin/shared/_custom_fields.html.haml index 156c8737..b30b77ba 100644 --- a/app/views/admin/asset_collections/_custom_fields.html.haml +++ b/app/views/admin/shared/_custom_fields.html.haml @@ -1,6 +1,10 @@ +- collection_name = "#{collection_name.singularize}_custom_fields" +- custom_fields = f.object.send(collection_name.to_sym) +- ordered_custom_fields = f.object.send(:"ordered_#{collection_name}") + = f.foldable_inputs :name => :custom_fields, :class => 'editable-list fields' do - - f.object.ordered_asset_custom_fields.each do |field| - = f.fields_for :asset_custom_fields, field, :child_index => field._index do |g| + - ordered_custom_fields.each do |field| + = f.fields_for collection_name.to_sym, field, :child_index => field._index do |g| %li{ :class => "item added #{'error' unless field.errors.empty?}"} %span.handle = image_tag 'admin/form/icons/drag.png' @@ -20,7 +24,7 @@ %span.actions = link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm') - = f.fields_for :asset_custom_fields, @collection.asset_custom_fields.build(:label => 'field name'), :child_index => '-1' do |g| + = f.fields_for collection_name.to_sym, custom_fields.build(:label => 'field name'), :child_index => '-1' do |g| %li{ :class => 'item template' } %span.handle = image_tag 'admin/form/icons/drag.png' diff --git a/config/locales/admin_ui_en.yml b/config/locales/admin_ui_en.yml index 577fa4d6..7e7f298b 100644 --- a/config/locales/admin_ui_en.yml +++ b/config/locales/admin_ui_en.yml @@ -150,6 +150,10 @@ en: help: "Your model should have one field at least. The items created from this content type would have their first field mandatory." show_items: show items new_item: new item + form: + order_by: + updated_at: 'By "updated at" date' + position: Manually contents: index: @@ -158,6 +162,11 @@ en: download: download items new: new item no_items: "There are no items for now. Just click here to create the first one." + new: + title: '{{type}} — new item' + edit: + title: '{{type}} — editing item' + formtastic: titles: diff --git a/config/routes.rb b/config/routes.rb index b0abb972..983b2bd9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,8 +45,9 @@ Locomotive::Application.routes.draw do |map| resources :content_types - resources :contents, :path => "content_types/:slug/contents" - + resources :contents, :path => "content_types/:slug/contents" do + put :sort, :on => :collection + end end # magic urls diff --git a/doc/TODO b/doc/TODO index 4d94a524..652fda7e 100644 --- a/doc/TODO +++ b/doc/TODO @@ -1,6 +1,9 @@ BOARD: - content types / models (CRUD) + - require a custom field at least + - pre-select the first custom field as the highlighted one - contents (CRUD) + - sort contents BACKLOG: - liquid rendering engine diff --git a/public/javascripts/admin/contents.js b/public/javascripts/admin/contents.js new file mode 100644 index 00000000..e3d463e7 --- /dev/null +++ b/public/javascripts/admin/contents.js @@ -0,0 +1,16 @@ +$(document).ready(function() { + + var updateContentsOrder = function() { + var list = $('ul#contents-list.sortable'); + var ids = jQuery.map(list.sortable('toArray'), function(e) { + return e.match(/content-(\w+)/)[1]; + }).join(','); + $('#order').val(ids || ''); + } + + $('ul#contents-list.sortable').sortable({ + items: 'li.content', + stop: function(event, ui) { updateContentsOrder(); } + }); + +}); \ No newline at end of file diff --git a/public/stylesheets/admin/application.css b/public/stylesheets/admin/application.css index 3824c024..fd96afea 100644 --- a/public/stylesheets/admin/application.css +++ b/public/stylesheets/admin/application.css @@ -54,6 +54,14 @@ ul.list li { background: transparent url(/images/admin/list/item.png) no-repeat 0 0; } +ul.list li em { + display: block; + float: left; + background: transparent url(/images/admin/list/item-left.png) no-repeat left 0; + height: 31px; + width: 18px; +} + ul.list li strong a { position: relative; top: 2px; @@ -151,6 +159,22 @@ div#asset-uploader { display: inline-block; margin-left: 10px; } div#asset-uploader span.spinner { position: relative; top: -3px; display: none; } div#uploadAssetsInputQueue { display: none; } +/* ___ contents ___ */ + +#contents-list li { background: none; } + +#contents-list li em { + background-position: left -31px; + cursor: move; +} + +#contents-list li strong { + margin-left: 18px; + display: block; + height: 31px; + background: transparent url(/images/admin/list/item-right.png) no-repeat right 0; +} + /* ___ pages ___ */ diff --git a/spec/models/content_instance_spec.rb b/spec/models/content_instance_spec.rb new file mode 100644 index 00000000..82c7365d --- /dev/null +++ b/spec/models/content_instance_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe ContentInstance do + + before(:each) do + Site.any_instance.stubs(:create_default_pages!).returns(true) + @content_type = Factory.build(:content_type) + @content_type.content_custom_fields.build :label => 'Title', :kind => 'String' + @content_type.content_custom_fields.build :label => 'Description', :kind => 'Text' + @content_type.highlighted_field_name = 'custom_field_1' + end + + context 'when validating' do + + it 'should be valid' do + build_content.should be_valid + end + + # Validations ## + + it 'should validate presence of title' do + content = build_content :title => nil + content.should_not be_valid + content.errors[:title].should == ["can't be blank"] + end + + end + + def build_content(options = {}) + @content_type.contents.build({ :title => 'Locomotive', :description => 'Lorem ipsum....' }.merge(options)) + end + +end \ No newline at end of file diff --git a/spec/models/content_type_spec.rb b/spec/models/content_type_spec.rb index e6d4f804..85547e02 100644 --- a/spec/models/content_type_spec.rb +++ b/spec/models/content_type_spec.rb @@ -6,30 +6,34 @@ describe ContentType do Site.any_instance.stubs(:create_default_pages!).returns(true) end - it 'should have a valid factory' do - Factory.build(:content_type).should be_valid - end - - # Validations ## - - %w{site name}.each do |field| - it "should validate presence of #{field}" do - content_type = Factory.build(:content_type, field.to_sym => nil) - content_type.should_not be_valid - content_type.errors[field.to_sym].should == ["can't be blank"] + context 'when validating' do + + it 'should have a valid factory' do + Factory.build(:content_type).should be_valid end - end - it 'should validate presence of slug' do - content_type = Factory.build(:content_type, :name => nil, :slug => nil) - content_type.should_not be_valid - content_type.errors[:slug].should == ["can't be blank"] - end + # Validations ## - it 'should validate uniqueness of slug' do - content_type = Factory(:content_type) - (content_type = Factory.build(:content_type, :site => content_type.site)).should_not be_valid - content_type.errors[:slug].should == ["is already taken"] + %w{site name}.each do |field| + it "should validate presence of #{field}" do + content_type = Factory.build(:content_type, field.to_sym => nil) + content_type.should_not be_valid + content_type.errors[field.to_sym].should == ["can't be blank"] + end + end + + it 'should validate presence of slug' do + content_type = Factory.build(:content_type, :name => nil, :slug => nil) + content_type.should_not be_valid + content_type.errors[:slug].should == ["can't be blank"] + end + + it 'should validate uniqueness of slug' do + content_type = Factory(:content_type) + (content_type = Factory.build(:content_type, :site => content_type.site)).should_not be_valid + content_type.errors[:slug].should == ["is already taken"] + end + end end \ No newline at end of file diff --git a/vendor/plugins/custom_fields/lib/custom_fields/extensions/mongoid/document.rb b/vendor/plugins/custom_fields/lib/custom_fields/extensions/mongoid/document.rb index 72a37253..50e94ac5 100644 --- a/vendor/plugins/custom_fields/lib/custom_fields/extensions/mongoid/document.rb +++ b/vendor/plugins/custom_fields/lib/custom_fields/extensions/mongoid/document.rb @@ -6,6 +6,7 @@ module Mongoid #:nodoc: parentize_without_custom_fields(object, association_name) if self.custom_fields?(object, association_name) + puts "[parentize_with_custom_fields] association_name = #{association_name} / #{self.custom_fields_association_name(association_name)}" self.class.send(:define_method, :custom_fields) do fields = object.send(self.custom_fields_association_name(association_name)) fields.sort { |a, b| (a.position || 0) <=> (b.position || 0) }