para 0.6.2 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/para/admin/{importers.coffee → job-tracker.coffee} +10 -9
  3. data/app/controllers/para/admin/crud_resources_controller.rb +0 -14
  4. data/app/controllers/para/admin/exports_controller.rb +33 -0
  5. data/app/controllers/para/admin/imports_controller.rb +2 -21
  6. data/app/controllers/para/admin/jobs_controller.rb +66 -0
  7. data/app/helpers/para/admin/base_helper.rb +9 -8
  8. data/app/helpers/para/application_helper.rb +1 -1
  9. data/app/helpers/para/translations_helper.rb +25 -0
  10. data/app/views/admin/para/exporter/bases/_completed.html.haml +11 -0
  11. data/app/views/para/admin/{imports → jobs}/_completed.html.haml +3 -3
  12. data/app/views/para/admin/{imports → jobs}/_failed.html.haml +1 -1
  13. data/app/views/para/admin/jobs/_progress.html.haml +5 -0
  14. data/app/views/para/admin/jobs/show.html.haml +10 -0
  15. data/app/views/para/admin/resources/_exports_menu.html.haml +15 -8
  16. data/app/views/para/admin/resources/_imports_menu.html.haml +2 -2
  17. data/config/locales/fr.yml +19 -10
  18. data/lib/para/component/exportable.rb +10 -12
  19. data/lib/para/exporter/base.rb +27 -25
  20. data/lib/para/exporter/csv.rb +10 -15
  21. data/lib/para/exporter.rb +0 -50
  22. data/lib/para/importer/base.rb +7 -10
  23. data/lib/para/job/base.rb +53 -0
  24. data/lib/para/job.rb +8 -0
  25. data/lib/para/version.rb +1 -1
  26. data/lib/para.rb +1 -0
  27. data/lib/rails/routing_mapper.rb +5 -1
  28. data/lib/tasks/para_tasks.rake +3 -3
  29. metadata +14 -9
  30. data/app/helpers/para/exports_helper.rb +0 -11
  31. data/app/views/para/admin/imports/_progress.html.haml +0 -5
  32. data/app/views/para/admin/imports/show.html.haml +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 194f93e32421619179ac7bbe6ecafbca1956e001
4
- data.tar.gz: 61742c2210cd54cb7081657d87bd811ee543b5cc
3
+ metadata.gz: f79b2e1b6163469c9247f53880b82b50527729e2
4
+ data.tar.gz: 46444b157efef7ace8f76a566dbcca7f90b62e1d
5
5
  SHA512:
6
- metadata.gz: 2ef54e425e19c33d6f202c6cf0615393027be98198428d53b8c656ada2d3bfc76c739b721ce8b953a9062cbd207feef849e6b6bc33ff835171135af44b60ef35
7
- data.tar.gz: 2940b7949484eb38d9c0006da8b3722383767a19373eeab8bce0cc2b41a66a499edcdc0f9a38215c0d6b9492ca2f6902dc9f1733f96b85b75889408f840aabe0
6
+ metadata.gz: 008274f36acb1819e3ac4b37d74eb115806c7c86d8a5e5f409e98bc568c788ce31e8e4019a412eeb1b86f11025805e7f2f47b9bb609f3e5a4994bebd733e176c
7
+ data.tar.gz: a08c53ae52881b5ce1d80a2bb951f089db476824802adea8b2e006165164063512b0f2bc660eaa309cd692a8b9a7ea2aee05a740dc81355e6628fbe9c04d453e
@@ -1,23 +1,24 @@
1
- class Para.Importer extends RemoteModalForm
1
+ class Para.JobTracker extends RemoteModalForm
2
2
  initialize: (options = {}) ->
3
3
  super(options)
4
4
  @refreshOnClose = false
5
+ @trackProgress()
5
6
 
6
7
  formSuccess: (e, response) ->
7
8
  super(e, response)
8
-
9
- if ($progressBar = @$el.find('[data-async-progress]')).length
10
- @trackProgress($progressBar)
9
+ @trackProgress()
11
10
 
12
11
  trackProgress: ($progressBar) ->
13
- @importStatusURL = @$el.data('import-status-url')
14
- @progress = new Para.AsyncProgress(el: $progressBar, progressUrl: @importStatusURL)
12
+ return unless ($progressBar = @$el.find('[data-async-progress]')).length
13
+
14
+ @jobStatusURL = @$el.data('job-status-url')
15
+ @progress = new Para.AsyncProgress(el: $progressBar, progressUrl: @jobStatusURL)
15
16
  @listenTo(@progress, 'completed', @onImportComplete)
16
17
  @listenTo(@progress, 'failed', @onImportComplete)
17
18
 
18
19
  onImportComplete: ->
