Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

base

  • Loading branch information...
commit 49a693e34205c602556717f9e1ab90fd98002aa9 0 parents
Jim Gay authored August 02, 2010

Showing 40 changed files with 1,054 additions and 0 deletions. Show diff stats Hide diff stats

  1. 33  README.md
  2. 136  Rakefile
  3. 41  app/controllers/admin/scripts_controller.rb
  4. 41  app/controllers/admin/styles_controller.rb
  5. 2  app/helpers/admin/scripts_helper.rb
  6. 2  app/helpers/admin/styles_helper.rb
  7. 15  app/models/javascript_page.rb
  8. 61  app/models/sheet.rb
  9. 15  app/models/stylesheet_page.rb
  10. 21  app/views/admin/scripts/edit.html.haml
  11. 18  app/views/admin/scripts/index.html.haml
  12. 21  app/views/admin/scripts/new.html.haml
  13. 21  app/views/admin/sheets/_watch_slug.html.haml
  14. 21  app/views/admin/styles/edit.html.haml
  15. 18  app/views/admin/styles/index.html.haml
  16. 21  app/views/admin/styles/new.html.haml
  17. 8  config/locales/en.yml
  18. 6  config/routes.rb
  19. 1  cucumber.yml
  20. 16  features/support/env.rb
  21. 14  features/support/paths.rb
  22. 58  lib/javascript_tags.rb
  23. 61  lib/stylesheet_tags.rb
  24. 55  lib/tasks/sheets_extension_tasks.rake
  25. BIN  public/images/admin/javascript.png
  26. BIN  public/images/admin/stylesheet.png
  27. 50  sheets_extension.rb
  28. 10  spec/controllers/admin/scripts_controller_spec.rb
  29. 10  spec/controllers/admin/styles_controller_spec.rb
  30. 16  spec/datasets/javascripts_dataset.rb
  31. 16  spec/datasets/stylesheets_dataset.rb
  32. 5  spec/helpers/admin/scripts_helper_spec.rb
  33. 5  spec/helpers/admin/styles_helper_spec.rb
  34. 36  spec/lib/javascript_tags_spec.rb
  35. 37  spec/lib/stylesheet_tags_spec.rb
  36. 56  spec/models/javascript_page_spec.rb
  37. 9  spec/models/page_spec.rb
  38. 56  spec/models/stylesheet_page_spec.rb
  39. 6  spec/spec.opts
  40. 36  spec/spec_helper.rb
33  README.md
Source Rendered
... ...
@@ -0,0 +1,33 @@
  1
+# Sheets
  2
+
  3
+Sheets is a way to manage stylesheets and scripts from your existing page tree. With Sheets, you'll create new types of pages: JavascriptPage and StylesheetPage.
  4
+
  5
+Pages of these types are Sheets.
  6
+
  7
+## Features
  8
+
  9
+The basic features:
  10
+
  11
+* provide a separate interface for editing CSS and Javascript content
  12
+* set a longer expire time for Sheet content with `sheet_cache_timeout` (similar to the typical `cache_timeout` set for pages)
  13
+* hide the JavascriptPage and StylesheetPage nodes from the standard Page index view
  14
+* provide Radius tags to output content as it is, as a link to content, and as an HTML element containing the content
  15
+* append a timestamp to sheet content URLs so that the cache need not be cleared with the same frequency for updates to typical pages
  16
+
  17
+## Differences with other solutions and the benefit of using Sheets
  18
+
  19
+Some standard site templates provided by Radiant include stylesheet content in a typical page. This requires a custom layout merely to set the content type, and displays content unintended for editing by typical users. This makes for a cluttered page index view and cluttered layout index view.
  20
+
  21
