--- src/app/controllers/image_controller.rb | 85 +++++++++++++++++++++++ src/app/controllers/instance_controller.rb | 6 +- src/app/controllers/pool_controller.rb | 51 +++++++++++++- src/app/helpers/image_helper.rb | 2 + src/app/models/image.rb | 37 ++++++++++ src/app/models/instance.rb | 32 +++++++++ src/app/views/image/show.html.erb | 85 +++++++++++++++++++++++ src/app/views/instance/new.html.erb | 96 +++++++++++++++++++++++--- src/app/views/layouts/_main_nav.html.erb | 1 + src/app/views/pool/images.html.erb | 20 ------ src/app/views/pool/show.html.erb | 89 +++++++++++++----------- src/test/functional/image_controller_test.rb | 8 ++ src/test/unit/helpers/image_helper_test.rb | 4 + 13 files changed, 442 insertions(+), 74 deletions(-) create mode 100644 src/app/controllers/image_controller.rb create mode 100644 src/app/helpers/image_helper.rb create mode 100644 src/app/views/image/show.html.erb delete mode 100644 src/app/views/pool/images.html.erb create mode 100644 src/test/functional/image_controller_test.rb create mode 100644 src/test/unit/helpers/image_helper_test.rb
diff --git a/src/app/controllers/image_controller.rb b/src/app/controllers/image_controller.rb new file mode 100644 index 0000000..0c15eb5 --- /dev/null +++ b/src/app/controllers/image_controller.rb @@ -0,0 +1,85 @@ +# +# Copyright (C) 2009 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. A copy of the GNU General Public License is +# also available at http://www.gnu.org/copyleft/gpl.html. + +# Filters added to this controller apply to all controllers in the application. +# Likewise, all the methods added will be available for all controllers. + +require 'util/taskomatic' + +class ImageController < ApplicationController + before_filter :require_user + + def index + end + + def show + # FIXME: check on privilege IMAGE_VIEW which currently doesn't exist + #require_privilege(Privilege::POOL_VIEW, @pool) + # only displays empty table + end + + def images_paginate + # FIXME: check on privilege IMAGE_VIEW which currently doesn't exist + #require_privilege(Privilege::POOL_VIEW, @pool) + + # datatables sends pagination in format: + # iDisplayStart - start index + # iDisplayLength - num of recs + # => we need to count page num + page = params[:iDisplayStart].to_i / Image::RECS_PER_PAGE + + if params[:mode].to_s == 'simple' + simple_mode = true + cols = Image::COLUMNS_SIMPLE + default_order_col = 1 + else + cols = Image::COLUMNS + simple_mode = false + default_order_col = 2 + end + + # FIXME: check sortable on selected column + order_col_rec = cols[params[:iSortCol_0].to_i] + order_col = cols[default_order_col] unless order_col_rec && order_col_rec[:opts][:searchable] + order = order_col[:id] + " " + (params[:sSortDir_0] == 'desc' ? 'desc' : 'asc') + + @images = Image.search_filter(params[:sSearch]).paginate( + :page => page + 1, + :include => :instances, + :order => order + ) + + expand_button_html = "<img src='/images/dir_closed.png'>" + + data = @images.map do |i| + if simple_mode + [i.id, i.name, i.architecture, i.instances.size] + else + [i.id, expand_button_html, i.name, i.architecture, i.instances.size, "TODO: some description here?"] + end + end + + render :json => { + :sEcho => params[:sEcho], + :iTotalRecords => @images.total_entries, + :iTotalDisplayRecords => @images.total_entries, + :aaData => data + } + end + +end diff --git a/src/app/controllers/instance_controller.rb b/src/app/controllers/instance_controller.rb index 0008c06..e73d7cd 100644 --- a/src/app/controllers/instance_controller.rb +++ b/src/app/controllers/instance_controller.rb @@ -38,9 +38,9 @@ class InstanceController < ApplicationController end def new - @instance = Instance.new({:pool_id => params[:id]}) - @pool = Pool.find(params[:id]) - require_privilege(Privilege::INSTANCE_MODIFY,@pool) + @instance = Instance.new(params[:instance]) + require_privilege(Privilege::POOL_VIEW, @instance.pool) if @instance.pool + @pools = Pool.list_for_user(@current_user, Privilege::POOL_VIEW) end def create diff --git a/src/app/controllers/pool_controller.rb b/src/app/controllers/pool_controller.rb index 2ae0988..5613ddd 100644 --- a/src/app/controllers/pool_controller.rb +++ b/src/app/controllers/pool_controller.rb @@ -78,9 +78,58 @@ class PoolController < ApplicationController def delete end - def images + def instances_paginate @pool = Pool.find(params[:id]) require_privilege(Privilege::POOL_VIEW, @pool) + + # datatables sends pagination in format: + # iDisplayStart - start index + # iDisplayLength - num of recs + # => we need to count page num + page = params[:iDisplayStart].to_i / Instance::RECS_PER_PAGE + + order_col_rec = Instance::COLUMNS[params[:iSortCol_0].to_i] + order_col = Instance::COLUMNS[2] unless order_col_rec && order_col_rec[:opts][:searchable] + order = order_col[:id] + " " + (params[:sSortDir_0] == 'desc' ? 'desc' : 'asc') + + @instances = Instance.search_filter(params[:sSearch]).paginate( + :page => page + 1, + :order => order, + :conditions => {:pool_id => @pool.id} + ) + + # FIXME: default stylesheet redefines link color + details_html = "<a href='#' class='details_link'>Details</a>" + + render :json => { + :sEcho => params[:sEcho], + :iTotalRecords => @instances.total_entries, + :iTotalDisplayRecords => @instances.total_entries, + :aaData => @instances.map {|i| [i.id, "", i.name, details_html, i.state, i.hardware_profile.name, i.image.name]} + } end + + def accounts_for_pool + @pool = Pool.find(params[:pool_id]) + require_privilege(Privilege::ACCOUNT_VIEW,@pool) + @cloud_accounts = [] + all_accounts = CloudAccount.list_for_user(@current_user, Privilege::ACCOUNT_ADD) + all_accounts.each {|account| + @cloud_accounts << account unless @pool.cloud_accounts.map{|x| x.id}.include?(account.id) + } + end + + def add_account + @pool = Pool.find(params[:pool]) + @cloud_account = CloudAccount.find(params[:cloud_account]) + require_privilege(Privilege::ACCOUNT_ADD,@pool) + require_privilege(Privilege::ACCOUNT_ADD,@cloud_account) + Pool.transaction do + @pool.cloud_accounts << @cloud_account unless @pool.cloud_accounts.map{|x| x.id}.include?(@cloud_account.id) + @pool.save! + @pool.populate_realms_and_images([...@cloud_account]) + end + redirect_to :action => 'show', :id => @pool.id + end end diff --git a/src/app/helpers/image_helper.rb b/src/app/helpers/image_helper.rb new file mode 100644 index 0000000..b1092d1 --- /dev/null +++ b/src/app/helpers/image_helper.rb @@ -0,0 +1,2 @@ +module ImageHelper +end diff --git a/src/app/models/image.rb b/src/app/models/image.rb index 86806a4..6591823 100644 --- a/src/app/models/image.rb +++ b/src/app/models/image.rb @@ -20,6 +20,11 @@ # Likewise, all the methods added will be available for all controllers. class Image < ActiveRecord::Base + RECS_PER_PAGE = 15 + + cattr_reader :per_page + @@per_page = RECS_PER_PAGE + has_many :instances belongs_to :provider @@ -43,6 +48,38 @@ class Image < ActiveRecord::Base validates_presence_of :architecture, :if => :provider + # used to get sorting column in controller and in view to generate datatable definition and + # html table structure + COLUMNS = [ + {:id => 'id', :header => '<input type="checkbox" id="image_id_all" onclick="checkAll(event)">', :opts => {:checkbox_id => 'image_id', :searchable => false, :sortable => false, :width => '1px', :class => 'center'}}, + {:id => 'expand_button', :header => '', :header => '', :opts => {:searchable => false, :sortable => false, :width => '1px'}}, + {:id => 'name', :header => 'Name', :opts => {:width => "30%"}}, + {:id => 'architecture', :header => 'Architecture', :opts => {:width => "10%"}}, + {:id => 'instances', :header => 'Instances', :opts => {:sortable => false, :width => "10%"}}, + {:id => 'details', :header => '', :opts => {:sortable => false, :visible => false, :searchable => false}}, + ] + + COLUMNS_SIMPLE = [ + {:id => 'id', :header => '', :opts => {:visible => false, :searchable => false, :sortable => false, :width => '1px', :class => 'center'}}, + {:id => 'name', :header => 'Name', :opts => {:width => "50%"}}, + {:id => 'architecture', :header => 'Architecture', :opts => {:width => "30%"}}, + {:id => 'instances', :header => 'Instances', :opts => {:sortable => false, :width => "20%"}}, + ] + + SEARCHABLE_COLUMNS = %w(name architecture) + + # FIXME: duplicit code with definition in app/models/image.rb + # what's the best strategy? inherit from same class, include module,...? + named_scope :search_filter, lambda {|str| + if str.to_s.empty? + {:conditions => {}} + else + $stderr.puts SEARCHABLE_COLUMNS.map {|c| "#{c} like ?"}.join(" OR ") + $stderr.puts Array.new(SEARCHABLE_COLUMNS.size, "%#{str}%").inspect + {:conditions => [SEARCHABLE_COLUMNS.map {|c| "#{c} like ?"}.join(" OR ")] + Array.new(SEARCHABLE_COLUMNS.size, "%#{str}%")} + end + } + def provider_image? !provider.nil? diff --git a/src/app/models/instance.rb b/src/app/models/instance.rb index fa89844..9cb6c62 100644 --- a/src/app/models/instance.rb +++ b/src/app/models/instance.rb @@ -20,6 +20,11 @@ # Likewise, all the methods added will be available for all controllers. class Instance < ActiveRecord::Base + RECS_PER_PAGE = 15 + + cattr_reader :per_page + @@per_page = RECS_PER_PAGE + belongs_to :pool belongs_to :cloud_account @@ -56,6 +61,33 @@ class Instance < ActiveRecord::Base :in => [STATE_NEW, STATE_PENDING, STATE_RUNNING, STATE_SHUTTING_DOWN, STATE_STOPPED, STATE_CREATE_FAILED] + # used to get sorting column in controller and in view to generate datatable definition and + # html table structure + COLUMNS = [ + {:id => 'id', :header => '<input type="checkbox" id="image_id_all" onclick="checkAll(event)">', :opts => {:checkbox_id => 'image_id', :searchable => false, :sortable => false, :width => '1px', :class => 'center'}}, + {:id => 'actions', :header => 'Actions', :opts => {:width => "15%", :sortable => false}}, + {:id => 'name', :header => 'Name', :opts => {:width => "25%"}}, + {:id => 'details', :header => '', :opts => {:width => "10%", :sortable => false, :searchable => false}}, + {:id => 'state', :header => 'State', :opts => {:width => "15%"}}, + {:id => 'hwprofile', :header => 'HW profile', :opts => {:width => "15%"}}, + {:id => 'image', :header => 'Image', :opts => {:sortable => false}}, + ] + + SEARCHABLE_COLUMNS = %w(name state) + + # FIXME: duplicit code with definition in app/models/image.rb + # what's the best strategy? inherit from same class, include module,...? + named_scope :search_filter, lambda {|str| + if str.to_s.empty? + {:conditions => {}} + else + $stderr.puts SEARCHABLE_COLUMNS.map {|c| "#{c} like ?"}.join(" OR ") + $stderr.puts Array.new(SEARCHABLE_COLUMNS.size, "%#{str}%").inspect + {:conditions => [SEARCHABLE_COLUMNS.map {|c| "#{c} like ?"}.join(" OR ")] + Array.new(SEARCHABLE_COLUMNS.size, "%#{str}%")} + end + } + + def get_action_list(user=nil) # return empty list rather than nil # FIXME: not handling pending state now -- only current state diff --git a/src/app/views/image/show.html.erb b/src/app/views/image/show.html.erb new file mode 100644 index 0000000..e25bd86 --- /dev/null +++ b/src/app/views/image/show.html.erb @@ -0,0 +1,85 @@ +<script type="text/javascript"> + + function fnOpenClose() { + $('td img', dataTable_images_table.fnGetNodes() ).each( function () { + $(this).click( function () { + var nTr = this.parentNode.parentNode; + if ( this.src.match('dir_open') ) + { + /* This row is already open - close it */ + this.src = "/images/dir_closed.png"; + /* fnClose doesn't do anything for server-side processing - do it ourselves :-) */ + var nRemove = $(nTr).next()[0]; + nRemove.parentNode.removeChild( nRemove ); + } + else + { + /* Open this row */ + this.src = "/images/dir_open.png"; + dataTable_images_table.fnOpen( nTr, dataTable_images_table.fnGetData(nTr)[5], 'details' ); + } + }); + }); + } + + function getCheckedRows() { + return $('input[name="image_id[]"]:checked', dataTable_images_table).length; + } + + function showButtons() { + getCheckedRows(); + $('input[name="create_image"]').attr('disabled', getCheckedRows() != 1); + } + + function checkRow(ev) { + var box = $('input[name="image_id[]"]', ev.target.parentNode); + if ($(ev.target).attr('name') != "image_id[]") + box.attr('checked', !box.attr('checked')); + showButtons(); + } + + function checkAll(ev) { + $('input[name="image_id[]"]', dataTable_images_table).attr('checked', $(ev.target).attr('checked')) + } + + function drawCallback() { + $('#image_id_all').attr('checked', false); + fnOpenClose(); + } + + function createInstance() { + var selected_row = $('input[name="image_id[]"]:checked', dataTable_images_table); + location.href = '<%= url_for(:controller => "instance", :action => "new") %>?instance[image_id]=' + selected_row.attr('value'); + } +</script> + +<%= datatable( + Image::COLUMNS.map {|c| c[:opts]}, + { + :table_dom_id => 'images_table', + :per_page => Image::RECS_PER_PAGE, + :sort_by => "[2, 'asc']", + :serverside => true, + :ajax_source => url_for(:controller => 'image', :action => 'images_paginate'), + :append => ".fnSetFilteringDelay()", + :persist_state => false, + :click_callback => "function(ev) {checkRow(ev);}", + :draw_callback => "drawCallback", + } +) %> + <div style="padding:20px"> + <input type="button" value="Create instance" name="create_image" disabled=true onclick="createInstance()"> + </div> + <table class="datatable display" id="images_table"> + <thead> + <tr> + <% Image::COLUMNS.each do |c| %> + <%= "<th>#{c[:header]}</th>" %> + <% end %> + </tr> + </thead> + <tbody> + </tbody> +</table> + + diff --git a/src/app/views/instance/new.html.erb b/src/app/views/instance/new.html.erb index 986959a..13077be 100644 --- a/src/app/views/instance/new.html.erb +++ b/src/app/views/instance/new.html.erb @@ -2,28 +2,102 @@ <%= error_messages_for 'instance' %> <h2>Add a New Instance</h2><br /> - <% form_tag :action => 'create' do-%> + <% form_tag({:action => 'create'}, + :id => "instance_form") do-%> <ul> <li><label>Name<span>Name for this new Instance</span></label><%= text_field :instance, :name, :class => "txtfield" %></li> - <% if @pool.hardware_profiles.size > 0 %> + + <li><label>Image<span>Choose a bundled image to use</span></label> + <input type="button" style="cursor:pointer" onclick="showImages(event)" name="image_selector" value="<%= @instance.image ? @instance.image.name : "click to select image"%>"/> + </li> + <input type=hidden name="instance[image_id]" value="<%= @instance.image ? @instance.image.id : '' %>"> + + <li><label>Pool<span>Pick your pool</span></label> + <%= select("instance", "pool_id", + @pools.collect {|p| [ p.name, p.id ] }, + { :include_blank => true }, + {:onchange => "poolSelected(this)"}) %> + </li> + + <% if @instance.pool and @instance.pool.hardware_profiles.size > 0 %> <li><label>Hardware Profile<span>Pick your hardware profile</span></label> <%= select("instance", "hardware_profile_id", - @pool.hardware_profiles.collect {|p| [ p.name, p.id ] }, + @instance.pool.hardware_profiles.collect {|p| [ p.name, p.id ] }, { :include_blank => true }) %> </li> <% end %> - <% if @pool.images.size > 0 %> - <li><label>Image<span>Choose a bundled image to use</span></label> - <%= select("instance", "image_id", @pool.images.collect {|i| [ truncate(i.name, 29, '...'), i.id ] }, { :include_blank => true }) %> - </li> - <% end %> - <% if @pool.realms.size > 0 %> + <% if @instance.pool and @instance.pool.realms.size > 0 %> <li><label>Realm<span>Choose a realm</span></label> - <%= select("instance", "front_end_realm", @pool.realms, { :include_blank => false }) %> + <%= select("instance", "front_end_realm", @instance.pool.realms, { :include_blank => false }) %> </li> <% end %> </ul> - <%= hidden_field :instance, :pool_id %> <%= submit_tag "Save", :class => "submit" %> <% end %> + </div> + + + +<script type="text/javascript"> + function showImages(ev) { + $("#images_table_wrapper").css('display', 'block'); + images_dialog = $("#images_table_wrapper").dialog({ + title: "Select image for new instance", + dialogClass: 'flora', + width: 600, + height: 500, + modal: true, + overlay: {opacity: 0.2, background: "black"} + }); + } + + function selectImage(ev) { + var image_id = dataTable_images_table.fnGetData(ev.target.parentNode)[0]; + var image_name = dataTable_images_table.fnGetData(ev.target.parentNode)[1]; + $("#images_table_wrapper").css('display', 'none'); + $("#images_table_wrapper").dialog('close'); + $('input[name="image_selector"]').attr('value', image_name); + $('input[name="instance[image_id]"]').attr('value', image_id); + } + + function poolSelected(field) { + // TODO: ajax load would be nicer, but complete page reload is easy to implement + location.href = '<%= url_for(:action => "new") %>?' + $("#instance_form").serialize(); + //var url = '<%= url_for(:controller => 'pool', :action => :profiles_and_realms) %>/' + field.value; + //$.getJSON(url, function(data) { + // $("#instance_hardware_profile_id").find('option').remove().end(); + // $(data.hardware_profiles).each(function(rec) { + // $("#instance_hardware_profile_id").append('<select value="'+rec.id+'">'+rec.name+'</select>'); + // }); + // $(data.front_end_realm).each(function(rec) { + // $("#instance_front_end_realm").append('<select value="'+rec+'">'+rec+'</select>'); + // }); + //}); + } +</script> + +<%= datatable( + Image::COLUMNS_SIMPLE.map {|c| c[:opts]}, + { + :table_dom_id => 'images_table', + :per_page => Image::RECS_PER_PAGE, + :sort_by => "[2, 'asc']", + :serverside => true, + :ajax_source => url_for(:controller => 'image', :action => 'images_paginate') + "?mode=simple", + :append => ".fnSetFilteringDelay()", + :click_callback => "function(event) {selectImage(event)}", + } +) %> +<div style="display:none" id="images_table_wrapper"> + <table class="datatable display" id="images_table"> + <thead> + <tr> + <% Image::COLUMNS_SIMPLE.each do |c| %> + <%= "<th>#{c[:header]}</th>" %> + <% end %> + </tr> + </thead> + <tbody> + </tbody> +</table> </div> diff --git a/src/app/views/layouts/_main_nav.html.erb b/src/app/views/layouts/_main_nav.html.erb index 0155bd6..d18ac48 100644 --- a/src/app/views/layouts/_main_nav.html.erb +++ b/src/app/views/layouts/_main_nav.html.erb @@ -1,3 +1,4 @@ +<%=link_to "View images", {:controller => "image", :action => "show"}, :class => "actionlink" %> <%=link_to "Add a provider", {:controller => "provider", :action => "new"}, :class => "actionlink" %> <%=link_to "Add a pool", {:controller => "pool", :action => "new"}, :class => "actionlink" %> diff --git a/src/app/views/pool/images.html.erb b/src/app/views/pool/images.html.erb deleted file mode 100644 index e0187a8..0000000 --- a/src/app/views/pool/images.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<% if @pool.images.size == 0 %> -<h1>There are no images to display</h1> -<% else %> - <table> - <thead> - <tr> - <th scope="col">Name</th> - <th scope="col">Arch</th> - </tr> - </thead> - <tbody> - <%[email protected] {|image| %> - <tr> - <td><%= image.name %></td> - <td><%= image.architecture %></td> - </tr> - <% } %> - </tbody> - </table> -<% end %> \ No newline at end of file diff --git a/src/app/views/pool/show.html.erb b/src/app/views/pool/show.html.erb index ef637f1..7a4ce39 100644 --- a/src/app/views/pool/show.html.erb +++ b/src/app/views/pool/show.html.erb @@ -1,42 +1,53 @@ -<% if @instances.size == 0 %> -<h1>There are no instances to display</h1> -<% else %> - <table> - <thead> - <tr> - <th scope="col">Actions</th> - <th scope="col">Name</th> - <th scope="col">State</th> - <th scope="col">Hardware Profile</th> - <th scope="col">Image</th> - </tr> - </thead> - <tbody> - <%[email protected] {|instance| %> - <tr> - <td> - <ul class="instance_action_list"> - <%instance.get_action_list.each {|action|%> - <li> - <%= link_to action, :controller => "instance", - :action => "instance_action", - :id => instance, - :instance_action => action %> - </li> - <% } %> - </ul> - </td> - <td><%= instance.name %></td> - <td><%= instance.state %></td> - <td><%= instance.hardware_profile.name %></td> - <td><%= instance.image.name %></td> - </tr> - <% } %> - </tbody> - </table> -<% end %> -<%= link_to "Add a new instance", {:controller => "instance", :action => "new", :id => @pool}, :class=>"actionlink"%> +<%= datatable( + Instance::COLUMNS.map {|c| c[:opts]}, + { + :table_dom_id => 'instances_table', + :per_page => Instance::RECS_PER_PAGE, + :sort_by => "[3, 'asc']", + :serverside => true, + :ajax_source => url_for(:controller => 'pool', :action => 'instances_paginate') + "/#[email protected]}", + :append => ".fnSetFilteringDelay()", + :persist_state => false, + :click_callback => "function(ev) {clickRow(ev);}", + } +) %> +<table class="datatable display" id="instances_table"> + <thead> + <tr> + <% Instance::COLUMNS.each do |c| %> + <%= "<th>#{c[:header]}</th>" %> + <% end %> + </tr> + </thead> + <tbody> + </tbody> +</table> +<script type="text/javascript"> + function showInstanceDetailsDialog() { + images_dialog = $("<div>TODO: show dialog</div>").dialog({ + title: "Instance details", + dialogClass: 'flora', + width: 600, + height: 300, + modal: true, + overlay: {opacity: 0.2, background: "black"} + }); + } + + function clickRow(ev) { + var box = $('input[name="image_id[]"]', ev.target.parentNode); + if ($(ev.target).hasClass('details_link')) showInstanceDetailsDialog(); + if ($(ev.target).attr('name') != "image_id[]") + box.attr('checked', !box.attr('checked')); + } + + function checkAll(ev) { + $('input[name="image_id[]"]', dataTable_instances_table).attr('checked', $(ev.target).attr('checked')) + } + +</script> + +<%= link_to "Add a new instance", {:controller => "instance", :action => "new", "instance[pool_id]" => @pool}, :class=>"actionlink"%> <%= link_to "User access", {:controller => "permissions", :action => "list", :pool_id => @pool.id}, :class=>"actionlink" if has_view_perms? %> <%= link_to "Hardware Profiles", {:action => "hardware_profiles", :id => @pool.id}, :class=>"actionlink"%> -<%=link_to "View Images", {:controller => "pool", :action => "images", :id => @pool}, :class => "actionlink" %> <%= link_to "Realms", {:action => "realms", :id => @pool.id}, :class=>"actionlink"%> diff --git a/src/test/functional/image_controller_test.rb b/src/test/functional/image_controller_test.rb new file mode 100644 index 0000000..73b754b --- /dev/null +++ b/src/test/functional/image_controller_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class ImageControllerTest < ActionController::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/src/test/unit/helpers/image_helper_test.rb b/src/test/unit/helpers/image_helper_test.rb new file mode 100644 index 0000000..7c407bc --- /dev/null +++ b/src/test/unit/helpers/image_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class ImageHelperTest < ActionView::TestCase +end -- 1.6.2.5 _______________________________________________ deltacloud-devel mailing list [email protected] https://fedorahosted.org/mailman/listinfo/deltacloud-devel
