Widgets Implementation

The big picture

  1. a set of placeable widgets
  2. a layout that can take widgets (a layout is a set of containers, or areas on the page, where widgets can be placed)
  3. a way to match widget with layout

Open Questions

  1. Do we use separate controllers or cells for widgets?
  2. How does the edit mode look like? For possibilities have a look on widgets-page.

Terminology

owner: the actual owner of the place (e.g. site-home, profile, dashboard) where the widget is being displayed. this is always a user or a group, and is certain to be there.

widget: some object that can give us the options of the widget. it includes a definition file (definition.yml), and a set of partials, and maybe some controller code. not sure if we are going to base it on cells or something like that or not.

WidgetSetting: a record in a database that records the configuration options for a particular widget in a particular place. a widget setting is therefore associated with a widget and some context.

widgetable: the database object that represents the page that we are going to render with widgets. for a group, it would be the profile. for a user, it could be their profile. if we had dashboard widgets, it would be the user. for site-home, the context would be the site itself.

fixed layout: we come up with some pre-canned partials that then people can place widgets into specific spots.

edit mode: The owner of a widget (site-home admin / group admin) can change the placement and settings for widgets when he is in something like an edit mode. For possible solutions on edit modes see widgets or open questions.

Layout

possible fixed layouts:

 .-------.   .---+---. 
 |       |   |   |   | 
 +-------+   +---+---+ 
 |       |   |       | 
 |       |   |       | 
 '-------'   '-------' 

 .---+---.   .---+---.   .-------.   .-------.
 |   |   |   |   |   |   |       |   |       |
 +---+---+   +---+---+   +---+---+   +-------+
 |       |   |   |   |   |   |   |   |       |
 +-------+   +---+---+   +---+---+   +-------+
 |       |   |       |   |       |   |       |
 |       |   |       |   |       |   |       |
 '-------'   '-------'   '-------'   '-------'

 .---+---.   .---+---.   .---+---. 
 |   |   |   |   |   |   |   |   | 
 |   +---+   |   |   |   +---+   | 
 |   |   |   |   |   |   |   |   | 
 +---+---+   +---+---+   +---+---+ 
 |       |   |       |   |       | 
 |       |   |       |   |       | 
 '---+---'   '---+---'   '---+---'

or, broken down into possible rows:

 .---+---.   .---+---.   .---+---.   .-------.   .---+---.  
 |   |   |   |   |   |   |   |   |   |       |   |   |   | 
 |   +---+   |   |   |   +---+   |   +-------+   +---+---+ 
 |   |   |   |   |   |   |   |   | 
 +---+---+   +---+---+   +---+---+ 

This breaks down in four possible sizes for widgets:

  • Small
  • Wide
  • Tall
  • Full

Instead of these canned list of fixed layouts, we could, in the long term, allow flexible placement of these four different sizes.

Models

create_table :widgets do |t|
  t.string  :name            # determines what widget definition file to use
  t.string  :placement       # where in the layout this widget goes
  t.integer :size            # enum of SIZES
  t.integer :widgetable_id   # \ a thing that 
  t.string  :widgetable_type # / holds widgets
  t.text    :options         # serialized hash of configured options
end

#
# Create a WIDGETS definition hash in the form of:
# 
#  { 'widget_name_1' => <<contents of yml definition>>, 
#    'widget_name_2' => <<contents of yml definition>> }
#
widget_files = RAILS_ROOT+'/views/widgets/*/definition.yml'
WIDGETS = Hash[
  Dir.glob(widget_files).collect {|file| 
    widget_name = File.basename(File.dirname(file))
    definition_hash = YAML::load_file(file)
    [widget_name, definition_hash]
  }
].freeze

class Widget < ActiveRecord::Base

  SIZES = {'wide' => 1, 'tall' => 2, 'small' => 3, 'full' => 4,
           1 => 'wide', 2 => 'tall', 3 => 'small', 4 => 'full'}.freeze
  
  # a widgetable is a thing that owns widgets
  belongs_to :widgetable, :polymophic => true

  def owner
    widgetable.entity
  end
  
  # returns the view template for this widget
  def template
    options['partials'][SIZES[self.size]]
  rescue
    'not_found'
  end
  
  def options
    WIDGETS[self.name] || {}
  end
  
  # returns a hash in the form:
  # {'placement1' => <<widgetobject1>>, 'placement2' => <<widgetobject2>>}
  def self.to_hash(widgets)
    Hash[ widgets.collect{|w|[w.placement,w]} ]
  end
end

end

class Profile
  has_many :widgets, :as => 'widgetable', :dependent => :destroy
end

Helper

# render a single widget
def render_widget(widget) 
  render :template => widget.template, :locals => {:widget => widget}
end

# extra widgets have no placement, they are just numbered. 
# this will render these extra/numbered widgets
def get_the_extra_widgets(widgets, &block)
  i = 0
  while widgets[i.to_s]
    yield widgets[i.to_s]
    i+=1
  end
end

in controller:

  def render_widget_template(profile)
    render :template => profile.widget_layout, :locals => {:widgets => Widget.to_hash(profile.widgets)}
  end

Layout

And the view code for this:

 .---+---.  
 |   |   |  
 +---+---+  
 |       |  
 +-------+  
 |       |  
 |       |  
 '-------'  

  <table>
    <tr>
      <td><%= render_widget(widgets['top_left']) %></td>
      <td><%= render_widget(widgets['top_right']) %></td>
    </tr>
    <tr>
      <td colspan="2"><%= render_widget(widgets['middle']) %></td>
    </tr>
    <% get_the_extra_widgets(widgets) do |widget| %>
      <tr>
        <td colspan="2"><%= render_widget(widget) %></td>
      </tr>
    <% end %>
    end
  </table>