+A popular solution to these problems is the [SNS extension](http://ext.radiantcms.org/extensions/53-sns). SNS separates the editing interfaces for these types of content, but the problem is that it makes too much of a separation. With SNS, stylesheet and javascript content is stored in a separate table and is generated by models completely unrelated to the Page model. This means that other extensions are *required* to do extra work to provide features and Radius tags to SNS content, and as a result many extensions do not do this work. This situation makes for a confusing introduction to Radiant for new users and developers alike.
  22
+
  23
+You can get around this SNS limitation by using the [SNS Page Hook extension](http://ext.radiantcms.org/extensions/232-sns-page-hook) but even this solution will lead to unpredictable results because it merely copies modules from the Page model to the TextAsset model and tricks the included methods into believing they are operating on a Page. But this is not a good long-term solution.
  24
+
  25
+Sheets keeps this content in the same location as all other content: Pages. Doing it this way allows any chosen URL or Page slug to be protected properly with newly created pages. Even more importantly, this allows user and developer expectations about the behavior of Radius tags to make sense. With Sheets, extensions which add Radius tags to the Page model add them to stylesheets and javascripts too; there is no extra work to be done.
  26
+
  27
+Not only does it do all this, but the standard Page model provides a `headers` method which Sheets overrides to alter the mime-type for the returned content. Sheets uses the built-in ways to serve content, rather than generating yet another way to serve content like SNS.
  28
+
  29
+## Thanks
  30
+
  31
+Special thanks goes to Chris Parrish for creating the SNS extension in the first place. And thanks to Dane Harrigan for some early bug checking.
  32
+
  33
+Built by [Saturn Flyer](http://www.saturnflyer.com)
136  Rakefile
... ...
@@ -0,0 +1,136 @@
  1
+begin
  2
+  require 'jeweler'
  3
+  Jeweler::Tasks.new do |gem|
  4
+    gem.name = "radiant-sheets-extension"
  5
+    gem.summary = %Q{Sheets Extension for Radiant CMS}
  6
+    gem.description = %Q{Describe your extension here}
  7
+    gem.email = "your email"
  8
+    gem.homepage = "http://yourwebsite.com/sheets"
  9
+    gem.authors = ["Your Name"]
  10
+    # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
  11
+  end
  12
+rescue LoadError
  13
+  puts "Jeweler (or a dependency) not available. This is only required if you plan to package sheets as a gem."
  14
+end
  15
+
  16
+# In rails 1.2, plugins aren't available in the path until they're loaded.
  17
+# Check to see if the rspec plugin is installed first and require
  18
+# it if it is.  If not, use the gem version.
  19
+
  20
+# Determine where the RSpec plugin is by loading the boot
  21
+unless defined? RADIANT_ROOT
  22
+  ENV["RAILS_ENV"] = "test"
  23
+  case
  24
+  when ENV["RADIANT_ENV_FILE"]
  25
+    require File.dirname(ENV["RADIANT_ENV_FILE"]) + "/boot"
  26
+  when File.dirname(__FILE__) =~ %r{vendor/radiant/vendor/extensions}
  27
+    require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../")}/config/boot"
  28
+  else
  29
+    require "#{File.expand_path(File.dirname(__FILE__) + "/../../../")}/config/boot"
  30
+  end
  31
+end
  32
+
  33
+require 'rake'
  34
+require 'rake/rdoctask'
  35
+require 'rake/testtask'
  36
+
  37
+rspec_base = File.expand_path(RADIANT_ROOT + '/vendor/plugins/rspec/lib')
  38
+$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
  39
+require 'spec/rake/spectask'
  40
+require 'cucumber'
  41
+require 'cucumber/rake/task'
  42
+
  43
+# Cleanup the RADIANT_ROOT constant so specs will load the environment
  44
+Object.send(:remove_const, :RADIANT_ROOT)
  45
+
  46
+extension_root = File.expand_path(File.dirname(__FILE__))
  47
+
  48
+task :default => :spec
  49
+task :stats => "spec:statsetup"
  50
+
  51
+desc "Run all specs in spec directory"
  52
+Spec::Rake::SpecTask.new(:spec) do |t|
  53
+  t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
  54
+  t.spec_files = FileList['spec/**/*_spec.rb']
  55
+end
  56
+
  57
+task :features => 'spec:integration'
  58
+
  59
+namespace :spec do
  60
+  desc "Run all specs in spec directory with RCov"
  61
+  Spec::Rake::SpecTask.new(:rcov) do |t|
  62
+    t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
  63
+    t.spec_files = FileList['spec/**/*_spec.rb']
  64
+    t.rcov = true
  65
+    t.rcov_opts = ['--exclude', 'spec', '--rails']
  66
+  end
  67
+  
  68
+  desc "Print Specdoc for all specs"
  69
+  Spec::Rake::SpecTask.new(:doc) do |t|
  70
+    t.spec_opts = ["--format", "specdoc", "--dry-run"]
  71
+    t.spec_files = FileList['spec/**/*_spec.rb']
  72
+  end
  73
+
  74
+  [:models, :controllers, :views, :helpers].each do |sub|
  75
+    desc "Run the specs under spec/#{sub}"
  76
+    Spec::Rake::SpecTask.new(sub) do |t|
  77
+      t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
  78
+      t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"]
  79
+    end
  80
+  end
  81
+  
  82
+  desc "Run the Cucumber features"
  83
+  Cucumber::Rake::Task.new(:integration) do |t|
  84
+    t.fork = true
  85
+    t.cucumber_opts = ['--format', (ENV['CUCUMBER_FORMAT'] || 'pretty')]
  86
+    # t.feature_pattern = "#{extension_root}/features/**/*.feature"
  87
+    t.profile = "default"
  88
+  end
  89
+
  90
+  # Setup specs for stats
  91
+  task :statsetup do
  92
+    require 'code_statistics'
  93
+    ::STATS_DIRECTORIES << %w(Model\ specs spec/models)
  94
+    ::STATS_DIRECTORIES << %w(View\ specs spec/views)
  95
+    ::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers)
  96
+    ::STATS_DIRECTORIES << %w(Helper\ specs spec/views)
  97
+    ::CodeStatistics::TEST_TYPES << "Model specs"
  98
+    ::CodeStatistics::TEST_TYPES << "View specs"
  99
+    ::CodeStatistics::TEST_TYPES << "Controller specs"
  100
+    ::CodeStatistics::TEST_TYPES << "Helper specs"
  101
+    ::STATS_DIRECTORIES.delete_if {|a| a[0] =~ /test/}
  102
+  end
  103
+
  104
+  namespace :db do
  105
+    namespace :fixtures do
  106
+      desc "Load fixtures (from spec/fixtures) into the current environment's database.  Load specific fixtures using FIXTURES=x,y"
  107
+      task :load => :environment do
  108
+        require 'active_record/fixtures'
  109
+        ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
  110
+        (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'spec', 'fixtures', '*.{yml,csv}'))).each do |fixture_file|
  111
+          Fixtures.create_fixtures('spec/fixtures', File.basename(fixture_file, '.*'))
  112
+        end
  113
+      end
  114
+    end
  115
+  end
  116
+end
  117
+
  118
+desc 'Generate documentation for the sheets extension.'
  119
+Rake::RDocTask.new(:rdoc) do |rdoc|
  120
+  rdoc.rdoc_dir = 'rdoc'
  121
+  rdoc.title    = 'SheetsExtension'
  122
+  rdoc.options << '--line-numbers' << '--inline-source'
  123
+  rdoc.rdoc_files.include('README')
  124
+  rdoc.rdoc_files.include('lib/**/*.rb')
  125
+end
  126
+
  127
+# For extensions that are in transition
  128
+desc 'Test the sheets extension.'
  129
+Rake::TestTask.new(:test) do |t|
  130
+  t.libs << 'lib'
  131
+  t.pattern = 'test/**/*_test.rb'
  132
+  t.verbose = true
  133
+end
  134
+
  135
+# Load any custom rakefiles for extension
  136
+Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f }
41  app/controllers/admin/scripts_controller.rb
... ...
@@ -0,0 +1,41 @@
  1
+class Admin::ScriptsController < Admin::ResourceController
  2
+  model_class JavascriptPage
  3
+  paginate_models
  4
+  only_allow_access_to :index, :new, :edit, :create, :update,
  5
+    :when => [:designer, :admin],
  6
+    :denied_url => { :controller => 'pages', :action => 'index' },
  7
+    :denied_message => 'You must have developer or administrator privileges to edit javascripts.'
  8
+  
  9
+  prepend_before_filter :find_root
  10
+  prepend_before_filter :create_root, :only => :new
  11
+  
  12
+  def new
  13
+    self.model = model_class.new_with_defaults
  14
+    response_for :new
  15
+  end
  16
+  
  17
+  def create
  18
+    model.parent_id = @root.id
  19
+    model.update_attributes!(params[model_symbol])
  20
+    response_for :create
  21
+  end
  22
+  
  23
+  private
  24
+  
  25
+  def load_models
  26
+    @root.try(:children) || []
  27
+  end
  28
+  
  29
+  def find_root
  30
+    @root = JavascriptPage.root
  31
+  end
  32
+  
  33
+  def create_root
  34
+    unless @root
  35
+      j = JavascriptPage.new_with_defaults
  36
+      j.parent_id = Page.find_by_slug('/').try(:id)
  37
+      j.slug = 'js'
  38
+      j.save
  39
+    end
  40
+  end
  41
+end
41  app/controllers/admin/styles_controller.rb
... ...
@@ -0,0 +1,41 @@
  1
+class Admin::StylesController < Admin::ResourceController
  2
+  model_class StylesheetPage
  3
+  paginate_models
  4
