How to create content elements

Content elements are collections of elements with the same properties. They can be used for creating any type of content like news, calendar items, companies, contacts, etc. These elements can then be displayed on the site by using the content collection section (for displaying lists) and the content item section (for displaying an individual item).

Content elements in Skyline are a subclass of articles (just like pages) which means they can contain static data and contain sections.

Here I'll guide you through creating a calendar item content element to create an events list and a detail page for each event.

Migration

First of all we'll create a migration:

$ script/generate migration CreateCalendarItemData

Our calendar item will have two static fields: title and date

class CreateCalendarItemData < ActiveRecord::Migration
  def self.up
    create_table :calendar_item_data do |t|
      t.string :title
      t.date :date
      t.timestamps
    end
  end

  def self.down
    drop_table :calendar_item_data
  end
end

Model

After we've created the migration we move onto the model.

calendar_item.rb

class CalendarItem < Skyline::Article
  class Data < Skyline::Article::Data
    set_table_name "calendar_item_data"
    
    include Skyline::Taggable
    
    has_one :calendar_item, :foreign_key => "published_publication_data_id", :class_name => "CalendarItem"
    
    named_scope :published, lambda{
      {:include => [:calendar_item], :conditions => "skyline_articles.published_publication_data_id = calendar_item_data.id"}
    }
    
    # Default scope for site
    default_scope :order => "date DESC"
    
    def date
      self[:date].present? ? self[:date] : Date.today
    end
  end
  
  include Skyline::ContentItemSectionSelectable
  def for_content_item_section
    self.published
  end  

  # Default scope for Skyline
  default_scope :include => :default_variant_data, :order => "calendar_item_data.date DESC"
  
  def title
    "#{I18n.l(self.default_variant_data.date, :format => :default)} #{self.default_variant_data.title}"
  end
  
  def url
    p = Settings.get_page(:content_pages, "calendar_item_page_id".to_sym)
    [p.andand.url,self.id].compact.join("/")
  end
  
end

In the code above you can see we define CalendarItem as a Skyline::Article

This gives us the functionalities needed to add sections to our content element.

The Data class defined within CalendarItem can be seen as a standard ActiveRecord object that links directly to our database table.

include Skyline::Taggable

This registers the model in the Skyline::taggable_models list. Which means it will show up in the list of the content collection section.

Although it will add the interface for tagging your calendar item it will not automatically add the fields to your view. We'll come to that when we start creating the views.

include Skyline::ContentItemSectionSelectable
def for_content_item_section
  self.published
end  

By adding Skyline::Sections::ContentItemSection the calendar_item will show up in your content_item section.

The method for_content_item_section is used to filter the elements that are shown in the content_item section. In this case it's only the published elements that show up.

Tip!

in your application folder run

$ rake doc:plugins

To generate the Skyline documentation in doc/plugins/skyline

has_one :calendar_item, :foreign_key => "published_publication_data_id", :class_name => "CalendarItem"

To be able  access the data in the calendar item we need to create a has_one relationship between them.

I'll explain the url method further down when we get to the configuration bit.

The rest of code is pretty much self explanatory so I'll skip to the next bit.

Views

The views are used to render the Skyline management screens for the content items. The folder structure is based on the module name space so we'll create the following folder structure:

|-app
  |-views
    |-skyline
      |-articles
        |-calendar_item
          |-_data.html.erb
          |-_header.html.erb

The views of content object consist of 2 parts.

  • _header.html.erb: view for general article metadata like variant and template selection
  • _data.html.erb : view with edit form fields to enter fields defined in article data in this case title and date. 

_header.html.erb

<dl class="advanced closed">
  <dt><a href="#" id="toggle_page_advanced"><span><%= t(:advanced, :scope => [@article.class, :headers]) %></span></a></dt>
  <dd>
    <table class="fields">
      <tbody>
        <tr>
          <th><%= v.label_with_text :name %></th>
          <td><%= v.text_field :name %></td>
        </tr>
        <% if @renderable_scope.templates_for(@variant.article).size > 1 %>
          <tr>
            <th><%= v.label_with_text :template %></th>
            <td><%= v.select :template, templates_for_select(@variant.article) %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  </dd>
</dl>

_data.html.erb

<div class="section">   	        
  <div class="head">    		      	
  	<%= a.object.class.human_name %>
  </div>
  <div class="body">  
    <div class="body">
      <table class="fields">
        <tbody>
          <tr>
            <th><%= vd.label_with_text :title %></th>
            <td><%= vd.text_field :title, :class => "full" %></td>
          </tr>
          <tr>
            <th><%= vd.label_with_text :date %></th>
            <td><%= vd.date_select :date %></td>
          </tr>
          <tr>
            <th><%= vd.label_with_text :raw_tags %></th>
            <td>
              <dl class="tagselector" id="calendar_item-tagselector">
                <dt></dt>
                <dd><%= vd.text_area :raw_tags, :rows => nil %></dd>
                <dt class="tags"><%= t(:available_tags, :scope => [:media_file, :edit]) %></dt>
                <dd class="tags">
                  <%= render :partial => "/skyline/tags/available_tags", :locals => {:tags => CalendarItem::Data.available_tags} %>
                </dd>
              </dl>              
              <script type="text/javascript" charset="utf-8">
  	            new Skyline.TagSelector("<%= vd.dom_id :raw_tags %>","#calendar_item-tagselector .taglist li");                              
              </script>
            </td>
          </tr>                           
        </tbody>
      </table>
    </div>
  </div>        
</div>

The view uses standard rails helpers to create the input fields for the calender item data.

