diff --git a/.rubocop_rails.yml b/.rubocop_rails.yml index 68f4c40d64..e3067271ee 100644 --- a/.rubocop_rails.yml +++ b/.rubocop_rails.yml @@ -89,6 +89,8 @@ Rails/SkipsModelValidations: Enabled: true Exclude: - db/migrate/*.rb + - lib/extends/models/decidim/decidim_awesome/proposal_extra_field_extends.rb + - spec/lib/tasks/decidim_app/set_decrypted_private_body_task_spec.rb Rails/Validation: Include: diff --git a/Gemfile b/Gemfile index b066a14083..4eb3c8f52b 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ gem "decidim-spam_detection", git: "https://github.com/OpenSourcePolitics/decidi gem "decidim-survey_multiple_answers", git: "https://github.com/OpenSourcePolitics/decidim-module-survey_multiple_answers" gem "decidim-term_customizer", git: "https://github.com/OpenSourcePolitics/decidim-module-term_customizer.git", branch: "fix/email_with_precompile" -gem "decidim-guest_meeting_registration", git: "https://github.com/alecslupu-pfa/guest-meeting-registration.git", branch: DECIDIM_BRANCH, ref: "532de9e" +gem "decidim-guest_meeting_registration", git: "https://github.com/alecslupu-pfa/guest-meeting-registration.git", branch: DECIDIM_BRANCH # Omniauth gems gem "omniauth-france_connect", git: "https://github.com/OpenSourcePolitics/omniauth-france_connect" gem "omniauth_openid_connect" diff --git a/Gemfile.lock b/Gemfile.lock index c49bd80729..ed7ec8559f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -153,8 +153,7 @@ GIT GIT remote: https://github.com/alecslupu-pfa/guest-meeting-registration.git - revision: 532de9e87ecb8b2959c0013c6408d4c092abce87 - ref: 532de9e + revision: 7b3af0d34d053cc430080e483cd6d1e48dcc0f32 branch: release/0.27-stable specs: decidim-guest_meeting_registration (0.27.7) diff --git a/app/jobs/active_storage_clear_orphans_job.rb b/app/jobs/active_storage_clear_orphans_job.rb new file mode 100644 index 0000000000..efa7554111 --- /dev/null +++ b/app/jobs/active_storage_clear_orphans_job.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class ActiveStorageClearOrphansJob < ApplicationJob + include ActionView::Helpers::NumberHelper + queue_as :default + + def perform(**args) + limit = args[:limit] || 10_000 + Rails.logger.info "Looking for orphan blobs in S3... (limit: #{limit})" + objects = ActiveStorage::Blob.service.bucket.objects + Rails.logger.info "Total files: #{objects.size}" + + current_iteration = 0 + sum = 0 + orphans_count = 0 + objects.each do |obj| + break if current_iteration >= limit + + current_iteration += 1 + next if ActiveStorage::Blob.exists?(key: obj.key) + + sum += delete_object(obj) + orphans_count += 1 + end + + Rails.logger.info "Size: #{number_to_human_size(sum)} in #{orphans_count} files" + Rails.logger.info "Configuration limit is #{limit} files" + Rails.logger.info "Terminated task... " + end + + private + + def delete_object(obj) + Rails.logger.info "Removing orphan: #{obj.key}" + size = obj.size + obj.delete + size + end +end diff --git a/app/jobs/private_body_decrypt_job.rb b/app/jobs/private_body_decrypt_job.rb new file mode 100644 index 0000000000..275f8bf62f --- /dev/null +++ b/app/jobs/private_body_decrypt_job.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class PrivateBodyDecryptJob < ApplicationJob + queue_as :default + + def perform + extra_fields = Decidim::DecidimAwesome::ProposalExtraField.where(decrypted_private_body: nil).where.not(private_body: nil) + return unless extra_fields.any? + + Rails.logger.info "Extra fields to update: #{extra_fields.size}" + count = 0 + extra_fields.find_each do |extra_field| + extra_field.update(decrypted_private_body: extra_field.private_body.to_s) + count += 1 if extra_field.decrypted_private_body_previous_change.present? + end + Rails.logger.info "Extra fields updated: #{count}" + end +end diff --git a/app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js b/app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js new file mode 100644 index 0000000000..c426e9645a --- /dev/null +++ b/app/packs/src/decidim/decidim_awesome/admin/custom_fields_builder.js @@ -0,0 +1,79 @@ +require("formBuilder/dist/form-builder.min.js") +import "src/decidim/decidim_awesome/forms/rich_text_plugin" + +window.CustomFieldsBuilders = window.CustomFieldsBuilders || []; + +$(() => { + $(".awesome-edit-config .proposal_custom_fields_editor").each((_idx, el) => { + const key = $(el).closest(".proposal_custom_fields_container").data("key"); + const configVar = $(el).closest(".proposal_custom_fields_container").data("var"); + // DOCS: https://formbuilder.online/docs + window.CustomFieldsBuilders.push({ + el: el, + key: key, + var: configVar, + config: { + i18n: { + locale: "fr-FR", + location: "https://decidim.storage.opensourcepolitics.eu/osp-cdn/form_builder/1.1.0" + }, + formData: $(`input[name="config[${configVar}][${key}]"]`).val(), + disableFields: ["button", "file"], + disabledActionButtons: ["save", "data", "clear"], + disabledAttrs: [ + "access", + "inline", + "className" + ], + controlOrder: [ + "text", + "textarea", + "number", + "date", + "checkbox-group", + "radio-group", + "select", + "autocomplete", + "header", + "paragraph" + ], + disabledSubtypes: { + // default color as it generate hashtags in decidim (TODO: fix hashtag generator with this) + text: ["color"], + // disable default wysiwyg editors as they present problems + textarea: ["tinymce", "quill"] + } + }, + instance: null + }); + }); + + $(document).on("formBuilder.create", (_event, idx, list) => { + if (!list[idx]) { + return; + } + + $(list[idx].el).formBuilder(list[idx].config).promise.then(function(res) { + list[idx].instance = res; + // Attach to DOM + list[idx].el.FormBuilder = res; + // remove spinner + $(list[idx].el).find(".loading-spinner").remove(); + // for external use + $(document).trigger("formBuilder.created", [list[idx]]); + if (idx < list.length) { + $(document).trigger("formBuilder.create", [idx + 1, list]); + } + }); + }); + + if (window.CustomFieldsBuilders.length) { + $(document).trigger("formBuilder.create", [0, window.CustomFieldsBuilders]); + } + + $("form.awesome-edit-config").on("submit", () => { + window.CustomFieldsBuilders.forEach((builder) => { + $(`input[name="config[${builder.var}][${builder.key}]"]`).val(builder.instance.actions.getData("json")); + }); + }); +}); diff --git a/app/packs/src/decidim/decidim_awesome/editors/editor.js b/app/packs/src/decidim/decidim_awesome/editors/editor.js new file mode 100644 index 0000000000..7bec3c9396 --- /dev/null +++ b/app/packs/src/decidim/decidim_awesome/editors/editor.js @@ -0,0 +1,214 @@ +/* eslint-disable require-jsdoc, func-style */ + +/* +* Since version 0.25 we follow a different strategy and opt to destroy and override completely the original editor +* That's because editors are instantiated directly instead of creating a global function to instantiate them +*/ + +import lineBreakButtonHandler from "src/decidim/editor/linebreak_module" +import InscrybMDE from "inscrybmde" +import Europa from "europa" +import "inline-attachment/src/inline-attachment"; +import "inline-attachment/src/codemirror-4.inline-attachment"; +import "inline-attachment/src/jquery.inline-attachment"; +import hljs from "highlight.js"; +import "highlight.js/styles/github.css"; +import "src/decidim/editor/clipboard_override" +import "src/decidim/vendor/image-resize.min" +import "src/decidim/vendor/image-upload.min" +import { marked } from "marked"; + +const DecidimAwesome = window.DecidimAwesome || {}; +const quillFormats = ["bold", "italic", "link", "underline", "header", "list", "video", "image", "alt", "break", "width", "style", "code", "blockquote", "indent"]; + +// A tricky way to destroy the quill editor +export function destroyQuillEditor(container) { + if (container) { + const content = $(container).find(".ql-editor").html(); + $(container).html(content); + $(container).siblings(".ql-toolbar").remove(); + $(container).find("*[class*='ql-']").removeClass((index, className) => (className.match(/(^|\s)ql-\S+/g) || []).join(" ")); + $(container).removeClass((index, className) => (className.match(/(^|\s)ql-\S+/g) || []).join(" ")); + if ($(container).next().is("p.help-text")) { + $(container).next().remove(); + } + } + else { + console.error(`editor [${container}] not exists`); + } +} + +export function createQuillEditor(container) { + const toolbar = $(container).data("toolbar"); + const disabled = $(container).data("disabled"); + const allowedEmptyContentSelector = "iframe,img"; + + let quillToolbar = [ + ["bold", "italic", "underline", "linebreak"], + [{ list: "ordered" }, { list: "bullet" }], + ["link", "clean"], + ["code", "blockquote"], + [{ "indent": "-1"}, { "indent": "+1" }] + ]; + + let addImage = false; + + if (toolbar === "full") { + quillToolbar = [ + [{ header: [2, 3, 4, 5, 6, false] }], + ...quillToolbar + ]; + if (DecidimAwesome.allow_images_in_full_editor) { + quillToolbar.push(["video", "image"]); + addImage = true; + } else { + quillToolbar.push(["video"]); + } + } else if (toolbar === "basic") { + if (DecidimAwesome.allow_images_in_small_editor) { + quillToolbar.push(["video", "image"]); + addImage = true; + } else { + quillToolbar.push(["video"]); + } + } else if (DecidimAwesome.allow_images_in_small_editor) { + quillToolbar.push(["image"]); + addImage = true; + } + + let modules = { + linebreak: {}, + toolbar: { + container: quillToolbar, + handlers: { + "linebreak": lineBreakButtonHandler + } + } + }; + + const $input = $(container).siblings('input[type="hidden"]'); + container.innerHTML = $input.val() || ""; + const token = $('meta[name="csrf-token"]').attr("content"); + if (addImage) { + modules.imageResize = { + modules: ["Resize", "DisplaySize"] + } + modules.imageUpload = { + url: DecidimAwesome.editor_uploader_path, + method: "POST", + name: "image", + withCredentials: false, + headers: { "X-CSRF-Token": token }, + callbackOK: (serverResponse, next) => { + $("div.ql-toolbar").last().removeClass("editor-loading") + next(serverResponse.url); + }, + callbackKO: (serverError) => { + $("div.ql-toolbar").last().removeClass("editor-loading") + let msg = serverError && serverError.body; + try { + msg = JSON.parse(msg).message; + } catch (evt) { console.error("Parsing error", evt); } + console.error(`Image upload error: ${msg}`); + let $p = $(`