+  only_allow_access_to :index, :new, :edit, :create, :update,
  5
+    :when => [:designer, :admin],
  6
+    :denied_url => { :controller => 'pages', :action => 'index' },
  7
+    :denied_message => 'You must have developer or administrator privileges to edit stylesheets.'
  8
+  
  9
+  prepend_before_filter :find_root
  10
+  prepend_before_filter :create_root, :only => :new
  11
+  
  12
+  def new
  13
+    self.model = model_class.new_with_defaults
  14
+    response_for :new
  15
+  end
  16
+  
  17
+  def create
  18
+    model.parent_id = @root.id
  19
+    model.update_attributes!(params[model_symbol])
  20
+    response_for :create
  21
+  end
  22
+  
  23
+  private
  24
+  
  25
+  def load_models
  26
+    @root.try(:children) || []
  27
+  end
  28
+  
  29
+  def find_root
  30
+    @root = StylesheetPage.root
  31
+  end
  32
+  
  33
+  def create_root
  34
+    unless @root
  35
+      j = StylesheetPage.new_with_defaults
  36
+      j.parent_id = Page.find_by_slug('/').try(:id)
  37
+      j.slug = 'css'
  38
+      j.save
  39
+    end
  40
+  end
  41
+end
2  app/helpers/admin/scripts_helper.rb
... ...
@@ -0,0 +1,2 @@
  1
+module Admin::ScriptsHelper
  2
+end
2  app/helpers/admin/styles_helper.rb
... ...
@@ -0,0 +1,2 @@
  1
+module Admin::StylesHelper
  2
+end
15  app/models/javascript_page.rb
... ...
@@ -0,0 +1,15 @@
  1
+class JavascriptPage < Page
  2
+  include Sheet::Instance
  3
+  
  4
+  def headers
  5
+    {'Content-Type' => 'text/javascript'}
  6
+  end
  7
+      
  8
+  def self.new_with_defaults(config = Radiant::Config)
  9
+    page = JavascriptPage.new
  10
+    page.parts.concat(self.default_page_parts)
  11
+    page.parent_id = JavascriptPage.root.try(:id)
  12
+    page.status_id = Status[:published].id
  13
+    page
  14
+  end
  15
+end
61  app/models/sheet.rb
... ...
@@ -0,0 +1,61 @@
  1
+module Sheet
  2
+  module Instance
  3
+    def self.included(base)
  4
+      base.class_eval {
  5
+        before_validation :set_title
  6
+        before_validation :set_breadcrumb
  7
+        
  8
+        def self.root
  9
+          root = self.first(:order => 'id')
  10
+        end
  11
+
  12
+        def self.default_page_parts
  13
+          PagePart.new(:name => 'body')
  14
+        end
  15
+      }
  16
+    end
  17
+    
  18
+    def cache?
  19
+      true
  20
+    end
  21
+  
  22
+    # stub to check type
  23
+    def sheet?
  24
+      true
  25
+    end
  26
+  
  27
+    # so that it is ignored when finding children in radius tags
  28
+    def virtual?
  29
+      true
  30
+    end
  31
+  
  32
+    def layout
  33
+      nil
  34
+    end
  35
+    
  36
+    def find_by_url(url, live = true, clean = true)
  37
+      url = clean_url(url) if clean
  38
+      my_url = self.url
  39
+      if (my_url == url) && (not live or published?)
  40
+        self
  41
