From 82823dd4cc55c525e86bad7a146449f3d403451d Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Thu, 29 Jun 2023 16:37:16 +0200 Subject: [PATCH 1/2] download project to markdown file --- Gemfile | 2 + Gemfile.lock | 8 ++- app/controllers/api/projects_controller.rb | 6 ++ app/frontend/templates/projects/show.html | 15 ++-- app/policies/project_policy.rb | 4 ++ app/services/project_to_markdown.rb | 83 ++++++++++++++++++++++ config/locales/app.public.en.yml | 1 + config/locales/app.public.fr.yml | 1 + config/locales/app.shared.en.yml | 3 + config/locales/app.shared.fr.yml | 3 + config/routes.rb | 1 + 11 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 app/services/project_to_markdown.rb diff --git a/Gemfile b/Gemfile index 822c4dd0b..a63b12a8a 100644 --- a/Gemfile +++ b/Gemfile @@ -149,3 +149,5 @@ gem 'acts_as_list' # Error reporting gem 'sentry-rails' gem 'sentry-ruby' + +gem "reverse_markdown" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 4606b128b..cd14fbf34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,7 +82,7 @@ GEM rails (>= 4.1) ast (2.4.2) attr_required (1.0.1) - awesome_print (1.8.0) + awesome_print (1.9.2) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) @@ -269,6 +269,8 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.14.3-x86_64-darwin) + racc (~> 1.4) nokogiri (1.14.3-x86_64-linux) racc (~> 1.4) oauth2 (1.4.4) @@ -396,6 +398,8 @@ GEM responders (3.1.0) actionpack (>= 5.2) railties (>= 5.2) + reverse_markdown (2.1.1) + nokogiri rexml (3.2.5) rolify (5.3.0) rubocop (1.31.2) @@ -524,6 +528,7 @@ GEM zeitwerk (2.6.7) PLATFORMS + x86_64-darwin-21 x86_64-linux DEPENDENCIES @@ -587,6 +592,7 @@ DEPENDENCIES redis-session-store repost responders (~> 3.0) + reverse_markdown rolify rubocop (~> 1.31) rubocop-rails diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index dc1cf7699..a2ea6de2f 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -18,6 +18,12 @@ class API::ProjectsController < API::APIController @project = Project.friendly.find(params[:id]) end + def markdown + @project = Project.friendly.find(params[:id]) + authorize @project + send_data ProjectToMarkdown.new(@project).call, filename: "#{@project.name.parameterize}-#{@project.id}.md", disposition: 'attachment', type: 'text/markdown' + end + def create @project = Project.new(project_params.merge(author_statistic_profile_id: current_user.statistic_profile.id)) if @project.save diff --git a/app/frontend/templates/projects/show.html b/app/frontend/templates/projects/show.html index 5c0aa93ab..109b69e1c 100644 --- a/app/frontend/templates/projects/show.html +++ b/app/frontend/templates/projects/show.html @@ -174,12 +174,17 @@ -
+ - -
+
+ +
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 8daad8143..96033e0cb 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -19,6 +19,10 @@ class ProjectPolicy < ApplicationPolicy user.admin? or record.author.user_id == user.id or record.users.include?(user) end + def markdown? + user.admin? or record.author.user_id == user.id or record.users.include?(user) + end + def destroy? user.admin? or record.author.user_id == user.id end diff --git a/app/services/project_to_markdown.rb b/app/services/project_to_markdown.rb new file mode 100644 index 000000000..0dfe81914 --- /dev/null +++ b/app/services/project_to_markdown.rb @@ -0,0 +1,83 @@ +class ProjectToMarkdown + attr_reader :project + + def initialize(project) + @project = project + end + + def call + md = [] + + md << "# #{project.name}" + + md << "![#{I18n.t('app.shared.project.illustration')}](#{full_url(project.project_image.attachment.url)})" if project.project_image + + md << ReverseMarkdown.convert(project.description.to_s) + + project_steps = project.project_steps.order(:step_nb) + + if project_steps.present? + md << "## #{I18n.t('app.shared.project.steps')}" + + project_steps.each do |project_step| + md << "### #{I18n.t('app.shared.project.step_N').gsub('{INDEX}', project_step.step_nb.to_s)} : #{project_step.title}" + md << ReverseMarkdown.convert(project_step.description.to_s) + + project_step.project_step_images.each_with_index do |image, i| + md << "![#{I18n.t('app.shared.project.step_image')} #{i+1}](#{full_url(project.project_image.attachment.url)})" + end + end + end + + if project.themes.present? + md << "## #{I18n.t('app.shared.project.themes')}" + md << project.themes.map(&:name).join(', ') + end + + if project.project_caos.present? + md << "## #{I18n.t('app.shared.project.CAD_files')}" + project.project_caos.each do |cao| + md << "![#{cao.attachment_identifier}](#{full_url(cao.attachment_url)})" + end + end + + md << "## #{I18n.t('app.shared.project.status')}" + md << project.status.name + + if project.machines.present? + md << "## #{I18n.t('app.shared.project.employed_machines')}" + md << project.machines.map(&:name).join(', ') + end + + if project.components.present? + md << "## #{I18n.t('app.shared.project.employed_materials')}" + md << project.components.map(&:name).join(', ') + end + + if project.project_users.present? + md << "## #{I18n.t('app.shared.project.collaborators')}" + md << project.project_users.map { |pu| pu.user.profile.full_name }.join(', ') + end + + if project.licence.present? + md << "## #{I18n.t('app.shared.project.licence')}" + md << project.licence.name + end + + if project.tags.present? + md << "## #{I18n.t('app.shared.project.tags')}" + md << project.tags + end + + + md = md.reject { |line| line.blank? } + + md.join("\n\n") + end + + private + + def full_url(path) + "#{Rails.application.routes.url_helpers.root_url[...-1]}#{path}" + end +end \ No newline at end of file diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 7dfc61f69..be378bcd5 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -216,6 +216,7 @@ en: report: "Report" do_you_really_want_to_delete_this_project: "Do you really want to delete this project?" status: "Status" + markdown_file: "Markdown file" #list of machines machines_list: the_fablab_s_machines: "The machines" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 09a07a116..5d2004658 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -216,6 +216,7 @@ fr: report: "Signaler" do_you_really_want_to_delete_this_project: "Êtes-vous sur de vouloir supprimer ce projet ?" status: "Statut" + markdown_file: "Fichier Markdown" #list of machines machines_list: the_fablab_s_machines: "Les machines" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 423e3a6f9..127ae2ed1 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -131,6 +131,7 @@ en: illustration: "Visual" add_an_illustration: "Add an illustration" CAD_file: "CAD file" + CAD_files: "CAD files" allowed_extensions: "Allowed extensions:" add_a_new_file: "Add a new file" description: "Description" @@ -138,6 +139,7 @@ en: steps: "Steps" step_N: "Step {INDEX}" step_title: "Step title" + step_image: "Image" add_a_picture: "Add a picture" change_the_picture: "Change the picture" delete_the_step: "Delete the step" @@ -150,6 +152,7 @@ en: employed_machines: "Employed machines" collaborators: "Collaborators" creative_commons_licences: "Creative Commons licences" + licence: "Licence" themes: "Themes" tags: "Tags" save_as_draft: "Save as draft" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index e03e762b3..40816e060 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -131,6 +131,7 @@ fr: illustration: "Illustration" add_an_illustration: "Ajouter un visuel" CAD_file: "Fichier CAO" + CAD_files: "Fichiers CAO" allowed_extensions: "Extensions autorisées :" add_a_new_file: "Ajouter un nouveau fichier" description: "Description" @@ -138,6 +139,7 @@ fr: steps: "Étapes" step_N: "Étape {INDEX}" step_title: "Titre de l'étape" + step_image: "Image" add_a_picture: "Ajouter une image" change_the_picture: "Modifier l'image" delete_the_step: "Supprimer l'étape" @@ -150,6 +152,7 @@ fr: employed_machines: "Machines utilisées" collaborators: "Les collaborateurs" creative_commons_licences: "Licences Creative Commons" + licence: "Licence" themes: "Thématiques" tags: "Étiquettes" save_as_draft: "Enregistrer comme brouillon" diff --git a/config/routes.rb b/config/routes.rb index 4f9872af2..f0e0c1087 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,6 +38,7 @@ Rails.application.routes.draw do get :last_published get :search end + get :markdown, on: :member end resources :openlab_projects, only: :index resources :machines From 50ed3c9ed2af3770c9b70c1026d7539b6310cb97 Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Fri, 30 Jun 2023 11:15:37 +0200 Subject: [PATCH 2/2] projects to markdown in zip --- Gemfile | 4 ++-- app/controllers/api/projects_controller.rb | 19 ++++++++++++---- .../src/javascript/controllers/projects.js | 5 +++++ app/frontend/templates/projects/index.html | 6 +++++ app/frontend/templates/projects/show.html | 4 ++-- app/models/project_step.rb | 2 ++ app/policies/project_policy.rb | 6 ++--- app/services/project_service.rb | 13 ++++++----- app/services/project_to_markdown.rb | 15 ++++++++----- app/services/projects_archive.rb | 22 +++++++++++++++++++ config/locales/app.public.en.yml | 1 + config/locales/app.public.fr.yml | 1 + config/locales/app.shared.en.yml | 1 + config/locales/app.shared.fr.yml | 1 + test/fixtures/project_steps.yml | 2 ++ test/fixtures/projects.yml | 1 + test/integration/projects_test.rb | 17 ++++++++++++++ test/services/project_to_markdown_test.rb | 21 ++++++++++++++++++ 18 files changed, 120 insertions(+), 21 deletions(-) create mode 100644 app/services/projects_archive.rb create mode 100644 test/integration/projects_test.rb create mode 100644 test/services/project_to_markdown_test.rb diff --git a/Gemfile b/Gemfile index a63b12a8a..bbe94468b 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,7 @@ group :development, :test do # comment over to use visual debugger (eg. RubyMine), uncomment to use manual debugging # gem 'byebug' gem 'dotenv-rails' + gem 'pry' end group :development do @@ -43,7 +44,6 @@ group :development do # Preview mail in the browser gem 'listen', '~> 3.0.5' gem 'overcommit' - gem 'pry' gem 'rb-readline' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'railroady' @@ -150,4 +150,4 @@ gem 'acts_as_list' gem 'sentry-rails' gem 'sentry-ruby' -gem "reverse_markdown" \ No newline at end of file +gem "reverse_markdown" diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index a2ea6de2f..e333ab64a 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -59,12 +59,23 @@ class API::ProjectsController < API::APIController def search service = ProjectService.new - res = service.search(params, current_user) + paginate = request.format.zip? ? false : true + res = service.search(params, current_user, paginate: paginate) + render json: res, status: :unprocessable_entity and return if res[:error] - @total = res[:total] - @projects = res[:projects] - render :index + respond_to do |format| + format.json do + @total = res[:total] + @projects = res[:projects] + render :index + end + format.zip do + head :forbidden unless current_user.admin? || current_user.manager? + + send_data ProjectsArchive.new(res[:projects]).call, filename: "projets.zip", disposition: 'attachment', type: 'application/zip' + end + end end private diff --git a/app/frontend/src/javascript/controllers/projects.js b/app/frontend/src/javascript/controllers/projects.js index 6aa7d8112..6c06f6365 100644 --- a/app/frontend/src/javascript/controllers/projects.js +++ b/app/frontend/src/javascript/controllers/projects.js @@ -332,6 +332,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P $scope.triggerSearch(); }; + $scope.zipUrl = '/api/projects/search.zip'; + /** * Callback triggered when the button "search from the whole network" is toggled */ @@ -420,6 +422,9 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P updateUrlParam('component_id', search.component_id); updateUrlParam('machine_id', search.machine_id); updateUrlParam('status_id', search.status_id); + + $scope.zipUrl = '/api/projects/search.zip?' + new URLSearchParams({ search: JSON.stringify($location.search()) }).toString(); + return true; }; diff --git a/app/frontend/templates/projects/index.html b/app/frontend/templates/projects/index.html index 22d8f8143..57bd02ecc 100644 --- a/app/frontend/templates/projects/index.html +++ b/app/frontend/templates/projects/index.html @@ -67,6 +67,12 @@ + +
diff --git a/app/frontend/templates/projects/show.html b/app/frontend/templates/projects/show.html index 109b69e1c..f628c1c1f 100644 --- a/app/frontend/templates/projects/show.html +++ b/app/frontend/templates/projects/show.html @@ -174,8 +174,8 @@
-
- + diff --git a/app/models/project_step.rb b/app/models/project_step.rb index 0324c54f2..b9b76a248 100644 --- a/app/models/project_step.rb +++ b/app/models/project_step.rb @@ -5,4 +5,6 @@ class ProjectStep < ApplicationRecord belongs_to :project, touch: true has_many :project_step_images, as: :viewable, dependent: :destroy accepts_nested_attributes_for :project_step_images, allow_destroy: true, reject_if: :all_blank + + default_scope -> { order(:step_nb) } end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 96033e0cb..8da49c747 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -16,14 +16,14 @@ class ProjectPolicy < ApplicationPolicy end def update? - user.admin? or record.author.user_id == user.id or record.users.include?(user) + user.admin? || record.author.user_id == user.id || record.users.include?(user) end def markdown? - user.admin? or record.author.user_id == user.id or record.users.include?(user) + user.admin? || user.manager? || record.author.user_id == user.id || record.users.include?(user) end def destroy? - user.admin? or record.author.user_id == user.id + user.admin? || record.author.user_id == user.id end end diff --git a/app/services/project_service.rb b/app/services/project_service.rb index 823d9365d..6b7bddecd 100644 --- a/app/services/project_service.rb +++ b/app/services/project_service.rb @@ -2,17 +2,17 @@ # Provides methods for Project class ProjectService - def search(params, current_user) + def search(params, current_user, paginate: true) connection = ActiveRecord::Base.connection return { error: 'invalid adapter' } unless connection.instance_values['config'][:adapter] == 'postgresql' - search_from_postgre(params, current_user) + search_from_postgre(params, current_user, paginate: paginate) end private - def search_from_postgre(params, current_user) - query_params = JSON.parse(params[:search]) + def search_from_postgre(params, current_user, paginate: true) + query_params = JSON.parse(params[:search] || "{}") records = Project.published_or_drafts(current_user&.statistic_profile&.id) records = Project.user_projects(current_user&.statistic_profile&.id) if query_params['from'] == 'mine' @@ -29,6 +29,9 @@ class ProjectService records.order(created_at: :desc) end - { total: records.count, projects: records.includes(:users, :project_image).page(params[:page]) } + records = records.includes(:users, :project_image) + records = records.page(params[:page]) if paginate + + { total: records.count, projects: records } end end diff --git a/app/services/project_to_markdown.rb b/app/services/project_to_markdown.rb index 0dfe81914..7f3521ff0 100644 --- a/app/services/project_to_markdown.rb +++ b/app/services/project_to_markdown.rb @@ -14,7 +14,7 @@ class ProjectToMarkdown md << ReverseMarkdown.convert(project.description.to_s) - project_steps = project.project_steps.order(:step_nb) + project_steps = project.project_steps if project_steps.present? md << "## #{I18n.t('app.shared.project.steps')}" @@ -29,6 +29,9 @@ class ProjectToMarkdown end end + md << "## #{I18n.t('app.shared.project.author')}" + md << project.author&.user&.profile&.full_name + if project.themes.present? md << "## #{I18n.t('app.shared.project.themes')}" md << project.themes.map(&:name).join(', ') @@ -41,8 +44,10 @@ class ProjectToMarkdown end end - md << "## #{I18n.t('app.shared.project.status')}" - md << project.status.name + if project.status + md << "## #{I18n.t('app.shared.project.status')}" + md << project.status.name + end if project.machines.present? md << "## #{I18n.t('app.shared.project.employed_machines')}" @@ -54,9 +59,9 @@ class ProjectToMarkdown md << project.components.map(&:name).join(', ') end - if project.project_users.present? + if project.users.present? md << "## #{I18n.t('app.shared.project.collaborators')}" - md << project.project_users.map { |pu| pu.user.profile.full_name }.join(', ') + md << project.users.map { |u| u.profile.full_name }.join(', ') end if project.licence.present? diff --git a/app/services/projects_archive.rb b/app/services/projects_archive.rb new file mode 100644 index 000000000..009ea661f --- /dev/null +++ b/app/services/projects_archive.rb @@ -0,0 +1,22 @@ +class ProjectsArchive + attr_reader :projects + + def initialize(projects) + @projects = projects + end + + def call + stringio = Zip::OutputStream.write_buffer do |zio| + projects.includes(:project_image, :themes, + :project_caos, :status, :machines, + :components, :licence, + project_steps: :project_step_images, + author: { user: :profile }, + users: :profile).find_each do |project| + zio.put_next_entry("#{project.name.parameterize}-#{project.id}.md") + zio.write ProjectToMarkdown.new(project).call + end + end + stringio.string + end +end \ No newline at end of file diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index be378bcd5..8c2a0cd1a 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -183,6 +183,7 @@ en: all_materials: "All materials" load_next_projects: "Load next projects" rough_draft: "Rough draft" + download_archive: Download status_filter: all_statuses: "All statuses" select_status: "Select a status" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 5d2004658..7c8ce8cc8 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -183,6 +183,7 @@ fr: all_materials: "Tous les matériaux" load_next_projects: "Charger les projets suivants" rough_draft: "Brouillon" + download_archive: Télécharger status_filter: all_statuses: "Tous les statuts" select_status: "Sélectionnez un statut" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 127ae2ed1..bfe88a627 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -151,6 +151,7 @@ en: employed_materials: "Employed materials" employed_machines: "Employed machines" collaborators: "Collaborators" + author: Author creative_commons_licences: "Creative Commons licences" licence: "Licence" themes: "Themes" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index 40816e060..2c655d255 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -151,6 +151,7 @@ fr: employed_materials: "Matériaux utilisés" employed_machines: "Machines utilisées" collaborators: "Les collaborateurs" + author: Auteur creative_commons_licences: "Licences Creative Commons" licence: "Licence" themes: "Thématiques" diff --git a/test/fixtures/project_steps.yml b/test/fixtures/project_steps.yml index cc2516cba..96d44b4fb 100644 --- a/test/fixtures/project_steps.yml +++ b/test/fixtures/project_steps.yml @@ -7,6 +7,7 @@ project_step_1: created_at: 2016-04-04 15:39:08.259759000 Z updated_at: 2016-04-04 15:39:08.259759000 Z title: Le manche + step_nb: 1 project_step_2: id: 2 @@ -16,3 +17,4 @@ project_step_2: created_at: 2016-04-04 15:39:08.265840000 Z updated_at: 2016-04-04 15:39:08.265840000 Z title: La presse + step_nb: 2 \ No newline at end of file diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml index 07e635e93..cec0d59b7 100644 --- a/test/fixtures/projects.yml +++ b/test/fixtures/projects.yml @@ -12,3 +12,4 @@ project_1: state: published slug: presse-puree published_at: 2016-04-04 15:39:08.267614000 Z + status_id: 1 \ No newline at end of file diff --git a/test/integration/projects_test.rb b/test/integration/projects_test.rb new file mode 100644 index 000000000..60477000f --- /dev/null +++ b/test/integration/projects_test.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ProjectsTest < ActionDispatch::IntegrationTest + def setup + @admin = User.find_by(username: 'admin') + login_as(@admin, scope: :user) + end + + test 'download markdown file' do + get "/api/projects/1/markdown" + + assert_response :success + assert_equal "text/markdown", response.content_type + end +end \ No newline at end of file diff --git a/test/services/project_to_markdown_test.rb b/test/services/project_to_markdown_test.rb new file mode 100644 index 000000000..388f615f1 --- /dev/null +++ b/test/services/project_to_markdown_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ProjectToMarkdownTest < ActiveSupport::TestCase + test "ProjectToMarkdown is working" do + project = projects(:project_1) + service = ProjectToMarkdown.new(project) + + markdown_str = nil + + assert_nothing_raised do + markdown_str = service.call + end + + assert_includes markdown_str, project.name + project.project_steps.each do |project_step| + assert_includes markdown_str, project_step.title + end + end +end