${msg}

`); + $(container).after($p) + setTimeout(() => { + $p.fadeOut(1000, () => { + $p.destroy(); + }); + }, 3000); + }, + checkBeforeSend: (file, next) => { + $("div.ql-toolbar").last().addClass("editor-loading") + next(file); + } + } + } + const quill = new Quill(container, { + modules: modules, + formats: quillFormats, + theme: "snow" + }); + + if (disabled) { + quill.disable(); + } + + quill.on("text-change", () => { + const text = quill.getText(); + + // Triggers CustomEvent with the cursor position + // It is required in input_mentions.js + let event = new CustomEvent("quill-position", { + detail: quill.getSelection() + }); + container.dispatchEvent(event); + + if ((text === "\n" || text === "\n\n") && quill.root.querySelectorAll(allowedEmptyContentSelector).length === 0 + && !$input.val().match(/img/)) { + $input.val(""); + } else { + const emptyParagraph = "


"; + const cleanHTML = quill.root.innerHTML.replace( + new RegExp(`^${emptyParagraph}|${emptyParagraph}$`, "g"), + "" + ); + $input.val(cleanHTML); + } + }); + // After editor is ready, linebreak_module deletes two extraneous new lines + quill.emitter.emit("editor-ready"); + + if (addImage) { + const text = $(container).data("dragAndDropHelpText") || DecidimAwesome.texts.drag_and_drop_image; + $(container).after(`

${text}

`); + } + + // After editor is ready, linebreak_module deletes two extraneous new lines + quill.emitter.emit("editor-ready"); + + return quill; +} + +export function createMarkdownEditor(container) { + const text = DecidimAwesome.texts.drag_and_drop_image; + const token = $('meta[name="csrf-token"]').attr("content"); + const $input = $(container).siblings('input[type="hidden"]'); + const $faker = $('