Writing Widgets

The widget writer’s api:

there will be different partials depending on the placement of the widget. The possible sizes are:

  • tall: a narrow layout, but one that might go on for a while.
  • wide: takes up the full width, but has limited height.
  • full: no limitations on height or width
  • small: limited height and limited width

In the partial, there are these variables: widget, current_user, widget.options.

widgets/upcoming_events/definition.yml

  name: Upcoming Events
  partials: 
    wide: events_wide
    small: events_small
    tall: events_tall
  supported_owners:
    - group
    - user
    - site
  options:
    - name: "limit"
      description: "Show no more than this many events"
      type: integer
      default: 10
    - name: "starred_only"
      description: "Only starred events"
      type: boolean
      default: false

widgets/upcoming_events/events_wide.html.erb

events = Event.upcoming.access_by(:owner => widget.owner, :current_user => current_user).starred(widget.options:starred_only).find(:all,:limit => widget.options:limit)

<% events.each do |event| %> <%=h event.title %> <%= friendly_date(event.when) %> <% end %>

Widget with separate controller
----------------------------------

bq. I don't understand what is written here. -elijah

widget/upcoming_events/controller.rb

class EventWidgetController def show(widget) @events = Event.upcoming.access_by(:owner => widget.owner, :current_user => current_user).starred(widget.options:starred_only).find(:all,:limit => widget.options:limit) render(widget) end protected def render(widget) render :partial => widget.partial, :locals => {:widget => widget) end end class EventController widget(‘upcoming’) do |widget| @events = Event.upcoming.access_by(:owner => widget.owner, :current_user => current_user).starred(widget.options:starred_only).find(:all,:limit => widget.options:limit) end end
    
   
  1. \/\/ this actually goes in some seperate module
before_filter :prepare_widgets def prepare_widgets @@widgets.each_pair do |name, block| @widget = # … fetch widget / setting / whatever if @widget block.call end end def self.widget name, &block @@widgetsname = block end end

widgets/upcoming_events/events_wide.html.erb

<% @events.each do |event| %> <%=h event.title %> <%= friendly_date(event.when) %> <% end %>


  1. options can be passed as local too
    events = Event.upcoming.access_by(current_user).starred(
    
    
    widgets/_page_list.html.erb
    
    

    <%
  2. expected options:
  3. - number_of_records

default_options = {:number_of_records => 20}

  1. merge given options with the widgets default options
    options.merge!(@@default_options)

params:path ||= ""
params:path = params:path.split(‘/’)
params:path += ’descending’, ’updated_at’ if params:path.empty?
params:path += [‘limit’,options:number_of_records]

pages = Page.find_by_path(params[:path], options_for_group(group))
-%>

<%= “Page list”:page_list %>

<%= render :partial => ‘pages/list’ %>



Tests
------------------------

Functional Tests for Widgets:

# showing the right widgets

1. Loading a Site Home should work, and show the default widgets for site homes

2. Changing the sites default settings for the site home, should work and end up in showing the widgets on the right position again

3. Changing the users default settings for that site home, should work, and end up in showing the widgets in the right position again.

# editing the widgets (presumes to know in what way the edit mode works)

1. checking all the ajax / js functions for moving, folding and closing widgets



Discussion
--------------------------------

(the original discussion was in German, this is a rough translation)

# Problem: The Site defines default values for displaying the sie-home. This includes a set of widgets to display, and where to display them. Now, once a user changes these settings for himself, we need to find a way to merge the default settings, with the users ones.

Site Settings:

Widget1: :position => 'left_sidebar', :order => 1, :options => { :number_of_records => 12 }
Widget2: :position => 'left_sidebar', :order => 2
Widget3: :position => 'left_sidebar', :order => 3, :options => { :number_of_records => 5 }

User Settings:

Widget2: :position => 'left_sidebar', :order => 1

From these to sets of options 

a.) Onlydisplay Widget 2 (as it is the only widget that is found searching by user_id_and_page_id)
b.) Only move Widget2 to the top of the Widgets (when merging the two sets of options)

1 see two possible solutions:

1. I think that as soon as a user updates any setting in a specific widget target (e.g. left_sidebar), all widgets in that target need to get a setting for the user. ah okay, i even thought that all widgets would need a setting for the user. But i think by target could be more convenient. 
But what happens when a widget (e.g. group-wiki) is displayed on two targets?
What should happen? nothing happens... Okay :p


I added a position field, because we need to find WidgetSettings by position
. gut, we should discuss in english, then we can put it on the wiki later.


Wollen wir die ganzen verplanten diskussionen nicht raus nehmen? Uebersetzt kommen die eh nicht richtig rueber :)
  # a: i think it's enough to save widget_settings for users, because groups will probably not want to define default settings how to view the site home for their users.
  # n: i thought like this groups might define default settings for their group home
  # a: up to now, i think we're only talking about site homes. But customizing group homes i good as well. But then we need a combination of site_id and group_id .. erm, yeah, well.. entity_id
  # n: i just thought it makes sense to be able to save widget settings in different contexts
  # n: imho it makes no sense to bind the settings to users, and from the usability of the model nothing changes when using polymorph association
  # a: kk
  
    # A: to me it could make sence, when thinking about custom appearance for site home after all, to
    # have something like a SiteHomeSettings Model, that is created for a user once he starts to
    # customize the site home appearance
    # N: widget settings are fetched very often, so the queries should be superfast. having an overall
    # settings model would either make that table pretty big, or require serialization (which slows
    # things terribly down if the serialized field stores conditions by which you want to find records)