diff options
32 files changed, 563 insertions, 6 deletions
diff --git a/app/assets/images/download-small.png b/app/assets/images/download-small.png Binary files differnew file mode 100644 index 000000000..6ffb8e642 --- /dev/null +++ b/app/assets/images/download-small.png diff --git a/app/assets/images/export-completed.png b/app/assets/images/export-completed.png Binary files differnew file mode 100644 index 000000000..a76d76b13 --- /dev/null +++ b/app/assets/images/export-completed.png diff --git a/app/assets/images/export-failed.png b/app/assets/images/export-failed.png Binary files differnew file mode 100644 index 000000000..f13b19234 --- /dev/null +++ b/app/assets/images/export-failed.png diff --git a/app/assets/images/export-pending.png b/app/assets/images/export-pending.png Binary files differnew file mode 100644 index 000000000..1e50e36ac --- /dev/null +++ b/app/assets/images/export-pending.png diff --git a/app/assets/javascripts/exports.js.coffee b/app/assets/javascripts/exports.js.coffee new file mode 100644 index 000000000..761567942 --- /dev/null +++ b/app/assets/javascripts/exports.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/stylesheets/common.css.scss b/app/assets/stylesheets/common.css.scss index b0655b5e0..9f9ce3346 100644 --- a/app/assets/stylesheets/common.css.scss +++ b/app/assets/stylesheets/common.css.scss @@ -49,6 +49,9 @@ a.remove { background: url(image-path('user_interface/ui/remove-small.png')) no-repeat 0% 50%; } + a.download { + background: url(image-path('download-small.png')) no-repeat 0% 50%; + } } p { diff --git a/app/assets/stylesheets/exports.css.scss b/app/assets/stylesheets/exports.css.scss new file mode 100644 index 000000000..cb8f23dc4 --- /dev/null +++ b/app/assets/stylesheets/exports.css.scss @@ -0,0 +1,41 @@ +// Place all the styles related to the Exports controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ + +@import "common"; + +#workspace.exports.index +{ + .export:after { + @include after_div_for_object; + } + + .exports { + margin-top: 20px; + } + + .exports:after { + @include content_to_clear; + } + + .export { + @include div_for_object; + + /* to create multi-column index */ + width: 300px; + float: left; + padding-right: 10px; + } +} + +#workspace.exports.show { + table { + th { + text-align: center; + font-style: italic; + } + td.message, td.created_at, td.position { + padding: 0 20px; + } + } +} diff --git a/app/controllers/exports_controller.rb b/app/controllers/exports_controller.rb new file mode 100644 index 000000000..6a77f7f45 --- /dev/null +++ b/app/controllers/exports_controller.rb @@ -0,0 +1,34 @@ +class ExportsController < ChouetteController + + respond_to :html, :xml, :json + respond_to :zip, :only => :show + + belongs_to :referential + + def create + create! do |success, failure| + success.html { redirect_to referential_exports_path(@referential) } + end + end + + def show + show! do |format| + format.zip { send_file @export.file, :type => :zip } + end + end + + + protected + + # FIXME why #resource_id is nil ?? + def build_resource + super.tap do |export| + export.referential_id = @referential.id + end + end + + def collection + @exports ||= end_of_association_chain.paginate(:page => params[:page]) + end + +end diff --git a/app/helpers/exports_helper.rb b/app/helpers/exports_helper.rb new file mode 100644 index 000000000..bd84b9886 --- /dev/null +++ b/app/helpers/exports_helper.rb @@ -0,0 +1,2 @@ +module ExportsHelper +end diff --git a/app/models/export.rb b/app/models/export.rb new file mode 100644 index 000000000..a202e77ef --- /dev/null +++ b/app/models/export.rb @@ -0,0 +1,84 @@ +class Export < ActiveRecord::Base + + belongs_to :referential + validates_presence_of :referential_id + + validates_inclusion_of :status, :in => %w{ pending completed failed } + + has_many :log_messages, :class_name => "ExportLogMessage", :order => :position, :dependent => :destroy + + serialize :options + + def self.option(name) + name = name.to_s + + define_method(name) do + self.options[name] + end + + define_method("#{name}=") do |prefix| + self.options[name] = prefix + end + end + + def options + read_attribute(:options) || write_attribute(:options, {}) + end + + @@root = "#{Rails.root}/tmp/exports" + cattr_accessor :root + + after_destroy :destroy_file + def destroy_file + FileUtils.rm file if File.exists? file + end + + def file + "#{root}/#{id}.zip" + end + + def name + "#{self.class.model_name.human} #{id}" + end + + def export_options + { :export_id => self.id, :output_file => file } + end + + before_validation :define_default_attributes, :on => :create + def define_default_attributes + self.status ||= "pending" + end + + after_create :delayed_export + def delayed_export + delay.export + end + + def export + FileUtils.mkdir_p root + + begin + log_messages.create :key => :started + + # TODO + # Make real export here + + update_attribute :status, "completed" + rescue => e + Rails.logger.error "Export #{id} failed : #{e}, #{e.backtrace}" + update_attribute :status, "failed" + end + + log_messages.create :key => status + end + + def self.new(attributes = {}, options = {}, &block) + if self == Export + Object.const_get(attributes.delete(:type) || "NeptuneExport").new(attributes, options) + else + super + end + end + +end diff --git a/app/models/export_log_message.rb b/app/models/export_log_message.rb new file mode 100644 index 000000000..61068689c --- /dev/null +++ b/app/models/export_log_message.rb @@ -0,0 +1,36 @@ +class ExportLogMessage < ActiveRecord::Base + belongs_to :export + + acts_as_list :scope => :export + + validates_presence_of :key + validates_inclusion_of :severity, :in => %w{info warning error} + + def arguments=(arguments) + write_attribute :arguments, (arguments.to_json if arguments.present?) + end + + def arguments + @decoded_arguments ||= + begin + if (stored_arguments = raw_attributes).present? + ActiveSupport::JSON.decode stored_arguments + else + {} + end + end + end + + def raw_attributes + read_attribute(:arguments) + end + + before_validation :define_default_attributes, :on => :create + def define_default_attributes + self.severity ||= "info" + end + + def full_message + I18n.translate key, arguments.symbolize_keys.merge(:scope => "export_log_messages.messages") + end +end diff --git a/app/models/neptune_export.rb b/app/models/neptune_export.rb new file mode 100644 index 000000000..fd429101c --- /dev/null +++ b/app/models/neptune_export.rb @@ -0,0 +1,7 @@ +class NeptuneExport < Export + + def export_options + super.merge(:format => :neptune) + end + +end diff --git a/app/models/referential.rb b/app/models/referential.rb index 2d6d5b8d5..3f4a62bb6 100644 --- a/app/models/referential.rb +++ b/app/models/referential.rb @@ -9,6 +9,7 @@ class Referential < ActiveRecord::Base validates_format_of :prefix, :with => %r{\A[0-9a-zA-Z_]+\Z} has_many :imports, :dependent => :destroy + has_many :exports, :dependent => :destroy def human_attribute_name(*args) self.class.human_attribute_name(*args) diff --git a/app/views/exports/_export.erb b/app/views/exports/_export.erb new file mode 100644 index 000000000..c5412fbc5 --- /dev/null +++ b/app/views/exports/_export.erb @@ -0,0 +1,14 @@ +<%= div_for(export, :class => :export) do %> + <%= link_to(referential_export_path(@referential, export), :class => "preview") do %> + <%= image_tag "export-#{export.status}.png" %> + <% end %> + <%= link_to(export.name, referential_export_path(@referential, export)) %> + <div class="info"> + <%= l export.created_at %> + + <div class="actions"> + <%= link_to t("exports.actions.download"), referential_export_path(@referential, export, :format => :zip), :class => "download" %> + <%= link_to t("actions.destroy"), referential_export_path(@referential, export), :method => :delete, :confirm => t('exports.actions.destroy_confirm'), :class => "remove" %> + </div> + </div> +<% end %> diff --git a/app/views/exports/index.html.erb b/app/views/exports/index.html.erb new file mode 100644 index 000000000..82803d725 --- /dev/null +++ b/app/views/exports/index.html.erb @@ -0,0 +1,20 @@ +<%= title_tag t('.title') %> + +<div class="pagination"> + <div class="page_info"> + <%= page_entries_info @exports %> + </div> + <%= will_paginate @exports, :container => false %> +</div> +<div class="exports paginated_content"> + <%= render :partial => "export", :collection => @exports %> +</div> +<div class="pagination"> + <%= will_paginate @exports, :container => false %> +</div> + +<% content_for :sidebar do %> +<ul class="actions"> + <li><%= link_to t('exports.actions.new'), new_referential_export_path(@referential), :class => "add" %></li> +</ul> +<% end %> diff --git a/app/views/exports/new.html.erb b/app/views/exports/new.html.erb new file mode 100644 index 000000000..24022e0f8 --- /dev/null +++ b/app/views/exports/new.html.erb @@ -0,0 +1,9 @@ +<%= title_tag t(".title") %> + +<%= semantic_form_for [@referential, @export], :as => :export, :url => referential_exports_path(@referential) do |form| %> + <%= form.buttons do %> + <%= form.commit_button true %> + <li><%= t('or') %></li> + <li><%= link_to t('cancel'), :back %></li> + <% end %> +<% end %> diff --git a/app/views/exports/show.html.erb b/app/views/exports/show.html.erb new file mode 100644 index 000000000..c3248b47a --- /dev/null +++ b/app/views/exports/show.html.erb @@ -0,0 +1,39 @@ +<%= title_tag @export.name %> + +<div class="export_show"> + <div class="summary"> + <p> + <label><%= Export.human_attribute_name(:created_at) %>: </label> + <%= l @export.created_at %> + </p> + <p> + <label><%= Export.human_attribute_name(:status) %>: </label> + <%= t @export.status, :scope => "exports.statuses" %> + </p> + </div> + + <h3><%= t(".report") %></h3> + <table> + <tr> + <th></th> + <th><%= ExportLogMessage.human_attribute_name(:created_at) %></th> + <th><%= ExportLogMessage.human_attribute_name(:position) %></th> + <th><%= ExportLogMessage.human_attribute_name(:full_message) %></th> + </tr> + <% @export.log_messages.each do |message| %> + <tr> + <td class="severity"><%= image_tag "severity-#{message.severity}.png", :alt => t(message.severity, :scope => "export_log_messages.severities") %></td> + <td class="created_at"><%= l message.created_at, :format => :short %></td> + <td class="position"><%= message.position %></td> + <td class="message"><%= message.full_message %></td> + </tr> + <% end %> + </table> +</div> + +<% content_for :sidebar do %> +<ul class="actions"> + <li><%= link_to t('exports.actions.download'), referential_export_path(@referential, @export, :format => :zip), :class => "download" %></li> + <li><%= link_to t('exports.actions.destroy'), referential_export_path(@referential, @export), :method => :delete, :confirm => t('exports.actions.destroy_confirm'), :class => "remove" %></li> +</ul> +<% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 4eb3b1b70..437aad3a7 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -45,6 +45,7 @@ <li><%= tab_link_to Chouette::TimeTable, referential_time_tables_path(@referential) %></li> <li><%= tab_link_to Import, referential_imports_path(@referential) %></li> + <li><%= tab_link_to Export, referential_exports_path(@referential) %></li> <% end %> </ul> </div> diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb index 20263f5c6..4d9b11b3c 100644 --- a/config/initializers/apartment.rb +++ b/config/initializers/apartment.rb @@ -1,6 +1,6 @@ Apartment.configure do |config| # set your options (described below) here - config.excluded_models = ["Referential", "User", "Import", "ImportLogMessage", "Delayed::Backend::ActiveRecord::Job"] # these models will not be multi-tenanted, but remain in the global (public) namespace + config.excluded_models = ["Referential", "User", "Import", "ImportLogMessage", "Export", "ExportLogMessage", "Delayed::Backend::ActiveRecord::Job"] # these models will not be multi-tenanted, but remain in the global (public) namespace # Dynamically get database names to migrate config.database_names = lambda{ Referential.select(:slug).map(&:slug) } diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 72aca7e44..b93a3824f 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -3,3 +3,5 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf # Mime::Type.register_alias "text/html", :iphone + + Mime::Type.register "application/zip", :zip diff --git a/config/locales/exports.yml b/config/locales/exports.yml new file mode 100644 index 000000000..74b7cd387 --- /dev/null +++ b/config/locales/exports.yml @@ -0,0 +1,79 @@ +en: + exports: + actions: + new: New export + destroy: Destroy + destroy_confirm: Are you sure you want destroy this export? + download: Download + new: + title: New export + index: + title: Exports + show: + report: Report + statuses: + pending: Pending + completed: Completed + failed: Failed + export_log_messages: + messages: + started: Started export + completed: Completed export + failed: Failed export + severities: + info: Information + warning: Warning + error: Error + activerecord: + models: + export: + zero: export + one: export + other: exports + attributes: + export: + status: Status + export_log_message: + created_at: Date + position: N. + full_message: Message +fr: + exports: + actions: + new: Nouvel export + destroy: Supprimer cet export + destroy_confirm: Etes vous sûr de détruire cet export ? + download: Télécharger + new: + title: Nouvel export + index: + title: Exports + show: + report: Rapport + statuses: + pending: En cours + completed: Achevé + failed: Echoué + export_log_messages: + messages: + started: Export démarré + completed: Export achevé avec succès + failed: Export interromptu + severities: + info: Information + warning: Alerte + error: Erreur + activerecord: + models: + export: Export + neptune_export: + zero: Export Neptune + one: Export Neptune + other: Exports Neptune + attributes: + export: + status: Status + export_log_message: + created_at: Date + position: "No" + full_message: Message diff --git a/config/routes.rb b/config/routes.rb index 5e5f68b72..c4686852b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,8 +1,4 @@ ChouetteIhm::Application.routes.draw do - get "imports/new" - - get "imports/index" - devise_for :users # The priority is based upon order of creation: @@ -42,6 +38,7 @@ ChouetteIhm::Application.routes.draw do end resources :imports + resources :exports resources :companies, :stop_areas diff --git a/db/migrate/20120607064150_create_exports.rb b/db/migrate/20120607064150_create_exports.rb new file mode 100644 index 000000000..6dda4948e --- /dev/null +++ b/db/migrate/20120607064150_create_exports.rb @@ -0,0 +1,13 @@ +class CreateExports < ActiveRecord::Migration + def change + create_table :exports do |t| + t.belongs_to :referential + t.string :status + t.string :type + t.string :options + + t.timestamps + end + add_index :exports, :referential_id + end +end diff --git a/db/migrate/20120607064625_create_export_log_messages.rb b/db/migrate/20120607064625_create_export_log_messages.rb new file mode 100644 index 000000000..e4ad03f98 --- /dev/null +++ b/db/migrate/20120607064625_create_export_log_messages.rb @@ -0,0 +1,14 @@ +class CreateExportLogMessages < ActiveRecord::Migration + def change + create_table :export_log_messages do |t| + t.belongs_to :export + t.string :key + t.string :arguments + t.integer :position + t.string :severity + + t.timestamps + end + add_index :export_log_messages, :export_id + end +end diff --git a/db/schema.rb b/db/schema.rb index d4c07cdc1..536cdc7d2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20120531091529) do +ActiveRecord::Schema.define(:version => 20120607064625) do create_table "access_links", :force => true do |t| t.integer "access_point_id", :limit => 8 @@ -120,6 +120,29 @@ ActiveRecord::Schema.define(:version => 20120531091529) do add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority" + create_table "export_log_messages", :force => true do |t| + t.integer "export_id" + t.string "key" + t.string "arguments" + t.integer "position" + t.string "severity" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "export_log_messages", ["export_id"], :name => "index_export_log_messages_on_export_id" + + create_table "exports", :force => true do |t| + t.integer "referential_id" + t.string "status" + t.string "type" + t.string "options" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "exports", ["referential_id"], :name => "index_exports_on_referential_id" + create_table "facilities", :force => true do |t| t.integer "stop_area_id", :limit => 8 t.integer "line_id", :limit => 8 diff --git a/spec/controllers/exports_controller_spec.rb b/spec/controllers/exports_controller_spec.rb new file mode 100644 index 000000000..e7a49a8e8 --- /dev/null +++ b/spec/controllers/exports_controller_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe ImportsController do + login_user + + describe "GET 'new'" do + it "returns http success" do + pending + get 'new' + response.should be_success + end + end + + describe "GET 'index'" do + it "returns http success" do + pending + get 'index' + response.should be_success + end + end + +end diff --git a/spec/factories.rb b/spec/factories.rb index c36f1c71a..fdd23eded 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -23,4 +23,13 @@ FactoryGirl.define do f.sequence(:key) { "key_#{n}" } end + factory :export do |f| + f.association :referential + end + + factory :export_log_message do |f| + f.association :export + f.sequence(:key) { "key_#{n}" } + end + end diff --git a/spec/helpers/exports_helper_spec.rb b/spec/helpers/exports_helper_spec.rb new file mode 100644 index 000000000..33257b1d1 --- /dev/null +++ b/spec/helpers/exports_helper_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +# Specs in this file have access to a helper object that includes +# the ExportsHelper. For example: +# +# describe ExportsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# helper.concat_strings("this","that").should == "this that" +# end +# end +# end +describe ExportsHelper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/export_log_message_spec.rb b/spec/models/export_log_message_spec.rb new file mode 100644 index 000000000..8aa3cde6e --- /dev/null +++ b/spec/models/export_log_message_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe ExportLogMessage do + + describe "#attributes" do + + subject { create :export_log_message } + + it "should read json stored in database" do + subject.update_attribute :arguments, { "key" => "value"} + subject.raw_attributes.should == { "key" => "value"}.to_json + end + + end + +end diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb new file mode 100644 index 000000000..bccaa4c42 --- /dev/null +++ b/spec/models/export_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Export do + + subject { create :export } + + RSpec::Matchers.define :be_log_message do |expected| + match do |actual| + actual and expected.all? { |k,v| actual[k.to_s] == v } + end + end + + describe "#export" do + + before(:each) do + subject.stub :exporter => mock(:export => true) + end + + it "should create a ExportLogmessage :started when started" do + subject.export + subject.log_messages.first.should be_log_message(:key => "started") + end + + it "should create a ExportLogmessage :completed when completed" do + subject.export + subject.log_messages.last.should be_log_message(:key => "completed") + end + + it "should create a ExportLogmessage :failed when failed" do + pending + # subject.loader.stub(:export).and_raise("export failed") + subject.export + subject.log_messages.last.should be_log_message(:key => "failed") + end + + end + + describe "#options" do + + it "should be empty by default" do + subject.options.should be_empty + end + + end + + describe ".types" do + + it "should return available Export implementations" do + pending + Export.types.should =~ %w{NeptuneExport} + end + + end + + describe ".new" do + + it "should use type attribute to create a subclass" do + Export.new(:type => "NeptuneExport").should be_an_instance_of(NeptuneExport) + end + + end + +end diff --git a/spec/views/exports/index.html.erb_spec.rb b/spec/views/exports/index.html.erb_spec.rb new file mode 100644 index 000000000..781b5787b --- /dev/null +++ b/spec/views/exports/index.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe "exports/index.html.erb" do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/exports/new.html.erb_spec.rb b/spec/views/exports/new.html.erb_spec.rb new file mode 100644 index 000000000..d4a3b255a --- /dev/null +++ b/spec/views/exports/new.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe "exports/new.html.erb" do + pending "add some examples to (or delete) #{__FILE__}" +end |