+      elsif (url =~ /^#{Regexp.quote(my_url)}([^\/]*)/)
  42
+        slug_child = children.find_by_slug($1)
  43
+        if slug_child
  44
+          return slug_child
  45
+        else
  46
+          super
  47
+        end
  48
+      end
  49
+    end
  50
+  
  51
+    private
  52
+  
  53
+    def set_title
  54
+      self.title = self.slug
  55
+    end
  56
+  
  57
+    def set_breadcrumb
  58
+      self.breadcrumb = self.slug
  59
+    end
  60
+  end
  61
+end
15  app/models/stylesheet_page.rb
... ...
@@ -0,0 +1,15 @@
  1
+class StylesheetPage < Page
  2
+  include Sheet::Instance
  3
+    
  4
+  def headers
  5
+    {'Content-Type' => 'text/css'}
  6
+  end
  7
+      
  8
+  def self.new_with_defaults(config = Radiant::Config)
  9
+    page = StylesheetPage.new
  10
+    page.parts.concat(self.default_page_parts)
  11
+    page.parent_id = StylesheetPage.root.try(:id)
  12
+    page.status_id = Status[:published].id
  13
+    page
  14
+  end
  15
+end
21  app/views/admin/scripts/edit.html.haml
... ...
@@ -0,0 +1,21 @@
  1
+%h1= model.new_record? ? t('new_javascript') : t('edit_javascript')
  2
+- form_for [:admin, model], 
  3
+  :url => (model.new_record? ? admin_scripts_url : admin_script_url(model)) do |f|
  4
+  %p.title
  5
+    = label :sheet_page, :slug, 'Slug'
  6
+    = f.text_field :slug, :class => 'textbox', :maxlength => 255, :id => 'sheet_page_slug'
  7
+    %span.hint= %{#{t('path')}: #{@root.url}<span id="this_slug">#{model.slug}</span>}
  8
+  - f.fields_for :parts do |p|
  9
+    %p
  10
+      %span.reference_links
  11
+        %span{:id => "tag_reference_link_body"}
  12
+          = link_to_function t('available_tags'), "loadTagReference('body');"
  13
+    %div
  14
+      = p.hidden_field :name, :value => 'body' if model.new_record?
  15
+      = p.text_area :content, :class => 'textarea large', :style => 'width: 100%'
  16
+  %p.buttons{:style=>"clear: left"}
  17
+    = save_model_button(model)
  18
+    = save_model_and_continue_editing_button(model)
  19
+    = t('or')
  20
+    = link_to t('cancel'), admin_scripts_url
  21
+= render 'admin/sheets/watch_slug'
18  app/views/admin/scripts/index.html.haml
... ...
@@ -0,0 +1,18 @@
  1
+.outset
  2
+  %table.index
  3
+    %thead
  4
+      %tr
  5
+        %th Javascript
  6
+        %th Modify
  7
+    %tbody
  8
+      - models.each do |javascript|
  9
+        %tr
  10
+          %td.name
  11
+            = image('javascript') 
  12
+            = link_to javascript.title, edit_admin_script_path(javascript)
  13
+          %td.actions= link_to(image('minus') + ' ' + t('remove'), admin_script_url(javascript), :class => "action", :method => :delete, :confirm => "Is it OK to delete #{javascript.slug}?")
  14
+
  15
+#actions
  16
+  = pagination_for(models)
  17
+  %ul
  18
+    %li= link_to image('plus') + " " + t("new_javascript"), new_admin_script_path
21  app/views/admin/scripts/new.html.haml
... ...
@@ -0,0 +1,21 @@
  1
+%h1= model.new_record? ? t('new_javascript') : t('edit_javascript')
  2
+- form_for [:admin, model], 
  3
+  :url => (model.new_record? ? admin_scripts_url : admin_script_url(model)) do |f|
  4
+  %p.title
  5
+    = label :sheet_page, :slug, 'Slug'
  6
+    = f.text_field :slug, :class => 'textbox', :maxlength => 255, :id => 'sheet_page_slug'
  7
+    %span.hint= %{#{t('path')}: #{@root.url}<span id="this_slug">#{model.slug}</span>}
  8
+  - f.fields_for :parts do |p|
  9
+    %p
  10
+      %span.reference_links
  11
+        %span{:id => "tag_reference_link_body"}
  12
+          = link_to_function t('available_tags'), "loadTagReference('body');"
  13
+    %div
  14
+      = p.hidden_field :name, :value => 'body' if model.new_record?
  15
+      = p.text_area :content, :class => 'textarea large', :style => 'width: 100%'
  16
+  %p.buttons{:style=>"clear: left"}
  17
+    = save_model_button(model)
  18
+    = save_model_and_continue_editing_button(model)
  19
+    = t('or')
  20
+    = link_to t('cancel'), admin_scripts_url
  21
+= render 'admin/sheets/watch_slug'
21  app/views/admin/sheets/_watch_slug.html.haml
... ...
@@ -0,0 +1,21 @@
  1
+- content_for :page_scripts do 
  2
+  :plain
  3
+    Event.addBehavior({
  4
+      'input#sheet_page_slug': function(){
  5
+        var slug = this;
  6
+        var this_slug = $('this_slug');
  7
+        new Form.Element.Observer(slug, 0.15, function() {
  8
+          this_slug.innerHTML = slug.value.toSlug(true);
  9
+        });
  10
+      }
  11
+    });
  12
+    var tagReferenceWindows = {};
  13
+    function loadTagReference(part) {
  14
+      var pageType = '#{model.class_name}';
  15
+      if (!tagReferenceWindows[pageType])
  16
+        tagReferenceWindows[pageType] = new Popup.AjaxWindow("#{admin_reference_path('tags')}?class_name=" + encodeURIComponent(pageType), {reload: false});
  17
+      var window = tagReferenceWindows[pageType];
  18
+      $('tag_reference_link_' + part).highlight();
  19
+      window.show();
  20
+      return false;
  21
+    }
21  app/views/admin/styles/edit.html.haml
... ...
@@ -0,0 +1,21 @@
  1
+%h1= model.new_record? ? t('new_stylesheet') : t('edit_stylesheet')
  2
+- form_for [:admin, model], 
  3
+  :url => (model.new_record? ? admin_styles_url() : admin_style_url(model)) do |f|
  4
+  %p.title
  5
+    = label :sheet_page, :slug, 'Slug'
  6
+    = f.text_field :slug, :class => 'textbox', :maxlength => 255, :id => "sheet_page_slug"
  7
+    %span.hint= %{Path: #{@root.url}<span id="this_slug">#{model.slug}</span>}
  8
+  - f.fields_for :parts do |p|
  9
+    %p
  10
+      %span.reference_links
  11
+        %span{:id => "tag_reference_link_body"}
  12
+          = link_to_function t('available_tags'), "loadTagReference('body');"
  13
+    %div
  14
+      = p.hidden_field :name, :value => 'body' if model.new_record?
  15
+      = p.text_area :content, :class => 'textarea large', :style => 'width: 100%'
  16
+  %p.buttons{:style=>"clear: left"}
  17
+    = save_model_button(model)
  18
+    = save_model_and_continue_editing_button(model)
  19
+    = t('or')
  20
+    = link_to t('cancel'), admin_styles_url
  21
+= render 'admin/sheets/watch_slug'
18  app/views/admin/styles/index.html.haml
... ...
@@ -0,0 +1,18 @@
  1
+.outset
  2
+  %table.index
  3
+    %thead
  4
+      %tr
  5
+        %th Stylesheet
  6
+        %th Modify
  7
+    %tbody
  8
+      - models.each do |stylesheet|
  9
+        %tr
  10
+          %td.name
  11
+            = image('stylesheet') 
  12
+            = link_to stylesheet.title, edit_admin_style_path(stylesheet)
  13
+          %td.actions= link_to(image('minus') + ' ' + t('remove'), admin_style_url(stylesheet), :class => "action", :method => :delete, :confirm => "Is it OK to delete #{stylesheet.slug}?")
  14
+
  15
+#actions
  16
+  = pagination_for(models)
  17
+  %ul
  18
+    %li= link_to image('plus') + " " + t("new_stylesheet"), new_admin_style_path
21  app/views/admin/styles/new.html.haml
... ...
@@ -0,0 +1,21 @@
  1
+%h1= model.new_record? ? t('new_stylesheet') : t('edit_stylesheet')
  2
+- form_for [:admin, model], 
  3
+  :url => (model.new_record? ? admin_styles_url() : admin_style_url(model)) do |f|
  4
+  %p.title
  5
+    = label :sheet_page, :slug, 'Slug'
  6
+    = f.text_field :slug, :class => 'textbox', :maxlength => 255, :id => "sheet_page_slug"
  7
+    %span.hint= %{Path: #{@root.url}<span id="this_slug">#{model.slug}</span>}
  8
+  - f.fields_for :parts do |p|
  9
+    %p
  10
+      %span.reference_links
  11
+        %span{:id => "tag_reference_link_body"}
  12
+          = link_to_function t('available_tags'), "loadTagReference('body');"
  13
+    %div
  14
+      = p.hidden_field :name, :value => 'body' if model.new_record?
  15
+      = p.text_area :content, :class => 'textarea large', :style => 'width: 100%'
  16
+  %p.buttons{:style=>"clear: left"}
  17
+    = save_model_button(model)
  18
+    = save_model_and_continue_editing_button(model)
  19
+    = t('or')
  20
+    = link_to t('cancel'), admin_styles_url
  21
+= render 'admin/sheets/watch_slug'
8  config/locales/en.yml
... ...
@@ -0,0 +1,8 @@
  1
+---
  2
+en: 
  3
+  edit_javascript: Edit Javascript
  4
+  edit_stylesheet: Edit Stylesheet
  5
+  new_javascript: New Javascript
  6
+  new_stylesheet: New Stylesheet
  7
+  path: Path
  8
+  sheets: Sheets
6  config/routes.rb
... ...
@@ -0,0 +1,6 @@
  1
+ActionController::Routing::Routes.draw do |map|
  2
+  map.namespace :admin, :member => { :remove => :get } do |admin|
  3
+    admin.resources :styles
  4
+    admin.resources :scripts
  5
+  end
  6
+end
1  cucumber.yml
... ...
@@ -0,0 +1 @@
  1
+default: --format progress features --tags ~@proposed,~@in_progress
16  features/support/env.rb
... ...
@@ -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 :pages, :stylesheets, :javascripts
  16
+end
14  features/support/paths.rb
... ...
@@ -0,0 +1,14 @@
  1
+def path_to(page_name)
  2
+  case page_name
  3
+  
  4
+  when /the homepage/i
  5
+    root_path
  6
+  
  7
+  when /login/i
  8
+    login_path
  9
+  # Add more page name => path mappings here
  10
+  
  11
+  else
  12
+    raise "Can't find mapping from \"#{page_name}\" to a path."
  13
+  end
  14
+end
58  lib/javascript_tags.rb
... ...
@@ -0,0 +1,58 @@
  1
+module JavascriptTags
  2
+  include Radiant::Taggable
  3
+  class TagError < StandardError; end
  4
+  
  5
+  desc %{
  6
+Renders the content from or a reference to the javascript specified in the @slug@ 
  7
+attribute. Additionally, the @as@ attribute can be used to make the tag render 
  8
+as one of the following:
  9
+
  10
+* with no @as@ value the javascript's content is rendered by default.
  11
+* @inline@ - wraps the javascript's content in an (X)HTML @<script>@ element.
  12
+* @url@ - the full path to the javascript.
  13
+* @link@ - embeds the url in an (X)HTML @<script>@ element (creating a link to the external javascript).
  14
+
  15
+*Additional Options:*
  16
+When rendering @as="inline"@ or @as="link"@, the (X)HTML @type@ attribute
  17
+is automatically be set to the default javascript content-type.
  18
+You can overrride this attribute or add additional ones by passing extra
  19
+attributes to the @<r:javascript>@ tag.
  20
+      
  21
+*Usage:*
  22
+      
  23
+<pre><code><r:javascript slug="site.css" as="inline" 
  24
+  type="text/custom" id="my_id" />
  25
+<r:javascript slug="other.js" as="link" /></code></pre>
  26
+
  27
+The above example will produce the following:
  28
+      
  29
+<pre><code>        <script type="text/custom" id="my_id">
  30
+//<![CDATA[
  31
+  var your_script = 'this content';
  32
+//]]>
  33
+</script>
  34
+<script type="text/javascript" src="/js/other.js"></script></code></pre>
  35
+  }
  36
+  tag 'javascript' do |tag|
  37
+    slug = (tag.attr['slug'] || tag.attr['name'])
  38
+    raise TagError.new("`javascript' tag must contain a `slug' attribute.") unless slug
  39
+    if (javascript = JavascriptPage.find_by_slug(slug))
  40
+      mime_type = tag.attr['type'] || javascript.headers['Content-Type']
  41
+      url = javascript.url.sub(/\/$/,'') + '?' + javascript.updated_at.to_i.to_s
  42
+      optional_attributes = tag.attr.except('slug', 'name', 'as', 'type').inject('') { |s, (k, v)| s << %{#{k}="#{v}" } }.strip
  43
+      optional_attributes = " #{optional_attributes}" unless optional_attributes.empty?
  44
+      case tag.attr['as']
  45
+      when 'url'
  46
+        url
  47
+      when 'inline'
  48
+        %{<script type="#{mime_type}"#{optional_attributes}>\n//<![CDATA[\n#{javascript.part('body').content}\n//]]>\n</script>}
  49
+      when 'link'
  50
+        %{<script type="#{mime_type}" src="#{url}"#{optional_attributes}></script>}
  51
+      else
  52
+        javascript.part('body').content
  53
+      end
  54
+    else
  55
+      raise TagError.new("javascript not found")
  56
+    end
  57
+  end
  58
+end
61  lib/stylesheet_tags.rb
... ...
@@ -0,0 +1,61 @@
  1
+module StylesheetTags
  2
+  include Radiant::Taggable
  3
+  class TagError < StandardError; end
  4
+  
  5
+  desc %{
  6
+Renders the content from or a reference to the stylesheet specified in the @slug@ 
  7
+attribute. Additionally, the @as@ attribute can be used to make the tag render 
  8
+as one of the following:
  9
+
  10
+* with no @as@ value the stylesheet's content is rendered by default.
  11
+* @inline@ - wraps the stylesheet's content in an (X)HTML @<style>@ element.
  12
+* @url@ - the full path to the stylesheet.
  13
+* @link@ - embeds the url in an (X)HTML @<link>@ element (creating a link to the external stylesheet).
  14
+
  15
+*Additional Options:*
  16
+When rendering @as="inline"@ or @as="link"@, the (X)HTML @type@ attribute
  17
+is automatically be set to the default stylesheet content-type.
  18
+You can overrride this attribute or add additional ones by passing extra
  19
+attributes to the @<r:stylesheet>@ tag.
  20
+      
  21
+*Usage:*
  22
+      
  23
+<pre><code><r:stylesheet slug="site.css" as="inline" 
  24
+  type="text/custom" id="my_id" />
  25
+<r:stylesheet slug="other.css" as="link" 
  26
+  rel="alternate stylesheet" /></code></pre>
  27
+
  28
+The above example will produce the following:
  29
+      
  30
+<pre><code>        <style type="text/custom" id="my_id">
  31
+/*<![CDATA[*/
  32
+  .your_stylesheet { content: 'here' }
  33
+/*]]>*/
  34
+</style>
  35
+<link rel="alternate stylesheet" type="text/css" 
  36
+  href="/css/other.css" /></code></pre>
  37
+  }
  38
+  tag 'stylesheet' do |tag|
  39
+    slug = (tag.attr['slug'] || tag.attr['name'])
  40
+    raise TagError.new("`stylesheet' tag must contain a `slug' attribute.") unless slug
  41
+    if (stylesheet = StylesheetPage.find_by_slug(slug))
  42
+      mime_type = tag.attr['type'] || stylesheet.headers['Content-Type']
  43
+      url = stylesheet.url.sub(/\/$/,'') + '?' + stylesheet.updated_at.to_i.to_s
  44
+      rel = tag.attr.delete('rel') || 'stylesheet'
  45
+      optional_attributes = tag.attr.except('slug', 'name', 'as', 'type').inject('') { |s, (k, v)| s << %{#{k}="#{v}" } }.strip
  46
+      optional_attributes = " #{optional_attributes}" unless optional_attributes.empty?
  47
+      case tag.attr['as']
  48
+      when 'url'
  49
+        url
  50
+      when 'inline'
  51
+        %{<style type="#{mime_type}"#{optional_attributes}>\n/*<![CDATA[*/\n#{stylesheet.part('body').content}\n/*]]>*/\n</style>}
  52
+      when 'link'
  53
+        %{<link rel="#{rel}" type="#{mime_type}" href="#{url}"#{optional_attributes} />}
  54
+      else
  55
+        stylesheet.part('body').content
  56
+      end
  57
+    else
  58
+      raise TagError.new("stylesheet not found")
  59
+    end
  60
+  end
  61
+end
55  lib/tasks/sheets_extension_tasks.rake
... ...
@@ -0,0 +1,55 @@
  1
+namespace :radiant do
  2
+  namespace :extensions do
  3
+    namespace :sheets do
  4
+      
  5
+      desc "Runs the migration of the Sheets extension"
  6
+      task :migrate => :environment do
  7
+        require 'radiant/extension_migrator'
  8
+        if ENV["VERSION"]
  9
+          SheetsExtension.migrator.migrate(ENV["VERSION"].to_i)
  10
+          Rake::Task['db:schema:dump'].invoke
  11
+        else
  12
+          SheetsExtension.migrator.migrate
  13
+          Rake::Task['db:schema:dump'].invoke
  14
+        end
  15
+      end
  16
+      
  17
+      desc "Copies public assets of the Sheets to the instance public/ directory."
  18
+      task :update => :environment do
  19
+        is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
  20
+        puts "Copying assets from SheetsExtension"
  21
+        Dir[SheetsExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
  22
+          path = file.sub(SheetsExtension.root, '')
  23
+          directory = File.dirname(path)
  24
+          mkdir_p RAILS_ROOT + directory, :verbose => false
  25
+          cp file, RAILS_ROOT + path, :verbose => false
  26
+        end
  27
+        unless SheetsExtension.root.starts_with? RAILS_ROOT # don't need to copy vendored tasks
  28
+          puts "Copying rake tasks from SheetsExtension"
  29
+          local_tasks_path = File.join(RAILS_ROOT, %w(lib tasks))
  30
+          mkdir_p local_tasks_path, :verbose => false
  31
+          Dir[File.join SheetsExtension.root, %w(lib tasks *.rake)].each do |file|
  32
+            cp file, local_tasks_path, :verbose => false
  33
+          end
  34
+        end
  35
+      end  
  36
+      
  37
+      desc "Syncs all available translations for this ext to the English ext master"
  38
+      task :sync => :environment do
  39
+        # The main translation root, basically where English is kept
  40
+        language_root = SheetsExtension.root + "/config/locales"
  41
+        words = TranslationSupport.get_translation_keys(language_root)
  42
+        
  43
+        Dir["#{language_root}/*.yml"].each do |filename|
  44
+          next if filename.match('_available_tags')
  45
+          basename = File.basename(filename, '.yml')
  46
+          puts "Syncing #{basename}"
  47
+          (comments, other) = TranslationSupport.read_file(filename, basename)
  48
+          words.each { |k,v| other[k] ||= words[k] }  # Initializing hash variable as empty if it does not exist
  49
+          other.delete_if { |k,v| !words[k] }         # Remove if not defined in en.yml
  50
+          TranslationSupport.write_file(filename, basename, comments, other)
  51
+        end
  52
+      end
  53
+    end
  54
+  end
  55
+end
BIN  public/images/admin/javascript.png
BIN  public/images/admin/stylesheet.png
50  sheets_extension.rb
... ...
@@ -0,0 +1,50 @@
  1
+# Uncomment this if you reference any of your controllers in activate
  2
+require_dependency 'application_controller'
  3
+
  4
+class SheetsExtension < Radiant::Extension
  5
+  version "1.0"
  6
+  description "Manage CSS and Javascript content in Radiant CMS as Sheets, a subset of Pages"
  7
+  url "http://github.com/radiant/radiant"
  8
+  
  9
+  def activate
  10
+    tab 'Design' do
  11
+      add_item "Stylesheets", "/admin/styles"
  12
+      add_item "Javascripts", "/admin/scripts"
  13
+    end
  14
+    
  15
+    Admin::NodeHelper.module_eval do
  16
+      def render_node_with_sheets(page, locals = {})
  17
+        unless page.sheet?
  18
+          render_node_without_sheets(page, locals)
  19
+        end
  20
+      end
  21
+      alias_method_chain :render_node, :sheets
  22
+    end
  23
+    
  24
+    Page.class_eval do
  25
+      def sheet?
  26
+        false
  27
+      end
  28
+      
  29
+      include JavascriptTags
  30
+      include StylesheetTags
  31
+    end
  32
+    
  33
+    SiteController.class_eval do
  34
+      cattr_writer :sheet_cache_timeout
  35
+  
  36
+      def self.sheet_cache_timeout
  37
+        @@sheet_cache_timeout ||= 30.days
  38
+      end
  39
+      
  40
+      def set_cache_control_with_sheets
  41
+        if @page.sheet?
  42
+          expires_in self.class.sheet_cache_timeout, :public => true, :private => false
  43
+        else
  44
+          set_cache_control_without_sheets
  45
+        end
  46
+      end
  47
+      alias_method_chain :set_cache_control, :sheets
  48
+    end
  49
+  end
  50
+end
10  spec/controllers/admin/scripts_controller_spec.rb
... ...
@@ -0,0 +1,10 @@
  1
+require File.dirname(__FILE__) + '/../../spec_helper'
  2
+
  3
+describe Admin::ScriptsController do
  4
+
  5
+  #Delete this example and add some real ones
  6
+  it "should use Admin::ScriptsController" do
  7
+    controller.should be_an_instance_of(Admin::ScriptsController)
  8
+  end
  9
+
  10
+end
10  spec/controllers/admin/styles_controller_spec.rb
... ...
@@ -0,0 +1,10 @@
  1
+require File.dirname(__FILE__) + '/../../spec_helper'
  2
+
  3
+describe Admin::StylesController do
  4
+
  5
+  #Delete this example and add some real ones
  6
+  it "should use Admin::StylesController" do
  7
+    controller.should be_an_instance_of(Admin::StylesController)
  8
+  end
  9
+
  10
+end
16  spec/datasets/javascripts_dataset.rb
... ...
@@ -0,0 +1,16 @@
  1
+class JavascriptsDataset < Dataset::Base
  2
+  uses :pages
  3
+  
  4
+  def load
  5
+    create_javascript "js" do
  6
+      create_javascript "site.js"
  7
+    end
  8
+  end
  9
+  
  10
+  helpers do
  11
+    def create_javascript(name, attributes={}, &block)    
  12
+      attributes = page_params(attributes.reverse_merge(:title => name, :slug => name, :class_name => 'JavascriptPage'))
  13
+      create_page(name, attributes, &block)
  14
+    end
  15
+  end
  16
+end
16  spec/datasets/stylesheets_dataset.rb
... ...
@@ -0,0 +1,16 @@
  1
+class StylesheetsDataset < Dataset::Base
  2
+  uses :pages
  3
+  
  4
+  def load
  5
+    create_stylesheet "css" do
  6
+      create_stylesheet "site.css"
  7
+    end
  8
+  end
  9
+  
  10
+  helpers do
  11
+    def create_stylesheet(name, attributes={}, &block)    
  12
+      attributes = page_params(attributes.reverse_merge(:title => name, :slug => name, :class_name => 'StylesheetPage'))
  13
+      create_page(name, attributes, &block)
  14
+    end
  15
+  end
  16
+end
5  spec/helpers/admin/scripts_helper_spec.rb
... ...
@@ -0,0 +1,5 @@
  1
+require File.dirname(__FILE__) + '/../../spec_helper'
  2
+
  3
+describe Admin::ScriptsHelper do
  4
+  
  5
+end
5  spec/helpers/admin/styles_helper_spec.rb
... ...
@@ -0,0 +1,5 @@
  1
+require File.dirname(__FILE__) + '/../../spec_helper'
  2
+
  3
+describe Admin::StylesHelper do
  4
+  
  5
+end
36  spec/lib/javascript_tags_spec.rb
... ...
@@ -0,0 +1,36 @@
  1
+# require 'spec_helper'
  2
+require File.dirname(__FILE__) + '/../spec_helper'
  3
+
  4
+describe "Javascript Tags" do
  5
+  dataset :javascripts
  6
+
  7
+  let(:page){ pages(:home) }
  8
+
  9
+  describe "<r:javascript>" do
  10
+    let(:javascript_page){ JavascriptPage.find_by_slug('site.js')}
  11
+    subject { page }
  12
+    it { should render(%{<r:javascript />}).with_error("`javascript' tag must contain a `slug' attribute.") }
  13
+    it { should render(%{<r:javascript slug="bogus" />}).with_error("javascript not found") }
  14
+    it { should render(%{<r:javascript slug="site.js" />}).as("site.js body.") }
  15
+    it { should render(%{<r:javascript slug="site.js" as="url" />}).as("/js/site.js?#{javascript_page.updated_at.to_i}") }
  16
+    it { should render(%{<r:javascript slug="site.js" as="link" />}).as(%{<script type="#{javascript_page.headers['Content-Type']}" src="#{javascript_page.url.gsub(/\/$/,'')}?#{javascript_page.updated_at.to_i.to_s}"></script>}) }
  17
+    it { should render(%{<r:javascript slug="site.js" as="link" type="special/type" />}).as(%{<script type="special/type" src="#{javascript_page.url.gsub(/\/$/,'')}?#{javascript_page.updated_at.to_i.to_s}"></script>}) }
  18
+    it { should render(%{<r:javascript slug="site.js" as="link" something="custom" />}).as(%{<script type="#{javascript_page.headers['Content-Type']}" src="#{javascript_page.url.gsub(/\/$/,'')}?#{javascript_page.updated_at.to_i.to_s}" something="custom"></script>}) }
  19
+    it { should render(%{<r:javascript slug="site.js" as="inline" />}).as(%{<script type="#{javascript_page.headers['Content-Type']}">
  20
+//<![CDATA[
  21
+site.js body.
  22
+//]]>
  23
+</script>}) }
  24
+    it { should render(%{<r:javascript slug="site.js" as="inline" type="special/type" />}).as(%{<script type="special/type">
  25
+//<![CDATA[
  26
+site.js body.
  27
+//]]>
  28
+</script>}) }
  29
+    it { should render(%{<r:javascript slug="site.js" as="inline" something="custom" />}).as(%{<script type="#{javascript_page.headers['Content-Type']}" something="custom">
  30
+//<![CDATA[
  31
+site.js body.
  32
+//]]>
  33
+</script>}) }
  34
+  end
  35
+
  36
+end
37  spec/lib/stylesheet_tags_spec.rb
... ...
@@ -0,0 +1,37 @@
  1
+# require 'spec_helper'
  2
+require File.dirname(__FILE__) + '/../spec_helper'
  3
+
  4
+describe "Stylesheet Tags" do
  5
+  dataset :stylesheets
  6
+
  7
+  let(:page){ pages(:home) }
  8
+
  9
+  describe "<r:stylesheet>" do
  10
+    let(:stylesheet_page){ StylesheetPage.find_by_slug('site.css')}
  11
+    subject { page }
  12
+    it { should render(%{<r:stylesheet />}).with_error("`stylesheet' tag must contain a `slug' attribute.") }
  13
+    it { should render(%{<r:stylesheet slug="bogus" />}).with_error("stylesheet not found") }
  14
+    it { should render(%{<r:stylesheet slug="site.css" />}).as("site.css body.") }
  15
+    it { should render(%{<r:stylesheet slug="site.css" as="url" />}).as("/css/site.css?#{stylesheet_page.updated_at.to_i}") }
  16
+    it { should render(%{<r:stylesheet slug="site.css" as="link" />}).as(%{<link rel="stylesheet" type="text/css" href="/css/site.css?#{stylesheet_page.updated_at.to_i.to_s}" />}) }
  17
+    it { should render(%{<r:stylesheet slug="site.css" as="link" type="special/type" />}).as(%{<link rel="stylesheet" type="special/type" href="/css/site.css?#{stylesheet_page.updated_at.to_i.to_s}" />}) }
  18
+    it { should render(%{<r:stylesheet slug="site.css" as="link" something="custom" />}).as(%{<link rel="stylesheet" type="text/css" href="/css/site.css?#{stylesheet_page.updated_at.to_i.to_s}" something="custom" />}) }
  19
+    it { should render(%{<r:stylesheet slug="site.css" as="link" rel="alternate" />}).as(%{<link rel="alternate" type="text/css" href="/css/site.css?#{stylesheet_page.updated_at.to_i.to_s}" />}) }
  20
+    it { should render(%{<r:stylesheet slug="site.css" as="inline" />}).as(%{<style type="text/css">
  21
+/*<![CDATA[*/
  22
+site.css body.
  23
+/*]]>*/
  24
+</style>}) }
  25
+    it { should render(%{<r:stylesheet slug="site.css" as="inline" type="special/type" />}).as(%{<style type="special/type">
  26
+/*<![CDATA[*/
  27
+site.css body.
  28
+/*]]>*/
  29
+</style>}) }
  30
+    it { should render(%{<r:stylesheet slug="site.css" as="inline" something="custom" />}).as(%{<style type="text/css" something="custom">
  31
+/*<![CDATA[*/
  32
+site.css body.
  33
+/*]]>*/
  34
+</style>}) }
  35
+  end
  36
+
  37
+end
56  spec/models/javascript_page_spec.rb
... ...
@@ -0,0 +1,56 @@
  1
+require File.dirname(__FILE__) + '/../spec_helper'
  2
+
  3
+describe JavascriptPage do
  4
+  dataset :javascripts
  5
+  let(:javascript){ pages(:js) }
  6
+  let(:site_js){ JavascriptPage.find_by_slug('site.js') }
  7
+  
  8
+  subject{ javascript }
  9
+  its(:cache?) { should be_true }
  10
+  its(:sheet?) { should be_true }
  11
+  its(:virtual?) { should be_true }
  12
+  its(:layout) { should be_nil }
  13
+  
  14
+  describe '.root' do
  15
+    subject{ JavascriptPage }
  16
+    its(:root) { should == javascript }
  17
+  end
  18
+  
  19
+  describe '.new_with_defaults' do
  20
+    describe '#parts' do
  21
+      let(:parts){ JavascriptPage.new_with_defaults.parts }
  22
+      subject{ parts }
  23
+      its(:length) { should == 1 }
  24
+      it "should have a body part" do
  25
+        parts[0].name.should == 'body'
  26
+      end
  27
+    end
  28
+  end
  29
+  
  30
+  describe '#headers' do
  31
+    it "should have a 'Content-Type' of 'text/javascript'" do
  32
+      javascript.headers['Content-Type'].should == 'text/javascript'
  33
+    end
  34
+  end
  35
+  
  36
+  describe '#find_by_url' do
  37
+    context 'with a valid url' do
  38
+      it 'should return the child found by the given slug' do
  39
+        javascript.find_by_url('/js/site.js').should == site_js
  40
+      end
  41
+    end
  42
+  end
  43
+  
  44
+  context 'when validating a new page' do
  45
+    it "should automatically set the title to the given slug" do
  46
+      j = JavascriptPage.new(:slug => 'site.js')
  47
+      j.valid?
  48
+      j.title.should == 'site.js'
  49
+    end
  50
+    it "should automatically set the breadcrumb to the given slug" do
  51
+      j = JavascriptPage.new(:slug => 'site.js')
  52
+      j.valid?
  53
+      j.breadcrumb.should == 'site.js'
  54
+    end
  55
+  end
  56
+end
9  spec/models/page_spec.rb
... ...
@@ -0,0 +1,9 @@
  1
+require File.dirname(__FILE__) + '/../spec_helper'
  2
+
  3
+describe Page do
  4
+  describe '#sheet?' do
  5
+    it "should return false" do
  6
+      Page.new.sheet?.should be_false
  7
+    end
  8
+  end
  9
+end
56  spec/models/stylesheet_page_spec.rb
... ...
@@ -0,0 +1,56 @@
  1
+require File.dirname(__FILE__) + '/../spec_helper'
  2
+
  3
+describe StylesheetPage do
  4
+  dataset :stylesheets
  5
+  let(:stylesheet){ pages(:css) }
  6
+  let(:site_css){ StylesheetPage.find_by_slug('site.css') }
  7
+  
  8
+  subject{ stylesheet }
  9
+  its(:cache?) { should be_true }
  10
+  its(:sheet?) { should be_true }
  11
+  its(:virtual?) { should be_true }
  12
+  its(:layout) { should be_nil }
  13
+  
  14
+  describe '.root' do
  15
+    subject{ StylesheetPage }
  16
+    its(:root) { should == stylesheet }
  17
+  end
  18
+  
  19
+  describe '.new_with_defaults' do
  20
+    describe '#parts' do
  21
+      let(:parts){ StylesheetPage.new_with_defaults.parts }
  22
+      subject{ parts }
  23
+      its(:length) { should == 1 }
  24
+      it "should have a body part" do
  25
+        parts[0].name.should == 'body'
  26
+      end
  27
+    end
  28
+  end
  29
+  
  30
+  describe '#headers' do
  31
+    it "should have a 'Content-Type' of 'text/css'" do
  32
+      stylesheet.headers['Content-Type'].should == 'text/css'
  33
+    end
  34
+  end
  35
+  
  36
+  describe '#find_by_url' do
  37
+    context 'with a valid url' do
  38
+      it 'should return the child found by the given slug' do
  39
+        stylesheet.find_by_url('/css/site.css').should == site_css
  40
+      end
  41
+    end
  42
+  end
  43
+  
  44
+  context 'when validating a new page' do
  45
+    it "should automatically set the title to the given slug" do
  46
+      j = StylesheetPage.new(:slug => 'site.css')
  47
+      j.valid?
  48
+      j.title.should == 'site.css'
  49
+    end
  50
+    it "should automatically set the breadcrumb to the given slug" do
  51
+      j = StylesheetPage.new(:slug => 'site.css')
  52
+      j.valid?
  53
+      j.breadcrumb.should == 'site.css'
  54
+    end
  55
+  end
  56
+end
6  spec/spec.opts
... ...
@@ -0,0 +1,6 @@
  1
+--colour
  2
+--format
  3
+progress
  4
+--loadby
  5
+mtime
  6
+--reverse
36  spec/spec_helper.rb
... ...
@@ -0,0 +1,36 @@
  1
+unless defined? RADIANT_ROOT
  2
+  ENV["RAILS_ENV"] = "test"
  3
+  case
  4
+  when ENV["RADIANT_ENV_FILE"]
  5
+    require ENV["RADIANT_ENV_FILE"]
  6
+  when File.dirname(__FILE__) =~ %r{vendor/radiant/vendor/extensions}
  7
+    require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../../")}/config/environment"
  8
+  else
  9
+    require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../")}/config/environment"
  10
+  end
  11
+end
  12
+require "#{RADIANT_ROOT}/spec/spec_helper"
  13
+
  14
+Dataset::Resolver.default << (File.dirname(__FILE__) + "/datasets")
  15
+
  16
+if File.directory?(File.dirname(__FILE__) + "/matchers")
  17
+  Dir[File.dirname(__FILE__) + "/matchers/*.rb"].each {|file| require file }
  18
+end
  19
+
  20
+Spec::Runner.configure do |config|
  21
+  # config.use_transactional_fixtures = true
  22
+  # config.use_instantiated_fixtures  = false
  23
+  # config.fixture_path = RAILS_ROOT + '/spec/fixtures'
  24
+
  25
+  # You can declare fixtures for each behaviour like this:
  26
+  #   describe "...." do