diff --git a/.env b/.env index b11bc3d..b24b3fc 100644 --- a/.env +++ b/.env @@ -12,3 +12,8 @@ REDFLAGS_FAQ_PAGE_ID=4410 REDFLAGS_STATS_PAGE_ID=8778 ROLLBAR_ACCESS_TOKEN= + +GOOGLE_APPLICATION_CREDENTIALS= +GOOGLE_SHEET_ID= +GOOGLE_SHEET_EXPORT_ID= +GOOGLE_SHEET_SCRIPT_URL= diff --git a/Gemfile b/Gemfile index 0961090..d1e5e00 100644 --- a/Gemfile +++ b/Gemfile @@ -48,6 +48,11 @@ gem 'annotate' gem 'rollbar' gem 'oj' +# Use Google API gems +gem 'google-apis-docs_v1' +gem 'google-apis-drive_v3' +gem 'google-apis-sheets_v4' + group :development, :test do gem 'dotenv-rails' gem 'rspec-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 1d21add..b664aaa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,6 +46,7 @@ GEM arel (8.0.0) autoprefixer-rails (9.7.6) execjs + base64 (0.2.0) bindex (0.8.1) bootstrap (4.0.0) autoprefixer-rails (>= 6.0.3) @@ -75,10 +76,11 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.6) + declarative (0.0.20) diff-lcs (1.3) - discourse_api (0.40.0) - faraday (~> 0.9) - faraday_middleware (~> 0.10) + discourse_api (1.1.0) + faraday (~> 1.0) + faraday_middleware (~> 1.0) rack (>= 1.6) docile (1.3.2) dotenv (2.7.5) @@ -93,17 +95,41 @@ GEM factory_bot_rails (5.2.0) factory_bot (~> 5.2.0) railties (>= 4.2.0) - faraday (0.17.3) + faraday (1.0.1) multipart-post (>= 1.2, < 3) - faraday_middleware (0.14.0) - faraday (>= 0.7.4, < 1.0) + faraday_middleware (1.2.0) + faraday (~> 1.0) ffi (1.12.2) font-awesome-rails (4.7.0.5) railties (>= 3.2, < 6.1) foreman (0.87.1) globalid (0.4.2) activesupport (>= 4.2.0) + google-apis-core (0.15.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 1.9) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-docs_v1 (0.27.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-drive_v3 (0.51.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-sheets_v4 (0.32.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-env (2.1.1) + faraday (>= 1.0, < 3.a) + googleauth (1.11.0) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) hashdiff (1.0.1) + httpclient (2.8.3) i18n (1.8.2) concurrent-ruby (~> 1.0) jbuilder (2.10.0) @@ -113,6 +139,8 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (2.3.0) + jwt (2.8.1) + base64 kaminari (1.2.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.1) @@ -138,14 +166,16 @@ GEM mini_mime (1.0.2) mini_portile2 (2.7.1) minitest (5.14.1) + multi_json (1.15.0) multipart-post (2.1.1) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) - nio4r (2.5.8) + nio4r (2.7.3) nokogiri (1.13.1) mini_portile2 (~> 2.7.0) racc (~> 1.4) oj (3.10.6) + os (1.1.4) pg (0.21.0) popper_js (1.16.0) public_suffix (4.0.6) @@ -190,6 +220,13 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) regexp_parser (1.7.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.8) + strscan (>= 3.0.9) rollbar (2.25.0) rspec-core (3.9.2) rspec-support (~> 3.9.3) @@ -222,6 +259,11 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) simplecov (0.17.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -243,14 +285,17 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + strscan (3.1.0) thor (1.0.1) thread_safe (0.3.6) tilt (2.0.10) + trailblazer-option (0.1.2) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) tzinfo (1.2.7) thread_safe (~> 0.1) + uber (0.1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) vcr (6.0.0) @@ -284,6 +329,9 @@ DEPENDENCIES factory_bot_rails font-awesome-rails foreman + google-apis-docs_v1 + google-apis-drive_v3 + google-apis-sheets_v4 jbuilder (~> 2.5) jquery-rails kaminari diff --git a/app/assets/javascripts/projects.js b/app/assets/javascripts/projects.js index 002297e..e271ce9 100644 --- a/app/assets/javascripts/projects.js +++ b/app/assets/javascripts/projects.js @@ -2,3 +2,22 @@ $(document).on('ready turbolinks:load', function () { $('[data-toggle="tooltip"]').tooltip(); $('.tag-tooltip').tooltip(); }); + +$(document).ready(function(){ + $('[data-toggle="collapse"]').on('click', function() { + $(this).find('.collapsed,.expanded').toggleClass('d-none'); + }); +}); + +document.addEventListener("turbolinks:load", function() { + document.addEventListener("turbolinks:load", function() { + if (!window.printCalled && window.location.pathname.endsWith("/pdf")) { + window.printCalled = true; + window.print(); + } + }); + + document.addEventListener("turbolinks:before-render", function() { + window.printCalled = false; + }); +}); diff --git a/app/controllers/admin/pages_controller.rb b/app/controllers/admin/pages_controller.rb index 4bb7cf7..6d2c907 100644 --- a/app/controllers/admin/pages_controller.rb +++ b/app/controllers/admin/pages_controller.rb @@ -2,59 +2,67 @@ class Admin::PagesController < AdminController before_action :load_page, only: [:show, :preview, :publish, :unpublish, :sync_one] def index - @pages = Page.order(id: :desc).page(params[:page]) - @projects = @pages.map { |page| Project.find_by(page: page) } + @pages = Page.includes(phase: :project).order(id: :desc) + @projects = @pages.map { |page| page.phase.project }.uniq end def show @revisions = @page.revisions.order(version: :desc).page(params[:page]) - @project = Project.find_by(page: @page) + @project = @page.phase.project end def preview - @project = Project.find_by(page: @page) + @phase = @page.phase if @project.present? if params['version'] == 'latest' - @revision = ProjectRevision.find_by!(revision: @page.latest_revision) + @phase_revision = @phase.revisions.find_by!(revision: @page.latest_revision) else - @revision = ProjectRevision.joins(:project, :revision).find_by!(projects: { page: @page }, revisions: { version: params['version'] }) + @phase_revision = @phase.revisions.find_by!(revision: @page.revisions.where(version: params['version'])) end - @ratings_by_type = @revision.ratings.index_by(&:rating_type) + @ratings_by_type = @phase.revisions.ratings.index_by(&:rating_type) else if params['version'] == 'latest' - @revision = @page.latest_revision + @phase_revision = PhaseRevision.find_by(revision: @page.latest_revision) else - @revision = @page.revisions.find_by!(version: params['version']) + @phase_revision = PhaseRevision.joins(:revision) + .find_by(revisions: { version: params['version'] }) end end end def publish if params['version'] == 'latest' - @page.update!(published_revision: @page.latest_revision) + revision = @page.latest_revision + @page.update!(published_revision: revision) else - @page.update!(published_revision: @page.revisions.find_by!(version: params['version'])) + revision = @page.revisions.find_by!(version: params['version']) + @page.update!(published_revision: revision) end + @page.publish_and_enqueue_jobs(revision) + redirect_back fallback_location: { action: :index } end def unpublish + revision_id = @page.published_revision.id if @page.published_revision.present? + @page.update!(published_revision: nil) + @page.unpublish_and_enqueue_jobs(revision_id) redirect_back fallback_location: { action: :index } end def sync - SyncCategoryTopicsJob.perform_later(ENV.fetch('REDFLAGS_CATEGORY_SLUG')) + SyncAllTopicsJob.perform_later redirect_back fallback_location: { action: :index } end def sync_one - SyncTopicJob.perform_later(@page.id) + SyncOneTopicJob.perform_later(@page.id) redirect_back fallback_location: { action: :index } end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 70cae5c..a7403a2 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,18 +1,9 @@ class Admin::ProjectsController < AdminController - before_action :load_project, only: [:update] - - def update - @project.update!(project_params) - redirect_to admin_page_path(@project.page) - end + before_action :load_project private def load_project @project = Project.find(params[:id]) end - - def project_params - params.require(:project).permit(:category) - end end diff --git a/app/controllers/phase_revision_controller.rb b/app/controllers/phase_revision_controller.rb new file mode 100644 index 0000000..82a4b62 --- /dev/null +++ b/app/controllers/phase_revision_controller.rb @@ -0,0 +1,43 @@ +class PhaseRevisionController < ApplicationController + def show + @project = Project.find_by!(id: params[:project_id]) + @phase_revision = PhaseRevision.find_published_revision(@project.id, params[:revision_type]) + + if @phase_revision + @revision = @phase_revision.revision + @ratings_by_type = @phase_revision.ratings.index_by(&:rating_type) + @metadata.og.title = @revision.title + @metadata.og.description = 'Kolaboratívne hodnotenie projektu metodikou Red Flags.' + + render :show + end + end + + def pdf + @project = Project.find_by!(id: params[:project_id]) + @phase_revision = PhaseRevision.find_published_revision(@project.id, params[:revision_type]) + + if @phase_revision + @revision = @phase_revision.revision + @ratings_by_type = @phase_revision.ratings.index_by(&:rating_type) + @metadata.og.title = @revision.title + @metadata.og.description = 'Kolaboratívne hodnotenie projektu metodikou Red Flags.' + + render layout: "no_header_footer" + end + end + + def show_history + @project = Project.find_by!(id: params[:project_id]) + @phase_revision = PhaseRevision.find_revision_history(@project.id, params[:revision_type], params[:version]) + + if @phase_revision + @revision = @phase_revision.revision + @ratings_by_type = @phase_revision.ratings.index_by(&:rating_type) + @metadata.og.title = @revision.title + @metadata.og.description = 'Kolaboratívne hodnotenie projektu metodikou Red Flags.' + + render :show + end + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 9d77b17..83b39cd 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,15 +1,7 @@ class ProjectsController < ApplicationController - def show - @project = Project.find(params[:id]).published_revision - @ratings_by_type = @project.ratings.index_by(&:rating_type) - @metadata.og.title = @project.title - @metadata.og.description = 'Kolaboratívne hodnotenie projektu metodikou Red Flags.' - end def index @selected_tag = params[:tag] - @projects = Project.published - @projects = @projects.with_tag(params[:tag]) if ProjectsHelper::ALLOWED_TAGS.keys.include?(params[:tag]) - @projects = @projects.map { |p| p.published_revision }.sort_by(&:aggregated_rating) + @projects = Project.filtered_projects(@selected_tag, params[:sort]) end end diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 32d9d3e..4cd8d44 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -1,9 +1,9 @@ class StaticController < ApplicationController def index - top_projects = Project.published.joins(:published_revision).limit(5) + top_project_revisions = PhaseRevision.where(published: true).joins(:revision).limit(5) - @good_projects = top_projects.good.order('redflags_count ASC, total_score::float / maximum_score DESC').map(&:published_revision) - @bad_projects = top_projects.bad.order('redflags_count DESC, total_score::float / maximum_score ASC').map(&:published_revision) + @good_projects = top_project_revisions.where('phase_revisions.redflags_count = ?', 0).order('phase_revisions.total_score::float / phase_revisions.maximum_score DESC') + @bad_projects = top_project_revisions.order('phase_revisions.redflags_count DESC, phase_revisions.total_score::float / phase_revisions.maximum_score ASC') end def about diff --git a/app/helpers/previews_helper.rb b/app/helpers/previews_helper.rb index c12c886..d8a7882 100644 --- a/app/helpers/previews_helper.rb +++ b/app/helpers/previews_helper.rb @@ -4,8 +4,11 @@ def page_preview?(page) end def revision_preview?(revision) - return true unless Project.exists?(page: revision.page) - project_revision = ProjectRevision.where(revision: revision).first - project_revision && !project_revision.total_score_percentage.nan? + return true unless revision.phase_revision&.phase&.project.present? + + phase_revision = revision.phase_revision + return false if phase_revision.nil? + + phase_revision.total_score.present? && !phase_revision.total_score_percentage.nan? end end diff --git a/app/jobs/export_topic_into_sheet_job.rb b/app/jobs/export_topic_into_sheet_job.rb new file mode 100644 index 0000000..883f2e8 --- /dev/null +++ b/app/jobs/export_topic_into_sheet_job.rb @@ -0,0 +1,171 @@ +class ExportTopicIntoSheetJob < ApplicationJob + queue_as :default + + COLUMN_NAMES = [ + 'Projekt', 'Projekt ID', 'ID hodnotenia prípravy', 'Link na hodnotenie prípravy', 'ID hodnotenia produktu', 'Link na hodnotenie produktu', + 'Názov-Príprava', 'Garant-Príprava', 'Stručný opis-Príprava', 'Náklady na projekt-Príprava', 'Aktuálny stav projektu-Príprava', + 'Čo sa práve deje-Príprava', 'Zhrnutie hodnotenia Red Flags-Príprava', 'Stanovisko Slovensko.Digital-Príprava', 'Reforma VS body-Príprava', + 'Reforma VS-Príprava', 'Merateľné ciele (KPI) body-Príprava', 'Merateľné ciele (KPI)-Príprava', 'Postup dosiahnutia cieľov body-Príprava', + 'Postup dosiahnutia cieľov-Príprava', 'Súlad s KRIT body-Príprava', 'Súlad s KRIT-Príprava', 'Biznis prínos body-Príprava', + 'Biznis prínos-Príprava', 'Príspevok v informatizácii body-Príprava', 'Príspevok v informatizácii-Príprava', 'Kalkulácia efektívnosti body-Príprava', + 'Kalkulácia efektívnosti-Príprava', 'Transparentnosť a participácia body-Príprava', 'Transparentnosť a participácia-Príprava', 'Názov-Produkt', + 'Garant-Produkt', 'Stručný opis-Produkt', 'Náklady na projekt-Produkt', 'Aktuálny stav projektu-Produkt', 'Čo sa práve deje-Produkt', + 'Zhrnutie hodnotenia Red Flags-Produkt', 'Stanovisko Slovensko.Digital-Produkt', 'Reforma VS body-Produkt', 'Reforma VS-Produkt', + 'Merateľné ciele (KPI) body-Produkt', 'Merateľné ciele (KPI)-Produkt', 'Postup dosiahnutia cieľov body-Produkt', 'Postup dosiahnutia cieľov-Produkt', + 'Súlad s KRIT body-Produkt', 'Súlad s KRIT-Produkt', 'Biznis prínos body-Produkt', 'Biznis prínos-Produkt', 'Príspevok v informatizácii body-Produkt', + 'Príspevok v informatizácii-Produkt', 'Kalkulácia efektívnosti body-Produkt', 'Kalkulácia efektívnosti-Produkt', 'Transparentnosť a participácia body-Produkt', + 'Transparentnosť a participácia-Produkt', 'Súlad s požiadavkami body-Produkt', 'Súlad s požiadavkami-Produkt', 'Elektronické služby body-Produkt', + 'Elektronické služby-Produkt', 'Identifikácia, autentifikácia, autorizácia (IAA) body-Produkt', 'Identifikácia, autentifikácia, autorizácia (IAA)-Produkt', + 'Riadenie údajov body-Produkt', 'Riadenie údajov-Produkt', 'OpenData body-Produkt', 'OpenData-Produkt', 'MyData body-Produkt', 'MyData-Produkt', + 'OpenAPI body-Produkt', 'OpenAPI-Produkt', 'Zdrojový kód body-Produkt', 'Zdrojový kód-Produkt' + ].freeze + + RATINGS_CONSTANT = { + "Reforma VS" => "Reforma VS body", + "Merateľné ciele (KPI)" => "Merateľné ciele (KPI) body", + "Postup dosiahnutia cieľov" => "Postup dosiahnutia cieľov body", + "Súlad s KRIT" => "Súlad s KRIT body", + "Biznis prínos" => "Biznis prínos body", + "Príspevok v informatizácii" => "Príspevok v informatizácii body", + "Kalkulácia efektívnosti" => "Kalkulácia efektívnosti body", + "Transparentnosť a participácia" => "Transparentnosť a participácia body", + "Súlad s požiadavkami" => "Súlad s požiadavkami body", + "Elektronické služby" => "Elektronické služby body", + "Identifikácia, autentifikácia, autorizácia (IAA)" => "Identifikácia, autentifikácia, autorizácia (IAA) body", + "Riadenie údajov" => "Riadenie údajov body", + "OpenData" => "OpenData body", + "MyData" => "MyData body", + "OpenAPI" => "OpenAPI body", + "Zdrojový kód" => "Zdrojový kód body" + } + + def perform(new_revision) + if new_revision.published? + update_sheet(new_revision) + else + delete_row(new_revision) + end + end + + private + + def update_sheet(new_revision) + suffix = new_revision.phase.phase_type.name == 'Prípravná fáza' ? '-Príprava' : '-Produkt' + + result = extract_content_from_html(new_revision.body_html) + ratings = new_revision.ratings.includes(:rating_type).index_by { |rating| rating.rating_type.name } + + result["Názov"] = new_revision.title + result["Garant"] = new_revision.guarantor + result["Stručný opis"] = new_revision.description + result["Náklady na projekt"] = new_revision.budget + result["Aktuálny stav projektu"] = new_revision.stage.name + result["Čo sa práve deje"] = new_revision.current_status + result["Zhrnutie hodnotenia Red Flags"] = new_revision.summary + result["Stanovisko Slovensko.Digital"] = new_revision.recommendation + + RATINGS_CONSTANT.each do |type_name, result_field_name| + result[result_field_name] = ratings[type_name]&.score || 'N/A' + end + + result.transform_keys! { |k| k + suffix } + + result["Projekt"] = new_revision.title + result["Projekt ID"] = new_revision.phase.project_id + + if new_revision.phase.phase_type.name == 'Prípravná fáza' + result["ID hodnotenia prípravy"] = new_revision.revision.page.id + result["Link na hodnotenie prípravy"] = %(=HYPERLINK("https://redflags.slovensko.digital/admin/pages/#{new_revision.revision.page.id}"; "Hodnotenie v adminovi")) + else + result["ID hodnotenia produktu"] = new_revision.revision.page.id + result["Link na hodnotenie produktu"] = %(=HYPERLINK("https://redflags.slovensko.digital/admin/pages/#{new_revision.revision.page.id}"; "Hodnotenie v adminovi")) + end + + sheets_service = GoogleApiService.get_sheets_service + response = sheets_service.get_spreadsheet_values(ENV['GOOGLE_SHEET_EXPORT_ID'], 'A:CA') + header_row = response.values[2] + current_row_count = response.values.count + column_indices = COLUMN_NAMES.map { |name| header_row.index(name) } + + values = COLUMN_NAMES.map { |name| result[name] } + + if find_row_index_by_project_id(response.values[3..], header_row, new_revision.phase.project_id) + range = "Hárok1!#{column_letter(column_indices.min + 1)}#{current_row_count}:#{column_letter(column_indices.max + 1)}#{current_row_count}" + else + range = "Hárok1!#{column_letter(column_indices.min + 1)}#{current_row_count + 1}:#{column_letter(column_indices.max + 1)}#{current_row_count + 1}" + end + update_google_sheet(sheets_service, ENV['GOOGLE_SHEET_EXPORT_ID'], column_indices, values, range) + end + + def delete_row(new_revision) + sheets_service = GoogleApiService.get_sheets_service + response = sheets_service.get_spreadsheet_values(ENV['GOOGLE_SHEET_EXPORT_ID'], 'A:BA') + header_row = response.values[2] + + row_index = find_row_index(response.values[3..-1], header_row, new_revision.revision.page.id) + + if row_index + delete_google_sheet_row(sheets_service, ENV['GOOGLE_SHEET_EXPORT_ID'], row_index + 3) + else + raise ArgumentError, "No data found for the given page_id in the spreadsheet. ID may not match or is not in string format." + end + end + + def extract_content_from_html(body_html) + result = {} + doc = Nokogiri::HTML(body_html) + doc.css('h3').each do |header| + siblings = header.xpath('following-sibling::*[not(self::div)]').take_while { |node| node.name != 'h3' } + content = siblings.map { |sibling| sibling.text.strip }.join(' ') + result[header.text.strip] = content + end + result + end + + def update_google_sheet(sheets_service, google_sheet_id, column_indices, values, range) + value_range_object = Google::Apis::SheetsV4::ValueRange.new(values: [values]) + sheets_service.update_spreadsheet_value(google_sheet_id, range, value_range_object, value_input_option: 'USER_ENTERED') + end + + def delete_google_sheet_row(sheets_service, google_sheet_id, row_index) + request_body = Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest.new( + requests: [ + { + delete_dimension: { + range: { + sheet_id: get_sheet_id(sheets_service, google_sheet_id), + dimension: 'ROWS', + start_index: row_index - 1, + end_index: row_index + } + } + } + ] + ) + sheets_service.batch_update_spreadsheet(google_sheet_id, request_body) + end + + def get_sheet_id(sheets_service, google_sheet_id) + spreadsheet = sheets_service.get_spreadsheet(google_sheet_id) + spreadsheet.sheets.first.properties.sheet_id + end + + def find_row_index(rows, header_row, page_id) + id_index = header_row.index('ID hodnotenia') + rows.find_index { |row| row[id_index] == page_id.to_s } + end + + def find_row_index_by_project_id(rows, header_row, project_id) + project_id_index = header_row.index('Projekt ID') + rows.find_index { |row| row[project_id_index] == project_id.to_s } + end + + def column_letter(number) + letter = '' + while number > 0 + number, remainder = (number - 1).divmod(26) + letter = (65 + remainder).chr + letter + end + letter + end +end diff --git a/app/jobs/initialization_of_topics_to_sheets_job.rb b/app/jobs/initialization_of_topics_to_sheets_job.rb new file mode 100644 index 0000000..dbc78d0 --- /dev/null +++ b/app/jobs/initialization_of_topics_to_sheets_job.rb @@ -0,0 +1,55 @@ +class InitializationOfTopicsToSheetsJob < ApplicationJob + queue_as :default + + COLUMN_NAMES = ['Projekt', 'Projekt ID', 'Platforma', 'Dátum poslednej aktualizácie', 'Draft prípravy', 'ID draft prípravy', 'ID prípravy', 'Príprava publikovaná?', 'Dátum publikácie prípravy', 'RF web príprava'].freeze + + def perform(topic_id, found_page, project) + uri = URI(ENV['GOOGLE_SHEET_SCRIPT_URL']) + Net::HTTP.get(uri) + + sheets_service = GoogleApiService.get_sheets_service + response_values = fetch_response_values(sheets_service) + column_indices = get_column_indices(response_values[2]) + + values = construct_values(topic_id, found_page, project) + + update_sheet_cells(sheets_service, column_indices, response_values.count, values) + + ExportTopicIntoSheetJob.set(wait: 15.seconds).perform_later(found_page.published_revision.phase_revision) if found_page.published_revision.present? + end + + private + + def fetch_response_values(sheets_service) + response = sheets_service.get_spreadsheet_values(ENV['GOOGLE_SHEET_ID'], 'A:Z') + response.values + end + + def get_column_indices(header_row) + COLUMN_NAMES.map { |name| header_row.index(name) } + end + + def construct_values(topic_id, found_page, project) + title_parametrized = found_page.latest_revision.title.parameterize + [ + found_page.latest_revision.title, + project.id, + %(=HYPERLINK("https://platforma.slovensko.digital/t/#{title_parametrized}/#{topic_id}"; "Platforma link")), + found_page.published_revision&.updated_at&.in_time_zone('Europe/Bratislava')&.strftime('%H:%M %d.%m.%Y') || '', + '', + '', + topic_id.to_s, + found_page.published_revision.present? ? 'Áno' : 'Nie', + found_page.published_revision&.updated_at&.in_time_zone('Europe/Bratislava')&.strftime('%H:%M %d.%m.%Y') || '', + found_page.published_revision.present? ? %(=HYPERLINK("https://redflags.slovensko.digital/admin/pages/#{found_page.id}"; "Admin link")) : '' + ] + end + + def update_sheet_cells(sheets_service, indices, current_row_count, values) + values.each_with_index do |value, idx| + range = "Hárok1!#{(indices[idx] + 65).chr}#{current_row_count}" + value_range_object = Google::Apis::SheetsV4::ValueRange.new(values: [[value]]) + sheets_service.update_spreadsheet_value(ENV['GOOGLE_SHEET_ID'], range, value_range_object, value_input_option: 'USER_ENTERED') + end + end +end \ No newline at end of file diff --git a/app/jobs/sync_all_topics_job.rb b/app/jobs/sync_all_topics_job.rb new file mode 100644 index 0000000..8c67207 --- /dev/null +++ b/app/jobs/sync_all_topics_job.rb @@ -0,0 +1,43 @@ +class SyncAllTopicsJob < ApplicationJob + queue_as :default + + COLUMN_NAMES = ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'].freeze + + def perform + sheets_service = GoogleApiService.get_sheets_service + response_values = sheets_service.get_spreadsheet_values(ENV.fetch('GOOGLE_SHEET_ID'), 'A:Z')&.values + + indices = find_indices(response_values[2]) + response_values[3..-1].each { |row| process_row(row, indices) } + end + + private + + def find_indices(header_row) + indices = COLUMN_NAMES.map { |name| [name, header_row.index(name)] }.to_h + return indices if indices.values.all? + + raise ArgumentError, "Could not find required columns in the spreadsheet." + end + + def process_row(row, indices) + project_name = row[indices["Projekt"]] + project_id = row[indices["Projekt ID"]] + platform_link = row[indices["Platforma"]] + preparation_document_id = row[indices["ID draft prípravy"]] + preparation_page_id = row[indices["ID prípravy"]] + product_document_id = row[indices["ID draft produktu"]] + product_page_id = row[indices["ID produktu"]] + + if platform_link != '' + SyncTopicJob.perform_later(project_id, preparation_page_id) + else + enqueue_job_for_update("#{project_name} - Príprava", project_id, preparation_document_id, preparation_page_id, 'Prípravná fáza') + end + enqueue_job_for_update("#{project_name} - Produkt", project_id, product_document_id, product_page_id, 'Fáza produkt') + end + + def enqueue_job_for_update(name, project_id, document_id, page_id, page_type) + SyncGoogleDocumentJob.perform_later(name, project_id, document_id, page_id, page_type) + end +end diff --git a/app/jobs/sync_category_topics_job.rb b/app/jobs/sync_category_topics_job.rb index fcaaf6d..5976dfd 100644 --- a/app/jobs/sync_category_topics_job.rb +++ b/app/jobs/sync_category_topics_job.rb @@ -9,7 +9,7 @@ def perform(category_slug, page = 0, sync_topic_job: SyncTopicJob) SyncCategoryTopicsJob.perform_later(category_slug, page + 1) if topics.count >= 30 topics.each do |topic| - sync_topic_job.perform_later(topic.fetch('id')) + sync_topic_job.perform_later(nil, topic.fetch('id')) end end end diff --git a/app/jobs/sync_google_document_job.rb b/app/jobs/sync_google_document_job.rb new file mode 100644 index 0000000..1d3453e --- /dev/null +++ b/app/jobs/sync_google_document_job.rb @@ -0,0 +1,72 @@ +class SyncGoogleDocumentJob < ApplicationJob + queue_as :default + + def perform(project_name, project_id, google_document_id, page_id, phase_type_name) + parsed_content = parse_google_doc(google_document_id) + create_or_update_page(project_name, project_id, google_document_id, page_id, phase_type_name, parsed_content) + end + + def create_or_update_page(project_name, project_id, google_document_id, page_id, phase_type_name, parsed_content) + Page.transaction do + page = Page.find_or_create_by!(id: page_id) do |new_page| + new_project = Project.find_or_create_by(id: project_id) + phase_type = PhaseType.find_by(name: phase_type_name) + new_phase = Phase.find_or_create_by(project: new_project, phase_type: phase_type) + new_page.phase = new_phase + end + + drive_service = GoogleApiService.get_drive_service + revisions = drive_service.list_revisions(google_document_id)&.revisions&.count + version = revisions + + revision = page.revisions.find_or_initialize_by(version: version) + revision.title = project_name + revision.raw = parsed_content + revision.save! + + page.latest_revision = revision + page.save! + end + +=begin + Page.transaction do + page = setup_page(project_id, page_id) + revision = setup_revision(page, project_name, parsed_content, google_document_id) + update_page_info(page, page_type, revision) + end +=end + end + + def setup_page(project_id, page_id) + Page.find_or_create_by!(id: page_id) do |new_page| + new_project = Project.find_or_create_by!(id: project_id) + new_page.project = new_project + end + end + + def setup_revision(page, project_name, parsed_content, google_document_id) + drive_service = GoogleApiService.get_drive_service + revisions = drive_service.list_revisions(google_document_id)&.revisions&.count + version = revisions + revision = page.revisions.find_or_initialize_by(version: version) + revision.title = project_name + revision.raw = parsed_content + revision.load_ratings(parsed_content) + revision.save! + revision + end + + def update_page_info(page, page_type, revision) + page.latest_revision = revision + page.page_type = page_type + page.save! + end + + def parse_google_doc(google_document_id) + document = GoogleApiService.get_document(google_document_id) + parser_service = DocumentParserService.new(document) + + html_content = parser_service.to_html + parser_service.to_hash(html_content) + end +end \ No newline at end of file diff --git a/app/jobs/sync_one_topic_job.rb b/app/jobs/sync_one_topic_job.rb new file mode 100644 index 0000000..460fcc7 --- /dev/null +++ b/app/jobs/sync_one_topic_job.rb @@ -0,0 +1,50 @@ +class SyncOneTopicJob < ApplicationJob + queue_as :default + + COLUMN_NAMES = ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'].freeze + + def perform(page_id) + sheets_service = GoogleApiService.get_sheets_service + response_values = sheets_service.get_spreadsheet_values(ENV.fetch('GOOGLE_SHEET_ID'), 'A:Z')&.values + + indices = find_indices(response_values[2]) + row = response_values[3..-1].find do |row| + row[indices["ID prípravy"]] == page_id.to_s || row[indices["ID produktu"]] == page_id.to_s + end + + process_row(row, indices, page_id) if row + end + + private + + def find_indices(header_row) + indices = COLUMN_NAMES.map { |name| [name, header_row.index(name)] }.to_h + return indices if indices.values.all? + + raise ArgumentError, "Could not find required columns in the spreadsheet." + end + + def process_row(row, indices, target_id) + project_name = row[indices["Projekt"]] + project_id = row[indices["Projekt ID"]] + platform_link = row[indices["Platforma"]] + preparation_document_id = row[indices["ID draft prípravy"]] + preparation_page_id = row[indices["ID prípravy"]] + product_document_id = row[indices["ID draft produktu"]] + product_page_id = row[indices["ID produktu"]] + + if platform_link != '' + SyncTopicJob.perform_later(project_id, preparation_page_id) + else + if target_id == preparation_page_id.to_i + enqueue_job_for_update("#{project_name} - Príprava", project_id, preparation_document_id, preparation_page_id, 'Prípravná fáza') + else target_id == product_page_id.to_i + enqueue_job_for_update("#{project_name} - Produkt", project_id, product_document_id, product_page_id, 'Fáza produkt') + end + end + end + + def enqueue_job_for_update(name, project_id, document_id, page_id, page_type) + SyncGoogleDocumentJob.perform_later(name, project_id, document_id, page_id, page_type) + end +end diff --git a/app/jobs/sync_revision_job.rb b/app/jobs/sync_revision_job.rb index 920b262..5a664b5 100644 --- a/app/jobs/sync_revision_job.rb +++ b/app/jobs/sync_revision_job.rb @@ -3,13 +3,14 @@ class SyncRevisionJob < ApplicationJob def perform(revision) return if revision.raw['category_id'] != ENV.fetch('REDFLAGS_PROJECTS_CATEGORY_ID').to_i - Project.transaction do + + Phase.transaction do page = revision.page - project = Project.find_or_create_by!(id: page.id, page_id: page.id) + phase = page.phase - project_revision = project.revisions.find_or_initialize_by(revision_id: revision.id) - project_revision.load_from_data(revision.raw) - project_revision.save! + phase_revision = phase.revisions.find_or_initialize_by(revision_id: revision.id) + phase_revision.load_from_data(revision.raw) + phase_revision.save! end end end diff --git a/app/jobs/sync_topic_job.rb b/app/jobs/sync_topic_job.rb index febe201..ac32ef9 100644 --- a/app/jobs/sync_topic_job.rb +++ b/app/jobs/sync_topic_job.rb @@ -1,15 +1,20 @@ class SyncTopicJob < ApplicationJob queue_as :default - def perform(topic_id) + def perform(project_id, topic_id) client = DiscourseApi::Client.new(ENV.fetch('REDFLAGS_DISCOURSE_URL')) topic = client.topic(topic_id) + page = nil + project = nil Page.transaction do + page = Page.find_or_create_by!(id: topic_id) do |new_page| + project = Project.find_or_create_by(id: project_id) + phase_type = PhaseType.find_by(name: 'Prípravná fáza') + new_phase = Phase.find_or_create_by(project: project, phase_type: phase_type) + new_page.phase = new_phase + end - # TODO latest_revision_id should be not null on DB level - - page = Page.find_or_create_by!(id: topic_id) version = topic['post_stream']['posts'].first['version'] revision = page.revisions.find_or_initialize_by(version: version) @@ -21,5 +26,8 @@ def perform(topic_id) page.latest_revision = revision page.save! end + + # For initial import of current topics into Google Sheets + #InitializationOfTopicsToSheetsJob.set(wait: 15.seconds).perform_later(topic_id, page, project) end end diff --git a/app/jobs/update_multiple_sheet_columns_job.rb b/app/jobs/update_multiple_sheet_columns_job.rb new file mode 100644 index 0000000..c489b47 --- /dev/null +++ b/app/jobs/update_multiple_sheet_columns_job.rb @@ -0,0 +1,18 @@ +class UpdateMultipleSheetColumnsJob < ApplicationJob + queue_as :default + + def perform(page_id, updates) + updates.each do |update| + column_names = update[:column_names] + page_type = update[:page_type] + published_value = update[:published_value] + + UpdateSheetValueJob.perform_now( + page_id, + column_names, + page_type, + published_value + ) + end + end +end \ No newline at end of file diff --git a/app/jobs/update_sheet_value_job.rb b/app/jobs/update_sheet_value_job.rb new file mode 100644 index 0000000..1f361c6 --- /dev/null +++ b/app/jobs/update_sheet_value_job.rb @@ -0,0 +1,54 @@ +class UpdateSheetValueJob < ApplicationJob + queue_as :default + + REQUIRED_COLUMNS = ['ID prípravy', 'ID produktu'].freeze + + def perform(page_id, column_names, page_type, published_value) + sheets_service = GoogleApiService.get_sheets_service + response_values = sheets_service.get_spreadsheet_values(ENV['GOOGLE_SHEET_ID'], 'A:Z')&.values + + header_row = response_values[2] + indices = find_indices(header_row) + + row_index = find_row_index(response_values[3..-1], indices, page_id) + if row_index.nil? + raise ArgumentError, "No data found for the given page_id in the spreadsheet. ID may not match or is not in string format." + end + + match_column_name = column_names[page_type] + if match_column_name + handle_row(sheets_service, ENV['GOOGLE_SHEET_ID'], header_row, row_index, match_column_name, published_value) + else + raise ArgumentError, "No matching column for page type." + end + end + + private + + def find_indices(header_row) + indices = REQUIRED_COLUMNS.map { |name| [name, header_row.index(name)] }.to_h + return indices if indices.values.all? + + raise ArgumentError, "Could not find required columns in the spreadsheet." + end + + def find_row_index(rows, indices, page_id) + rows.find_index do |row| + row[indices['ID prípravy']] == page_id.to_s || row[indices['ID produktu']] == page_id.to_s + end + end + + def handle_row(sheets_service, google_sheet_id, header_row, row_index, column_name, published_value) + column_index = header_row.index(column_name) + raise ArgumentError, "Could not find the provided column in the spreadsheet." if column_index.nil? + + update_google_sheet(sheets_service, google_sheet_id, row_index, column_index, published_value) + end + + def update_google_sheet(sheets_service, google_sheet_id, row, column_index, value) + range = "Hárok1!#{(column_index + 65).chr}#{row + 4}" + value_range_object = Google::Apis::SheetsV4::ValueRange.new(values: [[value]]) + + sheets_service.update_spreadsheet_value(google_sheet_id, range, value_range_object, value_input_option: 'USER_ENTERED') + end +end diff --git a/app/models/page.rb b/app/models/page.rb index 7128ee4..1e2aef7 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -15,6 +15,8 @@ # class Page < ApplicationRecord + belongs_to :phase + has_many :revisions belongs_to :published_revision, class_name: 'Revision', optional: true @@ -22,8 +24,6 @@ class Page < ApplicationRecord delegate :title, to: :latest_revision - after_save :schedule_sync_project_job - def publishable? true end @@ -36,9 +36,76 @@ def synced? published_revision == latest_revision end - private + def publish_and_enqueue_jobs(revision) + new_revision = update_associated_phase_revision(revision) + + if new_revision + updates = build_publish_updates(new_revision) + UpdateMultipleSheetColumnsJob.perform_later(id, updates) + ExportTopicIntoSheetJob.perform_later(new_revision) + end + end + + def update_associated_phase_revision(revision) + related_revisions = PhaseRevision.where(revision_id: revisions.ids).where.not(revision_id: revision.id) + related_revisions.update_all(published: false) if revision + + new_revision = PhaseRevision.find_by(revision_id: revision.id) + if new_revision + new_revision.update!(published: true, was_published: true, published_at: Time.now) + new_revision + end + end + + def build_publish_updates(revision) + [ + { + column_names: { "Prípravná fáza" => "Príprava publikovaná?", "Fáza produkt" => "Produkt publikovaný?" }, + page_type: revision.phase.phase_type.name, + published_value: "Áno" + }, + { + column_names: { "Prípravná fáza" => "Dátum publikácie prípravy", "Fáza produkt" => "Dátum publikácie produktu" }, + page_type: revision.phase.phase_type.name, + published_value: revision.published_at.in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y') + }, + { + column_names: { "project" => "Dátum poslednej aktualizácie" }, + page_type: "project", + published_value: revision.published_at.in_time_zone('Europe/Bratislava').strftime('%H:%M %d.%m.%Y') + }, + { + column_names: { "Prípravná fáza" => "RF web príprava", "Fáza produkt" => "RF web produkt" }, + page_type: revision.phase.phase_type.name, + published_value: %(=HYPERLINK("https://redflags.slovensko.digital/admin/pages/#{id}"; "Admin link")) + } + ] + end + + def unpublish_and_enqueue_jobs(revision_id) + PhaseRevision.where(revision_id: revision_id).update_all(published: false, published_at: nil) if revision_id + + updates = build_unpublish_updates + UpdateMultipleSheetColumnsJob.perform_later(id, updates) + end - def schedule_sync_project_job - SyncProjectJob.perform_later(self) + def build_unpublish_updates + [ + { + column_names: { "Prípravná fáza" => "Príprava publikovaná?", "Fáza produkt" => "Produkt publikovaný?" }, + page_type: phase.phase_type.name, + published_value: "Nie" + }, + { + column_names: { "Prípravná fáza" => "Dátum publikácie prípravy", "Fáza produkt" => "Dátum publikácie produktu" }, + page_type: phase.phase_type.name, + published_value: "" + }, + { + column_names: { "Prípravná fáza" => "RF web príprava", "Fáza produkt" => "RF web produkt" }, + page_type: phase.phase_type.name, + published_value: "" + } + ] end end diff --git a/app/models/phase.rb b/app/models/phase.rb new file mode 100644 index 0000000..9687a8c --- /dev/null +++ b/app/models/phase.rb @@ -0,0 +1,29 @@ + # == Schema Information + # + # Table name: phases + # + # id :integer not null, primary key + # created_at :datetime not null + # updated_at :datetime not null + # + + class Phase < ApplicationRecord + belongs_to :project + belongs_to :phase_type + + has_many :pages + has_many :revisions, class_name: 'PhaseRevision' + + has_one :published_revision, -> { where(published: true) }, class_name: 'PhaseRevision' + + def phase_type_label + case phase_type.name + when "Prípravná fáza" + "Hodnotenie prípravy" + when "Fáza produkt" + "Hodnotenie produktu" + else + phase_type + end + end + end diff --git a/app/models/phase_revision.rb b/app/models/phase_revision.rb new file mode 100644 index 0000000..53bb001 --- /dev/null +++ b/app/models/phase_revision.rb @@ -0,0 +1,215 @@ +# == Schema Information +# +# Table name: project_revisions +# +# id :integer not null, primary key +# project_id :integer not null +# revision_id :integer not null +# title :string not null +# full_name :string +# guarantor :string +# description :string +# budget :string +# created_at :datetime not null +# updated_at :datetime not null +# body_html :string +# total_score :integer +# maximum_score :integer +# redflags_count :integer default(0) +# summary :text +# recommendation :text +# stage_id :integer +# current_status :string +# total_score :integer +# maximum_score :integer +# redflags_count :integer default(0) +# published :boolean default(false) +# was_published :boolean default(false) +# published_at :datetime +# +# Indexes +# +# index_project_revisions_on_project_id (project_id) +# index_project_revisions_on_revision_id (revision_id) +# index_project_revisions_on_stage_id (stage_id) +# +# Foreign Keys +# +# fk_rails_... (project_id => projects.id) +# fk_rails_... (revision_id => revisions.id) +# fk_rails_... (stage_id => project_stages.id) +# + +class PhaseRevision < ApplicationRecord + belongs_to :phase + belongs_to :revision + belongs_to :stage, class_name: 'ProjectStage', optional: true + + has_many :ratings, class_name: 'PhaseRevisionRating' + + delegate :version, :tags, to: :revision + + scope :published, -> { where(published: true) } + scope :once_published, -> { where(was_published: true, published: false) } + + ROUTE_MAP = { + 'Prípravná fáza' => 'hodnotenie-pripravy', + 'Fáza produkt' => 'hodnotenie-produktu' + }.freeze + + def load_from_data(raw) + self.title = raw['title'].gsub('Red Flags:', '').strip + + body = raw['post_stream']['posts'].first['cooked'] + summary, rest = body.split(/

