diff --git a/app/controllers/admin/asset_collections_controller.rb b/app/controllers/admin/asset_collections_controller.rb new file mode 100644 index 00000000..b2d6c35c --- /dev/null +++ b/app/controllers/admin/asset_collections_controller.rb @@ -0,0 +1,66 @@ +module Admin + class AssetCollectionsController < BaseController + + sections 'assets' + + before_filter :set_collections + + def index + if not @collections.empty? + redirect_to(edit_admin_asset_collection_url(@collections.first)) and return + end + end + + def show + @collection = current_site.asset_collections.find(params[:id]) + render :action => 'edit' + end + + def new + @collection = current_site.asset_collections.build + end + + def edit + @collection = current_site.asset_collections.find(params[:id]) + end + + def create + @collection = current_site.asset_collections.build(params[:asset_collection]) + + if @collection.save + flash_success! + redirect_to edit_admin_asset_collection_url(@collection) + else + flash_error! + render :action => 'new' + end + end + + def update + @collection = current_site.asset_collections.find(params[:id]) + + if @collection.update_attributes(params[:asset_collection]) + flash_success! + redirect_to edit_admin_asset_collection_url(@collection) + else + flash_error! + render :action => 'edit' + end + end + + def destroy + @collection = current_site.asset_collections.find(params[:id]) + @collection.destroy + + flash_success! + + redirect_to admin_asset_collections_url + end + + protected + + def set_collections + @collections = current_site.asset_collections + end + end +end diff --git a/app/controllers/admin/assets_controller.rb b/app/controllers/admin/assets_controller.rb new file mode 100644 index 00000000..cdeb1926 --- /dev/null +++ b/app/controllers/admin/assets_controller.rb @@ -0,0 +1,48 @@ +module Admin + class AssetsController < BaseController + + sections 'assets' + + before_filter :set_collections_and_current_collection + + def new + @asset = @collection.assets.build + end + + def edit + @asset = @collection.assets.find(params[:id]) + end + + def create + @asset = @collection.assets.build(params[:asset]) + + if @asset.save + flash_success! + redirect_to edit_admin_asset_collection_url(@collection) + else + flash_error! + render :action => 'new' + end + end + + def update + @asset = @collection.assets.find(params[:id]) + + if @asset.update_attributes(params[:asset]) + flash_success! + redirect_to edit_admin_asset_collection_url(@collection) + else + flash_error! + render :action => 'edit' + end + end + + protected + + def set_collections_and_current_collection + @collections = current_site.asset_collections + @collection = @collections.find(params[:collection_id]) + end + + end +end diff --git a/app/models/asset.rb b/app/models/asset.rb new file mode 100644 index 00000000..69322400 --- /dev/null +++ b/app/models/asset.rb @@ -0,0 +1,28 @@ +class Asset + include Mongoid::Document + include Mongoid::Timestamps + + ## fields ## + 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, AssetUploader + + ## associations ## + embedded_in :collection, :class_name => 'AssetCollection', :inverse_of => :assets + + ## validations ## + validates_presence_of :name, :source + + ## methods ## + + %w{image stylesheet javascript pdf video audio}.each do |type| + define_method("#{type}?") do + self.content_type == type + end + end + +end \ No newline at end of file diff --git a/app/models/asset_collection.rb b/app/models/asset_collection.rb new file mode 100644 index 00000000..9150f32b --- /dev/null +++ b/app/models/asset_collection.rb @@ -0,0 +1,57 @@ +class AssetCollection + include Mongoid::Document + include Mongoid::Timestamps + + ## fields ## + field :name, :type => String + field :slug, :type => String + + ## associations ## + belongs_to_related :site + embeds_many :assets + # has_many_related :assets + + ## callbacks ## + before_validate :normalize_slug + before_save :store_asset_positions! + + ## validations ## + validates_presence_of :site, :name, :slug + validates_uniqueness_of :slug, :scope => :site_id + + ## 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 + + 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? + + @assets_order.split(',').each_with_index do |asset_id, index| + self.assets.find(asset_id).position = index + end + + self.assets.each do |asset| + if !@assets_order.split(',').include?(asset._id) + self.assets.delete(asset) + asset.send(:delete) + end + end + end +end \ No newline at end of file diff --git a/app/models/site.rb b/app/models/site.rb index 29204c40..8bc13c7a 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -12,6 +12,7 @@ class Site has_many_related :layouts has_many_related :snippets has_many_related :theme_assets + has_many_related :asset_collections embeds_many :memberships ## validations ## @@ -82,7 +83,7 @@ class Site end def destroy_in_cascade! - %w{pages layouts snippets theme_assets}.each do |association| + %w{pages layouts snippets theme_assets asset_collections}.each do |association| self.send(association).destroy_all end end diff --git a/app/models/theme_asset.rb b/app/models/theme_asset.rb index 5a180cce..9e21af48 100644 --- a/app/models/theme_asset.rb +++ b/app/models/theme_asset.rb @@ -20,7 +20,7 @@ class ThemeAsset ## validations ## validate :extname_can_not_be_changed - validates_presence_of :site + validates_presence_of :site, :source validates_presence_of :slug, :if => Proc.new { |a| a.new_record? && a.performing_plain_text? } validates_integrity_of :source @@ -84,9 +84,7 @@ class ThemeAsset def extname_can_not_be_changed return if self.new_record? - - Rails.logger.debug "previous = #{self.source.file.original_filename.inspect} / #{self.source_filename.inspect}" - + if File.extname(self.source.file.original_filename) != File.extname(self.source_filename) self.errors.add(:source, :extname_changed) end diff --git a/app/uploaders/asset_uploader.rb b/app/uploaders/asset_uploader.rb new file mode 100644 index 00000000..aa230047 --- /dev/null +++ b/app/uploaders/asset_uploader.rb @@ -0,0 +1,64 @@ +# encoding: utf-8 + +class AssetUploader < CarrierWave::Uploader::Base + + include CarrierWave::RMagick + + def store_dir + "sites/#{model.collection.site_id}/assets/#{model.id}" + end + + version :thumb do + process :resize_to_fill => [50, 50] + process :convert => 'png' + end + + version :medium do + process :resize_to_fill => [80, 80] + process :convert => 'png' + end + + version :preview do + process :resize_to_fit => [880, 1100] + process :convert => 'png' + end + + process :set_content_type + process :set_size + process :set_width_and_height + + def set_content_type + value = :other + + self.class.content_types.each_pair do |type, rules| + rules.each do |rule| + case rule + when String then value = type if file.content_type == rule + when Regexp then value = type if (file.content_type =~ rule) == 0 + end + end + end + + model.content_type = value + end + + def set_size + model.size = file.size + end + + def set_width_and_height + if model.image? + model.width, model.height = `identify -format "%wx%h" #{file.path}`.split(/x/).collect(&:to_i) + end + end + + def self.content_types + { + :image => ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png', 'image/jpg'], + :movie => [/^video/, 'application/x-shockwave-flash', 'application/x-swf'], + :audio => [/^audio/, 'application/ogg', 'application/x-mp3'], + :pdf => ['application/pdf', 'application/x-pdf'] + } + end + +end diff --git a/app/uploaders/theme_asset_uploader.rb b/app/uploaders/theme_asset_uploader.rb index bce366e2..fcfd863a 100644 --- a/app/uploaders/theme_asset_uploader.rb +++ b/app/uploaders/theme_asset_uploader.rb @@ -71,8 +71,6 @@ class ThemeAssetUploader < CarrierWave::Uploader::Base end def filename - Rails.logger.debug "slug ===> #{model.slug} / #{model.content_type} / #{original_filename}" - if model.slug.present? model.filename else @@ -80,8 +78,6 @@ class ThemeAssetUploader < CarrierWave::Uploader::Base basename = File.basename(original_filename, extension).slugify(:underscore => true) "#{basename}#{extension}" end - # - # original_filename end end diff --git a/app/views/admin/asset_collections/_asset.html.haml b/app/views/admin/asset_collections/_asset.html.haml new file mode 100644 index 00000000..31615f46 --- /dev/null +++ b/app/views/admin/asset_collections/_asset.html.haml @@ -0,0 +1,7 @@ +%li{ :id => "asset-#{asset.id}", :class => "asset #{'last' if (asset_counter + 1) % 6 == 0}"} + %h4= link_to truncate(asset.name, :length => 22), edit_admin_asset_path(@collection, asset) + .image + .inside + = 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/asset_collections/edit.html.haml b/app/views/admin/asset_collections/edit.html.haml new file mode 100644 index 00000000..d0fe0adc --- /dev/null +++ b/app/views/admin/asset_collections/edit.html.haml @@ -0,0 +1,28 @@ +- title link_to(@collection.name.blank? ? @collection.name_was : @collection.name, '#', :rel => 'asset_collection_name', :title => t('.ask_for_name'), :class => 'editable') + +- content_for :head do + = javascript_include_tag 'admin/asset_collections.js' + +- content_for :submenu do + = render 'admin/shared/menu/assets' + +%p= t('.help') + +- content_for :buttons do + = admin_button_tag :add_asset, new_admin_asset_url(@collection), :class => 'add' + +%p.no-items{ :style => "#{'display: none' unless @collection.assets.empty? }" } + = t('.no_items', :url => new_admin_asset_url(@collection)) + +%ul#assets.assets.sortable + = render :partial => 'asset', :collection => @collection.ordered_assets + %li.clear + += semantic_form_for @collection, :url => admin_asset_collection_url(@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/shared/form_actions', :delete_button => link_to(content_tag(:span, t('.destroy')), admin_asset_collection_url(@collection), :confirm => t('admin.messages.confirm'), :method => :delete, :class => 'button small remove'), :button_label => :update \ No newline at end of file diff --git a/app/views/admin/asset_collections/index.html.haml b/app/views/admin/asset_collections/index.html.haml new file mode 100644 index 00000000..bf0bdc48 --- /dev/null +++ b/app/views/admin/asset_collections/index.html.haml @@ -0,0 +1,8 @@ +- title t('.title') + +- content_for :submenu do + = render 'admin/shared/menu/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 new file mode 100644 index 00000000..2f901276 --- /dev/null +++ b/app/views/admin/asset_collections/new.html.haml @@ -0,0 +1,17 @@ +- title t('.title') + +- content_for :head do + = javascript_include_tag 'admin/asset_collections.js' + +- content_for :submenu do + = render 'admin/shared/menu/assets' + +%p= t('.help') + += semantic_form_for @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/assets/_form.html.haml b/app/views/admin/assets/_form.html.haml new file mode 100644 index 00000000..461e1b7a --- /dev/null +++ b/app/views/admin/assets/_form.html.haml @@ -0,0 +1,10 @@ += f.inputs :name => :information do + = f.input :name + = f.input :source + +- if @asset.image? + = 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 new file mode 100644 index 00000000..85a07282 --- /dev/null +++ b/app/views/admin/assets/edit.html.haml @@ -0,0 +1,15 @@ +- title t('.title') + +- content_for :submenu do + = render 'admin/shared/menu/assets' + +- content_for :buttons do + = admin_button_tag t('admin.asset_collections.edit.add_asset'), new_admin_asset_url(@collection), :class => 'add' + +%p= t('.help') + += semantic_form_for @asset, :url => admin_asset_url(@collection, @asset), :html => { :multipart => true } do |form| + + = render 'form', :f => form + + = render 'admin/shared/form_actions', :back_url => edit_admin_asset_collection_url(@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 new file mode 100644 index 00000000..64b32203 --- /dev/null +++ b/app/views/admin/assets/new.html.haml @@ -0,0 +1,12 @@ +- title t('.title') + +- content_for :submenu do + = render 'admin/shared/menu/assets' + +%p= t('.help') + += semantic_form_for @asset, :url => admin_assets_url(@collection), :html => { :multipart => true } do |form| + + = render 'form', :f => form + + = render 'admin/shared/form_actions', :back_url => edit_admin_asset_collection_url(@collection), :button_label => :create \ No newline at end of file diff --git a/app/views/admin/shared/_form_actions.html.haml b/app/views/admin/shared/_form_actions.html.haml index ad872595..342fc091 100644 --- a/app/views/admin/shared/_form_actions.html.haml +++ b/app/views/admin/shared/_form_actions.html.haml @@ -3,8 +3,9 @@ %p - if defined?(back_url) = link_to escape_once('← ') + t('.back'), back_url - - else -   + - elsif defined?(delete_button) + = delete_button +   .span-12.last %p diff --git a/app/views/admin/shared/_menu.html.haml b/app/views/admin/shared/_menu.html.haml index 07856b2b..362f098d 100644 --- a/app/views/admin/shared/_menu.html.haml +++ b/app/views/admin/shared/_menu.html.haml @@ -1,5 +1,5 @@ %ul#menu = admin_menu_item('contents', admin_pages_url) - = admin_menu_item('assets', '#') + = 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/menu/_assets.html.haml b/app/views/admin/shared/menu/_assets.html.haml new file mode 100644 index 00000000..ce47f91a --- /dev/null +++ b/app/views/admin/shared/menu/_assets.html.haml @@ -0,0 +1,7 @@ +%ul + - @collections.each do |c| + %li{ :class => "#{'on' if @collection.id == c.id}" } + = link_to content_tag(:span, truncate(c.name, :length => 20)), edit_admin_asset_collection_url(c) + +.action + = 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/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 2f8e7531..1e8fa51b 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -44,5 +44,13 @@ module CarrierWave record.errors.add attr, options[:message] if record.send("#{attr}_integrity_error") end end + + def validates_processing_of(*attrs) + options = attrs.last.is_a?(Hash) ? attrs.last : {} + options[:message] ||= I18n.t('carrierwave.errors.processing', :default => 'failed to be processed.') + validates_each(*attrs) do |record, attr, value| + record.errors.add attr, options[:message] if record.send("#{attr}_processing_error") + end + end end end diff --git a/config/locales/admin_ui_en.yml b/config/locales/admin_ui_en.yml index aeb7e8a0..45d0591d 100644 --- a/config/locales/admin_ui_en.yml +++ b/config/locales/admin_ui_en.yml @@ -68,12 +68,16 @@ en: title: Listing layouts no_items: "There are no layouts for now. Just click here to create the first one." new: new layout + new: + title: New layout snippets: index: title: Listing snippets no_items: "There are no snippets for now. Just click here to create the first one." new: new snippet + new: + title: New snippet sites: new: @@ -113,6 +117,24 @@ en: choose_file: Choose file choose_plain_text: Choose plain text + asset_collections: + index: + title: Asset collections + new: new collection + no_items: "There are no collections for now. Just click here to create the first one." + new: + title: New collection + edit: + help: "The collection name can be updated by clicking it. Uploading several files at once is possible, just click on the 'upload files' button at the right corner." + add_asset: add asset + destroy: remove collection + no_items: "There are no assets for now. Just click here to create the first one." + + assets: + new: + title: New asset + + formtastic: titles: information: General information @@ -126,6 +148,7 @@ en: membership_email: Account email file: File preview: Preview + options: Advanced options labels: theme_asset: new: diff --git a/config/routes.rb b/config/routes.rb index 0f796d9b..347b23ea 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,9 +38,14 @@ Locomotive::Application.routes.draw do |map| resources :memberships resources :theme_assets + + resources :asset_collections + + resources :assets, :path => "asset_collections/:collection_id" + end - # magic url + # magic urls match '/' => 'pages#show' match '*path' => 'pages#show' end diff --git a/doc/TODO b/doc/TODO index 703c1779..750b9105 100644 --- a/doc/TODO +++ b/doc/TODO @@ -41,9 +41,14 @@ x domain scoping when authenticating x can not replace a javascript by a stylesheet - disable version if not image - asset collections + x create / update + x sort assets + x removing assets - assets + - destroy - custom resizing - +- assets uploader: + - remove old files if new one BACKLOG: - liquid rendering engine diff --git a/lib/core_ext.rb b/lib/core_ext.rb index d4893e31..22dc5ed9 100644 --- a/lib/core_ext.rb +++ b/lib/core_ext.rb @@ -21,7 +21,7 @@ class String # Turn unwanted chars into the seperator s.gsub!(/[^a-zA-Z0-9\-_\+\/]+/i, options[:sep]) # Underscore - s.gsub!(/[\-]/i, '') if options[:underscore] + s.gsub!(/[\-]/i, '_') if options[:underscore] s end diff --git a/public/javascripts/admin/asset_collections.js b/public/javascripts/admin/asset_collections.js new file mode 100644 index 00000000..6ffba1be --- /dev/null +++ b/public/javascripts/admin/asset_collections.js @@ -0,0 +1,50 @@ +$(document).ready(function() { + + // automatic slug from snippet 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 || '0'); + } + + $('ul.assets.sortable').sortable({ + items: 'li.asset', + stop: function(event, ui) { updateAssetsOrder(); } + }); + + $('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(); + + $('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--; + } + } + e.preventDefault(); + e.stopPropagation(); + }); +}); \ No newline at end of file diff --git a/public/javascripts/admin/pages.js b/public/javascripts/admin/pages.js index 507d6919..85d9e3cc 100644 --- a/public/javascripts/admin/pages.js +++ b/public/javascripts/admin/pages.js @@ -38,7 +38,7 @@ $(document).ready(function() { if (!slug.hasClass('filled')) { setTimeout(function() { - slug.val(input.val().replace(' ', '_')).addClass('touched'); + slug.val(input.val().replace(/\s/g, '_').toLowerCase()).addClass('touched'); }, 50); } }); diff --git a/public/javascripts/admin/snippets.js b/public/javascripts/admin/snippets.js index 94208609..c66f8077 100644 --- a/public/javascripts/admin/snippets.js +++ b/public/javascripts/admin/snippets.js @@ -7,7 +7,7 @@ $(document).ready(function() { if (!slug.hasClass('filled')) { setTimeout(function() { - slug.val(input.val().replace(' ', '_').toLowerCase()); + slug.val(input.val().replace(/\s/g, '_').toLowerCase()); }, 50); } }); diff --git a/public/stylesheets/admin/formtastic_changes.css b/public/stylesheets/admin/formtastic_changes.css index 246d6e0e..001993a4 100644 --- a/public/stylesheets/admin/formtastic_changes.css +++ b/public/stylesheets/admin/formtastic_changes.css @@ -426,7 +426,7 @@ form.formtastic fieldset.preview li .inside { text-align: center; } -form.formtastic fieldset.preview li img { } +form.formtastic fieldset.preview li img { } /* ___ main error message ___ */