Merge branch 'crear_sitios' into rails

This commit is contained in:
f 2019-08-02 18:01:26 -03:00
commit 42bf7e173b
No known key found for this signature in database
GPG key ID: 2AE5A13E321F953D
128 changed files with 3303 additions and 580 deletions

View file

@ -1,4 +1,7 @@
SECRET_KEY_BASE=
RAILS_ENV=production
IMAP_SERVER=
DEFAULT_FROM=
DEVISE_PEPPER=
SKEL_SUTTY=https://0xacab.org/sutty/skel.sutty.nl
SUTTY=sutty.nl
REDIS_SERVER=
REDIS_CLIENT=

8
.gitignore vendored
View file

@ -24,6 +24,10 @@
/_sites/*
/_deploy/*
/_usuarias/*
/_invitadxs/*
/data/*
.env
# Ignore master key for decrypting credentials and more.
/config/master.key
/config/credentials.yml.enc

View file

@ -36,18 +36,17 @@ Metrics/MethodLength:
Metrics/BlockLength:
Exclude:
- 'config/environments/development.rb'
- 'config/environments/production.rb'
- 'config/initializers/devise.rb'
- 'db/schema.rb'
- 'config/routes.rb'
Metrics/ClassLength:
Exclude:
- 'app/models/site.rb'
- 'app/controllers/posts_controller.rb'
Performance/TimesMap:
Exclude:
- 'app/models/site.rb'
- 'app/controllers/sites_controller.rb'
Lint/HandleExceptions:
Exclude:

1
.ruby-version Normal file
View file

@ -0,0 +1 @@
2.5.5

View file

@ -11,7 +11,6 @@ require 'capistrano/passenger'
require 'capistrano/bundler'
require 'capistrano/rbenv'
require 'capistrano/rails'
require 'whenever/capistrano'
require 'capistrano/scm/git'
install_plugin Capistrano::SCM::Git

116
Dockerfile Normal file
View file

@ -0,0 +1,116 @@
# Este Dockerfile está armado pensando en una compilación lanzada desde
# el mismo repositorio de trabajo. Cuando tengamos CI/CD algunas cosas
# como el tarball van a tener que cambiar porque ya vamos a haber hecho
# un clone/pull limpio.
FROM sutty/sdk-ruby:latest as build
MAINTAINER "f <f@sutty.nl>"
# Un entorno base
ENV NOKOGIRI_USE_SYSTEM_LIBRARIES=1
ENV SECRET_KEY_BASE solo_es_necesaria_para_correr_rake
ENV RAILS_ENV production
# Para compilar los assets en brotli
RUN apk add --no-cache brotli libgit2-dev rsync cmake
# Empezamos con la usuaria app creada por sdk-ruby
USER app
# Vamos a trabajar dentro de este directorio
WORKDIR /home/app/sutty
# Copiamos solo el Gemfile para poder instalar las gemas necesarias
COPY --chown=app:www-data ./Gemfile .
COPY --chown=app:www-data ./Gemfile.lock .
# XXX: Esto va a tener permisos de 1000, idealmente el usuario que lanza
# la compilación
RUN rsync -a 172.17.0.1::ccache/ /home/app/.ccache/
# Instalar las gemas de producción usando ccache para no recompilar
# gemas nativas
# XXX: No usamos la flag --production porque luego no nos deja
# desinstalar las gemas de los assets
# RUN --mount=type=cache,target=/home/app/.ccache \
RUN if ! bundle install --path=./vendor --without test development ; then rsync -a /home/app/.ccache/ 172.17.0.1::ccache/ ; exit 1 ; fi
RUN rsync -a /home/app/.ccache/ 172.17.0.1::ccache/
# Vaciar la caché
RUN rm vendor/ruby/2.5.0/cache/*.gem
# Limpiar las librerías nativas, esto ahorra más espacio y uso de
# memoria ya que no hay que cargar símbolos que no se van a usar.
RUN find vendor -name "*.so" | xargs -rn 1 strip --strip-unneeded
# Copiar el repositorio git
COPY --chown=app:www-data ./.git/ ./.git/
# Hacer un tarball de los archivos desde el repositorio
RUN git archive -o ../sutty.tar.gz HEAD
# Extraer archivos necesarios para compilar los assets
RUN tar xf ../sutty.tar.gz Rakefile config app yarn.lock package.json
# Instalar los paquetes JS
RUN yarn
# Pre-compilar los assets
RUN bundle exec rake assets:precompile
# Comprimirlos usando brotli
RUN find public/assets -type f | grep -v ".gz$" | xargs -r brotli -k -9
# Eliminar la necesidad de un runtime JS en producción, porque los
# assets ya están pre-compilados.
RUN sed -re "/(uglifier|bootstrap|coffee-rails)/d" -i Gemfile
RUN bundle clean
# Contenedor final
FROM sutty/monit:latest
ENV RAILS_ENV production
# Instalar las dependencias, separamos la librería de base de datos para
# poder reutilizar este primer paso desde otros contenedores
RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-bundler ruby-json ruby-bigdecimal ruby-rake
RUN apk add --no-cache sqlite-libs
# Necesitamos yarn para que Jekyll pueda generar los sitios
# XXX: Eliminarlo cuando extraigamos la generación de sitios del proceso
# principal
RUN apk add --no-cache yarn
RUN apk add --no-cache libgit2
# Instalar foreman para poder correr los servicios
RUN gem install --no-document --no-user-install foreman
# Agregar el grupo del servidor web
RUN addgroup -g 82 -S www-data
# Agregar la usuaria
RUN adduser -s /bin/sh -G www-data -h /srv/http -D app
# Convertirse en app para instalar
USER app
WORKDIR /srv/http
# Traer los archivos y colocarlos donde van definitivamente
COPY --from=build --chown=app:www-data /home/app/sutty.tar.gz /tmp/
# XXX: No vale la pena borrarlo porque sigue ocupando espacio en la capa
# anterior
RUN tar xf /tmp/sutty.tar.gz && rm /tmp/sutty.tar.gz
# Traer los assets compilados y las gemas
COPY --from=build --chown=app:www-data /home/app/sutty/public/assets public/assets
COPY --from=build --chown=app:www-data /home/app/sutty/vendor vendor
COPY --from=build --chown=app:www-data /home/app/sutty/.bundle .bundle
COPY --from=build --chown=app:www-data /home/app/sutty/Gemfile Gemfile
COPY --from=build --chown=app:www-data /home/app/sutty/Gemfile.lock Gemfile.lock
COPY ./config/credentials.yml.enc ./config/credentials.yml.enc
# Volver a root para cerrar la compilación
USER root
# Sincronizar los assets a un directorio compartido
RUN apk add --no-cache rsync
COPY ./sync_assets.sh /usr/local/bin/sync_assets
RUN chmod 755 /usr/local/bin/sync_assets
# Instalar la configuración de monit y comprobarla
RUN install -m 640 -o root -g root ./monit.conf /etc/monit.d/sutty.conf
RUN monit -t
# Mantener estos directorios!
VOLUME "/srv/http/_deploy"
VOLUME "/srv/http/_sites"
VOLUME "/srv/http/_public"
# El puerto de puma
EXPOSE 3000

20
Gemfile
View file

@ -46,14 +46,22 @@ gem 'email_address'
gem 'exception_notification'
gem 'font-awesome-rails'
gem 'friendly_id'
gem 'haml-rails'
gem 'hamlit-rails'
gem 'hiredis'
gem 'jekyll'
gem 'jquery-rails'
gem 'mini_magick'
gem 'mobility'
gem 'pundit'
gem 'rails-i18n'
gem 'rails_warden'
gem 'whenever', require: false
gem 'redis', require: %w[redis redis/connection/hiredis]
gem 'redis-rails'
gem 'rubyzip'
gem 'rugged'
gem 'sidekiq'
gem 'terminal-table'
gem 'validates_hostname'
group :development, :test do
gem 'pry'
@ -76,9 +84,15 @@ group :development do
gem 'capistrano-rails'
gem 'capistrano-rbenv'
gem 'ed25519'
gem 'haml-lint', require: false
gem 'letter_opener'
gem 'rbnacl', '< 5.0'
gem 'rubocop'
gem 'rubocop-rails'
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
end
group :test do
gem 'database_cleaner'
gem 'factory_bot_rails'
end

View file

@ -91,13 +91,14 @@ GEM
carrierwave-i18n (0.2.0)
childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
coderay (1.1.2)
colorator (1.1.0)
commonmarker (0.18.2)
ruby-enum (~> 0.5)
concurrent-ruby (1.1.5)
connection_pool (2.2.2)
crass (1.0.4)
database_cleaner (1.7.0)
devise (4.6.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@ -121,12 +122,16 @@ GEM
netaddr (~> 2.0)
simpleidn
erubi (1.8.0)
erubis (2.7.0)
eventmachine (1.2.7)
exception_notification (4.3.0)
actionmailer (>= 4.0, < 6)
activesupport (>= 4.0, < 6)
execjs (2.7.0)
factory_bot (5.0.2)
activesupport (>= 4.2.0)
factory_bot_rails (5.0.2)
factory_bot (~> 5.0.2)
railties (>= 4.2.0)
fastimage (2.1.5)
ffi (1.11.1)
font-awesome-rails (4.7.0.4)
@ -139,21 +144,28 @@ GEM
haml (5.0.4)
temple (>= 0.8.0)
tilt
haml-rails (1.0.0)
haml-lint (0.999.999)
haml_lint
haml_lint (0.32.0)
haml (>= 4.0, < 5.2)
rainbow
rake (>= 10, < 13)
rubocop (>= 0.50.0)
sysexits (~> 1.1)
hamlit (2.9.3)
temple (>= 0.8.0)
thor
tilt
hamlit-rails (0.2.3)
actionpack (>= 4.0.1)
activesupport (>= 4.0.1)
haml (>= 4.0.6, < 6.0)
html2haml (>= 1.0.1)
hamlit (>= 1.2.0)
railties (>= 4.0.1)
html2haml (2.2.0)
erubis (~> 2.7.0)
haml (>= 4.0, < 6)
nokogiri (>= 1.6.0)
ruby_parser (~> 3.5)
hiredis (0.6.3)
http_parser.rb (0.6.0)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.2)
jaro_winkler (1.5.3)
jbuilder (2.8.0)
activesupport (>= 4.2.0)
multi_json (>= 1.2)
@ -201,10 +213,13 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
mimemagic (0.3.3)
mini_magick (4.9.3)
mini_magick (4.9.4)
mini_mime (1.0.1)
mini_portile2 (2.4.0)
minitest (5.11.3)
mobility (0.8.7)
i18n (>= 0.6.10, < 2)
request_store (~> 1.0)
multi_json (1.13.1)
net-scp (2.0.0)
net-ssh (>= 2.6.5, < 6.0.0)
@ -214,8 +229,8 @@ GEM
nokogiri (1.10.3)
mini_portile2 (~> 2.4.0)
orm_adapter (0.5.0)
parallel (1.16.0)
parser (2.6.2.0)
parallel (1.17.0)
parser (2.6.3.0)
ast (~> 2.4.0)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
@ -223,12 +238,13 @@ GEM
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
psych (3.1.0)
public_suffix (3.0.3)
puma (3.12.1)
pundit (2.0.1)
activesupport (>= 3.0.0)
rack (2.0.6)
rack-protection (2.0.5)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.3)
@ -267,25 +283,45 @@ GEM
ffi (~> 1.0)
rbnacl (4.0.2)
ffi
redis (4.1.2)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 2)
redis-activesupport (5.0.7)
activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2)
redis-rack (2.0.5)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
redis-store (1.6.0)
redis (>= 2.2, < 5)
request_store (1.4.1)
rack (>= 1.4)
responders (3.0.0)
actionpack (>= 5.0)
railties (>= 5.0)
rouge (3.3.0)
rubocop (0.66.0)
rubocop (0.72.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1)
psych (>= 3.1.0)
parser (>= 2.6)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.6)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-rails (2.2.0)
rack (>= 1.1)
rubocop (>= 0.72.0)
ruby-enum (0.7.2)
i18n
ruby-progressbar (1.10.0)
ruby-progressbar (1.10.1)
ruby_dep (1.5.0)
ruby_parser (3.13.1)
sexp_processor (~> 4.9)
rubyzip (1.2.2)
rugged (0.28.2)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
@ -310,7 +346,11 @@ GEM
selenium-webdriver (3.141.0)
childprocess (~> 0.5)
rubyzip (~> 1.2, >= 1.2.2)
sexp_processor (4.12.0)
sidekiq (5.2.7)
connection_pool (~> 2.2, >= 2.2.2)
rack (>= 1.5.0)
rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5)
simpleidn (0.1.1)
unf (~> 0.1.4)
spring (2.0.2)
@ -329,7 +369,10 @@ GEM
sshkit (1.18.2)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
sysexits (1.2.0)
temple (0.8.1)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thor (0.20.3)
thread_safe (0.3.6)
tilt (2.0.9)
@ -343,7 +386,10 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
unicode-display_width (1.5.0)
unicode-display_width (1.6.0)
validates_hostname (1.0.8)
activerecord (>= 3.0)
activesupport (>= 3.0)
warden (1.2.8)
rack (>= 2.0.6)
web-console (3.7.0)
@ -354,8 +400,6 @@ GEM
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
whenever (0.10.0)
chronic (>= 0.6.3)
xpath (3.2.0)
nokogiri (~> 1.8)
@ -376,6 +420,7 @@ DEPENDENCIES
carrierwave-bombshelter
carrierwave-i18n
commonmarker
database_cleaner
devise
devise-i18n
devise_invitable
@ -383,15 +428,19 @@ DEPENDENCIES
ed25519
email_address
exception_notification
factory_bot_rails
font-awesome-rails
friendly_id
haml-rails
haml-lint
hamlit-rails
hiredis
jbuilder (~> 2.5)
jekyll
jquery-rails
letter_opener
listen (>= 3.0.5, < 3.2)
mini_magick
mobility
pry
puma (~> 3.7)
pundit
@ -399,16 +448,22 @@ DEPENDENCIES
rails-i18n
rails_warden
rbnacl (< 5.0)
rubocop
redis
redis-rails
rubocop-rails
rubyzip
rugged
sass-rails (~> 5.0)
selenium-webdriver
sidekiq
spring
spring-watcher-listen (~> 2.0.0)
sqlite3 (~> 1.3.6)
terminal-table
turbolinks (~> 5)
uglifier (>= 1.3.0)
validates_hostname
web-console (>= 3.3.0)
whenever
BUNDLED WITH
1.17.3
2.0.2

3
Procfile Normal file
View file

@ -0,0 +1,3 @@
migrate: bundle exec rake db:migrate db:seed
sutty: bundle exec puma -d config.ru
sidekiq: bundle exec sidekiq -t 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

BIN
app/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,5 @@
$(document).on('turbolinks:load', function() {
$("a[href^='http://']").attr('target', '_blank');
$("a[href^='https://']").attr('target', '_blank');
$("a[href^='//']").attr('target', '_blank');
});

View file

@ -5,6 +5,37 @@
@import "select2-theme-bootstrap4/dist/select2-bootstrap";
@import "dragula-with-animation/dist/dragula";
@font-face {
font-family: 'Saira';
font-style: normal;
font-weight: 500;
font-display: optional;
src: local('Saira Medium'), local('Saira-Medium'),
font-url('saira/v3/SairaMedium.ttf') format('truetype');
}
@font-face {
font-family: 'Saira';
font-style: normal;
font-weight: 700;
font-display: optional;
src: local('Saira Bold'), local('Saira-Bold'),
font-url('saira/v3/SairaBold.ttf') format('truetype');
}
body {
font-family: Saira, sans-serif;
}
a {
&[target=_blank] {
/* TODO: Convertir a base64 para no hacer peticiones extra */
&:after {
content: image-url('icon_external_link.png');
}
}
}
$footer-height: 60px;
/* Colores */
@ -55,7 +86,6 @@ ol.breadcrumb {
.background-cover {
background: image-url("background.jpg") no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;

View file

@ -5,8 +5,12 @@ class ApplicationController < ActionController::Base
include ExceptionHandler
protect_from_forgery with: :exception
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :set_locale
layout :layout_by_usuarie
# No tenemos índice de sutty, vamos directamente a ver el listado de
# sitios
def index
@ -17,6 +21,14 @@ class ApplicationController < ActionController::Base
private
def layout_by_usuarie
if current_usuarie
'application'
else
'devise'
end
end
# Encontrar un sitio por su nombre
def find_site
id = params[:site_id] || params[:id]
@ -50,6 +62,12 @@ class ApplicationController < ActionController::Base
end
def set_locale
I18n.locale = session[:lang] if session[:lang].present?
I18n.locale = current_usuarie.lang
end
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:account_update, keys: %i[lang])
end
end

View file

@ -56,7 +56,7 @@ class PostsController < ApplicationController
# Las usuarias pueden especificar una autora, de la contrario por
# defecto es la usuaria actual
if current_user.is_a? Usuaria
if @site.usuarie? current_usuarie
@post.update_attributes(author: params[:post][:author])
else
# Todo lo que crean lxs invitadxs es borrador
@ -99,7 +99,7 @@ class PostsController < ApplicationController
@post.update_attributes(repair_nested_params(post_params))
# Solo las usuarias pueden modificar la autoría
if current_user.is_a? Usuaria
if @site.usuarie? current_usuarie
if params[:post][:author].present?
@post.update_attributes(author: params[:post][:author])
end

View file

@ -14,12 +14,54 @@ class SitesController < ApplicationController
# No tenemos propiedades de un sitio aún, así que vamos al listado de
# artículos
def show
authorize Site
site = find_site
authorize site
redirect_to site_posts_path(site)
end
def new
@site = Site.new
authorize @site
@site.deploys.build type: 'DeployLocal'
@site.deploys.build type: 'DeployZip'
end
def create
@site = Site.new(site_params)
@site.roles << Rol.new(site: @site,
usuarie: current_usuarie,
temporal: false,
rol: 'usuarie')
# XXX: Necesitamos escribir la configuración después porque queremos
# registrar quién hizo los cambios en el repositorio
if @site.save && @site.config.write(current_usuarie)
redirect_to site_path(@site)
else
render 'new'
end
end
def edit
@site = find_site
authorize @site
end
def update
@site = find_site
authorize @site
# XXX: Necesitamos escribir la configuración después porque queremos
# registrar quién hizo los cambios en el repositorio
if @site.update(site_params) && @site.config.write(current_usuarie)
redirect_to sites_path
else
render 'edit'
end
end
# Envía un archivo del directorio público de Jekyll
def send_public_file
authorize Site
@ -42,23 +84,15 @@ class SitesController < ApplicationController
end
def enqueue
@site = find_site
authorize @site
@site.enqueue!
site = find_site
authorize site
# XXX: Convertir en una máquina de estados?
DeployWorker.perform_async site.id if site.enqueue!
redirect_to sites_path
end
def build_log
@site = find_site
authorize @site
# TODO: eliminar ANSI
render file: @site.build_log,
layout: false,
content_type: 'text/plain; charset=utf-8'
end
def reorder_posts
@site = find_site
authorize @site
@ -79,4 +113,32 @@ class SitesController < ApplicationController
redirect_to site_posts_path @site
end
def fetch
@site = find_site
authorize @site
@commits = @site.repository.commits
end
def merge
@site = find_site
authorize @site
if @site.repository.merge(current_usuarie)
flash[:success] = I18n.t('sites.fetch.merge.success')
else
flash[:error] = I18n.t('sites.fetch.merge.error')
end
redirect_to sites_path
end
private
def site_params
params.require(:site)
.permit(:name, :design_id, :licencia_id, :description, :title,
deploys_attributes: %i[type id _destroy])
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
# Estadísticas del sitio
class StatsController < ApplicationController
include Pundit
before_action :authenticate_usuarie!
def index
@site = find_site
authorize SiteStat.new(@site)
# Solo queremos el promedio de tiempo de compilación, no de
# instalación de dependencias.
stats = @site.build_stats.jekyll
@build_avg = stats.average(:seconds).to_f.round(2)
@build_max = stats.maximum(:seconds).to_f.round(2)
end
end

View file

@ -1,5 +1,6 @@
# frozen_string_literal: true
# Helpers
module ApplicationHelper
# Devuelve el atributo name de un campo posiblemente anidado
def field_name_for_post(names)
@ -21,7 +22,31 @@ module ApplicationHelper
"#{f.first}[#{f.last}]"
end
def distance_of_time_in_words_if_more_than_a_minute(seconds)
if seconds > 60
distance_of_time_in_words seconds
else
I18n.t('seconds', seconds: seconds)
end
end
def sanitize_markdown(text, options = {})
sanitize(CommonMarker.render_html(text), options)
end
def invalid?(model, field)
model.errors.messages[field].present?
end
def form_control(model, field)
if invalid? model, field
'form-control is-invalid'
else
'form-control'
end
end
def form_class(model)
model.errors.messages.empty? ? 'needs-validation' : 'was-validated'
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module CoreExtensions
module String
# Elimina el HTML
module StripTags
def strip_tags
ActionController::Base.helpers.strip_tags(self)
end
end
end
end

View file

@ -1,6 +1,17 @@
# frozen_string_literal: true
# Configuración base del correo
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
helper :application
before_action :inline_logo!
default from: ENV.fetch('DEFAULT_FROM', "noreply@#{Site.domain}")
layout 'mailer'
private
def inline_logo!
attachments.inline['logo.png'] ||=
File.read('app/assets/images/logo.png')
end
end

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
# Notifica a les usuaries cuando un sitio se generó con éxito
#
# XXX: No será mejor enviarles un correo con copia?
# TODO: Agregar headers de desuscripción de notificaciones cuando
# tengamos opciones de usuarie
# TODO: Agregar firma GPG y header Autocrypt
# TODO: Cifrar con GPG si le usuarie nos dio su llave
class DeployMailer < ApplicationMailer
# rubocop:disable Metrics/AbcSize
def deployed(which_ones)
@usuarie = Usuarie.find(params[:usuarie])
@site = @usuarie.sites.find(params[:site])
@deploys = which_ones
@deploy_local = @site.deploys.find_by(type: 'DeployLocal')
# Informamos a cada quien en su idioma y damos una dirección de
# respuesta porque a veces les usuaries nos escriben
I18n.with_locale(@usuarie.lang) do
mail(to: @usuarie.email,
reply_to: "sutty@#{Site.domain}",
subject: I18n.t('mailers.deploy_mailer.deployed.subject',
site: @site.name))
end
end
# rubocop:enable Metrics/AbcSize
end

8
app/models/build_stat.rb Normal file
View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# Recolecta estadísticas durante la generación del sitio
class BuildStat < ApplicationRecord
belongs_to :deploy
scope :jekyll, -> { where(action: 'bundle_exec_jekyll_build') }
end

65
app/models/deploy.rb Normal file
View file

@ -0,0 +1,65 @@
# frozen_string_literal: true
require 'open3'
# Este modelo implementa los distintos tipos de alojamiento que provee
# Sutty.
#
# Los datos se guardan en la tabla `deploys`. Para guardar los
# atributos, cada modelo tiene que definir su propio `store
# :attributes`.
class Deploy < ApplicationRecord
belongs_to :site
has_many :build_stats
def deploy
raise NotImplementedError
end
def limit
raise NotImplementedError
end
def size
raise NotImplementedError
end
def time_start
@start = Time.now
end
def time_stop
@stop = Time.now
end
def time_spent_in_seconds
(@stop - @start).round(3)
end
# Corre un comando y devuelve true si terminó correctamente
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
def run(cmd)
# XXX: prestar atención a la concurrencia de sqlite3, se podría
# enviar los datos directamente a una API para que se manejen desde
# el proceso principal de rails y evitar problemas.
stat = build_stats.build action: cmd.split(' -', 2).first.tr(' ', '_')
r = nil
time_start
Dir.chdir(site.path) do
Open3.popen2e(env, cmd, unsetenv_others: true) do |_, o, t|
r = t.value
stat.log = o.read
end
end
time_stop
stat.seconds = time_spent_in_seconds
stat.bytes = size
stat.save
r.try :exited?
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize
end

View file

@ -0,0 +1,88 @@
# frozen_string_literal: true
# Alojamiento local, solo genera el sitio, con lo que no necesita hacer
# nada más
class DeployLocal < Deploy
store :values, accessors: %i[fqdn destination], coder: JSON
before_create :fqdn!, :destination!
before_destroy :remove_destination!
# Realizamos la construcción del sitio usando Jekyll y un entorno
# limpio para no pasarle secretos
#
# Pasamos variables de entorno mínimas para no filtrar secretos de
# Sutty
#
# TODO: Recolectar estadísticas y enviarlas a la base de datos
def deploy
yarn && bundle && jekyll_build
end
# Sólo permitimos un deploy local
def limit
1
end
# Obtener el tamaño de todos los archivos y directorios (los
# directorios son archivos :)
def size
paths = [destination, File.join(destination, '**', '**')]
Dir.glob(paths).map do |file|
File.size file
end.inject(:+)
end
private
# Un entorno que solo tiene lo que necesitamos
def env
# XXX: This doesn't support Windows paths :B
paths = [File.dirname(`which bundle`), '/usr/bin']
{ 'PATH' => paths.join(':'), 'JEKYLL_ENV' => 'production' }
end
def yarn_lock
File.join(site.path, 'yarn.lock')
end
def yarn_lock?
File.exist? yarn_lock
end
# Corre yarn dentro del repositorio
def yarn
return unless yarn_lock?
run 'yarn'
end
def bundle
run 'bundle'
end
def jekyll_build
run "bundle exec jekyll build --destination \"#{escaped_destination}\""
end
def fqdn!
self.fqdn ||= "#{site.name}.#{Site.domain}"
end
def destination!
self.destination ||= File.join(Rails.root, '_deploy', fqdn)
end
# no debería haber espacios ni caracteres especiales, pero por si
# acaso...
def escaped_destination
Shellwords.escape destination
end
# Eliminar el destino si se elimina el deploy
def remove_destination!
FileUtils.rm_rf destination
end
end

64
app/models/deploy_zip.rb Normal file
View file

@ -0,0 +1,64 @@
# frozen_string_literal: true
# Genera un ZIP a partir del sitio ya construido
#
# TODO: Firmar con minisign
class DeployZip < Deploy
store :values, accessors: %i[fqdn destination file path], coder: JSON
before_create :fqdn!, :destination!
before_create :file!, :path!
# Una vez que el sitio está generado, tomar todos los archivos y
# y generar un zip accesible públicamente.
#
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
def deploy
time_start
Dir.chdir(destination) do
Zip::File.open(path, Zip::File::CREATE) do |z|
Dir.glob('./**/**').each do |f|
File.directory?(f) ? z.mkdir(f) : z.add(f, f)
end
end
end
time_stop
build_stats.create action: 'zip',
seconds: time_spent_in_seconds,
bytes: size
File.exist? path
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize
def limit
1
end
def size
File.size path
end
private
# Copiamos de DeployLocal para no cargar todos los métodos de
# compilación...
def fqdn!
self.fqdn ||= "#{site.name}.#{Site.domain}"
end
def destination!
self.destination ||= File.join(Rails.root, '_deploy', fqdn)
end
def file!
self.file ||= "#{fqdn}.zip"
end
def path!
self.path = File.join(destination, file)
end
end

19
app/models/design.rb Normal file
View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
# El diseño de un sitio es la plantilla/tema. En este modelo cargamos
# las propiedades para poder verlas desde el panel y elegir un diseño
# para el sitio.
#
# TODO: Agregar captura de pantalla con ActiveStorage
class Design < ApplicationRecord
extend Mobility
translates :name, type: :string, locale_accessors: true
translates :description, type: :text, locale_accessors: true
has_many :sites
validates :name, presence: true, uniqueness: true
validates :gem, presence: true, uniqueness: true
validates :description, presence: true
end

18
app/models/licencia.rb Normal file
View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
# Las licencias son completamente traducibles
class Licencia < ApplicationRecord
extend Mobility
translates :name, type: :string, locale_accessors: true
translates :url, type: :string, locale_accessors: true
translates :description, type: :text, locale_accessors: true
translates :deed, type: :text, locale_accessors: true
has_many :sites
validates :name, presence: true, uniqueness: true
validates :url, presence: true
validates :description, presence: true
validates :deed, presence: true
end

View file

@ -5,19 +5,64 @@
class Site < ApplicationRecord
include FriendlyId
validates :name, uniqueness: true, hostname: true
validates :design_id, presence: true
validate :deploy_local_presence
validates_inclusion_of :status, in: %w[waiting enqueued building]
validates_presence_of :title
validates :description, length: { in: 50..160 }
friendly_id :name, use: %i[finders]
belongs_to :design
belongs_to :licencia
has_many :deploys
has_many :build_stats, through: :deploys
has_many :roles
has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') },
through: :roles
has_many :invitades, -> { where('roles.rol = ?', 'invitade') },
through: :roles, source: :usuarie
# Carga el sitio Jekyll una vez que se inicializa el modelo
# Clonar el directorio de esqueleto antes de crear el sitio
before_create :clone_skel!
# Elimina el directorio al destruir un sitio
before_destroy :remove_directories!
# Carga el sitio Jekyll una vez que se inicializa el modelo o después
# de crearlo
after_initialize :load_jekyll!
after_create :load_jekyll!
# Cambiar el nombre del directorio
before_update :update_name!
# Guardar la configuración si hubo cambios
after_save :sync_attributes_with_config!
attr_accessor :jekyll, :collections
accepts_nested_attributes_for :deploys, allow_destroy: true
# No permitir HTML en estos atributos
def title=(title)
super(title.strip_tags)
end
def description=(description)
super(description.strip_tags)
end
# El repositorio git para este sitio
def repository
@repository ||= Site::Repository.new path
end
# Trae los cambios del skel y verifica que haya cambios
def needs_pull?
!repository.commits.empty?
end
# TODO: Mover esta consulta a la base de datos para no traer un montón
# de cosas a la memoria
def invitade?(usuarie)
invitades.pluck(:id).include? usuarie.id
end
@ -30,12 +75,16 @@ class Site < ApplicationRecord
#
# Equivale a _sites + nombre
def path
@path ||= File.join(Site.site_path, name)
File.join(Site.site_path, name)
end
def old_path
File.join(Site.site_path, name_was)
end
# Este sitio acepta invitadxs?
def invitadxs?
jekyll.config.fetch('invitadxs', false)
config.fetch('invitadxs', false)
end
def cover
@ -49,12 +98,12 @@ class Site < ApplicationRecord
# Define si el sitio tiene un glosario
def glossary?
jekyll.config.fetch('glossary', false)
config.fetch('glossary', false)
end
# Obtiene la lista de traducciones actuales
def translations
@jekyll.config.dig('i18n') || []
config.fetch('i18n', [])
end
# Devuelve el idioma por defecto del sitio
@ -111,14 +160,10 @@ class Site < ApplicationRecord
end
def config
if @jekyll.config.empty?
read
Rails.logger.info 'Leyendo config'
end
@jekyll.config
@config ||= Site::Config.new(self)
end
# TODO: Cambiar a Site::Config apenas empecemos a testear esto
def collections_names
@jekyll.config['collections'].keys
end
@ -182,65 +227,13 @@ class Site < ApplicationRecord
end.flatten.uniq.compact
end
def failed_file
File.join(path, '.failed')
end
def failed?
File.exist? failed_file
end
def defail
FileUtils.rm failed_file if failed?
end
alias defail! defail
def build_log
File.join(path, 'build.log')
end
def build_log?
File.exist? build_log
end
def queue_file
File.join(path, '.generate')
def enqueue!
!enqueued? && update_attribute(:status, 'enqueued')
end
def enqueued?
File.exist? queue_file
status == 'enqueued'
end
alias queued? enqueued?
# El sitio se genera cuando se coloca en una cola de generación, para
# que luego lo construya un cronjob
def enqueue
defail!
# TODO: ya van tres métodos donde usamos esta idea, convertir en un
# helper o algo
File.open(queue_file, File::RDWR | File::CREAT, 0o640) do |f|
# Bloquear el archivo para que no sea accedido por otro
# proceso u otra editora
f.flock(File::LOCK_EX)
# Empezar por el principio
f.rewind
# Escribir la fecha de creación
f.write(Time.now.to_i.to_s)
# Eliminar el resto
f.flush
f.truncate(f.pos)
end
end
alias enqueue! enqueue
# Eliminar de la cola
def dequeue
FileUtils.rm(queue_file) if enqueued?
end
alias dequeue! dequeue
# Verifica si los posts están ordenados
def ordered?(collection = 'posts')
@ -287,31 +280,14 @@ class Site < ApplicationRecord
File.join('/', 'sites', id, path.gsub('..', ''))
end
def get_url_from_site(path)
"https://#{name}#{path}"
end
# El directorio donde se almacenan los sitios
def self.site_path
File.join(Rails.root, '_sites')
end
# El directorio de los sitios de una usuaria
#
# Los sitios se organizan por usuaria, entonces los sitios que
# administra pueden encontrarse directamente en su directorio.
#
# Si comparten gestión con otras usuarias, se hacen links simbólicos
# entre sí.
def self.site_path_for(site)
File.join(Site.site_path, site)
end
# Comprueba que el directorio parezca ser de jekyll
def self.jekyll?(dir)
File.directory?(dir) && File.exist?(File.join(dir, '_config.yml'))
end
# TODO: En lugar de leer todo junto de una vez, extraer la carga de
# documentos de Jekyll hacia Sutty para que podamos leer los datos que
# necesitamos.
def self.load_jekyll(path)
# Pasamos destination porque configuration() toma el directorio
# actual y se mezclan :/
@ -339,12 +315,58 @@ class Site < ApplicationRecord
Jekyll::Site.new(config)
end
def self.domain
ENV.fetch('SUTTY', 'sutty.nl')
end
private
# Clona el esqueleto de Sutty para crear el sitio nuevo, no pasa nada
# si el sitio ya existe
def clone_skel!
return if File.directory? path
Rugged::Repository.clone_at ENV['SKEL_SUTTY'], path
end
# Carga el sitio Jekyll
def load_jekyll!
return unless name.present? && File.directory?(path)
Dir.chdir(path) do
@jekyll ||= Site.load_jekyll(Dir.pwd)
end
end
# Elimina el directorio del sitio
def remove_directories!
FileUtils.rm_rf path
end
def update_name!
return unless name_changed?
FileUtils.mv old_path, path
end
# Sincroniza algunos atributos del sitio con su configuración y
# guarda los cambios
def sync_attributes_with_config!
config.theme = design.gem unless design_id_changed?
config.description = description unless description_changed?
config.title = title unless title_changed?
end
# Valida si el sitio tiene al menos una forma de alojamiento asociada
# y es la local
#
# TODO: Volver opcional el alojamiento local, pero ahora mismo está
# atado a la generación del sitio así que no puede faltar
def deploy_local_presence
# Usamos size porque queremos saber la cantidad de deploys sin
# guardar también
return if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal')
errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence'))
end
end

62
app/models/site/config.rb Normal file
View file

@ -0,0 +1,62 @@
# frozen_string_literal: true
class Site
# Representa la configuración del sitio de forma que podamos leer y
# escribir en el archivo _config.yml
class Config < OpenStruct
def initialize(site)
# Iniciar el OpenStruct con el sitio
super(site: site)
read
end
# Obtener un valor por defecto a partir de la configuración
def fetch(key, default)
send(:[], key) || default
end
# Leer el archivo de configuración y setear los atributos en el
# objeto actual, creando los metodos de ostruct
def read
data = YAML.safe_load(File.read(path))
@hash = data.hash
data.each do |key, value|
send("#{key}=".to_sym, value)
end
end
# Escribe los cambios en el repositorio
def write(usuarie = nil)
return if persisted?
I18n.with_locale(usuarie.try(:lang) || I18n.default_locale) do
Site::Writer.new(site: site, file: path,
content: content.to_yaml, usuarie: usuarie,
message: I18n.t('sites.repository.config')).save
end
# Actualizar el hash para no escribir dos veces
@hash = content.hash
end
# Detecta si la configuración cambió comparando con el valor inicial
def persisted?
@hash == content.hash
end
# Obtener el contenido de la configuración como un hash, sin el
# sitio correspondiente.
def content
h = to_h.stringify_keys
h.delete 'site'
h
end
# Obtener la ruta donde se encuentra la configuración.
def path
File.join site.path, '_config.yml'
end
end
end

View file

@ -0,0 +1,106 @@
# frozen_string_literal: true
class Site
# Acciones para el repositorio Git de un sitio. Por ahora hacemos un
# uso muy básico de Git, con lo que asumimos varias cosas, por ejemplo
# que un sitio tiene un solo origen, que siempre se trabaja con la
# rama master, etc.
class Repository
attr_reader :rugged, :changes
def initialize(path)
@rugged = Rugged::Repository.new(path)
@changes = 0
end
def remote
@remote ||= rugged.remotes.first
end
# Trae los cambios del repositorio de origen sin aplicarlos y
# devuelve la cantidad de commits pendientes.
#
# XXX: Prestar atención a la velocidad de respuesta cuando tengamos
# repositorios remotos.
def fetch
if remote.check_connection :fetch
@changes = rugged.fetch(remote)[:received_objects]
else
0
end
end
# Incorpora los cambios en el repositorio actual
#
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def merge(author)
master = rugged.branches['master'].target
origin = rugged.branches['origin/master'].target
merge = rugged.merge_commits(master, origin)
# No hacemos nada si hay conflictos
#
# TODO: Enviar un correo a administración para poder revisar
# manualmente. Idealmente no deberíamos tener conflictos pero
# quién sabe.
return if merge.conflicts?
author = { name: author.name, email: author.email }
commit = Rugged::Commit
.create(rugged,
parents: [master, origin],
tree: merge.write_tree(rugged),
message: I18n.t('sites.fetch.merge.message'),
author: author,
committer: author,
update_ref: 'HEAD')
# Forzamos el checkout para mover el HEAD al último commit y
# escribir los cambios
rugged.checkout 'HEAD', strategy: :force
commit
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
# Compara los commits entre el repositorio remoto y el actual para
# que luego los podamos mostrar.
def commits
walker = Rugged::Walker.new rugged
# Obtenemos todos los commits que existen en origin/master que no
# están en la rama master local
#
# XXX: monitorear esto por performance
walker.push 'refs/remotes/origin/master'
walker.hide 'refs/heads/master'
walker.each.to_a
end
# Guarda los cambios en git, de a un archivo por vez
# rubocop:disable Metrics/AbcSize
def commit(file:, usuarie:, message:)
rugged.index.add(file)
rugged.index.write
Rugged::Commit.create(rugged,
update_ref: 'HEAD',
parents: [rugged.head.target],
tree: rugged.index.write_tree,
message: message,
author: author(usuarie),
committer: committer)
end
# rubocop:enable Metrics/AbcSize
def author(author)
{ name: author.name, email: author.email, time: Time.now }
end
def committer
{ name: 'Sutty', email: "sutty@#{Site.domain}", time: Time.now }
end
end
end

46
app/models/site/writer.rb Normal file
View file

@ -0,0 +1,46 @@
# frozen_string_literal: true
class Site
# Se encarga de guardar los cambios en los archivos y mantenerlos
# actualizados en git
class Writer
attr_reader :site, :file, :content, :usuarie, :message
def initialize(site:, file:, content:, usuarie:, message:)
@site = site
@content = content
@file = file
@usuarie = usuarie
@message = message
end
# rubocop:disable Metrics/AbcSize
def save
r = File.open(file, File::RDWR | File::CREAT, 0o640) do |f|
# Bloquear el archivo para que no sea accedido por otro
# proceso u otra editora
f.flock(File::LOCK_EX)
# Empezar por el principio
f.rewind
# Escribir el contenido
f.write(content)
# Eliminar el resto
f.flush
f.truncate(f.pos)
end
r.zero? && site.repository.commit(file: relative_file,
usuarie: usuarie,
message: message)
end
# rubocop:enable Metrics/AbcSize
# Devuelve la ruta relativa a la raíz del sitio
def relative_file
Pathname.new(file).relative_path_from(Pathname.new(site.path)).to_s
end
end
end

3
app/models/site_stat.rb Normal file
View file

@ -0,0 +1,3 @@
# frozen_string_literal: true
SiteStat = Struct.new(:site)

View file

@ -11,6 +11,10 @@ class Usuarie < ApplicationRecord
has_many :roles
has_many :sites, through: :roles
def name
email.split('@', 2).first
end
def rol_for_site(site)
site.roles.merge(roles).first
end

View file

@ -16,13 +16,35 @@ class SitePolicy
# Todes les usuaries pueden ver el sitio si aceptaron la invitación
def show?
!@usuarie.rol_for_site(@site).temporal
!current_role.temporal
end
# Todes pueden crear nuevos sitios
def new?
true
end
def create?
new?
end
# Para poder editarlos también tienen que haber aceptado la invitación
def edit?
show? && usuarie?
end
def update?
edit?
end
def destroy?
edit?
end
# Les invitades no pueden generar el sitio y les usuaries solo hasta
# que aceptan la invitación
def build?
show? && !site.invitade?(usuarie)
show? && usuarie?
end
def send_public_file?
@ -33,11 +55,33 @@ class SitePolicy
build?
end
def build_log?
build?
end
def reorder_posts?
build?
end
def pull?
build?
end
def fetch?
pull?
end
def merge?
pull?
end
private
def current_role
usuarie.rol_for_site(site)
end
def usuarie?
site.usuarie? usuarie
end
def invitade?
site.invitade? usuarie
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
# Política de acceso a las estadísticas
class SiteStatPolicy
attr_reader :site_stat, :usuarie
def initialize(usuarie, site_stat)
@usuarie = usuarie
@site_stat = site_stat
end
def index?
site_stat.site.usuarie? usuarie
end
end

View file

@ -0,0 +1,17 @@
%h1= t('.hi')
= sanitize_markdown t('.explanation', fqdn: @deploy_local.fqdn),
tags: %w[p a strong em]
%table
%thead
%tr
%th= t('.th.type')
%th= t('.th.status')
%tbody
- @deploys.each do |deploy, value|
%tr
%td= t(".#{deploy}.title")
%td= value ? t(".#{deploy}.success") : t(".#{deploy}.error")
= sanitize_markdown t('.help'), tags: %w[p a strong em]

View file

@ -0,0 +1,12 @@
= "# #{t('.hi')}"
\
= t('.explanation', fqdn: @deploy_local.fqdn)
\
= Terminal::Table.new do |table|
- table << [t('.th.type'), t('.th.status')]
- table.add_separator
- @deploys.each do |deploy, value|
- table << [t(".#{deploy}.title"),
value ? t(".#{deploy}.success") : t(".#{deploy}.error")]
\
= t('.help')

View file

@ -0,0 +1,14 @@
-#
Formulario para alojamiento local. Como el alojamiento local no es
opcional aun, solo enviamos el tipo con el formulario, no necesitamos
nada más.
.row
.col
%h3= t('.title')
- name = site.name || t('.ejemplo')
= sanitize_markdown t('.help',
fqdn: deploy.object.fqdn || "#{name}.#{Site.domain}"),
tags: %w[p strong em a]
= deploy.hidden_field :type

View file

@ -0,0 +1,21 @@
-# Formulario para "alojar" en un zip
.row
.col
= deploy.hidden_field :id
= deploy.hidden_field :type
%h3
-#
El checkbox invierte la lógica de destrucción porque queremos
crear el deploy si está activado y destruirlo si está
desactivado.
= deploy.check_box :_destroy,
{ checked: deploy.object.persisted? },
'0', '1'
= deploy.label :_destroy, t('.title')
-# TODO: secar la generación de URLs
- name = site.name || t('.ejemplo')
= sanitize_markdown t('.help',
fqdn: deploy.object.fqdn || "#{name}.#{Site.domain}",
file: deploy.object.file || "#{name}.zip"),
tags: %w[p strong em a]

View file

@ -1,4 +1,3 @@
- binding.pry
%p= t("devise.mailer.invitation_instructions.hello", email: @resource.email)
%p= t("devise.mailer.invitation_instructions.someone_invited_you", url: @resource.sites.first.name)
%p= link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, invitation_token: @token)