.+?<\/h1>/m, 2) + + self.body_html = rest + + load_metadata(summary) + load_ratings(rest) + end + + def outdated? + tags.include?('rf-outdated') + end + + def total_score_percentage + 100.0 * total_score / maximum_score + end + + def aggregated_rating + [redflags_count, -total_score_percentage] + end + + private + + def load_metadata(summary) + doc = Nokogiri::HTML.parse(summary) + + metadata_mapping = { + "Názov:" => :full_name, + "Garant:" => :guarantor, + "Stručný opis:" => :description, + "Náklady na projekt:" => :budget, + "Aktuálny stav projektu:" => :stage, + "Čo sa práve deje:" => :current_status, + "Zhrnutie hodnotenia Red Flags:" => :summary, + "Stanovisko Slovensko.Digital:" => :recommendation + } + + current_label = nil + current_status_content = '' + collecting = false + + doc.search('h3, p, ul, li').each do |element| + if element.name == 'h3' + current_label = element.text.strip.chomp(':').strip + + if collecting + assign_value(metadata_mapping["Čo sa práve deje:"], current_status_content) + collecting = false + current_status_content = '' + end + + collecting = true if current_label == "Čo sa práve deje" + end + + if collecting && %w[p ul].include?(element.name) + current_status_content += element.to_html + + elsif element.name == 'p' + strong_element = element.at('strong') + if strong_element + type = strong_element.text.strip.chomp(':') + if type == "Čo sa práve deje" + value = element.next_element.try(:to_html) + else + value = element.text.gsub(strong_element.text, '').strip + end + assign_value(metadata_mapping[type + ":"], value) if metadata_mapping.key?(type + ":") + + elsif current_label + value = element.text.strip + assign_value(metadata_mapping[current_label + ":"], value) if metadata_mapping.key?(current_label + ":") + current_label = nil unless current_label == "Čo sa práve deje:" + end + end + end + + if collecting && metadata_mapping.key?("Čo sa práve deje:") + assign_value(metadata_mapping["Čo sa práve deje:"], current_status_content) + end + end + + def assign_value(attribute, value) + if attribute == :stage + self.stage = ProjectStage.find_by(name: value) + else + self.send("#{attribute}=", value) + end + end + + def load_ratings(body) + redflags_count = 0 + total_score = 0 + maximum_score = 0 + + doc = Nokogiri::HTML.parse(body) + doc.css('h3').each do |heading| + value = heading.text.strip.gsub(/[^0-9A-Za-záäčďéíĺľňóôŕřšťúůýžÁÄČĎÉÍĹĽŇÓÔŔŘŠŤÚŮÝŽ(), ]/, '').strip + rating_type = RatingType.find_by(name: value) + if rating_type + score = heading.css('img.emoji[title=":star:"]').count + bad_score = heading.css('img.emoji[title=":grey_star:"]').count + red_score = heading.css('img.emoji[title=":triangular_flag_on_post:"]').count + if red_score > 0 + bad_score = 4 + end + if score + bad_score > 0 + rating = self.ratings.find_or_initialize_by(rating_type: rating_type) + rating.score = score + redflags_count += 1 if bad_score == 4 + total_score += score + maximum_score += 4 + end + end + end + + self.redflags_count = redflags_count + self.total_score = total_score + self.maximum_score = maximum_score + end + + def self.map_phase_type_to_route(phase_type) + ROUTE_MAP[phase_type] || phase_type + end + + def self.find_published_revision(project_id, revision_type) + phase_name = map_revision_type_to_phase_name(revision_type) + joins(phase: { project: :phases }) + .where(projects: { id: project_id }) + .where(phases: { phase_type: PhaseType.find_by(name: phase_name) }) + .where(published: true) + .first + end + + def self.find_revision_history(project_id, revision_type, version) + phase_name = map_revision_type_to_phase_name(revision_type) + joins(phase: { project: :phases }) + .joins(:revision) + .where(projects: { id: project_id }) + .where(phases: { phase_type: PhaseType.find_by(name: phase_name) }) + .where(revisions: { version: version }) + .first + end + + private + + def self.map_revision_type_to_phase_name(revision_type) + phase_type_map = { + 'hodnotenie-pripravy' => 'Prípravná fáza', + 'hodnotenie-produktu' => 'Fáza produkt' + } + phase_type_map[revision_type] || revision_type + end +end diff --git a/app/models/phase_revision_rating.rb b/app/models/phase_revision_rating.rb new file mode 100644 index 0000000..48ce546 --- /dev/null +++ b/app/models/phase_revision_rating.rb @@ -0,0 +1,26 @@ +# == Schema Information +# +# Table name: revision_ratings +# +# id :bigint not null, primary key +# rating_type_id :bigint not null +# score :integer +# created_at :datetime not null +# updated_at :datetime not null +# revision_id :bigint +# +# Indexes +# +# index_revision_ratings_on_rating_type_id (rating_type_id) +# index_revision_ratings_on_phase_revision_id (phase_revision_id) +# +# Foreign Keys +# +# fk_rails_... (rating_type_id => rating_types.id) +# fk_rails_... (phase_revision_id => phase_revision.id) +# + +class PhaseRevisionRating < ApplicationRecord + belongs_to :phase_revision + belongs_to :rating_type +end diff --git a/app/models/phase_type.rb b/app/models/phase_type.rb new file mode 100644 index 0000000..92ce359 --- /dev/null +++ b/app/models/phase_type.rb @@ -0,0 +1,12 @@ +# == Schema Information +# +# Table name: phase_types +# +# id :integer not null, primary key +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class PhaseType < ApplicationRecord +end diff --git a/app/models/project.rb b/app/models/project.rb index 4804f43..9ede6c4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -3,32 +3,69 @@ # Table name: projects # # id :integer not null, primary key -# page_id :integer not null # created_at :datetime not null # updated_at :datetime not null -# published_revision_id :integer -# category :integer default("boring"), not null -# -# Indexes -# -# index_projects_on_page_id (page_id) -# index_projects_on_published_revision_id (published_revision_id) -# -# Foreign Keys -# -# fk_rails_... (page_id => pages.id) -# fk_rails_... (published_revision_id => project_revisions.id) # class Project < ApplicationRecord - belongs_to :page + has_many :phases + + def has_published_phases? + phases.any? { |phase| phase.published_revision.present? } + end - has_many :revisions, class_name: 'ProjectRevision' + def self.filtered_projects(selected_tag, sort_param) - belongs_to :published_revision, class_name: 'ProjectRevision', optional: true + case sort_param + when 'newest' + projects = Project.joins(phases: :published_revision) + .select('projects.*, MAX(phase_revisions.published_at) AS newest_published_at') + .group('projects.id') + .order('newest_published_at DESC') + when 'oldest' + projects = Project.joins(phases: :published_revision) + .select('projects.*, MIN(phase_revisions.published_at) AS oldest_published_at') + .group('projects.id') + .order('oldest_published_at') + when 'alpha', 'alpha_reverse' + projects = Project.joins(phases: :published_revision) + .select('DISTINCT ON (projects.id) projects.*, phase_revisions.title AS alpha_title') + .order('projects.id, alpha_title') + projects = projects.sort_by(&:alpha_title) + projects = projects.reverse if sort_param == 'alpha_reverse' + when 'preparation_lowest' + projects = Project.joins(phases: :published_revision) + .where(phases: { phase_type: PhaseType.find_by(name: 'Prípravná fáza') }) + .select('projects.*, phase_revisions.total_score') + .order('phase_revisions.total_score ASC NULLS LAST') + .distinct + when 'preparation_highest' + projects = Project.joins(phases: :published_revision) + .where(phases: { phase_type: PhaseType.find_by(name: 'Prípravná fáza') }) + .select('projects.*, phase_revisions.total_score') + .order('phase_revisions.total_score DESC NULLS LAST') + .distinct + when 'product_lowest' + projects = Project.joins(phases: :published_revision) + .where(phases: { phase_type: PhaseType.find_by(name: 'Fáza produkt') }) + .select('projects.*, phase_revisions.total_score') + .order('phase_revisions.total_score ASC NULLS LAST') + .distinct + when 'product_highest' + projects = Project.joins(phases: :published_revision) + .where(phases: { phase_type: PhaseType.find_by(name: 'Fáza produkt') }) + .select('projects.*, phase_revisions.total_score') + .order('phase_revisions.total_score DESC NULLS LAST') + .distinct + else + projects = Project.joins(phases: :published_revision).distinct - scope :published, -> { where('published_revision_id IS NOT NULL') } - scope :with_tag, -> (tag) { joins(published_revision: :revision).where("? = ANY(revisions.tags)", tag) } + if ProjectsHelper::ALLOWED_TAGS.keys.include?(selected_tag) + projects = Project.joins(phases: :published_revision) + .where(phase_revisions: { tags: selected_tag }) + end + end - enum category: { good: 0, bad: 1, boring: 2 } + projects + end end diff --git a/app/models/project_revision.rb b/app/models/project_revision.rb deleted file mode 100644 index a0f02f8..0000000 --- a/app/models/project_revision.rb +++ /dev/null @@ -1,129 +0,0 @@ -# == Schema Information -# -# Table name: project_revisions -# -# id :integer not null, primary key -# project_id :integer not null -# revision_id :integer not null -# title :string not null -# full_name :string -# guarantor :string -# description :string -# budget :string -# created_at :datetime not null -# updated_at :datetime not null -# body_html :string -# total_score :integer -# maximum_score :integer -# redflags_count :integer default(0) -# summary :text -# recommendation :text -# stage_id :integer -# current_status :string -# -# Indexes -# -# index_project_revisions_on_project_id (project_id) -# index_project_revisions_on_revision_id (revision_id) -# index_project_revisions_on_stage_id (stage_id) -# -# Foreign Keys -# -# fk_rails_... (project_id => projects.id) -# fk_rails_... (revision_id => revisions.id) -# fk_rails_... (stage_id => project_stages.id) -# - -class ProjectRevision < ApplicationRecord - belongs_to :project - belongs_to :revision - belongs_to :stage, class_name: 'ProjectStage', optional: true - - has_many :ratings, class_name: 'ProjectRevisionRating' - - delegate :category, to: :project - delegate :version, :tags, to: :revision - - def total_score_percentage - 100.0 * total_score / maximum_score - end - - def aggregated_rating - [redflags_count, -total_score_percentage] - end - - # TODO move elsewhere? - def load_from_data(raw) - self.title = raw['title'].gsub('Red Flags:', '').strip - - body = raw['post_stream']['posts'].first['cooked'] - summary, rest = body.split(/

