Mailinglist Archive: opensuse-buildservice (238 mails)

< Previous Next >
[opensuse-buildservice] [patch] spec file wizard
  • From: Michal Marek <mmarek@xxxxxxx>
  • Date: Fri, 30 May 2008 14:10:32 +0200
  • Message-id: <483FEEB8.9030509@xxxxxxx>
Hi,

I'm working on a new feature of the buildservice, a wizard that helps
with creating spec files (and in the future maybe with other tasks). See
http://lizards.opensuse.org/2008/05/26/learning-ruby/ .

I now have a working prototype, including a webclient interface. It
consists of
- A controller on the api (frontend), that sends a set of xml-encoded
questions to the client, receives answers from the client and either
asks more questions (suggesting defaults where possible) or generates a
spec file
- A webclient part that converts the xml questions into a HTML form,
let's the user fill in the form and sends the responses to the api.

It should be possible to also write an osc command that asks the
questions on terminal instead.

Here are some screenshots of the webclient:
http://michal.markovi.net/images/bs-wizard-2008-05-30-1.png
http://michal.markovi.net/images/bs-wizard-2008-05-30-2.png
http://michal.markovi.net/images/bs-wizard-2008-05-30-3.png
http://michal.markovi.net/images/bs-wizard-2008-05-30-4.png
http://michal.markovi.net/images/bs-wizard-2008-05-30-5.png
http://michal.markovi.net/images/bs-wizard-2008-05-30-6.png
http://michal.markovi.net/images/bs-wizard-2008-05-30-7.png
http://michal.markovi.net/images/bs-wizard-2008-05-30-8.png
http://michal.markovi.net/images/bs-wizard-2008-05-30-9.png

An example of the an xml document describing the questions:
$ curl -n 'http://localhost:3001/source/home:mmarek/hello/_wizard'
<?xml version="1.0" encoding="UTF-8"?>
<wizard last="false">
<label>Step 1/2</label>
<legend>What do you want to package?</legend>
<entry name="tarball" type="file">
<label>Source tarball to upload</label>
<legend></legend>
<value></value>
</entry>
</wizard>

- label and legend are self-explanatory.
- value is the default value to present to the user.
- If the last attribute of the root element is true, it means that the
client shouldn't send any further replies.
- There can be an arbitrary number of <entry> elements, each
representing a question to ask
- The name attribute of an entry has the same meaning as in HTML forms
- type can currently be one of
- file: the client should PUT a file to the package directory in a
*separate* request and send the filename as response (e.g.
tarball=foo.tar.gz)
- text or longtext: text data, longtext expects a multiline text (e.g.
the package description)

Client sends back either a GET request with the data in the query string
or a POST request encoding the data either as
application/x-www-form-urlencoded or multipart/form-data (i.e. anything
rails handles).

All communication with the API happens via the
/source/<project>/<package>/_wizard url, which means that the client
must first create the package by uploading a dummy _meta (see the
wizard_new action in the webclient).

To maintain it's state, the frontend stores a wizard.xml file in the
package directory. I know it's a hack, but it seemed as the easies way
for the time being :).
$ curl -sn 'http://localhost:3001/source/home:mmarek/hello/wizard.xml' |
xmllint -format -
<?xml version="1.0"?>
<wizard>
<data name="name">hello</data>
<data name="tarball">hello-2.3.tar.bz2</data>
<guess name="license">GPL v2 or later</guess>
<guess name="version">2.3</guess>
</wizard>

The patch is attached. Note that this my first ruby code ever (I started
reading some tutorials this week), so please bear with me. If some of
the ruby & rails experts have some time to review the patch for
security, bugs, style, etc, that would be great.

