para 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 }