As described before content items can be made taggable. This can be used to filter certain elements when displayed in a list. To do this we include the following code into our view:

<tr>
  <th><%= vd.label_with_text :raw_tags %></th>
  <td>
    <dl class="tagselector" id="calendar_item-tagselector">
      <dt></dt>
      <dd><%= vd.text_area :raw_tags, :rows => nil %></dd>
      <dt class="tags"><%= t(:available_tags, :scope => [:media_file, :edit]) %></dt>
      <dd class="tags">
        <%= render :partial => "/skyline/tags/available_tags", :locals => {:tags => CalendarItem::Data.available_tags} %>
      </dd>
    </dl>              
    <script type="text/javascript" charset="utf-8">
      new Skyline.TagSelector("<%= vd.dom_id :raw_tags %>","#calendar_item-tagselector .taglist li");                              
    </script>
  </td>
</tr>

Templates for collection

As described before, the folder structure of templates is based on the model name space excluding Skyline.

So to create a template for our content element we need to create the following folder structure:

|-app
  |-templates
    |-sections
      |-content_collection_section
        |-default
          |-index.html.erb
          |-_calendar_items.html.erb

index.html.erb

When rendering a content collection template Skyline looks for an index.html.erb

The index creates a collection of the elements based filtered by tag and limited by the number of rows. Then it renders the partial corresponding with the name of the content elements:

<% proxy = content_collection_section.content_class.published.with_tags(content_collection_section.tags).scoped(:limit => content_collection_section.number) %>

<%= render :partial => "#{content_collection_section.content_name.pluralize}", :locals => {content_collection_section.content_name.pluralize.to_sym => proxy} %>

_calendar_items.html.erb

The partial _calendar_times renders the collection just as normal partial would do.

<dl>
  <dt>Calendar</dt>
  <dd>
    <% if calendar_items.any? %>
      <ul class="links">
        <% calendar_items.each do |item| %>
          <li>
            <a href="<%= item.calendar_item.url %>">
              <span class="date"><%= l(item.date,:format => :default) %></span>
              <%= item.title %>
            </a>
          </li>
        <% end %>
      </ul>
    <% end %>
  </dd>
</dl>

Note!

methods outside of the Data class of the calendar_item are available as item.calendar_item

Templates for content items

To show an individual item on a page we can use the content_item section. Above we can see we included

Skyline::Sections::ContentItemSection

To have the calendar_items show up in the list.

Now we can create a template for it.

|-app
  |-templates
    |-sections
      |-content_item_section
        |-default
          |-_calendar_item.html.erb

_calendar_item.html.erb

As a calendar item is a subclass of article we can render it by using the same heplers as page.

<h1><%= calendar_item.published_publication.data.title %></h1>
<%= render_collection(calendar_item.published_publication.sections) %>

Hint!

You should check in the view if the calendar item is published and has a title.

Configuration

Now that we've created our migration, model, views and templates it's time to configure Skyline to use all of the things.

First of all we want our calendar item to show up in the content library. For this we need to add a line in the configure block to our skyline_configuration.rb in config/initializers.

skyline_configuration.rb

config.articles = ["CalendarItem"]

settings.rb

In Skyline you have the possibility to use a model named Settings. Settings can be used to store any constants related to the webiste.

To use Settings we first have to create a migration:

class CreateSettings < ActiveRecord::Migration
  def self.up
    create_table :settings do |t|
      t.column :page, :string
      t.column :data, :text
    end
  end

  def self.down
    drop_table :settings
  end
end

in the CalendarItem model we have a method called url. This method reads a preset page from the skyline settings that is used to render an individual calendar item.

So here is the code for the Settings model (app/models/settings.rb):

class Settings < ActiveRecord::Base
  include Skyline::Settings
  include Skyline::ContentItem
  
  referable_serialized_content  :calendar_item_page
                                
  page :content_pages, :title => "Content pages" do |p|
    p.field :calendar_item_page_id do |f|
      f.editor = :page_browser
      f.label = "Calendar item page"
      f.description = "This page will be used to render an individual calendar item."      
    end
  end
end

In our settings model we scope the calendar_item_page into content_pages to keep things clear.

Skyline provides a helper page_browser to select a page so we won't have to bother with creating views.

After you can select a page to render in the Admin -> Settings on the Skyline website.

Hint!

As you select a new page to render a content item you can create a separate template for these pages.

PagesController

Finally we have to tell Skyline what to do when we navigate to the calendar detail page.

This is done by overwriting the show method in the Skyline PagesController.

pages_controller.rb

class PagesController < Skyline::Site::PagesController 
  def show
    renderer = @site.renderer

    if @page_version    
      
      language = "nl"
      renderer.assigns.update(:language => language)
      
      # =================
      # = Calendar item =
      # =================
      if page = Settings.get_page(:content_pages, :calendar_item_page_id) && @url_parts.any?
        @calendar_item = CalendarItem.find_by_id(@url_parts.join("/")).andand.published_publication
        if @calendar_item
          body = renderer.render(@calendar_item)
          renderer.assigns.update(:body => body)
        end
      end
            
      # ========
      # = Page =
      # ========
      if renderer.assigns[:body].blank? && @url_parts.empty?
        renderer.assigns.update(:body => self.response.body)
      end      
      
      render :text => renderer.render(@page_version) if renderer.assigns[:body].present?
    end    

    # ================================================
    # = Fallback; render 404 if nothing was rendered =
    # ================================================
    self.handle_404 unless performed?
  end
end

Hint!

You could also add a page for dealing with a 404 error to the settings and overwrite the handle_404 method to return the page from the settings.

This will give your users a nice page when they reach a page that doesn't exist.