Michal
Index: frontend/test/unit/wizard_form_test.rb
===================================================================
--- frontend/test/unit/wizard_form_test.rb (revision 0)
+++ frontend/test/unit/wizard_form_test.rb (revision 0)
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WizardFormTest < ActiveSupport::TestCase
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
Index: frontend/test/unit/wizard_state_test.rb
===================================================================
--- frontend/test/unit/wizard_state_test.rb (revision 0)
+++ frontend/test/unit/wizard_state_test.rb (revision 0)
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WizardStateTest < ActiveSupport::TestCase
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
Index: frontend/test/functional/wizard_controller_test.rb
===================================================================
--- frontend/test/functional/wizard_controller_test.rb (revision 0)
+++ frontend/test/functional/wizard_controller_test.rb (revision 0)
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WizardControllerTest < ActionController::TestCase
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
Index: frontend/test/fixtures/wizard_forms.yml
===================================================================
--- frontend/test/fixtures/wizard_forms.yml (revision 0)
+++ frontend/test/fixtures/wizard_forms.yml (revision 0)
@@ -0,0 +1,7 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+
+# one:
+# column: value
+#
+# two:
+# column: value
Index: frontend/app/helpers/wizard_helper.rb
===================================================================
--- frontend/app/helpers/wizard_helper.rb (revision 0)
+++ frontend/app/helpers/wizard_helper.rb (revision 0)
@@ -0,0 +1,2 @@
+module WizardHelper
+end
Index: frontend/app/models/wizard_form.rb
===================================================================
--- frontend/app/models/wizard_form.rb (revision 0)
+++ frontend/app/models/wizard_form.rb (revision 0)
@@ -0,0 +1,31 @@
+# wizard input form with entries
+class WizardForm
+
+ attr_reader(:label, :legend, :entries)
+ attr_accessor(:last)
+
+ def initialize(label, legend="")
+ @label = label
+ @legend = legend
+ @entries = []
+ end
+
+ class Entry
+ attr_reader(:name, :type, :label, :legend, :value)
+ def initialize(name, type, label, legend, value)
+ @name = name
+ @type = type
+ @label = label
+ @legend = legend
+ @value = value
+ end
+ end
+
+ def add_entry(name, type, label, legend="", value="")
+ e = Entry.new(name, type, label, legend, value)
+ @entries << e
+ end
+
+end
+
+# vim:et:ts=2:sw=2
Index: frontend/app/models/wizard_state.rb
===================================================================
--- frontend/app/models/wizard_state.rb (revision 0)
+++ frontend/app/models/wizard_state.rb (revision 0)
@@ -0,0 +1,62 @@
+class WizardState
+
+ attr_reader(:dirty)
+
+ def initialize(text = "")
+ @data = {}
+ @guess = {}
+ @dirty = false
+ xml = REXML::Document.new(text)
+ xml.elements.each("wizard/data") do |element|
+ @data[element.attributes["name"]] = element.text
+ end
+ xml.elements.each("wizard/guess") do |element|
+ @guess[element.attributes["name"]] = element.text
+ end
+ end
+
+ def store(name, value)
+ if @data[name] != value
+ @data[name] = value
+ @dirty = true
+ end
+ end
+
+ def store_guess(name, value)
+ if @guess[name] != value
+ @guess[name] = value
+ @dirty = true
+ end
+ end
+
+ def get(name)
+ return @data[name] || @guess[name]
+ end
+
+ def get_data(name)
+ return @data[name]
+ end
+
+ def serialize
+ xml = REXML::Document.new
+ xml.add_element(REXML::Element.new("wizard"))
+ @data.each do |name, value|
+ e = REXML::Element.new("data")
+ e.attributes["name"] = name
+ e.text = value
+ xml.root.add_element(e)
+ end
+ @guess.each do |name, value|
+ e = REXML::Element.new("guess")
+ e.attributes["name"] = name
+ e.text = value
+ xml.root.add_element(e)
+ end
+ res = ""
+ xml.write(res)
+ return res
+ end
+
+end
+
+# vim:et:ts=2:sw=2
Index: frontend/app/controllers/wizard_controller.rb
===================================================================
--- frontend/app/controllers/wizard_controller.rb (revision 0)
+++ frontend/app/controllers/wizard_controller.rb (revision 0)
@@ -0,0 +1,135 @@
+class WizardController < ApplicationController
+
+ # GET/POST /source/<project>/<package>/_wizard
+ def package_wizard
+ prj_name = params[:project]
+ pkg_name = params[:package]
+ pkg = DbPackage.find_by_project_and_name(prj_name, pkg_name)
+ unless pkg
+ render_error :status => 404, :errorcode => "unknown_package",
+ :message => "unknown package '#{pkg_name}' in project '#{prj_name}'"
+ return
+ end
+ if not @http_user.can_modify_package?(pkg)
+ render_error :status => 403, :errorcode =>
"change_package_no_permission",
+ :message => "no permission to change package"
+ return
+ end
+
+ logger.debug("package_wizard, #{params.inspect}")
+
+ wizard_xml = "/source/#{prj_name}/#{pkg_name}/wizard.xml"
+ begin
+ @wizard_state = WizardState.new(backend_get(wizard_xml))
+ rescue ActiveXML::Transport::NotFoundError
+ @wizard_state = WizardState.new("")
+ end
+ @wizard_state.store("name", pkg_name)
+ wizard_step_tarball
+ if @wizard_state.dirty
+ backend_put(wizard_xml, @wizard_state.serialize)
+ end
+ end
+
+ private
+
+ @@wizard_entries = {
+ # name => [type, description, legend]
+ "tarball" => ["file", "Source tarball to upload", ""],
+ "version" => ["text", "Version of the package", "Note that the version
must not contain dashes (-)"],
+ "summary" => ["text", "Short summary of the package", ""],
+ "description" => ["longtext", "Describe your package", ""],
+ "license" => ["text", "License of the package", ""],
+ "group" => ["text", "Package group", "See
http://en.opensuse.org/SUSE_Package_Conventions/RPM_Groups";],
+ # autogenerated
+ "name" => [],
+ "date" => [],
+ "email" => [],
+ }
+
+ def wizard_add_entry(name)
+ @wizard_form.add_entry(name,
+ @@wizard_entries[name][0],
+ @@wizard_entries[name][1],
+ @@wizard_entries[name][2],
+ @wizard_state.get(name))
+ end
+
+ def wizard_step_tarball
+ if params[:tarball] && ! params[:tarball].empty?
+ filename = params[:tarball]
+ # heuristics
+ @wizard_state.store("tarball", filename)
+ if filename =~ /^#{params[:package]}-(.*)\.tar\.(gz|bz2)$/i
+ @wizard_state.store_guess("version", $1)
+ end
+ @wizard_state.store_guess("license", "GPL v2 or later")
+ # TODO: unpack the tarball somehow and try to guess as much as
possible...
+ end
+ if @wizard_state.get_data("tarball")
+ wizard_step_meta
+ return
+ end
+ @wizard_form = WizardForm.new("Step 1/2", "What do you want to package?")
+ @wizard_form.add_entry("tarball", "file", "Source tarball to upload")
+ render :template => "wizard", :status => 200;
+ end
+
+ def wizard_step_meta
+ have_all = true
+ ["version", "summary", "description", "license", "group"].each do |entry|
+ if params[entry] && ! params[entry].empty?
+ @wizard_state.store(entry, params[entry])
+ end
+ if ! @wizard_state.get_data(entry)
+ have_all = false
+ end
+ end
+ if have_all
+ wizard_step_finish
+ return
+ end
+ @wizard_form = WizardForm.new("Step 2/2", "Please describe your package")
+ wizard_add_entry("summary")
+ wizard_add_entry("description")
+ wizard_add_entry("version")
+ wizard_add_entry("license")
+ wizard_add_entry("group")
+ render :template => "wizard", :status => 200
+ end
+
+ def wizard_step_finish
+ if @wizard_state.get_data("created_spec") == "true"
+ wizard_step_done
+ return
+ end
+ package = Package.find(params[:package], :project => params[:project])
+ # FIXME: is there a cleaner way to do it?
+ package.data.elements["title"].text = @wizard_state.get_data("summary")
+ package.data.elements["description"].text =
@wizard_state.get_data("description")
+ package.save
+ specfile = "#{params[:package]}.spec"
+ template = File.read("#{RAILS_ROOT}/files/wizardtemplate.spec")
+ @wizard_state.store("date", Date.today.strftime("%a %b %d %Y"))
+ @wizard_state.store("email", @http_user.email)
+ @@wizard_entries.each_key do |entry|
+ template.gsub!(/@#{entry}@/, @wizard_state.get_data(entry) || "UNSET")
+ end
+ backend_put("/source/#{params[:project]}/#{params[:package]}/#{specfile}",
template)
+ @wizard_state.store("created_spec", "true")
+ @wizard_form = WizardForm.new("Finished",
+ "I created #{specfile} for you")
+ @wizard_form.last = true
+ render :template => "wizard", :status => 200
+ end
+
+ def wizard_step_done
+ @wizard_form = WizardForm.new("Nothing to do",
+ "There is nothing I can do for you now. In the future, I will be able to
help you updating your package or fixing build errors")
+ @wizard_form.last = true
+ render :template => "wizard", :status => 200
+ end
+
+end
+
+# vim:et:ts=2:sw=2
Index: frontend/app/views/wizard.rxml
===================================================================
--- frontend/app/views/wizard.rxml (revision 0)
+++ frontend/app/views/wizard.rxml (revision 0)
@@ -0,0 +1,14 @@
+xml.instruct!
+xml.wizard("last" => @wizard_form.last ? "true" : "false") do
+ xml.label(@wizard_form.label)
+ xml.legend(@wizard_form.legend)
+ @wizard_form.entries.each do |entry|
+ xml.entry("name" => entry.name, "type" => entry.type) do
+ xml.label(entry.label)
+ xml.legend(entry.legend)
+ xml.value(entry.value)
+ end
+ end
+end
+
+# vim:et:ts=2:sw=2
Index: frontend/files/wizardtemplate.spec
===================================================================
--- frontend/files/wizardtemplate.spec (revision 0)
+++ frontend/files/wizardtemplate.spec (revision 0)
@@ -0,0 +1,34 @@
+Name: @name@
+Version: @version@
+Release: 1
+License: GPL
+Source: @tarball@
+Group: @group@
+Summary: @summary@
+BuildRoot: %{_tmppath}/%{name}-%{version}-build
+
+%description
+@description@
+
+%prep
+%setup
+
+%build
+%configure
+make
+
+%install
+make DESTDIR=%buildroot install
+
+echo '%%defattr(-,root,root)' >filelist
+find %buildroot -type f -printf '/%%P*\n' >>filelist
+
+%clean
+rm -rf %buildroot
+
+%files -f filelist
+%defattr(-,root,root)
+
+%changelog
+* @date@ @email@
+- packaged @name@ version @version@ using the buildservice spec file wizard
Index: frontend/config/routes.rb
===================================================================
--- frontend/config/routes.rb (revision 4060)
+++ frontend/config/routes.rb (working copy)
@@ -48,6 +48,8 @@
:action => 'package_meta'
map.connect 'source/:project/:package/_tags', :controller => 'tag',
:action => 'package_tags'
+ map.connect 'source/:project/:package/_wizard', :controller => 'wizard',
+ :action => 'package_wizard'
map.connect 'source/:project/:package/:file', :controller => "source",
:action => 'file'
map.connect 'source/:project/_pattern', :controller => 'source',
Index: frontend/config/environments/development_base.rb
===================================================================
Index: common/lib/activexml/transport.rb
===================================================================
--- common/lib/activexml/transport.rb (revision 4060)
+++ common/lib/activexml/transport.rb (working copy)
@@ -443,6 +443,7 @@
#new substitution rules:
#when param is not there, don't put anything in url
#when param is array, put multiple params in url
+ #when param is a hash, put key=value params in url
#any other case, stringify param and put it in url
next if not params.has_key? $1.to_sym or params[$1.to_sym].nil?
sub_val = params[$1.to_sym]
@@ -450,6 +451,10 @@
sub_val.each do |val|
new_pairs << $1 + "=" + CGI.escape(val)
end
+ elsif sub_val.kind_of? Hash
+ sub_val.each_key do |key|
+ new_pairs << CGI.escape(key) + "=" + CGI.escape(sub_val[key])
+ end
else
new_pairs << $1 + "=" + CGI.escape(sub_val.to_s)
end
Index: webclient/app/models/wizard.rb
===================================================================
--- webclient/app/models/wizard.rb (revision 0)
+++ webclient/app/models/wizard.rb (revision 0)
@@ -0,0 +1,3 @@
+class Wizard < ActiveXML::Base
+ handles_xml_element 'wizard'
+end
Index: webclient/app/controllers/package_controller.rb
===================================================================
--- webclient/app/controllers/package_controller.rb (revision 4060)
+++ webclient/app/controllers/package_controller.rb (working copy)
@@ -157,6 +157,54 @@
@project = Project.find( params[:project] )
end

+ def wizard_new
+ @project = Project.find( params[:project] )
+ if params[:name]
+ if !valid_package_name? params[:name]
+ flash[:error] = "Invalid package name: '#{params[:name]}'"
+ redirect_to :action => 'wizard_new', :project => params[:project]
+ else
+ @package = Package.new( :name => params[:name], :project => @project )
+ if @package.save
+ redirect_to :action => 'wizard', :project => params[:project],
:package => params[:name]
+ else
+ flash[:note] = "Failed to save package '#{@package}'"
+ redirect_to :controller => 'project', :action => 'show', :project =>
params[:project]
+ end
+ end
+ end
+ end
+
+ def wizard
+ @project = Project.find( params[:project] )
+ @package = Package.find( params[:package], :project => params[:project] )
+ files = params[:wizard_files]
+ fnames = {}
+ if files
+ logger.debug "files: #{files.inspect}"
+ files.each_key do |key|
+ file = files[key]
+ next if ! file.respond_to?(:original_filename)
+ fname = file.original_filename
+ fnames[key] = fname
+ # TODO: reuse code from PackageController#save_file and add_file.rhtml
+ # to also support fetching remote urls
+ @package.save_file :file => file, :filename => fname
+ end
+ end
+ other = params[:wizard]
+ if other
+ response = other.merge(fnames)
+ elsif ! fnames.empty?
+ response = fnames
+ else
+ response = nil
+ end
+ @wizard = Wizard.find(:project => params[:project],
+ :package => params[:package],
+ :response => response)
+ end
+
def edit
@project = Project.find( params[:project] )
@package = Package.find( params[:package], :project => params[:project] )
@@ -801,3 +849,5 @@


end
+
+# vim:et:ts=2:sw=2
Index: webclient/app/views/package/wizard_new.rhtml
===================================================================
--- webclient/app/views/package/wizard_new.rhtml (revision 0)
+++ webclient/app/views/package/wizard_new.rhtml (revision 0)
@@ -0,0 +1,16 @@
+<% @pagetitle = "New Package" %>
+<%
+@crumb_list = [
+ link_to( 'Projects', :controller => 'project', :action => :list_public),
+ link_to( @project, :controller => 'project', :action => :show, :project =>
@project ),
+ 'Add Package'
+]
+-%>
+<h2>New Package in project <%= @project.name %></h2>
+
+<% form_tag :action => "wizard_new" do %>
+<b>Name:</b><br/>
+<%= text_field_tag 'name', '', :size => 80 %><br/>
+<p><%= submit_tag 'Next' %></p>
+<%= hidden_field_tag 'project', @project.name %>
+<% end %>
Index: webclient/app/views/package/new.rhtml
===================================================================
--- webclient/app/views/package/new.rhtml (revision 4060)
+++ webclient/app/views/package/new.rhtml (working copy)
@@ -6,6 +6,7 @@
'Add Package'
]
-%>
+<p><%= link_to('Create package using spec file wizard', :controller =>
'package', :action => 'wizard_new', :project => @project) %> (experimental)</p>
<h2>New Package in project <%= @project.name %></h2>

<% form_tag :action => "save_new" do %>
Index: webclient/app/views/package/wizard.rhtml
===================================================================
--- webclient/app/views/package/wizard.rhtml (revision 0)
+++ webclient/app/views/package/wizard.rhtml (revision 0)
@@ -0,0 +1,37 @@
+<% @pagetitle = "New Package" %>
+
+<h2><%= @wizard.label %></h2>
+<p><%= @wizard.legend %></p>
+<%
+ if @wizard.last == "true"
+ action = "show"
+ submit = "Finish"
+ else
+ action = "wizard"
+ submit = "Next"
+ end
+%>
+<% form_tag({:action => action}, {:multipart => true}) do %>
+ <% @wizard.each_entry do |entry| %>
+ <p>
+ <b><label for="wizard[<%= entry.name %>]"><%= entry.label
%></label></b><br/>
+ <%= typ3 = entry.data.attributes["type"] # entry.type would call Oject#type
+ case typ3
+ when "text":
+ text_field_tag("wizard[#{entry.name}]", entry.value)
+ when "longtext":
+ text_area_tag("wizard[#{entry.name}]", entry.value)
+ when "file":
+ file_field_tag("wizard_files[#{entry.name}]")
+ else
+ raise RuntimeError.new("WizardError: unknown entry type #{typ3}")
+ end
+ %>
+ <br/>
+ <i><%= entry.legend %></i>
+ </p>
+ <% end %>
+ <%= submit_tag(submit) %>
+ <%= hidden_field_tag 'project', @project.name %>
+ <%= hidden_field_tag 'package', @package.name %>
+<% end %>
Index: webclient/config/environment.rb
===================================================================
--- webclient/config/environment.rb (revision 4060)
+++ webclient/config/environment.rb (working copy)
@@ -102,6 +102,8 @@

map.connect :architecture, "rest:///architecture"

+ map.connect :wizard, "rest:///source/:project/:package/_wizard?:response"
+
##DEPRECATED
map.connect :platform, "rest:///platform/:project/:name",
:all => "rest:///platform/"
Index: webclient/config/environments/development_base.rb
===================================================================
< Previous Next >
This Thread
Follow Ups