View file

@ -1,42 +1,65 @@
.row
.col
= render 'layouts/breadcrumb',
crumbs: [link_to(t('.index'), sites_path), t('.title')]
.row.align-items-center.justify-content-center.full-height
.col-md-6.align-self-center
%h2= t('.title', resource: resource.model_name.human)
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f|
%h2= t('.title')
= form_for(resource,
as: resource_name,
url: registration_path(resource_name),
html: { method: :put }) do |f|
= render 'devise/shared/error_messages', resource: resource
.form-group
= f.label :email
= f.email_field :email, autofocus: true, autocomplete: 'email',
class: 'form-control'
- if devise_mapping.confirmable? && resource.pending_reconfirmation?
%div= t('.currently_waiting_confirmation_for_email', email: resource.unconfirmed_email)
%div
= t('.currently_waiting_confirmation_for_email',
email: resource.unconfirmed_email)
.form-group
= f.label :lang
= f.select :lang,
I18n.available_locales.map { |lang| [t(lang), lang] }, {},
class: 'form-control'
.form-group
= f.label :password
%i
(#{t('.leave_blank_if_you_don_t_want_to_change_it')})
= f.password_field :password, autocomplete: 'new-password',
class: 'form-control'
- if @minimum_password_length
%em= t('devise.shared.minimum_password_length', count: @minimum_password_length)
class: 'form-control', 'aria-describedby': 'password-help'
%small.text-muted.form-text#password-help
= t('.leave_blank_if_you_don_t_want_to_change_it')
- if @minimum_password_length
= t('devise.shared.minimum_password_length',
count: @minimum_password_length)
.form-group
= f.label :password_confirmation
= f.password_field :password_confirmation,
autocomplete: 'new-password',
class: 'form-control'
.form-group
= f.label :current_password
%i
(#{t('.we_need_your_current_password_to_confirm_your_changes')})
= f.password_field :current_password,
autocomplete: 'current-password',
class: 'form-control'
required: true,
class: 'form-control',
'aria-describedby': 'current-password-help'
%small.text-muted.form-text#current-password-help
= t('.we_need_your_current_password_to_confirm_your_changes')
.actions
= f.submit t('.update'),
class: 'btn btn-lg btn-primary btn-block'
%hr/
%h3= t('.cancel_my_account')
%p
= t('.unhappy')
= button_to t('.cancel_my_account'),
registration_path(resource_name),
data: { confirm: t('.are_you_sure') },
method: :delete
= link_to t('devise.shared.links.back'), :back
method: :delete, class: 'btn btn-danger btn-block'

View file

@ -1,17 +1,20 @@
%nav{'aria-label': 'breadcrumb', role: 'navigation'}
%nav{ 'aria-label': 'breadcrumb', role: 'navigation' }
%ol.breadcrumb
%li.breadcrumb-item
= link_to destroy_usuarie_session_path, method: :delete,
data: { toggle: 'tooltip' }, title: t('help.logout'),
role: 'button', class: 'btn-text' do
= fa_icon 'sign-out', title: t('help.logout')
- if help = @site.try(:config).try(:dig, 'help')
%li.breadcrumb-item
= link_to edit_usuarie_registration_path,
data: { toggle: 'tooltip' }, title: t('help.usuarie.edit') do
= current_usuarie.email
- if @site.try(:persisted?) && (help = @site.try(:config).try(:dig, 'help'))
%li.breadcrumb-item= link_to t('.help'), help, target: '_blank'
- crumbs.compact.each do |crumb|
- if current_user.is_a? Invitadx
- if /\/sites/ =~ crumb
- next
- if crumb == crumbs.last
%li.breadcrumb-item.active{'aria-current': 'page'}= crumb
%li.breadcrumb-item.active{ 'aria-current': 'page' }= crumb
- else
%li.breadcrumb-item= crumb

View file

@ -0,0 +1 @@
%time{ datetime: time, title: time }= time_ago_in_words time

View file

@ -1,14 +1,26 @@
!!!
%html
%head
%meta{content: "text/html; charset=UTF-8", 'http-equiv': "Content-Type"}/
%meta{ content: 'text/html; charset=UTF-8',
'http-equiv': 'Content-Type' }/
%title Sutty
= csrf_meta_tags
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
- if @site.try(:config).try(:dig, 'css')
%link{rel: 'stylesheet', type: 'text/css', href: @site.get_url_from_site(@site.config.dig('css'))}
- style = "background-image: url(#{@site.try(:cover) || image_url('background.jpg')})"
%body{class: @has_cover ? 'background-cover' : '', style: @has_cover ? style : ''}
#sutty.container-fluid
= stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track': 'reload'
= javascript_include_tag 'application',
'data-turbolinks-track': 'reload'
- if @site.try(:persisted?) && @site.try(:config).try(:dig, 'css')
%link{ rel: 'stylesheet',
type: 'text/css',
href: @site.get_url_from_site(@site.config.dig('css')) }
- style = "background-image: url(#{@site.try(:cover)})"
-# haml-lint:disable InlineStyles
%body{ class: @has_cover ? 'background-cover' : '',
style: @has_cover ? style : '' }
.container-fluid#sutty
= yield
-# haml-lint:enable InlineStyles

View file

@ -1,8 +0,0 @@
!!!
%html
%head
%meta{:content => "text/html; charset=utf-8", "http-equiv" => "Content-Type"}/
:css
/* Email styles need to be inline */
%body
= yield

View file

@ -1,3 +1,12 @@
!!!
%html
%head
%meta{ content: 'text/html; charset=utf-8',
'http-equiv': 'Content-Type' }/
:css
/* Inline */
%body
= yield
= image_tag attachments['logo.png'].url, alt: 'Logo de Sutty'
= t('.signature')

View file

@ -1 +1,3 @@
= yield
= t('.signature')

View file

@ -1,19 +1,22 @@
- tags = %w[h1 h2 h3 h4 h5 h6 p a ul ol li table tr td th tbody thead tfoot em strong sup blockquote cite pre]
.row
.col
= render 'layouts/breadcrumb',
crumbs: [ link_to(t('sites.index'), sites_path),
@site.name,
link_to(t('posts.index'),
site_posts_path(@site)),
@post.title ]
crumbs: [link_to(t('sites.index'), sites_path),
@site.name,
link_to(t('posts.index'),
site_posts_path(@site)),
@post.title]
.row
.col
%h1{class: @post.get_front_matter(:dir)}= @post.title
%h1{ class: @post.get_front_matter(:dir) }= @post.title
%p
- translations = @post.translations.map do |translation|
- link_to translation.title, site_post_path(@site, translation, lang: translation.lang)
- link_to translation.title,
site_post_path(@site, translation, lang: translation.lang)
= raw translations.join(' / ')
.row
@ -24,16 +27,17 @@
.row
.col
.content{class: @post.get_front_matter(:dir)}
:markdown
#{@post.content}
.content{ class: @post.get_front_matter(:dir) }
= sanitize_markdown @post.content,
tags: tags
-# Representar los datos en una tabla:
-# Texto: tal cual en una celda
-# Array: píldoras
-# Array de Hashes: Tabla
-# Hash: Tabla
-# TODO DRY
-#
Representar los datos en una tabla:
Texto: tal cual en una celda
Array: píldoras
Array de Hashes: Tabla
Hash: Tabla
TODO DRY
%table.table.table-condensed.table-striped.table-responsive
%tbody
- @post.front_matter.each do |key, data|
@ -51,7 +55,7 @@
%tbody
- data.each do |r|
%tr
- r.each do |_,v|
- r.each do |_, v|
%td
- if v.is_a? Array
- v.each do |s|
@ -73,16 +77,15 @@
%td= v
- elsif data.respond_to? :content
-# Contenido del artículo
:markdown
#{data.content}
= sanitize_markdown data.content, tags: tags
- elsif data.respond_to? :strftime
-# Fecha
= data.strftime('%F')
- else
-# Texto
- if @post.image? key
%img.img-fluid{src: @site.get_url_for_sutty(data)}
%img.img-fluid{ src: @site.get_url_for_sutty(data) }
- elsif @post.url? key
%a{href: @site.get_url_for_sutty(data)}= data
%a{ href: @site.get_url_for_sutty(data) }= data
- else
= data

View file

@ -0,0 +1,97 @@
= form_for site, html: { class: form_class(site) } do |f|
.form-group
%h2= f.label :name
%p.lead= t('.help.name')
-#
El dominio contiene letras y números
No puede empezar ni terminar con guiones
No puede estar compuesto solo de números
= f.text_field :name,
class: form_control(site, :name),
required: true,
pattern: '^([a-z0-9][a-z0-9\-]*)?[a-z0-9]$',
minlength: 1,
maxlength: 63
- if invalid? site, :name
.invalid-feedback= site.errors.messages[:name].join(', ')
.form-group
%h2= f.label :title
%p.lead= t('.help.title')
= f.text_field :title, class: form_control(site, :title),
required: true
- if invalid? site, :title
.invalid-feedback= site.errors.messages[:title].join(', ')
.form-group
%h2= f.label :description
%p.lead= t('.help.description')
= f.text_area :description, class: form_control(site, :description),
maxlength: 160, minlength: 50, required: true
- if invalid? site, :description
.invalid-feedback= site.errors.messages[:description].join(', ')
%hr/
.form-group
%h2= t('.design.title')
%p.lead= t('.help.design')
.row
-# Demasiado complejo para un f.collection_radio_buttons
- Design.all.each do |design|
.col
%h3
= f.radio_button :design_id, design.id,
checked: design.id == site.design_id,
disabled: design.disabled
= f.label "design_id_#{design.id}", design.name
= sanitize_markdown design.description,
tags: %w[p a strong em]
.btn-group{ role: 'group', 'aria-label': t('.design.actions') }
- if design.url
= link_to t('.design.url'), design.url,
target: '_blank', class: 'btn btn-info'
- if design.license
= link_to t('.design.license'), design.license,
target: '_blank', class: 'btn btn-info'
%hr/
.form-group
%h2= t('.licencia.title')
%p.lead= t('.help.licencia')
- Licencia.all.each do |licencia|
.row
.col
%h3
= f.radio_button :licencia_id, licencia.id,
checked: licencia.id == site.licencia_id
= f.label "licencia_id_#{licencia.id}" do
= image_tag licencia.icons, alt: licencia.name
= licencia.name
= sanitize_markdown licencia.description,
tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6]
.btn-group{ role: 'group', 'aria-label': t('.licencia.actions') }
= link_to t('.licencia.url'), licencia.url,
target: '_blank', class: 'btn btn-info'
%hr/
.form-group
%h2= t('.privacidad.title')
%p.lead= sanitize_markdown t('.help.privacidad'), tags: %w[a]
%hr/
.form-group
%h2= t('.deploys.title')
%p.lead= t('.help.deploys')
= f.fields_for :deploys do |deploy|
= render "deploys/#{deploy.object.type.underscore}",
deploy: deploy, site: site
%hr/
.form-group
= f.submit submit, class: 'btn btn-success'

10
app/views/sites/edit.haml Normal file
View file

@ -0,0 +1,10 @@
.row
.col
= render 'layouts/breadcrumb',
crumbs: [link_to(t('sites.index'), sites_path),
t('.title', site: @site.name)]
.row
.col
%h1= t('.title', site: @site.name)
= render 'form', site: @site, submit: t('.submit')

View file

@ -0,0 +1,34 @@
.row
.col
= render 'layouts/breadcrumb',
crumbs: [link_to(t('sites.index'), sites_path), t('.title')]
.row.justify-content-center
.col-md-8#pull
%h1= t('.title')
%p.lead= sanitize_markdown t('.help.fetch'), tags: %w[em strong a]
%h2= t('.toc')
%ul.toc
- @commits.each do |commit|
%li= link_to commit.summary, "##{commit.oid}"
- @commits.each do |commit|
.row.justify-content-center
.col-md-8{ id: commit.oid }
%h1= commit.summary
%p.lead= render 'layouts/time', time: commit.time
-#
No hay forma de obtener el cuerpo del commit separado del
resumen, cortamos por el primer salto de línea doble y obtenemos
todo lo demás
= sanitize_markdown commit.message.split("\n\n", 2).last,
tags: %w[p a h1 h2 h3 h4 h5 h6 ol ul li strong em]
%hr
- unless @commits.empty?
.row.justify-content-center
.col-md-8
= link_to t('.merge.request'), site_pull_path(@site),
method: 'post', class: 'btn btn-lg btn-success'

View file

@ -1,10 +1,13 @@
.row
.col
= render 'layouts/breadcrumb', crumbs: [ t('sites.index') ]
= render 'layouts/breadcrumb', crumbs: [t('sites.index')]
.row
.col
%h1= t('sites.title')
%h1
= t('sites.title')
- if policy(Site).new?
= link_to t('sites.new.title'), new_site_path,
class: 'btn btn-info'
= render 'layouts/help', help: t('help.sites.index')
@ -16,14 +19,15 @@
%h2
- if policy(site).show?
= link_to site.name, site_path(site)
- else
- else
= site.name
- if site.invitade? current_usuarie
%span.badge.badge-warning{data: { toggle: 'tooltip' },
title: t('help.sites.invitade')}
%span.badge.badge-warning{ data: { toggle: 'tooltip' },
title: t('help.sites.invitade') }
= t('.invitade')
%br
.btn-group{role: 'group', 'aria-label': t('sites.actions')}
.btn-group{ role: 'group',
'aria-label': t('sites.actions') }
- if current_usuarie.rol_for_site(site).temporal
= button_to t('sites.invitations.accept'),
site_usuaries_accept_invitation_path(site),
@ -64,7 +68,8 @@
type: 'secondary',
link: nil
- else
= form_tag site_enqueue_path(site), method: :post, class: 'form-inline' do
= form_tag site_enqueue_path(site),
method: :post, class: 'form-inline' do
= button_tag type: 'submit',
class: 'btn btn-success',
title: t('help.sites.enqueue'),
@ -72,12 +77,9 @@
= fa_icon 'building'
= t('sites.enqueue')
- if policy(site).build_log?
- if site.failed?
%button.btn.btn-danger= t('sites.failed')
- if site.build_log?
= render 'layouts/btn_with_tooltip',
tooltip: t('help.sites.build_log'),
text: t('sites.build_log'),
type: 'warning',
link: site_build_log_path(site)
- if policy(site).pull? && site.needs_pull?
= render 'layouts/btn_with_tooltip',
tooltip: t('help.sites.pull'),
text: t('.pull'),
type: 'info',
link: site_pull_path(site)

9
app/views/sites/new.haml Normal file
View file

@ -0,0 +1,9 @@
.row
.col
= render 'layouts/breadcrumb',
crumbs: [link_to(t('sites.index'), sites_path), t('.title')]
.row
.col
%h1= t('.title')
= render 'form', site: @site, submit: t('.submit')

View file

@ -0,0 +1,18 @@
.row
.col
= render 'layouts/breadcrumb',
crumbs: [link_to(t('sites.index'), sites_path),
link_to(@site.name, site_path(@site)), t('.title')]
.row
.col
%h1= t('.title')
%p.lead= t('.help')
%table.table.table-striped.table-condensed
%tbody
%tr
%td= t('.build.average')
%td= distance_of_time_in_words_if_more_than_a_minute @build_avg
%tr
%td= t('.build.maximum')
%td= distance_of_time_in_words_if_more_than_a_minute @build_max

View file

@ -0,0 +1,45 @@
# frozen_string_literal: true
# Realiza el deploy de un sitio
class DeployWorker
include Sidekiq::Worker
def perform(site)
site = Site.find(site)
site.update_attribute :status, 'building'
# Asegurarse que DeployLocal sea el primero!
deployed = { deploy_local: deploy_local(site) }
# No es opcional
unless deployed[:deploy_local]
site.update_attribute :status, 'waiting'
raise
end
deploy_others site, deployed
notify_usuaries site, deployed
site.update_attribute :status, 'waiting'
end
private
def deploy_local(site)
site.deploys.find_by(type: 'DeployLocal').deploy
end
def deploy_others(site, deployed)
site.deploys.where.not(type: 'DeployLocal').find_each do |d|
deployed[d.type.underscore.to_sym] = d.deploy
end
end
def notify_usuaries(site, deployed)
# TODO: existe site.usuaries_ids?
site.usuaries.find_each do |usuarie|
DeployMailer.with(usuarie: usuarie.id, site: site.id)
.deployed(deployed)
.deliver_now
end
end
end

29
bin/haml-lint Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'haml-lint' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
bundle_binstub = File.expand_path("../bundle", __FILE__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("haml_lint", "haml-lint")

View file

@ -1,80 +0,0 @@
#!/bin/bash
# TODO convertir a ruby!
set -e
rails_root="${PWD}"
# Encontrar todos los sitios únicos con el archivo `.generate`. Esto
# significa que la usuaria quiso generar el sitio.
find -L ./_sites -mindepth 2 -maxdepth 2 -name .generate \
| sed "s/\/\.generate$//" \
| while read _path ; do
# Como seguimos todos los symlinks y los sitios pueden estar
# vinculados entre sí, volvemos a chequear si existe el archivo para
# no generarlo dos veces
test -f "${_path}/.generate" || continue
test -f "${_path}/.generating" && continue
# Obtenemos las direcciones de correo de las responsables
_mail=($(cat "${_path}/.usuarias"))
_site="$(echo "${_path}" | xargs basename)"
_deploy="${rails_root}/_deploy/${_site}"
# Entrar al directorio del sitio
pushd "${_path}" &>/dev/null
# Reiniciar el log con la fecha
date > build.log
# Instalar las gemas si no están
test -f .bundle/config \
|| bundle install --path=/srv/http/gems.kefir.red \
>> build.log
# Actualizar las gemas
bundle >> build.log
# Instalar los assets
test -f yarn.lock \
&& yarn >> build.log
# Crear el sitio con lujo de detalles y guardar un log, pero a la vez
# tenerlo en la salida estándar para poder enviar al MAILTO del
# cronjob.
#
# Ya que estamos, eliminamos la ruta donde estamos paradas para no dar
# información sobre la servidora.
touch .generating
# Correr en baja prioridad
nice -n 19 \
bundle exec \
jekyll build --trace --destination "${_deploy}" 2>&1 \
| sed -re "s,${_path},,g" \
>> "build.log"
# Acciones posteriores
# TODO convertir en un plugin de cada sitio?
if test $? -eq 0; then
# Si funciona, enviar un mail
# TODO enviar un mail más completo y no hardcodear direcciones
echo "Everything was good! You can see your changes in https://${_site}" \
| mail -b "sysadmin@kefir.red" \
-s "${_site}: :)" \
${_mail[@]}
else
echo "There was an error, please check build log at https://sutty.kefir.red/" \
| mail -b "sysadmin@kefir.red" \
-s "${_site}: :(" \
${_mail[@]}
date +%s >.failed
fi
# Eliminar el archivo para sacar el sitio de la cola de compilación
rm -f .generate .generating
# TODO descubrir el grupo según la distro?
chgrp -R http "${_deploy}"
find "${_deploy}" -type f -print0 | xargs -r -0 chmod 640
find "${_deploy}" -type d -print0 | xargs -r -0 chmod 2750
# Volver al principio para continuar con el siguiente sitio
popd &>/dev/null
done

View file

@ -1,11 +1,4 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
begin
load File.expand_path('spring', __dir__)
rescue LoadError => e
raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

View file

@ -1,11 +1,4 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
begin
load File.expand_path('spring', __dir__)
rescue LoadError => e
raise unless e.message.include?('spring')
end
require_relative '../config/boot'
require 'rake'
Rake.application.run

View file

@ -1,12 +1,9 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'pathname'
require 'fileutils'
include FileUtils
# path to your application root.
APP_ROOT = Pathname.new File.expand_path('..', __dir__)
APP_ROOT = File.expand_path('..', __dir__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")

32
bin/sidekiq Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'sidekiq' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require 'pathname'
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
Pathname.new(__FILE__).realpath)
bundle_binstub = File.expand_path('bundle', __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort(
'Your `bin/bundle` was not generated by Bundler, so this binstub
cannot run. Replace `bin/bundle` by running `bundle binstubs
bundler --force`, then run this command again.'
)
end
end
require 'rubygems'
require 'bundler/setup'
load Gem.bin_path('sidekiq', 'sidekiq')

32
bin/sidekiqctl Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'sidekiqctl' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require 'pathname'
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
Pathname.new(__FILE__).realpath)
bundle_binstub = File.expand_path('bundle', __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort(
'Your `bin/bundle` was not generated by Bundler, so this binstub
cannot run. Replace `bin/bundle` by running `bundle binstubs
bundler --force`, then run this command again.'
)
end
end
require 'rubygems'
require 'bundler/setup'
load Gem.bin_path('sidekiq', 'sidekiqctl')

View file

@ -1,12 +1,9 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'pathname'
require 'fileutils'
include FileUtils
# path to your application root.
APP_ROOT = Pathname.new File.expand_path('..', __dir__)
APP_ROOT = File.expand_path('..', __dir__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
@ -20,6 +17,9 @@ chdir APP_ROOT do
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
# Install JavaScript dependencies if using Yarn
# system('bin/yarn')
puts "\n== Updating database =="
system! 'bin/rails db:migrate'

11
bin/yarn Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env ruby
APP_ROOT = File.expand_path('..', __dir__)
Dir.chdir(APP_ROOT) do
begin
exec "yarnpkg", *ARGV
rescue Errno::ENOENT
$stderr.puts "Yarn executable was not detected in the system."
$stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
exit 1
end
end

View file

@ -19,13 +19,18 @@ require 'rails/test_unit/railtie'
Bundler.require(*Rails.groups)
module Sutty
# Sutty!
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
# Initialize configuration defaults for originally generated Rails
# version.
config.load_defaults 5.1
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
config.action_dispatch.rescue_responses['Pundit::NotAuthorizedError'] = :forbidden
# Settings in config/environments/* take precedence over those
# specified here. Application configuration should go into files in
# config/initializers -- all .rb files in that directory are
# automatically loaded.
config.action_dispatch
.rescue_responses['Pundit::NotAuthorizedError'] = :forbidden
config.active_record.sqlite3.represent_boolean_as_integer = true
end
end

View file

@ -22,4 +22,4 @@ test:
production:
<<: *default
database: db/production.sqlite3
database: data/production.sqlite3

View file

@ -1,11 +1,13 @@
# frozen_string_literal: true
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Settings specified here will take precedence over those in
# config/application.rb.
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
# In the development environment your application's code is reloaded
# on every request. This slows down response time but is perfect for
# development since you don't have to restart the web server when you
# make code changes.
config.cache_classes = false
# Do not eager load code on boot.
@ -18,7 +20,7 @@ Rails.application.configure do
if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.cache_store = :memory_store
config.cache_store = :redis_cache_store
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}"
}
@ -39,8 +41,8 @@ Rails.application.configure do
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# Debug mode disables concatenation and preprocessing of assets. This
# option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = true
@ -50,8 +52,9 @@ Rails.application.configure do
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
# Use an evented file watcher to asynchronously detect changes in
# source code, routes, locales, etc. This feature depends on the
# listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
CarrierWave.configure do |config|
@ -63,5 +66,6 @@ Rails.application.configure do
config.action_mailer.perform_caching = false
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config.action_mailer.default_url_options = { host: 'localhost',
port: 3000 }
end

View file

@ -56,12 +56,12 @@ Rails.application.configure do
config.log_tags = [:request_id]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
config.cache_store = :redis_cache_store
# Use a real queuing backend for Active Job (and separate queues per
# environment)
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "sutty_#{Rails.env}"
config.active_job.queue_adapter = :sidekiq
config.active_job.queue_name_prefix = "sutty_#{Rails.env}"
config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.

View file

@ -1,17 +0,0 @@
# frozen_string_literal: true
require 'commonmarker'
module Haml::Filters
remove_filter('Markdown') # remove the existing Markdown filter
module Markdown
include Haml::Filters::Base
def render(text)
CommonMarker.render_html(text,
[:TABLE_PREFER_STYLE_ATTRIBUTES],
%i[table strikethrough autolink tagfilter])
end
end
end

View file

@ -0,0 +1,3 @@
# frozen_string_literal: true
String.include CoreExtensions::String::StripTags

View file

@ -78,7 +78,7 @@ Devise.setup do |config|
# `config.http_authenticatable = [:database]` will enable it only for
# database authentication. The supported strategies are: :database
# = Support basic authentication with authentication key + password
# config.http_authenticatable = false
config.http_authenticatable = true
# If 401 status code should be returned for AJAX requests. True by
# default.
@ -127,7 +127,7 @@ Devise.setup do |config|
config.stretches = Rails.env.test? ? 1 : 11
# Set up a pepper to generate the hashed password.
config.pepper = ENV['DEVISE_PEPPER']
config.pepper = Rails.application.credentials.devise_pepper
# Send a notification to the original email when the user's email is
# changed.

View file

@ -7,6 +7,8 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.singular 'invitades', 'invitade'
inflect.plural 'usuarie', 'usuaries'
inflect.singular 'usuaries', 'usuarie'
inflect.plural 'licencia', 'licencias'
inflect.singular 'licencias', 'licencia'
inflect.plural 'rol', 'roles'
inflect.singular 'roles', 'rol'
end
@ -20,4 +22,6 @@ ActiveSupport::Inflector.inflections(:es) do |inflect|
inflect.singular 'usuaries', 'usuarie'
inflect.plural 'rol', 'roles'
inflect.singular 'roles', 'rol'
inflect.plural 'licencia', 'licencias'
inflect.singular 'licencias', 'licencia'
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
Rails.application.configure do
config.i18n.available_locales = %i[es en ar]
config.i18n.available_locales = %i[es en]
config.i18n.default_locale = :es
end

View file

@ -0,0 +1,97 @@
# frozen_string_literal: true
Mobility.configure do |config|
# Sets the default backend to use in models. This can be overridden in
# models by passing +backend: ...+ to +translates+.
config.default_backend = :key_value
# By default, Mobility uses the +translates+ class method in models to
# describe translated attributes, but you can configure this method to
# be whatever you like. This may be useful if using Mobility alongside
# another translation gem which uses the same method name.
config.accessor_method = :translates
# To query on translated attributes, you need to append a scope to
# your model. The name of this scope is +i18n+ by default, but this
# can be changed to something else.
config.query_method = :i18n
# Uncomment and remove (or add) items to (from) this list to
# completely disable/enable plugins globally (so they cannot be used
# and are never even loaded). Note that if you remove an item from the
# list, you will not be able to use the plugin at all, and any options
# for the plugin will be ignored by models. (In most cases, you
# probably don't want to change this.)
#
# config.plugins = %i[
# query
# cache
# dirty
# fallbacks
# presence
# default
# attribute_methods
# fallthrough_accessors
# locale_accessors
# ]
# The translation cache is on by default, but you can turn it off by
# uncommenting this line. (This may be helpful in debugging.)
#
# config.default_options[:cache] = false
# Dirty tracking is disabled by default. Uncomment this line to enable
# it. If you enable this, you should also enable +locale_accessors+
# by default (see below).
#
# config.default_options[:dirty] = true
# No fallbacks are used by default. To define default fallbacks,
# uncomment and set the default fallback option value here. A "true"
# value will use whatever is defined by +I18n.fallbacks+ (if defined),
# or alternatively will fallback to your +I18n.default_locale+.
#
config.default_options[:fallbacks] = true
# The Presence plugin converts empty strings to nil when fetching and
# setting translations. By default it is on, uncomment this line to
# turn it off.
#
# config.default_options[:presence] = false
# Set a default value to use if the translation is nil. By default
# this is off, uncomment and set a default to use it across all models
# (you probably don't want to do that).
#
# config.default_options[:default] = ...
# Uncomment to enable locale_accessors by default on models. A true
# value will use the locales defined either in
# Rails.application.config.i18n.available_locales or
# I18n.available_locales. If you want something else, pass an array
# of locales instead.
#
# config.default_options[:locale_accessors] = true
# Uncomment to enable fallthrough accessors by default on models. This
# will allow you to call any method with a suffix like _en or _pt_br,
# and Mobility will catch the suffix and convert it into a locale in
# +method_missing+. If you don't need this kind of open-ended
# fallthrough behavior, it's better to use locale_accessors instead
# (which define methods) since method_missing is very slow. (You can
# use both fallthrough and locale accessor plugins together without
# conflict.)
#
# Note: The dirty plugin enables fallthrough_accessors by default.
#
# config.default_options[:fallthrough_accessors] = true
# You can also include backend-specific default options. For example,
# if you want to default to using the text-type translation table with
# the KeyValue backend, you can set that as a default by uncommenting
# this line, or change it to :string to default to the string-type
# translation table instead. (For other backends, this option is
# ignored.)
#
# config.default_options[:type] = :text
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
Sidekiq.configure_server do |config|
config.redis = {
url: ENV.fetch('REDIS_SERVER', 'redis://localhost:6379/1')
}
end
Sidekiq.configure_client do |config|
config.redis = {
url: ENV.fetch('REDIS_CLIENT', 'redis://localhost:6379/1')
}
end

View file

@ -93,14 +93,14 @@ en:
registrations:
destroyed: Bye! Your account has been successfully cancelled. We hope to see you again soon.
edit:
index: 'Back to sites'
are_you_sure: Are you sure?
cancel_my_account: Cancel my account
currently_waiting_confirmation_for_email: 'Currently waiting confirmation for: %{email}'
leave_blank_if_you_don_t_want_to_change_it: leave blank if you don't want to change it
title: Edit %{resource}
unhappy: Unhappy?
leave_blank_if_you_don_t_want_to_change_it: Leave empty if you don't want to change it.
title: Edit my account
update: Update
we_need_your_current_password_to_confirm_your_changes: we need your current password to confirm your changes
we_need_your_current_password_to_confirm_your_changes: We need your current password to confirm your changes
new:
sign_up: Sign up
signed_up: Welcome! You have signed up successfully.
@ -126,8 +126,8 @@ en:
sign_in_with_provider: Sign in with %{provider}
sign_up: Sign up
minimum_password_length:
one: "(%{count} character minimum)"
other: "(%{count} characters minimum)"
one: "%{count} character minimum."
other: "%{count} characters minimum."
unlocks:
new:
resend_unlock_instructions: Resend unlock instructions

View file

@ -93,14 +93,14 @@ es:
registrations:
destroyed: "¡Adiós! Tu cuenta ha sido cancelada correctamente. Esperamos verte pronto."
edit:
are_you_sure: "¿Estás segura?"
cancel_my_account: Anular mi cuenta
index: 'Volver a sitios'
are_you_sure: "¿Estás segure?"
cancel_my_account: Eliminar mi cuenta
currently_waiting_confirmation_for_email: 'Actualmente esperando la confirmacion de: %{email} '
leave_blank_if_you_don_t_want_to_change_it: dejar en blanco si no desea cambiarlo
title: Editar %{resource}
unhappy: "¿Disconforme?"
update: Actualizar
we_need_your_current_password_to_confirm_your_changes: necesitamos tu contraseña actual para confirmar los cambios
leave_blank_if_you_don_t_want_to_change_it: Deja este campo vacío si no deseas cambiarla.
title: Editar mi cuenta
update: Actualizar mi perfil
we_need_your_current_password_to_confirm_your_changes: Necesitamos tu contraseña actual para confirmar los cambios.
new:
sign_up: Registrarme por primera vez
email: O simplemente continuar con tu dirección de correo y contraseña
@ -110,7 +110,6 @@ es:
signed_up_but_unconfirmed: Para recibir actualizaciones, se ha enviado un mensaje con un enlace de confirmación a tu correo electrónico. Abre el enlace para activar tu cuenta.
update_needs_confirmation: Has actualizado tu cuenta correctamente, pero es necesario confirmar tu nuevo correo electrónico. Por favor, comprueba tu correo y sigue el enlace de confirmación para finalizar la comprobación del nuevo correo electrónico.
updated: Tu cuenta se ha actualizado.
updated_but_not_signed_in:
sessions:
already_signed_out: Sesión finalizada.
new:
@ -129,8 +128,8 @@ es:
i_dont_have_account: ¿Nunca te registraste en LUNAR?
i_have_account: ¿Ya tenés cuenta?
minimum_password_length:
one: "(%{count} caracter como mínimo)"
other: "(%{count} caracteres como mínimo)"
one: "%{count} caracter como mínimo."
other: "%{count} caracteres como mínimo."
unlocks:
new:
resend_unlock_instructions: Reenviar instrucciones para desbloquear

View file

@ -1,7 +1,46 @@
en:
es: Castillian Spanish
en: English
seconds: '%{seconds} seconds'
deploy_mailer:
deployed:
subject: "[Sutty] The site %{site} has been built"
hi: "Hi!"
explanation: |
This e-mail is to notify you that Sutty has built your site and
it's available at <https://%{fqdn}>.
You'll find details bellow.
th:
type: Type
status: Status
deploy_local:
title: Build the site
success: Success!
error: Error
deploy_zip:
title: Build ZIP file
success: Available for download
error: Error
help: You can contact us by replying this e-mail
activerecord:
models:
usuarie: User
attributes:
usuarie:
email: 'E-mail address'
password: 'Password'
password_confirmation: 'Password confirmation'
current_password: 'Current password'
lang: 'Main language'
site:
name: 'Name'
errors:
models:
site:
attributes:
deploys:
deploy_local_presence: 'We need to be build the site!'
invitadx:
attributes:
email:
@ -18,6 +57,8 @@ en:
disordered: "The posts are disordered, this will prevent you from reordering them!"
disordered_button: 'Reorder!'
layouts:
mailer:
signature: 'With love, Sutty'
breadcrumb:
help: Help
collaborations:
@ -25,23 +66,12 @@ en:
submit: Register
password:
incorrect: 'Wrong password, please try again.'
invitadxs:
index:
title: 'Guests'
new:
email: 'E-Mail'
password: 'Password'
password_confirmation: 'Repeat password'
submit: 'Register'
acepta_politicas_de_privacidad: 'I accept the <a href="%{privacy_policy}" target="_blank">Privacy policy</a>.'
confirmation:
confirmed: 'Your account is confirmed, please log in to continue'
show:
confirmation_sent: "We've sent a confirmation link to your e-mail address. Please open that link to continue."
info:
posts:
reorder: 'The articles have been reordered!'
help:
usuarie:
edit: Edit my profile
category: 'Category'
logout: 'Close the session'
breadcrumbs: "What you see up here are the bread crumbs for this site. When you enter a new section, you will see the previous ones and also have a path for where you're standing."
@ -140,7 +170,39 @@ en:
logout: 'Log out'
lang: 'Language'
error: 'There was an error during log in. Did you type your credentials correctly?'
deploys:
deploy_local:
title: 'Host at Sutty'
help: |
The site will be available at <https://%{fqdn}/>.
We're working out the details for allowing your own site
domains, you can help us!
ejemplo: 'example'
deploy_zip:
title: 'Generate a ZIP file'
help: |
ZIP files contain and compress all the files of your site. With
this option you can download and also share your whole site
through the <https://%{fqdn}/%{file}> address, keep it as backup
or have an strategy of solidarity hosting, were many people
shares a copy of your site.
It also helps with site archival for historical purposes :)
ejemplo: 'example'
stats:
index:
title: Statistics
help: |
Statistics show information about how your site is generated and
how many resources it uses.
build:
average: 'Average building time'
maximum: 'Maximum building time'
sites:
repository:
config: 'Changes in config'
actions: 'Actions'
posts: 'View and edit posts'
title: 'Sites'
@ -152,12 +214,55 @@ en:
invitations:
accept: 'Accept invitation'
reject: 'No, thanks'
new:
title: 'Create site'
submit: 'Create site'
edit:
title: 'Edit %{site}'
submit: 'Save changes'
form:
help:
name: "Your site's name. It can only contain numbers and letters."
design: 'Select the design for your site. You can change it later. We add more designs from time to time.'
licencia: 'Everything we publish has automatic copyright. This
means nobody can use our works without explicit permission. By
using licenses, we stablish conditions by which we want to share
them.'
privacidad: |
The [privacy policy](https://sutty.nl/en/privacy-policy/) and
[code of conduct](https://sutty.nl/en/code-of-conduct/) inform
your visitors about their privacy and expected conduct of the
site's community. We suggest you use the same documents Sutty
uses. You can modify them as articles after creating the
site.
deploys: |
Sutty allows you to host your site in different places at the
same time. This strategy makes your site available even when
some of them become unavailable.
design:
title: 'Design'
actions: 'Information about this design'
url: 'Demo'
licencia: 'Read the license'
licencia:
title: 'License for the site and everything in it'
url: 'Read the license'
privacidad:
title: 'Privacy policy and code of conduct'
deploys:
title: 'Where do you want your site to be hosted?'
fetch:
title: 'Upgrade the site'
help:
fetch: 'Any changes made to the site are saved into a _git_ repository. Git saves the differences between previous and current versions of files so we can explore them as the history of the project. Also, we can bring and send changes between repositories. In this case, every site managed with Sutty share a common root that we call [skeleton](https://0xacab.org/sutty/skel.sutty.nl). When we upgrade this skeleton, you can explore the changes here and accept them to make your site better.'
toc: 'Table of contents'
merge:
request: 'Upgrade my site with these changes'
success: 'Site upgrade has been completed. Your next build will run this upgrade :)'
error: "There was an error when we were trying to upgrade your site. This could be due to conflicts that couldn't be solved automatically. We've sent a report of the issue to Sutty's admins so they already know about it. Sorry! :("
message: 'Skeleton upgrade'
footer:
powered_by: 'is developed by'
templates:
index: 'Templates'
edit: 'Edit'
save: 'Save'
i18n:
index: 'Translations'
edit: 'Edit texts and translations'

View file

@ -1,4 +1,28 @@
es:
es: Castellano
en: Inglés
seconds: '%{seconds} segundos'
deploy_mailer:
deployed:
subject: "[Sutty] El sitio %{site} ha sido generado"
hi: "¡Hola!"
explanation: |
Este correo es para notificarte que Sutty ha generado tu sitio y
ya está disponible en la dirección <https://%{fqdn}>.
A continuación encontrarás el detalle de lo que hicimos.
th:
type: Tipo
status: Estado
deploy_local:
title: Generar el sitio
success: ¡Éxito!
error: Hubo un error
deploy_zip:
title: Generar archivo ZIP
success: Disponible para descargar
error: Hubo un error
help: Por cualquier duda, responde este correo para contactarte con nosotres.
activerecord:
models:
usuarie: Usuarie
@ -7,8 +31,18 @@ es:
email: 'Correo electrónico'
password: 'Contraseña'
password_confirmation: 'Confirmación de contraseña'
current_password: 'Contraseña actual'
lang: Idioma principal
site:
name: 'Nombre'
title: 'Título'
description: 'Descripción'
errors:
models:
site:
attributes:
deploys:
deploy_local_presence: '¡Necesitamos poder generar el sitio!'
invitadx:
attributes:
email:
@ -25,6 +59,8 @@ es:
disordered: 'Los artículos no tienen número de orden, esto impedirá que los puedas reordenar'
disordered_button: '¡Reordenar!'
layouts:
mailer:
signature: 'Con cariño, Sutty'
breadcrumb:
help: Ayuda
collaborations:
@ -36,6 +72,8 @@ es:
posts:
reorder: "¡Los artículos fueron reordenados!"
help:
usuarie:
edit: Editar mi perfil
category: 'Categoría'
logout: 'Cierra la sesión'
breadcrumbs: 'Lo que ves arriba son las migas de pan de este sitio.
@ -143,7 +181,39 @@ es:
lang: 'Idioma'
logout: 'Salir'
error: 'Hubo un error al iniciar la sesión. ¿Escribiste bien tus credenciales?'
deploys:
deploy_local:
title: 'Alojar en Sutty'
help: |
El sitio estará disponible en <https://%{fqdn}/>.
Estamos desarrollando la posibilidad de agregar tus propios
dominios, ¡ayudanos!
ejemplo: 'ejemplo'
deploy_zip:
title: 'Generar un archivo ZIP'
help: |
Los archivos ZIP contienen y comprimen todos los archivos de tu
sitio. Con esta opción podrás descargar y compartir tu sitio
entero a través de la dirección <https://%{fqdn}/%{file}> y
guardarla como copia de seguridad o una estrategia de
alojamiento solidario, donde muchas personas comparten una copia
de tu sitio.
También sirve para archivo histórico :)
ejemplo: 'ejemplo'
stats:
index:
title: Estadísticas
help: |
Las estadísticas visibilizan información sobre cómo se genera y
cuántos recursos utiliza tu sitio.
build:
average: 'Tiempo promedio de generación'
maximum: 'Tiempo máximo de generación'
sites:
repository:
config: 'Cambios en la configuración'
actions: 'Acciones'
posts: 'Ver y editar artículos'
title: 'Sitios'
@ -155,6 +225,57 @@ es:
invitations:
accept: 'Aceptar la invitación'
reject: 'No, gracias'
new:
title: 'Crear un sitio'
submit: 'Crear sitio'
edit:
title: 'Editar %{site}'
submit: 'Guardar cambios'
form:
help:
name: 'El nombre de tu sitio que formará parte de la dirección (ejemplo.sutty.nl). Solo puede contener letras minúsculas, números y guiones.'
title: 'El título de tu sitio puede ser lo que quieras.'
description: 'La descripción del sitio, que saldrá en buscadores. Entre 50 y 160 caracteres.'
design: 'Elegí el diseño que va a tener tu sitio aquí. Podés cambiarlo luego. De tanto en tanto vamos sumando diseños nuevos.'
licencia: 'Todo lo que publicamos posee automáticamente derechos
de autore. Esto significa que nadie puede hacer uso de nuestras
obras sin permiso explícito. Con las licencias establecemos
condiciones bajo las que queremos compartir.'
privacidad: |
Las [políticas de
privacidad](https://sutty.nl/es/politica-de-privacidad/) y
[código de
convivencia](https://sutty.nl/es/codigo-de-convivencia/)
informan a les visitantes qué garantías de privacidad vas a
darles y con qué códigos se va a autogestionar su comunidad.
Sugerimos las mismas que las de Sutty. Una vez creado el
sitio, podrás editarlas como artículos.
deploys: |
Sutty te permite alojar tu sitio en distintos lugares al mismo
tiempo. Esta estrategia facilita que el sitio esté disponible
aun cuando algunos de los alojamientos no funcionen.
design:
title: 'Diseño'
actions: 'Información sobre este diseño'
url: 'Demostración'
license: 'Leer la licencia'
licencia:
title: 'Licencia del sitio y todo lo que publiques'
url: 'Leer la licencia'
privacidad:
title: Políticas de privacidad y código de convivencia
deploys:
title: '¿Dónde querés alojar tu sitio?'
fetch:
title: 'Actualizar el sitio'
help:
fetch: 'Todos los cambios en el sitio se guardan en un repositorio _git_. En git, se guarda la diferencia entre una versión anterior y la actual de todos los archivos y podemos explorar la historia de un proyecto. Además, podemos traer y enviar cambios con otros repositorios. En este caso, todos los sitios gestionados desde Sutty tienen una raíz común, que llamamos [esqueleto](https://0xacab.org/sutty/skel.sutty.nl). Cuando hacemos cambios en el esqueleto para mejorar los sitios, podés explorar los cambios aquí y aceptarlos.'
toc: 'Tabla de contenidos'
merge:
request: 'Incorporar los cambios en mi sitio'
success: 'Ya se incorporaron los cambios en el sitio, se aplicarán en la próxima compilación que hagas :)'
error: 'Hubo un error al incorporar los cambios en el sitio. Esto puede deberse a conflictos entre cambios que no se pueden resolver automáticamente. Hemos enviado un reporte del problema a les administradores de Sutty para que estén al tanto de la situación. ¡Lo sentimos! :('
message: 'Actualización del esqueleto'
footer:
powered_by: 'es desarrollada por'
i18n:

View file

@ -11,11 +11,13 @@ Rails.application.routes.draw do
# como un objeto válido
resources :invitadxs, only: [:create]
resources :sites, only: %i[index show],
constraints: { site_id: %r{[^/]+}, id: %r{[^/]+} } do
resources :sites, constraints: { site_id: %r{[^/]+}, id: %r{[^/]+} } do
get 'public/:type/:basename', to: 'sites#send_public_file'
# Gestionar actualizaciones del sitio
get 'pull', to: 'sites#fetch'
post 'pull', to: 'sites#merge'
# Gestionar usuaries
get 'usuaries/invite', to: 'usuaries#invite'
post 'usuaries/invite', to: 'usuaries#send_invitations'
@ -39,7 +41,8 @@ Rails.application.routes.draw do
# Compilar el sitio
post 'enqueue', to: 'sites#enqueue'
get 'build_log', to: 'sites#build_log'
post 'reorder_posts', to: 'sites#reorder_posts'
resources :stats, only: [:index]
end
end

View file

@ -1,8 +0,0 @@
# frozen_string_literal: true
env 'MAILTO', 'sysadmin@kefir.red'
job_type :bash, 'cd :path && ./bin/:task'
every 3.minutes do
bash 'jekyll_build_all'
end

View file

@ -1,32 +0,0 @@
# Be sure to restart your server when you modify this file.
# Your secret key is used for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies will become invalid!
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
# You can use `rails secret` to generate a secure secret key.
# Make sure the secrets in this file are kept private
# if you're sharing your code publicly.
# Shared secrets are available across all environments.
# shared:
# api_key: a1B2c3D4e5F6
# Environmental secrets are only available for that specific environment.
development:
secret_key_base: 18809d32b6661e906759535c3de06955d0eb551a83de5639f1ca4f0375bafd9653b818c4b881942e5cd5cc8da265617c9164fdb63b9f491d4481036c3d23e677
test:
secret_key_base: 95f26bd27ca88acb1f0d8d207fa5e60ae7dc56463774990c4acb938110af035690c929f844eaa97cc9a06b67f44631663f40c927b19c706dcccf629143550a2f
# Do not keep production secrets in the unencrypted secrets file.
# Instead, either read values from the environment.
# Or, use `bin/rails secrets:setup` to configure encrypted secrets
# and move the `production:` environment over there.
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

View file

@ -44,7 +44,10 @@ class CreateSitios < ActiveRecord::Migration[5.2]
usuarie ||= Usuarie.create(email: email,
password: SecureRandom.hex,
confirmed_at: Date.today)
site.usuaries << usuarie
sql = "insert into sites_usuaries (site_id, usuarie_id)
values (#{site.id}, #{usuarie.id});"
ActiveRecord::Base.connection.execute(sql)
end
invitadxs.each do |email|
@ -52,7 +55,9 @@ class CreateSitios < ActiveRecord::Migration[5.2]
usuarie ||= Usuarie.create(email: email,
password: SecureRandom.hex,
confirmed_at: Date.today)
site.invitades << usuarie
sql = "insert into invitades_sites (site_id, usuarie_id)
values (#{site.id}, #{usuarie.id});"
ActiveRecord::Base.connection.execute(sql)
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
# Los nombres de los sitios son únicos
class AddUniqueToSiteName < ActiveRecord::Migration[5.2]
def change
remove_index :sites, :name
add_index :sites, :name, unique: true
end
end

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
# Convertir los valores binarios de sqlite
class SqliteBoolean < ActiveRecord::Migration[5.2]
def up
return unless adapter_name == 'SQLite'
Usuarie.where("acepta_politicas_de_privacidad = 't'")
.update_all(acepta_politicas_de_privacidad: 1)
Usuarie.where("acepta_politicas_de_privacidad = 'f'")
.update_all(acepta_politicas_de_privacidad: 0)
change_column :usuaries, :acepta_politicas_de_privacidad, :boolean,
default: 0
end
def down
return unless adapter_name == 'SQLite'
Usuarie.where('acepta_politicas_de_privacidad = 1')
.update_all(acepta_politicas_de_privacidad: 't')
Usuarie.where('acepta_politicas_de_privacidad = 0')
.update_all(acepta_politicas_de_privacidad: 'f')
change_column :usuaries, :acepta_politicas_de_privacidad, :boolean,
default: 'f'
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
# Crea la tabla de diseños
class CreateDesigns < ActiveRecord::Migration[5.2]
def change
create_table :designs do |t|
t.timestamps
t.string :name, unique: true
t.text :description
t.string :gem, unique: true
t.string :url
t.string :license
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
# Agrega la columna de idioma a cada usuarie para que pueda ver el sitio
# y escribir artículos en su idioma.
class AddLangToUsuaries < ActiveRecord::Migration[5.2]
def change
add_column :usuaries, :lang, :string, default: 'es'
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
# Tabla de traducción de textos utilizada por Mobility
class CreateTextTranslations < ActiveRecord::Migration[5.2]
def change
create_table :mobility_text_translations do |t|
t.string :locale, null: false
t.string :key, null: false
t.text :value
t.references :translatable, polymorphic: true, index: false
t.timestamps null: false
end
add_index :mobility_text_translations, %i[translatable_id translatable_type locale key], unique: true, name: :index_mobility_text_translations_on_keys
add_index :mobility_text_translations, %i[translatable_id translatable_type key], name: :index_mobility_text_translations_on_translatable_attribute
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
# Tabla de traducción de cadenas usada por Mobility
class CreateStringTranslations < ActiveRecord::Migration[5.2]
def change
create_table :mobility_string_translations do |t|
t.string :locale, null: false
t.string :key, null: false
t.string :value
t.references :translatable, polymorphic: true, index: false
t.timestamps null: false
end
add_index :mobility_string_translations, %i[translatable_id translatable_type locale key], unique: true, name: :index_mobility_string_translations_on_keys
add_index :mobility_string_translations, %i[translatable_id translatable_type key], name: :index_mobility_string_translations_on_translatable_attribute
add_index :mobility_string_translations, %i[translatable_type key value locale], name: :index_mobility_string_translations_on_query_keys
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# Los sitios tienen un diseño
class AddDesignToSites < ActiveRecord::Migration[5.2]
def change
add_belongs_to :sites, :design, index: true
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# Algunos diseños están deshabilitados
class AddDisabledToDesigns < ActiveRecord::Migration[5.2]
def change
add_column :designs, :disabled, :boolean, default: false
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
# Crea la tabla de licencias
class CreateLicencias < ActiveRecord::Migration[5.2]
def change
create_table :licencias do |t|
t.timestamps
t.string :name, unique: true
t.text :description
t.text :deed
t.string :url
end
add_belongs_to :sites, :licencia, index: true
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
# Agrega íconos a las licencias
class AddIconsToLicenses < ActiveRecord::Migration[5.2]
def change
# XXX: Cambiar por ActiveStorage?
add_column :licencias, :icons, :string
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
# Crea la tabla de deploys posibles para un sitio
class CreateDeploys < ActiveRecord::Migration[5.2]
def change
create_table :deploys do |t|
t.timestamps
t.belongs_to :site, index: true
t.string :type, index: true
t.text :values
end
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
# Crea la tabla de estadísticas de compilación
class CreateBuildStats < ActiveRecord::Migration[5.2]
def change
create_table :build_stats do |t|
t.timestamps
t.belongs_to :deploy, index: true
t.integer :bytes
t.float :seconds
t.string :action, null: false
t.text :log
end
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# El status de un sitio
class AddStatusToSite < ActiveRecord::Migration[5.2]
def change
add_column :sites, :status, :string, default: 'waiting'
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# Agrega la descripción de un sitio
class AddDescriptionToSite < ActiveRecord::Migration[5.2]
def change
add_column :sites, :description, :text
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# Agrega el título al sitio
class AddTitleToSites < ActiveRecord::Migration[5.2]
def change
add_column :sites, :title, :string
end
end

View file

@ -12,7 +12,74 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20_190_706_002_615) do
ActiveRecord::Schema.define(version: 20_190_730_211_756) do
create_table 'build_stats', force: :cascade do |t|
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.integer 'deploy_id'
t.integer 'bytes'
t.float 'seconds'
t.string 'action', null: false
t.text 'log'
t.index ['deploy_id'], name: 'index_build_stats_on_deploy_id'
end
create_table 'deploys', force: :cascade do |t|
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.integer 'site_id'
t.string 'type'
t.text 'values'
t.index ['site_id'], name: 'index_deploys_on_site_id'
t.index ['type'], name: 'index_deploys_on_type'
end
create_table 'designs', force: :cascade do |t|
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.string 'name'
t.text 'description'
t.string 'gem'
t.string 'url'
t.string 'license'
t.boolean 'disabled', default: false
end
create_table 'licencias', force: :cascade do |t|
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.string 'name'
t.text 'description'
t.text 'deed'
t.string 'url'
t.string 'icons'
end
create_table 'mobility_string_translations', force: :cascade do |t|
t.string 'locale', null: false
t.string 'key', null: false
t.string 'value'
t.string 'translatable_type'
t.integer 'translatable_id'
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.index %w[translatable_id translatable_type key], name: 'index_mobility_string_translations_on_translatable_attribute'
t.index %w[translatable_id translatable_type locale key], name: 'index_mobility_string_translations_on_keys', unique: true
t.index %w[translatable_type key value locale], name: 'index_mobility_string_translations_on_query_keys'
end
create_table 'mobility_text_translations', force: :cascade do |t|
t.string 'locale', null: false
t.string 'key', null: false
t.text 'value'
t.string 'translatable_type'
t.integer 'translatable_id'
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.index %w[translatable_id translatable_type key], name: 'index_mobility_text_translations_on_translatable_attribute'
t.index %w[translatable_id translatable_type locale key], name: 'index_mobility_text_translations_on_keys', unique: true
end
create_table 'roles', force: :cascade do |t|
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
@ -29,7 +96,14 @@ ActiveRecord::Schema.define(version: 20_190_706_002_615) do
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.string 'name'
t.index ['name'], name: 'index_sites_on_name'
t.integer 'design_id'
t.integer 'licencia_id'
t.string 'status', default: 'waiting'
t.text 'description'
t.string 'title'
t.index ['design_id'], name: 'index_sites_on_design_id'
t.index ['licencia_id'], name: 'index_sites_on_licencia_id'
t.index ['name'], name: 'index_sites_on_name', unique: true
end
create_table 'usuaries', force: :cascade do |t|
@ -56,6 +130,7 @@ ActiveRecord::Schema.define(version: 20_190_706_002_615) do
t.string 'invited_by_type'
t.integer 'invited_by_id'
t.integer 'invitations_count', default: 0
t.string 'lang', default: 'es'
t.index ['confirmation_token'], name: 'index_usuaries_on_confirmation_token', unique: true
t.index ['email'], name: 'index_usuaries_on_email', unique: true
t.index ['invitation_token'], name: 'index_usuaries_on_invitation_token', unique: true

Some files were not shown because too many files have changed in this diff Show more