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 ___ */