.+?<\/h1>/m, 2) - - self.body_html = rest - - load_metadata(summary) - load_ratings(rest) - end - - def outdated? - tags.include?('rf-outdated') - end - - private - - def load_metadata(summary) - doc = Nokogiri::HTML.parse(summary) - doc.search('p').each do |p| - type = p.search('strong').first.try(:text) - value = p.text.gsub(type, '').strip if type - case type - when 'Názov:' - self.full_name = value - when 'Garant:' - self.guarantor = value - when 'Stručný opis:' - self.description = value - when 'Náklady na projekt:' - self.budget = value - when 'Aktuálny stav projektu:' - self.stage = ProjectStage.find_by(name: value) - when 'Čo sa práve deje:' - self.current_status = p.next_element - when 'Zhrnutie hodnotenia Red Flags:' - self.summary = value - when 'Stanovisko Slovensko.Digital:' - self.recommendation = value - end - end - end - - def load_ratings(body) - redflags_count = 0 - total_score = 0 - maximum_score = 0 - doc = Nokogiri::HTML.parse(body) - doc.css('h3').each do |heading| - value = heading.text.strip - rating_type = RatingType.find_by(name: value) - if rating_type - score = heading.css('img.emoji[title=":star:"]').count - bad_score = heading.css('img.emoji[title=":grey_star:"]').count - red_score = heading.css('img.emoji[title=":triangular_flag_on_post:"]').count - if red_score > 0 - bad_score = 4 - end - if score + bad_score > 0 - rating = self.ratings.find_or_initialize_by(rating_type: rating_type) - rating.score = score - redflags_count += 1 if bad_score == 4 - total_score += score - maximum_score += 4 - end - end - end - - self.redflags_count = redflags_count - self.total_score = total_score - self.maximum_score = maximum_score - end -end diff --git a/app/models/project_revision_rating.rb b/app/models/project_revision_rating.rb deleted file mode 100644 index 003664f..0000000 --- a/app/models/project_revision_rating.rb +++ /dev/null @@ -1,26 +0,0 @@ -# == Schema Information -# -# Table name: project_revision_ratings -# -# id :integer not null, primary key -# project_revision_id :integer not null -# rating_type_id :integer not null -# score :integer -# created_at :datetime not null -# updated_at :datetime not null -# -# Indexes -# -# index_project_revision_ratings_on_project_revision_id (project_revision_id) -# index_project_revision_ratings_on_rating_type_id (rating_type_id) -# -# Foreign Keys -# -# fk_rails_... (project_revision_id => project_revisions.id) -# fk_rails_... (rating_type_id => rating_types.id) -# - -class ProjectRevisionRating < ApplicationRecord - belongs_to :project_revision - belongs_to :rating_type -end diff --git a/app/models/revision.rb b/app/models/revision.rb index 6ae153a..a6e47c6 100644 --- a/app/models/revision.rb +++ b/app/models/revision.rb @@ -2,14 +2,14 @@ # # Table name: revisions # -# id :integer not null, primary key -# page_id :integer not null -# version :integer not null -# raw :jsonb -# created_at :datetime not null -# updated_at :datetime not null -# title :string not null -# tags :string default([]), is an Array +# id :integer not null, primary key +# page_id :integer not null +# version :integer not null +# raw :jsonb +# created_at :datetime not null +# updated_at :datetime not null +# title :string not null +# tags :string default([]), is an Array # # Indexes # @@ -24,8 +24,9 @@ class Revision < ApplicationRecord belongs_to :page + has_one :phase_revision - after_save :schedule_sync_project_job # TODO: move to domain events and pubsub + after_save :schedule_sync_project_job def body_html raw['post_stream']['posts'].first['cooked'] @@ -39,8 +40,6 @@ def latest? page.latest_revision == self end - private - def schedule_sync_project_job SyncRevisionJob.perform_later(self) end diff --git a/app/services/document_parser_service.rb b/app/services/document_parser_service.rb new file mode 100644 index 0000000..c065f9f --- /dev/null +++ b/app/services/document_parser_service.rb @@ -0,0 +1,134 @@ +require 'nokogiri' + +class DocumentParserService + def initialize(document) + @document = document + end + + def to_html + html_content = "" + @document.body.content.each do |element| + html_content += parse_element(element) + end + html_content + end + + def to_hash(html_content) + { + title: extract_title(html_content), + category_id: 43, + post_stream: { + posts: [ + { cooked: html_content }, + ] + } + } + end + + private + + def parse_element(element) + if element.paragraph + parse_paragraph(element.paragraph) + else + "" + end + end + + def parse_paragraph(paragraph) + text_content = "" + paragraph.elements.each do |el| + if el.text_run + if el.text_run.text_style.link + link = el.text_run.text_style.link.url + text = el.text_run.content + + text_style = el.text_run.text_style + text = "#{text}" if text_style.bold? + text = "#{text}" if text_style.italic? + text = "#{text}" if text_style.underline? + + if link =~ /drive\.google\.com/ + id = link.split('/d/').last.split('/').first + link = "https://drive.google.com/uc?export=download&id=#{id}" + end + text_content += "#{text}" + else + text = el.text_run.content + + # Apply styles if present. + text_style = el.text_run.text_style + text = "#{text}" if text_style.bold? + text = "#{text}" if text_style.italic? + text = "#{text}" if text_style.underline? + + text_content += text + end + elsif el.inline_object_element + inline_object_id = el.inline_object_element.inline_object_id + text_content += parse_inline_object(@document.inline_objects[inline_object_id]) + end + end + if paragraph.bullet + parse_list_item(paragraph.bullet, text_content) + else + case paragraph.paragraph_style&.named_style_type + when "HEADING_1" + icon = '' + if text_content.downcase.include?('dokumenty') + icon = ":file_folder:" + elsif text_content.downcase.include?('aktivity') + icon = ":clock2:" + end + "

