asset collections almost done

This commit is contained in:
dinedine 2010-05-12 02:16:39 +02:00
parent 8c6e9bd981
commit 13cd09e3af
27 changed files with 472 additions and 18 deletions

View File

@ -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

View File

@ -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

28
app/models/asset.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -3,8 +3,9 @@
%p
- if defined?(back_url)
= link_to escape_once('&larr;&nbsp;') + t('.back'), back_url
- else
&nbsp;
- elsif defined?(delete_button)
= delete_button
&nbsp;
.span-12.last
%p

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -68,12 +68,16 @@ en:
title: Listing layouts
no_items: "There are no layouts for now. Just click <a href=\"{{url}}\">here</a> 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 <a href=\"{{url}}\">here</a> 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 <a href=\"{{url}}\">here</a> 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 <a href=\"{{url}}\">here</a> 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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();
});
});

View File

@ -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);
}
});

View File

@ -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);
}
});

View File

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