diff --git a/app/models/privacy_policy.rb b/app/models/privacy_policy.rb new file mode 100644 index 00000000..8805daa9 --- /dev/null +++ b/app/models/privacy_policy.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Políticas de privacidad +class PrivacyPolicy < ApplicationRecord + extend Mobility + + translates :title, type: :string, locale_accessors: true + translates :description, type: :text, locale_accessors: true + translates :content, type: :text, locale_accessors: true + + validates :title, presence: true, uniqueness: true + validates :description, presence: true + validates :content, presence: true +end diff --git a/app/models/site/config.rb b/app/models/site/config.rb index d2e78d98..fb9175c1 100644 --- a/app/models/site/config.rb +++ b/app/models/site/config.rb @@ -31,7 +31,7 @@ class Site # Escribe los cambios en el repositorio def write - return if persisted? + return true if persisted? @saved = Site::Writer.new(site: site, file: path, content: content.to_yaml).save.tap do |result| # Actualizar el hash para no escribir dos veces diff --git a/app/services/post_service.rb b/app/services/post_service.rb index e448bb4c..81ebe4a1 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -12,6 +12,10 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do post.usuaries << usuarie params[:post][:draft] = true if site.invitade? usuarie + params.require(:post).permit(:slug).tap do |p| + post.slug.value = p[:slug] if p[:slug].present? + end + commit(action: :created, file: update_related_posts) if post.update(post_params) # Devolver el post aunque no se haya salvado para poder rescatar los diff --git a/app/services/site_service.rb b/app/services/site_service.rb index 9d78592f..06375029 100644 --- a/app/services/site_service.rb +++ b/app/services/site_service.rb @@ -23,7 +23,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do site.config.write && commit_config(action: :create) && add_licencias && - add_code_of_conduct + add_code_of_conduct && + add_privacy_policy end site @@ -97,25 +98,27 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do end # Crea la licencia del sitio para cada locale disponible en el sitio + # + # @return [Boolean] def add_licencias - return unless site.layout? :license + return true unless site.layout? :license - site.locales.each do |locale| - next unless I18n.available_locales.include? locale - - Mobility.with_locale(locale) do - add_licencia lang: locale - end - end + with_all_locales do |locale| + add_licencia lang: locale + end.compact.map(&:valid?).all? end + # Crea una licencia + # + # @return [Post] def add_licencia(lang:) params = ActionController::Parameters.new( post: { layout: 'license', + slug: Jekyll::Utils.slugify(I18n.t('activerecord.models.licencia')), lang: lang, title: site.licencia.name, - description: I18n.t('sites.form.licencia.title'), + description: site.licencia.description, content: CommonMarker.render_html(site.licencia.deed) } ) @@ -125,18 +128,22 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do # Encuentra la licencia a partir de su enlace permanente y le cambia # el contenido + # + # @return [Boolean] def change_licencias - site.locales.each do |locale| - next unless I18n.available_locales.include? locale + return true unless site.layout? :license - Mobility.with_locale(locale) do - post = site.posts(lang: locale).find_by(layout: 'license') + with_all_locales do |locale| + post = site.posts(lang: locale).find_by(layout: 'license') - change_licencia(post: post) if post - end - end + change_licencia(post: post) if post + end.compact.map(&:valid?).all? end + # Cambia una licencia + # + # @param :post [Post] + # @return [Post] def change_licencia(post:) params = ActionController::Parameters.new( post: { @@ -149,8 +156,11 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do params: params).update end + # Agrega un código de conducta + # + # @return [Boolean] def add_code_of_conduct - return unless site.layout?(:code_of_conduct) || site.layout?(:page) + return true unless site.layout?(:code_of_conduct) || site.layout?(:page) # TODO: soportar más códigos de conducta coc = CodeOfConduct.first @@ -161,13 +171,36 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do layout: site.layout?(:code_of_conduct) ? 'code_of_conduct' : 'page', lang: locale.to_s, title: coc.title, - description: coc.description + description: coc.description, content: CommonMarker.render_html(coc.content) } ) PostService.new(site: site, usuarie: usuarie, params: params).create - end + end.compact.map(&:valid?).all? + end + + # Agrega política de privacidad + # + # @return [Boolean] + def add_privacy_policy + return true unless site.layout?(:privacy_policy) || site.layout?(:page) + + pp = PrivacyPolicy.first + + with_all_locales do |locale| + params = ActionController::Parameters.new( + post: { + layout: site.layout?(:privacy_policy) ? 'privacy_policy' : 'page', + lang: locale.to_s, + title: pp.title, + description: pp.description, + content: CommonMarker.render_html(pp.content) + } + ) + + PostService.new(site: site, usuarie: usuarie, params: params).create + end.compact.map(&:valid?).all? end # Crea los deploys necesarios para sincronizar a otros nodos de Sutty @@ -188,4 +221,16 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do end end end + + private + + def with_all_locales(&block) + site.locales.map do |locale| + next unless I18n.available_locales.include? locale + + Mobility.with_locale(locale) do + yield locale + end + end + end end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 46cb9d78..6002ee65 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -15,6 +15,8 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.singular 'rollups', 'rollup' inflect.plural 'code_of_conduct', 'codes_of_conduct' inflect.singular 'codes_of_conduct', 'code_of_conduct' + inflect.plural 'privacy_policy', 'privacy_policies' + inflect.singular 'privacy_policies', 'privacy_policy' end ActiveSupport::Inflector.inflections(:es) do |inflect| @@ -32,4 +34,6 @@ ActiveSupport::Inflector.inflections(:es) do |inflect| inflect.singular 'rollups', 'rollup' inflect.plural 'code_of_conduct', 'codes_of_conduct' inflect.singular 'codes_of_conduct', 'code_of_conduct' + inflect.plural 'privacy_policy', 'privacy_policies' + inflect.singular 'privacy_policies', 'privacy_policy' end diff --git a/db/migrate/20230322214924_add_code_of_conduct.rb b/db/migrate/20230322214924_add_code_of_conduct.rb index 0558d69f..f859b08c 100644 --- a/db/migrate/20230322214924_add_code_of_conduct.rb +++ b/db/migrate/20230322214924_add_code_of_conduct.rb @@ -12,7 +12,7 @@ class AddCodeOfConduct < ActiveRecord::Migration[6.1] # XXX: En lugar de ponerlo en las seeds YAML.safe_load(File.read('db/seeds/codes_of_conduct.yml')).each do |coc| - CodigoDeConducta.new(**coc).save! + CodeOfConduct.new(**coc).save! end end diff --git a/db/migrate/20230322231344_add_privacy_policy.rb b/db/migrate/20230322231344_add_privacy_policy.rb new file mode 100644 index 00000000..e0d7ae59 --- /dev/null +++ b/db/migrate/20230322231344_add_privacy_policy.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Agrega políticas de privacidad +class AddPrivacyPolicy < ActiveRecord::Migration[6.1] + def up + create_table :privacy_policies do |t| + t.timestamps + t.string :title + t.text :description + t.text :content + end + + # XXX: En lugar de ponerlo en las seeds + YAML.safe_load(File.read('db/seeds/privacy_policies.yml')).each do |pp| + PrivacyPolicy.new(**pp).save! + end + end + + def down + drop_table :privacy_policies + end +end diff --git a/db/seeds/codes_of_conduct.yml b/db/seeds/codes_of_conduct.yml index cde7fbfe..64582072 100644 --- a/db/seeds/codes_of_conduct.yml +++ b/db/seeds/codes_of_conduct.yml @@ -1,6 +1,6 @@ --- -- name_en: "Codes for sharing" - name_es: "Códigos para compartir" +- title_en: "Codes for sharing" + title_es: "Códigos para compartir" description_en: "Codes of conduct allow inclusive communities." description_es: "Los códigos de convivencia nos permiten alojar comunidades inclusivas." content_en: | diff --git a/db/seeds/privacy_policies.yml b/db/seeds/privacy_policies.yml new file mode 100644 index 00000000..98ce8379 --- /dev/null +++ b/db/seeds/privacy_policies.yml @@ -0,0 +1,113 @@ +--- +- title_en: "Privacy Policy" + title_es: "Políticas de privacidad" + description_en: "With what care does this site handles personal data of its users and visitors?" + description_es: "¿Cuáles son los cuidados de este sitio con respecto a sus usuaries y visitantes?" + content_en: | + > We use "them" as neutral pronoun to refer to people regardless of + > gender identity. + + This document details Sutty's privacy policy, including web site, + platform, other infrastructure (support channels, etc.) and web sites + generated by users. + + ## This is too long! + + * Sutty doesn't collect any kind of personal data. + + * Sutty may only collect statistical data that doesn't identify + individuals. + + ## Analytic data + + Sutty may only collect data for analytics (number of visits, duration, + etc.), not associated to personal data. + + Analytical data collected for every web site can only be used internally + by Sutty. Sutty doesn't share any data privately with any third + parties. Selected analytical data could be used publicly. + + Sutty doesn't recommend personal data collection in any way, but it + doesn't monitor if its users use third party services with their own + privacy policies. We recommend users and visitors to inform themselves + before using third parties analytics services. + + ## No personal data collection + + Sutty doesn't collect IP addresses from users nor visitors in any way. + + Sutty doesn't ask for personal data for registering user accounts in its + platform. + + Sutty only uses session "cookies" to identify users during their use of + the platform. It doesn't use "cookies" to identify visitors of web + sites hosted by Sutty. + + The only exception where Sutty could collect personal data is during + service payment. Digital safety measures will be taken to keep this + information and to discard it if possible after needed. + + Users will be notified when their personal data is removed. + + If users decide to host their web sites with third parties, they must + inform themselves about the corresponding privacy policies. Sutty only + recommends third parties with privacy policies compatible with these. + content_es: | + > Utilizamos la e como pronombre neutro para referirnos a personas + > independientemente de su identidad de género, por ejemplo “usuarie”. + + Este documento detalla la política de privacidad de Sutty, incluyendo + sitio web, plataforma de edición, infraestructura relacionada (salas de + chat, etc.) y sitios creados por sus usuaries a través de la plataforma, + en adelante "Sutty". + + ## ¡Esto es demasiado largo! + + Un resumen: + + * Sutty no recolecta datos personales de ningún tipo + + * Sutty solo recolectaría datos analíticos que no identifican a + personas + + ## Datos analíticos + + La única recolección de datos realizada por Sutty es con fines + analíticos (cantidad de visitas, duración, etc.), no asociados a datos + personales. + + Los datos analíticos recolectados por cada sitio podrán ser utilizados + internamente por Sutty. Sutty no comparte datos analíticos con + terceros en forma privada. Datos analíticos seleccionados podrán ser + utilizados públicamente. + + Sutty no recomienda la recolección de datos personales de ninguna forma, + pero no monitorea que les usuaries utilicen servicios de terceros con + sus propias políticas de privacidad. Recomendamos a les usuaries y + visitantes informarse antes de utilizar servicios de estadísticas de + terceros. + + ## No registro de datos personales + + Sutty no registra direcciones IP de usuaries ni de visitantes de ninguna + forma. + + Sutty no solicita datos personales para el registro de cuentas de + usuarie en su plataforma. + + Sutty solo utiliza “cookies” de sesión para identificar usuaries + mientras utilicen la plataforma. No se utilizan “cookies” para + identificar visitantes a los sitios alojados por Sutty. + + El único caso en el que Sutty podría solicitar datos personales es + durante el pago de servicios. Se tomarán medidas de seguridad digital + para salvaguardar esta información y descartar lo que sea posible una + vez que ya no sea necesaria. + + Se notificará a les usuaries cuando su información personal sea + eliminada. + + Si les usuaries deciden alojar sus sitios con terceros, deberán + informarse de las políticas de privacidad correspondientes. Sutty + recomienda servicios de terceros con políticas de privacidad coherentes + con estas.