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/production.sh
*.gem
tmp/*

View File

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

View File

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

View File

@ -32,17 +32,6 @@ module Admin
redirect_to edit_admin_my_account_url
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

View File

@ -1,3 +1,3 @@
class ApplicationController < ActionController::Base
class ApplicationController < ActionController::Base
protect_from_forgery
end

View File

@ -21,4 +21,11 @@ module Admin::CustomFieldsHelper
collection.map { |field| [field.label, field._name] }
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

View File

@ -20,4 +20,14 @@ module Admin::PagesHelper
list
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

View File

@ -15,6 +15,9 @@ class ContentInstance
## associations ##
embedded_in :content_type, :inverse_of => :contents
## callbacks ##
before_create :add_to_list_bottom
## named scopes ##
named_scope :latest_updated, :order_by => [[:updated_at, :desc]], :limit => Locomotive.config.lastest_items_nb
@ -26,6 +29,11 @@ class ContentInstance
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
_alias = self.content_type.highlighted_field._alias.to_sym
if self.send(_alias).blank?

View File

@ -8,6 +8,7 @@ class ContentType
field :slug
field :order_by
field :highlighted_field_name
field :group_by_field_name
field :api_enabled, :type => Boolean, :default => false
## associations ##
@ -28,6 +29,26 @@ class ContentType
## 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 = {})
column = self.order_by.to_sym
@ -49,6 +70,10 @@ class ContentType
self.content_custom_fields.detect { |f| f._name == self.highlighted_field_name }
end
def group_by_field
@group_by_field ||= self.content_custom_fields.detect { |f| f._name == self.group_by_field_name }
end
protected
def set_default_values

View File

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

View File

@ -6,6 +6,8 @@ class Site
field :name
field :subdomain
field :domains, :type => Array, :default => []
field :meta_keywords
field :meta_description
## associations ##
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 } } }
## behaviours ##
liquid_methods :name
liquid_methods :name, :meta_keywords, :meta_description
## methods ##

View File

@ -10,9 +10,13 @@
= render 'admin/custom_fields/index', :f => f, :collection_name => 'contents'
- 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 :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.custom_input :api_enabled, :css => 'toggle' do
= 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?
%p= @content_type.description
- if @contents.empty?
%p.no-items= t('.no_items', :url => new_admin_content_url(@content_type.slug))
- if @content_type.groupable?
- @contents.each do |group|
%h3= group[:name] || t('.category_noname')
= render 'list', :contents => group[:items]
%br
- else
%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
= render 'list', :contents => @contents
= form_tag sort_admin_contents_path(@content_type.slug), :method => :put, :class => 'formtastic' do
= hidden_field_tag :order

View File

@ -3,6 +3,11 @@
= 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.input :meta_keywords
= f.input :meta_description
= f.foldable_inputs :name => :access_points, :class => 'editable-list off' do
@ -42,4 +47,4 @@
%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
= link_to image_tag('admin/form/icons/trash.png'), admin_membership_url(membership), :class => 'remove first', :confirm => t('admin.messages.confirm'), :method => :delete

View File

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

View File

@ -32,4 +32,4 @@
%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')
%span= t('admin.buttons.new_item')

View File

@ -84,8 +84,16 @@ en:
successful_create: "Page was successfully created."
successful_update: "Page was successfully updated."
successful_destroy: "Page was successfully deleted."
successful_sort: "Pages were successfully sorted."
failed_create: "Page was not created."
failed_update: "Page was not updated."
form:
expiration:
never: Never
hour: 1 hour
day: 1 day
week: 1 week
month: 1 month
layouts:
index:
@ -264,10 +272,12 @@ en:
edit: edit model
destroy: remove model
download: download items
new: new item
no_items: "There are no items for now. Just click <a href=\"{{url}}\">here</a> to create the first one."
new: new item
category_noname: "No name"
lastest_items: "Lastest items"
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:
title: '{{type}} &mdash; new item'
edit:
@ -297,6 +307,7 @@ en:
options: Advanced options
custom_fields: Custom fields
other_fields: Other information
presentation: Presentation
labels:
theme_asset:
new:
@ -311,6 +322,8 @@ en:
page:
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."
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:
slug: "You need to know it in order to insert the snippet inside a page or a layout"
site:

View File

@ -1,7 +1,7 @@
BOARD:
- deploy on Heroku
- observers to add / remove domains (http://groups.google.com/group/heroku/browse_thread/thread/148d6ea68e4574fb/4d8f1c8545d52bda?lnk=gst&q=heroku+gem+api#4d8f1c8545d52bda)
- new custom field types
- boolean
BACKLOG:
@ -13,16 +13,15 @@ BACKLOG:
- theme assets: disable version if not image
- new custom field types
- new custom field types:
- file
- boolean
- date
- refactoring admin crud (pages + layouts + snippets)
- refactor slugify method (use parameterize + create a module)
- cucumber features for admin pages
- Heroku / S3 / Worker
- tiny mce or similar for custom field text type.
BUGS:
@ -32,8 +31,8 @@ NICE TO HAVE:
- asset collections: custom resizing if image
- super_finder
- better icons for mime type
- hint for custom fields (super easy to do !)
- tiny mce or similar for custom field text type.
- traffic statistics
- Heroku / S3 / Worker (not so sure finally)
DONE:
x admin layout
@ -133,4 +132,12 @@ x site subdomain regexp [a-z][A-Z][0-9]
! migrate content_instance to its own collection => http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24sliceoperator
x [BUG] "field name" for alias / hint for custom fields
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' : '')
# cookies stored in mongodb
# cookies stored in mongodb (mongoid_store)
Rails.application.config.session_store :mongoid_store, {
:key => Locomotive.config.cookie_key,
:domain => ".#{Locomotive.config.default_domain}"
:key => Locomotive.config.cookie_key
}
# Heroku support

View File

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

View File

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

View File

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

View File

@ -8,13 +8,13 @@ module Locomotive
protected
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
def locomotive_page
@ -35,6 +35,7 @@ module Locomotive
def locomotive_context
assigns = {
'site' => current_site,
'page' => @page,
'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new(current_site),
'stylesheets' => Locomotive::Liquid::Drops::Stylesheets.new(current_site),
'javascripts' => Locomotive::Liquid::Drops::Javascripts.new(current_site),
@ -42,13 +43,14 @@ module Locomotive
'current_page' => self.params[:page]
}
registers = { :controller => self, :site => current_site }
registers = { :controller => self, :site => current_site, :page => @page }
::Liquid::Context.new(assigns, registers)
end
def prepare_and_set_response(output)
response.headers["Content-Type"] = 'text/html; charset=utf-8'
def prepare_and_set_response(output, cache_expiration = 0)
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
end

View File

@ -1,16 +1,11 @@
module Locomotive
module Routing
class DefaultConstraint
def self.matches?(request)
domain, subdomain = domain_and_subdomain(request)
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)
end
@ -38,8 +33,7 @@ module Locomotive
parts = request.host.split('.')
parts[0..-(tld_length+2)]
end
end
end
end
end
end

View File

@ -1,17 +1,13 @@
module Locomotive
module Routing
module Routing
module SiteDispatcher
def self.included(base)
base.class_eval do
include Locomotive::Routing::SiteDispatcher::InstanceMethods
extend ActiveSupport::Concern
included do
before_filter :fetch_site
before_filter :fetch_site
helper_method :current_site
end
helper_method :current_site
end
module InstanceMethods
@ -42,8 +38,6 @@ module Locomotive
end
end
end
end
end
end

View File

@ -1,9 +1,11 @@
$(document).ready(function() {
var updateContentsOrder = function() {
var list = $('ul#contents-list.sortable');
var ids = jQuery.map(list.sortable('toArray'), function(e) {
return e.match(/content-(\w+)/)[1];
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 || '');
}

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,13 +16,22 @@ describe 'Locomotive rendering system' do
@controller.send(:prepare_and_set_response, 'Hello world !')
end
it 'should have a html content type' do
@controller.response.headers["Content-Type"].should == 'text/html; charset=utf-8'
it 'sets the content type to html' do
@controller.response.headers['Content-Type'].should == 'text/html; charset=utf-8'
end
it 'should display the output' do
it 'displays the output' do
@controller.output.should == 'Hello world !'
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

View File

@ -3,7 +3,7 @@ source "http://gemcutter.org"
gem "bson_ext", ">= 1.0.1"
gem "mongo_ext"
gem "mongoid", ">= 2.0.0.beta6"
gem "activesupport", ">= 3.0.0.beta4"
gem "activesupport", ">= 3.0.0.beta3"
group :test do
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) }
end
def category_names
self.category_items.collect(&:name)
end
def category_ids
self.category_items.collect(&:_id)
end
def apply_category_type(klass)
klass.cattr_accessor :"#{self.safe_alias}_items"
@ -28,11 +36,13 @@ module CustomFields
self.#{self.safe_alias}_items.collect(&:name)
end
def self.group_by_#{self.safe_alias}
def self.group_by_#{self.safe_alias}(list_method = nil)
groups = (if self.embedded?
self._parent.send(self.association_name).all
list_method ||= self.association_name
self._parent.send(list_method)
else
self.all
list_method ||= :all
self.send(list_method)
end).to_a.group_by(&:#{self._name})
self.#{self.safe_alias}_items.collect do |category|

View File

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

View File

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