categories in content types have been improved a lot + cross-site session + meta keys / description for site + clean code + fix bugs

This commit is contained in:
dinedine 2010-06-16 16:43:29 +02:00
parent 524ccc3613
commit b7e1cd0926
33 changed files with 224 additions and 98 deletions

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ rails_3_gems
doc/performance.txt doc/performance.txt
doc/production.sh doc/production.sh
*.gem *.gem
tmp/*

View File

@ -6,7 +6,7 @@ module Admin
before_filter :set_content_type before_filter :set_content_type
def index def index
@contents = @content_type.ordered_contents @contents = @content_type.list_or_group_contents
end end
def new def new

View File

@ -21,11 +21,10 @@ module Admin
protected protected
def new_host_if_subdomain_changed def new_host_if_subdomain_changed
host_from_site = "#{@site.subdomain}.#{Locomotive.config.default_domain}" if @site.domains.include?(request.host)
if request.host == host_from_site
{} {}
else else
{ :host => "#{host_from_site}:#{request.port}" } { :host => "#{@site.subdomain}.#{Locomotive.config.default_domain}:#{request.port}" }
end end
end end

View File

@ -33,16 +33,5 @@ module Admin
redirect_to edit_admin_my_account_url redirect_to edit_admin_my_account_url
end end
protected
def new_host_if_subdomain_changed
host_from_site = "#{@site.subdomain}.#{Locomotive.config.default_domain}"
if request.host == host_from_site
{}
else
{ :host => "#{host_from_site}:#{request.port}" }
end
end
end end
end end

View File

@ -21,4 +21,11 @@ module Admin::CustomFieldsHelper
collection.map { |field| [field.label, field._name] } collection.map { |field| [field.label, field._name] }
end end
def options_for_group_by_field(content_type, collection_name)
custom_fields_collection_name = "ordered_#{collection_name.singularize}_custom_fields".to_sym
collection = content_type.send(custom_fields_collection_name)
collection.delete_if { |f| not f.category? }
collection.map { |field| [field.label, field._name] }
end
end end

View File

@ -20,4 +20,14 @@ module Admin::PagesHelper
list list
end end
def options_for_page_cache_expiration
[
[t('.expiration.never'), 0],
[t('.expiration.hour'), 1.hour],
[t('.expiration.day'), 1.day],
[t('.expiration.week'), 1.week],
[t('.expiration.month'), 1.month]
]
end
end end

View File

@ -15,6 +15,9 @@ class ContentInstance
## associations ## ## associations ##
embedded_in :content_type, :inverse_of => :contents embedded_in :content_type, :inverse_of => :contents
## callbacks ##
before_create :add_to_list_bottom
## named scopes ## ## named scopes ##
named_scope :latest_updated, :order_by => [[:updated_at, :desc]], :limit => Locomotive.config.lastest_items_nb named_scope :latest_updated, :order_by => [[:updated_at, :desc]], :limit => Locomotive.config.lastest_items_nb
@ -26,6 +29,11 @@ class ContentInstance
protected protected
def add_to_list_bottom
Rails.logger.debug "add_to_list_bottom"
self._position_in_list = self.content_type.contents.size
end
def require_highlighted_field def require_highlighted_field
_alias = self.content_type.highlighted_field._alias.to_sym _alias = self.content_type.highlighted_field._alias.to_sym
if self.send(_alias).blank? if self.send(_alias).blank?

View File

@ -8,6 +8,7 @@ class ContentType
field :slug field :slug
field :order_by field :order_by
field :highlighted_field_name field :highlighted_field_name
field :group_by_field_name
field :api_enabled, :type => Boolean, :default => false field :api_enabled, :type => Boolean, :default => false
## associations ## ## associations ##
@ -28,6 +29,26 @@ class ContentType
## methods ## ## methods ##
def groupable?
self.group_by_field && group_by_field.category?
end
def list_or_group_contents
if self.groupable?
groups = self.contents.klass.send(:"group_by_#{self.group_by_field._alias}", :ordered_contents)
# look for items with no category or unknown ones
items_without_category = self.contents.find_all { |c| !self.group_by_field.category_ids.include?(c.send(self.group_by_field_name)) }
if not items_without_category.empty?
groups << { :name => nil, :items => items_without_category }
else
groups
end
else
self.ordered_contents
end
end
def ordered_contents(conditions = {}) def ordered_contents(conditions = {})
column = self.order_by.to_sym column = self.order_by.to_sym
@ -49,6 +70,10 @@ class ContentType
self.content_custom_fields.detect { |f| f._name == self.highlighted_field_name } self.content_custom_fields.detect { |f| f._name == self.highlighted_field_name }
end end
def group_by_field
@group_by_field ||= self.content_custom_fields.detect { |f| f._name == self.group_by_field_name }
end
protected protected
def set_default_values def set_default_values

View File

@ -12,8 +12,7 @@ class Page
field :slug field :slug
field :fullpath field :fullpath
field :published, :type => Boolean, :default => false field :published, :type => Boolean, :default => false
field :keywords field :cache_expires_in, :type => Integer, :default => 0
field :description
## associations ## ## associations ##
belongs_to_related :site belongs_to_related :site
@ -36,6 +35,7 @@ class Page
named_scope :not_found, :where => { :slug => '404', :depth => 0, :published => true } named_scope :not_found, :where => { :slug => '404', :depth => 0, :published => true }
## behaviours ## ## behaviours ##
liquid_methods :title, :fullpath
liquify_template :joined_parts liquify_template :joined_parts
## methods ## ## methods ##

View File

@ -6,6 +6,8 @@ class Site
field :name field :name
field :subdomain field :subdomain
field :domains, :type => Array, :default => [] field :domains, :type => Array, :default => []
field :meta_keywords
field :meta_description
## associations ## ## associations ##
has_many_related :pages has_many_related :pages
@ -33,7 +35,7 @@ class Site
named_scope :match_domain_with_exclusion_of, lambda { |domain, site| { :where => { :domains => domain, :_id.ne => site.id } } } named_scope :match_domain_with_exclusion_of, lambda { |domain, site| { :where => { :domains => domain, :_id.ne => site.id } } }
## behaviours ## ## behaviours ##
liquid_methods :name liquid_methods :name, :meta_keywords, :meta_description
## methods ## ## methods ##

View File

@ -10,9 +10,13 @@
= render 'admin/custom_fields/index', :f => f, :collection_name => 'contents' = render 'admin/custom_fields/index', :f => f, :collection_name => 'contents'
- unless f.object.new_record? - unless f.object.new_record?
= f.foldable_inputs :name => :options, :class => 'switchable' do = f.foldable_inputs :name => :presentation, :class => 'switchable' do
= f.input :highlighted_field_name, :as => :select, :collection => options_for_highlighted_field(f.object, 'contents'), :include_blank => false = f.input :highlighted_field_name, :as => :select, :collection => options_for_highlighted_field(f.object, 'contents'), :include_blank => false
= f.input :group_by_field_name, :as => :select, :collection => options_for_group_by_field(f.object, 'contents')
= f.foldable_inputs :name => :options, :class => 'switchable' do
= f.input :order_by, :as => :select, :collection => options_for_order_by(f.object, 'contents'), :include_blank => false = f.input :order_by, :as => :select, :collection => options_for_order_by(f.object, 'contents'), :include_blank => false
= f.custom_input :api_enabled, :css => 'toggle' do = f.custom_input :api_enabled, :css => 'toggle' do
= f.check_box :api_enabled = f.check_box :api_enabled

View File

@ -0,0 +1,16 @@
- if contents.empty?
%p.no-items= t('.no_items', :url => new_admin_content_url(@content_type.slug))
- else
- puts contents.inspect
%ul{ :id => 'contents-list', :class => "list #{'sortable' if @content_type.order_by == '_position_in_list'}" }
- contents.each do |content|
%li.content{ :id => "content-#{content._id}" }
%em
%strong
= link_to content.send(@content_type.highlighted_field_name), edit_admin_content_path(@content_type.slug, content)
.more
%span
= t('admin.contents.index.updated_at')
= l content.updated_at, :format => :short rescue 'n/a'
= link_to image_tag('admin/list/icons/trash.png'), admin_content_path(@content_type.slug, content), :class => 'remove', :confirm => t('admin.messages.confirm'), :method => :delete

View File

@ -13,21 +13,13 @@
- if @content_type.description.present? - if @content_type.description.present?
%p= @content_type.description %p= @content_type.description
- if @contents.empty? - if @content_type.groupable?
%p.no-items= t('.no_items', :url => new_admin_content_url(@content_type.slug)) - @contents.each do |group|
%h3= group[:name] || t('.category_noname')
= render 'list', :contents => group[:items]
%br
- else - else
%ul{ :id => 'contents-list', :class => "list #{'sortable' if @content_type.order_by == '_position_in_list'}" } = render 'list', :contents => @contents
- @contents.each do |content|
%li.content{ :id => "content-#{content._id}" }
%em
%strong
= link_to content.send(@content_type.highlighted_field_name), edit_admin_content_path(@content_type.slug, content)
.more
%span
= t('admin.contents.index.updated_at')
= l content.updated_at, :format => :short rescue 'n/a'
= link_to image_tag('admin/list/icons/trash.png'), admin_content_path(@content_type.slug, content), :class => 'remove', :confirm => t('admin.messages.confirm'), :method => :delete
= form_tag sort_admin_contents_path(@content_type.slug), :method => :put, :class => 'formtastic' do = form_tag sort_admin_contents_path(@content_type.slug), :method => :put, :class => 'formtastic' do
= hidden_field_tag :order = hidden_field_tag :order

View File

@ -4,6 +4,11 @@
= f.foldable_inputs :name => :information, :style => "#{'display: none' unless @site.new_record?}" do = f.foldable_inputs :name => :information, :style => "#{'display: none' unless @site.new_record?}" do
= f.input :name, :required => false = f.input :name, :required => false
= f.foldable_inputs :name => :meta do
= f.input :meta_keywords
= f.input :meta_description
= f.foldable_inputs :name => :access_points, :class => 'editable-list off' do = f.foldable_inputs :name => :access_points, :class => 'editable-list off' do
= f.custom_input :subdomain, :css => 'path' do = f.custom_input :subdomain, :css => 'path' do

View File

@ -16,10 +16,7 @@
= f.custom_input :published, :css => 'toggle' do = f.custom_input :published, :css => 'toggle' do
= f.check_box :published = f.check_box :published
= f.foldable_inputs :name => :meta do = f.input :cache_expires_in, :as => :select, :collection => options_for_page_cache_expiration, :include_blank => false
= f.input :keywords
= f.input :description
#page-parts #page-parts
.nav .nav

View File

@ -84,8 +84,16 @@ en:
successful_create: "Page was successfully created." successful_create: "Page was successfully created."
successful_update: "Page was successfully updated." successful_update: "Page was successfully updated."
successful_destroy: "Page was successfully deleted." successful_destroy: "Page was successfully deleted."
successful_sort: "Pages were successfully sorted."
failed_create: "Page was not created." failed_create: "Page was not created."
failed_update: "Page was not updated." failed_update: "Page was not updated."
form:
expiration:
never: Never
hour: 1 hour
day: 1 day
week: 1 week
month: 1 month
layouts: layouts:
index: index:
@ -265,9 +273,11 @@ en:
destroy: remove model destroy: remove model
download: download items download: download items
new: new item new: new item
no_items: "There are no items for now. Just click <a href=\"{{url}}\">here</a> to create the first one." category_noname: "No name"
lastest_items: "Lastest items" lastest_items: "Lastest items"
updated_at: "Updated at" updated_at: "Updated at"
list:
no_items: "There are no items for now. Just click <a href=\"{{url}}\">here</a> to create the first one."
new: new:
title: '{{type}} &mdash; new item' title: '{{type}} &mdash; new item'
edit: edit:
@ -297,6 +307,7 @@ en:
options: Advanced options options: Advanced options
custom_fields: Custom fields custom_fields: Custom fields
other_fields: Other information other_fields: Other information
presentation: Presentation
labels: labels:
theme_asset: theme_asset:
new: new:
@ -311,6 +322,8 @@ en:
page: page:
keywords: "Meta keywords used within the head tag of the page. They are separeted by an empty space. Required for SEO." keywords: "Meta keywords used within the head tag of the page. They are separeted by an empty space. Required for SEO."
description: "Meta description used within the head tag of the page. Required for SEO." description: "Meta description used within the head tag of the page. Required for SEO."
published: "Only authenticated accounts can view unpublished pages."
cache_expires_in: "Cache the page for better performance. Pressing shift-reload in the browser will regenerate the page."
snippet: snippet:
slug: "You need to know it in order to insert the snippet inside a page or a layout" slug: "You need to know it in order to insert the snippet inside a page or a layout"
site: site:

View File

@ -1,7 +1,7 @@
BOARD: BOARD:
- deploy on Heroku - new custom field types
- observers to add / remove domains (http://groups.google.com/group/heroku/browse_thread/thread/148d6ea68e4574fb/4d8f1c8545d52bda?lnk=gst&q=heroku+gem+api#4d8f1c8545d52bda) - boolean
BACKLOG: BACKLOG:
@ -13,16 +13,15 @@ BACKLOG:
- theme assets: disable version if not image - theme assets: disable version if not image
- new custom field types - new custom field types:
- file - file
- boolean
- date - date
- refactoring admin crud (pages + layouts + snippets) - refactoring admin crud (pages + layouts + snippets)
- refactor slugify method (use parameterize + create a module) - refactor slugify method (use parameterize + create a module)
- cucumber features for admin pages - cucumber features for admin pages
- Heroku / S3 / Worker - tiny mce or similar for custom field text type.
BUGS: BUGS:
@ -32,8 +31,8 @@ NICE TO HAVE:
- asset collections: custom resizing if image - asset collections: custom resizing if image
- super_finder - super_finder
- better icons for mime type - better icons for mime type
- hint for custom fields (super easy to do !) - traffic statistics
- tiny mce or similar for custom field text type. - Heroku / S3 / Worker (not so sure finally)
DONE: DONE:
x admin layout x admin layout
@ -134,3 +133,11 @@ x site subdomain regexp [a-z][A-Z][0-9]
x [BUG] "field name" for alias / hint for custom fields x [BUG] "field name" for alias / hint for custom fields
x [BUG] can not remove custom fields at creation if object is invalid x [BUG] can not remove custom fields at creation if object is invalid
x internal logger x internal logger
x deploy on Heroku
x observers to add / remove domains (http://groups.google.com/group/heroku/browse_thread/thread/148d6ea68e4574fb/4d8f1c8545d52bda?lnk=gst&q=heroku+gem+api#4d8f1c8545d52bda)
x varnish caching (only with Heroku ?)
x page not included in liquid templates
x redirect to referer url when updating site
x [BUG] items non sorted in a category
x contents grouped by "category" in the admin
x hint for custom fields (super easy to do !)

View File

@ -32,10 +32,9 @@ module Locomotive
ActionMailer::Base.default_url_options[:host] = self.config.default_domain + (Rails.env.development? ? ':3000' : '') ActionMailer::Base.default_url_options[:host] = self.config.default_domain + (Rails.env.development? ? ':3000' : '')
# cookies stored in mongodb # cookies stored in mongodb (mongoid_store)
Rails.application.config.session_store :mongoid_store, { Rails.application.config.session_store :mongoid_store, {
:key => Locomotive.config.cookie_key, :key => Locomotive.config.cookie_key
:domain => ".#{Locomotive.config.default_domain}"
} }
# Heroku support # Heroku support

View File

@ -1,2 +0,0 @@
Devise::SessionsController.class_eval do
end

View File

@ -53,9 +53,13 @@ module Locomotive
def before_method(meth) def before_method(meth)
klass = @content_type.contents.klass # delegate to the proxy class klass = @content_type.contents.klass # delegate to the proxy class
if (meth.to_s =~ /^group_by_.+$/) == 0
klass.send(meth, :ordered_contents)
else
klass.send(meth) klass.send(meth)
end end
end end
end end
end end
end
end end

View File

@ -1,2 +1 @@
# load patches for devise, ...etc # load patches here
require 'locomotive/devise/sessions_controller'

View File

@ -8,13 +8,13 @@ module Locomotive
protected protected
def render_locomotive_page def render_locomotive_page
page = locomotive_page @page = locomotive_page
redirect_to application_root_url and return if page.nil? redirect_to application_root_url and return if @page.nil?
output = page.render(locomotive_context) output = @page.render(locomotive_context)
prepare_and_set_response(output) prepare_and_set_response(output, @page.cache_expires_in || 0)
end end
def locomotive_page def locomotive_page
@ -35,6 +35,7 @@ module Locomotive
def locomotive_context def locomotive_context
assigns = { assigns = {
'site' => current_site, 'site' => current_site,
'page' => @page,
'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new(current_site), 'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new(current_site),
'stylesheets' => Locomotive::Liquid::Drops::Stylesheets.new(current_site), 'stylesheets' => Locomotive::Liquid::Drops::Stylesheets.new(current_site),
'javascripts' => Locomotive::Liquid::Drops::Javascripts.new(current_site), 'javascripts' => Locomotive::Liquid::Drops::Javascripts.new(current_site),
@ -42,13 +43,14 @@ module Locomotive
'current_page' => self.params[:page] 'current_page' => self.params[:page]
} }
registers = { :controller => self, :site => current_site } registers = { :controller => self, :site => current_site, :page => @page }
::Liquid::Context.new(assigns, registers) ::Liquid::Context.new(assigns, registers)
end end
def prepare_and_set_response(output) def prepare_and_set_response(output, cache_expiration = 0)
response.headers["Content-Type"] = 'text/html; charset=utf-8' response.headers['Cache-Control'] = "public, max-age=#{cache_expiration}" if cache_expiration > 0
response.headers['Content-Type'] = 'text/html; charset=utf-8'
render :text => output, :layout => false, :status => :ok render :text => output, :layout => false, :status => :ok
end end

View File

@ -1,16 +1,11 @@
module Locomotive module Locomotive
module Routing module Routing
class DefaultConstraint class DefaultConstraint
def self.matches?(request) def self.matches?(request)
domain, subdomain = domain_and_subdomain(request) domain, subdomain = domain_and_subdomain(request)
subdomain = 'www' if subdomain.blank? subdomain = 'www' if subdomain.blank?
# puts "domain = #{domain} / #{Locomotive.config.default_domain.inspect}"
# puts "subdomain = #{subdomain} / #{Locomotive.config.reserved_subdomains.inspect}"
domain == Locomotive.config.default_domain && Locomotive.config.reserved_subdomains.include?(subdomain) domain == Locomotive.config.default_domain && Locomotive.config.reserved_subdomains.include?(subdomain)
end end
@ -38,8 +33,7 @@ module Locomotive
parts = request.host.split('.') parts = request.host.split('.')
parts[0..-(tld_length+2)] parts[0..-(tld_length+2)]
end end
end
end end
end
end end

View File

@ -1,18 +1,14 @@
module Locomotive module Locomotive
module Routing module Routing
module SiteDispatcher module SiteDispatcher
def self.included(base) extend ActiveSupport::Concern
base.class_eval do
include Locomotive::Routing::SiteDispatcher::InstanceMethods
included do
before_filter :fetch_site before_filter :fetch_site
helper_method :current_site helper_method :current_site
end end
end
module InstanceMethods module InstanceMethods
@ -43,7 +39,5 @@ module Locomotive
end end
end end
end end
end end

View File

@ -1,9 +1,11 @@
$(document).ready(function() { $(document).ready(function() {
var updateContentsOrder = function() { var updateContentsOrder = function() {
var list = $('ul#contents-list.sortable'); var lists = $('ul#contents-list.sortable');
var ids = jQuery.map(list.sortable('toArray'), function(e) { var ids = jQuery.map(lists, function(list) {
return e.match(/content-(\w+)/)[1]; return(jQuery.map($(list).sortable('toArray'), function(el) {
return el.match(/content-(\w+)/)[1];
}).join(','));
}).join(','); }).join(',');
$('#order').val(ids || ''); $('#order').val(ids || '');
} }

View File

@ -0,0 +1,33 @@
require 'spec_helper'
describe Locomotive::Liquid::Drops::Contents do
before(:each) do
@site = Factory.build(:site)
@content_type = Factory.build(:content_type, :site => @site, :slug => 'projects')
end
it 'retrieves a content type from a slug' do
@site.content_types.expects(:where).with(:slug => 'projects')
render_template '{{ contents.projects }}'
end
context '#group_by' do
it 'orders contents' do
@site.content_types.stubs(:where).returns([@content_type])
@content_type.contents.klass.expects(:group_by_category).with(:ordered_contents)
render_template '{% for group in contents.projects.group_by_category %} {{ group.name }} {% endfor %}'
end
end
def render_template(template = '', assigns = {})
assigns = {
'contents' => Locomotive::Liquid::Drops::Contents.new(@site)
}.merge(assigns)
Liquid::Template.parse(template).render assigns
end
end

View File

@ -16,14 +16,23 @@ describe 'Locomotive rendering system' do
@controller.send(:prepare_and_set_response, 'Hello world !') @controller.send(:prepare_and_set_response, 'Hello world !')
end end
it 'should have a html content type' do it 'sets the content type to html' do
@controller.response.headers["Content-Type"].should == 'text/html; charset=utf-8' @controller.response.headers['Content-Type'].should == 'text/html; charset=utf-8'
end end
it 'should display the output' do it 'displays the output' do
@controller.output.should == 'Hello world !' @controller.output.should == 'Hello world !'
end end
it 'does not set the cache' do
@controller.response.headers['Cache-Control'].should be_nil
end
it 'sets the cache' do
@controller.send(:prepare_and_set_response, 'Hello world !', 3600)
@controller.response.headers['Cache-Control'].should == 'public, max-age=3600'
end
end end
context 'when retrieving page' do context 'when retrieving page' do

View File

@ -3,7 +3,7 @@ source "http://gemcutter.org"
gem "bson_ext", ">= 1.0.1" gem "bson_ext", ">= 1.0.1"
gem "mongo_ext" gem "mongo_ext"
gem "mongoid", ">= 2.0.0.beta6" gem "mongoid", ">= 2.0.0.beta6"
gem "activesupport", ">= 3.0.0.beta4" gem "activesupport", ">= 3.0.0.beta3"
group :test do group :test do
gem 'rspec', '>= 2.0.0.beta.10' gem 'rspec', '>= 2.0.0.beta.10'

View File

@ -18,6 +18,14 @@ module CustomFields
self.category_items.sort { |a, b| (a.position || 0) <=> (b.position || 0) } self.category_items.sort { |a, b| (a.position || 0) <=> (b.position || 0) }
end end
def category_names
self.category_items.collect(&:name)
end
def category_ids
self.category_items.collect(&:_id)
end
def apply_category_type(klass) def apply_category_type(klass)
klass.cattr_accessor :"#{self.safe_alias}_items" klass.cattr_accessor :"#{self.safe_alias}_items"
@ -28,11 +36,13 @@ module CustomFields
self.#{self.safe_alias}_items.collect(&:name) self.#{self.safe_alias}_items.collect(&:name)
end end
def self.group_by_#{self.safe_alias} def self.group_by_#{self.safe_alias}(list_method = nil)
groups = (if self.embedded? groups = (if self.embedded?
self._parent.send(self.association_name).all list_method ||= self.association_name
self._parent.send(list_method)
else else
self.all list_method ||= :all
self.send(list_method)
end).to_a.group_by(&:#{self._name}) end).to_a.group_by(&:#{self._name})
self.#{self.safe_alias}_items.collect do |category| self.#{self.safe_alias}_items.collect do |category|

View File

@ -13,4 +13,6 @@ class Project
custom_fields_for :people custom_fields_for :people
custom_fields_for :tasks custom_fields_for :tasks
named_scope :ordered, :order_by => [[:name, :asc]]
end end

View File

@ -67,6 +67,11 @@ describe CustomFields::Types::Category do
@groups[2][:items].size.should == 2 @groups[2][:items].size.should == 2
end end
it 'passes method to retrieve items' do
@project.class.expects(:ordered)
@project.class.group_by_global_category(:ordered)
end
end end
end end