19
20
  $.ajax(
20
- url: @importStatusURL
21
+ url: @jobStatusURL
21
22
  # Force HTTP ACCEPT header to HTML since Rails treats XHR request without
22
23
  # a specific ACCEPT header as JS or JSON by defaut.
23
24
  accepts:
@@ -34,5 +35,5 @@ class Para.Importer extends RemoteModalForm
34
35
  @progress?.stop()
35
36
 
36
37
  $(document).on 'page:change turbolinks:load', ->
37
- $('body').on 'ajax:success', '[data-importer-button]', (e, response) ->
38
- new Para.Importer(modalMarkup: response, $link: $(e.currentTarget))
38
+ $('body').on 'ajax:success', '[data-job-tracker-button]', (e, response) ->
39
+ new Para.JobTracker(modalMarkup: response, $link: $(e.currentTarget))
@@ -48,20 +48,6 @@ module Para
48
48
  end
49
49
  end
50
50
 
51
- def export
52
- if @component.exportable?
53
- exporter = Para::Exporter.for(
54
- resource_model.name, params[:format]
55
- ).new(@component.resources.search(params[:q]).result.uniq)
56
-
57
- send_data exporter.render, type: exporter.mime_type,
58
- disposition: exporter.disposition,
59
- filename: exporter.file_name
60
- else
61
- redirect_to @component.path
62
- end
63
- end
64
-
65
51
  private
66
52
 
67
53
  def resource_model
@@ -0,0 +1,33 @@
1
+ module Para
2
+ module Admin
3
+ class ExportsController < Para::Admin::JobsController
4
+ layout false
5
+
6
+ before_action :load_exporter
7
+
8
+ def create
9
+ job = @exporter.perform_later(
10
+ model_name: @component.model.name,
11
+ search: params[:q]
12
+ )
13
+
14
+ track_job(job)
15
+ end
16
+
17
+ private
18
+
19
+ def load_exporter
20
+ exporter_name = params[:exporter]
21
+
22
+ @exporter = @component.exporters.find do |exporter|
23
+ exporter.name == exporter_name
24
+ end
25
+
26
+ unless @exporter
27
+ raise "Requested exporter (#{ exporter_name }) not found for " +
28
+ ":#{ @component.identifier } component."
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,28 +1,10 @@
1
1
  module Para
2
2
  module Admin
3
- class ImportsController < Para::Admin::ComponentController
4
- include Para::Admin::ResourceControllerConcerns
5
-
3
+ class ImportsController < Para::Admin::JobsController
6
4
  layout false
7
5
 
8
6
  before_action :load_importer
9
7
 
10
- def show
11
- @status = ActiveJob::Status.get(params[:id])
12
-
13
- respond_to do |format|
14
- format.json do
15
- if @status.failed?
16
- render json: { status: @status.status }, status: 422
17
- else
18
- render json: { status: @status.status, progress: @status.progress * 100 }
19
- end
20
- end
21
-
22
- format.html
23
- end
24
- end
25
-
26
8
  def new
27
9
  @file = Para::Library::File.new
28
10
  @model = resource_model
@@ -33,9 +15,8 @@ module Para
33
15
 
34
16
  if @file.save
35
17
  job = @importer.perform_later(@file)
36
- @status = ActiveJob::Status.get(job)
37
18
 
38
- render 'show'
19
+ track_job(job)
39
20
  else
40
21
  render 'new'
41
22
  end
@@ -0,0 +1,66 @@
1
+ # This serves as the base controller class to create simple async ActiveJob
2
+ # tracking modals, allowing easy job launching and tracking interfaces
3
+ # implementation in Para and plugins.
4
+ #
5
+ # To use this controller, inherit from it in your app, call perform_async on an
6
+ # ActiveJob class and pass the returned job to the `#track_job(job)` method.
7
+ #
8
+ # On the client side, it is advised to use the job-tracker javascript
9
+ # plugin included into para, that will handle modal displaying and job
10
+ # status tracking automatically with Ajax requests (ActionCable may come later).
11
+ # Use a remote link or form, and add the [data-job-tracker-button] attribute,
12
+ # which will immediately handle the ajax response and display the resulting
13
+ # modal.
14
+ #
15
+ # This will render a modal and the javascript will automatically start tracking
16
+ # the job progress and refresh the view with progression, success and error
17
+ # informations
18
+ #
19
+ # Example :
20
+ #
21
+ # class StatsGenerationController < Para::Admin::JobsController
22
+ # def run
23
+ # job = StatsGeneration.perform_async
24
+ # track_job(job)
25
+ # end
26
+ # end
27
+ #
28
+ module Para
29
+ module Admin
30
+ class JobsController < Para::Admin::ComponentController
31
+ include Para::Admin::ResourceControllerConcerns
32
+
33
+ def show
34
+ @status = ActiveJob::Status.get(params[:id])
35
+
36
+ respond_to do |format|
37
+ format.json do
38
+ if @status.failed?
39
+ render json: { status: @status.status }, status: 422
40
+ else
41
+ render json: { status: @status.status, progress: @status.progress * 100 }
42
+ end
43
+ end
44
+
45
+ format.html do
46
+ @job = @status[:job_type].constantize.new
47
+ # Assign job id to allow status to be retrieved, which in our case
48
+ # allows data persistence though the `#store` method
49
+ @job.job_id = params[:id]
50
+
51
+ render layout: false
52
+ end
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ def track_job(job)
59
+ @job = job
60
+ @status = ActiveJob::Status.get(@job)
61
+
62
+ render 'show', layout: false
63
+ end
64
+ end
65
+ end
66
+ end
@@ -4,15 +4,16 @@ module Para
4
4
  include Para::ApplicationHelper
5
5
 
6
6
  def find_partial_for(relation, partial, partial_dir: 'admin/resources')
7
- relation_class = if relation.kind_of?(ActiveRecord::Base)
7
+ relation_class = if model?(relation.class)
8
8
  relation = relation.class
9
9
  elsif model?(relation)
10
10
  relation
11
11
  end
12
12
 
13
13
  relation_name = find_relation_name_for(
14
- 'admin', plural_file_path_for(relation), partial,
15
- relation_class: relation_class
14
+ plural_file_path_for(relation), partial,
15
+ relation_class: relation_class,
16
+ overrides_root: 'admin'
16
17
  )
17
18
 
18
19
  if relation_name
@@ -22,8 +23,8 @@ module Para
22
23
  end
23
24
  end
24
25
 
25
- def find_relation_name_for(namespace, relation, partial, options = {})
26
- return relation if partial_exists?(relation, partial)
26
+ def find_relation_name_for(relation, partial, options = {})
27
+ return relation if partial_exists?(relation, partial, options)
27
28
  return nil unless options[:relation_class]
28
29
 
29
30
  relation = options[:relation_class].ancestors.find do |ancestor|
@@ -31,7 +32,7 @@ module Para
31
32
  break if ancestor == ActiveRecord::Base
32
33
 
33
34
  ancestor_name = plural_file_path_for(ancestor.name)
34
- partial_exists?(ancestor_name, partial)
35
+ partial_exists?(ancestor_name, partial, options)
35
36
  end
36
37
 
37
38
  plural_file_path_for(relation) if relation
@@ -88,10 +89,10 @@ module Para
88
89
  object.respond_to?(:model_name)
89
90
  end
90
91
 
91
- def partial_exists?(relation, partial)
92
+ def partial_exists?(relation, partial, overrides_root: 'admin', **options)
92
93
  partial_path = partial.to_s.split('/')
93
94
  partial_path[-1] = "_#{ partial_path.last }"
94
- lookup_context.find_all("admin/#{relation}/#{ partial_path.join('/') }").any?
95
+ lookup_context.find_all("#{ overrides_root }/#{relation}/#{ partial_path.join('/') }").any?
95
96
  end
96
97
  end
97
98
  end
@@ -9,6 +9,6 @@ module Para
9
9
  include Para::TagHelper
10
10
  include Para::TreeHelper
11
11
  include Para::FlashHelper
12
- include Para::ExportsHelper
12
+ include Para::TranslationsHelper
13
13
  end
14
14
  end
@@ -0,0 +1,25 @@
1
+ module Para
2
+ module TranslationsHelper
3
+ # This helper method allows to use ActiveModel or ActiveRecord model
4
+ # hierarchy to use translations with automatic defaults from parent models.
5
+ #
6
+ # This works by scanning all the model ancestors to find an existing
7
+ # translation, allowing defining parent class translations and optionnaly
8
+ # overriding translations in subclasses scope
9
+ #
10
+ def model_translate(key, model: nil, scope: nil, **options)
11
+ # Get model class if model argument was passed a model instance
12
+ model = model.class if model.class.respond_to?(:model_name)
13
+
14
+ # Create a key for every parent class that could contain a translation
15
+ defaults = model.lookup_ancestors.map do |klass|
16
+ :"#{ scope }.#{ klass.model_name.i18n_key }.#{ key }"
17
+ end
18
+
19
+ defaults << options.delete(:default) if options[:default]
20
+ options[:default] = defaults
21
+
22
+ I18n.translate(defaults.shift, options)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ = modal.body do
2
+ .alert.alert-success
3
+ = model_translate(:success, scope: "para.jobs", model: job)
4
+
5
+ = javascript_tag do
6
+ :plain
7
+ window.location.href = '#{ job.file.attachment.url }';
8
+
9
+ = modal.footer do
10
+ %button.btn.btn-default{ data: { dismiss: 'modal' } }
11
+ = t('para.shared.close')
@@ -1,7 +1,7 @@
1
1
  = modal.body do
2
- - if status[:errors].any?
2
+ - if status[:errors].try(:any?)
3
3
  .alert.alert-warning
4
- = t('para.flash.shared.import.success_with_errors')
4
+ = model_translate(:success_with_errors, scope: "para.jobs", model: job)
5
5
 
6
6
  %ul
7
7
  - status[:errors].each do |message|
@@ -9,7 +9,7 @@
9
9
 
10
10
  - else
11
11
  .alert.alert-success
12
- = t('para.flash.shared.import.success')
12
+ = model_translate(:success, scope: "para.jobs", model: job)
13
13
 
14
14
  = modal.footer do
15
15
  %button.btn.btn-default{ data: { dismiss: 'modal' } }
@@ -1,6 +1,6 @@
1
1
  = modal.body do
2
2
  .alert.alert-danger
3
- = t('para.flash.shared.import.error')
3
+ = model_translate(:error, scope: "para.jobs", model: job)
4
4
 
5
5
  = modal.footer do
6
6
  %button.btn.btn-default{ data: { dismiss: 'modal' } }
@@ -0,0 +1,5 @@
1
+ = modal.body do
2
+ %p= model_translate(:progressing, scope: "para.jobs", model: job)
3
+
4
+ .progress{ data: { :'async-progress' => 'import' } }
5
+ .progress-bar.progress-bar-striped.active{ role: "progressbar", style: "width: #{ status.progress.nan? ? 100 : (status.progress * 100) }%" }
@@ -0,0 +1,10 @@
1
+ = modal id: "job-#{ @job.model_name.route_key }", data: { :'job-status-url' => url_for(action: :show, id: @status.job_id) } do |modal|
2
+ = modal.header do
3
+ = @job.model_name.human
4
+
5
+ - if @status.failed?
6
+ = render partial: find_partial_for(@job, :failed, partial_dir: 'admin/jobs'), locals: { modal: modal, job: @job }
7
+ - elsif @status.completed?
8
+ = render partial: find_partial_for(@job, :completed, partial_dir: 'admin/jobs'), locals: { modal: modal, job: @job, status: @status }
9
+ - else
10
+ = render partial: find_partial_for(@job, :progress, partial_dir: 'admin/jobs'), locals: { modal: modal, job: @job, status: @status }
@@ -1,12 +1,19 @@
1
1
  - if component.exportable?
2
- - component.exports.each do |export|
2
+ - if component.exporters.length == 1
3
+ - exporter = component.exporters.first
4
+
5
+ = link_to component.path(namespace: :exports, exporter: exporter.name, q: params[:q]), class: 'btn btn-default', method: :post, remote: true, data: { :'job-tracker-button' => true } do
6
+ = fa_icon 'download'
7
+ = exporter.model_name.human
8
+
9
+ - else
3
10
  .btn-group
4
- %button.btn.btn-default.dropdown-toggle{"aria-expanded" => "false", "data-toggle" => "dropdown", :type => "button"}
5
- %i.fa.fa-download
6
- = export_name_for(model, export)
11
+ %button.btn.btn-default{ type: 'button', data: { toggle: 'dropdown' } }
12
+ = fa_icon 'download'
13
+ = t('para.export.name')
7
14
  %span.caret
8
- %ul.dropdown-menu{:role => "menu"}
9
- - export[:formats].each do |format|
15
+ %ul.dropdown-menu
16
+ - component.exporters.each do |exporter|
10
17
  %li
11
- = link_to component.relation_path(model.model_name.route_key, action: :export, format: format) do
12
- = t('para.export.as', extension: format)
18
+ = link_to component.path(namespace: :exports, exporter: exporter.name, q: params[:q]), method: :post, remote: true, data: { :'job-tracker-button' => true } do
19
+ = exporter.model_name.human
@@ -2,7 +2,7 @@
2
2
  - if component.importers.length == 1
3
3
  - importer = component.importers.first
4
4
 
5
- = link_to component.path(namespace: :import, action: :new, importer: importer.model_name.singular_route_key), class: 'btn btn-default', remote: true, data: { :'importer-button' => true } do
5
+ = link_to component.path(namespace: :import, action: :new, importer: importer.model_name.singular_route_key), class: 'btn btn-default', remote: true, data: { :'job-tracker-button' => true } do
6
6
  = fa_icon 'upload'
7
7
  = importer.model_name.human
8
8
 
@@ -15,5 +15,5 @@
15
15
  %ul.dropdown-menu
16
16
  - component.importers.each do |importer|
17
17
  %li
18
- = link_to component.path(:imports, action: :new, importer: importer.model_name.singular_route_key), remote: true, data: { :'importer-button' => true } do
18
+ = link_to component.path(namespace: :import, action: :new, importer: importer.model_name.singular_route_key), remote: true, data: { :'job-tracker-button' => true } do
19
19
  = importer.model_name.human
@@ -14,13 +14,21 @@ fr:
14
14
  clone:
15
15
  success: "%{model} cloné(e)"
16
16
  error: "Impossible de cloner le(a) %{model}"
17
- import:
18
- success: "L'import du fichier a été effectué avec succès"
19
- success_with_errors: |
20
- L'import du fichier a été effectué, mais certaines lignes n'ont pas
21
- été prises en compte à causes d'erreurs :
22
- other_errors: "<br>Et <b>%{count}</b> autres erreurs ..."
23
- error: "Le fichier choisi contient des erreurs et n'a pu être importé"
17
+
18
+ jobs:
19
+ para/importer/base:
20
+ progressing: "Le fichier est en cours d'import, merci de patienter quelques instants ..."
21
+ success: "L'import du fichier a été effectué avec succès"
22
+ success_with_errors: |
23
+ L'import du fichier a été effectué, mais certaines lignes n'ont pas
24
+ été prises en compte à causes d'erreurs :
25
+ other_errors: "<br>Et <b>%{count}</b> autres erreurs ..."
26
+ error: "Le fichier choisi contient des erreurs et n'a pu être importé"
27
+
28
+ para/exporter/base:
29
+ progressing: "Le fichier est en cours d'export, merci de patienter quelques instants ..."
30
+ success: "Le fichier d'export a été généré, son téléchargement va démarrer ..."
31
+ error: "Erreur lors de la génération du fichier d'export ..."
24
32
 
25
33
  admin:
26
34
  title: "Administration"
@@ -57,7 +65,7 @@ fr:
57
65
  empty: "Aucune entrée créée pour le moment. Créez une nouvelle entrée avec le bouton suivant :"
58
66
 
59
67
  export:
60
- name: "Export %{name}"
68
+ name: "Exporter"
61
69
  as: "Exporter (.%{extension})"
62
70
 
63
71
  import:
@@ -69,7 +77,6 @@ fr:
69
77
  cliquez sur le bouton "Importer".
70
78
  placeholder: Fichier au format ( .csv .xlsx )
71
79
  row_error_prefix: "Ligne %{number} :"
72
- importing_file: "Le fichier est en cours d'import, merci de patienter quelques instants ..."
73
80
 
74
81
  shared:
75
82
  save: "Enregistrer"
@@ -116,7 +123,9 @@ fr:
116
123
  settings_rails/form:
117
124
  one: "Configuration"
118
125
  other: "Configuration"
119
-
126
+ para/exporter/base:
127
+ one: "Export des données"
128
+ other: "Exports des données"
120
129
  date:
121
130
  formats:
122
131
  admin: '%d/%m/%Y'
@@ -4,21 +4,19 @@ module Para
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- configurable_on :export
8
- end
7
+ configurable_on :exporters
9
8
 
10
- def exportable?
11
- @exportable ||= exports.length > 0
9
+ define_method(:exporters) do
10
+ @exporters ||= if (exporters = configuration['exporters'].presence)
11
+ eval(exporters).map(&:constantize)
12
+ else
13
+ []
14
+ end
15
+ end
12
16
  end
13
17
 
14
- # TODO : Move :configuration column store to JSON instead of HStore
15
- # which handles more data types and will help us avoid eval here
16
- def exports
17
- @exports ||= if export.present?
18
- eval(export).map(&:with_indifferent_access)
19
- else
20
- []
21
- end
18
+ def exportable?
19
+ @exportable ||= exporters.length > 0
22
20
  end
23
21
  end
24
22
  end
@@ -1,39 +1,41 @@
1
1
  module Para
2
2
  module Exporter
3
- class Base
4
- attr_reader :resources
5
- class_attribute :model_name
3
+ class Base < Para::Job::Base
4
+ attr_reader :name, :model, :options
6
5
 
7
- def initialize(resources)
8
- @resources = resources
9
- end
10
-
11
- def model
12
- @model ||= if (model_name = self.class.model_name)
13
- model_name.constantize
14
- else
15
- raise 'You must define model to export in your exporter as following: `exports \'YourModelName\'`'
16
- end
17
- end
6
+ def perform(model_name: nil, **options)
7
+ @model = model_name && model_name.constantize
8
+ @options = options
9
+ @name = model.try(:model_name).try(:route_key).try(:parameterize)
18
10
 
19
- def self.exports model_name
20
- self.model_name = model_name
11
+ # Render file and store it in a Library::File object, allowing us
12
+ # to retrieve that file easily from the job and subsequent requests
13
+ #
14
+ file = Para::Library::File.create!(attachment: render)
15
+ store(:file_gid, file.to_global_id)
21
16
  end
22
17
 
23
- def disposition
24
- 'inline'
25
- end
26
-
27
- def extension
28
- raise '#extension must be defined to create the export file name'
18
+ def file
19
+ @file ||= GlobalID::Locator.locate(store(:file_gid))
29
20
  end
30
21
 
31
22
  def file_name
32
- @file_name ||= [name, extension].join('.')
23
+ @file_name ||= [name, extension].join
33
24
  end
34
25
 
35
- def self.register_base_exporter(type, exporter)
36
- Exporter.base_exporters[type] = exporter
26
+ private
27
+
28
+ # Allow passing a `:resources` option or a ransack search hash to filter
29
+ # exported resources
30
+ #
31
+ def resources
32
+ @resources ||= if options[:resources]
33
+ options[:resources]
34
+ elsif options[:search]
35
+ model.search(options[:search]).result
36
+ else
37
+ model.all
38
+ end
37
39
  end
38
40
  end
39
41
  end
@@ -3,32 +3,27 @@ require 'csv'
3
3
  module Para
4
4
  module Exporter
5
5
  class Csv < Base
6
- register_base_exporter :csv, self
7
-
8
- def extension
9
- 'csv'
10
- end
11
-
12
- def mime_type
13
- 'text/csv'
14
- end
15
-
16
- def export_type
17
- :excel
18
- end
19
-
20
6
  def render
21
- CSV.generate do |csv|
7
+ data = CSV.generate do |csv|
22
8
  csv << headers
23
9
 
24
10
  resources.each do |resource|
25
11
  csv << row_for(resource)
26
12
  end
27
13
  end
14
+
15
+ Tempfile.new([name, extension]).tap do |file|
16
+ file.write(data)
17
+ file.rewind
18
+ end
28
19
  end
29
20
 
30
21
  private
31
22
 
23
+ def extension
24
+ '.csv'
25
+ end
26
+
32
27
  def headers
33
28
  fields.map do |field|
34
29
  encode(model.human_attribute_name(field))
data/lib/para/exporter.rb CHANGED
@@ -1,55 +1,5 @@
1
1
  module Para
2
2
  module Exporter
3
- class MissingExporterError < StandardError
4
- attr_accessor :model_name, :format, :exporter_name
5
-
6
- def initialize(model_name, format, exporter_name)
7
- @model_name = model_name
8
- @format = format
9
- @exporter_name = exporter_name
10
- end
11
-
12
- def message
13
- "No exporter found for model \"#{ model_name }\" and format " +
14
- "\"#{ format }\". Please create the #{ exporter_name } class " +
15
- "manually or with the following command : " +
16
- "`rails g para:exporter #{ model_name.underscore } #{ format }"
17
- end
18
- end
19
-
20
- def self.for(model_name, format)
21
- exporter_name = name_for(model_name, format)
22
-
23
- begin
24
- const_get(exporter_name)
25
- rescue NameError => e
26
- if e.message == "uninitialized constant Para::Exporter::#{ exporter_name }"
27
- raise MissingExporterError.new(model_name, format, exporter_name)
28
- else
29
- raise e
30
- end
31
- end
32
- end
33
-
34
- def self.name_for(model_name, format)
35
- [
36
- '',
37
- format_exporter_name(format),
38
- model_exporter_name(model_name)
39
- ].join('::')
40
- end
41
-
42
- def self.model_exporter_name(model_name)
43
- [model_name.to_s.pluralize, 'Exporter'].join
44
- end
45
-
46
- def self.format_exporter_name(format)
47
- format.to_s.camelize
48
- end
49
-
50
- def self.base_exporters
51
- @base_exporters ||= {}.with_indifferent_access
52
- end
53
3
  end
54
4
  end
55
5
 
@@ -1,12 +1,6 @@
1
1
  module Para
2
2
  module Importer
3
- class Base < ActiveJob::Base
4
- include ActiveJob::Status
5
- # Used to store import errors on the object
6
- include ActiveModel::Validations
7
- # Used to translate importer name with rails default `activemodel` i18n keys
8
- extend ActiveModel::Naming
9
-
3
+ class Base < Para::Job::Base
10
4
  rescue_from Exception, with: :rescue_exception
11
5
 
12
6
  class_attribute :allows_import_errors
@@ -16,12 +10,11 @@ module Para
16
10
  def perform(file, options = {})
17
11
  @file = file
18
12
  @sheet = Roo::Spreadsheet.open(file.attachment_path, options)
19
- progress.total = sheet.last_row - 1
20
13
 
21
14
  ActiveRecord::Base.transaction do
22
15
  (2..(sheet.last_row)).each do |index|
23
16
  begin
24
- progress.increment
17
+ progress!
25
18
  import_from_row(sheet.row(index))
26
19
  rescue ActiveRecord::RecordInvalid => error
27
20
  if allows_import_errors?
@@ -33,11 +26,15 @@ module Para
33
26
  end
34
27
  end
35
28
 
36
- status.update(errors: errors.full_messages)
29
+ save_errors!
37
30
  end
38
31
 
39
32
  private
40
33
 
34
+ def progress_total
35
+ sheet.last_row - 1
36
+ end
37
+
41
38
  def import_from_row(row)
42
39
  raise '#import_from_row(row) must be defined'
43
40
  end
@@ -0,0 +1,53 @@
1
+ module Para
2
+ module Job
3
+ class Base < ActiveJob::Base
4
+ include ActiveJob::Status
5
+ # Used to store import errors on the object
6
+ include ActiveModel::Validations
7
+ # Used to translate importer name with rails default `activemodel` i18n keys
8
+ extend ActiveModel::Translation
9
+
10
+ before_perform :store_job_type
11
+
12
+ protected
13
+
14
+ def store_job_type
15
+ status.update(job_type: self.class.name)
16
+ end
17
+
18
+ def progress!
19
+ ensure_total_progress
20
+ progress.increment
21
+ end
22
+
23
+ def save_errors!
24
+ status.update(errors: errors.full_messages)
25
+ end
26
+
27
+ # Default total progress to nil, making the UI show an animated porgress
28
+ # bar, indicating work is in progress, but not the exact progress
29
+ def total_progress
30
+ nil
31
+ end
32
+
33
+ def ensure_total_progress
34
+ return if @total_progress
35
+
36
+ @total_progress ||= if respond_to?(:progress_total)
37
+ progress.total = progress_total
38
+ else
39
+ progress[:total]
40
+ end
41
+ end
42
+
43
+ def store(key, value = nil)
44
+ if value
45
+ status.update(key => value)
46
+ else
47
+ status[key]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
data/lib/para/job.rb ADDED
@@ -0,0 +1,8 @@
1
+ module Para
2
+ module Job
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Base
6
+ end
7
+ end
8
+
data/lib/para/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Para
2
- VERSION = '0.6.2'
2
+ VERSION = '0.6.3'
3
3
  end
data/lib/para.rb CHANGED
@@ -39,6 +39,7 @@ require 'para/form_builder'
39
39
  require 'para/markup'
40
40
  require 'para/engine'
41
41
  require 'para/components_configuration'
42
+ require 'para/job'
42
43
  require 'para/exporter'
43
44
  require 'para/importer'
44
45
  require 'para/sti'
@@ -47,6 +47,7 @@ module ActionDispatch
47
47
  #
48
48
  controller = options.fetch(:controller, '/para/admin/crud_resources')
49
49
  imports_controller = options.fetch(:imports_controller, '/para/admin/imports')
50
+ exports_controller = options.fetch(:exports_controller, '/para/admin/exports')
50
51
 
51
52
  namespace :admin do
52
53
  constraints Para::Routing::ComponentControllerConstraint.new(controller) do
@@ -56,7 +57,6 @@ module ActionDispatch
56
57
  collection do
57
58
  patch :order
58
59
  patch :tree
59
- get :export
60
60
  end
61
61
 
62
62
  member do
@@ -70,6 +70,10 @@ module ActionDispatch
70
70
  scope ':importer' do
71
71
  resources :imports, controller: imports_controller
72
72
  end
73
+
74
+ scope ':exporter' do
75
+ resources :exports, controller: exports_controller
76
+ end
73
77
  end
74
78
  end
75
79
  end
@@ -9,9 +9,9 @@ namespace :para do
9
9
  end
10
10
 
11
11
  Para::Component::Base.where(type: 'Para::Component::SingletonResource').pluck(:identifier, :id).each do |identifier, id|
12
- Para::ComponentResource.where(component_id: id).update_all(
13
- component_id: Para::Component::Form.find_by_identifier(identifier: identifier).id
14
- )
12
+ if (form_component_id = Para::Component::Form.find_by_identifier(identifier).try(:id))
13
+ Para::ComponentResource.where(component_id: id).update_all(component_id: form_component_id)
14
+ end
15
15
 
16
16
  Para::Component::Base.where(id: id).destroy_all
17
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: para
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Valentin Ballestrino
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-27 00:00:00.000000000 Z
11
+ date: 2016-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -388,7 +388,7 @@ files:
388
388
  - app/assets/javascripts/para/admin.coffee
389
389
  - app/assets/javascripts/para/admin/async-progress.coffee
390
390
  - app/assets/javascripts/para/admin/filters-form.coffee
391
- - app/assets/javascripts/para/admin/importers.coffee
391
+ - app/assets/javascripts/para/admin/job-tracker.coffee
392
392
  - app/assets/javascripts/para/admin/table.coffee
393
393
  - app/assets/javascripts/para/admin/tabs.coffee
394
394
  - app/assets/javascripts/para/admin/theme_actions.coffee
@@ -438,8 +438,10 @@ files:
438
438
  - app/controllers/para/admin/base_controller.rb
439
439
  - app/controllers/para/admin/component_controller.rb
440
440
  - app/controllers/para/admin/crud_resources_controller.rb
441
+ - app/controllers/para/admin/exports_controller.rb
441
442
  - app/controllers/para/admin/form_resources_controller.rb
442
443
  - app/controllers/para/admin/imports_controller.rb
444
+ - app/controllers/para/admin/jobs_controller.rb
443
445
  - app/controllers/para/admin/main_controller.rb
444
446
  - app/controllers/para/admin/resources_controller.rb
445
447
  - app/controllers/para/admin/settings_component_controller.rb
@@ -456,7 +458,6 @@ files:
456
458
  - app/helpers/para/admin/page_helper.rb
457
459
  - app/helpers/para/admin/resources_helper.rb
458
460
  - app/helpers/para/application_helper.rb
459
- - app/helpers/para/exports_helper.rb
460
461
  - app/helpers/para/flash_helper.rb
461
462
  - app/helpers/para/form_helper.rb
462
463
  - app/helpers/para/markup_helper.rb
@@ -465,6 +466,7 @@ files:
465
466
  - app/helpers/para/ordering_helper.rb
466
467
  - app/helpers/para/search_helper.rb
467
468
  - app/helpers/para/tag_helper.rb
469
+ - app/helpers/para/translations_helper.rb
468
470
  - app/helpers/para/tree_helper.rb
469
471
  - app/models/para/ability.rb
470
472
  - app/models/para/component/base.rb
@@ -477,16 +479,17 @@ files:
477
479
  - app/models/para/library.rb
478
480
  - app/models/para/library/file.rb
479
481
  - app/models/para/page/section.rb
482
+ - app/views/admin/para/exporter/bases/_completed.html.haml
480
483
  - app/views/layouts/para/admin.html.haml
481
484
  - app/views/layouts/para/application.html.erb
482
485
  - app/views/para/admin/crud_resources/index.html.haml
483
486
  - app/views/para/admin/dashboard.html.haml
484
487
  - app/views/para/admin/form_resources/show.html.haml
485
- - app/views/para/admin/imports/_completed.html.haml
486
- - app/views/para/admin/imports/_failed.html.haml
487
- - app/views/para/admin/imports/_progress.html.haml
488
488
  - app/views/para/admin/imports/new.html.haml
489
- - app/views/para/admin/imports/show.html.haml
489
+ - app/views/para/admin/jobs/_completed.html.haml
490
+ - app/views/para/admin/jobs/_failed.html.haml
491
+ - app/views/para/admin/jobs/_progress.html.haml
492
+ - app/views/para/admin/jobs/show.html.haml
490
493
  - app/views/para/admin/main/index.html.haml
491
494
  - app/views/para/admin/resources/_actions.html.haml
492
495
  - app/views/para/admin/resources/_add_button.html.haml
@@ -612,6 +615,8 @@ files:
612
615
  - lib/para/inputs.rb
613
616
  - lib/para/inputs/nested_many_input.rb
614
617
  - lib/para/inputs/nested_one_input.rb
618
+ - lib/para/job.rb
619
+ - lib/para/job/base.rb
615
620
  - lib/para/logging.rb
616
621
  - lib/para/logging/active_job_log_subscriber.rb
617
622
  - lib/para/markup.rb
@@ -668,7 +673,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
668
673
  version: '0'
669
674
  requirements: []
670
675
  rubyforge_project:
671
- rubygems_version: 2.5.1
676
+ rubygems_version: 2.6.7
672
677
  signing_key:
673
678
  specification_version: 4
674
679
  summary: Rails admin engine
@@ -1,11 +0,0 @@
1
- module Para
2
- module ExportsHelper
3
- def export_name_for(model, export)
4
- model_name = (
5
- export[:model] && export[:model].constantize.model_name.human
6
- ) || model.model_name.human
7
-
8
- t('para.export.name', name: model_name)
9
- end
10
- end
11
- end
@@ -1,5 +0,0 @@
1
- = modal.body do
2
- %p= t('para.import.importing_file')
3
-
4
- .progress{ data: { :'async-progress' => 'import' } }
5
- .progress-bar.progress-bar-striped.active{ role: "progressbar", style: "width: #{ status.progress * 100 }%" }
@@ -1,10 +0,0 @@
1
- = modal id: "component-import-#{ @importer.model_name.route_key }", data: { :'import-status-url' => @component.path(namespace: :import, importer: @importer.name, id: @status.job_id) } do |modal|
2
- = modal.header do
3
- = @importer.model_name.human
4
-
5
- - if @status.failed?
6
- = render partial: 'failed', locals: { modal: modal }
7
- - elsif @status.completed?
8
- = render partial: 'completed', locals: { modal: modal, status: @status }
9
- - else
10
- = render partial: 'progress', locals: { modal: modal, status: @status }