radiant-library-extension 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +112 -0
- data/Rakefile +137 -0
- data/app/models/library_page.rb +61 -0
- data/app/views/admin/assets/_edit_metadata.html.haml +20 -0
- data/app/views/admin/assets/_edit_title.html.haml +31 -0
- data/app/views/admin/tags/_show_assets.html.haml +22 -0
- data/cucumber.yml +1 -0
- data/db/migrate/20091030115120_furniture.rb +9 -0
- data/features/support/env.rb +16 -0
- data/features/support/paths.rb +14 -0
- data/lib/library/library_tag.rb +15 -0
- data/lib/library/library_tags.rb +276 -0
- data/lib/library/link_renderer.rb +22 -0
- data/lib/library/more_asset_tags.rb +151 -0
- data/lib/library/more_tag_tags.rb +179 -0
- data/lib/library/site_controller.rb +33 -0
- data/lib/library/tagged_asset.rb +42 -0
- data/lib/tasks/library_extension_tasks.rake +28 -0
- data/library_extension.rb +24 -0
- data/spec/controllers/library_site_controller_spec.rb +97 -0
- data/spec/datasets/library_pages_dataset.rb +10 -0
- data/spec/datasets/library_sites_dataset.rb +9 -0
- data/spec/datasets/library_tags_dataset.rb +43 -0
- data/spec/models/library_page_spec.rb +47 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +36 -0
- metadata +128 -0
data/README.md
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# Library
|
2
|
+
|
3
|
+
This is a gallery/library displayer. It combines [paperclipped](http://github.com/spanner/paperclipped) (for images and documents) and [taggable](http://github.com/spanner/radiant-taggable-extension) to offer faceted tag-based retrieval of everything through a single page. I use it for image libraries, document downloads, and on big sites just for page-retrieval. Anything that is tagged can be offered through this interface.
|
4
|
+
|
5
|
+
## Status
|
6
|
+
|
7
|
+
Fairly mature and in use in the world. I've just rationalised the radius tags to support what has emerged as the likely uses, so the interface is now reliable.
|
8
|
+
|
9
|
+
We're now using built-in pagination and gem-based configuration, so radiant 0.9 is required.
|
10
|
+
|
11
|
+
The requirement for our fork of paperclipped should soon disappear.
|
12
|
+
|
13
|
+
## Requirements
|
14
|
+
|
15
|
+
* Radiant 0.9
|
16
|
+
* [paperclipped](http://github.com/spanner/paperclipped) (currently, requires our fork) and [taggable](http://github.com/spanner/radiant-taggable-extension) extensions
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
As usual:
|
21
|
+
|
22
|
+
./script/extension install library
|
23
|
+
|
24
|
+
or some variation on:
|
25
|
+
|
26
|
+
git submodule add git://github.com/spanner/radiant-library-extension.git vendor/extensions/library
|
27
|
+
rake radiant:extensions:library:update
|
28
|
+
rake radiant:extensions:library:migrate
|
29
|
+
|
30
|
+
## Configuration
|
31
|
+
|
32
|
+
You need to make sure that paperclipped and taggable load before this does, and anything that adds content types to paperclipped. This is the most awkward sequence I've had to use:
|
33
|
+
|
34
|
+
config.extensions = [ :share_layouts, :sites, :taggable, :reader, :reader_group, :paperclipped, :paperclipped_gps, :all, :library ]
|
35
|
+
|
36
|
+
## Library pages
|
37
|
+
|
38
|
+
The **LibraryPage** page type is a handy cache-friendly way of catching tag parameters and displaying lists of related items: any path following the address of the page is taken as a slash-separated list of tags, so with a tag page at /archive you can call addresses like:
|
39
|
+
|
40
|
+
/archive/lasagne/chips/pudding
|
41
|
+
|
42
|
+
and the right tags will be retrieved, if they exist.
|
43
|
+
|
44
|
+
## Examples
|
45
|
+
|
46
|
+
This will display a faceted image browser on a library page:
|
47
|
+
|
48
|
+
<r:library:if_requested_tags>
|
49
|
+
<p>Displaying pictures tagged with <r:library:requested_tags /></p>
|
50
|
+
</r:library:if_requested_tags>
|
51
|
+
|
52
|
+
<r:library:images:each paginated="true" per_page="20">
|
53
|
+
<r:assets:link size="full"><r:assets:image size="small" /></r:assets:link>
|
54
|
+
</r:library:images:each>
|
55
|
+
|
56
|
+
<r:library:tags for="images" />
|
57
|
+
|
58
|
+
And this will automate the illustration of any page based on tag-overlap:
|
59
|
+
|
60
|
+
<r:related_images:each limit="3">
|
61
|
+
<r:assets:image size="standard" />
|
62
|
+
<p class="caption"><r:assets:caption /></p>
|
63
|
+
</r:related_images:each>
|
64
|
+
|
65
|
+
## Radius tags
|
66
|
+
|
67
|
+
This extension used to define a ridiculous confusion of tags, so I have slimmed it ruthlessly to make likely uses easy rather than to make all uses possible.
|
68
|
+
|
69
|
+
### Library page tags
|
70
|
+
|
71
|
+
The library tags now focus on two tasks: choosing a set of tags and displaying a set of matching objects.
|
72
|
+
|
73
|
+
<r:library:tags />
|
74
|
+
<r:library:tags:each>...</r:library:tags:each>
|
75
|
+
|
76
|
+
Displays a list of the tags available. If any tags have been requested, this will show the list of coincident tags (that can be used to limit the result set further). If not it shows all the available tags. If a `for` attribute is set:
|
77
|
+
|
78
|
+
<r:library:tags for="images" />
|
79
|
+
<r:library:tags for="pages" />
|
80
|
+
|
81
|
+
Then we show only the set of tags attached to any object of that kind.
|
82
|
+
|
83
|
+
<r:library:requested_tags />
|
84
|
+
<r:library:requested_tags:each>...</r:library:requested_tags:each>
|
85
|
+
|
86
|
+
Displays the currently-limiting set of tags.
|
87
|
+
|
88
|
+
<r:library:pages:each>...</r:library:pages:each>
|
89
|
+
<r:library:assets:each>...</r:library:assets:each>
|
90
|
+
<r:library:images:each>...</r:library:images:each>
|
91
|
+
<r:library:videos:each>...</r:library:videos:each>
|
92
|
+
|
93
|
+
Display the list of (that kind of) objects associated with the current tag set.
|
94
|
+
|
95
|
+
### Tag assets and asset tags
|
96
|
+
|
97
|
+
All the page tags have asset equivalents:
|
98
|
+
|
99
|
+
<r:tags:assets:each tags="foo, bar">...</r:tags:assets:each>
|
100
|
+
<r:related_assets:each>...</r:related_assets:each>
|
101
|
+
|
102
|
+
and for any `*asset*` tag you can substitute an asset type, so this also works:
|
103
|
+
|
104
|
+
<r:related_images:each>...</r:related_images:each>
|
105
|
+
|
106
|
+
Within these lists you can use all the usual page and asset tags.
|
107
|
+
|
108
|
+
## Author and copyright
|
109
|
+
|
110
|
+
* William Ross, for spanner. will at spanner.org
|
111
|
+
* Copyright 2007-2010 spanner ltd
|
112
|
+
* released under the same terms as Rails and/or Radiant
|
data/Rakefile
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gem|
|
4
|
+
gem.name = "radiant-library-extension"
|
5
|
+
gem.summary = %Q{Library Extension for Radiant CMS}
|
6
|
+
gem.description = %Q{Combines paperclipped and taggable to create a general purpose faceted library}
|
7
|
+
gem.email = "will@spanner.org"
|
8
|
+
gem.homepage = "http://github.com/spanner/radiant-library-extension"
|
9
|
+
gem.authors = ["spanner"]
|
10
|
+
gem.add_dependency "radiant", ">= 0.9.0"
|
11
|
+
gem.add_dependency "radiant-taggable-extension", ">= 1.2.0"
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
puts "Jeweler (or a dependency) not available. This is only required if you plan to package library as a gem."
|
15
|
+
end
|
16
|
+
|
17
|
+
# In rails 1.2, plugins aren't available in the path until they're loaded.
|
18
|
+
# Check to see if the rspec plugin is installed first and require
|
19
|
+
# it if it is. If not, use the gem version.
|
20
|
+
|
21
|
+
# Determine where the RSpec plugin is by loading the boot
|
22
|
+
unless defined? RADIANT_ROOT
|
23
|
+
ENV["RAILS_ENV"] = "test"
|
24
|
+
case
|
25
|
+
when ENV["RADIANT_ENV_FILE"]
|
26
|
+
require File.dirname(ENV["RADIANT_ENV_FILE"]) + "/boot"
|
27
|
+
when File.dirname(__FILE__) =~ %r{vendor/radiant/vendor/extensions}
|
28
|
+
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../")}/config/boot"
|
29
|
+
else
|
30
|
+
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../")}/config/boot"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'rake'
|
35
|
+
require 'rake/rdoctask'
|
36
|
+
require 'rake/testtask'
|
37
|
+
|
38
|
+
rspec_base = File.expand_path(RADIANT_ROOT + '/vendor/plugins/rspec/lib')
|
39
|
+
$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
|
40
|
+
require 'spec/rake/spectask'
|
41
|
+
require 'cucumber'
|
42
|
+
require 'cucumber/rake/task'
|
43
|
+
|
44
|
+
# Cleanup the RADIANT_ROOT constant so specs will load the environment
|
45
|
+
Object.send(:remove_const, :RADIANT_ROOT)
|
46
|
+
|
47
|
+
extension_root = File.expand_path(File.dirname(__FILE__))
|
48
|
+
|
49
|
+
task :default => :spec
|
50
|
+
task :stats => "spec:statsetup"
|
51
|
+
|
52
|
+
desc "Run all specs in spec directory"
|
53
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
54
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
55
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
56
|
+
end
|
57
|
+
|
58
|
+
task :features => 'spec:integration'
|
59
|
+
|
60
|
+
namespace :spec do
|
61
|
+
desc "Run all specs in spec directory with RCov"
|
62
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
63
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
64
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
65
|
+
t.rcov = true
|
66
|
+
t.rcov_opts = ['--exclude', 'spec', '--rails']
|
67
|
+
end
|
68
|
+
|
69
|
+
desc "Print Specdoc for all specs"
|
70
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
71
|
+
t.spec_opts = ["--format", "specdoc", "--dry-run"]
|
72
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
73
|
+
end
|
74
|
+
|
75
|
+
[:models, :controllers, :views, :helpers].each do |sub|
|
76
|
+
desc "Run the specs under spec/#{sub}"
|
77
|
+
Spec::Rake::SpecTask.new(sub) do |t|
|
78
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
79
|
+
t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
desc "Run the Cucumber features"
|
84
|
+
Cucumber::Rake::Task.new(:integration) do |t|
|
85
|
+
t.fork = true
|
86
|
+
t.cucumber_opts = ['--format', (ENV['CUCUMBER_FORMAT'] || 'pretty')]
|
87
|
+
# t.feature_pattern = "#{extension_root}/features/**/*.feature"
|
88
|
+
t.profile = "default"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Setup specs for stats
|
92
|
+
task :statsetup do
|
93
|
+
require 'code_statistics'
|
94
|
+
::STATS_DIRECTORIES << %w(Model\ specs spec/models)
|
95
|
+
::STATS_DIRECTORIES << %w(View\ specs spec/views)
|
96
|
+
::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers)
|
97
|
+
::STATS_DIRECTORIES << %w(Helper\ specs spec/views)
|
98
|
+
::CodeStatistics::TEST_TYPES << "Model specs"
|
99
|
+
::CodeStatistics::TEST_TYPES << "View specs"
|
100
|
+
::CodeStatistics::TEST_TYPES << "Controller specs"
|
101
|
+
::CodeStatistics::TEST_TYPES << "Helper specs"
|
102
|
+
::STATS_DIRECTORIES.delete_if {|a| a[0] =~ /test/}
|
103
|
+
end
|
104
|
+
|
105
|
+
namespace :db do
|
106
|
+
namespace :fixtures do
|
107
|
+
desc "Load fixtures (from spec/fixtures) into the current environment's database. Load specific fixtures using FIXTURES=x,y"
|
108
|
+
task :load => :environment do
|
109
|
+
require 'active_record/fixtures'
|
110
|
+
ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
|
111
|
+
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'spec', 'fixtures', '*.{yml,csv}'))).each do |fixture_file|
|
112
|
+
Fixtures.create_fixtures('spec/fixtures', File.basename(fixture_file, '.*'))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
desc 'Generate documentation for the library extension.'
|
120
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
121
|
+
rdoc.rdoc_dir = 'rdoc'
|
122
|
+
rdoc.title = 'LibraryExtension'
|
123
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
124
|
+
rdoc.rdoc_files.include('README')
|
125
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
126
|
+
end
|
127
|
+
|
128
|
+
# For extensions that are in transition
|
129
|
+
desc 'Test the library extension.'
|
130
|
+
Rake::TestTask.new(:test) do |t|
|
131
|
+
t.libs << 'lib'
|
132
|
+
t.pattern = 'test/**/*_test.rb'
|
133
|
+
t.verbose = true
|
134
|
+
end
|
135
|
+
|
136
|
+
# Load any custom rakefiles for extension
|
137
|
+
Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f }
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class LibraryPage < Page
|
2
|
+
include Library::LibraryTags
|
3
|
+
include WillPaginate::ViewHelpers
|
4
|
+
|
5
|
+
class RedirectRequired < StandardError
|
6
|
+
def initialize(message = nil); super end
|
7
|
+
end
|
8
|
+
|
9
|
+
description %{ Takes tag names in child position or as paramaters so that tagged items can be listed. }
|
10
|
+
|
11
|
+
attr_accessor :requested_tags, :strict_match
|
12
|
+
|
13
|
+
def cache?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_by_url(url, live = true, clean = false)
|
18
|
+
url = clean_url(url) if clean
|
19
|
+
my_url = self.url
|
20
|
+
return false unless url =~ /^#{Regexp.quote(my_url)}(.*)/
|
21
|
+
tags = $1.split('/')
|
22
|
+
if slug_child = children.find_by_slug(tags[0])
|
23
|
+
found = slug_child.find_by_url(url, live, clean)
|
24
|
+
return found if found
|
25
|
+
end
|
26
|
+
remove_tags, add_tags = tags.partition{|t| t.first == '-'}
|
27
|
+
add_request_tags(add_tags)
|
28
|
+
remove_request_tags(remove_tags)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_request_tags(tags=[])
|
33
|
+
if tags.any?
|
34
|
+
tags.collect! { |tag| Tag.find_by_title(Rack::Utils::unescape(tag)) }
|
35
|
+
self.requested_tags = (self.requested_tags + tags.select{|t| !t.nil?}).uniq
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def remove_request_tags(tags=[])
|
40
|
+
if tags.any?
|
41
|
+
tags.collect! { |tag|
|
42
|
+
tag.slice!(0) if tag.first == '-'
|
43
|
+
Tag.find_by_title(Rack::Utils::unescape(tag))
|
44
|
+
}
|
45
|
+
self.requested_tags = (self.requested_tags - tags.select{|t| !t.nil?}).uniq
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def requested_tags
|
50
|
+
@requested_tags ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
# this isn't very pleasing but it's the best way to let the controller know
|
54
|
+
# of our real address once tags have been added and removed.
|
55
|
+
|
56
|
+
def url_with_tags(tags = requested_tags)
|
57
|
+
clean_url( url_without_tags + '/' + tags.uniq.map(&:clean_title).to_param )
|
58
|
+
end
|
59
|
+
alias_method_chain :url, :tags
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
- fields_for :asset, @asset do |fields|
|
2
|
+
#extended-metadata{ :class => "row", :style => "display: none" }
|
3
|
+
%table.fieldset{ :cellpadding => "0", :cellspacing => "0", :border => "0" }
|
4
|
+
%tr
|
5
|
+
%td.label
|
6
|
+
= fields.label :caption
|
7
|
+
%td.field
|
8
|
+
= fields.text_field :caption, :class => 'textbox', :maxlength => 255
|
9
|
+
-if @asset.new_record? || @asset.image?
|
10
|
+
%tr
|
11
|
+
%td.label
|
12
|
+
Display
|
13
|
+
%td.field{:style => 'text-align: left;'}
|
14
|
+
= fields.check_box :furniture
|
15
|
+
= fields.label :furniture, "Site furniture? (not shown in lists and galleries)"
|
16
|
+
= render_region :extended_metadata
|
17
|
+
%p
|
18
|
+
%small
|
19
|
+
%a{ :id => "more-extended-metadata", :href => "#", :onclick => "#{toggle_javascript_for('extended-metadata')}; return false;" } More
|
20
|
+
%a{ :id => "less-extended-metadata", :href => "#", :onclick => "#{toggle_javascript_for('extended-metadata')}; return false;", :style => "display: none" } Less
|
@@ -0,0 +1,31 @@
|
|
1
|
+
- content_for :page_css do
|
2
|
+
:sass
|
3
|
+
div#a_header
|
4
|
+
:width 99%
|
5
|
+
div#a_title
|
6
|
+
:width 70%
|
7
|
+
:float left
|
8
|
+
div#a_keywords
|
9
|
+
:width 28%
|
10
|
+
:float right
|
11
|
+
div#content
|
12
|
+
.form-area
|
13
|
+
.keywords
|
14
|
+
.textbox
|
15
|
+
:font-family Georgia, Palatino, "Times New Roman", Times, serif
|
16
|
+
:font-size 200%
|
17
|
+
:width 100%
|
18
|
+
:color #c00
|
19
|
+
|
20
|
+
- fields_for :asset, @asset do |fields|
|
21
|
+
#a_header
|
22
|
+
#a_title
|
23
|
+
%p.title
|
24
|
+
%label{:for=>"asset_title"}
|
25
|
+
Title
|
26
|
+
= fields.text_field :title, :class => 'textbox', :maxlength => 100
|
27
|
+
#a_keywords
|
28
|
+
%p.keywords
|
29
|
+
%label{:for=>"asset_keywords"}
|
30
|
+
Tags
|
31
|
+
= fields.text_field :keywords, :class => 'textbox'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
- include_stylesheet 'admin/assets'
|
2
|
+
|
3
|
+
- asset_taggings = @tag.taggings.of_a :asset
|
4
|
+
- if asset_taggings.any?
|
5
|
+
%h2
|
6
|
+
Tagged assets
|
7
|
+
%table.index
|
8
|
+
- asset_taggings.each do |tagging|
|
9
|
+
- asset = tagging.tagged
|
10
|
+
- if asset
|
11
|
+
- dom_id = "tagging_#{tagging.id}"
|
12
|
+
%tr{:id => dom_id}
|
13
|
+
%td.asset{:style => 'width: 48px'}
|
14
|
+
= link_to image_tag(asset.thumbnail(:icon)), edit_admin_asset_path(asset), :class => 'icon'
|
15
|
+
%td.asset-title
|
16
|
+
= link_to asset.title, edit_admin_asset_path(asset)
|
17
|
+
%td.actions
|
18
|
+
= link_to_remote image('minus') + ' detach', |
|
19
|
+
:html => { :class => "action", :title => "Detach tag from asset" }, |
|
20
|
+
:url => admin_tagging_url(tagging), :method => :delete, |
|
21
|
+
:after => "Effect.Fade('#{dom_id}', { duration: 0.5 })", |
|
22
|
+
:complete => "Element.remove('#{dom_id}');"
|
data/cucumber.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
default: --format progress features --tags ~@proposed,~@in_progress
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Sets up the Rails environment for Cucumber
|
2
|
+
ENV["RAILS_ENV"] = "test"
|
3
|
+
# Extension root
|
4
|
+
extension_env = File.expand_path(File.dirname(__FILE__) + '/../../../../../config/environment')
|
5
|
+
require extension_env+'.rb'
|
6
|
+
|
7
|
+
Dir.glob(File.join(RADIANT_ROOT, "features", "**", "*.rb")).each {|step| require step}
|
8
|
+
|
9
|
+
Cucumber::Rails::World.class_eval do
|
10
|
+
include Dataset
|
11
|
+
datasets_directory "#{RADIANT_ROOT}/spec/datasets"
|
12
|
+
Dataset::Resolver.default = Dataset::DirectoryResolver.new("#{RADIANT_ROOT}/spec/datasets", File.dirname(__FILE__) + '/../../spec/datasets', File.dirname(__FILE__) + '/../datasets')
|
13
|
+
self.datasets_database_dump_path = "#{Rails.root}/tmp/dataset"
|
14
|
+
|
15
|
+
# dataset :library
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Library
|
2
|
+
module LibraryTag
|
3
|
+
|
4
|
+
def assets
|
5
|
+
Asset.from_tags([self])
|
6
|
+
end
|
7
|
+
|
8
|
+
Asset.known_types.each do |type|
|
9
|
+
define_method type.to_s.pluralize.intern do
|
10
|
+
Asset.send("#{type.to_s.pluralize}".intern).from_tags([self])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|