diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..53607ea5 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--colour diff --git a/Gemfile b/Gemfile index dd90b913..78d8385b 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source :rubygems gem 'rake', '0.8.7' -gem 'rails', '>= 3.0.8' +gem 'rails', '3.0.9' gem 'warden' gem 'devise', '1.3.4' @@ -21,9 +21,12 @@ gem 'formtastic', '~> 1.2.3' gem 'inherited_resources', '~> 1.1.2' gem 'rmagick', '2.12.2' -gem 'locomotive_carrierwave', '0.5.4.beta2' +gem 'locomotive_carrierwave', '0.5.4.beta3' +gem 'dragonfly', '~> 0.9.1' +gem 'rack-cache', :require => 'rack/cache' -gem 'custom_fields', '1.0.0.beta.18' +gem 'custom_fields', '1.0.0.beta.19' +gem 'cancan' gem 'fog', '0.8.2' gem 'mimetype-fu' gem 'actionmailer-with-request', :require => 'actionmailer_with_request' diff --git a/Gemfile.lock b/Gemfile.lock index 1651565e..36523e33 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -62,6 +62,7 @@ GEM highline (>= 1.6.1) json (>= 1.4.6) rest-client (>= 1.6.1) + cancan (1.6.5) capybara (0.4.1.2) celerity (>= 0.7.9) culerity (>= 0.2.4) @@ -86,7 +87,7 @@ GEM cucumber-rails (0.3.2) cucumber (>= 0.8.0) culerity (0.2.15) - custom_fields (1.0.0.beta.18) + custom_fields (1.0.0.beta.19) activesupport (>= 3.0.7) mongoid (= 2.0.2) daemons (1.1.4) @@ -102,6 +103,8 @@ GEM orm_adapter (~> 0.0.3) warden (~> 1.0.3) diff-lcs (1.1.2) + dragonfly (0.9.4) + rack erubis (2.6.6) abstract (>= 1.0.0) excon (0.6.3) @@ -132,7 +135,7 @@ GEM heroku (1.19.1) activesupport (>= 2.1.0) launchy (~> 0.3.2) - rest-client (>= 1.4.0, < 1.7.0) + rest-client (< 1.7.0, >= 1.4.0) highline (1.6.2) httparty (0.7.8) crack (= 0.1.8) @@ -151,7 +154,7 @@ GEM linecache (0.43) linecache19 (0.5.12) ruby_core_source (>= 0.1.4) - locomotive_carrierwave (0.5.4.beta2) + locomotive_carrierwave (0.5.4.beta3) activesupport (~> 3.0) locomotive_jammit-s3 (0.5.4.4) jammit (>= 0.5.4) @@ -176,7 +179,7 @@ GEM tzinfo (~> 0.3.22) net-ssh (2.1.4) nokogiri (1.4.6) - open4 (1.0.1) + open4 (1.1.0) orm_adapter (0.0.5) pickle (0.4.7) cucumber (>= 0.8) @@ -184,6 +187,8 @@ GEM polyglot (0.3.1) proxies (0.2.1) rack (1.2.3) + rack-cache (1.0.2) + rack (>= 0.4) rack-mount (0.6.14) rack (>= 1.0.0) rack-test (0.5.7) @@ -202,6 +207,7 @@ GEM rake (>= 0.8.7) rdoc (~> 3.4) thor (~> 0.14.4) + raindrops (0.7.0) rake (0.8.7) rdoc (3.6.1) responders (0.6.4) @@ -253,9 +259,10 @@ GEM polyglot (>= 0.3.1) trollop (1.16.2) tzinfo (0.3.28) - unicorn (3.7.0) - kgio (~> 2.3) + unicorn (4.0.0) + kgio (~> 2.4) rack + raindrops (~> 0.6) warden (1.0.4) rack (>= 1.0) will_paginate (2.3.15) @@ -276,14 +283,16 @@ DEPENDENCIES bson_ext (~> 1.3.0) bushido bushido_stub! + cancan capybara cucumber (= 0.8.5) cucumber-rails - custom_fields (= 1.0.0.beta.18) + custom_fields (= 1.0.0.beta.19) database_cleaner delayed_job (= 2.1.4) delayed_job_mongoid (= 1.0.2) devise (= 1.3.4) + dragonfly (~> 0.9.1) factory_girl_rails fog (= 0.8.2) formtastic (~> 1.2.3) @@ -294,7 +303,7 @@ DEPENDENCIES inherited_resources (~> 1.1.2) launchy linecache (= 0.43) - locomotive_carrierwave (= 0.5.4.beta2) + locomotive_carrierwave (= 0.5.4.beta3) locomotive_jammit-s3 locomotive_liquid (= 2.2.2) locomotive_mongoid_acts_as_tree (= 0.1.5.7) @@ -302,7 +311,8 @@ DEPENDENCIES mocha! mongoid (~> 2.0.2) pickle - rails (>= 3.0.8) + rack-cache + rails (= 3.0.9) rake (= 0.8.7) rmagick (= 2.12.2) rspec-rails (= 2.3.1) diff --git a/README.textile b/README.textile index b9863ef1..75339fc2 100644 --- a/README.textile +++ b/README.textile @@ -19,7 +19,7 @@ h2. Gems Here is a short list of main gems used in the application. -* Rails 3.0.8 +* Rails 3.0.9 * Mongoid 2.0.2 (with MongoDB 1.6) * Liquid * Devise diff --git a/app/controllers/admin/asset_collections_controller.rb b/app/controllers/admin/asset_collections_controller.rb deleted file mode 100644 index ade55f68..00000000 --- a/app/controllers/admin/asset_collections_controller.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Admin - class AssetCollectionsController < BaseController - - sections 'assets' - - before_filter :set_collections - - def index - if not @asset_collections.empty? - redirect_to(edit_admin_asset_collection_url(@asset_collections.first)) and return - end - end - - def show - @asset_collection = current_site.asset_collections.find(params[:id]) - render :action => 'edit' - end - - protected - - def set_collections - @asset_collections = current_site.asset_collections.not_internal.order_by([[:name, :asc]]) - end - end -end diff --git a/app/controllers/admin/assets_controller.rb b/app/controllers/admin/assets_controller.rb index 35e371b1..4daf9015 100644 --- a/app/controllers/admin/assets_controller.rb +++ b/app/controllers/admin/assets_controller.rb @@ -1,29 +1,55 @@ module Admin class AssetsController < BaseController - sections 'assets' + include ActionView::Helpers::SanitizeHelper + include ActionView::Helpers::TextHelper - before_filter :set_collections_and_current_collection + respond_to :json, :only => [:index, :create, :destroy] - respond_to :json, :only => :update - - def create - create! { edit_admin_asset_collection_url(@asset_collection) } + def index + index! do |response| + response.json do + render :json => { :assets => @assets.collect { |asset| asset_to_json(asset) } } + end + end end - def update - update! { edit_admin_asset_collection_url(@asset_collection) } + def create + @asset = current_site.assets.build(:name => params[:name], :source => params[:file]) + + create! do |success, failure| + success.json do + render :json => asset_to_json(@asset) + end + failure.json do + render :json => { :status => 'error' } + end + end + rescue Exception => e + render :json => { :status => 'error', :message => e.message } end protected - def begin_of_association_chain - @asset_collection + def collection + if params[:image] + @assets ||= begin_of_association_chain.assets.only_image + else + @assets ||= begin_of_association_chain.assets + end end - def set_collections_and_current_collection - @asset_collections = current_site.asset_collections.not_internal.order_by([[:name, :asc]]) - @asset_collection = current_site.asset_collections.find(params[:collection_id]) + def asset_to_json(asset) + { + :status => 'success', + :filename => asset.source_filename, + :short_name => truncate(asset.name, :length => 15), + :extname => truncate(asset.extname, :length => 3), + :content_type => asset.content_type, + :url => asset.source.url, + :vignette_url => asset.vignette_url, + :destroy_url => admin_asset_url(asset, :json) + } end end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 8277040e..210aaf02 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -9,11 +9,13 @@ module Admin before_filter :require_site + load_and_authorize_resource + before_filter :validate_site_membership before_filter :set_locale - helper_method :sections, :current_site_url, :site_url, :page_url + helper_method :sections, :current_site_url, :site_url, :page_url, :current_ability # https://rails.lighthouseapp.com/projects/8994/tickets/1905-apphelpers-within-plugin-not-being-mixed-in Dir[File.dirname(__FILE__) + "/../../helpers/**/*_helper.rb"].each do |file| @@ -26,8 +28,24 @@ module Admin respond_to :html + rescue_from CanCan::AccessDenied do |exception| + ::Locomotive::Logger.info "[CanCan::AccessDenied] #{exception.inspect}" + + if request.xhr? + render :json => { :error => exception.message } + else + flash[:alert] = exception.message + + redirect_to admin_pages_url + end + end + protected + def current_ability + @current_ability ||= Ability.new(current_admin, current_site) + end + def require_admin authenticate_admin! end diff --git a/app/controllers/admin/contents_controller.rb b/app/controllers/admin/contents_controller.rb index 8fe2813c..51e45ade 100644 --- a/app/controllers/admin/contents_controller.rb +++ b/app/controllers/admin/contents_controller.rb @@ -5,7 +5,11 @@ module Admin before_filter :set_content_type - respond_to :json, :only => :update + respond_to :json, :only => [:update, :sort] + + skip_load_and_authorize_resource + + before_filter :authorize_content def index @contents = @content_type.list_or_group_contents @@ -20,7 +24,7 @@ module Admin end def sort - @content_type.sort_contents!(params[:order]) + @content_type.sort_contents!(params[:children]) respond_with(@content_type, :location => admin_contents_url(@content_type.slug)) end @@ -39,5 +43,9 @@ module Admin set_content_type end + def authorize_content + authorize! params[:action].to_sym, ContentInstance + end + end end diff --git a/app/controllers/admin/cross_domain_sessions_controller.rb b/app/controllers/admin/cross_domain_sessions_controller.rb index 1a0c42c2..aad86fe1 100644 --- a/app/controllers/admin/cross_domain_sessions_controller.rb +++ b/app/controllers/admin/cross_domain_sessions_controller.rb @@ -9,6 +9,8 @@ module Admin before_filter :require_admin, :only => :new + skip_load_and_authorize_resource + def new if site = current_admin.sites.detect { |s| s._id.to_s == params[:target_id] } if Rails.env == 'development' diff --git a/app/controllers/admin/current_sites_controller.rb b/app/controllers/admin/current_sites_controller.rb index a143b764..154fbdae 100644 --- a/app/controllers/admin/current_sites_controller.rb +++ b/app/controllers/admin/current_sites_controller.rb @@ -7,6 +7,10 @@ module Admin actions :edit, :update + skip_load_and_authorize_resource + + load_and_authorize_resource :class => 'Site' + respond_to :json, :only => :update def update diff --git a/app/controllers/admin/custom_fields_controller.rb b/app/controllers/admin/custom_fields_controller.rb index 5016b4d8..d5ca67a7 100644 --- a/app/controllers/admin/custom_fields_controller.rb +++ b/app/controllers/admin/custom_fields_controller.rb @@ -5,6 +5,8 @@ module Admin before_filter :set_parent_and_fields + skip_load_and_authorize_resource + def edit @field = @fields.find(params[:id]) render :action => "edit_#{@field.kind.downcase}" @@ -23,13 +25,8 @@ module Admin protected def set_parent_and_fields - if params[:parent] == 'asset_collection' - @parent = current_site.asset_collections.where(:slug => params[:slug]).first - @fields = @parent.asset_custom_fields - else - @parent = current_site.content_types.where(:slug => params[:slug]).first - @fields = @parent.content_custom_fields - end + @parent = current_site.content_types.where(:slug => params[:slug]).first + @fields = @parent.content_custom_fields end end diff --git a/app/controllers/admin/images_controller.rb b/app/controllers/admin/images_controller.rb deleted file mode 100644 index 8a98a46a..00000000 --- a/app/controllers/admin/images_controller.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Admin - class ImagesController < BaseController - - include ActionView::Helpers::SanitizeHelper - include ActionView::Helpers::TextHelper - - defaults :collection_name => 'assets', :instance_name => 'asset' - - respond_to :json, :only => [:index, :create, :destroy] - - def index - index! do |response| - response.json do - render :json => { :images => @assets.collect { |image| image_to_json(image) } } - end - end - end - - def create - params[:asset] = { :name => params[:name], :source => params[:file] } if params[:file] - - create! do |success, failure| - success.json do - render :json => image_to_json(@asset) - end - failure.json do - render :json => { :status => 'error' } - end - end - rescue Exception => e - render :json => { :status => 'error', :message => e.message } - end - - protected - - def collection - @assets ||= begin_of_association_chain.assets - end - - def begin_of_association_chain - @asset_collection ||= AssetCollection.find_or_create_internal(current_site) - end - - def image_to_json(image) - { - :status => 'success', - :name => truncate(image.name, :length => 15), - :url => image.source.url, - :vignette_url => image.vignette_url, - :destroy_url => admin_image_url(image, :json) - } - end - - end -end diff --git a/app/controllers/admin/imports_controller.rb b/app/controllers/admin/imports_controller.rb index ab717bf1..00516eb6 100644 --- a/app/controllers/admin/imports_controller.rb +++ b/app/controllers/admin/imports_controller.rb @@ -5,6 +5,10 @@ module Admin actions :show, :new, :create + skip_load_and_authorize_resource + + before_filter :authorize_import + def show @job = Delayed::Job.where({ :job_type => 'import', :site_id => current_site.id }).last @@ -41,5 +45,11 @@ module Admin end end + protected + + def authorize_import + authorize! :import, Site + end + end end \ No newline at end of file diff --git a/app/controllers/admin/installation_controller.rb b/app/controllers/admin/installation_controller.rb index acf19cee..f4c5b0eb 100644 --- a/app/controllers/admin/installation_controller.rb +++ b/app/controllers/admin/installation_controller.rb @@ -15,6 +15,8 @@ module Admin before_filter :allow_installation? + skip_load_and_authorize_resource + def show request.get? ? self.handle_get : self.handle_post end diff --git a/app/controllers/admin/my_accounts_controller.rb b/app/controllers/admin/my_accounts_controller.rb index 0036647a..dad74354 100644 --- a/app/controllers/admin/my_accounts_controller.rb +++ b/app/controllers/admin/my_accounts_controller.rb @@ -7,6 +7,8 @@ module Admin respond_to :json, :only => :update + skip_load_and_authorize_resource + def update update! { edit_admin_my_account_url } end diff --git a/app/controllers/admin/pages_controller.rb b/app/controllers/admin/pages_controller.rb index ab0dc6a5..a0030484 100644 --- a/app/controllers/admin/pages_controller.rb +++ b/app/controllers/admin/pages_controller.rb @@ -33,7 +33,7 @@ module Admin end def get_path - page = current_site.pages.build(:parent => current_site.pages.find(params[:parent_id]), :slug => params[:slug].slugify) + page = current_site.pages.build(:parent => current_site.pages.find(params[:parent_id]), :slug => params[:slug].permalink) render :json => { :url => page_url(page), :slug => page.slug } end diff --git a/app/controllers/admin/sitemaps_controller.rb b/app/controllers/admin/sitemaps_controller.rb index 270ad84c..98e8e9a3 100644 --- a/app/controllers/admin/sitemaps_controller.rb +++ b/app/controllers/admin/sitemaps_controller.rb @@ -7,6 +7,8 @@ module Admin respond_to :xml + skip_load_and_authorize_resource + def show @pages = current_site.pages.published end diff --git a/app/controllers/admin/theme_assets_controller.rb b/app/controllers/admin/theme_assets_controller.rb index f9068944..55f2fe7f 100644 --- a/app/controllers/admin/theme_assets_controller.rb +++ b/app/controllers/admin/theme_assets_controller.rb @@ -49,8 +49,8 @@ module Admin def sanitize_params params[:theme_asset] = { :source => params[:file] } if params[:file] - performing_plain_text = params[:theme_asset][:performing_plain_text] - params[:theme_asset].delete(:content_type) if performing_plain_text.blank? || performing_plain_text == 'false' + # performing_plain_text = params[:theme_asset][:performing_plain_text] + # params[:theme_asset].delete(:content_type) if performing_plain_text.blank? || performing_plain_text == 'false' end end diff --git a/app/helpers/admin/accounts_helper.rb b/app/helpers/admin/accounts_helper.rb index 8982592d..c6406965 100644 --- a/app/helpers/admin/accounts_helper.rb +++ b/app/helpers/admin/accounts_helper.rb @@ -1,7 +1,7 @@ module Admin::AccountsHelper def admin_on?(site = current_site) - site.memberships.detect { |a| a.admin? && a.account == current_admin } + site.memberships.detect { |m| m.admin? && m.account == current_admin } end end diff --git a/app/helpers/admin/custom_fields_helper.rb b/app/helpers/admin/custom_fields_helper.rb index e5705f05..25cc3ce8 100644 --- a/app/helpers/admin/custom_fields_helper.rb +++ b/app/helpers/admin/custom_fields_helper.rb @@ -45,15 +45,43 @@ module Admin::CustomFieldsHelper end.compact end - def options_for_has_one(field) - target_contents_from_field(field).collect { |c| [c._label, c._id] } + def options_for_has_one(field, value) + self.options_for_has_one_or_has_many(field) do |groups| + grouped_options_for_select(groups.collect do |g| + if g[:items].empty? + nil + else + [g[:name], g[:items].collect { |c| [c._label, c._id] }] + end + end.compact, value) + end end - alias :options_for_has_many :options_for_has_one + def options_for_has_many(field) + self.options_for_has_one_or_has_many(field) + end - def target_contents_from_field(field) + def options_for_has_one_or_has_many(field, &block) content_type = field.target.constantize._parent - content_type.ordered_contents + + if content_type.groupable? + grouped_contents = content_type.list_or_group_contents + + if block_given? + block.call(grouped_contents) + else + grouped_contents.collect do |g| + if g[:items].empty? + nil + else + { :name => g[:name], :items => g[:items].collect { |c| [c._label, c._id] } } + end + end.compact + end + else + contents = content_type.ordered_contents + contents.collect { |c| [c._label, c._id] } + end end end diff --git a/app/models/ability.rb b/app/models/ability.rb new file mode 100644 index 00000000..cb419161 --- /dev/null +++ b/app/models/ability.rb @@ -0,0 +1,66 @@ +class Ability + include CanCan::Ability + + ROLES = %w(admin designer author) + + def initialize(account, site) + @account, @site = account, site + + alias_action :index, :show, :edit, :update, :to => :touch + + @membership = @site.memberships.where(:account_id => @account.id).first + + return false if @membership.blank? + + if @membership.admin? + setup_admin_permissions! + else + setup_default_permissions! + + setup_designer_permissions! if @membership.designer? + + setup_author_permissions! if @membership.author? + end + end + + def setup_default_permissions! + cannot :manage, :all + end + + def setup_author_permissions! + can :touch, [Page, ThemeAsset] + can :sort, Page + + can :manage, [ContentInstance, Asset] + + can :touch, Site do |site| + site == @site + end + end + + def setup_designer_permissions! + can :manage, Page + + can :manage, ContentInstance + + can :manage, ContentType + + can :manage, Snippet + + can :manage, ThemeAsset + + can :manage, Site do |site| + site == @site + end + + can :import, Site + + can :point, Site + + can :manage, Membership + end + + def setup_admin_permissions! + can :manage, :all + end +end diff --git a/app/models/asset.rb b/app/models/asset.rb index d2fcc646..9305f5cf 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -3,12 +3,11 @@ class Asset include Mongoid::Document include Mongoid::Timestamps - ## Extensions ## + ## extensions ## + include Extensions::Asset::Types include Extensions::Asset::Vignette - include CustomFields::ProxyClassEnabler ## fields ## - field :name, :type => String field :content_type, :type => String field :width, :type => Integer field :height, :type => Integer @@ -17,32 +16,24 @@ class Asset mount_uploader :source, AssetUploader ## associations ## - embedded_in :collection, :class_name => 'AssetCollection', :inverse_of => :assets + referenced_in :site ## validations ## - validates_presence_of :name, :source + validates_presence_of :source ## behaviours ## ## methods ## - %w{image stylesheet javascript pdf media}.each do |type| - define_method("#{type}?") do - self.content_type.to_s == type - end - end + alias :name :source_filename def extname return nil unless self.source? File.extname(self.source_filename).gsub(/^\./, '') end - def site_id # needed by the uploader of custom fields - self.collection.site_id - end - def to_liquid - Locomotive::Liquid::Drops::Asset.new(self) + { :url => self.source.url }.merge(self.attributes).stringify_keys end end diff --git a/app/models/asset_collection.rb b/app/models/asset_collection.rb deleted file mode 100644 index da004d60..00000000 --- a/app/models/asset_collection.rb +++ /dev/null @@ -1,81 +0,0 @@ -class AssetCollection - - include Locomotive::Mongoid::Document - - ## fields ## - field :name - field :slug - field :internal, :type => Boolean, :default => false - - ## associations ## - referenced_in :site - embeds_many :assets, :validate => false - - ## behaviours ## - custom_fields_for :assets - liquid_methods :name, :ordered_assets - - ## callbacks ## - before_validation :normalize_slug - before_save :store_asset_positions! - after_destroy :remove_uploaded_files - - ## validations ## - validates_presence_of :site, :name, :slug - validates_uniqueness_of :slug, :scope => :site_id - - ## named scopes ## - scope :internal, :where => { :internal => true } - scope :not_internal, :where => { :internal => false } - - ## methods ## - - def ordered_assets - self.assets.sort { |a, b| (a.position || 0) <=> (b.position || 0) } - end - - def assets_order - self.ordered_assets.collect(&:id).join(',') - end - - def assets_order=(order) - @assets_order = order - end - - def self.find_or_create_internal(site) - site.asset_collections.internal.first || site.asset_collections.create(:name => 'system', :slug => 'system', :internal => true) - end - - protected - - def normalize_slug - self.slug = self.name.clone if self.slug.blank? && self.name.present? - self.slug.slugify! if self.slug.present? - end - - def store_asset_positions! - return if @assets_order.nil? - - ids = @assets_order.split(',').collect { |id| BSON::ObjectId(id) } - - ids.each_with_index do |asset_id, index| - self.assets.find(asset_id).position = index - end - - self.assets.clone.each do |asset| - if !ids.include?(asset._id) - self.assets.delete(asset) - asset.send(:delete) - end - end - end - - def remove_uploaded_files # callbacks are not called on each asset so we do it manually - self.assets.each do |asset| - self.asset_custom_fields.each do |field| - asset.send(:"remove_#{field._name}!") if field.kind == 'file' - end - end - end - -end diff --git a/app/models/content_instance.rb b/app/models/content_instance.rb index 96174dd0..5e794e9c 100644 --- a/app/models/content_instance.rb +++ b/app/models/content_instance.rb @@ -14,12 +14,14 @@ class ContentInstance ## validations ## validate :require_highlighted_field + validate :validate_uniqueness_of_slug + validates_presence_of :_slug ## associations ## embedded_in :content_type, :inverse_of => :contents ## callbacks ## - before_save :set_slug + before_validation :set_slug before_save :set_visibility before_create :add_to_list_bottom after_create :send_notifications @@ -67,8 +69,8 @@ class ContentInstance protected def set_slug - _alias = self.highlighted_field_alias - self._slug = self.send(_alias).parameterize('_') + self._slug = self.highlighted_field_value.clone if self._slug.blank? && self.highlighted_field_value.present? + self._slug.permalink! if self._slug.present? end def set_visibility @@ -77,7 +79,6 @@ class ContentInstance end def add_to_list_bottom - Rails.logger.debug "add_to_list_bottom" self._position_in_list = self.content_type.contents.size end @@ -88,6 +89,12 @@ class ContentInstance end end + def validate_uniqueness_of_slug + if self._parent.contents.any? { |c| c._id != self._id && c._slug == self._slug } + self.errors.add(:_slug, :taken) + end + end + def highlighted_field_alias self.content_type.highlighted_field._alias.to_sym end diff --git a/app/models/content_type.rb b/app/models/content_type.rb index c7330f2e..d56c02ba 100644 --- a/app/models/content_type.rb +++ b/app/models/content_type.rb @@ -107,8 +107,8 @@ class ContentType self.asc_order? ? list : list.reverse end - def sort_contents!(order) - order.split(',').each_with_index do |id, position| + def sort_contents!(ids) + ids.each_with_index do |id, position| self.contents.find(BSON::ObjectId(id))._position_in_list = position end self.save @@ -131,7 +131,7 @@ class ContentType def normalize_slug self.slug = self.name.clone if self.slug.blank? && self.name.present? - self.slug.slugify! if self.slug.present? + self.slug.permalink! if self.slug.present? end def remove_uploaded_files # callbacks are not called on each content so we do it manually diff --git a/app/models/extensions/asset/types.rb b/app/models/extensions/asset/types.rb new file mode 100644 index 00000000..7c8a94f6 --- /dev/null +++ b/app/models/extensions/asset/types.rb @@ -0,0 +1,19 @@ +module Extensions + module Asset + module Types + + extend ActiveSupport::Concern + + included do + %w{media image stylesheet javascript font pdf}.each do |type| + scope :"only_#{type}", where(:content_type => type) + + define_method("#{type}?") do + self.content_type.to_s == type + end + end + end + + end + end +end \ No newline at end of file diff --git a/app/models/extensions/asset/vignette.rb b/app/models/extensions/asset/vignette.rb index 4bbae1ba..2cb16027 100644 --- a/app/models/extensions/asset/vignette.rb +++ b/app/models/extensions/asset/vignette.rb @@ -7,11 +7,11 @@ module Extensions if self.width < 80 && self.height < 80 self.source.url else - self.source.url(:medium) + Locomotive::Dragonfly.resize_url(self.source, '80x80#') end end end end end -end +end \ No newline at end of file diff --git a/app/models/extensions/content_type/item_template.rb b/app/models/extensions/content_type/item_template.rb index 46ac8253..6ee5f5db 100644 --- a/app/models/extensions/content_type/item_template.rb +++ b/app/models/extensions/content_type/item_template.rb @@ -42,14 +42,6 @@ module Extensions @item_parsing_errors.try(:each) { |msg| self.errors.add :item_template, msg } end - # def item_template - # self.read_attribute(:default_item_template) || self.default_item_template - # end - # - # def default_item_template - # '{{ content.highlighted_field_value }}' - # end - end end diff --git a/app/models/extensions/shared/seo.rb b/app/models/extensions/shared/seo.rb index 06d190bc..be386296 100644 --- a/app/models/extensions/shared/seo.rb +++ b/app/models/extensions/shared/seo.rb @@ -4,10 +4,11 @@ module Extensions extend ActiveSupport::Concern included do + field :seo_title, :type => String field :meta_keywords, :type => String field :meta_description, :type => String end - + end # Seo end # Shared end # Extensions diff --git a/app/models/extensions/site/first_installation.rb b/app/models/extensions/site/first_installation.rb index 4e9473c3..3679f663 100644 --- a/app/models/extensions/site/first_installation.rb +++ b/app/models/extensions/site/first_installation.rb @@ -6,7 +6,7 @@ module Extensions def create_first_one(attributes) site = self.new(attributes) - site.memberships.build :account => Account.first, :admin => true + site.memberships.build :account => Account.first, :role => 'admin' site.save diff --git a/app/models/membership.rb b/app/models/membership.rb index a3f3a6b7..f9d9d8a6 100644 --- a/app/models/membership.rb +++ b/app/models/membership.rb @@ -3,7 +3,8 @@ class Membership include Locomotive::Mongoid::Document ## fields ## - field :admin, :type => Boolean, :default => false + # field :admin, :type => Boolean, :default => false + field :role, :default => 'author' ## associations ## referenced_in :account, :validate => false @@ -12,8 +13,17 @@ class Membership ## validations ## validates_presence_of :account + ## callbacks ## + before_save :define_role + ## methods ## + Ability::ROLES.each do |_role| + define_method("#{_role}?") do + self.role == _role + end + end + def email; @email; end def email=(email) @@ -36,4 +46,14 @@ class Membership end end + def ability + @ability ||= Ability.new(self.account, self.site) + end + + protected + + def define_role + self.role = Ability::ROLES.include?(role.downcase) ? role.downcase : Ability::ROLES.first + end + end diff --git a/app/models/page.rb b/app/models/page.rb index 2a6a4125..fe2dba03 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -97,7 +97,7 @@ class Page def normalize_slug self.slug = self.title.clone if self.slug.blank? && self.title.present? - self.slug.slugify!(:without_extension => true) if self.slug.present? + self.slug.permalink! if self.slug.present? end def set_default_raw_template diff --git a/app/models/site.rb b/app/models/site.rb index 7873c6d5..3d18dfcc 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -5,17 +5,17 @@ class Site ## Extensions ## extend Extensions::Site::SubdomainDomains extend Extensions::Site::FirstInstallation + extend Extensions::Site::FirstInstallation + include Extensions::Shared::Seo ## fields ## field :name - field :meta_keywords - field :meta_description ## associations ## references_many :pages, :validate => false references_many :snippets, :dependent => :destroy, :validate => false references_many :theme_assets, :dependent => :destroy, :validate => false - references_many :asset_collections, :dependent => :destroy, :validate => false + references_many :assets, :dependent => :destroy, :validate => false references_many :content_types, :dependent => :destroy, :validate => false embeds_many :memberships @@ -28,6 +28,7 @@ class Site ## behaviours ## enable_subdomain_n_domains_if_multi_sites + accepts_nested_attributes_for :memberships ## methods ## diff --git a/app/models/snippet.rb b/app/models/snippet.rb index a2d4f69f..0767bae0 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -26,7 +26,7 @@ class Snippet def normalize_slug # TODO: refactor it self.slug = self.name.clone if self.slug.blank? && self.name.present? - self.slug.slugify!(:without_extension => true, :downcase => true) if self.slug.present? + self.slug.permalink! if self.slug.present? end def update_templates diff --git a/app/models/theme_asset.rb b/app/models/theme_asset.rb index f1057ea6..9548ca16 100644 --- a/app/models/theme_asset.rb +++ b/app/models/theme_asset.rb @@ -2,6 +2,9 @@ class ThemeAsset include Locomotive::Mongoid::Document + ## extensions ## + include Extensions::Asset::Types + ## fields ## field :local_path field :content_type @@ -35,16 +38,10 @@ class ThemeAsset scope :visible, lambda { |all| all ? {} : { :where => { :hidden => false } } } ## accessors ## - attr_accessor :plain_text_name, :plain_text, :performing_plain_text + attr_accessor :plain_text_name, :plain_text, :plain_text_type, :performing_plain_text ## methods ## - %w{media image stylesheet javascript font}.each do |type| - define_method("#{type}?") do - self.content_type.to_s == type - end - end - def stylesheet_or_javascript? self.stylesheet? || self.javascript? end @@ -77,11 +74,17 @@ class ThemeAsset end end + def plain_text_type + @plain_text_type || (stylesheet_or_javascript? ? self.content_type : nil) + end + def performing_plain_text? Boolean.set(self.performing_plain_text) || false end def store_plain_text + self.content_type ||= @plain_text_type if self.performing_plain_text? + data = self.performing_plain_text? ? self.plain_text : self.source.read return if !self.stylesheet_or_javascript? || self.plain_text_name.blank? || data.blank? @@ -95,7 +98,7 @@ class ThemeAsset end def to_liquid - { :url => self.source.url }.merge(self.attributes) + { :url => self.source.url }.merge(self.attributes).stringify_keys end def self.all_grouped_by_folder(site, include_all = true) diff --git a/app/uploaders/asset_uploader.rb b/app/uploaders/asset_uploader.rb index c515da41..041cca08 100644 --- a/app/uploaders/asset_uploader.rb +++ b/app/uploaders/asset_uploader.rb @@ -2,26 +2,10 @@ class AssetUploader < CarrierWave::Uploader::Base - include CarrierWave::RMagick include Locomotive::CarrierWave::Uploader::Asset - version :thumb, :if => :image? do - process :resize_to_fill => [50, 50] - process :convert => 'png' - end - - version :medium, :if => :image? do - process :resize_to_fill => [80, 80] - process :convert => 'png' - end - - version :preview, :if => :image? do - process :resize_to_fit => [880, 1100] - process :convert => 'png' - end - def store_dir - self.build_store_dir('sites', model.collection.site_id, 'assets', model.id) + self.build_store_dir('sites', model.site_id, 'assets', model.id) end end diff --git a/app/views/admin/asset_collections/edit.html.haml b/app/views/admin/asset_collections/edit.html.haml deleted file mode 100644 index e033c8d4..00000000 --- a/app/views/admin/asset_collections/edit.html.haml +++ /dev/null @@ -1,36 +0,0 @@ -- title link_to(@asset_collection.name.blank? ? @asset_collection.name_was : @asset_collection.name, '#', :rel => 'asset_collection_name', :title => t('.ask_for_name'), :class => 'editable') - -- content_for :head do - = include_javascripts :asset_collections - = include_stylesheets :fancybox - -- content_for :submenu do - = render 'admin/shared/menu/assets' - -- content_for :actions do - = render 'admin/shared/actions/assets' - -%p!= t('.help') - -- content_for :buttons do - = admin_button_tag :add_asset, new_admin_asset_url(@asset_collection), :class => 'new' - -%p.no-items{ :style => "#{'display: none' unless @asset_collection.assets.empty? }" } - != t('.no_items', :url => new_admin_asset_url(@asset_collection)) - -%ul#assets.assets.sortable - = render :partial => 'asset', :collection => @asset_collection.ordered_assets - %li.clear - -= semantic_form_for @asset_collection, :url => admin_asset_collection_url(@asset_collection), :html => { :multipart => true } do |f| - = f.hidden_field :assets_order - - = f.foldable_inputs :name => :options do - = f.input :name - = f.input :slug, :required => false - - = render 'admin/custom_fields/index', :form => f, :collection_name => 'assets' - - = render 'admin/shared/form_actions', :delete_button => link_to(content_tag(:em, escape_once(' ')) + t('.destroy'), admin_asset_collection_url(@asset_collection), :confirm => t('admin.messages.confirm'), :method => :delete, :class => 'button small remove'), :button_label => :update - -= render 'admin/custom_fields/edit_field' diff --git a/app/views/admin/asset_collections/index.html.haml b/app/views/admin/asset_collections/index.html.haml deleted file mode 100644 index 5be0af5c..00000000 --- a/app/views/admin/asset_collections/index.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- title t('.title') - -- content_for :submenu do - = render 'admin/shared/menu/assets' - -- content_for :actions do - = render 'admin/shared/actions/assets' - -%p!= t('.help') - -%p.no-items!= t('.no_items', :url => new_admin_asset_collection_url) diff --git a/app/views/admin/asset_collections/new.html.haml b/app/views/admin/asset_collections/new.html.haml deleted file mode 100644 index 32a360b8..00000000 --- a/app/views/admin/asset_collections/new.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -- title t('.title') - -- content_for :head do - = include_javascripts :asset_collections - -- content_for :submenu do - = render 'admin/shared/menu/assets' - -- content_for :actions do - = render 'admin/shared/actions/assets' - -%p!= t('.help') - -= semantic_form_for @asset_collection, :url => admin_asset_collections_url do |f| - - = f.inputs :name => :information do - = f.input :name - = f.input :slug, :required => false - - = render 'admin/shared/form_actions', :back_url => admin_asset_collections_url, :button_label => :create diff --git a/app/views/admin/asset_collections/_asset.html.haml b/app/views/admin/assets/_asset.html.haml similarity index 89% rename from app/views/admin/asset_collections/_asset.html.haml rename to app/views/admin/assets/_asset.html.haml index ee75b036..6390aed1 100644 --- a/app/views/admin/asset_collections/_asset.html.haml +++ b/app/views/admin/assets/_asset.html.haml @@ -1,5 +1,5 @@ %li{ :id => "asset-#{asset.id}", :class => "asset #{'last' if (asset_counter + 1) % 6 == 0}"} - %h4= link_to truncate(asset.name, :length => 17), edit_admin_asset_path(@asset_collection, asset) + %h4= link_to truncate(asset.name, :length => 17), edit_admin_asset_path(asset) = vignette_tag(asset) .actions = link_to image_tag('admin/list/icons/cross.png'), '#', :class => 'remove', :confirm => t('admin.messages.confirm') \ No newline at end of file diff --git a/app/views/admin/assets/_form.html.haml b/app/views/admin/assets/_form.html.haml deleted file mode 100644 index 20fc1a7c..00000000 --- a/app/views/admin/assets/_form.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- content_for :head do - = include_javascripts :edit_custom_fields, :assets - = include_stylesheets :fancybox - -= f.inputs :name => :information do - = f.input :name - = f.input :source, :hint_options => @asset.persisted? ? { :url => link_to(@asset.source_filename, @asset.source.url), :escaping => false } : {} - -- unless @asset.custom_fields.blank? - = render 'admin/custom_fields/custom_form', :form => f, :title => :other_fields, :parent => @asset_collection - -- if @asset.image? && @asset.valid? - = f.foldable_inputs :name => "#{t('formtastic.titles.preview')} #{image_dimensions_and_size(@asset)}", :class => 'preview' do - %li - .image - .inside - = image_tag(@asset.source.url(:preview)) \ No newline at end of file diff --git a/app/views/admin/assets/edit.html.haml b/app/views/admin/assets/edit.html.haml deleted file mode 100644 index 26aeef17..00000000 --- a/app/views/admin/assets/edit.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- title t('.title') - -- content_for :submenu do - = render 'admin/shared/menu/assets' - -- content_for :actions do - = render 'admin/shared/actions/assets' - -- content_for :buttons do - = admin_button_tag t('admin.asset_collections.edit.add_asset'), new_admin_asset_url(@asset_collection), :class => 'new' - -%p!= t('.help') - -= semantic_form_for @asset, :url => admin_asset_url(@asset_collection, @asset), :html => { :multipart => true, :class => 'save-with-shortcut' } do |form| - - = render 'form', :f => form - - = render 'admin/shared/form_actions', :back_url => edit_admin_asset_collection_url(@asset_collection), :button_label => :update \ No newline at end of file diff --git a/app/views/admin/assets/new.html.haml b/app/views/admin/assets/new.html.haml deleted file mode 100644 index c6292abd..00000000 --- a/app/views/admin/assets/new.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -- title t('.title') - -- content_for :submenu do - = render 'admin/shared/menu/assets' - -- content_for :actions do - = render 'admin/shared/actions/assets' - -%p!= t('.help') - -= semantic_form_for @asset, :url => admin_assets_url(@asset_collection), :html => { :multipart => true } do |form| - - = render 'form', :f => form - - = render 'admin/shared/form_actions', :back_url => edit_admin_asset_collection_url(@asset_collection), :button_label => :create \ No newline at end of file diff --git a/app/views/admin/contents/_form.html.haml b/app/views/admin/contents/_form.html.haml index 745b8129..348ce7f1 100644 --- a/app/views/admin/contents/_form.html.haml +++ b/app/views/admin/contents/_form.html.haml @@ -2,9 +2,11 @@ = include_javascripts :edit_custom_fields, :contents = include_stylesheets :fancybox -= f.foldable_inputs :name => :meta do += render 'admin/custom_fields/custom_form', :form => f, :title => :attributes, :parent => @content_type += f.foldable_inputs :name => :advanced_options do + + = f.input :_slug + = f.input :seo_title = f.input :meta_keywords = f.input :meta_description - -= render 'admin/custom_fields/custom_form', :form => f, :title => :attributes, :parent => @content_type \ No newline at end of file diff --git a/app/views/admin/contents/_list.html.haml b/app/views/admin/contents/_list.html.haml index 573ddd5c..497ede88 100644 --- a/app/views/admin/contents/_list.html.haml +++ b/app/views/admin/contents/_list.html.haml @@ -1,7 +1,7 @@ - if contents.empty? %p.no-items!= t('.no_items', :url => new_admin_content_url(@content_type.slug)) - else - %ul{ :id => 'contents-list', :class => "list #{'sortable' if @content_type.order_by == '_position_in_list'}" } + %ul{ :id => 'contents-list', :class => "list #{'sortable' if @content_type.order_by == '_position_in_list'}", :'data-url' => sort_admin_contents_path(@content_type.slug, :json) } - contents.each do |content| %li.content{ :id => "content-#{content._id}" } %em diff --git a/app/views/admin/contents/edit.html.haml b/app/views/admin/contents/edit.html.haml index 39a45487..ab197ca7 100644 --- a/app/views/admin/contents/edit.html.haml +++ b/app/views/admin/contents/edit.html.haml @@ -1,13 +1,15 @@ - title t('.title', :type => @content_type.name.capitalize) - + - content_for :submenu do = render 'admin/shared/menu/contents' - + - content_for :actions do = render 'admin/shared/actions/contents' - content_for :buttons do - = admin_button_tag t('admin.contents.index.edit'), edit_admin_content_type_url(@content_type), :class => 'edit' + - if can?(:manage, ContentType) + = admin_button_tag t('admin.contents.index.edit'), edit_admin_content_type_url(@content_type), :class => 'edit' + = admin_button_tag t('admin.contents.index.new'), new_admin_content_url(@content_type.slug), :class => 'new' %p= @content_type.description diff --git a/app/views/admin/contents/index.html.haml b/app/views/admin/contents/index.html.haml index abfcb52d..ce7f0ed6 100644 --- a/app/views/admin/contents/index.html.haml +++ b/app/views/admin/contents/index.html.haml @@ -10,7 +10,9 @@ = include_javascripts :contents - content_for :buttons do - = admin_button_tag :edit, edit_admin_content_type_url(@content_type), :class => 'edit' + - if can?(:manage, ContentType) + = admin_button_tag :edit, edit_admin_content_type_url(@content_type), :class => 'edit' + = admin_button_tag :new, new_admin_content_url(@content_type.slug), :class => 'new' - if @content_type.description.present? @@ -24,7 +26,7 @@ - else = render 'list', :contents => @contents -= 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(:em, escape_once(' ')) + 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 +- if can?(:manage, ContentType) + #local-actions-bottom-bar + %p.tleft + = link_to(content_tag(:em, escape_once(' ')) + t('.destroy'), admin_content_type_url(@content_type), :confirm => t('admin.messages.confirm'), :method => :delete, :class => 'button small remove') diff --git a/app/views/admin/contents/new.html.haml b/app/views/admin/contents/new.html.haml index 874d366b..dbcf6e2b 100644 --- a/app/views/admin/contents/new.html.haml +++ b/app/views/admin/contents/new.html.haml @@ -6,8 +6,9 @@ - content_for :actions do = render 'admin/shared/actions/contents' -- content_for :buttons do - = admin_button_tag t('admin.contents.index.edit'), edit_admin_content_type_url(@content_type), :class => 'edit' +- if can?(:manage, ContentType) + - 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 diff --git a/app/views/admin/current_sites/_form.html.haml b/app/views/admin/current_sites/_form.html.haml index 17c082f7..85c31718 100644 --- a/app/views/admin/current_sites/_form.html.haml +++ b/app/views/admin/current_sites/_form.html.haml @@ -4,49 +4,65 @@ = f.foldable_inputs :name => :information, :style => "#{'display: none' unless @site.new_record?}" do = f.input :name, :required => false -= f.foldable_inputs :name => :meta do - += f.foldable_inputs :name => :seo do + = f.input :seo_title = f.input :meta_keywords = f.input :meta_description -- if manage_subdomain_or_domains? - = f.foldable_inputs :name => :access_points, :class => 'editable-list off' do +- if can?(:point, Site) + - if manage_subdomain_or_domains? + = f.foldable_inputs :name => :access_points, :class => 'editable-list off' do - = f.custom_input :subdomain, :css => 'path' do - %em - http:// - = f.text_field :subdomain, :readonly => !manage_subdomain? - \. - %em - = application_domain - - - if manage_domains? - - @site.domains_without_subdomain.each_with_index do |name, index| - %li{ :class => "item added #{'last' if index == @site.domains.size - 1}"} - %em - http:// - = text_field_tag 'site[domains][]', name, :class => 'string label void domain' -   - = error_on_domain(@site, name) - %span.actions - = link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm') - - %li.item.template + = f.custom_input :subdomain, :css => 'path' do %em http:// - = text_field_tag 'label', t('formtastic.hints.site.domain_name'), :class => 'string label void domain' -   - %span.actions - = link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm') - %button{ :class => 'button light add', :type => 'button' } - %span!= t('admin.buttons.new_item') + = f.text_field :subdomain, :readonly => !manage_subdomain? + \. + %em + = application_domain -= f.foldable_inputs :name => :memberships, :class => 'memberships' do - - @site.memberships.each_with_index do |membership, index| - - account = membership.account - %li{ :class => "item #{'last' if index == @site.memberships.size - 1}" } - %strong= account.name - %em= account.email - - if account != current_admin - %span.actions - = link_to image_tag('admin/form/icons/trash.png'), admin_membership_url(membership), :class => 'remove first', :confirm => t('admin.messages.confirm'), :method => :delete \ No newline at end of file + - if manage_domains? + - @site.domains_without_subdomain.each_with_index do |name, index| + %li{ :class => "item added #{'last' if index == @site.domains.size - 1}"} + %em + http:// + = text_field_tag 'site[domains][]', name, :class => 'string label void domain' +   + = error_on_domain(@site, name) + %span.actions + = link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm') + + %li.item.template + %em + http:// + = text_field_tag 'label', t('formtastic.hints.site.domain_name'), :class => 'string label void domain' +   + %span.actions + = link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm') + %button{ :class => 'button light add', :type => 'button' } + %span!= t('admin.buttons.new_item') + +- if can?(:manage, Membership) + + = f.foldable_inputs :name => :memberships, :class => 'memberships off' do + = f.semantic_fields_for :memberships do |fm| + + - membership, account = fm.object, fm.object.account + + %li.item.membership{ :'data-role' => membership.role } + %strong= account.name + + %em.email= account.email + + - if current_admin != account + .role + %em.editable= t("admin.memberships.roles.#{membership.role}") + + = fm.select :role, Ability::ROLES.collect { |r| [t("admin.memberships.roles.#{r}"), r] }, :include_blank => false + + %span.actions + = link_to image_tag('admin/form/icons/trash.png'), admin_membership_url(membership), :class => 'remove first', :confirm =>t('admin.messages.confirm'), :method => :delete + + - else + .role + %em.locked= t("admin.memberships.roles.#{membership.role}") diff --git a/app/views/admin/current_sites/edit.html.haml b/app/views/admin/current_sites/edit.html.haml index a164c714..fb0d1278 100644 --- a/app/views/admin/current_sites/edit.html.haml +++ b/app/views/admin/current_sites/edit.html.haml @@ -3,9 +3,10 @@ - content_for :submenu do = render 'admin/shared/menu/settings' -- content_for :buttons do - = admin_button_tag :import, new_admin_import_url, :class => 'new' - = admin_button_tag t('.new_membership'), new_admin_membership_url, :class => 'new' +- if can?(:manage, @site) + - content_for :buttons do + = admin_button_tag :import, new_admin_import_url, :class => 'new' + = admin_button_tag t('.new_membership'), new_admin_membership_url, :class => 'new' %p!= t('.help') diff --git a/app/views/admin/custom_fields/_index.html.haml b/app/views/admin/custom_fields/_index.html.haml index c7c60283..4a5f1d29 100644 --- a/app/views/admin/custom_fields/_index.html.haml +++ b/app/views/admin/custom_fields/_index.html.haml @@ -29,7 +29,7 @@ — - %em {{kind_name}} + %em.editable {{kind_name}} = select_tag '{{base_name}}[kind]', options_for_select(options_for_field_kind), :'data-field' => 'kind' diff --git a/app/views/admin/custom_fields/types/_has_one.html.haml b/app/views/admin/custom_fields/types/_has_one.html.haml index 86995ef3..d97c2415 100644 --- a/app/views/admin/custom_fields/types/_has_one.html.haml +++ b/app/views/admin/custom_fields/types/_has_one.html.haml @@ -1,4 +1,6 @@ - field.target.constantize.reload_parent! # to make sure all the contents from the parent are loaded -= form.input field._alias.to_sym, :label => field.label, :hint => field.hint, :input_html => { :class => 'has_one' }, :as => :select, :collection => options_for_has_one(field), :selected => form.object.send(field._alias.to_sym).try(:_id) +- selected_id = form.object.send(field._alias.to_sym).try(:_id) + += form.input field._alias.to_sym, :label => field.label, :hint => field.hint, :input_html => { :class => 'has_one' }, :as => :select, :collection => options_for_has_one(field, selected_id), :selected => selected_id diff --git a/app/views/admin/imports/show.html.haml b/app/views/admin/imports/show.html.haml index b3d47aa3..8873a366 100644 --- a/app/views/admin/imports/show.html.haml +++ b/app/views/admin/imports/show.html.haml @@ -9,7 +9,7 @@ %p!= t('.help') %ul{ :id => 'import-steps', :class => 'list', :'data-url' => admin_import_url(:json), :'data-success-message' => t('.messages.success'), :'data-failure-message' => t('.messages.failure') } - - %w(site content_types assets asset_collections snippets pages).each do |step| + - %w(site content_types assets snippets pages).each do |step| %li{ :id => "#{step}-step" } %em %strong diff --git a/app/views/admin/my_accounts/edit.html.haml b/app/views/admin/my_accounts/edit.html.haml index 3331d9bf..54a24727 100644 --- a/app/views/admin/my_accounts/edit.html.haml +++ b/app/views/admin/my_accounts/edit.html.haml @@ -8,7 +8,7 @@ - if multi_sites? - content_for :buttons do - = admin_button_tag t('.new_site'), new_admin_site_url, :class => 'new' + = admin_button_tag t('.new_site'), new_admin_site_url, :class => 'new' if can?(:manage, Site) %p= t('.help') diff --git a/app/views/admin/pages/_form.html.haml b/app/views/admin/pages/_form.html.haml index 34ed29bd..06a11738 100644 --- a/app/views/admin/pages/_form.html.haml +++ b/app/views/admin/pages/_form.html.haml @@ -2,47 +2,53 @@ = include_javascripts :image_picker, :edit_page = include_stylesheets :editable_elements, :fancybox -= f.foldable_inputs :name => :information do +- if can?(:manage, @page) - = f.input :title + = f.foldable_inputs :name => :information do - - if not @page.index? and not @page.not_found? - = f.input :parent_id, :as => :select, :collection => parent_pages_options, :include_blank => false + = f.input :title - = f.input :slug, :required => false, :hint => @page.slug.blank? ? ' ' : page_url(@page), :input_html => { :data_url => get_path_admin_pages_url, :disabled => @page.index? || @page.not_found? }, :wrapper_html => { :style => "#{'display: none' if @page.templatized?}; height: 50px" } + - if not @page.index? and not @page.not_found? + = f.input :parent_id, :as => :select, :collection => parent_pages_options, :include_blank => false -= f.foldable_inputs :name => :meta do - - = f.input :meta_keywords - = f.input :meta_description - -= f.foldable_inputs :name => :advanced_options do - - = f.input :content_type_id, :as => :select, :collection => current_site.content_types.all.to_a, :include_blank => false, :wrapper_html => { :style => "#{'display: none' unless @page.templatized?}; height: 50px" } - - = f.custom_input :templatized, :css => 'toggle', :style => "#{'display: none' if @page.redirect?}" do - = f.check_box :templatized - - = f.custom_input :published, :css => 'toggle' do - = f.check_box :published - - = f.custom_input :listed, :css => 'toggle' do - = f.check_box :listed - - = f.custom_input :redirect, :css => 'toggle', :style => "#{'display: none' if @page.templatized?}" do - = f.check_box :redirect - - = f.input :cache_strategy, :as => :select, :collection => options_for_page_cache_strategy, :include_blank => false, :wrapper_html => { :style => "#{'display: none' if @page.redirect?}" } - - = f.input :redirect_url, :required => true, :wrapper_html => { :style => "#{'display: none' unless @page.redirect?}" } + = f.input :slug, :required => false, :hint => @page.slug.blank? ? ' ' : page_url(@page), :input_html => { :'data-url' => get_path_admin_pages_url, :disabled => @page.index? || @page.not_found? }, :wrapper_html => { :style => "#{'display: none' if @page.templatized?}; height: 50px" } = render 'editable_elements', :page => @page -= f.foldable_inputs :name => :raw_template do - = f.custom_input :value, :css => 'code full', :with_label => false do - = f.label :raw_template - %code{ :class => 'html' } - = f.text_area :raw_template - = f.errors_on :template - .more - = link_to t('admin.image_picker.link'), admin_theme_assets_path, :id => 'image-picker-link' \ No newline at end of file += f.foldable_inputs :name => :seo do + + = f.input :seo_title + = f.input :meta_keywords + = f.input :meta_description + +- if can?(:manage, @page) + + = f.foldable_inputs :name => :advanced_options do + + = f.input :content_type_id, :as => :select, :collection => current_site.content_types.all.to_a, :include_blank => false, :wrapper_html => { :style => "#{'display: none' unless @page.templatized?}; height: 50px" } + + = f.custom_input :templatized, :css => 'toggle', :style => "#{'display: none' if @page.redirect?}" do + = f.check_box :templatized + + = f.custom_input :published, :css => 'toggle' do + = f.check_box :published + + = f.custom_input :listed, :css => 'toggle' do + = f.check_box :listed + + = f.custom_input :redirect, :css => 'toggle', :style => "#{'display: none' if @page.templatized?}" do + = f.check_box :redirect + + = f.input :cache_strategy, :as => :select, :collection => options_for_page_cache_strategy, :include_blank => false, :wrapper_html => { :style => "#{'display: none' if @page.redirect?}" } + + = f.input :redirect_url, :required => true, :wrapper_html => { :style => "#{'display: none' unless @page.redirect?}" } + + + = f.foldable_inputs :name => :raw_template do + = f.custom_input :value, :css => 'code full', :with_label => false do + = f.label :raw_template + %code{ :class => 'html' } + = f.text_area :raw_template + = f.errors_on :template + .more + = link_to t('admin.image_picker.link'), admin_theme_assets_path, :id => 'image-picker-link', :class => 'picture' \ No newline at end of file diff --git a/app/views/admin/pages/_page.html.haml b/app/views/admin/pages/_page.html.haml index 7540a46a..b1f4f378 100644 --- a/app/views/admin/pages/_page.html.haml +++ b/app/views/admin/pages/_page.html.haml @@ -1,5 +1,8 @@ %li{ :id => "item-#{page.id}", :class => "#{'not-found' if page.not_found? } #{'templatized' if page.templatized?}"} - - with_children = !page.children.empty? + + - children = can?(:manage, page) ? page.children : page.children.find_all { |p| !p.templatized? } + + - with_children = !children.empty? - if not page.index? and with_children = image_tag 'admin/list/icons/node_closed.png', :class => 'toggler' @@ -10,9 +13,10 @@ %span!= t('.updated_at') = l page.updated_at, :format => :short - - if not page.index? and not page.not_found? + - if !page.index_or_not_found? && can?(:manage, page) = link_to image_tag('admin/list/icons/trash.png'), admin_page_url(page), :class => 'remove', :confirm => t('admin.messages.confirm'), :method => :delete - if with_children - %ul{ :id => "folder-#{page._id}", :class => "folder depth-#{(page.depth || 0) + 1}", :data_url => sort_admin_page_url(page), :style => "display: #{cookies["folder-#{page._id}"] || 'block'}" } - = render page.children \ No newline at end of file + %ul{ :id => "folder-#{page._id}", :class => "folder depth-#{(page.depth || 0) + 1}", :'data-url' => sort_admin_page_url(page), :style => "display: #{cookies["folder-#{page._id}"] || 'block'}" } + + = render children \ No newline at end of file diff --git a/app/views/admin/pages/index.html.haml b/app/views/admin/pages/index.html.haml index 0e4957d8..9ab7f8b2 100644 --- a/app/views/admin/pages/index.html.haml +++ b/app/views/admin/pages/index.html.haml @@ -5,12 +5,13 @@ - content_for :submenu do = render 'admin/shared/menu/contents' - + - content_for :actions do = render 'admin/shared/actions/contents' -- content_for :buttons do - = admin_button_tag :new, new_admin_page_url, :class => 'new' +- if can? :create, Page + - content_for :buttons do + = admin_button_tag :new, new_admin_page_url, :class => 'new' %p!= t('.help') diff --git a/app/views/admin/shared/_form_actions.html.haml b/app/views/admin/shared/_form_actions.html.haml index 1d936200..7109a251 100644 --- a/app/views/admin/shared/_form_actions.html.haml +++ b/app/views/admin/shared/_form_actions.html.haml @@ -1,4 +1,4 @@ -.actions +#local-actions-bottom-bar .span-12 %p - if defined?(back_url) diff --git a/app/views/admin/shared/_menu.html.haml b/app/views/admin/shared/_menu.html.haml index 362f098d..19237f62 100644 --- a/app/views/admin/shared/_menu.html.haml +++ b/app/views/admin/shared/_menu.html.haml @@ -1,5 +1,4 @@ %ul#menu = admin_menu_item('contents', admin_pages_url) - = admin_menu_item('assets', admin_asset_collections_url) = admin_menu_item('settings', edit_admin_current_site_url) %li.clear diff --git a/app/views/admin/shared/actions/_assets.html.haml b/app/views/admin/shared/actions/_assets.html.haml deleted file mode 100644 index a8807861..00000000 --- a/app/views/admin/shared/actions/_assets.html.haml +++ /dev/null @@ -1 +0,0 @@ -= link_to content_tag(:span, t('admin.asset_collections.index.new')), new_admin_asset_collection_url \ No newline at end of file diff --git a/app/views/admin/shared/actions/_contents.html.haml b/app/views/admin/shared/actions/_contents.html.haml index d5b72e43..86d1b3bc 100644 --- a/app/views/admin/shared/actions/_contents.html.haml +++ b/app/views/admin/shared/actions/_contents.html.haml @@ -1 +1,2 @@ -= link_to content_tag(:em) + content_tag(:span, t('admin.content_types.index.new')), new_admin_content_type_url \ No newline at end of file +- if can? :manage, ContentType + = link_to content_tag(:em) + content_tag(:span, t('admin.content_types.index.new')), new_admin_content_type_url \ No newline at end of file diff --git a/app/views/admin/shared/menu/_assets.html.haml b/app/views/admin/shared/menu/_assets.html.haml deleted file mode 100644 index 6997d55e..00000000 --- a/app/views/admin/shared/menu/_assets.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -- @asset_collections.each do |c| - %li - = link_to content_tag(:span, truncate(c.name, :length => 20)), edit_admin_asset_collection_url(c), :class => "#{'on' if @asset_collection.id == c.id}" \ No newline at end of file diff --git a/app/views/admin/shared/menu/_contents.html.haml b/app/views/admin/shared/menu/_contents.html.haml index 1c078e9a..490e0c9c 100644 --- a/app/views/admin/shared/menu/_contents.html.haml +++ b/app/views/admin/shared/menu/_contents.html.haml @@ -1,6 +1,7 @@ = admin_submenu_item 'pages', admin_pages_url do - .header - %p= link_to t('admin.pages.index.new'), new_admin_page_url + - if can? :manage, @page + .header + %p= link_to t('admin.pages.index.new'), new_admin_page_url .inner %h2!= t('admin.pages.index.lastest_items') %ul @@ -12,6 +13,10 @@ - each_content_type_menu_item do |content_type| .header %p= link_to t('admin.contents.index.new'), new_admin_content_url(content_type.slug) + + - if can? :manage, content_type + %p.edit= link_to t('admin.contents.index.edit'), edit_admin_content_type_url(content_type) + .inner %h2!= t('admin.contents.index.lastest_items') %ul diff --git a/app/views/admin/snippets/_form.html.haml b/app/views/admin/snippets/_form.html.haml index 273938a7..7a159573 100644 --- a/app/views/admin/snippets/_form.html.haml +++ b/app/views/admin/snippets/_form.html.haml @@ -11,4 +11,4 @@ %code{ :class => 'html' } = f.text_area :template .more - = link_to t('admin.image_picker.link'), admin_theme_assets_path, :id => 'image-picker-link' \ No newline at end of file + = link_to t('admin.image_picker.link'), admin_theme_assets_path, :id => 'image-picker-link', :class => 'picture' \ No newline at end of file diff --git a/app/views/admin/snippets/edit.html.haml b/app/views/admin/snippets/edit.html.haml index 5f8b080d..626b5b50 100644 --- a/app/views/admin/snippets/edit.html.haml +++ b/app/views/admin/snippets/edit.html.haml @@ -6,7 +6,7 @@ - content_for :buttons do = admin_button_tag t('admin.snippets.index.new'), new_admin_snippet_url, :class => 'new' -%p!= t('.help') +%p!= t('.help', :slug => @snippet.slug) = semantic_form_for @snippet, :url => admin_snippet_url(@snippet), :html => { :class => 'save-with-shortcut' } do |form| diff --git a/app/views/admin/theme_assets/_form.html.haml b/app/views/admin/theme_assets/_form.html.haml index 19a14f63..53083cde 100644 --- a/app/views/admin/theme_assets/_form.html.haml +++ b/app/views/admin/theme_assets/_form.html.haml @@ -19,14 +19,14 @@ - if @theme_asset.new_record? = f.input :plain_text_name - = f.custom_input :content_type do - = f.select :content_type, %w(stylesheet javascript) + = f.custom_input :plain_text_type do + = f.select :plain_text_type, %w(stylesheet javascript) = f.custom_input :plain_text, :css => 'full', :with_label => false do %code{ :class => (@theme_asset.size && @theme_asset.size > 40000 ? 'nude' : (@theme_asset.content_type || 'stylesheet')) } = f.text_area :plain_text .more - = link_to t('admin.image_picker.link'), admin_theme_assets_path, :id => 'image-picker-link' + = link_to t('admin.image_picker.link'), admin_theme_assets_path, :id => 'image-picker-link', :class => 'picture' %span.alt != t('admin.theme_assets.form.choose_file') diff --git a/app/views/admin/theme_assets/edit.html.haml b/app/views/admin/theme_assets/edit.html.haml index 8b8e7cf4..9258792a 100644 --- a/app/views/admin/theme_assets/edit.html.haml +++ b/app/views/admin/theme_assets/edit.html.haml @@ -3,10 +3,16 @@ - content_for :submenu do = render 'admin/shared/menu/settings' -- content_for :buttons do - = admin_button_tag t('admin.theme_assets.index.new'), new_admin_theme_asset_url, :class => 'new' +- if can?(:manage, ThemeAsset) + - content_for :buttons do + = admin_button_tag t('admin.theme_assets.index.new'), new_admin_theme_asset_url, :class => 'new' + +%p + - if %w(image javascript stylesheet).include?(@theme_asset.content_type.to_s) + != t(".help_#{@theme_asset.content_type}", :path => @theme_asset.local_path(true), :width => @theme_asset.width, :height => @theme_asset.height) + + != t('.help', :url => @theme_asset.source.url) -%p!= t('.help', :url => @theme_asset.source.url) = semantic_form_for @theme_asset, :url => admin_theme_asset_url(@theme_asset), :html => { :multipart => true, :class => 'save-with-shortcut' } do |form| diff --git a/app/views/admin/theme_assets/index.html.haml b/app/views/admin/theme_assets/index.html.haml index bd25489c..4f979655 100644 --- a/app/views/admin/theme_assets/index.html.haml +++ b/app/views/admin/theme_assets/index.html.haml @@ -4,29 +4,31 @@ = render 'admin/shared/menu/settings' - content_for :buttons do - = admin_button_tag t('admin.theme_assets.index.all'), all_admin_theme_assets_url, :class => 'show' - = admin_button_tag t('admin.snippets.index.new'), new_admin_snippet_url, :class => 'new' - = admin_button_tag :new, new_admin_theme_asset_url, :class => 'new' + = admin_button_tag t('admin.theme_assets.index.all'), all_admin_theme_assets_url, :class => 'show' if can?(:manage, ThemeAsset) + = admin_button_tag t('admin.snippets.index.new'), new_admin_snippet_url, :class => 'new' if can?(:manage, Snippet) + = admin_button_tag :new, new_admin_theme_asset_url, :class => 'new' if can?(:manage, ThemeAsset) %p!= t('.help') -%h3!= t('.snippets') -- if @snippets.empty? - %p.no-items!= t('admin.snippets.index.no_items', :url => new_admin_snippet_url) -- else - %ul.list.theme-assets - = render @snippets +- if can?(:manage, Snippet) + %h3!= t('.snippets') + - if @snippets.empty? + %p.no-items!= t('admin.snippets.index.no_items', :url => new_admin_snippet_url) + - else + %ul.list.theme-assets + = render @snippets -%br + %br -%h3!= t('.css_and_js') -- if @js_and_css_assets.empty? - %p.no-items!= t('.no_items', :url => new_admin_theme_asset_url) -- else - %ul.list.theme-assets - = render :partial => 'asset', :collection => @js_and_css_assets +- if can?(:manage, ThemeAsset) + %h3!= t('.css_and_js') + - if @js_and_css_assets.empty? + %p.no-items!= t('.no_items', :url => new_admin_theme_asset_url) + - else + %ul.list.theme-assets + = render :partial => 'asset', :collection => @js_and_css_assets -%br + %br %h3!= t('.images') - if @assets[:images].nil? @@ -35,12 +37,13 @@ %ul.list.theme-assets = render :partial => 'asset', :collection => @assets[:images] -- if @assets[:fonts] - %br +- if can?(:manage, ThemeAsset) + - if @assets[:fonts] + %br - %h3!= t('.fonts') - %ul.list.theme-assets - = render :partial => 'asset', :collection => @assets[:fonts] + %h3!= t('.fonts') + %ul.list.theme-assets + = render :partial => 'asset', :collection => @assets[:fonts] - if @assets[:medias] %br diff --git a/config/application.rb b/config/application.rb index 266f043b..cf435b9c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,7 +45,5 @@ module Locomotive # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters << :password - - config.middleware.insert_after Rack::Lock, '::Locomotive::Middlewares::Fonts', :path => %r{^/fonts} end -end +end \ No newline at end of file diff --git a/config/assets.yml b/config/assets.yml index 606b23fb..e23346d9 100644 --- a/config/assets.yml +++ b/config/assets.yml @@ -38,13 +38,6 @@ javascripts: - public/javascripts/admin/plugins/mustache.js - public/javascripts/admin/custom_fields/category.js - public/javascripts/admin/custom_fields/has_many.js - asset_collections: - - public/javascripts/admin/plugins/fancybox.js - - public/javascripts/admin/plugins/mustache.js - - public/javascripts/admin/custom_fields.js - - public/javascripts/admin/asset_collections.js - assets: - - public/javascripts/admin/assets.js contents: - public/javascripts/admin/plugins/tiny_mce/tinymce.js - public/javascripts/admin/contents.js diff --git a/config/environments/development.rb b/config/environments/development.rb index c98a6c95..02694487 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -38,4 +38,4 @@ Locomotive::Application.configure do # 'BUSHIDO_CLAIMED' => 'true', # 'BUSHIDO_METRICS_TOKEN' => 'foobar' # } -end \ No newline at end of file +end diff --git a/config/initializers/dragonfly.rb b/config/initializers/dragonfly.rb new file mode 100644 index 00000000..7b9a6a2b --- /dev/null +++ b/config/initializers/dragonfly.rb @@ -0,0 +1,31 @@ +require 'locomotive' + +unless Locomotive.engine? + + require 'dragonfly' + + ## initialize Dragonfly ## + + app = Dragonfly[:images] + app.configure_with(:rails) + app.configure_with(:imagemagick) + + ## configure it ## + + Dragonfly[:images].configure do |c| + # Convert absolute location needs to be specified + # to avoid issues with Phusion Passenger not using $PATH + convert = `which convert`.strip.presence || "/usr/local/bin/convert" + c.convert_command = convert + c.identify_command = convert + + c.allow_fetch_url = true + c.allow_fetch_file = true + + c.url_format = '/images/dynamic/:job/:basename.:format' + end + +end + + + diff --git a/config/initializers/locomotive.rb b/config/initializers/locomotive.rb index 80cfe3b1..b8742900 100644 --- a/config/initializers/locomotive.rb +++ b/config/initializers/locomotive.rb @@ -62,4 +62,18 @@ Locomotive.configure do |config| # config.mailer_sender = 'support' # # => 'support@heroku.com' (Heroku), 'support@bushi.do' (Bushido), 'support@example.com' (Dev) or 'support@' (Multi-sites) config.mailer_sender = 'support' + + # Rack-cache settings, mainly used for the inline resizing image module. Default options: + # config.rack_cache = { + # :verbose => true, + # :metastore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/meta"), # URI encoded in case of spaces + # :entitystore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/body") + # } + # If you do want to disable it for good, just use the following syntax + # config.rack_cache = false + # + # Note: by default, rack/cache is disabled in the Heroku platform + end unless Locomotive.engine? || Rails.env.test? + +# Rails.application.middleware.use '::Locomotive::Middlewares::SeoTrailingSlash' diff --git a/config/locales/admin_ui.de.yml b/config/locales/admin_ui.de.yml index 25fd4df0..bda43408 100644 --- a/config/locales/admin_ui.de.yml +++ b/config/locales/admin_ui.de.yml @@ -61,9 +61,11 @@ de: title: Optionen help: Organisiere alle Optionen für die Auswahlbox. collection_label: Liste der Optionen - custom_form: - edit_categories: Optionen - delete_file: Datei löschen + types: + category: + edit_categories: Optionen + file: + delete_file: Datei löschen index: is_required: ist erforderlich default_label: Feld-Name @@ -190,22 +192,6 @@ de: title: Bilder anzeigen no_items: "Momentan gibt es keine Bilder." - asset_collections: - index: - title: Galerie - help: "Der Name der Galerie kann durch darauf klicken bearbeitet werden. Du kannst die Bilder in einer Galerie bearbeiten, indem du neue Felder hinzufügst." - new: neue Galerie - no_items: "Momentan gibt es keine Galerien. Klicke einfach hier, um eine neue Galerie zu erstellen." - new: - title: Neue Galerie - help: "Gib erst mal einen Namen ein. Alle anderen Einstellungen kannst du vornehmen, sobald das Formular abgesendet ist." - edit: - help: "Der Name der Galerie kann durch darauf klicken bearbeitet werden. Du kannst Bilder in einer Galerie bearbeiten, indem du neue Felder hinzufügst." - add_asset: Bild hinzufügen - destroy: Galerie löschen - no_items: "Momentan gibt es keine Bilder. Klicke einfach hier, um das erste Bild zu erstellen." - ask_for_name: "Gib den neuen Namen ein" - assets: new: title: Neues Bild @@ -287,7 +273,6 @@ de: site: Webseiten-Informationen content_types: Benutzerdefinierte Inhalts-Typen assets: Template-Dateien - asset_collections: Galerien snippets: Snippets pages: Seiten messages: diff --git a/config/locales/admin_ui.en.yml b/config/locales/admin_ui.en.yml index da955813..e767dbca 100644 --- a/config/locales/admin_ui.en.yml +++ b/config/locales/admin_ui.en.yml @@ -60,9 +60,11 @@ en: title: Edit options help: Manage the list of options for your select box. collection_label: List of options - custom_form: - edit_categories: Edit options - delete_file: Delete file + types: + category: + edit_categories: Edit options + file: + delete_file: Delete file index: is_required: is required default_label: Field name @@ -99,7 +101,7 @@ en: updated_at: updated at edit: show: show - help: "The page title can be updated by clicking it." + help: "The page title can be updated by clicking it. To apply your changes, click on the \"Update\" button." ask_for_title: "Please type the new page title" form: delete_file: Delete file @@ -123,7 +125,7 @@ en: help: "Fill in the form below to update your snippet." edit: title: Editing snippet - help: "Fill in the form below to update your snippet." + help: "Include your snippet in your page templates with the following liquid code : {% include '%{slug}' %}." snippet: updated_at: Updated at @@ -136,10 +138,14 @@ en: edit: import: import new_membership: add account - help: "The site name can be updated by clicking it." + help: "The site name can be updated by clicking it. To apply your changes, click on the \"Update\" button." ask_for_name: "Please type the new site name" memberships: + roles: + admin: Administrator + designer: Designer + author: Author new: title: New membership help: "Please give the account email to add. If it does not exist, you will be redirected to the account creation form." @@ -151,7 +157,7 @@ en: my_accounts: edit: - help: "Your name can be updated by clicking it." + help: "Your name can be updated by clicking it. To apply your changes, click on the \"Update\" button." new_site: new site en: English de: German @@ -163,7 +169,7 @@ en: theme_assets: index: title: Listing theme files - help: "The theme files section is the place where you manage the files needed by your layout, ...etc. If you need to manage an image gallery, go to the Assets section instead." + help: "The theme files section is the place where you manage the files needed by your layout, snippets...etc. If you need to manage an image gallery, create a new content type instead.
Warning: you may not see all the assets depending on your rights." all: all assets new: new file snippets: Snippets @@ -171,7 +177,7 @@ en: fonts: Fonts images: Images medias: Medias - no_items: "There are no files for now. Just click here to create the first one." + no_items: "There are no files for now." asset: updated_at: Updated at new: @@ -179,7 +185,10 @@ en: help: "You have the choice to either upload any file or to copy/paste a stylesheet or a javascript in plain text." edit: title: "Editing %{file}" - help: "This asset is accessible from the following url: %{url}" + help: "This asset is accessible directly from the following url: %{url}" + help_image: "Include your image in your page templates or snippets with the following liquid code : {{ '%{path}' | theme_image_tag }}.
Your current image dimensions : %{width}px x %{height}px.
" + help_javascript: "Include your javascript file in your page templates or snippets with the following code : {{ '%{path}' | javascript_tag }}.
" + help_stylesheet: "Include your stylesheet file in your page templates or snippets with the following code : {{ '%{path}' | stylesheet_tag }}.
" form: picker_link: Insert a file into the code choose_file: Choose file @@ -188,22 +197,6 @@ en: title: Listing images no_items: "There are no files for now." - asset_collections: - index: - title: Asset collections - help: "The collection name can be updated by clicking it. You can customize assets in a collection by adding new fields." - new: new collection - no_items: "There are no collections for now. Just click here to create the first one." - new: - title: New collection - help: "For now, just type a name. Other settings will come once the form is sent." - edit: - help: "The collection name can be updated by clicking it. You can customize assets in a collection by adding new fields." - add_asset: add asset - destroy: remove collection - no_items: "There are no assets for now. Just click here to create the first one." - ask_for_name: "Please type the new name" - assets: new: title: New asset @@ -225,8 +218,8 @@ en: new_item: new item form: order_by: - created_at: 'By "created at" date' - updated_at: 'By "updated at" date' + created_at: 'By creation date' + updated_at: 'By updating date' position_in_list: Manually order_direction: asc: Ascending @@ -268,7 +261,6 @@ en: site: Site information content_types: Custom content types assets: Theme files - asset_collections: Asset collections snippets: Snippets pages: Pages messages: diff --git a/config/locales/admin_ui.fr.yml b/config/locales/admin_ui.fr.yml index 9e7671ad..83052244 100644 --- a/config/locales/admin_ui.fr.yml +++ b/config/locales/admin_ui.fr.yml @@ -60,9 +60,11 @@ fr: title: Editer options help: Gèrer la liste des options de votre liste déroulante collection_label: Liste des options - custom_form: - edit_categories: Editer options - delete_file: Supprimer fichier + types: + category: + edit_categories: Editer options + file: + delete_file: Supprimer fichier index: is_required: est obligatoire default_label: Nom du champ @@ -99,7 +101,7 @@ fr: updated_at: Mise à jour le edit: show: voir - help: "Le titre de la page est modifiable en cliquant dessus." + help: "Le titre de la page est modifiable en cliquant dessus. Pour appliquer votre modification, cliquez après sur le bouton \"Modifier\"" ask_for_title: "Veuillez entrer le nouveau titre" form: delete_file: Supprimer fichier @@ -124,6 +126,7 @@ fr: edit: title: Edition snippet help: "Remplissez le formulaire ci-dessous pour mettre à jour votre snippet." + help: "Utilisez votre snippet dans le template d'une page avec le code Liquid suivant : {% include '%{slug}' %}." snippet: updated_at: Mis à jour le @@ -136,7 +139,7 @@ fr: edit: import: importer new_membership: ajouter compte - help: "Le nom du site est modifiable en cliquant dessus." + help: "Le nom du site est modifiable en cliquant dessus. Pour appliquer votre modification, cliquez après sur le bouton \"Modifier\"" ask_for_name: "Veuillez entrer le nouveau nom" memberships: @@ -151,7 +154,7 @@ fr: my_accounts: edit: - help: "Votre nom est modifiable en cliquant dessus." + help: "Votre nom est modifiable en cliquant dessus. Pour appliquer votre modification, cliquez après sur le bouton \"Modifier\"" new_site: nouveau site en: en Anglais de: en Allemand @@ -163,7 +166,7 @@ fr: theme_assets: index: title: Liste des fichiers du thème - help: "Les fichiers du thème sont utilisés pour construire le gabarit de vos pages. Si vous avez besoin d'une galerie d'images, la section Média est plus adéquate." + help: "Les fichiers du thème sont utilisés pour construire le gabarit de vos pages. Si vous avez besoin d'une galerie d'images, créer un modèle sera plus adéquate.
Attention : Suivant les droits que vous avez, il se peut que vous ne puissiez pas voir tous les fichiers." all: tous les fichiers new: nouveau fichier snippets: Snippets @@ -171,7 +174,7 @@ fr: images: Images medias: Médias fonts: Polices - no_items: "Il n'existe pas de fichiers. Vous pouvez commencer par créer un ici." + no_items: "Il n'existe pas de fichiers." asset: updated_at: Mis à jour le new: @@ -180,6 +183,11 @@ fr: edit: title: "Edition %{file}" help: "L'url du fichier est %{url}" + help: "L'url du fichier est directement disponible : %{url}" + help_image: "Utilisez votre image dans le template de vos pages ou snippets avec le code Liquid suivant : {{ '%{path}' | theme_image_tag }}.
. Votre image a comme dimensions : %{width}px x %{height}px.
" + help_javascript: "Utilisez votre javascript dans le template de vos pages avec le code Liquid suivant : {{ '%{path}' | javascript_tag }}.
" + help_stylesheet: "Utilisez votre stylesheet dans le template de vos pages avec le code Liquid suivant : {{ '%{path}' | stylesheet_tag }}.
" + form: choose_file: Choisir fichier choose_plain_text: Passer en mode texte @@ -187,22 +195,6 @@ fr: title: Liste des images no_items: "Il n'y a pas d'images." - asset_collections: - index: - title: Collections - help: "Le nom de la collection est modifiable en cliquant dessus. Vous pouvez personnaliser une collection en ajoutant d'autres champs à vos médias." - new: nouvelle collection - no_items: "Il n'existe pas de collections. Vous pouvez commencer par créer une ici." - new: - title: Nouvelle collection - help: "Pour le moment, veuillez rentrer le nom. Les autres options viendront dans le formulaire suivant." - edit: - help: "Le nom de la collection est modifiable en cliquant dessus. Vous pouvez personnaliser une collection en ajoutant d'autres champs à vos médias." - add_asset: ajouter média - destroy: supprimer collection - no_items: "Il n'existe pas de médias. Vous pouvez commencer par créer un ici." - ask_for_name: "Veuillez entrer le nouveau nom" - assets: new: title: "Nouveau média" @@ -224,7 +216,7 @@ fr: new_item: nouvel élément form: order_by: - created_at: 'Par date création' + created_at: 'Par date de création' updated_at: 'Par date de mise à jour' position_in_list: Manuellement order_direction: @@ -267,7 +259,6 @@ fr: site: Informations sur le site content_types: Modèles de données personnalisés assets: Fichiers du thème - asset_collections: Collections de média snippets: Snippets pages: Pages messages: diff --git a/config/locales/admin_ui.it.yml b/config/locales/admin_ui.it.yml index a946a1be..56d0ae76 100644 --- a/config/locales/admin_ui.it.yml +++ b/config/locales/admin_ui.it.yml @@ -60,9 +60,11 @@ it: title: Modifica opzioni help: Amministra la lista opzioni del select box. collection_label: Lista delle opzioni - custom_form: - edit_categories: Modifica opzioni - delete_file: Elimina file + types: + category: + edit_categories: Modifica opzioni + file: + delete_file: Elimina file index: is_required: è richiesto default_label: Nome campo @@ -188,22 +190,6 @@ it: title: Lista immagini no_items: "Per ora non ci sono file." - asset_collections: - index: - title: Collezioni di risorse - help: "Puoi cambiare il nome della collezione cliccandoci sopra. Puoi personalizzare le risorse in una collezione aggiugendo nuovi campi." - new: nuov collezione - no_items: "Per ora non ci sono collezioni. Clicca qui per creare la prima." - new: - title: Nuova collezione - help: "Per ora inserisci solo in nome. Altre opzioni appariranno dopo aver inviato il modulo." - edit: - help: "Puoi cambiare il nome della collezione cliccandoci sopra. Puoi personalizzare le risorse in una collezione aggiugendo nuovi campi." - add_asset: aggiungi risorsa - destroy: elimina collezione - no_items: "Per ora non ci sono risorse. Clicca qui per creare la prima." - ask_for_name: "Prego, digita il nuovo nome" - assets: new: title: Nuova risorsa @@ -268,7 +254,6 @@ it: site: Informazioni sito content_types: Tipi di contenuti personalizzati assets: File del tema - asset_collections: Collezioni di risorse snippets: Frammenti pages: Pagine messages: diff --git a/config/locales/admin_ui.pt-BR.yml b/config/locales/admin_ui.pt-BR.yml index 6d102d1c..9d1e7e91 100644 --- a/config/locales/admin_ui.pt-BR.yml +++ b/config/locales/admin_ui.pt-BR.yml @@ -60,9 +60,11 @@ pt-BR: title: Editar opções help: Gerenciar a lista de opções da sua caixa de seleçõa. collection_label: Lista de opções - custom_form: - edit_categories: Editar opções - delete_file: Excluir arquivo + types: + category: + edit_categories: Editar opções + file: + delete_file: Excluir arquivo sessions: new: @@ -154,7 +156,7 @@ pt-BR: de: Alemão fr: Francês pt-BR: "Português do Brasil" - it: Italiano + it: Italiano ask_for_name: "Por favor preencha o novo nome" theme_assets: @@ -185,22 +187,6 @@ pt-BR: title: Listando imagens no_items: "Não existem imagens ainda." - asset_collections: - index: - title: Coleções de arquivos - help: "O nome da coleção pode ser alterado clicando nele. Você pode customizar os arquivos nas coleções adicionando campos." - new: nova coleção - no_items: "Não existem coleções ainda. Clique aqui para criar a primeira." - new: - title: Nova Coleção - help: "Por enquanto, apenas insira o nome. Outras opções irão aparecer após o formulário ser enviado." - edit: - help: "O nome da coleção pode ser alterado clicando nele. Você pode customizar os arquivos nas coleções adicionando campos." - add_asset: adicionar arquivo - destroy: excluir coleção - no_items: "Não existem coleções ainda. Clique aqui para criar a primeira." - ask_for_name: Por favor preencha o novo nome" - assets: new: title: Novo arquivo @@ -262,7 +248,6 @@ pt-BR: site: Informações do site content_types: Tipos de Conteúdo customizados assets: Arquivos do Tema - asset_collections: Coleções de Arquivos snippets: Blocos pages: Páginas messages: diff --git a/config/locales/default.de.yml b/config/locales/default.de.yml index 00b80140..dd93f90d 100644 --- a/config/locales/default.de.yml +++ b/config/locales/default.de.yml @@ -72,11 +72,7 @@ de: highlighted_field_name: Hervorgehobener Feld-Name group_by_field_name: Gruppierung nach Feld-Name api_enabled: API aktiviert - asset_collection: - name: Name - slug: Slug asset: - name: Name source: Quelle account: email: Email diff --git a/config/locales/default.fr.yml b/config/locales/default.fr.yml index 5fcf7a37..01b9f63d 100644 --- a/config/locales/default.fr.yml +++ b/config/locales/default.fr.yml @@ -63,6 +63,7 @@ fr: redirect: Redirection redirect_url: Url de redirection cache_strategy: Cache + seo_title: Titre SEO content_type: name: Nom description: Description @@ -71,11 +72,7 @@ fr: highlighted_field_name: Champ mis en avant group_by_field_name: Champ pour grouper api_enabled: Activation API - asset_collection: - name: Nom - slug: Raccourci asset: - name: Nom source: Fichier account: email: E-mail diff --git a/config/locales/default.it.yml b/config/locales/default.it.yml index 8fc0f581..b3bcc6cc 100644 --- a/config/locales/default.it.yml +++ b/config/locales/default.it.yml @@ -77,11 +77,7 @@ it: highlighted_field_name: Campo evidenziato group_by_field_name: Raggruppa per nome campo api_enabled: API Attiva - asset_collection: - name: Nome - slug: Slug asset: - name: Nome source: Sorgente account: email: Email diff --git a/config/locales/default.pt-BR.yml b/config/locales/default.pt-BR.yml index 7a8da224..7198ba37 100644 --- a/config/locales/default.pt-BR.yml +++ b/config/locales/default.pt-BR.yml @@ -71,11 +71,7 @@ pt-BR: highlighted_field_name: Nome do Campo em destaque group_by_field_name: Agrupar por name do campo api_enabled: Activation API - asset_collection: - name: Nome - slug: Slug asset: - name: Nome source: Arquivo account: email: Email diff --git a/config/locales/flash.de.yml b/config/locales/flash.de.yml index 0a2ce05e..afc0c123 100644 --- a/config/locales/flash.de.yml +++ b/config/locales/flash.de.yml @@ -73,16 +73,6 @@ de: alert: "Account wurde nicht erstellt." already_created: "Account wurde bereits zu dieser Webseite hinzugefügt." - asset_collections: - create: - notice: "Sammlung wurde erfolgreich erstellt." - alert: "Sammlung wurde nicht erstellt." - update: - notice: "Sammlung wurde erfolreich aktualisiert." - alert: "Sammlung wurde nicht aktualisiert." - destroy: - notice: "Sammlung wurde erfolgreich gelöscht." - assets: create: notice: "Asset wurde erfolgreich erstellt." diff --git a/config/locales/flash.en.yml b/config/locales/flash.en.yml index 6c6b3a89..7e3f7c0d 100644 --- a/config/locales/flash.en.yml +++ b/config/locales/flash.en.yml @@ -73,16 +73,6 @@ en: alert: "Membership was not created." already_created: "Account was already added the current site." - asset_collections: - create: - notice: "Collection was successfully created." - alert: "Collection was not created." - update: - notice: "Collection was successfully updated." - alert: "Collection was not updated." - destroy: - notice: "Collection was successfully deleted." - assets: create: notice: "Asset was successfully created." diff --git a/config/locales/flash.fr.yml b/config/locales/flash.fr.yml index a651a986..2a2155f5 100644 --- a/config/locales/flash.fr.yml +++ b/config/locales/flash.fr.yml @@ -73,16 +73,6 @@ fr: alert: "Le compte n'a pas été ajouté." already_created: "Le compte a déjà été ajouté pour ce site." - asset_collections: - create: - notice: "La collection a été créée avec succès." - alert: "La collection n'a pas été créée." - update: - notice: "La collection a été mise à jour avec succès." - alert: "La collection n'a pas été mise à jour." - destroy: - notice: "La collection a été supprimée avec succès." - assets: create: notice: "Le média a été crée avec succès." diff --git a/config/locales/flash.it.yml b/config/locales/flash.it.yml index de1d2c38..c59a0ed6 100644 --- a/config/locales/flash.it.yml +++ b/config/locales/flash.it.yml @@ -73,16 +73,6 @@ it: alert: "La partecipazione non è stata creata." already_created: "L'account è già stato aggiunto al corrente sito." - asset_collections: - create: - notice: "La collezione è stata creata con successo." - alert: "La collezione non è stata creata." - update: - notice: "La collezione è stata modificata con successo." - alert: "La collezione non è stata modificata." - destroy: - notice: "La collezione è stata eliminata con successo." - assets: create: notice: "La risorsa è stata creata con successo." diff --git a/config/locales/flash.pt-BR.yml b/config/locales/flash.pt-BR.yml index 19971750..ebd77827 100644 --- a/config/locales/flash.pt-BR.yml +++ b/config/locales/flash.pt-BR.yml @@ -45,7 +45,7 @@ pt-BR: notice: "Fragmento foi criado com sucesso." alert: "Fragmento não foi criado." update: - notice: "Fragmento foi atualizado com sucesso." + notice: "Fragmento foi atualizado com sucesso." alert: "Fragmento não foi atualizado." destroy: notice: "Fragmento foi apagado com sucesso." @@ -57,7 +57,7 @@ pt-BR: my_accounts: update: - notice: "Minha conta foi atualizada com sucesso." + notice: "Minha conta foi atualizada com sucesso." alert: "Minha conta não foi atualizada." sites: @@ -73,16 +73,6 @@ pt-BR: alert: "Adesão não foi criada." already_created: "Conta já foi adicionada ao site atual." - asset_collections: - create: - notice: "Coleção criada com sucesso." - alert: "Coleção não foi criada." - update: - notice: "Coleção foi atualizada com sucesso." - alert: "Coleção não foi atualizada." - destroy: - notice: "Coleção foi apagada com sucesso." - assets: create: notice: "Asset foi criada com sucesso." @@ -94,7 +84,7 @@ pt-BR: theme_assets: create: notice: "Arquivo foi criado com sucesso." - alert: "Arquivo não foi criado." + alert: "Arquivo não foi criado." update: notice: "Arquivo foi atualizado com sucesso." alert: "Arquivo não foi atualizado." diff --git a/config/locales/formtastic.en.yml b/config/locales/formtastic.en.yml index 8dafb198..496f0e52 100644 --- a/config/locales/formtastic.en.yml +++ b/config/locales/formtastic.en.yml @@ -4,6 +4,7 @@ en: information: General information advanced_options: Advanced options meta: SEO Metadata + seo: SEO settings code: Code raw_template: Template credentials: Credentials @@ -40,10 +41,14 @@ en: content_type: raw_item_template: Item template api_accounts: Notified Accounts + content_instance: + _slug: Permalink account: edit: password: New password password_confirmation: New password confirmation + page: + seo_title: Title hints: page: @@ -52,11 +57,13 @@ en: templatized: "Use the page as a template for a model you defined." listed: "Control whether to show the page from generated menus." content_type_id: "The type of content this page will be a template for." + seo_title: "Define a page title which should be used as the value for the title tag in the head section. Leave it empty if you want to use the default value from the site settings." meta_keywords: "Overrides the site's meta keywords used within the head tag of the page. They are separated by a comma." meta_description: "Overrides the site's meta description used within the head tag of the page." snippet: slug: "You need to know it in order to insert the snippet inside a page" site: + seo_title: "Define a global value here which should be used as the value for the title tag in the head section." meta_keywords: "Meta keywords used within the head tag of the page. They are separated by a comma. Required for SEO." meta_description: "Meta description used within the head tag of the page. Required for SEO." domain_name: "ex: locomotiveapp.org" @@ -74,6 +81,11 @@ en: field: _alias: "Property available in liquid templates" hint: "Text displayed in the model form just below the field" + content_instance: + _slug: "Property used to generate the url of a page working as a template for this content type (ex: \"template_page/{{ your_object._permalink }})\"." + seo_title: "The value you fill in will replace the SEO title of the templatized page related to your model." + meta_keywords: "Overrides the site's meta keywords used within the head tag of the page. They are separated by a comma." + meta_description: "Overrides the site's meta description used within the head tag of the page." import: source: "A zipfile containing a database.yml along with assets and templates" samples: "If enabled, the import process will also copy contents and assets" diff --git a/config/locales/formtastic.fr.yml b/config/locales/formtastic.fr.yml index c57acd0c..d8d0f98e 100644 --- a/config/locales/formtastic.fr.yml +++ b/config/locales/formtastic.fr.yml @@ -4,6 +4,7 @@ fr: information: Informations générales advanced_options: Options avancées meta: SEO Metadata + seo: Paramètres SEO code: Code raw_template: Gabarit credentials: Informations de connexion @@ -42,10 +43,14 @@ fr: content_type: raw_item_template: Template d'affichage api_accounts: Comptes à notifier + content_instance: + _slug: Permalink account: edit: password: Nouveau mot de passe password_confirmation: Confirmation nouveau mot de passe + page: + seo_title: Titre hints: page: @@ -54,6 +59,7 @@ fr: templatized: "Utilise la page comme un template pour un modèle défini." listed: "Controle si la page doit être visible depuis les menus automatiquement générés." content_type_id: "Le type du contenu pour lequel cette page est un template." + seo_title: "Définit un titre de page à mettre dans la balise TITLE de la page. Laissez le blanc pour utiliser la valeur par défaut (voir configuration du site)." meta_keywords: "Redéfinit les mots-clés du site. Utilisés à l'intérieur de la balise HEAD. Ils sont séparés par une virgule." meta_description: "Redéfinit la description du site. Utilisée à l'intérieur de la balise HEAD." snippet: @@ -76,6 +82,11 @@ fr: field: _alias: "Champ utilisable dans les templates liquid" hint: "Texte affiché dans le formulaire de l'élément juste en dessous du champ." + content_instance: + _slug: "Propriété utilisée pour générer l'url d'une page faisant office de template pour ce modèle (ex: \"template_de_la_page/{{ votre_object._permalink }})\"." + seo_title: "La valeur que vous rentrez sera utilisée comme titre SEO pour la page faisant office de template pour ce modèle." + meta_keywords: "Redéfinit les mots-clés du site. Utilisés à l'intérieur de la balise HEAD. Ils sont séparés par une virgule." + meta_description: "Redéfinit la description du site. Utilisée à l'intérieur de la balise HEAD." import: source: "Un fichier zip contenant database.yml, les fichiers du thème et les templates de page" samples: "Si activé, les contenus et les média seront aussi copiés lors de l'import" diff --git a/config/routes.rb b/config/routes.rb index 56a846f1..3b5d2510 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -33,11 +33,9 @@ Rails.application.routes.draw do get :all, :action => 'index', :on => :collection, :defaults => { :all => true } end - resources :asset_collections + resources :assets - resources :assets, :path => 'asset_collections/:collection_id/assets' - - resources :images + resources :images, :controller => 'assets', :defaults => { :image => true } resources :content_types diff --git a/doc/TODO b/doc/TODO index 0c45ca12..ba0317ba 100644 --- a/doc/TODO +++ b/doc/TODO @@ -1,8 +1,59 @@ BOARD: +x bushido version + x default template ~ editable_elements: inheritable: false (Mattias) => seems to be fixed by Dirk's last pull request (#44) => content tag -- bushido version - - default template +x locomedia tinyMCE plugin (Bernd) +x remove asset_collections + x site templates + x tinyMCE plugin + x vignette.rb + x code + x helpers + x ui + x rake task + x internal collection + x assign same _id +x pick up a theme_asset +x pull request locomedia +x refactor slugify method (use parameterize + create a module) +x contents permalink (UI) +x BUG: has_one / has_many. Delete an author +x bushido changes in the master +? edit sidebar (inline editor). Unable to reset it +x SEO: support and support/ should be 2 different pages. Remove trailing slash +x issue #91: httparty +x issue #90: seo metadata +x issue #57: seo page title +x issue #56 +x tweak ui: form, quick link to edit a model in the popup menu +x Has_one => group by in the select +x better hints: + x notify the user that after changing the page title, they still have to click "update" for the change to be saved + x created_by ASC => "Creation date ascending" +x cancan: (authors / designers / admin) + x model + x ui + x controllers / views: + x page + x content / content type + x asset + x site + x account + x snippet + x theme asset + x features / specs +x enable rack-cache only for a specific url +- convert existing templates (the 2 of the themes section) + +=> MERGE + +- bugs + - heroku: unable to upload a new file + - import + - delete an item +- liquid tag: Date.today (now), add a test to compare 2 dates +- better ui: increase text field length + refactor error message BACKLOG: @@ -19,11 +70,16 @@ BACKLOG: - icon for redirection page in the pages section (back-office) - write my first tutorial about locomotive - upgrade warning if new version of locomotive (maybe based on the commit id) +- deploying workflow: + - roll back a bad update + - conflicts with content types + - dev -> staging -> production + - sync data + - import only theme assets REFACTORING: - - refactor slugify method (use parameterize + create a module) - move content_type and content_instances in the CustomFields plugin (much more appropriate) BUGS: diff --git a/features/admin/authorization/content_type.feature b/features/admin/authorization/content_type.feature new file mode 100644 index 00000000..fc334d69 --- /dev/null +++ b/features/admin/authorization/content_type.feature @@ -0,0 +1,32 @@ +Feature: Editing a content type + In order to edit a content type + As an admin, designer, or author + I will be restricted based on my role + +Background: + Given I have the site: "test site" set up + And I have a custom project model + And I have a designer and an author + + Scenario: As an unauthenticated user + Given I am not authenticated + When I go to the "Projects" model edition page + Then I should see "Log in" + + Scenario: Accessing edition functionality as an Admin + Given I am an authenticated "admin" + When I go to the "Projects" model edition page + Then I should see "Editing model" + And I should see "Custom fields" + + Scenario: Accessing edition functionality as a Designer + Given I am an authenticated "designer" + When I go to the "Projects" model edition page + Then I should see "Editing model" + And I should see "Custom fields" + + Scenario: Accessing edition functionality as an Author + Given I am an authenticated "author" + When I go to the "Projects" model edition page + Then I should be on the pages list + And I should see the access denied message diff --git a/features/admin/authorization/current_site.feature b/features/admin/authorization/current_site.feature new file mode 100644 index 00000000..8cfdcb6a --- /dev/null +++ b/features/admin/authorization/current_site.feature @@ -0,0 +1,53 @@ +Feature: Site Settings + In order to ensure site settings are not tampered with + As an admin, designer or author + I will be restricted based on my role + +Background: + Given I have the site: "test site" set up + And I have a designer and an author + + Scenario: As an unauthenticated user + Given I am not authenticated + When I go to site settings + Then I should see "Log in" + + Scenario: Accessing site settings as an Admin + Given I am an authenticated "admin" + When I go to site settings + Then I should see "import" + And I should see "add account" + And I should see "SEO settings" + And I should see "Access points" + And I should not see the role dropdown on myself + And I should see the role dropdown on the "designer" + And I should see the role dropdown on the "author" + And I should not see delete on myself + And I should see delete on the "designer" + And I should see delete on the "author" + + Scenario: Accessing site settings as a Designer + Given I am an authenticated "designer" + When I go to site settings + Then I should see "import" + And I should see "add account" + And I should see "SEO settings" + And I should see "Access points" + And I should not see the role dropdown on myself + And I should see the role dropdown on the "admin" + And I should see the role dropdown on the "author" + And I should see delete on the "admin" + And I should not see delete on myself + And I should see delete on the "author" + + Scenario: Accessing site settings as an Author + Given I am an authenticated "author" + When I go to site settings + Then I should not see "import" + And I should not see "add account" + And I should see "SEO settings" + And I should not see "Access points" + And I should not see "Accounts" + # Paranoid Checks + And I should not see any role dropdowns + And I should not see any delete buttons diff --git a/features/admin/authorization/importing.feature b/features/admin/authorization/importing.feature new file mode 100644 index 00000000..663614ce --- /dev/null +++ b/features/admin/authorization/importing.feature @@ -0,0 +1,31 @@ +Feature: Importing a Site + In order to populate the site with data and assets + As an admin, designer, or author + I will be restricted based on my role + +Background: + Given I have the site: "test site" set up + And I have a designer and an author + + Scenario: As an unauthenticated user + Given I am not authenticated + When I go to import page + Then I should see "Log in" + + Scenario: Accessing importing functionality as an Admin + Given I am an authenticated "admin" + When I go to the import page + Then I should see "Import" + And I should see "Upload" + + Scenario: Accessing importing functionality as a Designer + Given I am an authenticated "designer" + When I go to the import page + Then I should see "Import" + And I should see "Upload" + + Scenario: Accessing importing functionality as an Author + Given I am an authenticated "author" + When I go to the import page + Then I should be on the pages list + And I should see the access denied message diff --git a/features/admin/authorization/pages.feature b/features/admin/authorization/pages.feature new file mode 100644 index 00000000..495a5ec6 --- /dev/null +++ b/features/admin/authorization/pages.feature @@ -0,0 +1,95 @@ +Feature: Pages + In order to ensure pages are not tampered with + As an admin, designer or author + I will be restricted based on my role + +Background: + Given I have the site: "test site" set up + And I have a custom project model + And I have a designer and an author + And a page named "hello-world" with the template: + """ + Hello World + """ + + Scenario: As an unauthenticated user + Given I am not authenticated + When I go to pages + Then I should see "Log in" + + # listing pages + + Scenario: Accessing pages as an Admin + Given I am an authenticated "admin" + When I go to pages + Then I should see "new page" + And I should see "new model" + And I should see "Projects" + And I should see "edit model" + And I should see delete page buttons + + Scenario: Accessing pages as a Designer + Given I am an authenticated "designer" + When I go to pages + Then I should see "new page" + And I should see "new model" + And I should see "Projects" + And I should see "edit model" + And I should see delete page buttons + + Scenario: Accessing pages as an Author + Given I am an authenticated "author" + When I go to pages + Then I should not see "new page" + And I should not see "new model" + And I should see "Projects" + And I should not see "edit model" + And I should not see delete page buttons + + # new page + + Scenario: Accessing new page as an Admin + Given I am an authenticated "admin" + When I go to the new page + Then I should see "New page" + + Scenario: Accessing new page as a Designer + Given I am an authenticated "designer" + When I go to the new page + Then I should see "New page" + + Scenario: Accessing new page as an Author + Given I am an authenticated "author" + When I go to the new page + Then I should be on the pages list + And I should see the access denied message + + # edit page + + Scenario: Accessing edit page as an Admin + Given I am an authenticated "admin" + When I go to the "hello-world" edition page + Then I should see "some title" + And I should see "General information" + And I should see "SEO settings" + And I should see "Advanced options" + And I should see "Template" + + Scenario: Accessing edit page as a Designer + Given I am an authenticated "designer" + When I go to the "hello-world" edition page + Then I should see "some title" + And I should see "General information" + And I should see "SEO settings" + And I should see "Advanced options" + And I should see "Template" + + Scenario: Accessing edit page as an Author + Given I am an authenticated "author" + When I go to the "hello-world" edition page + Then I should see "some title" + And I should not see "General Information" + And I should see "SEO settings" + And I should not see "Advanced options" + And I should not see "Template" + diff --git a/features/admin/authorization/theme_assets.feature b/features/admin/authorization/theme_assets.feature new file mode 100644 index 00000000..b77c25b7 --- /dev/null +++ b/features/admin/authorization/theme_assets.feature @@ -0,0 +1,43 @@ +Feature: Theme Assets + In order to ensure theme assets are not tampered with + As an admin, designer or author + I will be restricted based on my role + +Background: + Given I have the site: "test site" set up + And I have a designer and an author + + Scenario: As an unauthenticated user + Given I am not authenticated + When I go to theme assets + Then I should see "Log in" + + Scenario: Accessing theme assets as an Admin + Given I am an authenticated "admin" + When I go to theme assets + Then I should see "all assets" + And I should see "new snippet" + And I should see "new file" + And I should see "Snippets" + And I should see "Style and javascript" + And I should see "Images" + + Scenario: Accessing theme assets as a Designer + Given I am an authenticated "designer" + When I go to theme assets + Then I should see "all assets" + And I should see "new snippet" + And I should see "new file" + And I should see "Snippets" + And I should see "Style and javascript" + And I should see "Images" + + Scenario: Accessing theme assets as an Author + Given I am an authenticated "author" + When I go to theme assets + Then I should not see "all assets" + And I should not see "new snippet" + And I should not see "new file" + And I should not see "Snippets" + And I should not see "Style and javascript" + And I should see "Images" diff --git a/features/admin/pages.feature b/features/admin/pages.feature index 3dd941bd..2867b37a 100644 --- a/features/admin/pages.feature +++ b/features/admin/pages.feature @@ -15,7 +15,7 @@ Scenario: Pages list is not accessible for non authenticated accounts Scenario: Creating a valid page When I go to pages And I follow "new page" - And I fill in "Title" with "Test" + And I fill in "page_title" with "Test" And I fill in "Slug" with "test" And I select "Home page" from "Parent" And I fill in "Raw template" with "Lorem ipsum...." @@ -26,7 +26,7 @@ Scenario: Creating a valid page Scenario: Updating a valid page When I go to pages And I follow "Home page" - And I fill in "Title" with "Home page !" + And I fill in "page_title" with "Home page !" And I fill in "Raw template" with "My new content is here" And I press "Update" Then I should see "Page was successfully updated." diff --git a/features/admin/theme_assets.feature b/features/admin/theme_assets.feature index 031d6878..022751d7 100644 --- a/features/admin/theme_assets.feature +++ b/features/admin/theme_assets.feature @@ -30,6 +30,17 @@ Scenario: Uploading a stylesheet And I should see "Code" And I should see "stylesheets/main.css" +Scenario: Updating a stylesheet + Given a stylesheet asset named "application" + When I go to theme assets + And I follow "stylesheets/application.css" + And I fill in "theme_asset[plain_text]" with "Lorem ipsum (updated)" + And I press "Update" + Then I should see "File was successfully updated." + And I should see "Editing application.css" + And I should see "application.css" + And I should see "Lorem ipsum (updated)" as theme asset code + Scenario: Uploading a javascript When I go to theme assets And I follow "new file" @@ -40,6 +51,14 @@ Scenario: Uploading a javascript And I should see "Code" And I should see "javascripts/test/application.js" +Scenario: Updating a javascript + Given a javascript asset named "application" + When I go to theme assets + And I follow "javascripts/application.js" + And I fill in "theme_asset[plain_text]" with "Lorem ipsum (updated)" + And I press "Update" + Then I should see "File was successfully updated." + Scenario: Uploading an image which already exists When I go to theme assets And I follow "new file" diff --git a/features/step_definitions/admin_steps.rb b/features/step_definitions/admin_steps.rb index 759cadbd..8476491c 100644 --- a/features/step_definitions/admin_steps.rb +++ b/features/step_definitions/admin_steps.rb @@ -4,13 +4,23 @@ Given /^I am not authenticated$/ do visit('/admin/sign_out') end -Given /^I am an authenticated user$/ do +Given /^I am an authenticated "([^"]*)"$/ do |role| + @member = Site.first.memberships.where(:role => role.downcase).first || Factory(role.downcase.to_sym, :site => Site.first) + Given %{I go to login} - And %{I fill in "Email" with "admin@locomotiveapp.org"} + And %{I fill in "Email" with "#{@member.account.email}"} And %{I fill in "Password" with "easyone"} And %{I press "Log in"} end +Given /^I am an authenticated user$/ do + Given %{I am an authenticated "admin"} +end + +Then /^I should see the access denied message$/ do + Then %{I should see "You are not authorized to access this page"} +end + Then /^I am redirected to "([^\"]*)"$/ do |url| assert [301, 302].include?(@integration_session.status), "Expected status to be 301 or 302, got #{@integration_session.status}" location = @integration_session.headers["Location"] @@ -27,3 +37,7 @@ end ### Common +Then /^I debug$/ do + debugger + 0 +end diff --git a/features/step_definitions/content_types_steps.rb b/features/step_definitions/content_types_steps.rb new file mode 100644 index 00000000..86a57b07 --- /dev/null +++ b/features/step_definitions/content_types_steps.rb @@ -0,0 +1,7 @@ +Given /^I have a custom project model/ do + site = Site.first + @content_type = Factory.build(:content_type, :site => site, :name => 'Projects') + @content_type.content_custom_fields.build :label => 'Name', :kind => 'string' + @content_type.content_custom_fields.build :label => 'Description', :kind => 'text' + @content_type.save.should be_true +end \ No newline at end of file diff --git a/features/step_definitions/current_site_steps.rb b/features/step_definitions/current_site_steps.rb new file mode 100644 index 00000000..bfb76706 --- /dev/null +++ b/features/step_definitions/current_site_steps.rb @@ -0,0 +1,35 @@ +Then /^I should see the role dropdown on the "([^"]*)"$/ do |user| + find(:css, "li.membership[data-role=#{user}] select").text.should == 'AdministratorDesignerAuthor' +end + +Then /^I should see the role dropdown on myself$/ do + Then %{I should see the role dropdown on the "#{@member.role}"} +end + +Then /^I should not see the role dropdown on the "([^"]*)"$/ do |user| + page.has_css?("li.membership[data-role=#{user}] select").should be_false +end + +Then /^I should not see the role dropdown on myself$/ do + Then %{I should not see the role dropdown on the "#{@member.role}"} +end + +Then /^I should not see any role dropdowns$/ do + page.has_css?('li.membership select').should be_false +end + +Then /^I should see delete on the "([^"]*)"$/ do |role| + page.has_css?("li.membership[data-role=#{role}] .actions a.remove").should be_true +end + +Then /^I should not see delete on the "([^"]*)"$/ do |role| + page.has_css?("li.membership[data-role=#{role}] .actions a.remove").should be_false +end + +Then /^I should not see delete on myself$/ do + Then %{I should not see delete on the "#{@member.role}"} +end + +Then /^I should not see any delete buttons$/ do + page.has_css?('li.membership .actions a.remove').should be_false +end diff --git a/features/step_definitions/page_steps.rb b/features/step_definitions/page_steps.rb index aa498b11..ea6b8e6c 100644 --- a/features/step_definitions/page_steps.rb +++ b/features/step_definitions/page_steps.rb @@ -42,4 +42,12 @@ Then /^the rendered output should look like:$/ do |body_contents| page.body.should == body_contents end +Then /^I should see delete page buttons$/ do + page.has_css?("ul#pages-list li .more a.remove").should be_true +end + +Then /^I should not see delete page buttons$/ do + page.has_css?("ul#pages-list li .more a.remove").should be_false +end + diff --git a/features/step_definitions/site_steps.rb b/features/step_definitions/site_steps.rb index d8cdb8f8..4ac96333 100644 --- a/features/step_definitions/site_steps.rb +++ b/features/step_definitions/site_steps.rb @@ -12,3 +12,7 @@ Given /^I have the site: "([^"]*)" set up(?: with #{capture_fields})?$/ do |site @admin.should_not be_nil end +Given /^I have a designer and an author$/ do + Factory(:designer, :site => Site.first) + Factory(:author, :site => Site.first) +end \ No newline at end of file diff --git a/features/step_definitions/theme_asset_steps.rb b/features/step_definitions/theme_asset_steps.rb new file mode 100644 index 00000000..0db95683 --- /dev/null +++ b/features/step_definitions/theme_asset_steps.rb @@ -0,0 +1,31 @@ +### Theme assets + +# helps create a theme asset +def create_plain_text_asset(name, type) + asset = Factory.build(:theme_asset, { + :site => @site, + :plain_text_name => name, + :plain_text => 'Lorem ipsum', + :plain_text_type => type, + :performing_plain_text => true + }) + # asset.should be_valid + asset.save! + +end + +# creates various theme assets + +Given /^a javascript asset named "([^"]*)"$/ do |name| + @asset = create_plain_text_asset(name, 'javascript') +end + +Given /^a stylesheet asset named "([^"]*)"$/ do |name| + @asset = create_plain_text_asset(name, 'stylesheet') +end + +# other stuff + +Then /^I should see "([^"]*)" as theme asset code$/ do |code| + find(:css, "#theme_asset_plain_text").text.should == code +end \ No newline at end of file diff --git a/features/support/env.rb b/features/support/env.rb index 27e29a37..54b48b83 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -16,6 +16,8 @@ require 'capybara/rails' require 'capybara/cucumber' require 'capybara/session' +require 'ruby-debug' + # envjs doesnt work at the moment # require 'capybara/envjs' diff --git a/features/support/paths.rb b/features/support/paths.rb index bf5bc85f..0a713a8d 100644 --- a/features/support/paths.rb +++ b/features/support/paths.rb @@ -14,10 +14,22 @@ module NavigationHelpers new_admin_session_path when /logout/ destroy_admin_session_path - when /pages/ + when /pages( list)?/ admin_pages_path + when /new page/ + new_admin_page_path + when /"(.*)" edition page/ + page = Site.first.pages.where(:slug => $1).first + edit_admin_page_path(page) when /theme assets/ admin_theme_assets_path + when /site settings/ + edit_admin_current_site_path + when /import page/ + new_admin_import_path + when /the "(.*)" model edition page/ + content_type = Site.first.content_types.where(:name => $1).first + edit_admin_content_type_path(content_type) # Add more mappings here. # Here is an example that pulls values out of the Regexp: diff --git a/lib/generators/locomotive/install/install_generator.rb b/lib/generators/locomotive/install/install_generator.rb index e4a0644e..66fb844a 100644 --- a/lib/generators/locomotive/install/install_generator.rb +++ b/lib/generators/locomotive/install/install_generator.rb @@ -20,6 +20,8 @@ module Locomotive template 'locomotive.rb', 'config/initializers/locomotive.rb' template 'carrierwave.rb', 'config/initializers/carrierwave.rb' + + template 'dragonfly.rb', 'config/initializers/dragonfly.rb' end def remove_index_html diff --git a/lib/generators/locomotive/install/templates/README b/lib/generators/locomotive/install/templates/README index d9e19a50..1d8d8b32 100644 --- a/lib/generators/locomotive/install/templates/README +++ b/lib/generators/locomotive/install/templates/README @@ -6,6 +6,7 @@ The Locomotive Engine has been correctly installed in your Rails application. - config/initializers/locomotive.rb - config/initializers/carrierwave.rb + - config/initializers/dragonfly.rb - config/mongoid.yml 2. Launch the server diff --git a/lib/generators/locomotive/install/templates/dragonfly.rb b/lib/generators/locomotive/install/templates/dragonfly.rb new file mode 100644 index 00000000..dd42f956 --- /dev/null +++ b/lib/generators/locomotive/install/templates/dragonfly.rb @@ -0,0 +1,22 @@ +require 'dragonfly' + +## initialize Dragonfly ## + +app = Dragonfly[:images] +app.configure_with(:rails) +app.configure_with(:imagemagick) + +## configure it ## + +Dragonfly[:images].configure do |c| + # Convert absolute location needs to be specified + # to avoid issues with Phusion Passenger not using $PATH + convert = `which convert`.strip.presence || "/usr/local/bin/convert" + c.convert_command = convert + c.identify_command = convert + + c.allow_fetch_url = true + c.allow_fetch_file = true + + c.url_format = '/images/dynamic/:job/:basename.:format' +end diff --git a/lib/locomotive.rb b/lib/locomotive.rb index 6d49cb32..0e5ebd95 100644 --- a/lib/locomotive.rb +++ b/lib/locomotive.rb @@ -5,6 +5,7 @@ require 'locomotive/version' require 'locomotive/core_ext' require 'locomotive/configuration' require 'locomotive/logger' +require 'locomotive/dragonfly' require 'locomotive/liquid' require 'locomotive/mongoid' require 'locomotive/carrierwave' @@ -23,8 +24,9 @@ require 'locomotive/hosting' module Locomotive - include Locomotive::Hosting::Heroku - include Locomotive::Hosting::Bushido + extend Locomotive::Hosting::Heroku + extend Locomotive::Hosting::Bushido + extend Locomotive::Hosting::Default class << self attr_accessor :config @@ -73,15 +75,28 @@ module Locomotive :key => self.config.cookie_key } + # add middlewares (dragonfly, font, seo, ...etc) + self.add_middlewares + # Load all the dynamic classes (custom fields) begin ContentType.all.collect(&:fetch_content_klass) - AssetCollection.all.collect(&:fetch_asset_klass) rescue ::Mongoid::Errors::InvalidDatabase => e # let assume it's because of the first install (meaning no config.yml file) end end + def self.add_middlewares + self.app_middleware.insert 0, 'Dragonfly::Middleware', :images + + if self.rack_cache? + self.app_middleware.insert_before 'Dragonfly::Middleware', '::Locomotive::Middlewares::Cache', self.config.rack_cache + end + + self.app_middleware.insert_before Rack::Lock, '::Locomotive::Middlewares::Fonts', :path => %r{^/fonts} + self.app_middleware.use '::Locomotive::Middlewares::SeoTrailingSlash' + end + def self.configure_multi_sites if self.config.multi_sites? domain_name = self.config.multi_sites.domain @@ -116,4 +131,15 @@ module Locomotive end end + # rack_cache: needed by default + def self.rack_cache? + self.config.rack_cache != false + end + + protected + + def self.app_middleware + Rails.application.middleware + end + end diff --git a/lib/locomotive/configuration.rb b/lib/locomotive/configuration.rb index 195ed596..b6dba527 100644 --- a/lib/locomotive/configuration.rb +++ b/lib/locomotive/configuration.rb @@ -16,7 +16,12 @@ module Locomotive :mailer_sender => 'support', #support@example.com' :manage_subdomain => false, :manage_manage_domains => false, - :lastest_items_nb => 5 + :lastest_items_nb => 5, + :rack_cache => { + :verbose => true, + :metastore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/meta"), # URI encoded in case of spaces + :entitystore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/body") + } } cattr_accessor :settings diff --git a/lib/locomotive/core_ext.rb b/lib/locomotive/core_ext.rb index 14896c06..817657b5 100644 --- a/lib/locomotive/core_ext.rb +++ b/lib/locomotive/core_ext.rb @@ -1,37 +1,15 @@ ## String class String - # def perma_string(sep = '_') - # ActiveSupport::Inflector.parameterize(self, sep).to_s - # end - def slugify(options = {}) - options = { :sep => '_', :without_extension => false, :downcase => false, :underscore => false }.merge(options) - # replace accented chars with ther ascii equivalents - s = ActiveSupport::Inflector.transliterate(self).to_s - # No more than one slash in a row - s.gsub!(/(\/[\/]+)/, '/') - # Remove leading or trailing space - s.strip! - # Remove leading or trailing slash - s.gsub! /(^[\/]+)|([\/]+$)/, '' - # Remove extensions - s.gsub! /(\.[a-zA-Z]{2,})/, '' if options[:without_extension] - # Downcase - s.downcase! if options[:downcase] - # Turn unwanted chars into the seperator - s.gsub!(/[^a-zA-Z0-9\-_\+\/]+/i, options[:sep]) - # Underscore - s.gsub!(/[\-]/i, '_') if options[:underscore] - s + def permalink + self.parameterize('-') end - def slugify!(options = {}) - replace(self.slugify(options)) + def permalink! + replace(self.permalink) end - def parameterize!(sep = '_') - replace(self.parameterize(sep)) - end + alias :parameterize! :permalink! end diff --git a/lib/locomotive/dragonfly.rb b/lib/locomotive/dragonfly.rb new file mode 100644 index 00000000..6f6b29c5 --- /dev/null +++ b/lib/locomotive/dragonfly.rb @@ -0,0 +1,36 @@ +module Locomotive + module Dragonfly + + def self.resize_url(source, resize_string) + file = nil + + if source.is_a?(String) || source.is_a?(Hash) # simple string or theme asset + source = source['url'] if source.is_a?(Hash) + + if source =~ /^http/ + file = self.app.fetch_url(source) + else + file = self.app.fetch_file(File.join('public', source)) + end + + elsif source.respond_to?(:url) # carrierwave uploader + if source.file.respond_to?(:url) + file = self.app.fetch_url(source.url) # amazon s3, cloud files, ...etc + else + file = self.app.fetch_file(source.path) + end + + else + Locomotive.logger.warning "Unable to resize on the fly: #{source.inspect}" + return + end + + file.process(:thumb, resize_string).url + end + + def self.app + ::Dragonfly[:images] + end + + end +end \ No newline at end of file diff --git a/lib/locomotive/engine.rb b/lib/locomotive/engine.rb index b1f233cc..e399e178 100644 --- a/lib/locomotive/engine.rb +++ b/lib/locomotive/engine.rb @@ -33,9 +33,10 @@ module Locomotive load "railties/tasks.rake" end - initializer "serving fonts" do |app| - app.middleware.insert_after Rack::Lock, '::Locomotive::Middlewares::Fonts', :path => %r{^/fonts} - end + # initializer "serving fonts" do |app| + # app.middleware.insert_after Rack::Lock, '::Locomotive::Middlewares::Fonts', :path => %r{^/fonts} + # app.middleware.insert_after Rack::Lock, '::Locomotive::Middlewares::SeoTrailingSlash' + # end end end diff --git a/lib/locomotive/hosting/base.rb b/lib/locomotive/hosting/base.rb new file mode 100644 index 00000000..73b3a85e --- /dev/null +++ b/lib/locomotive/hosting/base.rb @@ -0,0 +1,7 @@ +module Locomotive + module Hosting + class Base + + end + end +end \ No newline at end of file diff --git a/lib/locomotive/hosting/bushido.rb b/lib/locomotive/hosting/bushido.rb index fa11d805..1dd9d95a 100644 --- a/lib/locomotive/hosting/bushido.rb +++ b/lib/locomotive/hosting/bushido.rb @@ -1,110 +1,20 @@ -require 'bushido' -require 'locomotive/hosting/bushido/custom_domain' -require 'locomotive/hosting/bushido/first_installation' -require 'locomotive/hosting/bushido/account_ext' -require 'locomotive/hosting/bushido/middleware' -require 'locomotive/hosting/bushido/devise' +require 'locomotive/hosting/bushido/enabler' module Locomotive module Hosting module Bushido - extend ActiveSupport::Concern - - included do - class << self - attr_accessor :bushido_domains - attr_accessor :bushido_subdomain - end + def bushido? + self.config.hosting == :bushido || + (self.config.hosting == :auto && ENV['APP_TLD'] == 'bushi.do') end - module ClassMethods - - def bushido? - self.config.hosting == :bushido || - (self.config.hosting == :auto && ENV['APP_TLD'] == 'bushi.do') - end - - def bushido_app_claimed? - ENV['BUSHIDO_CLAIMED'].present? && ENV['BUSHIDO_CLAIMED'].to_s.downcase == 'true' - end - - def enable_bushido - self.config.domain = ENV['APP_TLD'] unless self.config.multi_sites? - - self.enhance_models_with_bushido - - self.disable_authentication_for_not_claimed_app - - self.setup_smtp_settings - - self.add_middleware - - self.config.delayed_job = true # force to use delayed_job - - self.bushido_domains = ::Bushido::App.domains - self.bushido_subdomain = ::Bushido::App.subdomain - end - - def enhance_models_with_bushido - Site.send :include, Locomotive::Hosting::Bushido::CustomDomain - Site.send :include, Locomotive::Hosting::Bushido::FirstInstallation - Account.send :include, Locomotive::Hosting::Bushido::AccountExt - end - - def disable_authentication_for_not_claimed_app - Admin::BaseController.send :include, Locomotive::Hosting::Bushido::Devise - end - - def setup_smtp_settings - ActionMailer::Base.delivery_method = :smtp - ActionMailer::Base.smtp_settings = { - :authentication => ENV['SMTP_AUTHENTICATION'], - :address => ENV['SMTP_SERVER'], - :port => ENV['SMTP_PORT'], - :domain => ENV['SMTP_DOMAIN'], - :user_name => ENV['SMTP_USER'], - :password => ENV['SMTP_PASSWORD'], - :enable_starttls_auto => ENV['SMTP_TLS'].to_s == 'true' - } - end - - def add_middleware - ::Locomotive::Application.configure do |config| - config.middleware.use '::Locomotive::Hosting::Bushido::Middleware' - end - end - - # manage domains - - def add_bushido_domain(name) - Locomotive.logger "[add bushido domain] #{name}" - ::Bushido::App.add_domain(name) - - if ::Bushido::Command.last_command_successful? - self.bushido_domains << name - end - end - - def remove_bushido_domain(name) - Locomotive.logger "[remove bushido domain] #{name}" - ::Bushido::App.remove_domain(name) - - if ::Bushido::Command.last_command_successful? - self.bushido_domains.delete(name) - end - end - - def set_bushido_subdomain(name) - Locomotive.logger "[set bushido subdomain] #{name}.bushi.do" - ::Bushido::App.set_subdomain(name) - - if ::Bushido::Command.last_command_successful? - self.bushido_subdomain = name - end - end + def enable_bushido + Locomotive.send(:include, Locomotive::Hosting::Bushido::Enabler) + self.enable_bushido! end + end end end \ No newline at end of file diff --git a/lib/locomotive/hosting/bushido/enabler.rb b/lib/locomotive/hosting/bushido/enabler.rb new file mode 100644 index 00000000..c4108757 --- /dev/null +++ b/lib/locomotive/hosting/bushido/enabler.rb @@ -0,0 +1,105 @@ +require 'bushido' +require 'locomotive/hosting/bushido/custom_domain' +require 'locomotive/hosting/bushido/first_installation' +require 'locomotive/hosting/bushido/account_ext' +require 'locomotive/hosting/bushido/middleware' +require 'locomotive/hosting/bushido/devise' + +module Locomotive + module Hosting + module Bushido + module Enabler + + extend ActiveSupport::Concern + + included do + class << self + attr_accessor :bushido_domains + attr_accessor :bushido_subdomain + end + end + + module ClassMethods + + def bushido_app_claimed? + ENV['BUSHIDO_CLAIMED'].present? && ENV['BUSHIDO_CLAIMED'].to_s.downcase == 'true' + end + + def enable_bushido! + self.config.domain = ENV['APP_TLD'] unless self.config.multi_sites? + + self.enhance_models + + self.disable_authentication_for_not_claimed_app + + self.setup_smtp_settings + + self.add_middlewares + + self.config.delayed_job = true # force to use delayed_job + + self.bushido_domains = ::Bushido::App.domains + self.bushido_subdomain = ::Bushido::App.subdomain + end + + def enhance_models + Site.send :include, Locomotive::Hosting::Bushido::CustomDomain + Site.send :include, Locomotive::Hosting::Bushido::FirstInstallation + Account.send :include, Locomotive::Hosting::Bushido::AccountExt + end + + def disable_authentication_for_not_claimed_app + Admin::BaseController.send :include, Locomotive::Hosting::Bushido::Devise + end + + def setup_smtp_settings + ActionMailer::Base.delivery_method = :smtp + ActionMailer::Base.smtp_settings = { + :authentication => ENV['SMTP_AUTHENTICATION'], + :address => ENV['SMTP_SERVER'], + :port => ENV['SMTP_PORT'], + :domain => ENV['SMTP_DOMAIN'], + :user_name => ENV['SMTP_USER'], + :password => ENV['SMTP_PASSWORD'], + :enable_starttls_auto => ENV['SMTP_TLS'].to_s == 'true' + } + end + + def add_middlewares + Rails.application.config.middleware.use '::Locomotive::Hosting::Bushido::Middleware' + end + + # manage domains + + def add_bushido_domain(name) + Locomotive.logger "[add bushido domain] #{name}" + ::Bushido::App.add_domain(name) + + if ::Bushido::Command.last_command_successful? + self.bushido_domains << name + end + end + + def remove_bushido_domain(name) + Locomotive.logger "[remove bushido domain] #{name}" + ::Bushido::App.remove_domain(name) + + if ::Bushido::Command.last_command_successful? + self.bushido_domains.delete(name) + end + end + + def set_bushido_subdomain(name) + Locomotive.logger "[set bushido subdomain] #{name}.bushi.do" + ::Bushido::App.set_subdomain(name) + + if ::Bushido::Command.last_command_successful? + self.bushido_subdomain = name + end + end + + end + end + end + end +end \ No newline at end of file diff --git a/lib/locomotive/hosting/bushido/middleware.rb b/lib/locomotive/hosting/bushido/middleware.rb index 10df1bf7..beb770ef 100644 --- a/lib/locomotive/hosting/bushido/middleware.rb +++ b/lib/locomotive/hosting/bushido/middleware.rb @@ -5,46 +5,36 @@ module Locomotive module Bushido class Middleware - # BUSHIDO_JS_URL = 'http://localhost:4567/javascripts/bushido.js' - BUSHIDO_JS_URL = 'http://bushi.do/api/bushido.js' - include Rack::Utils def initialize(app, opts = {}) @app = app - @bushido_app_name = ENV['BUSHIDO_APP'] - @bushido_metrics_token = ENV['BUSHIDO_METRICS_TOKEN'] - @bushido_claimed = ::Locomotive.bushido_app_claimed? + @path_regexp = %r{^/admin/} end def call(env) - status, headers, response = @app.call(env) + if env['PATH_INFO'] =~ @path_regexp + status, headers, response = @app.call(env) - content = "" - response.each { |part| content += part } + content = "" + response.each { |part| content += part } - # "claiming" bar + stats ? - content.gsub!(/<\/body>/i, <<-STR - - - STR - ) + # enable chat + content.gsub!(/<\/head>/i, <<-STR + + + STR + ) - headers['content-length'] = bytesize(content).to_s + headers['content-length'] = bytesize(content).to_s - [status, headers, [content]] + [status, headers, [content]] + else + @app.call(env) + end end end end end -end +end \ No newline at end of file diff --git a/lib/locomotive/hosting/default.rb b/lib/locomotive/hosting/default.rb new file mode 100644 index 00000000..8fd50f86 --- /dev/null +++ b/lib/locomotive/hosting/default.rb @@ -0,0 +1,11 @@ +module Locomotive + module Hosting + module Default + + def default_hosting? + true + end + + end + end +end \ No newline at end of file diff --git a/lib/locomotive/hosting/heroku.rb b/lib/locomotive/hosting/heroku.rb index da00d303..5fc825d7 100644 --- a/lib/locomotive/hosting/heroku.rb +++ b/lib/locomotive/hosting/heroku.rb @@ -1,80 +1,18 @@ -require 'heroku' -require 'heroku/client' -require 'locomotive/hosting/heroku/custom_domain' -require 'locomotive/hosting/heroku/first_installation' +require 'locomotive/hosting/heroku/enabler' module Locomotive module Hosting module Heroku - extend ActiveSupport::Concern - - included do - class << self - attr_accessor :heroku_connection - attr_accessor :heroku_domains - end + def heroku? + self.config.hosting == :heroku || + (self.config.hosting == :auto && ENV['HEROKU_SLUG'].present?) end - module ClassMethods - - def heroku? - self.config.hosting == :heroku || - (self.config.hosting == :auto && ENV['HEROKU_SLUG'].present?) - end - - def enable_heroku - self.config.domain = 'heroku.com' unless self.config.multi_sites? - - self.config.heroku ||= {} - self.config.heroku[:name] = ENV['APP_NAME'] - - raise 'Heroku application name is mandatory' if self.config.heroku[:name].blank? - - self.open_heroku_connection - - self.enhance_site_model_with_heroku - - self.apply_patches - - # "cache" domains for better performance - self.heroku_domains = self.heroku_connection.list_domains(self.config.heroku[:name]).collect { |h| h[:domain] } - end - - def open_heroku_connection - login = self.config.heroku[:login] || ENV['HEROKU_LOGIN'] - password = self.config.heroku[:password] || ENV['HEROKU_PASSWORD'] - - self.heroku_connection = ::Heroku::Client.new(login, password) - end - - def enhance_site_model_with_heroku - Site.send :include, Locomotive::Hosting::Heroku::CustomDomain - Site.send :include, Locomotive::Hosting::Heroku::FirstInstallation - end - - def apply_patches - # for various reasons, Heroku can modify the behaviour of an application by changing the gem versions (json/pure for instance) - # so the purpose of this method is to correct those potential differences. - - # http://blog.ethanvizitei.com/2010/11/json-pure-ruins-my-morning.html - Fixnum.class_eval { def to_json(options = nil); to_s; end } - end - - # manage domains - - def add_heroku_domain(name) - Locomotive.logger "[add heroku domain] #{name}" - self.heroku_connection.add_domain(self.config.heroku[:name], name) - self.heroku_domains << name - end - - def remove_heroku_domain(name) - Locomotive.logger "[remove heroku domain] #{name}" - self.heroku_connection.remove_domain(self.config.heroku[:name], name) - self.heroku_domains.delete(name) - end + def enable_heroku + Locomotive.send(:include, Locomotive::Hosting::Heroku::Enabler) + self.enable_heroku! end end diff --git a/lib/locomotive/hosting/heroku/enabler.rb b/lib/locomotive/hosting/heroku/enabler.rb new file mode 100644 index 00000000..24a81bc8 --- /dev/null +++ b/lib/locomotive/hosting/heroku/enabler.rb @@ -0,0 +1,84 @@ +require 'heroku' +require 'heroku/client' +require 'locomotive/hosting/heroku/custom_domain' +require 'locomotive/hosting/heroku/first_installation' + +module Locomotive + module Hosting + module Heroku + module Enabler + + extend ActiveSupport::Concern + + included do + class << self + attr_accessor :heroku_connection + attr_accessor :heroku_domains + end + end + + module ClassMethods + + def enable_heroku! + self.config.domain = 'heroku.com' unless self.config.multi_sites? + + self.config.heroku ||= {} + self.config.heroku[:name] = ENV['APP_NAME'] + + raise 'Heroku application name is mandatory' if self.config.heroku[:name].blank? + + self.open_heroku_connection + + self.enhance_site_model + + self.apply_patches + + # "cache" domains for better performance + self.heroku_domains = self.heroku_connection.list_domains(self.config.heroku[:name]).collect { |h| h[:domain] } + end + + def open_heroku_connection + login = self.config.heroku[:login] || ENV['HEROKU_LOGIN'] + password = self.config.heroku[:password] || ENV['HEROKU_PASSWORD'] + + self.heroku_connection = ::Heroku::Client.new(login, password) + end + + def enhance_site_model + Site.send :include, Locomotive::Hosting::Heroku::CustomDomain + Site.send :include, Locomotive::Hosting::Heroku::FirstInstallation + end + + def apply_patches + # for various reasons, Heroku can modify the behaviour of an application by changing the gem versions (json/pure for instance) + # so the purpose of this method is to correct those potential differences. + + # http://blog.ethanvizitei.com/2010/11/json-pure-ruins-my-morning.html + Fixnum.class_eval { def to_json(options = nil); to_s; end } + end + + # manage domains + + def add_heroku_domain(name) + Locomotive.logger "[add heroku domain] #{name}" + self.heroku_connection.add_domain(self.config.heroku[:name], name) + self.heroku_domains << name + end + + def remove_heroku_domain(name) + Locomotive.logger "[remove heroku domain] #{name}" + self.heroku_connection.remove_domain(self.config.heroku[:name], name) + self.heroku_domains.delete(name) + end + + # rack_cache: disabled because of Varnish + def rack_cache? + false + end + + end + + end + end + end +end \ No newline at end of file diff --git a/lib/locomotive/httparty/webservice.rb b/lib/locomotive/httparty/webservice.rb index 02881b71..f2753103 100644 --- a/lib/locomotive/httparty/webservice.rb +++ b/lib/locomotive/httparty/webservice.rb @@ -1,3 +1,5 @@ +require 'uri' + module Locomotive module Httparty class Webservice @@ -7,7 +9,11 @@ module Locomotive def self.consume(url, options = {}) url = HTTParty.normalize_base_uri(url) - options[:base_uri], path = url.scan(/^(http[s]?:\/\/.+\.[a-z]{2,})(\/.+)*/).first + uri = URI.parse(url) + options[:base_uri] = "#{uri.scheme}://#{uri.host}" + options[:base_uri] += ":#{uri.port}" if uri.port != 80 + path = uri.request_uri + options.delete(:format) if options[:format] == 'default' username, password = options.delete(:username), options.delete(:password) diff --git a/lib/locomotive/import.rb b/lib/locomotive/import.rb index a78381ce..2d42bee6 100644 --- a/lib/locomotive/import.rb +++ b/lib/locomotive/import.rb @@ -3,7 +3,6 @@ require 'locomotive/import/base' require 'locomotive/import/job' require 'locomotive/import/site' require 'locomotive/import/assets' -require 'locomotive/import/asset_collections' require 'locomotive/import/content_types' require 'locomotive/import/snippets' require 'locomotive/import/pages' diff --git a/lib/locomotive/import/asset_collections.rb b/lib/locomotive/import/asset_collections.rb deleted file mode 100644 index 1372e0aa..00000000 --- a/lib/locomotive/import/asset_collections.rb +++ /dev/null @@ -1,87 +0,0 @@ -module Locomotive - module Import - class AssetCollections < Base - - def process - asset_collections = database['site']['asset_collections'] - - return if asset_collections.nil? - - asset_collections.each do |name, attributes| - self.log "slug = #{attributes['slug']}" - - asset_collection = site.asset_collections.where(:slug => attributes['slug']).first - - asset_collection ||= self.build_asset_collection(attributes.merge(:name => name)) - - self.add_or_update_fields(asset_collection, attributes['fields']) - - if options[:samples] && attributes['assets'] - self.insert_samples(asset_collection, attributes['assets']) - end - - asset_collection.save! - - site.reload - end - end - - protected - - def build_asset_collection(data) - attributes = { :internal => false }.merge(data) - - attributes.delete_if { |name, value| %w{fields assets}.include?(name) } - - site.asset_collections.build(attributes) - end - - def add_or_update_fields(asset_collection, fields) - return if fields.blank? - - fields.each_with_index do |data, position| - name, data = data.keys.first, data.values.first - - attributes = { :_alias => name, :label => name.humanize, :kind => 'string', :position => position }.merge(data).symbolize_keys - - field = asset_collection.asset_custom_fields.detect { |f| f._alias == attributes[:_alias] } - - field ||= asset_collection.asset_custom_fields.build(attributes) - - field.send(:set_unique_name!) if field.new_record? - - field.attributes = attributes - end - end - - def insert_samples(asset_collection, assets) - assets.each_with_index do |data, position| - value, attributes = data.is_a?(Array) ? [data.first, data.last] : [data.keys.first, data.values.first] - - url = attributes.delete('url') - - # build with default attributes - asset = asset_collection.assets.build(:name => value, :position => position, :source => self.open_sample_asset(url)) - - attributes.each do |name, value| - field = asset_collection.asset_custom_fields.detect { |f| f._alias == name } - - value = (case field.kind.downcase - when 'file' then self.open_sample_asset(value) - when 'boolean' then Boolean.set(value) - else - value - end) - - asset.send("#{name}=", value) - end - - asset.save - - self.log "insert asset '#{asset.name}'" - end - end - - end - end -end \ No newline at end of file diff --git a/lib/locomotive/import/assets.rb b/lib/locomotive/import/assets.rb index 216850bf..98eb4485 100644 --- a/lib/locomotive/import/assets.rb +++ b/lib/locomotive/import/assets.rb @@ -42,8 +42,6 @@ module Locomotive end def add_other_assets - collection = AssetCollection.find_or_create_internal(site) - Dir[File.join(theme_path, 'public', 'samples', '*')].each do |asset_path| next if File.directory?(asset_path) @@ -52,7 +50,7 @@ module Locomotive name = File.basename(asset_path, File.extname(asset_path)).parameterize('_') - collection.assets.create! :name => name, :source => File.open(asset_path) + self.site.assets.create! :name => name, :source => File.open(asset_path) end end diff --git a/lib/locomotive/import/content_types.rb b/lib/locomotive/import/content_types.rb index 0665671e..4192eec8 100644 --- a/lib/locomotive/import/content_types.rb +++ b/lib/locomotive/import/content_types.rb @@ -1,3 +1,5 @@ +require 'ostruct' + module Locomotive module Import class ContentTypes < Base @@ -5,6 +7,8 @@ module Locomotive def process return if content_types.nil? + contents_with_associations, content_types_with_associations = [], [] + content_types.each do |name, attributes| self.log "[content_types] slug = #{attributes['slug']}" @@ -14,6 +18,10 @@ module Locomotive self.add_or_update_fields(content_type, attributes['fields']) + if content_type.content_custom_fields.any? { |f| ['has_many', 'has_one'].include?(f.kind) } + content_types_with_associations << content_type + end + self.set_highlighted_field_name(content_type) self.set_order_by_value(content_type) @@ -21,17 +29,20 @@ module Locomotive self.set_group_by_value(content_type) if options[:samples] && attributes['contents'] - self.insert_samples(content_type, attributes['contents']) + contents_with_associations += self.insert_samples(content_type, attributes['contents']) end content_type.save! - - site.reload end + # look up for associations and replace their target field by the real class name + self.replace_target(content_types_with_associations) + + # update all the contents with associations now that every content is stored in mongodb + self.insert_samples_with_associations(contents_with_associations) + # invalidate the cache of the dynamic classes (custom fields) - ContentType.all.collect(&:invalidate_content_klass) - AssetCollection.all.collect(&:invalidate_asset_klass) + site.content_types.all.collect { |c| c.invalidate_content_klass; c.fetch_content_klass } end protected @@ -61,13 +72,33 @@ module Locomotive field.send(:set_unique_name!) if field.new_record? field.attributes = attributes + + field[:kind] = field[:kind].downcase # old versions of the kind are capitalized + end + end + + def replace_target(content_types) + content_types.each do |content_type| + content_type.content_custom_fields.each do |field| + next unless ['has_many', 'has_one'].include?(field.kind) + + target_content_type = site.content_types.where(:name => field.target).first + + field.target = target_content_type.content_klass.to_s if target_content_type + end + + content_type.save end end def insert_samples(content_type, contents) + contents_with_associations = [] + contents.each_with_index do |data, position| value, attributes = data.is_a?(Array) ? [data.first, data.last] : [data.keys.first, data.values.first] + associations = [] + # build with default attributes content = content_type.contents.build(content_type.highlighted_field_name.to_sym => value, :_position_in_list => position) @@ -76,7 +107,14 @@ module Locomotive next if field.nil? # the attribute name is not related to a field (name misspelled ?) - value = (case field.kind.downcase + kind = field.kind + + if ['has_many', 'has_one'].include?(kind) + associations << OpenStruct.new(:name => name, :kind => kind, :value => value, :target => field.target) + next + end + + value = (case kind when 'file' then self.open_sample_asset(value) when 'boolean' then Boolean.set(value) when 'date' then value.is_a?(Date) ? value : Date.parse(value) @@ -85,17 +123,50 @@ module Locomotive field.category_items.build :name => value end value - else + else # string, text value end) content.send("#{name}=", value) end - content.save + content.save(:validate => false) + + contents_with_associations << [content, associations] unless associations.empty? self.log "insert content '#{content.send(content_type.highlighted_field_name.to_sym)}'" end + + contents_with_associations + end + + def insert_samples_with_associations(contents) + contents.each do |content_information| + next if content_information.empty? + + content, associations = content_information + + content = content._parent.reload.contents.find(content._id) # target should be updated + + associations.each do |association| + target_content_type = site.content_types.where(:name => association.target).first + + next if target_content_type.nil? + + value = (case association.kind + when 'has_one' then + target_content_type.contents.detect { |c| c.highlighted_field_value == association.value } + when 'has_many' then + association.value.collect do |v| + target_content_type.contents.detect { |c| c.highlighted_field_value == v }._id + end + end) + + content.send("#{association.name}=", value) + end + + content.save + end end def set_highlighted_field_name(content_type) diff --git a/lib/locomotive/import/job.rb b/lib/locomotive/import/job.rb index 5be365be..4d79d82e 100644 --- a/lib/locomotive/import/job.rb +++ b/lib/locomotive/import/job.rb @@ -30,7 +30,7 @@ module Locomotive self.unzip! - raise "No database.yml found in the theme zipfile" if @database.nil? + raise "No config/compiled_site.yml found in the theme zipfile" if @database.nil? context = { :database => @database, @@ -42,7 +42,7 @@ module Locomotive self.reset! if @options[:reset] - %w(site content_types assets asset_collections snippets pages).each do |step| + %w(site content_types assets snippets pages).each do |step| if @options[:enabled][step] != false "Locomotive::Import::#{step.camelize}".constantize.process(context, @options) @worker.update_attributes :step => step if @worker @@ -145,10 +145,10 @@ module Locomotive zipfile.each do |entry| next if entry.name =~ /__MACOSX/ - if entry.name =~ /database.yml$/ + if entry.name =~ /config\/compiled_site.yml$/ @database = YAML.load(zipfile.read(entry.name)) - @theme_path = File.join(destination_path, entry.name.gsub('database.yml', '')) + @theme_path = File.join(destination_path, entry.name.gsub('config/compiled_site.yml', '')) next end @@ -162,9 +162,9 @@ module Locomotive def reset! @site.pages.destroy_all + @site.assets.destroy_all @site.theme_assets.destroy_all @site.content_types.destroy_all - @site.asset_collections.destroy_all end def get_uploader(site) diff --git a/lib/locomotive/import/pages.rb b/lib/locomotive/import/pages.rb index 3647af7b..4ebce0eb 100644 --- a/lib/locomotive/import/pages.rb +++ b/lib/locomotive/import/pages.rb @@ -9,9 +9,9 @@ module Locomotive self.add_page('index') - Dir[File.join(theme_path, 'templates', '**/*')].each do |template_path| + Dir[File.join(theme_path, 'app', 'views', 'pages', '**/*')].each do |template_path| - fullpath = template_path.gsub(File.join(theme_path, 'templates'), '').gsub('.liquid', '').gsub(/^\//, '') + fullpath = template_path.gsub(File.join(theme_path, 'app', 'views', 'pages'), '').gsub('.liquid', '').gsub(/^\//, '') next if %w(index 404).include?(fullpath) @@ -37,7 +37,7 @@ module Locomotive return page if page # already added, so skip it - template = File.read(File.join(theme_path, 'templates', "#{fullpath}.liquid")) rescue "Unable to find #{fullpath}.liquid" + template = File.read(File.join(theme_path, 'app', 'views', 'pages', "#{fullpath}.liquid")) rescue "Unable to find #{fullpath}.liquid" self.replace_images!(template) @@ -146,8 +146,6 @@ module Locomotive fullpath = data.keys.first.to_s unless %w(index 404).include?(fullpath) - # position = fullpath == 'index' ? 0 : 1 - # else (segments = fullpath.split('/')).pop position_key = segments.empty? ? 'index' : segments.join('/') diff --git a/lib/locomotive/import/site.rb b/lib/locomotive/import/site.rb index bf93be04..ffe389db 100644 --- a/lib/locomotive/import/site.rb +++ b/lib/locomotive/import/site.rb @@ -3,7 +3,7 @@ module Locomotive class Site < Base def process - attributes = database['site'].clone.delete_if { |name, value| %w{name pages assets content_types asset_collections}.include?(name) } + attributes = database['site'].clone.delete_if { |name, value| %w{name pages assets content_types}.include?(name) } site.attributes = attributes diff --git a/lib/locomotive/import/snippets.rb b/lib/locomotive/import/snippets.rb index abab4f2f..2208449f 100644 --- a/lib/locomotive/import/snippets.rb +++ b/lib/locomotive/import/snippets.rb @@ -3,14 +3,14 @@ module Locomotive class Snippets < Base def process - Dir[File.join(theme_path, 'snippets', '*')].each do |snippet_path| + Dir[File.join(theme_path, 'app', 'views', 'snippets', '*')].each do |snippet_path| self.log "path = #{snippet_path}" name = File.basename(snippet_path, File.extname(snippet_path)).parameterize('_') snippet = site.snippets.where(:slug => name).first || site.snippets.build(:name => name) - snippet.template = File.read(snippet_path) # = site.snippets.create! :name => name, :template => + snippet.template = File.read(snippet_path) snippet.save! end diff --git a/lib/locomotive/liquid/drops/asset.rb b/lib/locomotive/liquid/drops/asset.rb deleted file mode 100644 index 5049721d..00000000 --- a/lib/locomotive/liquid/drops/asset.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Locomotive - module Liquid - module Drops - class Asset < Base - - def before_method(meth) - return '' if @source.nil? - - if not @@forbidden_attributes.include?(meth.to_s) - value = @source.send(meth) - end - end - - def url - @source.source.url - end - - end - end - end -end diff --git a/lib/locomotive/liquid/drops/asset_collections.rb b/lib/locomotive/liquid/drops/asset_collections.rb index ef1df315..99f0deca 100644 --- a/lib/locomotive/liquid/drops/asset_collections.rb +++ b/lib/locomotive/liquid/drops/asset_collections.rb @@ -1,54 +1,14 @@ +require 'locomotive/liquid/drops/contents' + module Locomotive module Liquid module Drops - class AssetCollections < ::Liquid::Drop + class AssetCollections < Contents def before_method(meth) - collection = @context.registers[:site].asset_collections.where(:slug => meth.to_s).first - AssetCollectionProxy.new(collection) - end - - end - - class AssetCollectionProxy < ::Liquid::Drop - - def initialize(collection) - @collection = collection - end - - def first - @collection.ordered_assets.first - end - - def last - @collection.ordered_assets.last - end - - def each(&block) - @collection.ordered_assets.each(&block) - end - - def paginate(options = {}) - paginated_collection = @collection.ordered_assets.paginate(options) - { - :collection => paginated_collection, - :current_page => paginated_collection.current_page, - :previous_page => paginated_collection.previous_page, - :next_page => paginated_collection.next_page, - :total_entries => paginated_collection.total_entries, - :total_pages => paginated_collection.total_pages, - :per_page => paginated_collection.per_page - } - end - - def size - @collection.assets.size - end - - def before_method(meth) - return '' if @collection.nil? - @collection.send(meth) + Rails.logger.warn "\n[WARNING] asset_collections is deprecated and will be removed in the next commits. Please, use contents. instead.\n\n" + super(meth) end end diff --git a/lib/locomotive/liquid/drops/base.rb b/lib/locomotive/liquid/drops/base.rb index 731a75a5..8ba0d8a9 100644 --- a/lib/locomotive/liquid/drops/base.rb +++ b/lib/locomotive/liquid/drops/base.rb @@ -8,18 +8,18 @@ module Locomotive class_inheritable_reader :liquid_attributes write_inheritable_attribute :liquid_attributes, [] - attr_reader :source - delegate :hash, :to => :source + attr_reader :_source + delegate :hash, :to => :_source def initialize(source) unless source.nil? - @source = source - @liquid = liquid_attributes.flatten.inject({}) { |h, k| h.update k.to_s => @source.send(k) } + @_source = source + @liquid = liquid_attributes.flatten.inject({}) { |h, k| h.update k.to_s => @_source.send(k) } end end def id - (@source.respond_to?(:id) ? @source.id : nil) || 'new' + (@_source.respond_to?(:id) ? @_source.id : nil) || 'new' end def before_method(method) diff --git a/lib/locomotive/liquid/drops/content.rb b/lib/locomotive/liquid/drops/content.rb index 5525bbfc..1475f47b 100644 --- a/lib/locomotive/liquid/drops/content.rb +++ b/lib/locomotive/liquid/drops/content.rb @@ -2,22 +2,22 @@ module Locomotive module Liquid module Drops class Content < Base - delegate :meta_keywords, :meta_description, :to => '@source' + delegate :seo_title, :meta_keywords, :meta_description, :to => '_source' def _id - @source._id.to_s + self._source._id.to_s end def before_method(meth) - return '' if @source.nil? + return '' if self._source.nil? if not @@forbidden_attributes.include?(meth.to_s) - value = @source.send(meth) + value = self._source.send(meth) end end def highlighted_field_value - @source.highlighted_field_value + self._source.highlighted_field_value end end diff --git a/lib/locomotive/liquid/drops/page.rb b/lib/locomotive/liquid/drops/page.rb index a6b092a8..18aa0def 100644 --- a/lib/locomotive/liquid/drops/page.rb +++ b/lib/locomotive/liquid/drops/page.rb @@ -2,26 +2,26 @@ module Locomotive module Liquid module Drops class Page < Base - delegate :meta_keywords, :meta_description, :to => "@source" + delegate :seo_title, :meta_keywords, :meta_description, :to => "_source" def title - @source.templatized? ? @context['content_instance'].highlighted_field_value : @source.title + self._source.templatized? ? @context['content_instance'].highlighted_field_value : self._source.title end def slug - @source.templatized? ? @source.content_type.slug.singularize : @source.slug + self._source.templatized? ? self._source.content_type.slug.singularize : self._source.slug end def children - @children ||= liquify(*@source.children) + @children ||= liquify(*self._source.children) end def fullpath - @fullpath ||= @source.fullpath + @fullpath ||= self._source.fullpath end def depth - @source.depth + self._source.depth end end diff --git a/lib/locomotive/liquid/drops/site.rb b/lib/locomotive/liquid/drops/site.rb index 07f040b8..1ff8f8d2 100644 --- a/lib/locomotive/liquid/drops/site.rb +++ b/lib/locomotive/liquid/drops/site.rb @@ -3,14 +3,14 @@ module Locomotive module Drops class Site < Base - liquid_attributes << :name << :meta_keywords << :meta_description + liquid_attributes << :name << :seo_title << :meta_keywords << :meta_description def index - @index ||= @source.pages.root.first + @index ||= self._source.pages.root.first end def pages - @pages ||= @source.pages.to_a.collect(&:to_liquid) + @pages ||= self._source.pages.to_a.collect(&:to_liquid) end end diff --git a/lib/locomotive/liquid/filters/resize.rb b/lib/locomotive/liquid/filters/resize.rb new file mode 100644 index 00000000..0672ed52 --- /dev/null +++ b/lib/locomotive/liquid/filters/resize.rb @@ -0,0 +1,16 @@ +module Locomotive + module Liquid + module Filters + module Resize + + def resize(input, resize_string) + Locomotive::Dragonfly.resize_url(input, resize_string) + end + + end + + ::Liquid::Template.register_filter(Resize) + + end + end +end \ No newline at end of file diff --git a/lib/locomotive/liquid/tags/seo.rb b/lib/locomotive/liquid/tags/seo.rb new file mode 100644 index 00000000..5a93a98f --- /dev/null +++ b/lib/locomotive/liquid/tags/seo.rb @@ -0,0 +1,72 @@ +module Locomotive + module Liquid + module Tags + module SEO + + class Base < ::Liquid::Tag + + def render(context) + %{ + #{self.render_title(context)} + #{self.render_metadata(context)} + } + end + + protected + + def render_title(context) + title = self.value_for(:seo_title, context) + title = context.registers[:site].name if title.blank? + + %{ + #{title} + } + end + + def render_metadata(context) + %{ + + + } + end + + # Removes whitespace and quote charactets from the input + def sanitized_string(string) + string ? string.strip.gsub(/"/, '') : '' + end + + def value_for(attribute, context) + object = self.metadata_object(context) + value = object.try(attribute.to_sym).blank? ? context.registers[:site].send(attribute.to_sym) : object.send(attribute.to_sym) + self.sanitized_string(value) + end + + def metadata_object(context) + context['content_instance'] || context['page'] + end + end + + class Title < Base + + def render(context) + self.render_title(context) + end + + end + + class Metadata < Base + + def render(context) + self.render_metadata(context) + end + + end + + end + + ::Liquid::Template.register_tag('seo', SEO::Base) + ::Liquid::Template.register_tag('seo_title', SEO::Title) + ::Liquid::Template.register_tag('seo_metadata', SEO::Metadata) + end + end +end \ No newline at end of file diff --git a/lib/locomotive/liquid/tags/seo_metadata.rb b/lib/locomotive/liquid/tags/seo_metadata.rb deleted file mode 100644 index 5958b47d..00000000 --- a/lib/locomotive/liquid/tags/seo_metadata.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Locomotive - module Liquid - module Tags - class SEOMetadata < ::Liquid::Tag - - def render(context) - %{ - - - } - end - - # Removes whitespace and quote charactets from the input - def sanitized_string(string) - string.strip.gsub(/"/, '') - end - - def meta_description(context) - object = metadata_object(context) - object.try(:meta_description).blank? ? context.registers[:site].meta_description : object.meta_description - end - - def meta_keywords(context) - object = metadata_object(context) - object.try(:meta_keywords).blank? ? context.registers[:site].meta_keywords : object.meta_keywords - end - - def metadata_object(context) - context['content_instance'] || context['page'] - end - end - - ::Liquid::Template.register_tag('seo_metadata', SEOMetadata) - end - end -end \ No newline at end of file diff --git a/lib/locomotive/middlewares.rb b/lib/locomotive/middlewares.rb index e241b9d3..dbbe5e93 100644 --- a/lib/locomotive/middlewares.rb +++ b/lib/locomotive/middlewares.rb @@ -1 +1,3 @@ -require 'locomotive/middlewares/fonts' \ No newline at end of file +require 'locomotive/middlewares/fonts' +require 'locomotive/middlewares/seo_trailing_slash' +require 'locomotive/middlewares/cache' \ No newline at end of file diff --git a/lib/locomotive/middlewares/cache.rb b/lib/locomotive/middlewares/cache.rb new file mode 100644 index 00000000..b82e197c --- /dev/null +++ b/lib/locomotive/middlewares/cache.rb @@ -0,0 +1,28 @@ +require 'rack/cache' + +module Locomotive + module Middlewares + class Cache + + def initialize(app, opts = {}, &block) + url_format = Locomotive::Dragonfly.app.configuration[:url_format] + + base_format = url_format.split('/:').first + + @regexp = %r{^#{base_format}/} + @app = app + @context = ::Rack::Cache.new(app, opts, &block) + end + + def call(env) + if env['PATH_INFO'] =~ @regexp + @context.call(env) + else + @app.call(env) + end + end + + end + + end +end diff --git a/lib/locomotive/middlewares/seo_trailing_slash.rb b/lib/locomotive/middlewares/seo_trailing_slash.rb new file mode 100644 index 00000000..6f5a1918 --- /dev/null +++ b/lib/locomotive/middlewares/seo_trailing_slash.rb @@ -0,0 +1,24 @@ +module Locomotive + module Middlewares + class SeoTrailingSlash + + def initialize(app, opts = {}) + @app = app + end + + def call(env) + path = env['PATH_INFO'] + + if !path.starts_with('/admin/') && (match = path.match(%r{(.+)/$})) + response = Rack::Response.new + response.redirect(match[1], 301) # moved permanently + response.finish + response.to_a + else + @app.call(env) + end + end + + end + end +end diff --git a/lib/locomotive/render.rb b/lib/locomotive/render.rb index c8d10e9f..7fd2daf2 100644 --- a/lib/locomotive/render.rb +++ b/lib/locomotive/render.rb @@ -59,11 +59,12 @@ module Locomotive assigns = { 'site' => current_site, 'page' => @page, - 'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new, + 'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new, # depracated, will be removed shortly 'contents' => Locomotive::Liquid::Drops::Contents.new, 'current_page' => self.params[:page], 'params' => self.params, - 'url' => request.url + 'url' => request.url, + 'now' => Date.today }.merge(flash.stringify_keys) # data from api if @page.templatized? # add instance from content type @@ -107,7 +108,7 @@ module Locomotive end def page_status - @page == not_found_page ? :not_found : :ok + @page.not_found? ? :not_found : :ok end end diff --git a/lib/locomotive/session_store.rb b/lib/locomotive/session_store.rb index ec99407c..d4139ef0 100644 --- a/lib/locomotive/session_store.rb +++ b/lib/locomotive/session_store.rb @@ -54,7 +54,7 @@ module ActionDispatch def destroy(env) session = @@session_class.first(:conditions => { :_id => env[SESSION_RECORD_KEY].id }) - session.destroy + session.try(:destroy) env[SESSION_RECORD_KEY] = nil end diff --git a/lib/tasks/locomotive.rake b/lib/tasks/locomotive.rake index 227de113..4e89182a 100644 --- a/lib/tasks/locomotive.rake +++ b/lib/tasks/locomotive.rake @@ -1,3 +1,7 @@ +# encoding: utf-8 + +require 'locomotive' + namespace :locomotive do desc 'Fetch the Locomotive default site template for the installation' @@ -7,6 +11,168 @@ namespace :locomotive do puts '...done' end + namespace :upgrade do + + desc 'Set roles to the existing users' + task :set_roles => :environment do + Site.all.each do |site| + site.memberships.each do |membership| + if membership.attributes['admin'] == true + puts "...[#{site.name}] #{membership.account.name} has now the admin role" + membership.role = 'admin' + else + puts "...[#{site.name}] #{membership.account.name} has now the author role" + membership.role = 'author' + end + end + site.save + end + end + + desc 'Remove asset collections and convert them into content types' + task :remove_asset_collections => :environment do + puts "Processing #{AssetCollection.count} asset collection(s)..." + + Asset.destroy_all # TODO + + AssetCollection.all.each do |collection| + site = Site.find(collection.attributes['site_id']) + + if collection.internal? + # internal collection => create simple assets without associated to a collection + + collection.assets.each do |tmp_asset| + # puts "tmp asset = #{tmp_asset.inspect} / #{tmp_asset.source.url.inspect}" TODO + + sanitized_attributes = tmp_asset.attributes.dup + sanitized_attributes[:_id] = tmp_asset._id + + asset = site.assets.build(sanitized_attributes) + + asset.save(:validate => false) + + # puts "asset = #{asset.inspect} / #{asset.source.url.inspect}" TODO + end + else + collection.fetch_asset_klass.class_eval { def self.model_name; 'Asset'; end } + + # create content_types reflection of an asset collection + ContentType.where(:slug => collection.slug).all.collect(&:destroy) # TODO + + content_type = site.content_types.build({ + :name => collection.name, + :slug => collection.slug, + :order_by => '_position_in_list' + }) + + content_type._id = collection._id + + # extra custom fields + collection.asset_custom_fields.each_with_index do |field, i| + content_type.content_custom_fields.build(field.attributes.merge(:position => i + 3)) + end + + # add default custom fields + content_type.content_custom_fields.build(:label => 'Name', :_alias => 'name', :kind => 'string', :required => true, :position => 1) + content_type.content_custom_fields.build(:label => 'Source', :_alias => 'source', :kind => 'file', :required => true, :position => 2) + + content_type.save! + + content_type = ContentType.find(content_type._id) # hard reload + + # set the highlighted field name + field = content_type.content_custom_fields.detect { |f| f._alias == 'name' } + content_type.highlighted_field_name = field._name + content_type.save + + # insert data + collection.ordered_assets.each do |asset| + attributes = (if asset.custom_fields.blank? + { :created_at => asset.created_at, :updated_at => asset.updated_at } + else + {}.tap do |h| + asset.custom_fields.each do |field| + case field.kind + when 'file' then h["#{field._name}"] = asset.send("#{field._name}".to_sym) + else + h[field._alias] = asset.send(field._name.to_sym) + end + end + end + end) + + attributes.merge!(:name => asset.name, :_position_in_list => asset.position) + + content = content_type.contents.build(attributes) + + content._id = asset._id + + content.source = asset.source.file + + content.save(:validate => false) + end + end + + puts "...the collection named '#{collection.slug}' for the '#{site.name}' site has been migrated with success !" + end + end + + end + end +class TmpAssetUploader < CarrierWave::Uploader::Base + include CarrierWave::RMagick + include Locomotive::CarrierWave::Uploader::Asset + version :thumb, :if => :image? do + process :resize_to_fill => [50, 50] + process :convert => 'png' + end + version :medium, :if => :image? do + process :resize_to_fill => [80, 80] + process :convert => 'png' + end + version :preview, :if => :image? do + process :resize_to_fit => [880, 1100] + process :convert => 'png' + end + def store_dir + self.build_store_dir('sites', model.collection.site_id, 'assets', model.id) + end +end + +# Classes only used during the upgrade mechanism. Will be removed in a few weeks +class AssetCollection + include Locomotive::Mongoid::Document + field :name + field :slug + field :internal, :type => Boolean, :default => false + referenced_in :site + embeds_many :assets, :class_name => 'TmpAsset', :validate => false + custom_fields_for :assets + after_destroy :remove_uploaded_files + scope :internal, :where => { :internal => true } + scope :not_internal, :where => { :internal => false } + def ordered_assets + self.assets.sort { |a, b| (a.position || 0) <=> (b.position || 0) } + end + def self.find_or_create_internal(site) + site.asset_collections.internal.first || site.asset_collections.create(:name => 'system', :slug => 'system', :internal => true) + end +end + +class TmpAsset + include Mongoid::Document + include Mongoid::Timestamps + include CustomFields::ProxyClassEnabler + field :name, :type => String + field :content_type, :type => String + field :width, :type => Integer + field :height, :type => Integer + field :size, :type => Integer + field :position, :type => Integer, :default => 0 + mount_uploader :source, TmpAssetUploader + embedded_in :collection, :class_name => 'AssetCollection', :inverse_of => :assets + def site_id; self.collection.site_id; end +end diff --git a/locomotive_cms.gemspec b/locomotive_cms.gemspec index 3b824169..ef729eb0 100644 --- a/locomotive_cms.gemspec +++ b/locomotive_cms.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = ">= 1.3.6" s.rubyforge_project = "nowarning" - s.add_dependency "rails", ">= 3.0.8" + s.add_dependency "rails", ">= 3.0.9" s.add_dependency "warden" s.add_dependency "devise", "1.3.4" s.add_dependency "mongoid", "2.0.2" @@ -36,9 +36,12 @@ Gem::Specification.new do |s| s.add_dependency "heroku", "1.19.1" s.add_dependency "rmagick", "2.12.2" - s.add_dependency "locomotive_carrierwave", "0.5.4.beta2" + s.add_dependency "locomotive_carrierwave", "0.5.4.beta3" + s.add_dependency "dragonfly", "~> 0.9.1" + s.add_dependency "rack-cache" - s.add_dependency "custom_fields", "1.0.0.beta.18" + s.add_dependency "custom_fields", "1.0.0.beta.19" + s.add_dependency "cancan", "~> 1.6.0" s.add_dependency "fog", "0.8.2" s.add_dependency "mimetype-fu" s.add_dependency "actionmailer-with-request" diff --git a/public/images/admin/icons/asset_add.png b/public/images/admin/icons/asset_add.png new file mode 100644 index 00000000..b7a08586 Binary files /dev/null and b/public/images/admin/icons/asset_add.png differ diff --git a/public/images/admin/icons/asset_switch.png b/public/images/admin/icons/asset_switch.png new file mode 100644 index 00000000..f5325c2d Binary files /dev/null and b/public/images/admin/icons/asset_switch.png differ diff --git a/public/images/admin/icons/membership_edit.png b/public/images/admin/icons/membership_edit.png new file mode 100644 index 00000000..59cb94b7 Binary files /dev/null and b/public/images/admin/icons/membership_edit.png differ diff --git a/public/images/admin/icons/membership_lock.png b/public/images/admin/icons/membership_lock.png new file mode 100644 index 00000000..70615628 Binary files /dev/null and b/public/images/admin/icons/membership_lock.png differ diff --git a/public/javascripts/admin/application.js b/public/javascripts/admin/application.js index 5c789ccd..26e5c7f5 100644 --- a/public/javascripts/admin/application.js +++ b/public/javascripts/admin/application.js @@ -48,9 +48,10 @@ var TinyMceDefaultSettings = { script_url : '/javascripts/admin/plugins/tiny_mce/tiny_mce.js', theme : 'advanced', skin : 'locomotive', - plugins: 'safari,inlinepopups,locoimage', + plugins: 'safari,inlinepopups,locomedia', + extended_valid_elements: 'iframe[width|height|frameborder|allowfullscreen|src|title]', theme_advanced_buttons1 : 'code,|,bold,italic,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,|,outdent,indent,blockquote,|,link,unlink', - theme_advanced_buttons2 : 'formatselect,fontselect,fontsizeselect,|,image', + theme_advanced_buttons2 : 'formatselect,fontselect,fontsizeselect,|,locomedia', theme_advanced_buttons3 : '', theme_advanced_toolbar_location : "top", theme_advanced_toolbar_align : "left", diff --git a/public/javascripts/admin/asset_collections.js b/public/javascripts/admin/asset_collections.js deleted file mode 100644 index 0deaa1be..00000000 --- a/public/javascripts/admin/asset_collections.js +++ /dev/null @@ -1,55 +0,0 @@ -$(document).ready(function() { - - // automatic slug from collection name - $('#asset_collection_name').keypress(function() { - var input = $(this); - var slug = $('#asset_collection_slug'); - - if (!slug.hasClass('filled')) { - setTimeout(function() { - slug.val(input.val().replace(/\s/g, '_').toLowerCase()); - }, 50); - } - }); - - $('#asset_collection_slug').keypress(function() { $(this).addClass('filled'); }); - - // sortable assets - - var updateAssetsOrder = function() { - var list = $('ul.assets.sortable'); - var ids = jQuery.map(list.sortable('toArray'), function(e) { - return e.match(/asset-(\w+)/)[1]; - }).join(','); - $('#asset_collection_assets_order').val(ids || ''); - } - - var setLastClassForAssets = function() { - $('ul.assets li.last').removeClass('last'); - var i = parseInt($('ul.assets li.asset').size() / 6); - while (i > 0) { - $('ul.assets li.asset:eq(' + (i * 6 - 1) + ')').addClass('last'); - i--; - } - } - - $('ul.assets.sortable').sortable({ - items: 'li.asset', - stop: function(event, ui) { updateAssetsOrder(); setLastClassForAssets(); } - }); - - $('ul.assets.sortable li div.actions a.remove').click(function(e) { - if (confirm($(this).attr('data-confirm'))) { - $(this).parents('li').remove(); - - updateAssetsOrder(); - - if ($('ul.assets li.asset').size() == 0) $('p.no-items').show(); - - setLastClassForAssets(); - } - e.preventDefault(); - e.stopPropagation(); - }); - -}); diff --git a/public/javascripts/admin/assets.js b/public/javascripts/admin/assets.js deleted file mode 100644 index 326ab781..00000000 --- a/public/javascripts/admin/assets.js +++ /dev/null @@ -1,5 +0,0 @@ -$(document).ready(function() { - - $('textarea.html').tinymce(TinyMceDefaultSettings); - -}); diff --git a/public/javascripts/admin/contents.js b/public/javascripts/admin/contents.js index 90e5cb1c..4ae128f5 100644 --- a/public/javascripts/admin/contents.js +++ b/public/javascripts/admin/contents.js @@ -1,18 +1,20 @@ $(document).ready(function() { - var updateContentsOrder = function() { - var lists = $('ul#contents-list.sortable'); - var ids = jQuery.map(lists, function(list) { - return(jQuery.map($(list).sortable('toArray'), function(el) { - return el.match(/content-(\w+)/)[1]; - }).join(',')); - }).join(','); - $('#order').val(ids || ''); - } + // sortable items $('ul#contents-list.sortable').sortable({ - handle: 'em', - items: 'li.content', - stop: function(event, ui) { updateContentsOrder(); } + 'handle': 'em', + 'items': 'li.content', + 'axis': 'y', + 'update': function(event, ui) { + var params = $(this).sortable('serialize', { 'key': 'children[]' }); + params += '&_method=put'; + params += '&' + $('meta[name=csrf-param]').attr('content') + '=' + $('meta[name=csrf-token]').attr('content'); + + $.post($(this).attr('data-url'), params, function(data) { + var error = typeof(data.error) != 'undefined'; + $.growl((error ? 'error' : 'success'), (error ? data.error : data.notice)); + }, 'json'); + } }); try { diff --git a/public/javascripts/admin/custom_fields.js b/public/javascripts/admin/custom_fields.js index fd9e4b64..b94ea53f 100644 --- a/public/javascripts/admin/custom_fields.js +++ b/public/javascripts/admin/custom_fields.js @@ -59,8 +59,10 @@ $(document).ready(function() { onComplete: function() { $('#fancybox-wrap .popup-actions button[type=submit]').click(function(e) { $.each(attributes, function(index, name) { - var val = domBoxAttrVal(name).trim(); - if (val != '') domFieldVal(domField, name, val); + try { + var val = domBoxAttrVal(name).trim(); + if (val != '') domFieldVal(domField, name, val); + } catch(e) {} }); domBoxAttr('text_formatting').parent().hide(); diff --git a/public/javascripts/admin/custom_fields/has_many.js b/public/javascripts/admin/custom_fields/has_many.js index c66001ee..e749a36b 100644 --- a/public/javascripts/admin/custom_fields/has_many.js +++ b/public/javascripts/admin/custom_fields/has_many.js @@ -7,12 +7,28 @@ $(document).ready(function() { $.fn.hasManySelector = function(options) { var populateSelect = function(context) { - context.select.find('option').remove(); + context.select.find('optgroup, option').remove(); for (var i = 0; i < context.data.collection.length; i++) { var obj = context.data.collection[i]; - if ($.inArray(obj[1], context.data.taken_ids) == -1) - context.select.append(new Option(obj[0], obj[1], true, true)); + + if (typeof(obj.name) != 'undefined') { // grouped items + var optgroup = $('').attr('label', obj.name); + var size = 0; + + for (var j = 0; j < obj.items.length; j++) { + var innerObj = obj.items[j]; + if ($.inArray(innerObj[1], context.data.taken_ids) == -1) { + optgroup.append(new Option(innerObj[0], innerObj[1], true, true)); + size++; + } + } + + if (size > 0) context.select.append(optgroup); + } else { + if ($.inArray(obj[1], context.data.taken_ids) == -1) + context.select.append(new Option(obj[0], obj[1], true, true)); + } } if (context.select.find('option').size() == 0) @@ -146,8 +162,22 @@ $(document).ready(function() { for (var j = 0; j < context.data.collection.length; j++) { var current = context.data.collection[j]; - if (data.id == current[1]) - data.label = current[0]; + + if (typeof(current.name) == 'undefined') { + if (data.id == current[1]) { + data.label = current[0]; + break; + } + } else { // loop thru the groups + for (var k = 0; k < current.items.length; k++) { + var localCurrent = current.items[k]; + + if (data.id == localCurrent[1]) { + data.label = localCurrent[0]; + break; + } + } + } } addElement(context, data); diff --git a/public/javascripts/admin/import.js b/public/javascripts/admin/import.js index 4b700eb0..53302b00 100644 --- a/public/javascripts/admin/import.js +++ b/public/javascripts/admin/import.js @@ -5,7 +5,7 @@ $(document).ready(function() { dataType: 'json', minTimeout: 100 }, function(data) { - var steps = ['site', 'content_types', 'assets', 'asset_collections', 'snippets', 'pages']; + var steps = ['site', 'content_types', 'assets', 'snippets', 'pages']; var currentIndex = data.step == 'done' ? steps.length - 1 : steps.indexOf(data.step); diff --git a/public/javascripts/admin/inline_editor_toolbar.js b/public/javascripts/admin/inline_editor_toolbar.js index 87b4c5b6..1ee356ab 100644 --- a/public/javascripts/admin/inline_editor_toolbar.js +++ b/public/javascripts/admin/inline_editor_toolbar.js @@ -84,6 +84,9 @@ var InlineEditorToolbar = { /* ___ internal methods ___ */ _buildHTML: function() { + var csrf_token = $('meta[name=csrf-token]').attr('content'), + csrf_param = $('meta[name=csrf-param]').attr('content'); + var labels = this._translations[this.locale]; var showPageUrl = $('meta[name=page-url]').attr('content'); @@ -94,6 +97,10 @@ var InlineEditorToolbar = { \ "; + if (csrf_param != null && csrf_token != null) { + formContentHTML += ''; + } + $('body').prepend("
\
    \
  •  
  • \ diff --git a/public/javascripts/admin/pages.js b/public/javascripts/admin/pages.js index a74fee85..3b9ccc85 100644 --- a/public/javascripts/admin/pages.js +++ b/public/javascripts/admin/pages.js @@ -24,14 +24,15 @@ $(document).ready(function() { 'update': function(event, ui) { var params = $(this).sortable('serialize', { 'key': 'children[]' }); params += '&_method=put'; + params += '&' + $('meta[name=csrf-param]').attr('content') + '=' + $('meta[name=csrf-token]').attr('content'); - $.post($(this).attr('data_url'), params, function(data) { + $.post($(this).attr('data-url'), params, function(data) { var error = typeof(data.error) != 'undefined'; $.growl((error ? 'error' : 'success'), (error ? data.error : data.notice)); }, 'json'); } }); - + // templatized feature $.subscribe('toggle.page_templatized.checked', function(event, data) { @@ -81,7 +82,7 @@ $(document).ready(function() { var lookForSlugAndUrl = function() { params = 'parent_id=' + $('#page_parent_id').val() + "&slug=" + $('#page_slug').val(); - $.get($('#page_slug').attr('data_url'), params, function(data) { + $.get($('#page_slug').attr('data-url'), params, function(data) { $('#page_slug_input .inline-hints').html(data.url).effect('highlight'); }, 'json'); }; @@ -99,7 +100,7 @@ $(document).ready(function() { if (typeof $.fn.imagepicker != 'undefined') $('a#image-picker-link').imagepicker({ insertFn: function(link) { - return "{{ '" + link.attr('data-local-path') + "' | theme_image_url }}"; + return "{{ '/" + link.attr('data-local-path') + "' | theme_image_url }}"; } }); diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/editor_plugin.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/editor_plugin.js deleted file mode 100644 index 8d6580f0..00000000 --- a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/editor_plugin.js +++ /dev/null @@ -1 +0,0 @@ -(function(){tinymce.create('tinymce.plugins.LocoImagePlugin',{init:function(ed,url){ed.addCommand('locoImage',function(){ed.windowManager.open({file:url+'/image.htm?6',width:645,height:650,inline:1},{plugin_url:url})});ed.addButton('image',{title:'locoimage.image_desc',cmd:'locoImage'})},getInfo:function(){return{longname:'Locomotive image',author:'Locomotive Engine',authorurl:'http://www.locomotiveapp.org',infourl:'http://www.locomotiveapp.org',version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add('locoimage',tinymce.plugins.LocoImagePlugin)})(); \ No newline at end of file diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/de_dlg.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/de_dlg.js deleted file mode 100644 index 0393d5c9..00000000 --- a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/de_dlg.js +++ /dev/null @@ -1,9 +0,0 @@ -tinyMCE.addI18n('de.locoimage_dlg',{ - dialog_title: 'Bild einfügen', - upload: 'Bild hochladen', - loading: 'Laden...', - uploading: 'Hochladen...', - destroying: 'Löschen...', - confirm: 'Bist du sicher ?', - no_items: 'Momentan gibt es hier keine Bilder.' -}); \ No newline at end of file diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/en_dlg.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/en_dlg.js deleted file mode 100644 index 0035721f..00000000 --- a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/en_dlg.js +++ /dev/null @@ -1,9 +0,0 @@ -tinyMCE.addI18n('en.locoimage_dlg',{ - dialog_title: 'Insert image', - upload: 'Upload image', - loading: 'Loading...', - uploading: 'Uploading...', - destroying: 'Destroying...', - confirm: 'Are you sure ?', - no_items: 'There are no images for now.' -}); \ No newline at end of file diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/fr_dlg.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/fr_dlg.js deleted file mode 100644 index e89fc8f9..00000000 --- a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/fr_dlg.js +++ /dev/null @@ -1,9 +0,0 @@ -tinyMCE.addI18n('fr.locoimage_dlg',{ - dialog_title: 'Insérer une image', - upload: 'Uploader image', - loading: 'Chargement...', - uploading: 'Uploading...', - destroying: 'Suppression...', - confirm: 'Êtes-vous sûr(e) ?', - no_items: 'Il n\'y a aucune image pour l\'instant' -}); \ No newline at end of file diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/css/locoimage.css b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/css/style.css similarity index 66% rename from public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/css/locoimage.css rename to public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/css/style.css index 71f3bf07..d6ab462a 100644 --- a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/css/locoimage.css +++ b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/css/style.css @@ -1,18 +1,3 @@ -/*#src_list, #over_list, #out_list {width:280px;} -.mceActionPanel {margin-top:7px;} -.alignPreview {border:1px solid #000; width:140px; height:140px; overflow:hidden; padding:5px;} -.checkbox {border:0;} -.panel_wrapper div.current {height:305px;} -#prev {margin:0; border:1px solid #000; width:428px; height:150px; overflow:auto;} -#align, #classlist {width:150px;} -#width, #height {vertical-align:middle; width:50px; text-align:center;} -#vspace, #hspace, #border {vertical-align:middle; width:30px; text-align:center;} -#class_list {width:180px;} -input {width: 280px;} -#constrain, #onmousemovecheck {width:auto;} -#id, #dir, #lang, #usemap, #longdesc {width:200px;} -*/ - body { font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif; font-size: 1em; @@ -43,7 +28,7 @@ ul.assets li.asset h4 a { font-size: 0.7em; } -ul.assets li.asset .image img { +ul.assets li.asset .inside { cursor: pointer; } diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/image.htm b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/dialog.htm similarity index 70% rename from public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/image.htm rename to public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/dialog.htm index 43baba14..8f42a34d 100644 --- a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/image.htm +++ b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/dialog.htm @@ -1,7 +1,7 @@ - {#locoimage_dlg.dialog_title} + {#locomedia_dlg.dialog_title} @@ -17,34 +17,33 @@ - + - + - +
    -
    {#locoimage_dlg.loading}
    - - +
    {#locomedia_dlg.loading}
    + +
    - +
    • NoName

      -
      +
      -
      @@ -54,7 +53,7 @@
    • diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/editor_plugin.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/editor_plugin.js new file mode 100644 index 00000000..3e0c0bb7 --- /dev/null +++ b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/editor_plugin.js @@ -0,0 +1 @@ +(function(){tinymce.create('tinymce.plugins.LocoMediaPlugin',{init:function(ed,url){ed.addCommand('locoMedia',function(){ed.windowManager.open({file:url+'/dialog.htm?7',width:645,height:650,inline:1},{plugin_url:url})});ed.addButton('locomedia',{title:'locomedia.image_desc',cmd:'locoMedia'})},getInfo:function(){return{longname:'Locomotive Media File',author:'Didier Lafforgue',authorurl:'http://www.locomotivecms.com',infourl:'http://www.locomotivecms.com',version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add('locomedia',tinymce.plugins.LocoMediaPlugin)})(); \ No newline at end of file diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/editor_plugin_src.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/editor_plugin_src.js similarity index 54% rename from public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/editor_plugin_src.js rename to public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/editor_plugin_src.js index d506cccd..93c6cdfc 100644 --- a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/editor_plugin_src.js +++ b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/editor_plugin_src.js @@ -9,12 +9,12 @@ */ (function() { - tinymce.create('tinymce.plugins.LocoImagePlugin', { + tinymce.create('tinymce.plugins.LocoMediaPlugin', { init : function(ed, url) { // Register commands - ed.addCommand('locoImage', function() { + ed.addCommand('locoMedia', function() { ed.windowManager.open({ - file : url + '/image.htm', + file : url + '/dialog.htm', width : 645, height : 650, inline : 1 @@ -24,23 +24,23 @@ }); // Register buttons - ed.addButton('image', { - title : 'locoimage.image_desc', - cmd : 'locoImage' + ed.addButton('locomedia', { + title : 'locomedia.image_desc', + cmd : 'locoMedia' }); }, getInfo : function() { return { - longname : 'Locomotive image', - author : 'Locomotive Engine', - authorurl : 'http://www.locomotiveapp.org', - infourl : 'http://www.locomotiveapp.org', + longname : 'Locomotive Media File', + author : 'Didier Lafforgue', + authorurl : 'http://www.locomotivecms.com', + infourl : 'http://www.locomotivecms.com', version : tinymce.majorVersion + "." + tinymce.minorVersion }; } }); // Register plugin - tinymce.PluginManager.add('locoimage', tinymce.plugins.LocoImagePlugin); + tinymce.PluginManager.add('locomedia', tinymce.plugins.LocoMediaPlugin); })(); \ No newline at end of file diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/img/sample.gif b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/img/sample.gif similarity index 100% rename from public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/img/sample.gif rename to public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/img/sample.gif diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/js/image.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/js/dialog.js similarity index 56% rename from public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/js/image.js rename to public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/js/dialog.js index 924a0698..0e7dd9c7 100644 --- a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/js/image.js +++ b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/js/dialog.js @@ -1,4 +1,4 @@ -var ImageDialog = { +var MediafileDialog = { formElement: null, listElement: null, @@ -15,13 +15,20 @@ var ImageDialog = { init : function(ed) { var self = this; + with(window.parent) { + var csrf_token = $('meta[name=csrf-token]').attr('content'), + csrf_param = $('meta[name=csrf-param]').attr('content'); + } + + $.fn.setCsrfSettings(csrf_token, csrf_param); + formElement = $(document.forms[0]); listElement = formElement.find('ul'); - $.getJSON('/admin/images.json', function(data) { - $(data.images).each(function() { - self._addImage(this); + $.getJSON('/admin/assets.json', function(data) { + $(data.assets).each(function() { + self._addAsset(this); }); self.setupUploader(); @@ -42,7 +49,7 @@ var ImageDialog = { $('#spinner').show(); }, - insert: function(url) { + insertFile: function(asset) { var ed = tinyMCEPopup.editor, f = document.forms[0], nl = f.elements, v, args = {}, el; tinyMCEPopup.restoreSelection(); @@ -50,17 +57,28 @@ var ImageDialog = { // Fixes crash in Safari if (tinymce.isWebKit) ed.getWin().focus(); - tinymce.extend(args, { src : url }); + if (asset.content_type == 'image') + tinymce.extend(args, { src : asset.url }); + else + tinymce.extend(args, { href : asset.url }); el = ed.selection.getNode(); - if (el && el.nodeName == 'IMG') { + if (el && (el.nodeName == 'IMG' || el.nodeName == 'A')) { ed.dom.setAttribs(el, args); } else { - ed.execCommand('mceInsertContent', false, '', {skip_undo : 1}); - ed.dom.setAttribs('__mce_tmp', args); - ed.dom.setAttrib('__mce_tmp', 'id', ''); - ed.undoManager.add(); + if (asset.content_type == 'image') { + ed.execCommand('mceInsertContent', false, '', { skip_undo: 1 }); + } else { + var html = ed.selection.getContent(); + if (html == '') html = asset.filename; + ed.execCommand('mceInsertContent', false, '' + html + '', { skip_undo: 1 }); + } + + + ed.dom.setAttribs('__mce_tmp', args); + ed.dom.setAttrib('__mce_tmp', 'id', ''); + ed.undoManager.add(); } tinyMCEPopup.close(); @@ -76,15 +94,14 @@ var ImageDialog = { var uploader = new plupload.Uploader({ runtimes : (jQuery.browser.webkit == true ? 'flash' : 'html5,flash'), - // container: 'theme-images', browse_button : 'upload-link', - max_file_size : '5mb', + max_file_size : '10mb', url : $('a#upload-link').attr('href'), flash_swf_url : '/javascripts/admin/plugins/plupload/plupload.flash.swf', multipart: true, multipart_params: multipartParams, filters : [ - { title : "Image files", extensions : "jpg,gif,png" }, + { title : 'Media files', extensions : 'png,gif,jpg,jpeg,pdf,doc,docx,xls,xlsx,txt' }, ] }); @@ -97,7 +114,7 @@ var ImageDialog = { var json = JSON.parse(response.response); if (json.status == 'success') - self._addImage(json); + self._addAsset(json); self.hideSpinner(); }); @@ -105,7 +122,7 @@ var ImageDialog = { uploader.init(); }, - _addImage: function(data) { + _addAsset: function(data) { var self = this; var asset = $('ul li.new-asset') @@ -114,29 +131,37 @@ var ImageDialog = { .addClass('asset'); asset.find('h4 a').attr('href', data.url) - .html(data.name) + .html(data.short_name) .bind('click', function(e) { - self.insert(data.url); + self.insertFile(data); e.stopPropagation(); e.preventDefault(); }); - asset.find('.image .inside img') - .attr('src', data.vignette_url) - .bind('click', function(e) { - self.insert(data.url); - }); + html = ''; + + if (data.content_type == 'image') { + asset.find('.icon').removeClass('icon').addClass('image'); + html = $('') + .attr('src', data.vignette_url) + .bind('click', function(e) { + self.insertFile(data); + }); + } else { + asset.find('.icon').addClass(data.content_type); + html = data.content_type == 'other' ? data.extname : data.content_type; + if (html == '') html = '?' + html = $('').html(html) + .bind('click', function(e) { + self.insertFile(data); + }); + } + + asset.find('.inside').append(html); asset.find('.actions a') .attr('href', data.destroy_url) - .bind('click', function(e) { - if (confirm($(this).attr('data-confirm'))) { - self.showSpinner('destroying'); - $(this).callRemote(); - } - e.preventDefault(); e.stopPropagation(); - }) .bind('ajax:success', function(event, data) { - self._destroyImage(asset); + self._destroyAsset(asset); }); if ($('ul li.asset').length % 4 == 0) @@ -149,7 +174,7 @@ var ImageDialog = { $('ul').scrollTo($('li.asset:last'), 400); }, - _destroyImage: function(asset) { + _destroyAsset: function(asset) { asset.remove(); if ($('ul li.asset').length == 0) { @@ -168,5 +193,5 @@ var ImageDialog = { }; -ImageDialog.preInit(); -tinyMCEPopup.onInit.add(ImageDialog.init, ImageDialog); \ No newline at end of file +MediafileDialog.preInit(); +tinyMCEPopup.onInit.add(MediafileDialog.init, MediafileDialog); \ No newline at end of file diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/de_dlg.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/de_dlg.js new file mode 100644 index 00000000..0c0fc6a1 --- /dev/null +++ b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/de_dlg.js @@ -0,0 +1,9 @@ +tinyMCE.addI18n('de.locomedia_dlg',{ + dialog_title: 'Mediendatei einfügen', + upload: 'Mediendatei hochladen', + loading: 'Laden...', + uploading: 'Hochladen...', + destroying: 'Löschen...', + confirm: 'Bist du sicher ?', + no_items: 'Momentan gibt es hier keine Mediendateien.' +}); \ No newline at end of file diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/en_dlg.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/en_dlg.js new file mode 100644 index 00000000..47b6b504 --- /dev/null +++ b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/en_dlg.js @@ -0,0 +1,9 @@ +tinyMCE.addI18n('en.locomedia_dlg',{ + dialog_title: 'Insert media', + upload: 'Upload media', + loading: 'Loading...', + uploading: 'Uploading...', + destroying: 'Destroying...', + confirm: 'Are you sure ?', + no_items: 'There are no media for now.' +}); \ No newline at end of file diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/fr_dlg.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/fr_dlg.js new file mode 100644 index 00000000..fd481ece --- /dev/null +++ b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/fr_dlg.js @@ -0,0 +1,9 @@ +tinyMCE.addI18n('fr.locomedia_dlg',{ + dialog_title: 'Insérer un média', + upload: 'Uploader média', + loading: 'Chargement...', + uploading: 'Uploading...', + destroying: 'Suppression...', + confirm: 'Êtes-vous sûr(e) ?', + no_items: 'Il n\'y a aucun média pour l\'instant' +}); \ No newline at end of file diff --git a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/it_dlg.js b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/it_dlg.js similarity index 83% rename from public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/it_dlg.js rename to public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/it_dlg.js index a90ef488..e1d651ef 100644 --- a/public/javascripts/admin/plugins/tiny_mce/plugins/locoimage/langs/it_dlg.js +++ b/public/javascripts/admin/plugins/tiny_mce/plugins/locomedia/langs/it_dlg.js @@ -1,4 +1,4 @@ -tinyMCE.addI18n('it.locoimage_dlg',{ +tinyMCE.addI18n('it.locomedia_dlg',{ dialog_title: 'Inserisci immagine', upload: 'Carica immagine', loading: 'Caricamento...', diff --git a/public/javascripts/admin/plugins/tiny_mce/themes/advanced/skins/locomotive/ui.css b/public/javascripts/admin/plugins/tiny_mce/themes/advanced/skins/locomotive/ui.css index 0a54f0a2..48190d08 100755 --- a/public/javascripts/admin/plugins/tiny_mce/themes/advanced/skins/locomotive/ui.css +++ b/public/javascripts/admin/plugins/tiny_mce/themes/advanced/skins/locomotive/ui.css @@ -728,6 +728,10 @@ background-position: -380px 0 } +.locomotiveSkin span.mce_locomedia { + background-position: -380px 0 +} + .locomotiveSkin span.mce_help { background-position: -340px 0 } diff --git a/public/javascripts/admin/rails.js b/public/javascripts/admin/rails.js index 103032a5..c13675b0 100644 --- a/public/javascripts/admin/rails.js +++ b/public/javascripts/admin/rails.js @@ -3,6 +3,12 @@ jQuery(function ($) { csrf_param = $('meta[name=csrf-param]').attr('content'); $.fn.extend({ + + setCsrfSettings: function(token, param) { + csrf_token = token; + csrf_param = param; + }, + /** * Triggers a custom event on an element and returns the event result * this is used to get around not being able to ensure callbacks are placed @@ -32,6 +38,10 @@ jQuery(function ($) { } else { if (el.triggerAndReturn('ajax:before')) { var data = el.is('form') ? el.serializeArray() : []; + + if (!el.is('form') && method != 'GET') + data.push({ 'name': csrf_param, 'value': csrf_token }); + $.ajax({ url: url, data: data, diff --git a/public/javascripts/admin/site.js b/public/javascripts/admin/site.js index 16f0e388..bca2dd00 100644 --- a/public/javascripts/admin/site.js +++ b/public/javascripts/admin/site.js @@ -46,4 +46,29 @@ $(document).ready(function() { $('#header h1 a span.ui-selectmenu-status').html(value); $('#site-selector-menu li.ui-selectmenu-item-selected a').html(value); }, []); + + // account roles + $('.membership .role em.editable').click(function() { + $(this).hide(); + $(this).next().show(); + }); + + $('.membership .role select').each(function() { + var select = $(this); + select.hover(function() { + clearTimeout($.data(select, 'timer')); + }, + function() { + $.data(select, 'timer', setTimeout(function() { + select.hide(); + select.prev().show(); + }, 1000)); + }).change(function() { + select.hide().prev() + .show() + .html(select[0].options[select[0].options.selectedIndex].text); + }); + }).hide(); + + }); diff --git a/public/javascripts/admin/snippets.js b/public/javascripts/admin/snippets.js index d77875a8..637c0fd1 100644 --- a/public/javascripts/admin/snippets.js +++ b/public/javascripts/admin/snippets.js @@ -16,7 +16,7 @@ $(document).ready(function() { $('a#image-picker-link').imagepicker({ insertFn: function(link) { - return "{{ '" + link.attr('data-local-path') + "' | theme_image_url }}"; + return "{{ '/" + link.attr('data-local-path') + "' | theme_image_url }}"; } }); }); diff --git a/public/stylesheets/admin/application.css b/public/stylesheets/admin/application.css index ad70916f..df890fce 100644 --- a/public/stylesheets/admin/application.css +++ b/public/stylesheets/admin/application.css @@ -244,5 +244,13 @@ ul.theme-assets li.hidden strong a { font-style: italic; color: #8B8D9A; font-we color: #1F82BC; } +/* paragraph (for help for example) */ + +p span.code { + background: #EBEDF4; + color: #8B8D9A; + text-shadow: #fff 0px 1px; + padding: 2px 3px; +} diff --git a/public/stylesheets/admin/formtastic_changes.css b/public/stylesheets/admin/formtastic_changes.css index e1b1c65e..b6b6d507 100644 --- a/public/stylesheets/admin/formtastic_changes.css +++ b/public/stylesheets/admin/formtastic_changes.css @@ -24,6 +24,7 @@ form.formtastic legend span { color: #1e1f26; font-size: 0.7em; padding: 4px 0 0 20px; + text-shadow: #fff 0px 1px; } form.formtastic legend span small { @@ -80,7 +81,7 @@ form.formtastic fieldset.inputs ol { form.formtastic fieldset ol li { width: 100%; position: relative; margin-bottom: 1.3em; } -form.formtastic fieldset ol li label { text-align: left; padding: 0.3em 2em 0 20px; font-size: 0.8em; color: #17171b; width: 15%; } +form.formtastic fieldset ol li label { text-align: left; padding: 0.4em 2em 0 20px; font-size: 0.8em; width: 15%; color: #585a69; text-shadow: #fff 0px 1px; } form.formtastic fieldset ol li.string input, form.formtastic fieldset ol li.password input, @@ -88,12 +89,16 @@ form.formtastic fieldset ol li.numeric input, form.formtastic fieldset ol li.text textarea, form.formtastic fieldset ol li code textarea, form.formtastic fieldset ol li input[type=password] { - padding: 4px; + padding: 4px 5px; font-size: 0.9em; + font-weight: bold; width: 45%; - color: #787a89; + color: #17171b; background: white url(/images/admin/form/field.png) repeat-x 0 0; border: 1px solid #a6a8b8; + -webkit-box-shadow: #f1f1f1 0px 1px; + -moz-box-shadow: #f1f1f1 0px 1px; + box-shadow: #f1f1f1 0px 1px; } form.formtastic fieldset ol li.text textarea.html { @@ -160,38 +165,11 @@ form.formtastic fieldset ol .more { text-align: right; width: auto; margin: 10px form.formtastic fieldset ol .more a { text-decoration: none; color: #787A89; font-size: 0.7em; } form.formtastic fieldset ol .more a:hover { text-decoration: underline; } -/*form.formtastic hr { border-top: 2px solid #ccc; }*/ - -/*form.formtastic fieldset.buttons { padding-left: 28%; padding-bottom: 20px; }*/ - -form.formtastic div.actions { - position: relative; - top: 27px; - left: -15px; - width: 950px; - background: #8b8d9a; +form.formtastic fieldset ol .more a.picture { + padding-left: 23px; + background: transparent url(/images/admin/icons/asset_add.png) no-repeat left 1px; } -form.formtastic div.actions p { - padding: 15px; - margin: 0px; -} - -form.formtastic div.actions a { - color: #fff !important; - text-decoration: none; - font-size: 0.8em; - position: relative; - top: 4px; -} - -form.formtastic div.actions a.remove { color: #ff092c !important; } - -form.formtastic div.actions p a:hover { text-decoration: underline; } - -form.formtastic div.actions .last p { text-align: right; } - - /* ___ pages ___ */ form.formtastic fieldset ol li.path em { @@ -242,6 +220,28 @@ form.formtastic fieldset ol li.item em { color: #757575; } +form.formtastic fieldset ol li em.editable { + display: inline-block; + position: relative; + top: -1px; + color: #8b8d9a; + font-size: 0.9em; + font-style: italic; + margin-left: 3px; + border: 1px solid transparent; + padding: 2px 5px; + height: 18px; + line-height: 16px; +} + +form.formtastic fieldset ol li em.editable:hover { + background: #fffbe5; + border: 1px dotted #efe4a5; + cursor: pointer; + color: #17171D; + font-weight: bold; +} + form.formtastic fieldset ol li.item span.actions { position: absolute; top: 5px; @@ -277,27 +277,6 @@ form.formtastic fieldset.editable-list ol li.added select { top: -1px; } -form.formtastic fieldset.editable-list ol li.added em { - display: inline-block; - position: relative; - top: -1px; - color: #8b8d9a; - font-size: 0.9em; - font-style: italic; - margin-left: 3px; - border: 1px solid transparent; - padding: 2px 5px; - height: 18px; - line-height: 16px; -} -form.formtastic fieldset.editable-list ol li.added em:hover { - background: #fffbe5; - border: 1px dotted #efe4a5; - cursor: pointer; - color: #17171D; - font-weight: bold; -} - form.formtastic fieldset.editable-list ol li.added select, form.formtastic fieldset.editable-list ol li.added em { width: 150px; @@ -548,7 +527,43 @@ form.formtastic fieldset.email li.full input { margin-left: 20px; } -/* ___ assets ___ */ +form.formtastic fieldset.memberships ol li .role { + position: absolute; + top: 2px; + right: 30px; + width: 170px; + text-align: left; +} + +form.formtastic fieldset.memberships ol li .role em { + display: inline-block; + position: relative; + top: -1px; + color: #757575; + font-size: 0.8em; + padding: 2px 5px 2px 17px; + height: 18px; + line-height: 16px; + margin-left: 0px; +} + +form.formtastic fieldset.memberships ol li .role em.locked { + background: transparent url(/images/admin/icons/membership_lock.png) no-repeat 1px 3px; +} + +form.formtastic fieldset.memberships ol li .role em.editable { + background: transparent url(/images/admin/icons/membership_edit.png) no-repeat left 3px; +} + +form.formtastic fieldset.memberships ol li .role em.editable { font-style: normal; font-size: 0.8em; } +form.formtastic fieldset.memberships ol li .role em.editable:hover { color: #000; font-style: normal; background: #fffbe5; padding-left: 5px; } + +form.formtastic fieldset.memberships ol li select { + position: relative; + top: -1px; +} + +/* ___ theme assets ___ */ .selector { position: relative; @@ -562,8 +577,12 @@ form.formtastic fieldset.email li.full input { font-size: 0.7em; text-decoration: none; cursor: pointer; + padding-left: 20px; + background: transparent url(/images/admin/icons/asset_switch.png) no-repeat left 2px; } +.selector span.alt:hover { text-decoration: underline; } + form.formtastic fieldset.file li.full input { margin-left: 20px; diff --git a/public/stylesheets/admin/inline_editor.css b/public/stylesheets/admin/inline_editor.css index d11401f5..f3aef040 100644 --- a/public/stylesheets/admin/inline_editor.css +++ b/public/stylesheets/admin/inline_editor.css @@ -14,6 +14,8 @@ #page-toolbar ul { margin-top: 3px; list-style: none; + padding: 0px; + -moz-padding-start: 0px; } #page-toolbar ul li.link { diff --git a/public/stylesheets/admin/layout.css b/public/stylesheets/admin/layout.css index 93eb469a..e15bec4d 100644 --- a/public/stylesheets/admin/layout.css +++ b/public/stylesheets/admin/layout.css @@ -151,6 +151,33 @@ body { #content #local-actions-bar a:hover { text-decoration: none; color: #333; } +#content #local-actions-bottom-bar { + position: relative; + top: 27px; + left: -15px; + width: 950px; + background: #8b8d9a; +} + +#content #local-actions-bottom-bar p { + padding: 15px; + margin: 0px; +} + +#content #local-actions-bottom-bar a { + color: #fff !important; + text-decoration: none; + font-size: 0.8em; + position: relative; + top: 4px; +} + +#content #local-actions-bottom-bar a.remove { color: #ff092c !important; } + +#content #local-actions-bottom-bar p a:hover { text-decoration: underline; } + +#content #local-actions-bottom-bar .last p { text-align: right; } + /* ___ footer ___ */ #footer { diff --git a/public/stylesheets/admin/menu.css b/public/stylesheets/admin/menu.css index c67876c0..21665c72 100644 --- a/public/stylesheets/admin/menu.css +++ b/public/stylesheets/admin/menu.css @@ -154,22 +154,28 @@ text-decoration: none; } #submenu .popup a:hover { text-decoration: underline; } + #submenu .popup .header { + border-bottom: 1px dotted #bbbbbd; + padding-bottom: 6px; + margin: 0px 16px; } #submenu .popup .inner { padding: 8px 16px; } #submenu .popup h2 { font-size: 0.7em; font-weight: bold; color: #1e1f26; - border-top: 1px dotted #bbbbbd; - padding-top: 6px; margin-bottom: 0px; } #submenu .popup p { - margin: 0px 15px; + margin: 0px; padding: 10px 0 0 0px; } #submenu .popup p a { font-size: 0.8em; background: transparent url(/images/admin/menu/popup/add.png) no-repeat left 4px; padding-left: 12px; } + #submenu .popup p.edit { + padding-top: 0px; } + #submenu .popup p.edit a { + background: transparent url(/images/admin/menu/popup/bullet.png) no-repeat left 5px; } #submenu .popup ul { list-style-image: url(/images/admin/menu/popup/bullet.png); margin: 0px 0px 0 15px; } diff --git a/public/stylesheets/sass/admin/menu.scss b/public/stylesheets/sass/admin/menu.scss index aedbb53e..12d90d9a 100644 --- a/public/stylesheets/sass/admin/menu.scss +++ b/public/stylesheets/sass/admin/menu.scss @@ -164,19 +164,23 @@ &:hover { text-decoration: underline; } } + .header { + border-bottom: 1px dotted #bbbbbd; + padding-bottom: 6px; + margin: 0px 16px; + } + .inner { padding: 8px 16px; } h2 { font-size: 0.7em; font-weight: bold; color: #1e1f26; - border-top: 1px dotted #bbbbbd; - padding-top: 6px; margin-bottom: 0px; } p { - margin: 0px 15px; + margin: 0px; padding: 10px 0 0 0px; a { @@ -184,6 +188,14 @@ background: transparent url(/images/admin/menu/popup/add.png) no-repeat left 4px; padding-left: 12px; } + + &.edit { + padding-top: 0px; + + a { + background: transparent url(/images/admin/menu/popup/bullet.png) no-repeat left 5px; + } + } } ul { diff --git a/spec/factories.rb b/spec/factories.rb index d6d50c2b..e2591fb7 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -9,7 +9,7 @@ Factory.define "test site", :parent => :site do |s| s.name 'Locomotive test website' s.subdomain 'test' s.after_build do |site_test| - site_test.memberships.build :account => Account.where(:name => "Admin").first || Factory("admin user"), :admin => true + site_test.memberships.build :account => Account.where(:name => "Admin").first || Factory("admin user"), :role => 'admin' end end @@ -54,8 +54,23 @@ end ## Memberships ## Factory.define :membership do |m| - m.admin true - m.account{ Account.where(:name => "Bart Simpson").first || Factory(:account) } + m.role 'admin' + m.account { Account.where(:name => "Bart Simpson").first || Factory('admin user') } +end + +Factory.define :admin, :parent => :membership do |m| + m.role 'admin' + m.account { Factory('admin user', :locale => 'en') } +end + +Factory.define :designer, :parent => :membership do |m| + m.role 'designer' + m.account { Factory('frenchy user', :locale => 'en') } +end + +Factory.define :author, :parent => :membership do |m| + m.role 'author' + m.account { Factory('brazillian user', :locale => 'en') } end @@ -77,16 +92,15 @@ Factory.define :snippet do |s| end -## Theme assets ## -Factory.define :theme_asset do |a| +## Assets ## +Factory.define :asset do |a| a.site { Site.where(:subdomain => "acme").first || Factory(:site) } end -## Asset collections ## -Factory.define :asset_collection do |s| - s.name 'Trip to Chicago' - s.site { Site.where(:subdomain => "acme").first || Factory(:site) } +## Theme assets ## +Factory.define :theme_asset do |a| + a.site { Site.where(:subdomain => "acme").first || Factory(:site) } end diff --git a/spec/fixtures/themes/default.zip b/spec/fixtures/themes/default.zip index 404afe89..9d3cc5c9 100644 Binary files a/spec/fixtures/themes/default.zip and b/spec/fixtures/themes/default.zip differ diff --git a/spec/lib/locomotive/heroku_spec.rb b/spec/lib/locomotive/heroku_spec.rb index 899bd196..17d72460 100644 --- a/spec/lib/locomotive/heroku_spec.rb +++ b/spec/lib/locomotive/heroku_spec.rb @@ -35,7 +35,7 @@ describe 'Heroku support' do end it 'has a nil connection' do - Locomotive.heroku_connection.should be_nil + Locomotive.respond_to?(:heroku_connection).should be_false end it 'tells heroku is disabled' do diff --git a/spec/lib/locomotive/httparty/webservice_spec.rb b/spec/lib/locomotive/httparty/webservice_spec.rb index 3f5f23ea..ca59e585 100644 --- a/spec/lib/locomotive/httparty/webservice_spec.rb +++ b/spec/lib/locomotive/httparty/webservice_spec.rb @@ -13,6 +13,11 @@ describe Locomotive::Httparty::Webservice do Locomotive::Httparty::Webservice.consume('http://blog.locomotiveapp.org') end + it 'sets the base uri from a much more complex url' do + Locomotive::Httparty::Webservice.expects(:get).with('/feed/weather.ashx?key=secretapikey&format=json', { :base_uri => 'http://free.worldweatheronline.com' }).returns(@response) + Locomotive::Httparty::Webservice.consume('http://free.worldweatheronline.com/feed/weather.ashx?key=secretapikey&format=json') + end + it 'sets both the base uri and the path from an url with parameters' do Locomotive::Httparty::Webservice.expects(:get).with('/api/read/json?num=3', { :base_uri => 'http://blog.locomotiveapp.org' }).returns(@response) Locomotive::Httparty::Webservice.consume('http://blog.locomotiveapp.org/api/read/json?num=3') diff --git a/spec/lib/locomotive/import_spec.rb b/spec/lib/locomotive/import_spec.rb index 5935e0dd..516d522b 100644 --- a/spec/lib/locomotive/import_spec.rb +++ b/spec/lib/locomotive/import_spec.rb @@ -20,9 +20,9 @@ describe Locomotive::Import::Job do end it 'adds content types' do - @site.content_types.count.should == 2 + @site.content_types.count.should == 4 content_type = @site.content_types.where(:slug => 'projects').first - content_type.content_custom_fields.size.should == 7 + content_type.content_custom_fields.size.should == 9 end it 'converts correctly the order_by option for content types' do @@ -38,6 +38,8 @@ describe Locomotive::Import::Job do content.name.should == 'Locomotive App' content.thumbnail.url.should_not be_nil content.featured.should == true + content.client.name.should == 'My client #1' + content.team.first.name.should == 'Michael Scott' end it 'inserts theme assets' do diff --git a/spec/lib/locomotive/liquid/filters/resize_spec.rb b/spec/lib/locomotive/liquid/filters/resize_spec.rb new file mode 100644 index 00000000..c20b7774 --- /dev/null +++ b/spec/lib/locomotive/liquid/filters/resize_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Locomotive::Liquid::Filters::Resize do + + before :each do + @site = Factory.create(:site) + @theme_asset = Factory.create(:theme_asset, :source => FixturedAsset.open('5k.png'), :site => @site) + @theme_asset_path = "/sites/#{@theme_asset.site_id}/theme/images/5k.png" + @asset = Factory.create(:asset, :source => FixturedAsset.open('5k.png'), :site => @site) + @asset_url = @asset.source.url + @asset_path = "/sites/#{@asset.site_id}/assets/#{@asset.id}/5k.png" + @context = Liquid::Context.new( { }, { 'asset_url' => @asset_url, 'theme_asset' => @theme_asset.to_liquid }, { :site => @site }) + @app = Locomotive::Dragonfly.app + end + + describe '#resize' do + + context 'when an asset url string is given' do + + before :each do + @template = Liquid::Template.parse('{{ asset_url | resize: "40x30" }}') + end + + it 'should return the location of the resized image' do + @template.render(@context).should =~ /images\/dynamic\/.*\/5k.png/ + end + + it 'should use the path in the public folder to generate a location' do + @template.render(@context).should == @app.fetch_file("public#{@asset_path}").thumb('40x30').url + end + + end + + context 'when a theme asset is given' do + + before :each do + @template = Liquid::Template.parse("{{ theme_asset | resize: '300x400' }}") + end + + it 'should return the location of the resized image' do + @template.render(@context).should =~ /images\/dynamic\/.*\/5k.png/ + end + + it 'should use the path of the theme asset to generate a location' do + @template.render(@context).should == @app.fetch_file("public#{@theme_asset_path}").thumb('300x400').url + end + + end + + context 'when no resize string is given' do + + before :each do + @template = Liquid::Template.parse('{{ asset | resize: }}') + end + + it 'should return a liquid error' do + @template.render(@context).should include 'Liquid error: wrong number of arguments' + end + + end + + end + +end \ No newline at end of file diff --git a/spec/lib/locomotive/liquid/tags/seo_metadata_spec.rb b/spec/lib/locomotive/liquid/tags/seo_spec.rb similarity index 51% rename from spec/lib/locomotive/liquid/tags/seo_metadata_spec.rb rename to spec/lib/locomotive/liquid/tags/seo_spec.rb index 5a5eeea3..7f5399a0 100644 --- a/spec/lib/locomotive/liquid/tags/seo_metadata_spec.rb +++ b/spec/lib/locomotive/liquid/tags/seo_spec.rb @@ -1,12 +1,30 @@ require 'spec_helper' -describe Locomotive::Liquid::Tags::SEOMetadata do +describe Locomotive::Liquid::Tags::SEO do + let(:site) do - Factory.build(:site, :meta_description => 'A short site description', :meta_keywords => 'test only cat dog') + Factory.build(:site, :seo_title => 'Site title (SEO)', :meta_description => 'A short site description', :meta_keywords => 'test only cat dog') end describe 'rendering' do - it 'renders a a meta description tag' do + + it 'renders everything' do + html = render_seo + html.should include 'Site title (SEO)' + html.should include '' + html.should include '' + end + + it 'renders a seo title' do + render_seo_title.should include 'Site title (SEO)' + end + + it 'renders the site title if no seo title is provided' do + site.seo_title = nil + render_seo_title.should include 'Acme Website' + end + + it 'renders a meta description tag' do render_seo_metadata.should include '' end @@ -23,8 +41,26 @@ describe Locomotive::Liquid::Tags::SEOMetadata do site.meta_keywords = ' one " two " three ' render_seo_metadata.should include '' end - + + it 'renders an empty string if no meta' do + site.meta_keywords = nil + render_seo_metadata.should include '' + end + context "when page" do + + context "has seo title" do + let(:page) { site.pages.build(:seo_title => 'Page title (SEO)', :meta_keywords => 'hulk,gamma', :meta_description => "Bruce Banner") } + subject { render_seo_title('page' => page) } + it { should include(%Q[Page title (SEO)]) } + end + + context "does not have seo title" do + let(:page) { site.pages.build } + subject { render_seo_title('page' => page) } + it { should include(%Q[Site title (SEO)]) } + end + context "has metadata" do let(:page) { site.pages.build(:meta_keywords => 'hulk,gamma', :meta_description => "Bruce Banner") } subject { render_seo_metadata('page' => page) } @@ -33,20 +69,34 @@ describe Locomotive::Liquid::Tags::SEOMetadata do end context "does not have metadata" do - let(:page) { site.pages.build } + let(:page) { site.pages.build } subject { render_seo_metadata('page' => page) } it { should include(%Q[]) } it { should include(%Q[]) } end + end - + context "when content instance" do + let(:content_type) do Factory.build(:content_type, :site => site).tap do |ct| ct.content_custom_fields.build :label => 'anything', :kind => 'String' end end + context "has seo title" do + let(:content) { content_type.contents.build(:seo_title => 'Content title (SEO)', :meta_keywords => 'Libidinous, Angsty', :meta_description => "Quite the combination.") } + subject { render_seo_title('content_instance' => content) } + it { should include(%Q[Content title (SEO)]) } + end + + context "does not have seo title" do + let(:content) { content_type.contents.build } + subject { render_seo_title('content_instance' => content) } + it { should include(%Q[Site title (SEO)]) } + end + context "has metadata" do let(:content) { content_type.contents.build(:meta_keywords => 'Libidinous, Angsty', :meta_description => "Quite the combination.") } subject { render_seo_metadata('content_instance' => content) } @@ -60,13 +110,26 @@ describe Locomotive::Liquid::Tags::SEOMetadata do it { should include(%Q[]) } it { should include(%Q[]) } end - end - end - - def render_seo_metadata(assigns={}) + end + + end + + def render_seo(assigns = {}) + render_seo_tag('seo', assigns) + end + + def render_seo_title(assigns = {}) + render_seo_tag('seo_title', assigns) + end + + def render_seo_metadata(assigns = {}) + render_seo_tag('seo_metadata', assigns) + end + + def render_seo_tag(tag_name, assigns = {}) registers = { :site => site } liquid_context = ::Liquid::Context.new({}, assigns, registers) - output = Liquid::Template.parse("{% seo_metadata %}").render(liquid_context) + output = Liquid::Template.parse("{% #{tag_name} %}").render(liquid_context) end end \ No newline at end of file diff --git a/spec/lib/locomotive/render_spec.rb b/spec/lib/locomotive/render_spec.rb index 63194652..4cbee81e 100644 --- a/spec/lib/locomotive/render_spec.rb +++ b/spec/lib/locomotive/render_spec.rb @@ -51,7 +51,7 @@ describe 'Locomotive rendering system' do end it 'sets the status to 404 not found when no page is found' do - @controller.expects(:not_found_page).returns(@page) + @page.stubs(:not_found?).returns(true) @controller.send(:prepare_and_set_response, 'Hello world !') @controller.status.should == :not_found end @@ -85,7 +85,7 @@ describe 'Locomotive rendering system' do @controller.send(:locomotive_page).should be_true end - context 'redirect page' do + context 'redirect' do before(:each) do @page.redirect = true diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb new file mode 100644 index 00000000..3339764f --- /dev/null +++ b/spec/models/ability_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper' + +describe Ability do + + before :each do + @site = Factory(:site) + @account = Factory(:account) + + @admin = Factory(:membership, :account => Factory.stub(:account), :site => Factory.stub(:site)) + @designer = Factory(:membership, :account => Factory.stub(:account), :site => @site, :role => %(designer)) + @author = Factory(:membership, :account => Factory.stub(:account), :site => @site, :role => %(author)) + end + + context 'pages' do + + subject { Page.new } + + context 'management' do + it 'should allow management of pages from (admin, designer, author)' do + should allow_permission_from :manage, @admin + should allow_permission_from :manage, @designer + should_not allow_permission_from :manage, @author + end + end + + context 'touching' do + it 'should allow touching of pages from (author)' do + should allow_permission_from :touch, @author + end + end + + end + + context 'content instance' do + + subject { ContentInstance.new } + + context 'management' do + it 'should allow management of pages from (admin, designer, author)' do + should allow_permission_from :manage, @admin + should allow_permission_from :manage, @designer + should allow_permission_from :manage, @author + end + end + + end + + context 'content type' do + + subject { ContentType.new } + + context 'management' do + it 'should allow management of pages from (admin, designer)' do + should allow_permission_from :manage, @admin + should allow_permission_from :manage, @designer + should_not allow_permission_from :manage, @author + end + end + + # context 'touching' do + # it 'should allow touching of pages from (author)' do + # should_not allow_permission_from :touch, @author + # end + # end + + end + + context 'theme assets' do + + subject { ThemeAsset.new } + + context 'management' do + it 'should allow management of pages from (admin, designer)' do + should allow_permission_from :manage, @admin + should allow_permission_from :manage, @designer + should_not allow_permission_from :manage, @author + end + end + + context 'touching' do + it 'should allow touching of pages from (author)' do + should allow_permission_from :touch, @author + end + end + + end + + context 'site' do + + subject { Site.new } + + context 'management' do + it 'should allow management of pages from (admin)' do + should allow_permission_from :manage, @admin + should_not allow_permission_from :manage, @designer + should_not allow_permission_from :manage, @author + end + end + + context 'importing' do + it 'should allow importing of sites from (designer)' do + should allow_permission_from :import, @designer + should_not allow_permission_from :import, @author + end + end + + context 'pointing' do + it 'should allow importing of sites from (designer)' do + should allow_permission_from :point, @designer + should_not allow_permission_from :point, @author + end + end + + end + + context 'membership' do + + subject { Membership.new } + + context 'management' do + it 'should allow management of memberships from (admin, designer)' do + should allow_permission_from :manage, @admin + should allow_permission_from :manage, @designer + should_not allow_permission_from :manage, @author + end + end + + end + +end diff --git a/spec/models/asset_collection_spec.rb b/spec/models/asset_collection_spec.rb deleted file mode 100644 index 889b4c96..00000000 --- a/spec/models/asset_collection_spec.rb +++ /dev/null @@ -1,186 +0,0 @@ -require 'spec_helper' - -describe AssetCollection do - - it 'should have a valid factory' do - Factory.build(:asset_collection).should be_valid - end - - describe 'custom fields (beta)' do - - before(:each) do - Site.any_instance.stubs(:create_default_pages!).returns(true) - site = Factory.build(:site) - Site.stubs(:find).returns(site) - @collection = Factory.build(:asset_collection, :site => site) - @collection.asset_custom_fields.build :label => 'My Description', :_alias => 'description', :kind => 'Text' - @collection.asset_custom_fields.build :label => 'Active', :kind => 'Boolean' - # AssetCollection.logger = Logger.new($stdout) - # AssetCollection.db.connection.instance_variable_set(:@logger, Logger.new($stdout)) - end - - context 'unit' do - - before(:each) do - @field = CustomFields::Field.new(:kind => 'String') - end - - it 'should tell if it is a String' do - @field.string?.should be_true - end - - it 'should tell if it is a Text' do - @field.kind = 'Text' - @field.text?.should be_true - end - - end - - context 'validation' do - - %w{label kind}.each do |key| - it "should validate presence of #{key}" do - field = @collection.asset_custom_fields.build({ :label => 'Shortcut', :kind => 'String' }.merge(key.to_sym => nil)) - field.should_not be_valid - field.errors[key.to_sym].should == ["can't be blank"] - end - end - - it 'should not have unique label' do - field = @collection.asset_custom_fields.build :label => 'Active', :kind => 'Boolean' - field.should_not be_valid - field.errors[:label].should == ["is already taken"] - end - - it 'should invalidate parent if custom field is not valid' do - field = @collection.asset_custom_fields.build - @collection.should_not be_valid - @collection.asset_custom_fields.last.errors[:label].should == ["can't be blank"] - end - - end - - context 'define core attributes' do - - it 'should have an unique name' do - @collection.asset_custom_fields.first._name.should == "custom_field_1" - @collection.asset_custom_fields.last._name.should == "custom_field_2" - end - - it 'should have an unique alias' do - @collection.asset_custom_fields.first.safe_alias.should == "description" - @collection.asset_custom_fields.last.safe_alias.should == "active" - end - - end - - context 'build and save' do - - it 'should build asset' do - asset = @collection.assets.build - lambda { - asset.description - asset.active - asset.custom_fields.size.should == 2 - }.should_not raise_error - end - - it 'should assign values to newly built asset' do - asset = build_asset(@collection) - asset.description.should == 'Lorem ipsum' - asset.active.should == true - end - - it 'should save asset' do - asset = build_asset(@collection) - asset.save and @collection.reload - asset = @collection.assets.first - asset.description.should == 'Lorem ipsum' - asset.active.should == true - end - - it 'should not modify assets from another collection' do - asset = build_asset(@collection) - asset.save and @collection.reload - new_collection = AssetCollection.new - lambda { new_collection.assets.build.description }.should raise_error - end - - end - - context 'modifying fields' do - - before(:each) do - @asset = build_asset(@collection).save - end - - it 'should add new field' do - @collection.asset_custom_fields.build :label => 'Active at', :name => 'active_at', :kind => 'Date' - @collection.upsert(:validate => false) - @collection.invalidate_asset_klass - @collection.reload - asset = @collection.assets.first - lambda { asset.active_at }.should_not raise_error - end - - it 'should remove field' do - @collection.asset_custom_fields.clear - @collection.upsert(:validate => false) - @collection.invalidate_asset_klass - @collection.reload - asset = @collection.assets.first - lambda { asset.active_at }.should raise_error - end - - it 'should rename field label' do - @collection.asset_custom_fields.first.label = 'Simple description' - @collection.asset_custom_fields.first._alias = nil - @collection.upsert(:validate => false) - - @collection.invalidate_asset_klass - @collection.reload - - asset = @collection.assets.first - asset.simple_description.should == 'Lorem ipsum' - end - - end - - context 'managing from hash' do - - it 'adds new field' do - @collection.asset_custom_fields.clear - field = @collection.asset_custom_fields.build :label => 'Title' - @collection.asset_custom_fields_attributes = { 0 => { :id => field.id.to_s, 'label' => 'A title', 'kind' => 'String' }, 1 => { 'label' => 'Tagline', 'kind' => 'String' } } - @collection.asset_custom_fields.size.should == 2 - @collection.asset_custom_fields.first.label.should == 'A title' - @collection.asset_custom_fields.last.label.should == 'Tagline' - end - - it 'updates/removes fields' do - field = @collection.asset_custom_fields.build :label => 'Title', :kind => 'String' - @collection.save; @collection = AssetCollection.find(@collection.id) - @collection.update_attributes(:asset_custom_fields_attributes => { - '0' => { 'id' => lookup_field_id(0), 'label' => 'My Description', 'kind' => 'Text', '_destroy' => '1' }, - '1' => { 'id' => lookup_field_id(1), 'label' => 'Active', 'kind' => 'Boolean', '_destroy' => '1' }, - '2' => { 'id' => lookup_field_id(2), 'label' => 'My Title !', 'kind' => 'String' }, - 'new_record' => { 'label' => 'Published at', 'kind' => 'String' } - }) - @collection = AssetCollection.find(@collection.id) - @collection.asset_custom_fields.size.should == 2 - @collection.asset_custom_fields.first.label.should == 'My Title !' - end - - end - - end - - def build_asset(collection) - collection.assets.build(:name => 'Asset on steroids', :description => 'Lorem ipsum', :active => true) - end - - def lookup_field_id(index) - @collection.asset_custom_fields.all[index].id.to_s - end - -end diff --git a/spec/models/asset_spec.rb b/spec/models/asset_spec.rb new file mode 100644 index 00000000..3f883ada --- /dev/null +++ b/spec/models/asset_spec.rb @@ -0,0 +1,46 @@ +# coding: utf-8 + +require 'spec_helper' + +describe Asset do + + describe 'attaching a file' do + + before(:each) do + Asset.any_instance.stubs(:site_id).returns('test') + @asset = Factory.build(:asset) + end + + it 'should process picture' do + @asset.source = FixturedAsset.open('5k.png') + @asset.source.file.content_type.should_not be_nil + @asset.image?.should be_true + end + + it 'should get width and height from the image' do + @asset.source = FixturedAsset.open('5k.png') + @asset.width.should == 32 + @asset.height.should == 32 + end + + end + + describe 'vignette' do + + before(:each) do + @asset = Factory.build(:asset, :source => FixturedAsset.open('5k.png')) + end + + it 'does not resize image smaller than 50x50' do + @asset.vignette_url.should =~ /^\/spec\/.*\/5k.png/ + end + + it 'has any possible resized versions' do + @asset.stubs(:with).returns(81) + @asset.stubs(:height).returns(81) + @asset.vignette_url.should =~ /^\/images\/dynamic\/.*\/5k.png/ + end + + end + +end \ No newline at end of file diff --git a/spec/models/content_instance_spec.rb b/spec/models/content_instance_spec.rb index 987281f4..d390772c 100644 --- a/spec/models/content_instance_spec.rb +++ b/spec/models/content_instance_spec.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + require 'spec_helper' describe ContentInstance do @@ -17,14 +19,60 @@ describe ContentInstance do build_content.should be_valid end - # Validations ## + ## Validations ## - it 'requires presence of title' do + it 'requires the presence of title' do content = build_content :title => nil content.should_not be_valid content.errors[:title].should == ["can't be blank"] end + it 'requires the presence of the permalink (_slug)' do + content = build_content :title => nil + content.should_not be_valid + content.errors[:_slug].should == ["can't be blank"] + end + + it 'has an unique permalink' do + build_content.save; @content_type = ContentType.find(@content_type._id) + content = build_content + content.should_not be_valid + content.errors[:_slug].should == ["is already taken"] + end + + end + + describe '#permalink' do + + before(:each) do + @content = build_content + end + + it 'has a default value based on the highlighted field' do + @content.send(:set_slug) + @content._permalink.should == 'locomotive' + end + + it 'is empty if no value for the highlighted field is provided' do + @content.title = nil; @content.send(:set_slug) + @content._permalink.should be_nil + end + + it 'includes dashes instead of white spaces' do + @content.title = 'my content instance'; @content.send(:set_slug) + @content._permalink.should == 'my-content-instance' + end + + it 'removes accentued characters' do + @content.title = "une chèvre dans le pré"; @content.send(:set_slug) + @content._permalink.should == 'une-chevre-dans-le-pre' + end + + it 'removes dots' do + @content.title = "my.test"; @content.send(:set_slug) + @content._permalink.should == 'my-test' + end + end describe '#visibility' do @@ -93,7 +141,7 @@ describe ContentInstance do end end - + describe '#site' do it 'delegates to the content type' do @content_type.expects(:site) diff --git a/spec/models/content_type_spec.rb b/spec/models/content_type_spec.rb index 59b362b3..9152d124 100644 --- a/spec/models/content_type_spec.rb +++ b/spec/models/content_type_spec.rb @@ -43,16 +43,16 @@ describe ContentType do content_type.should_not be_valid content_type.errors[:content_custom_fields].should == ["is too small (minimum element number is 1)"] end - + %w(created_at updated_at).each do |_alias| - it "does not allow #{_alias} as alias" do + it "does not allow #{_alias} as alias" do content_type = Factory.build(:content_type) field = content_type.content_custom_fields.build :label => 'anything', :kind => 'String', :_alias => _alias field.valid?.should be_false - field.errors[:_alias].should == ['is reserved'] + field.errors[:_alias].should == ['is reserved'] end end - + end context '#ordered_contents' do @@ -92,4 +92,180 @@ describe ContentType do end + describe 'custom fields' do + + before(:each) do + site = Factory.build(:site) + Site.stubs(:find).returns(site) + @content_type = Factory.build(:content_type, :site => site, :highlighted_field_name => 'custom_field_1') + @content_type.content_custom_fields.build :label => 'My Description', :_alias => 'description', :kind => 'text' + @content_type.content_custom_fields.build :label => 'Active', :kind => 'boolean' + # ContentType.logger = Logger.new($stdout) + # ContentType.db.connection.instance_variable_set(:@logger, Logger.new($stdout)) + end + + context 'unit' do + + before(:each) do + @field = CustomFields::Field.new(:kind => 'String') + end + + it 'should tell if it is a String' do + @field.string?.should be_true + end + + it 'should tell if it is a Text' do + @field.kind = 'Text' + @field.text?.should be_true + end + + end + + context 'validation' do + + %w{label kind}.each do |key| + it "should validate presence of #{key}" do + field = @content_type.content_custom_fields.build({ :label => 'Shortcut', :kind => 'String' }.merge(key.to_sym => nil)) + field.should_not be_valid + field.errors[key.to_sym].should == ["can't be blank"] + end + end + + it 'should not have unique label' do + field = @content_type.content_custom_fields.build :label => 'Active', :kind => 'Boolean' + field.should_not be_valid + field.errors[:label].should == ["is already taken"] + end + + it 'should invalidate parent if custom field is not valid' do + field = @content_type.content_custom_fields.build + @content_type.should_not be_valid + @content_type.content_custom_fields.last.errors[:label].should == ["can't be blank"] + end + + end + + context 'define core attributes' do + + it 'should have an unique name' do + @content_type.content_custom_fields.first._name.should == "custom_field_1" + @content_type.content_custom_fields.last._name.should == "custom_field_2" + end + + it 'should have an unique alias' do + @content_type.content_custom_fields.first.safe_alias.should == "description" + @content_type.content_custom_fields.last.safe_alias.should == "active" + end + + end + + context 'build and save' do + + it 'should build asset' do + asset = @content_type.contents.build + lambda { + asset.description + asset.active + asset.custom_fields.size.should == 2 + }.should_not raise_error + end + + it 'should assign values to newly built asset' do + asset = build_content(@content_type) + asset.description.should == 'Lorem ipsum' + asset.active.should == true + end + + it 'should save asset' do + asset = build_content(@content_type) + asset.save and @content_type.reload + asset = @content_type.contents.first + asset.description.should == 'Lorem ipsum' + asset.active.should == true + end + + it 'should not modify contents from another collection' do + asset = build_content(@content_type) + asset.save and @content_type.reload + new_collection = ContentType.new + lambda { new_collection.contents.build.description }.should raise_error + end + + end + + context 'modifying fields' do + + before(:each) do + @asset = build_content(@content_type).save + end + + it 'should add new field' do + @content_type.content_custom_fields.build :label => 'Active at', :name => 'active_at', :kind => 'Date' + @content_type.upsert(:validate => false) + @content_type.invalidate_content_klass + @content_type.reload + asset = @content_type.contents.first + lambda { asset.active_at }.should_not raise_error + end + + it 'should remove field' do + @content_type.content_custom_fields.clear + @content_type.upsert(:validate => false) + @content_type.invalidate_content_klass + @content_type.reload + asset = @content_type.contents.first + lambda { asset.active_at }.should raise_error + end + + it 'should rename field label' do + @content_type.content_custom_fields.first.label = 'Simple description' + @content_type.content_custom_fields.first._alias = nil + @content_type.upsert(:validate => false) + + @content_type.invalidate_content_klass + @content_type.reload + + asset = @content_type.contents.first + asset.simple_description.should == 'Lorem ipsum' + end + + end + + context 'managing from hash' do + + it 'adds new field' do + @content_type.content_custom_fields.clear + field = @content_type.content_custom_fields.build :label => 'Title' + @content_type.content_custom_fields_attributes = { 0 => { :id => field.id.to_s, 'label' => 'A title', 'kind' => 'String' }, 1 => { 'label' => 'Tagline', 'kind' => 'String' } } + @content_type.content_custom_fields.size.should == 2 + @content_type.content_custom_fields.first.label.should == 'A title' + @content_type.content_custom_fields.last.label.should == 'Tagline' + end + + it 'updates/removes fields' do + field = @content_type.content_custom_fields.build :label => 'Title', :kind => 'String' + @content_type.save; @content_type = ContentType.find(@content_type.id) + @content_type.update_attributes(:content_custom_fields_attributes => { + '0' => { 'id' => lookup_field_id(0), 'label' => 'My Description', 'kind' => 'Text', '_destroy' => '1' }, + '1' => { 'id' => lookup_field_id(1), 'label' => 'Active', 'kind' => 'Boolean', '_destroy' => '1' }, + '2' => { 'id' => lookup_field_id(2), 'label' => 'My Title !', 'kind' => 'String' }, + 'new_record' => { 'label' => 'Published at', 'kind' => 'String' } + }) + @content_type = ContentType.find(@content_type.id) + @content_type.content_custom_fields.size.should == 2 + @content_type.content_custom_fields.first.label.should == 'My Title !' + end + + end + + end + + def build_content(content_type) + content_type.contents.build(:name => 'Asset on steroids', :description => 'Lorem ipsum', :active => true) + end + + def lookup_field_id(index) + @content_type.content_custom_fields.all[index].id.to_s + end + end diff --git a/spec/models/page_spec.rb b/spec/models/page_spec.rb index 9d896aa8..19996adc 100644 --- a/spec/models/page_spec.rb +++ b/spec/models/page_spec.rb @@ -70,11 +70,11 @@ describe Page do it 'should have normalized slug' do page = Factory.build(:page, :slug => ' Valid ité.html ') page.valid? - page.slug.should == 'Valid_ite' + page.slug.should == 'valid-ite-html' page = Factory.build(:page, :title => ' Valid ité.html ', :slug => nil, :site => page.site) page.should be_valid - page.slug.should == 'Valid_ite' + page.slug.should == 'valid-ite-html' end it 'has no cache strategy' do diff --git a/spec/models/theme_asset_spec.rb b/spec/models/theme_asset_spec.rb index 47e9e45b..42b94241 100644 --- a/spec/models/theme_asset_spec.rb +++ b/spec/models/theme_asset_spec.rb @@ -103,14 +103,14 @@ describe ThemeAsset do end it 'should handle stylesheet' do - @asset.content_type = 'stylesheet' + @asset.plain_text_type = 'stylesheet' @asset.valid?.should be_true @asset.stylesheet?.should be_true @asset.source.should_not be_nil end it 'should handle javascript' do - @asset.content_type = 'javascript' + @asset.plain_text_type = 'javascript' @asset.valid?.should be_true @asset.javascript?.should be_true @asset.source.should_not be_nil diff --git a/spec/requests/seo_trailing_slash_spec.rb b/spec/requests/seo_trailing_slash_spec.rb new file mode 100644 index 00000000..44181075 --- /dev/null +++ b/spec/requests/seo_trailing_slash_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'Locomotive::Middlewares::SeoTrailingSlash' do + + before(:all) do + Locomotive::Application.instance.instance_variable_set(:@app, nil) # re-initialize the stack + end + + it 'does not process the "/" url' do + get '/' + response.status.should_not be(301) + end + + it 'does not process the "/admin/" url' do + get '/admin/' + response.status.should_not be(301) + end + + it 'does not process the "/admin/*" urls' do + get '/admin/login' + response.status.should_not be(301) + end + + it 'redirects to the url without the trailing slash' do + get '/hello_world/' + response.status.should be(301) + end + +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dcd0a7c0..a466699b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,11 +1,12 @@ # This file is copied to spec/ when you run 'rails generate rspec:install' ENV["RAILS_ENV"] ||= 'test' require File.expand_path('../../config/environment', __FILE__) + require 'rspec/rails' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. -Dir[Rails.root.join('spec/support/**/*.rb')].each {|f| require f} +Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } Locomotive.configure_for_test diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb index a9350033..d864a606 100644 --- a/spec/support/carrierwave.rb +++ b/spec/support/carrierwave.rb @@ -2,7 +2,6 @@ require 'carrierwave/test/matchers' CarrierWave.configure do |config| config.storage = :file - # config.store_dir = "spec/tmp/uploads" config.cache_dir = "spec/tmp/cache" config.root = File.join(Rails.root, 'spec', 'tmp') end diff --git a/spec/support/locomotive.rb b/spec/support/locomotive.rb index d3602ab8..919c21a0 100644 --- a/spec/support/locomotive.rb +++ b/spec/support/locomotive.rb @@ -1,3 +1,18 @@ +# tiny patch to add middlewares after the initialization +module Rails + class Application < Engine + def app + @app ||= begin + if config.middleware.respond_to?(:merge_into) + config.middleware = config.middleware.merge_into(default_middleware_stack) + end + config.middleware.build(routes) + end + end + end +end + + def Locomotive.configure_for_test(force = false) Locomotive.configure do |config| config.multi_sites do |multi_sites| diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index ec5d8fbd..f47b01d3 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -93,6 +93,35 @@ module Locomotive IncludeClassMethod.new(meth) end + class PermissionsMatcher + def initialize(permission, member) + @permission = permission + @member = member + end + + def matches?(target) + @target = target + @member.ability.can? @permission, @target + end + + def failure_message_for_should + "expected #{show_object(@member)} to allow '#{@permission}' permission on #{show_object(@target)}" + end + + def failure_message_for_should_not + "expected #{show_object(@member)} not to allow '#{@permission}' permission on #{show_object(@target)}" + end + + protected + + def show_object(object) + "#{object.class.name}(#{object.id.inspect})" + end + end + + def allow_permission_from(permission, member) + PermissionsMatcher.new(permission, member) + end end end end diff --git a/spec/support/middlewares.rb b/spec/support/middlewares.rb new file mode 100644 index 00000000..e69de29b