#{icon} #{text_content}

" + when "HEADING_2" + "

#{text_content}

" + when "HEADING_3" + stars = text_content.scan(/★|☆/) + if stars.any? + text_content.gsub!(/★|☆/, '') + text_content += parse_stars(stars.join) + end + "

#{text_content}

" + else + "

#{text_content}

" + end + end + end + + def parse_stars(stars_str) + filled_stars = stars_str.count('★') + grey_stars = stars_str.count('☆') + ':star:' * filled_stars + ':grey_star:' * grey_stars + end + + def parse_inline_object(inline_object) + if inline_object.inline_object_properties + embedded_object = inline_object.inline_object_properties.embedded_object + if embedded_object&.image_properties + " +
+ Inline Object +
+ " + else + "" + end + else + "" + end + end + + def parse_list_item(bullet, text_content) + nesting_level = bullet.nesting_level || 0 + list_tag = nesting_level.even? ? "ul" : "ol" + "<#{list_tag}>
  • #{text_content}
  • " + end + + + def extract_title(html_content) + doc = Nokogiri::HTML(html_content) + + doc.at('h3').next_element.text.strip + end +end diff --git a/app/services/google_api_service.rb b/app/services/google_api_service.rb new file mode 100644 index 0000000..ebd7638 --- /dev/null +++ b/app/services/google_api_service.rb @@ -0,0 +1,28 @@ +class GoogleApiService + def self.authorize + Google::Auth::ServiceAccountCredentials.make_creds( + json_key_io: Base64.decode64(ENV['GOOGLE_APPLICATION_CREDENTIALS']), + scope: ['https://www.googleapis.com/auth/spreadsheets', + 'https://www.googleapis.com/auth/documents', + 'https://www.googleapis.com/auth/drive'] + ) + end + + def self.get_drive_service + drive_service = Google::Apis::DriveV3::DriveService.new + drive_service.authorization = self.authorize + drive_service + end + + def self.get_sheets_service + sheets_service = Google::Apis::SheetsV4::SheetsService.new + sheets_service.authorization = self.authorize + sheets_service + end + + def self.get_document(document_id) + docs_service = Google::Apis::DocsV1::DocsService.new + docs_service.authorization = self.authorize + docs_service.get_document(document_id) + end +end \ No newline at end of file diff --git a/app/views/admin/pages/index.html.erb b/app/views/admin/pages/index.html.erb index ab6c97a..704049e 100644 --- a/app/views/admin/pages/index.html.erb +++ b/app/views/admin/pages/index.html.erb @@ -28,7 +28,6 @@ ID Title - Category Flags + Revisions Updated Actions @@ -41,11 +40,6 @@ <%= link_to page.title, admin_page_path(page), class: 'text-dark' %> - - <% if project.present? %> - <%= project.category %> - <% end %> - <% if page.publishable? %> <% if page.published? %> @@ -65,7 +59,7 @@ <% end %> <%= page.revisions.size %> - <%= l page.updated_at, format: '%F %T' %> + <%= l page.revisions.max.updated_at.in_time_zone('Europe/Bratislava'), format: "%H:%M, %d.%m.%y" %> <% if page.publishable? %> <% if page.published? %> @@ -81,6 +75,4 @@ <% end %> - - -<%= paginate @pages, views_prefix: 'admin' %> + \ No newline at end of file diff --git a/app/views/admin/pages/preview.html.erb b/app/views/admin/pages/preview.html.erb index fc2b14d..e764daa 100644 --- a/app/views/admin/pages/preview.html.erb +++ b/app/views/admin/pages/preview.html.erb @@ -2,16 +2,16 @@
    Preview of page <%= @page.title %> - at <%= 'latest' if @page.latest_revision.version == @revision.version %> - version <%= @revision.version %> - last updated at <%= l @revision.updated_at, format: '%F %T' %>. + at <%= 'latest' if @page.latest_revision.version == @phase_revision.version %> + version <%= @phase_revision.version %> + last updated at <%= l @phase_revision.updated_at, format: '%F %T' %>. <%= link_to 'Synchonize', sync_one_admin_page_path(@page), class: 'btn btn-info btn-sm float-right', method: :put %> <%= link_to 'Go back', admin_pages_path, class: 'btn alert-link btn-sm float-right' %>
    -<% if @project.present? %> - <% @project = @revision %> - <%= render file: 'projects/show' %> +<% if @phase.present? %> + <% @project = @phase_revision %> + <%= render file: 'phase_revision/show' %> <% else %> <% @page = @revision %> <%= render file: 'static/page' %> diff --git a/app/views/admin/pages/show.html.erb b/app/views/admin/pages/show.html.erb index c7d63fc..7b16cdf 100644 --- a/app/views/admin/pages/show.html.erb +++ b/app/views/admin/pages/show.html.erb @@ -5,6 +5,7 @@
    Actions
    <%= link_to 'Back to pages', admin_pages_path, class: 'btn btn-secondary btn-sm' %> + <%= link_to 'Synchronize', sync_one_admin_page_path(@page), class: 'btn btn-info btn-sm', method: :put %>
    @@ -21,30 +22,10 @@
    - <% if @project.present? %> -
    -
    Project
    - <%= form_for [:admin, @project], html: { class: 'form-inline mb-3' } do |f| %> - Project category - <% Project.categories.keys.each do |name| %> -
    - -
    - <% end %> - <%= f.submit class: 'btn btn-primary btn-sm', value: 'Update', data: { disable_with: 'Update' } %> - <% end %> -
    - <% end %>

    <%= @page.title %> - <% if @project.present? %> - <%= @project.category %> - <% end %>

    @@ -61,7 +42,13 @@ <% @revisions.each do |revision| %> - + - +
    <%= revision.version %><%= revision.title %> + <% if revision.published? %> + <%= link_to revision.title, project_show_revision_type_path(@project, PhaseRevision.map_phase_type_to_route(@page.phase.phase_type.name)) %> + <% else %> + <%= revision.title %> + <% end %> + <% if @page.publishable? %> <% if revision.published? %> @@ -81,7 +68,7 @@ latest <% end %> <%= l revision.updated_at, format: '%F %T' %><%= l revision.updated_at.in_time_zone('Europe/Bratislava'), format: "%H:%M, %d.%m.%y" %> <% if @page.publishable? %> <% if revision.published? %> diff --git a/app/views/layouts/no_header_footer.html.erb b/app/views/layouts/no_header_footer.html.erb new file mode 100644 index 0000000..dcfd207 --- /dev/null +++ b/app/views/layouts/no_header_footer.html.erb @@ -0,0 +1,54 @@ + + + + <% if content_for?(:title) %><%= yield(:title) %><% else %>Red Flags · Slovensko.Digital<% end %> + <%= csrf_meta_tags %> + + <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> + <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> + + + + + + + + + + + + + + + + + + + + + +
    + Red Flags logo +
    +<%= yield %> + + diff --git a/app/views/phase_revision/pdf.html.erb b/app/views/phase_revision/pdf.html.erb new file mode 100644 index 0000000..373ea63 --- /dev/null +++ b/app/views/phase_revision/pdf.html.erb @@ -0,0 +1,97 @@ +<% content_for(:title) do %><%= @phase_revision.title %> · Red Flags · Slovensko.Digital<% end %> +
    +
    +
    +

    <%= @phase_revision.title %>

    + <%= link_to 'Hodnotenie na RF webe', project_show_revision_type_path(@phase_revision.phase.project_id, PhaseRevision.map_phase_type_to_route(@phase_revision.phase.phase_type.name))%> + <% @phase_revision.tags.select { |tag| ProjectsHelper::ALLOWED_TAGS[tag] }.each do |tag| %> +

    + <%= link_to projects_path(tag: tag) do%> + <%= ProjectsHelper::ALLOWED_TAGS[tag] %> + <% end %> +

    + <% end %> +

    <%= @phase_revision.description %>

    +
    +
    + +
    +
    +
    Náklady na projekt
    +

    <%= @phase_revision.budget %>

    +
    + +
    +
    Garant <%= help_icon_if_blank(@phase_revision.guarantor) %>
    +

    <%= @phase_revision.guarantor %>

    +
    +
    + + <% if @phase_revision.summary.present? %> +
    +
    +
    Zhrnutie hodnotenia Red Flags
    +

    <%= @phase_revision.summary %>

    +
    +
    + <% end %> + + <% if @phase_revision.recommendation.present? %> +
    +
    +
    Stanovisko Slovensko.Digital
    +

    <%= @phase_revision.recommendation %>

    +
    +
    + <% end %> + +
    +
    +
    Hodnotenie zverejnené dňa
    +

    <%= l(@phase_revision.updated_at.to_date, format: :human) %>

    + <% if @phase_revision.outdated? %> + + <% end %> +
    +
    + +
    +
    +
    Aktuálny stav projektu
    +

    <%= @phase_revision.stage %>

    +
    +
    +
    Čo sa práve deje
    +

    <%= @phase_revision.current_status&.html_safe %>

    +
    +
    + +
    +
    +
    +

    Hodnotenie<% if @phase_revision.redflags_count > 0 %> <%= @phase_revision.redflags_count %> × <%= fa_icon('flag', class: 'text-danger') %><% end %>

    +
    +
    +
    +
    + <% @phase_revision.ratings.index_by(&:rating_type).each do |rating_type, rating| %> +
    + <%= rating_stars(rating) %> +

    <%= rating_type.name %>

    +
    + <% end %> +
    +
    + +
    +

    Detailné hodnotenie projektu

    +
    + <%== formatted_body_html(@phase_revision.body_html) %> +
    +
    +
    diff --git a/app/views/phase_revision/show.html.erb b/app/views/phase_revision/show.html.erb new file mode 100644 index 0000000..74af6cf --- /dev/null +++ b/app/views/phase_revision/show.html.erb @@ -0,0 +1,109 @@ +<% content_for(:title) do %><%= @phase_revision.title %> · Red Flags · Slovensko.Digital<% end %> +
    + +
    +
    +

    <%= @phase_revision.title %>

    + <% if @phase_revision.published? %> + <%= link_to 'Stiahnuť PDF', project_show_pdf_project_path(@phase_revision.phase.project_id, PhaseRevision.map_phase_type_to_route(@phase_revision.phase.phase_type.name)) %> + <% end %> + <% @phase_revision.tags.select { |tag| ProjectsHelper::ALLOWED_TAGS[tag] }.each do |tag| %> +

    + <%= link_to projects_path(tag: tag) do%> + <%= ProjectsHelper::ALLOWED_TAGS[tag] %> + <% end %> +

    + <% end %> +

    <%= @phase_revision.description %>

    +
    +
    + +
    +
    +
    Náklady na projekt
    +

    <%= @phase_revision.budget %>

    +
    + +
    +
    Garant <%= help_icon_if_blank(@phase_revision.guarantor) %>
    +

    <%= @phase_revision.guarantor %>

    +
    +
    + + <% if @phase_revision.summary.present? %> +
    +
    +
    Zhrnutie hodnotenia Red Flags
    +

    <%= @phase_revision.summary %>

    +
    +
    + <% end %> + + <% if @phase_revision.recommendation.present? %> +
    +
    +
    Stanovisko Slovensko.Digital
    +

    <%= @phase_revision.recommendation %>

    +
    +
    + <% end %> + +
    +
    +
    Hodnotenie zverejnené dňa
    +

    <%= l(@phase_revision.updated_at.to_date, format: :human) %>

    + <% if @phase_revision.outdated? %> + + <% end %> +
    +
    + +
    +
    +
    Aktuálny stav projektu
    +

    <%= @phase_revision.stage %>

    +
    +
    +
    Čo sa práve deje
    +

    <%= @phase_revision.current_status&.html_safe %>

    +
    +
    + +
    +
    +
    +

    Hodnotenie<% if @phase_revision.redflags_count > 0 %> <%= @phase_revision.redflags_count %> × <%= fa_icon('flag', class: 'text-danger') %><% end %>

    +
    +
    +
    +
    + <% @phase_revision.ratings.index_by(&:rating_type).each do |rating_type, rating| %> +
    + <%= rating_stars(rating) %> +

    <%= rating_type.name %>

    +
    + <% end %> +
    +
    + +
    +

    Detailné hodnotenie projektu

    +
    + <%== formatted_body_html(@phase_revision.body_html) %> +
    +
    + + <% if @once_published_phase_revisions && @once_published_phase_revisions.any? %> +

    Predchádzajúce hodnotenia

    +
      + <% @once_published_phase_revisions.each do |phase_revision| %> +
    • <%= link_to "Hodnotenie z dňa - #{phase_revision.published_at.strftime('%d.%m.%Y')}", project_show_history_path(@project.id, PhaseRevision.map_phase_type_to_route(phase_revision.phase.phase_type.name), phase_revision.revision.version) %>
    • + <% end %> +
    + <% end %> +
    diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index 53d4a15..e5e6a09 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -2,44 +2,138 @@

    Zoznam hodnotených projektov

    Chýba Vám tu nejaký projekt? Nezdá sa Vám hodnotenie? Toto hodnotenie je možné <%= link_to 'dopĺňať a upravovať', contribute_path %>.

    +
    + <%= form_with url: projects_path, method: :get, local: true, id: "sort-form" do |form| %> + <% form.label :sort %> + <%= form.select :sort, options_for_select([ + ['Usporiadať', '', {disabled: true, selected: true}], + ['Abecedne (A-Z)', 'alpha'], + ['Abecedne (Z-A)', 'alpha_reverse'], + ['Podľa dátumu (najnovšie)', 'newest'], + ['Podľa dátumu (najstaršie)', 'oldest'], + ['Hodnotenie prípravy (vzostupne)', 'preparation_lowest'], + ['Hodnotenie prípravy (zostupne)', 'preparation_highest'], + ['Hodnotenie produktu (vzostupne)', 'product_lowest'], + ['Hodnotenie produktu (zostupne)', 'product_highest'], + ], params[:sort]), {}, { class: 'bg-primary px-4 py-2 rounded border-0 text-white', onchange: "this.form.submit();" } %> + <% end %> +
    + - - - + + + + + <% @projects.each do |project| %> - + + <% prep_revision = project.phases.select { |phase| phase.phase_type.name == 'Prípravná fáza' && phase.published_revision.present? }.first %> + <% prep_revision = prep_revision&.published_revision %> + <% prep_page = prep_revision&.revision&.page %> + - <% ratings = project.ratings.group_by(&:score) %> - <% 4.downto(0).each do |index| rt = ratings[index] %> - - <% end %> - + + + + + + + <% end %> diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb deleted file mode 100644 index 305ea0c..0000000 --- a/app/views/projects/show.html.erb +++ /dev/null @@ -1,103 +0,0 @@ -<% content_for(:title) do %><%= @project.title %> · Red Flags · Slovensko.Digital<% end %> -
    - -
    -
    -

    <%= @project.title %>

    - <% @project.tags.select { |tag| ProjectsHelper::ALLOWED_TAGS[tag] }.each do |tag| %> -

    - <%= link_to projects_path(tag: tag) do%> - <%= ProjectsHelper::ALLOWED_TAGS[tag] %> - <% end %> -

    - <% end %> -

    <%= @project.description %>

    -
    -
    - -
    -
    -
    Náklady na projekt
    -

    <%= @project.budget %>

    -
    - -
    -
    Garant <%= help_icon_if_blank(@project.guarantor) %>
    -

    <%= @project.guarantor %>

    -
    -
    - - <% if @project.summary.present? %> -
    -
    -
    Zhrnutie hodnotenia Red Flags
    -

    <%= @project.summary %>

    -
    -
    - <% end %> - - <% if @project.recommendation.present? %> -
    -
    -
    Stanovisko Slovensko.Digital
    -

    <%= @project.recommendation %>

    -
    -
    - <% end %> - -
    -
    -
    Hodnotenie zverejnené dňa
    -

    <%= l(@project.updated_at.to_date, format: :human) %>

    - <% if @project.outdated? %> - - <% end %> -
    -
    - -
    -
    -
    Aktuálny stav projektu
    -

    <%= @project.stage %>

    -
    -
    -
    Čo sa práve deje
    -

    <%= @project.current_status&.html_safe %>

    -
    -
    - -
    -
    -
    -

    Hodnotenie<% if @project.redflags_count > 0 %> <%= @project.redflags_count %> × <%= fa_icon('flag', class: 'text-danger') %><% end %>

    -
    - -
    -
    -
    - <% @project.ratings.index_by(&:rating_type).each do |rating_type, rating| %> -
    - <%= rating_stars(rating) %> -

    <%= rating_type.name %>

    -
    - <% end %> -
    -
    - -
    -

    Detailné hodnotenie projektu

    -
    - <%== formatted_body_html(@project.body_html) %> -
    -
    -
    diff --git a/app/views/static/index.html.erb b/app/views/static/index.html.erb index 232f592..1fbad62 100644 --- a/app/views/static/index.html.erb +++ b/app/views/static/index.html.erb @@ -39,10 +39,10 @@

    <%= fa_icon 'thumbs-o-up', class: 'mr-2' %> Najlepšie hodnotené

    ProjektHodnotenia podľa kritériíProjektHodnotenie prípravyHodnotenie produktuPosledná aktualizáciaDetail
    - <%= link_to truncate(project.title, length: 80), project_path(project.project), title: project.title %> - <% if project.stage %> -
    - <%= project.stage %> +
    + <% if project.has_published_phases? %> + <%= project.phases.select { |phase| phase.published_revision.present? }.map { |phase| phase.published_revision.title }.first %> + <% end %> + + <% if prep_page.nil? %> + - + <% elsif prep_revision.redflags_count > 0 %> +
    + + <%= prep_revision.redflags_count %> × <%= fa_icon('flag', class: 'text-danger') %> + + <% if prep_page.latest_revision %> + <%= link_to 'Zobraziť', project_show_revision_type_path(project.id, 'hodnotenie-pripravy') %> + <% end %> +
    + <% else %> +
    + + <%= number_to_percentage(prep_revision.total_score_percentage, precision: 0) %> + + <% if prep_page.latest_revision %> + <%= link_to 'Zobraziť', project_show_revision_type_path(project.id, 'hodnotenie-pripravy') %> + <% end %>
    <% end %>
    - <% if rt %> - "> - <%= rt.count %> × <%= rating_stars(rt.first) %> + + <% prod_revision = project.phases.select { |phase| phase.phase_type.name == 'Fáza produkt' && phase.published_revision.present? }.first %> + <% prod_revision = prod_revision&.published_revision %> + <% prod_page = prod_revision&.revision&.page%> + + <% if prod_page.nil? %> + - + <% elsif prod_revision&.redflags_count > 0 %> +
    + + <%= prod_revision.redflags_count %> × <%= fa_icon('flag', class: 'text-danger') %> - <% end %> -
    - <% if project.redflags_count > 0 %> - <%= project.redflags_count %> × <%= fa_icon('flag', class: 'text-danger') %> + <% if prod_page.latest_revision %> + <%= link_to 'Zobraziť', project_show_revision_type_path(project.id, 'hodnotenie-produktu') %> + <% end %> + <% else %> - - <%= number_to_percentage(project.total_score_percentage, precision: 0) %> - +
    + + <%= number_to_percentage(prod_revision.total_score_percentage, precision: 0) %> + + <% if prod_page.latest_revision %> + <%= link_to 'Zobraziť', project_show_revision_type_path(project.id, 'hodnotenie-produktu') %> + <% end %> +
    <% end %>
    + <% latest_revision_date = project.phases.map { |phase| phase.published_revision&.published_at }.compact.max %> + <%= latest_revision_date.strftime('%d.%m.%Y') if latest_revision_date.present? %> + + +
    - <% @good_projects.each_with_index do |project, index| %> + <% @good_projects.each_with_index do |phase_revision, index| %> - + <% end %> @@ -53,10 +53,10 @@

    <%= fa_icon 'thumbs-o-down', class: 'mr-2' %> Najhoršie hodnotené

    <%= index + 1 %><%= link_to project.title, project_path(project.project) %><%= link_to phase_revision.title, project_show_revision_type_path(phase_revision.phase.project, PhaseRevision.map_phase_type_to_route(phase_revision.phase.phase_type.name)) %>
    - <% @bad_projects.each_with_index do |project, index| %> + <% @bad_projects.each_with_index do |phase_revision, index| %> - + <% end %> diff --git a/config/routes.rb b/config/routes.rb index f1afd00..5a57a9c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,11 @@ end Rails.application.routes.draw do - resources :projects, path: 'projekty' + resources :projects, path: 'projekty' do + get ':revision_type/verzia/:version', to: 'phase_revision#show_history', as: 'show_history' + get ':revision_type', to: 'phase_revision#show', as: 'show_revision_type' + get ':revision_type/pdf', to: 'phase_revision#pdf', as: 'show_pdf_project' + end namespace :admin do root to: 'pages#index' diff --git a/db/migrate/20240609101956_remove_page_id_from_projects.rb b/db/migrate/20240609101956_remove_page_id_from_projects.rb new file mode 100644 index 0000000..8c87c73 --- /dev/null +++ b/db/migrate/20240609101956_remove_page_id_from_projects.rb @@ -0,0 +1,5 @@ +class RemovePageIdFromProjects < ActiveRecord::Migration[5.1] + def change + remove_reference :projects, :page, index: true, foreign_key: true + end +end diff --git a/db/migrate/20240609114857_rename_page_rating_to_revision_rating.rb b/db/migrate/20240609114857_rename_page_rating_to_revision_rating.rb new file mode 100644 index 0000000..8a7a5ab --- /dev/null +++ b/db/migrate/20240609114857_rename_page_rating_to_revision_rating.rb @@ -0,0 +1,5 @@ +class RenamePageRatingToRevisionRating < ActiveRecord::Migration[5.1] + def change + rename_table :project_revision_ratings, :phase_revision_ratings + end +end diff --git a/db/migrate/20240609120517_rename_project_revision_to_phase_revision_in_phase_revision_ratings.rb b/db/migrate/20240609120517_rename_project_revision_to_phase_revision_in_phase_revision_ratings.rb new file mode 100644 index 0000000..08ce989 --- /dev/null +++ b/db/migrate/20240609120517_rename_project_revision_to_phase_revision_in_phase_revision_ratings.rb @@ -0,0 +1,5 @@ +class RenameProjectRevisionToPhaseRevisionInPhaseRevisionRatings < ActiveRecord::Migration[5.1] + def change + rename_column :phase_revision_ratings, :project_revision_id, :phase_revision_id + end +end diff --git a/db/migrate/20240610100714_create_phase_types.rb b/db/migrate/20240610100714_create_phase_types.rb new file mode 100644 index 0000000..405a199 --- /dev/null +++ b/db/migrate/20240610100714_create_phase_types.rb @@ -0,0 +1,15 @@ +class CreatePhaseTypes < ActiveRecord::Migration[5.1] + def change + create_table :phase_types do |t| + t.string :name + + t.timestamps + end + + PhaseType.reset_column_information + PhaseType.create([ + { name: 'Prípravná fáza' }, + { name: 'Fáza produkt' } + ]) + end +end diff --git a/db/migrate/20240610100956_create_phases.rb b/db/migrate/20240610100956_create_phases.rb new file mode 100644 index 0000000..a9b9e44 --- /dev/null +++ b/db/migrate/20240610100956_create_phases.rb @@ -0,0 +1,9 @@ +class CreatePhases < ActiveRecord::Migration[5.1] + def change + create_table :phases do |t| + t.references :project, index:true, foreign_key: true + t.references :phase_type, index:true, foreign_key: true + t.timestamps + end + end +end diff --git a/db/migrate/20240610102411_rename_project_revisions_to_phase_revisions.rb b/db/migrate/20240610102411_rename_project_revisions_to_phase_revisions.rb new file mode 100644 index 0000000..98fa68d --- /dev/null +++ b/db/migrate/20240610102411_rename_project_revisions_to_phase_revisions.rb @@ -0,0 +1,5 @@ +class RenameProjectRevisionsToPhaseRevisions < ActiveRecord::Migration[5.1] + def change + rename_table :project_revisions, :phase_revisions + end +end diff --git a/db/migrate/20240610110713_update_phase_revisions_table.rb b/db/migrate/20240610110713_update_phase_revisions_table.rb new file mode 100644 index 0000000..e0ff559 --- /dev/null +++ b/db/migrate/20240610110713_update_phase_revisions_table.rb @@ -0,0 +1,36 @@ +class UpdatePhaseRevisionsTable < ActiveRecord::Migration[5.1] + def up + add_reference :phase_revisions, :phase, index: true, foreign_key: true + + project_to_phase = {} + + Phase.reset_column_information + + PhaseRevision.select(:project_id).distinct.find_each do |phase_revision| + project_id = phase_revision.project_id + next if project_to_phase[project_id] + + phase = Phase.find_or_create_by!( + project_id: project_id, + phase_type: PhaseType.find_by!(name: 'Prípravná fáza') + ) + project_to_phase[project_id] = phase.id + end + + PhaseRevision.find_each do |phase_revision| + phase_revision.update!(phase_id: project_to_phase[phase_revision.project_id]) + end + + remove_reference :phase_revisions, :project, index: true, foreign_key: true + end + + def down + add_reference :phase_revisions, :project, index: true, foreign_key: true + + remove_reference :phase_revisions, :phase, index: true, foreign_key: true + + PhaseRevision.find_each do |phase_revision| + phase_revision.update!(project_id: Phase.find(phase_revision.phase_id).project_id) + end + end +end diff --git a/db/migrate/20240611122213_remove_category_from_projects.rb b/db/migrate/20240611122213_remove_category_from_projects.rb new file mode 100644 index 0000000..cf10180 --- /dev/null +++ b/db/migrate/20240611122213_remove_category_from_projects.rb @@ -0,0 +1,5 @@ +class RemoveCategoryFromProjects < ActiveRecord::Migration[5.1] + def change + remove_column :projects, :category, :integer + end +end diff --git a/db/migrate/20240611153648_add_published_to_project_revisions.rb b/db/migrate/20240611153648_add_published_to_project_revisions.rb new file mode 100644 index 0000000..c0a96a1 --- /dev/null +++ b/db/migrate/20240611153648_add_published_to_project_revisions.rb @@ -0,0 +1,15 @@ +class AddPublishedToProjectRevisions < ActiveRecord::Migration[5.1] + def up + add_column :phase_revisions, :published, :boolean, default: false + + Project.find_each do |project| + project.phases.revisions.find_each do |revision| + revision.update_attribute(:published, project.published_revision_id == revision.id) + end + end + end + + def down + remove_column :phase_revisions, :published + end +end diff --git a/db/migrate/20240611153759_remove_published_revision_id_from_projects.rb b/db/migrate/20240611153759_remove_published_revision_id_from_projects.rb new file mode 100644 index 0000000..defc9dd --- /dev/null +++ b/db/migrate/20240611153759_remove_published_revision_id_from_projects.rb @@ -0,0 +1,5 @@ +class RemovePublishedRevisionIdFromProjects < ActiveRecord::Migration[5.1] + def change + remove_column :projects, :published_revision_id, :integer + end +end diff --git a/db/migrate/20240613113501_add_was_published_and_published_at_to_project_revisions.rb b/db/migrate/20240613113501_add_was_published_and_published_at_to_project_revisions.rb new file mode 100644 index 0000000..b9f8843 --- /dev/null +++ b/db/migrate/20240613113501_add_was_published_and_published_at_to_project_revisions.rb @@ -0,0 +1,18 @@ +class AddWasPublishedAndPublishedAtToProjectRevisions < ActiveRecord::Migration[5.1] + def up + add_column :phase_revisions, :was_published, :boolean, default: false + add_column :phase_revisions, :published_at, :datetime + + Phase.find_each do |phase| + if phase.published_revision.present? + published_revision = phase.published_revision + published_revision.update_attributes(was_published: true, published_at: published_revision.updated_at) + end + end + end + + def down + remove_column :phase_revisions, :was_published + remove_column :phase_revisions, :published_at + end +end diff --git a/db/migrate/20240621112043_add_phase_to_pages.rb b/db/migrate/20240621112043_add_phase_to_pages.rb new file mode 100644 index 0000000..334e1e4 --- /dev/null +++ b/db/migrate/20240621112043_add_phase_to_pages.rb @@ -0,0 +1,13 @@ +class AddPhaseToPages < ActiveRecord::Migration[5.1] + def change + add_reference :pages, :phase, index:true ,foreign_key: true + + Project.find_each do |project| + project.phases.find_each do |phase| + phase.revisions.find_each do |phase_revision| + phase_revision.revision.pages.update_all(phase_id: phase.id) + end + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 45f8e0f..8bb2725 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20220306165121) do +ActiveRecord::Schema.define(version: 20240621112043) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -20,20 +20,21 @@ t.datetime "updated_at", null: false t.bigint "published_revision_id" t.bigint "latest_revision_id" + t.bigint "phase_id" + t.index ["phase_id"], name: "index_pages_on_phase_id" end - create_table "project_revision_ratings", force: :cascade do |t| - t.bigint "project_revision_id", null: false + create_table "phase_revision_ratings", force: :cascade do |t| + t.bigint "phase_revision_id", null: false t.bigint "rating_type_id", null: false t.integer "score" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["project_revision_id"], name: "index_project_revision_ratings_on_project_revision_id" - t.index ["rating_type_id"], name: "index_project_revision_ratings_on_rating_type_id" + t.index ["phase_revision_id"], name: "index_phase_revision_ratings_on_phase_revision_id" + t.index ["rating_type_id"], name: "index_phase_revision_ratings_on_rating_type_id" end - create_table "project_revisions", force: :cascade do |t| - t.bigint "project_id", null: false + create_table "phase_revisions", force: :cascade do |t| t.bigint "revision_id", null: false t.string "title", null: false t.string "full_name" @@ -50,9 +51,28 @@ t.text "recommendation" t.bigint "stage_id" t.string "current_status" - t.index ["project_id"], name: "index_project_revisions_on_project_id" - t.index ["revision_id"], name: "index_project_revisions_on_revision_id" - t.index ["stage_id"], name: "index_project_revisions_on_stage_id" + t.boolean "published", default: false + t.boolean "was_published", default: false + t.datetime "published_at" + t.bigint "phase_id" + t.index ["phase_id"], name: "index_phase_revisions_on_phase_id" + t.index ["revision_id"], name: "index_phase_revisions_on_revision_id" + t.index ["stage_id"], name: "index_phase_revisions_on_stage_id" + end + + create_table "phase_types", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "phases", force: :cascade do |t| + t.bigint "project_id" + t.bigint "phase_type_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["phase_type_id"], name: "index_phases_on_phase_type_id" + t.index ["project_id"], name: "index_phases_on_project_id" end create_table "project_stages", force: :cascade do |t| @@ -63,13 +83,8 @@ end create_table "projects", force: :cascade do |t| - t.bigint "page_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.bigint "published_revision_id" - t.integer "category", default: 2, null: false - t.index ["page_id"], name: "index_projects_on_page_id" - t.index ["published_revision_id"], name: "index_projects_on_published_revision_id" end create_table "que_jobs", primary_key: ["queue", "priority", "run_at", "job_id"], force: :cascade, comment: "3" do |t| @@ -102,14 +117,15 @@ t.index ["tags"], name: "index_revisions_on_tags", using: :gin end + add_foreign_key "pages", "phases" add_foreign_key "pages", "revisions", column: "latest_revision_id" add_foreign_key "pages", "revisions", column: "published_revision_id" - add_foreign_key "project_revision_ratings", "project_revisions" - add_foreign_key "project_revision_ratings", "rating_types" - add_foreign_key "project_revisions", "project_stages", column: "stage_id" - add_foreign_key "project_revisions", "projects" - add_foreign_key "project_revisions", "revisions" - add_foreign_key "projects", "pages" - add_foreign_key "projects", "project_revisions", column: "published_revision_id" + add_foreign_key "phase_revision_ratings", "phase_revisions" + add_foreign_key "phase_revision_ratings", "rating_types" + add_foreign_key "phase_revisions", "phases" + add_foreign_key "phase_revisions", "project_stages", column: "stage_id" + add_foreign_key "phase_revisions", "revisions" + add_foreign_key "phases", "phase_types" + add_foreign_key "phases", "projects" add_foreign_key "revisions", "pages" end diff --git a/db/seeds.rb b/db/seeds.rb index 0f096ea..e8ca7b1 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,12 +10,22 @@ RatingType.find_or_create_by!(name: 'Merateľné ciele (KPI)') RatingType.find_or_create_by!(name: 'Postup dosiahnutia cieľov') RatingType.find_or_create_by!(name: 'Súlad s KRIS') +RatingType.find_or_create_by!(name: 'Súlad s KRIT') RatingType.find_or_create_by!(name: 'Biznis prínos') RatingType.find_or_create_by!(name: 'Príspevok v informatizácii') RatingType.find_or_create_by!(name: 'Štúdia uskutočniteľnosti') RatingType.find_or_create_by!(name: 'Alternatívy') RatingType.find_or_create_by!(name: 'Kalkulácia efektívnosti') RatingType.find_or_create_by!(name: 'Participácia na príprave projektu') +RatingType.find_or_create_by!(name: 'Transparentnosť a participácia') +RatingType.find_or_create_by!(name: 'Súlad s požiadavkami') +RatingType.find_or_create_by!(name: 'Elektronické služby') +RatingType.find_or_create_by!(name: 'Identifikácia, autentifikácia, autorizácia (IAA)') +RatingType.find_or_create_by!(name: 'Riadenie údajov') +RatingType.find_or_create_by!(name: 'OpenData') +RatingType.find_or_create_by!(name: 'MyData') +RatingType.find_or_create_by!(name: 'OpenAPI') +RatingType.find_or_create_by!(name: 'Zdrojový kód') RatingType.find_or_create_by!(name: 'Stratégia obstarávania') RatingType.find_or_create_by!(name: 'Prípravné trhové konzultácie') RatingType.find_or_create_by!(name: 'Druh postupu') @@ -38,3 +48,7 @@ ProjectStage.find_or_create_by!(name: 'Prevádzka').update!(position: 5) ProjectStage.find_or_create_by!(name: 'Zastavený projekt').update!(position: 998) ProjectStage.find_or_create_by!(name: 'Zrušený projekt').update!(position: 999) + +PhaseType.find_or_create_by!(name: 'Prípravná fáza') +PhaseType.find_or_create_by!(name: 'Fáza produkt') + diff --git a/spec/factories/pages.rb b/spec/factories/pages.rb index 09cff45..670876c 100644 --- a/spec/factories/pages.rb +++ b/spec/factories/pages.rb @@ -16,6 +16,9 @@ FactoryBot.define do factory :page do + + phase + after :create do |p| create :revision, page: p end diff --git a/spec/factories/project_revisions.rb b/spec/factories/phase_revision.rb similarity index 91% rename from spec/factories/project_revisions.rb rename to spec/factories/phase_revision.rb index 2ad8b9a..112a5ac 100644 --- a/spec/factories/project_revisions.rb +++ b/spec/factories/phase_revision.rb @@ -35,8 +35,8 @@ # FactoryBot.define do - factory :project_revision do - project + factory :phase_revision do + phase revision title { 'Red Flags: IS Obchodného registra' } @@ -47,5 +47,8 @@ body_html { revision.body_html } total_score { 75 } maximum_score { 100 } + published { false } + was_published { false } + published_at { Time.zone.now } end end diff --git a/spec/factories/project_revision_ratings.rb b/spec/factories/phase_revision_ratings.rb similarity index 92% rename from spec/factories/project_revision_ratings.rb rename to spec/factories/phase_revision_ratings.rb index dfc7eb9..e8877d6 100644 --- a/spec/factories/project_revision_ratings.rb +++ b/spec/factories/phase_revision_ratings.rb @@ -21,8 +21,8 @@ # FactoryBot.define do - factory :project_revision_rating do - project_revision nil + factory :phase_revision_rating do + phase_revision nil rating_type nil score { 1 } end diff --git a/spec/factories/phase_types.rb b/spec/factories/phase_types.rb new file mode 100644 index 0000000..7639e8f --- /dev/null +++ b/spec/factories/phase_types.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :phase_type do + name { "MyString" } + end +end diff --git a/spec/factories/phases.rb b/spec/factories/phases.rb new file mode 100644 index 0000000..dc88c73 --- /dev/null +++ b/spec/factories/phases.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :phase do + project + phase_type + end +end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 47bfa53..6e578ae 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -22,10 +22,5 @@ FactoryBot.define do factory :project do - page - - after :create do |p| - create :project_revision, project: p, revision: p.page.latest_revision - end end end diff --git a/spec/factories/revisions.rb b/spec/factories/revisions.rb index 50dfb4f..e9b0999 100644 --- a/spec/factories/revisions.rb +++ b/spec/factories/revisions.rb @@ -25,6 +25,7 @@ FactoryBot.define do factory :revision do page + title { 'Red Flags: IS Obchodného registra' } sequence(:version) { |n| 1_000_000 * page.id + n } @@ -32,6 +33,10 @@ r.page.update! latest_revision_id: r.id end + after :create do |revision| + create(:phase_revision, revision: revision) + end + raw { { "id" => 4228, "slug" => "red-flags-is-obchodneho-registra", "tags" => ["mssr", "redflags"], "draft" => nil, "title" => "Red Flags: IS Obchodného registra", "views" => 229, "closed" => false, "pinned" => false, "details" => { "links" => [{ "url" => "http://platforma.slovensko.digital/t/o-kategorii-red-flags/4034", "title" => "O projekte Red Flags", "clicks" => 3, "domain" => "platforma.slovensko.digital", "user_id" => 7, "internal" => false, "attachment" => false, "reflection" => false, "fancy_title" => nil }, { "url" => "http://www.informatizacia.sk/aktuality-tlacova-sprava-k-vyzvaniu-na-narodny-projekt-informacny-system-obchodneho-registra/25472c", "title" => "INFORMATIZÁCIA - Aktuálne", "clicks" => 3, "domain" => "www.informatizacia.sk", "user_id" => 7, "internal" => false, "attachment" => false, "reflection" => false, "fancy_title" => nil }, { "url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/e/e351d6d3c5cc79222275f78d0774aa96483f1c5f.pdf", "title" => nil, "clicks" => 2, "domain" => "platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com", "user_id" => 7, "internal" => false, "attachment" => true, "reflection" => false, "fancy_title" => nil }, { "url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/a/a34b54a00960e3af7fa92b736115b9e4620e5e83.PDF", "title" => nil, "clicks" => 1, "domain" => "platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com", "user_id" => 7, "internal" => false, "attachment" => true, "reflection" => false, "fancy_title" => nil }, { "url" => "http://platforma.slovensko.digital/t/implementacia-zmien-uprav-informacneho-systemu-obchodneho-registra-corwin-suvisiaca-so-zavedenim-noveho-typu-spolocnosti-s-nazvom-jednoducha-spolocnost-na-akcie-etapa-1-analyza-etapa-2-implementacia/3192", "title" => "Implementácia zmien (úprav) informačného systému obchodného registra CORWIN súvisiaca so zavedením nového typu spoločnosti s názvom ,,Jednoduchá spoločnosť na akcie\", Etapa 1- analýza, Etapa 2- implementácia", "clicks" => 1, "domain" => "platforma.slovensko.digital", "user_id" => 7, "internal" => false, "attachment" => false, "reflection" => false, "fancy_title" => nil }, { "url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/1/15854e70f13904751d2c879945bafaf435440b32.pdf", "title" => nil, "clicks" => 1, "domain" => "platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com", "user_id" => 7, "internal" => false, "attachment" => true, "reflection" => false, "fancy_title" => nil }, { "url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/5/52f2f45c3668014320214a9926ce30d98978536e.zip", "title" => nil, "clicks" => 1, "domain" => "platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com", "user_id" => 7, "internal" => false, "attachment" => true, "reflection" => false, "fancy_title" => nil }, { "url" => "http://platforma.slovensko.digital/t/vseobecne-dostupna-el-zbierka-listin-obchodneho-registra/3230", "title" => "Všeobecne dostupná el. zbierka listín obchodného registra", "clicks" => 0, "domain" => "platforma.slovensko.digital", "user_id" => 7, "internal" => false, "attachment" => false, "reflection" => false, "fancy_title" => nil }, { "url" => "http://platforma.slovensko.digital/t/obchodny-register-sr/3358/35", "title" => "Obchodný register SR", "clicks" => 0, "domain" => "platforma.slovensko.digital", "user_id" => 7, "internal" => false, "attachment" => false, "reflection" => false, "fancy_title" => nil }, { "url" => "http://platforma.slovensko.digital/t/obchodny-register-sr/3358", "title" => "Obchodný register SR", "clicks" => 0, "domain" => "platforma.slovensko.digital", "user_id" => 7, "internal" => false, "attachment" => false, "reflection" => false, "fancy_title" => nil }, { "url" => "https://metais.finance.gov.sk/studia/detail/67cab118-1a08-e9dc-a136-a2f3a51811dd?tab=basicForm", "title" => nil, "clicks" => 0, "domain" => "metais.finance.gov.sk", "user_id" => 7, "internal" => false, "attachment" => false, "reflection" => false, "fancy_title" => nil }, { "url" => "https://www.minv.sk/?schvalene-rz&subor=245880", "title" => nil, "clicks" => 0, "domain" => "www.minv.sk", "user_id" => 7, "internal" => false, "attachment" => false, "reflection" => false, "fancy_title" => nil }, { "url" => "https://www.opii.gov.sk/opiiapp.php/Vyzvania/show?id=369", "title" => "Operačný program Integrovaná infraštruktúra :: Vyhľadávanie vyzvaní", "clicks" => 0, "domain" => "www.opii.gov.sk", "user_id" => 7, "internal" => false, "attachment" => false, "reflection" => false, "fancy_title" => nil }], "created_by" => { "id" => 7, "username" => "Lubor", "avatar_template" => "/letter_avatar_proxy/v2/letter/l/96bed5/{size}.png" }, "last_poster" => { "id" => 226, "username" => "vojtob", "avatar_template" => "/letter_avatar_proxy/v2/letter/v/3da27b/{size}.png" }, "participants" => [{ "id" => 7, "username" => "Lubor", "post_count" => 1, "avatar_template" => "/letter_avatar_proxy/v2/letter/l/96bed5/{size}.png", "primary_group_name" => "core-team", "primary_group_flair_url" => "fa-asterisk ", "primary_group_flair_color" => "fff", "primary_group_flair_bg_color" => "fd0" }, { "id" => 226, "username" => "vojtob", "post_count" => 1, "avatar_template" => "/letter_avatar_proxy/v2/letter/v/3da27b/{size}.png", "primary_group_name" => nil, "primary_group_flair_url" => nil, "primary_group_flair_color" => nil, "primary_group_flair_bg_color" => nil }], "can_flag_topic" => false, "notification_level" => 1 }, "user_id" => 7, "visible" => true, "archived" => false, "can_vote" => false, "unpinned" => nil, "archetype" => "regular", "draft_key" => "topic_4228", "pinned_at" => nil, "bookmarked" => nil, "chunk_size" => 20, "created_at" => "2017-09-30T15:40:30.319Z", "deleted_at" => nil, "deleted_by" => nil, "like_count" => 2, "user_voted" => false, "vote_count" => nil, "word_count" => 1187, "category_id" => 43, "fancy_title" => "Red Flags: IS Obchodného registra", "has_summary" => false, "post_stream" => { "posts" => [{ "id" => 20183, "name" => "Ľubor Illek", "read" => true, "wiki" => true, "admin" => true, "reads" => 33, "score" => 79.65, "staff" => true, "yours" => false, "cooked" => "

    Názov: Informačný systém Obchodného registra (IS OR)

    Garant: Ministerstvo spravodlivosti SR

    \n

    Stručný opis: Implementovanť nový IS pre správu Obchodného registra a súvisiace opatrenia na zafektívnenie jeho procesov.

    \n

    Náklady na projekt: 13 250 000 EUR (s DPH, podľa vyzvania na národný projekt)

    \n

    Aktuálny stav projektu: Výzva na národný projekt v OPII

    Čo sa práve deje:

    \n
    • Nie sú známe žiadne plánované aktivity
    • Ani v ďalšej odrážke nie sú aktivity

    Zhrnutie hodnotenia Red Flags: Najvýraznejším nedostatkom projektu EDUNET je nedostatočná príprava projektu, chýbajúce zhodnotenie alternatív a ekonomické zhodnotenie rôznych modelov nákupu a budovania školskej siete. Projekt je nastavený tak, že na trhu s viac ako 300 dodávateľmi sa obstarávania zúčastnili len štyria. Ministerstvo taktiež odmieta odbornú diskusiu k projektu, čo výrazne zvyšuje obavu o jeho kvalitu.

    Stanovisko Slovensko.Digital: Odporúčame projekt pozastaviť a dôkladne zanalyzovať rôzne alternatívy budovania siete aj s ohľadom na podmienky na telekomunikačnom trhu.

    \n

    \n\":triangular_flag_on_post:\" HODNOTENIE RED FLAGS\n

    \n

    I. Prípravná fáza

    \n
    \n

    Reforma VS \":star:\"\":star:\"\":grey_star:\"\":grey_star:\"\n

    \n

    Projekt deklaruje optimalizáciu procesov priamo súvisiacich s Obchodným registrom.
    \nKonkrétne bola identifikovaná zmena v rozsahu subjektov ktoré môžu zapísať spoločnosť do OR.

    \n

    Projekt nerieši komplexne životné situácie súvisiace s podnikaním.
    \nNapr. celkový priemerný čas na založenie obchodnej spoločnosti je 45 dní (viď. RZ kap. 1). Tento projekt nerieši situáciu komplexne, ale iba časť týkajúcu sa zapísania do OR - z 2 dní na “obratom”. Aj v prípade uskutočnenia cieľov projektu sa výsledný celkový čas teda zlepší najviac o 5%.

    \n
    \n

    Merateľné ciele (KPI) \":star:\"\":star:\"\":star:\"\":grey_star:\"\n

    \n

    Podľa Reformného zámeru:

    \n


    \n

    \n

    “Termínom “obratom” sa v podmienkach Obchodného registra rozumie do konca dňa, obchodný register bude aktualizovaný na dennej báze.” (podľa Štúdie uskutočniteľnosti)

    \n

    Štúdia uskutočniteľnosti:

    \n
      \n
    • Dôležitým výstupovým ukazovateľom projektu je zabezpečenie 100% kvality a integrity dát v obchodnom registry. (kap. 1.1)
    • \n
    • Dáta z obchodného registra budú použiteľné na právne účely on-line, čím sa eliminuje potreba výpisov z obchodného registra. (kap 1.1)
    • \n
    \n

    Nie sú známe merateľné ciele súvisiace s optimalizáciou vnútorného prostredia verejnej správy súvisiacej s agendou OR, čo je jeden z deklarovaných zámerov projektu.

    \n
    \n

    Postup dosiahnutia cieľov \":star:\"\":star:\"\":star:\"\":grey_star:\"\n

    \n

    Projekt je rozdelený na dve etapy, prvá etapa sú funkcie pre ktoré nie je vyžadovaná legislatívna zmena (podľa štúdie uskutočniteľnosti). Štúdia uskutočniteľnosti obsahuje detailný harmonogram činností.
    \nOd iných subjektov je pri realizácii projektu závislá iba integrácia na referenčné údaje.

    \n

    Nie je známe, akým spôsobom bude realizované dosiahnutie cieľa “100% kvality dát v registri”, ktoré bude náročné aj vzhľadom na historické skúsenosti v tejto oblasti.

    \n
    \n

    Súlad s KRIS (nie je zatiaľ vyhodnotený)

    \n

    Súlad s KRIS či už ÚPVII, alebo OVM, ktoré majú byť do projektu zapojené, sme zatiaľ nevyhodnotili.
    \nVšetky KRIS majú do konca roka 2017 prejsť podstatnou aktualizáciou, doplnený by mal byť cieľový stav ISVS.

    \n
    \n

    Biznis prínos \":triangular_flag_on_post:\"\n

    \n

    Projekt má podstatne zjednodušiť internú administratívu súvisiacu s ORSR.
    \nImplementácia princípu “Digital by default” v rámci interného spracovania agendy.
    \nV súčasnosti je podstatná časť agiend OR už podporovaná špecializovaným IS, ide teda skôr o postupné zlepšenie v tejto oblasti.

    \n
    \n

    Príspevok v informatizácii \":star:\"\":star:\"\":star:\"\":star:\"\n

    \n

    Súčasťou projektu je riešenie kvality údajov (cieľ - 100% správnych údajov v registri).
    \nOd začiatku sú podporované princípy OpenData, OpenAPI, zverejnenie právne záväzných údajov.
    \nProjekt počíta s integráciou na všetky registre obsahujúce všetky údaje potrebné na jednotlivé procesy OR, čím sa zabezpečí princíp 1x a dosť voči OR.
    \nÚdaje OR majú byť referenčné a jednoducho použiteľné pre ostatné OVM. V tejto oblasti nie je jasné rozdelenie kompetencií medzi ORSR a RPO.

    \n

    Podľa štúdie bude v relevantných moduloch zvážený agilný prístup vývoja a možnosť obstarať časti ako službu (a nie ako dielo).

    \n

    V ďalších etapách projektu treba overiť, že uvažované je používateľsky prívetivé grafické rozhranie, nie len formulárový systém.

    \n
    \n

    Štúdia uskutočniteľnosti \":star:\"\":star:\"\":star:\"\":star:\"\n

    \n

    Štúdia uskutočniteľnosti bola vypracovaná, bolo umožnené jej pripomienkovanie, vzhľadom na (malý) rozsah projektu obsahuje dostatočnú mieru detailov, konkrétnych cieľov a postupov.
    \nUvedený prehľad v súčasnosti prevádzkovaného IS a súvisiacich nákladov.
    \nNavrhnutý nový IS je prehľadne členený na moduly.

    \n
    \n

    Alternatívy \":star:\"\":star:\"\":grey_star:\"\":grey_star:\"\n

    \n

    V Štúdii uskutočniteľnosti je podrobne analyzovaná možnosť rozvoja súčasného IS a vybudovanie nového IS a dopady rozšírenia okruhu registrátorov obchodných spoločností.
    \nNie sú analyzované varianty v závislosti od spôsobu realizácie jednotlivých modulov (napr. riešenie integrácií prostredníctvom MÚK vs. integrácie priamo G2G, obstaranie niektorých komponentov v režime SaaS).
    \nNie sú analyzované náklady a prínosy pri realizácie “nepovinných” súčastí, napr. publikovanie profilov obchodných firiem, resp. porovnanie s už existujúcimi riešeniami (napr. v komerčnej sfére).

    \n
    \n

    Kalkulácia efektívnosti \":star:\"\":grey_star:\"\":grey_star:\"\":grey_star:\"\n

    \n

    CBA uvádzanú v štúdii sme zatiaľ podrobne neanalyzovali. Vyzvanie na národný projekt obsahuje viaceré verzie CBA, s vzájomne nekonzistentnými hodnotami položiek.
    \nNiektoré položky projektu, napr. náklady na OpenData a OpenAPI - 400.000€, kalkulované náklady potrebné na integrácie G2G a BRIS - 2,2M€ sa javia ako neobvykle vysoké.
    \nNie je známe, akým spôsobom boli odhady nákladov na jednotlivé položky stanovené.

    \n
    \n

    Participácia na príprave projektu \":star:\"\":star:\"\":star:\"\":star:\"\n

    \n

    K reformnému zámeru aj štúdii uskutočniteľnosti bolo možné poslať pripomienky.
    \nMinisterstvo prizvalo vybrané subjekty (vrátane S.D), mohli sa vyjadriť a dať pripomienky k štúdii vo fáze draftu.
    \nUskutočnilo sa verejné prerokovanie štúdie, za účasti ministerky.

    \n

    Diskusie k projektu na platforme:

    \n\n
    \n

    \n\":file_folder:\" Dokumenty

    \n\n
    \n

    \n\":clock2:\" Aktivity

    \n

    V tomto projekte už prebehli nasledovné dôležité aktivity / míľniky:

    \n
      \n
    • 14.7.2016 Reformný zámer schválený HK OP EVS
    • \n
    • 2.6.2017 Štúdia uskutočniteľnosti schválená RV OPII PO 7
    • \n
    • 18.7.2017 v OPII PO 7 zverejnená výzva na Národný projekt IS Obchodného registra
    • \n
    ", "hidden" => false, "user_id" => 7, "version" => 2, "avg_time" => 65, "can_edit" => false, "can_wiki" => false, "topic_id" => 4228, "username" => "Lubor", "moderator" => true, "post_type" => 1, "can_delete" => false, "created_at" => "2017-09-30T15:40:30.482Z", "deleted_at" => nil, "topic_slug" => "red-flags-is-obchodneho-registra", "updated_at" => "2017-09-30T16:07:15.931Z", "user_title" => "Tím Slovensko.Digital", "can_recover" => false, "edit_reason" => nil, "link_counts" => [{ "url" => "http://www.informatizacia.sk/aktuality-tlacova-sprava-k-vyzvaniu-na-narodny-projekt-informacny-system-obchodneho-registra/25472c", "title" => "INFORMATIZÁCIA - Aktuálne", "clicks" => 3, "internal" => false, "reflection" => false }, { "url" => "http://platforma.slovensko.digital/t/o-kategorii-red-flags/4034", "title" => "O projekte Red Flags", "clicks" => 3, "internal" => true, "reflection" => false }, { "url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/e/e351d6d3c5cc79222275f78d0774aa96483f1c5f.pdf", "clicks" => 2, "internal" => false, "reflection" => false }, { "url" => "http://platforma.slovensko.digital/t/implementacia-zmien-uprav-informacneho-systemu-obchodneho-registra-corwin-suvisiaca-so-zavedenim-noveho-typu-spolocnosti-s-nazvom-jednoducha-spolocnost-na-akcie-etapa-1-analyza-etapa-2-implementacia/3192", "title" => "Implementácia zmien (úprav) informačného systému obchodného registra CORWIN súvisiaca so zavedením nového typu spoločnosti s názvom ,,Jednoduchá spoločnosť na akcie\", Etapa 1- analýza, Etapa 2- implementácia", "clicks" => 1, "internal" => true, "reflection" => false }, { "url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/5/52f2f45c3668014320214a9926ce30d98978536e.zip", "clicks" => 1, "internal" => false, "reflection" => false }, { "url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/1/15854e70f13904751d2c879945bafaf435440b32.pdf", "clicks" => 1, "internal" => false, "reflection" => false }, { "url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/a/a34b54a00960e3af7fa92b736115b9e4620e5e83.PDF", "clicks" => 1, "internal" => false, "reflection" => false }, { "url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/2/2f03dd98d3c8fdd5ad13f26a7a1d56811b9dc2f6.PNG", "title" => "2f03dd98d3c8fdd5ad13f26a7a1d56811b9dc2f6.PNG", "clicks" => 0, "internal" => false, "reflection" => false }, { "url" => "https://www.minv.sk/?schvalene-rz&subor=245880", "clicks" => 0, "internal" => false, "reflection" => false }, { "url" => "https://www.opii.gov.sk/opiiapp.php/Vyzvania/show?id=369", "title" => "Operačný program Integrovaná infraštruktúra :: Vyhľadávanie vyzvaní", "clicks" => 0, "internal" => false, "reflection" => false }, { "url" => "https://metais.finance.gov.sk/studia/detail/67cab118-1a08-e9dc-a136-a2f3a51811dd?tab=basicForm", "clicks" => 0, "internal" => false, "reflection" => false }, { "url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/9/9d20430e2999f9b8b4213e379807a55c882df401.PNG", "title" => "9d20430e2999f9b8b4213e379807a55c882df401.PNG", "clicks" => 0, "internal" => false, "reflection" => false }, { "url" => "http://platforma.slovensko.digital/t/vseobecne-dostupna-el-zbierka-listin-obchodneho-registra/3230", "title" => "Všeobecne dostupná el. zbierka listín obchodného registra", "clicks" => 0, "internal" => true, "reflection" => false }, { "url" => "http://platforma.slovensko.digital/t/obchodny-register-sr/3358", "title" => "Obchodný register SR", "clicks" => 0, "internal" => true, "reflection" => false }, { "url" => "http://platforma.slovensko.digital/t/obchodny-register-sr/3358/35", "title" => "Obchodný register SR", "clicks" => 0, "internal" => true, "reflection" => true }], "post_number" => 1, "quote_count" => 0, "reply_count" => 0, "trust_level" => 3, "user_deleted" => false, "last_wiki_edit" => "2017-09-30T16:07:15.994Z", "accepted_answer" => false, "actions_summary" => [{ "id" => 2, "count" => 2 }], "avatar_template" => "/letter_avatar_proxy/v2/letter/l/96bed5/{size}.png", "display_username" => "Ľubor Illek", "hidden_reason_id" => nil, "can_accept_answer" => false, "primary_group_name" => "core-team", "can_unaccept_answer" => false, "incoming_link_count" => 7, "reply_to_post_number" => nil, "can_view_edit_history" => true, "primary_group_flair_url" => "fa-asterisk ", "primary_group_flair_color" => "fff", "primary_group_flair_bg_color" => "fd0" }, { "id" => 20224, "name" => "Vojto Bálint", "read" => true, "wiki" => false, "admin" => false, "reads" => 23, "score" => 10.25, "staff" => false, "yours" => false, "cooked" => "

    Páči sa mi, že pri väčšine hodnotení, ktoré nie sú 100% je napísané čo chýba do 100%.
    \nNevidím to pri hodnotení biznis prínosu. Tam mi nie je veľmi jasné prečo sú iba 3 zo 4. Je to preto, že už čosi je urobené a teda prínos nie je príliš veľký? Čo by sa mohlo doplniť, aby bol prínos väčší? Jednoducho hodnotenie biznis prínosu sa mi zdá menej jasné ako pri ostatných atribútoch.

    ", "hidden" => false, "user_id" => 226, "version" => 1, "avg_time" => 17, "can_edit" => false, "can_wiki" => false, "topic_id" => 4228, "username" => "vojtob", "moderator" => false, "post_type" => 1, "can_delete" => false, "created_at" => "2017-10-03T09:37:10.218Z", "deleted_at" => nil, "topic_slug" => "red-flags-is-obchodneho-registra", "updated_at" => "2017-10-03T09:37:10.218Z", "user_title" => nil, "can_recover" => false, "edit_reason" => nil, "post_number" => 2, "quote_count" => 0, "reply_count" => 0, "trust_level" => 2, "user_deleted" => false, "accepted_answer" => false, "actions_summary" => [], "avatar_template" => "/letter_avatar_proxy/v2/letter/v/3da27b/{size}.png", "display_username" => "Vojto Bálint", "hidden_reason_id" => nil, "can_accept_answer" => false, "primary_group_name" => nil, "can_unaccept_answer" => false, "incoming_link_count" => 2, "reply_to_post_number" => nil, "can_view_edit_history" => true, "primary_group_flair_url" => nil, "primary_group_flair_color" => nil, "primary_group_flair_bg_color" => nil }], "stream" => [20183, 20224] }, "posts_count" => 2, "reply_count" => 0, "topic_timer" => nil, "pinned_until" => nil, "featured_link" => nil, "draft_sequence" => nil, "last_posted_at" => "2017-10-03T09:37:10.218Z", "actions_summary" => [{ "id" => 4, "count" => 0, "hidden" => false, "can_act" => false }, { "id" => 7, "count" => 0, "hidden" => false, "can_act" => false }, { "id" => 8, "count" => 0, "hidden" => false, "can_act" => false }], "pinned_globally" => false, "timeline_lookup" => [[1, 26], [2, 24]], "suggested_topics" => [{ "id" => 4237, "slug" => "red-flags-centralny-ekonomicky-system-ces", "tags" => ["ces", "mfsr"], "liked" => nil, "title" => "Red Flags: Centrálny ekonomický systém (CES)", "views" => 124, "bumped" => true, "closed" => false, "pinned" => false, "unseen" => false, "posters" => [{ "user" => { "id" => 3, "username" => "janhargas", "avatar_template" => "/user_avatar/platforma.slovensko.digital/janhargas/{size}/16_1.png" }, "extras" => "latest single", "description" => "Original Poster, Most Recent Poster" }], "visible" => true, "archived" => false, "unpinned" => nil, "archetype" => "regular", "bumped_at" => "2017-10-02T23:15:04.714Z", "image_url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/optimized/2X/9/98e99022eee3c425dd06555c953642497f5ab1e3_1_690x419.png", "bookmarked" => nil, "created_at" => "2017-10-02T23:07:25.117Z", "like_count" => 0, "category_id" => 43, "fancy_title" => "Red Flags: Centrálny ekonomický systém (CES)", "posts_count" => 1, "reply_count" => 0, "featured_link" => nil, "last_posted_at" => "2017-10-02T23:07:25.300Z", "highest_post_number" => 1 }, { "id" => 4262, "slug" => "red-flags-elektronicke-sluzby-narodneho-bezpecnostneho-uradu", "tags" => ["redflags", "nbú"], "liked" => nil, "title" => "Red Flags: Elektronické služby Národného bezpečnostného úradu", "views" => 101, "bumped" => true, "closed" => false, "pinned" => false, "unseen" => false, "posters" => [{ "user" => { "id" => 7, "username" => "Lubor", "avatar_template" => "/letter_avatar_proxy/v2/letter/l/96bed5/{size}.png" }, "extras" => "latest single", "description" => "Original Poster, Most Recent Poster" }], "visible" => true, "archived" => false, "unpinned" => nil, "archetype" => "regular", "bumped_at" => "2017-10-09T07:19:23.747Z", "image_url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/f/f0fed63872490f19a395d13b1bea66bee20ddbc1.PNG", "bookmarked" => nil, "created_at" => "2017-10-09T07:19:23.412Z", "like_count" => 0, "category_id" => 43, "fancy_title" => "Red Flags: Elektronické služby Národného bezpečnostného úradu", "posts_count" => 1, "reply_count" => 0, "featured_link" => nil, "last_posted_at" => "2017-10-09T07:19:23.747Z", "highest_post_number" => 1 }, { "id" => 4035, "slug" => "red-flags-elektronicke-sluzby-statneho-fondu-rozvoja-byvania-es-sfrb", "tags" => ["mdvrr", "šfrb", "redflags"], "liked" => nil, "title" => "Red Flags: Elektronické služby Štátneho fondu rozvoja bývania (ES ŠFRB)", "views" => 319, "bumped" => true, "closed" => false, "pinned" => false, "unseen" => false, "posters" => [{ "user" => { "id" => 1, "username" => "jsuchal", "avatar_template" => "/user_avatar/platforma.slovensko.digital/jsuchal/{size}/15_1.png" }, "extras" => nil, "description" => "Original Poster" }, { "user" => { "id" => 1334, "username" => "Tomas_Pavelka", "avatar_template" => "/letter_avatar_proxy/v2/letter/t/48db29/{size}.png" }, "extras" => "latest", "description" => "Most Recent Poster" }], "visible" => true, "archived" => false, "unpinned" => nil, "archetype" => "regular", "bumped_at" => "2017-09-04T13:08:23.895Z", "image_url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/optimized/2X/2/29a8e3cb1cfd3b376a3f866bc9888fbd84fd7452_1_446x500.PNG", "bookmarked" => nil, "created_at" => "2017-08-07T14:03:35.245Z", "like_count" => 2, "category_id" => 43, "fancy_title" => "Red Flags: Elektronické služby Štátneho fondu rozvoja bývania (ES ŠFRB)", "posts_count" => 2, "reply_count" => 0, "featured_link" => nil, "last_posted_at" => "2017-08-23T18:12:11.332Z", "highest_post_number" => 4 }, { "id" => 4236, "slug" => "red-flags-komplexne-zvysenie-kvality-procesov-v-podmienkach-zboru-vazenskej-a-justicnej-straze", "tags" => ["redflags"], "liked" => nil, "title" => "Red Flags: Komplexné zvýšenie kvality procesov v podmienkach Zboru väzenskej a justičnej stráže", "views" => 107, "bumped" => true, "closed" => false, "pinned" => false, "unseen" => false, "posters" => [{ "user" => { "id" => 1542, "username" => "Michal_Haman", "avatar_template" => "/user_avatar/platforma.slovensko.digital/michal_haman/{size}/1827_1.png" }, "extras" => "latest single", "description" => "Original Poster, Most Recent Poster" }], "visible" => true, "archived" => false, "unpinned" => nil, "archetype" => "regular", "bumped_at" => "2017-10-02T21:51:55.162Z", "image_url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/original/2X/f/f5267b6a7df2234e30b3877e9a9b74b988be5a6c.png", "bookmarked" => nil, "created_at" => "2017-10-02T21:51:54.195Z", "like_count" => 0, "category_id" => 43, "fancy_title" => "Red Flags: Komplexné zvýšenie kvality procesov v podmienkach Zboru väzenskej a justičnej stráže", "posts_count" => 1, "reply_count" => 0, "featured_link" => nil, "last_posted_at" => "2017-10-02T21:51:55.162Z", "highest_post_number" => 1 }, { "id" => 4284, "slug" => "red-flags-informacny-system-centra-pravnej-pomoci", "tags" => ["redflags", "mssr"], "liked" => nil, "title" => "Red Flags: Informačný systém Centra právnej pomoci", "views" => 61, "bumped" => true, "closed" => false, "pinned" => false, "unseen" => false, "posters" => [{ "user" => { "id" => 7, "username" => "Lubor", "avatar_template" => "/letter_avatar_proxy/v2/letter/l/96bed5/{size}.png" }, "extras" => "latest single", "description" => "Original Poster, Most Recent Poster" }], "visible" => true, "archived" => false, "unpinned" => nil, "archetype" => "regular", "bumped_at" => "2017-10-16T05:24:46.270Z", "image_url" => "//platforma-slovensko-digital-uploads.s3-eu-central-1.amazonaws.com/optimized/2X/5/5d559a19038a394b4fbe4386620a10e4bee75f4e_1_690x427.PNG", "bookmarked" => nil, "created_at" => "2017-10-16T00:26:01.070Z", "like_count" => 0, "category_id" => 43, "fancy_title" => "Red Flags: Informačný systém Centra právnej pomoci", "posts_count" => 1, "reply_count" => 0, "featured_link" => nil, "last_posted_at" => "2017-10-16T00:26:01.236Z", "highest_post_number" => 1 }], "participant_count" => 2, "highest_post_number" => 2, "message_bus_last_id" => 19, "pm_with_non_human_user" => false } } end diff --git a/spec/features/administration_spec.rb b/spec/features/administration_spec.rb index 13851af..45bedf0 100644 --- a/spec/features/administration_spec.rb +++ b/spec/features/administration_spec.rb @@ -7,7 +7,7 @@ authorize_as_admin visit admin_root_path - expect { click_on 'Synchronize' }.to have_enqueued_job(SyncCategoryTopicsJob) + expect { click_on 'Synchronize' }.to have_enqueued_job(SyncAllTopicsJob) end def see_all_pages @@ -29,8 +29,8 @@ def see_all_pages scenario 'As admin I want to see all project pages' do create(:page, :published) create(:page, :unpublished) - create(:project, page: Page.first) - create(:project, page: Page.second) + create(:project) + create(:project) see_all_pages end @@ -44,12 +44,11 @@ def preview_page end scenario 'As admin I want to preview page' do - create(:page) - preview_page - end + page_to_preview = create(:page) + revision = page_to_preview.revisions.first + + SyncRevisionJob.perform_now(revision) - scenario 'As admin I want to preview project page' do - create(:page) preview_page end @@ -65,13 +64,23 @@ def publish_page end scenario 'As admin I want to publish page' do - create(:page, :unpublished) - publish_page - end + allow(UpdateMultipleSheetColumnsJob).to receive(:perform_later) + allow(ExportTopicIntoSheetJob).to receive(:perform_later) + + page_to_preview = create(:page, :unpublished) + project = create(:project) + revision = page_to_preview.revisions.first + + phase_revision = nil + project.phases.each do |phase| + phase_revision = phase.revisions.find_or_initialize_by(revision_id: revision&.id) + end + + if phase_revision.present? + phase_revision.load_from_data(revision&.raw) + phase_revision.save! + end - scenario 'As admin I want to publish project page' do - create(:page, :unpublished) - create(:project, page: Page.first) publish_page end @@ -87,13 +96,22 @@ def unpublish_page end scenario 'As admin I want to unpublish page' do - create(:page, :published) - unpublish_page - end + allow(UpdateMultipleSheetColumnsJob).to receive(:perform_later) + + page_to_preview = create(:page, :published) + project = create(:project) + revision = page_to_preview.revisions.first + + phase_revision = nil + project.phases.each do |phase| + phase_revision = phase.revisions.find_or_initialize_by(revision_id: revision&.id) + end + + if phase_revision.present? + phase_revision.load_from_data(revision&.raw) + phase_revision.save! + end - scenario 'As admin I want to unpublish project page' do - create(:page, :published) - create(:project, page: Page.first) unpublish_page end @@ -119,90 +137,75 @@ def see_all_revisions end scenario 'As admin I want to see all revisions of project page' do - create(:page, :published) - create(:project, page: Page.first) - create(:revision, page: Page.first) + page = create(:page, :published) + create(:project) + create(:revision, page: page) + see_all_revisions end - def preview_non_latest_revision + def preview_non_latest_revision(version) authorize_as_admin visit admin_root_path - click_on Page.first.id - - within :id, dom_id(Revision.first) do + within :id, dom_id(Revision.find_by_version(version)) do expect(page).not_to have_content('published') expect(page).not_to have_content('latest') - - click_on 'Preview' end end scenario 'As admin I want to preview non-latest page revision' do - create(:page, :unpublished) - create(:revision, page: Page.first) - preview_non_latest_revision + page = create(:page, :unpublished) + project = create(:project) + + older_revision = create(:revision, page: page, version: 1) + create_project_revision(project, older_revision) + + latest_revision = create(:revision, page: page, version: 2) + create_project_revision(project, latest_revision) + + preview_non_latest_revision(older_revision.version) end - scenario 'As admin I want to preview non-latest project page revision' do - create(:page, :unpublished) - create(:project, page: Page.first) - create(:revision, page: Page.first) - preview_non_latest_revision + def create_project_revision(project, revision) + phase_revision = nil + project.phases.each do |phase| + phase_revision = phase.revisions.find_or_initialize_by(revision_id: revision&.id) + end + + if phase_revision.present? + phase_revision.load_from_data(revision&.raw) + phase_revision.save! + end end - def publish_non_latest_revision + def publish_non_latest_revision(version) authorize_as_admin visit admin_root_path - click_on Page.first.id - within :id, dom_id(Revision.first) do + within :id, dom_id(Revision.find_by_version(version)) do expect(page).not_to have_content('published') expect(page).not_to have_content('latest') - click_on 'Publish' end - within :id, dom_id(Revision.first) do + within :id, dom_id(Revision.find_by_version(version)) do expect(page).to have_content('published') end end scenario 'As admin I want to publish non-latest page revision' do - create(:page, :unpublished) - create(:revision, page: Page.first) - publish_non_latest_revision - end + page = create(:page, :unpublished) + phase = create(:phase) - scenario 'As admin I want to publish non-latest project page revision' do - create(:page, :unpublished) - create(:project, page: Page.first) - create(:revision, page: Page.first) - publish_non_latest_revision - end - - context 'project page specific features' do - scenario 'As admin I want to edit project category' do - create(:project) - - authorize_as_admin - visit admin_root_path + older_revision = create(:revision, page: page, version: 1) + create(:phase_revision, revision: older_revision, phase: phase) - click_on Page.first.id + latest_revision = create(:revision, page: page, version: 2) + create(:phase_revision, revision: latest_revision, phase: phase) - within 'h4' do - expect(page).to have_content('boring') - end - - choose 'good' - click_on 'Update' - - within 'h4' do - expect(page).to have_content('good') - end - end + publish_non_latest_revision(1) end private diff --git a/spec/jobs/sync_all_topics_job_spec.rb b/spec/jobs/sync_all_topics_job_spec.rb new file mode 100644 index 0000000..63543f9 --- /dev/null +++ b/spec/jobs/sync_all_topics_job_spec.rb @@ -0,0 +1,85 @@ +require 'rails_helper' + +RSpec.describe SyncAllTopicsJob, type: :job do + include ActiveJob::TestHelper + + subject(:job) { described_class.perform_later } + + let(:sheets_service) { instance_double('GoogleApiService') } + let(:response_values) do + [ + [], + [], + ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], + ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], + ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ] + end + + let(:indices) { { 'Projekt' => 0, 'Projekt ID' => 1, 'Platforma' => 2, 'ID draft prípravy' => 3, 'ID prípravy' => 4, 'ID draft produktu' => 5, 'ID produktu' => 6 } } + + before do + google_sheets_service = instance_double(Google::Apis::SheetsV4::SheetsService) + allow(GoogleApiService).to receive(:get_sheets_service).and_return(google_sheets_service) + allow(google_sheets_service).to receive(:get_spreadsheet_values).with(ENV.fetch('GOOGLE_SHEET_ID'), 'A:Z').and_return(OpenStruct.new(values: response_values)) + end + + it 'queues the job' do + expect { job }.to have_enqueued_job(described_class) + .on_queue("default") + end + + it 'executes perform' do + described_class.perform_now + expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq 4 + end + + context 'when mandatory columns are missing' do + let(:response_values) do + [ + [], + [], + ['Projekt'], + ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], + ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ] + end + + it 'raises an ArgumentError' do + expect { described_class.perform_now } + .to raise_error(ArgumentError, "Could not find required columns in the spreadsheet.") + end + end + + context 'when spreadsheet values are missing' do + let(:response_values) do + [ + [], + [], + ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'] + ] + end + + it 'does not enqueue any jobs' do + described_class.perform_now + expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq 0 + end + end + + context 'when Platforma is empty for any project' do + let(:response_values) do + [ + [], + [], + ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], + ['Projekt1', 'ABC1', '', 'ABC1', '', 'ABC1', ''], + ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ] + end + + it 'enqueues SyncGoogleDocumentJob' do + described_class.perform_now + expect(ActiveJob::Base.queue_adapter.enqueued_jobs.map { |j| j[:job] }).to include(SyncGoogleDocumentJob) + end + end +end diff --git a/spec/jobs/sync_category_topics_job_spec.rb b/spec/jobs/sync_category_topics_job_spec.rb index de26ce3..64a3164 100644 --- a/spec/jobs/sync_category_topics_job_spec.rb +++ b/spec/jobs/sync_category_topics_job_spec.rb @@ -6,9 +6,9 @@ it 'schedules topic sync jobs for all topics in category', vcr: true do subject.perform('red-flags', sync_topic_job: sync_topic_job) - expect(sync_topic_job).to have_received(:perform_later).with(4034) - expect(sync_topic_job).to have_received(:perform_later).with(4334) - expect(sync_topic_job).to have_received(:perform_later).with(4035) + expect(sync_topic_job).to have_received(:perform_later).with(nil, 4034) + expect(sync_topic_job).to have_received(:perform_later).with(nil, 4334) + expect(sync_topic_job).to have_received(:perform_later).with(nil, 4035) end it 'schedules next topic page when listing too long', vcr: true do diff --git a/spec/jobs/sync_google_document_job_spec.rb b/spec/jobs/sync_google_document_job_spec.rb new file mode 100644 index 0000000..cdf9773 --- /dev/null +++ b/spec/jobs/sync_google_document_job_spec.rb @@ -0,0 +1,69 @@ +require 'rails_helper' + +RSpec.describe SyncGoogleDocumentJob, type: :job do + include ActiveJob::TestHelper + + subject(:job) { described_class.perform_later(project_name, project_id, google_document_id, page_id, page_type) } + + let(:project_name) { 'Project1' } + let(:project_id) { 'ABC1' } + let(:google_document_id) { 'DOC1' } + let(:page_id) { 'PAGE1' } + let(:page_type) { 0 } + let(:drive_service) { instance_double(Google::Apis::DriveV3::DriveService) } + let(:revisions_count) { 1 } + let(:document) { instance_double('Google::Apis::DocsV1::Document') } + let(:parser_service) { instance_double('DocumentParserService') } + let(:html_content) { 'Content' } + let(:parsed_hash) { {content: 'Parsed content'} } + let(:page) { create(:page) } + let(:revision) { create(:revision) } + let(:revision) { create(:revision) } + + before do + allow(Page).to receive_message_chain(:find_or_create_by!).and_return(page) + allow(Project).to receive_message_chain(:find_or_create_by!).and_return(instance_double('Project')) + allow(GoogleApiService).to receive(:get_drive_service).and_return(drive_service) + allow(drive_service).to receive(:list_revisions).and_return(instance_double(Google::Apis::DriveV3::RevisionList, revisions: Array.new(revisions_count))) + allow(GoogleApiService).to receive(:get_document).and_return(document) + allow(DocumentParserService).to receive(:new).with(document).and_return(parser_service) + allow(parser_service).to receive(:to_html).and_return(html_content) + allow(parser_service).to receive(:to_hash).with(html_content).and_return(revision.raw) + allow(page).to receive(:revisions).and_return(Revision) + allow(Revision).to receive(:find_or_initialize_by).and_return(revision) + allow(revision.phase_revision).to receive(:load_ratings).with(revision.raw) + end + + it 'queues the job' do + expect { job }.to have_enqueued_job(described_class) + .on_queue("default") + .with(project_name, project_id, google_document_id, page_id, page_type) + .at(:no_wait) + end + + context 'when performing the job' do + it 'invokes methods in correct sequence' do + described_class.perform_now(project_name, project_id, google_document_id, page_id, page_type) + + expect(GoogleApiService).to have_received(:get_drive_service) + expect(drive_service).to have_received(:list_revisions) + expect(GoogleApiService).to have_received(:get_document) + expect(DocumentParserService).to have_received(:new).with(document) + expect(parser_service).to have_received(:to_html) + expect(parser_service).to have_received(:to_hash).with(html_content) + expect(page).to have_received(:revisions) + expect(Revision).to have_received(:find_or_initialize_by) + end + end + + context 'when GoogleApiService raises error' do + before do + allow(GoogleApiService).to receive(:get_drive_service).and_raise(StandardError.new) + end + + it 'raises an exception' do + expect { described_class.perform_now(project_name, project_id, google_document_id, page_id, page_type) } + .to raise_error(StandardError) + end + end +end \ No newline at end of file diff --git a/spec/jobs/sync_one_topic_job_spec.rb b/spec/jobs/sync_one_topic_job_spec.rb new file mode 100644 index 0000000..52d5c65 --- /dev/null +++ b/spec/jobs/sync_one_topic_job_spec.rb @@ -0,0 +1,74 @@ +require 'rails_helper' + +RSpec.describe SyncOneTopicJob, type: :job do + include ActiveJob::TestHelper + + subject(:job) { described_class.perform_later(page_id) } + + let(:page_id) { 'ABC1' } + let(:sheets_service) { instance_double('GoogleApiService') } + let(:response_values) do + [ + [], + [], + ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], + ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], + ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ] + end + + let(:row) { ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'] } + + let(:indices) { { 'Projekt' => 0, 'Projekt ID' => 1, 'Platforma' => 2, 'ID draft prípravy' => 3, 'ID prípravy' => 4, 'ID draft produktu' => 5, 'ID produktu' => 6 } } + + before do + google_sheets_service = instance_double(Google::Apis::SheetsV4::SheetsService) + allow(GoogleApiService).to receive(:get_sheets_service).and_return(google_sheets_service) + allow(google_sheets_service).to receive(:get_spreadsheet_values).with(ENV.fetch('GOOGLE_SHEET_ID'), 'A:Z').and_return(OpenStruct.new(values: response_values)) + end + + it 'queues the job' do + expect { job }.to have_enqueued_job(described_class) + .on_queue("default") + .with(page_id) + end + + it 'executes perform' do + described_class.perform_now(page_id) + expect(ActiveJob::Base.queue_adapter.enqueued_jobs.map { |j| j[:job] }).to include(SyncGoogleDocumentJob) + end + + context 'when performing the job' do + it 'fetches spreadsheet values, finds indices and processes row' do + expect_any_instance_of(SyncOneTopicJob).to receive(:find_indices).with(OpenStruct.new(values: response_values).values[2]).and_return(indices) + expect_any_instance_of(SyncOneTopicJob).to receive(:process_row).with(row, indices, page_id) + + described_class.perform_now(page_id) + end + end + + context 'when no row matches the given page_id' do + let(:page_id) { 'Prod2' } + + it 'does not attempt to process the row' do + expect_any_instance_of(SyncOneTopicJob).not_to receive(:process_row) + + described_class.perform_now(page_id) + end + end + + context 'when mandatory columns are missing' do + let(:response_values) do + [ + ['Projekt'], + ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], + ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ] + end + + it 'raises an ArgumentError' do + expect { described_class.perform_now(page_id) } + .to raise_error(ArgumentError, "Could not find required columns in the spreadsheet.") + end + end +end \ No newline at end of file diff --git a/spec/jobs/sync_project_job_spec.rb b/spec/jobs/sync_project_job_spec.rb deleted file mode 100644 index 896ac9b..0000000 --- a/spec/jobs/sync_project_job_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe SyncProjectJob, type: :job do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/jobs/sync_revision_job_spec.rb b/spec/jobs/sync_revision_job_spec.rb index c8192a5..98c0ed7 100644 --- a/spec/jobs/sync_revision_job_spec.rb +++ b/spec/jobs/sync_revision_job_spec.rb @@ -9,11 +9,12 @@ end it 'parses project metadata from revision' do - revision = create(:revision) + page_to_preview = create(:page) + revision = page_to_preview.revisions.first subject.perform(revision) - snapshot = ProjectRevision.first + snapshot = PhaseRevision.last expect(snapshot).to have_attributes( title: 'IS Obchodného registra', @@ -33,25 +34,4 @@ expect(ratings['Participácia na príprave projektu'].score).to eq(4) expect(ratings['Biznis prínos'].score).to eq(0) end - - it 'ignores pages from unknown category' do - revision = create(:revision) - revision.raw['category_id'] = 123 - revision.save! - - subject.perform(revision) - - expect(ProjectRevision.count).to eq(0) - end - - it 'adds calculated total and max score to revision' do - revision = create(:revision) - - subject.perform(revision) - - snapshot = ProjectRevision.first - - expect(snapshot.total_score).to eq(6) - expect(snapshot.maximum_score).to eq(12) - end end diff --git a/spec/jobs/sync_topic_job_spec.rb b/spec/jobs/sync_topic_job_spec.rb index f13105c..ea5c97a 100644 --- a/spec/jobs/sync_topic_job_spec.rb +++ b/spec/jobs/sync_topic_job_spec.rb @@ -2,7 +2,8 @@ RSpec.describe SyncTopicJob, type: :job do it 'loads current version of topic into page and revision', vcr: true do - subject.perform(4034) + project = create(:project) + subject.perform(project.id ,4034) page = Page.first diff --git a/spec/jobs/update_multiple_sheet_columns_job_spec.rb b/spec/jobs/update_multiple_sheet_columns_job_spec.rb new file mode 100644 index 0000000..f4ea84d --- /dev/null +++ b/spec/jobs/update_multiple_sheet_columns_job_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +RSpec.describe UpdateMultipleSheetColumnsJob, type: :job do + let(:page_id) { 'some_page_id' } + let(:updates) do + [ + { column_names: ['name1', 'name2'], page_type: 'type1', published_value: 'val1' }, + { column_names: ['name3', 'name4'], page_type: 'type2', published_value: 'val2' } + ] + end + + before do + allow(UpdateSheetValueJob).to receive(:perform_now) + end + + it 'tasks are enqueued and update is performed' do + described_class.perform_now(page_id, updates) + + expect(UpdateSheetValueJob).to have_received(:perform_now).with( + page_id, + updates[0][:column_names], + updates[0][:page_type], + updates[0][:published_value] + ) + expect(UpdateSheetValueJob).to have_received(:perform_now).with( + page_id, + updates[1][:column_names], + updates[1][:page_type], + updates[1][:published_value] + ) + expect(UpdateSheetValueJob).to have_received(:perform_now).twice + end +end \ No newline at end of file diff --git a/spec/jobs/update_sheet_value_job_spec.rb b/spec/jobs/update_sheet_value_job_spec.rb new file mode 100644 index 0000000..a557ddc --- /dev/null +++ b/spec/jobs/update_sheet_value_job_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe UpdateSheetValueJob, type: :job do + include ActiveJob::TestHelper + + let(:page_id) { 'Prod1' } + let(:column_names) { { 'topic' => 'ID prípravy', 'product' => 'ID produktu' } } + let(:page_type) { 'product' } + let(:published_value) { 'DraftProd1' } + let(:response_values) do + OpenStruct.new(values: [ + [], + [], + ['Projekt', 'Projekt ID', 'Platforma', 'ID draft prípravy', 'ID prípravy', 'ID draft produktu', 'ID produktu'], + ['Projekt1', 'ABC1', '', 'ABC1', 'ABC1', 'ABC1', 'ABC1'], + ['Projekt2', 'ABC2', 'http://google.com', 'ABC2', 'ABC2', 'ABC2', 'ABC2'] + ]) + end + let(:header_row) { response_values.values[2] } + let(:indices) { { 'ID prípravy' => 4, 'ID produktu' => 6 } } + let(:sheets_service) { instance_double(Google::Apis::SheetsV4::SheetsService) } + + before do + allow(GoogleApiService).to receive(:get_sheets_service).and_return(sheets_service) + allow(sheets_service).to receive(:get_spreadsheet_values).with(ENV['GOOGLE_SHEET_ID'], 'A:Z').and_return(response_values) + allow_any_instance_of(UpdateSheetValueJob).to receive(:find_indices).and_return(indices) + allow_any_instance_of(UpdateSheetValueJob).to receive(:find_row_index).and_return(0) + allow(sheets_service).to receive(:update_spreadsheet_value) + end + + context 'when performing the job' do + let(:instance) { described_class.new } + + before do + allow(UpdateSheetValueJob).to receive(:new).and_return(instance) + allow(instance).to receive(:find_indices).and_call_original + allow(instance).to receive(:find_row_index).and_return(3) + end + + it 'fetches spreadsheet values, finds indices, finds row, handles row and updates sheet' do + instance.perform(page_id, column_names, page_type, published_value) + + expect(GoogleApiService).to have_received(:get_sheets_service) + expect(sheets_service).to have_received(:get_spreadsheet_values).with(ENV['GOOGLE_SHEET_ID'], 'A:Z') + expect(instance).to have_received(:find_indices).with(header_row) + expect(instance).to have_received(:find_row_index).with(response_values.values[3..-1], indices, page_id) + expect(sheets_service).to have_received(:update_spreadsheet_value) + end + end + + context 'when no matching column for page type' do + let(:column_names) { {} } + + it 'raises an ArgumentError' do + expect { described_class.perform_now(page_id, column_names, page_type, published_value) }.to raise_error(ArgumentError, "No matching column for page type.") + end + end + + context 'when no matching row for page id' do + let(:page_id) { 'nonexistent' } + + before do + allow_any_instance_of(UpdateSheetValueJob).to receive(:find_row_index).and_return(nil) + end + + it 'raises an ArgumentError' do + expect { described_class.perform_now(page_id, column_names, page_type, published_value) }.to raise_error(ArgumentError, "No data found for the given page_id in the spreadsheet. ID may not match or is not in string format.") + end + end +end \ No newline at end of file
    <%= index + 1 %><%= link_to project.title, project_path(project.project) %><%= link_to phase_revision.revision.page.title, project_show_revision_type_path(phase_revision.phase.project, PhaseRevision.map_phase_type_to_route(phase_revision.phase.phase_type.name)) %>