gestionar usuaries de un sitio

This commit is contained in:
f 2019-07-05 20:55:59 -03:00
parent a125b388bb
commit 64fcf61c67
No known key found for this signature in database
GPG key ID: 2AE5A13E321F953D
22 changed files with 348 additions and 5 deletions

View file

@ -19,6 +19,7 @@ Metrics/AbcSize:
- 'app/controllers/posts_controller.rb'
- 'app/controllers/invitadxs_controller.rb'
- 'app/controllers/i18n_controller.rb'
- 'app/controllers/usuaries_controller.rb'
Metrics/MethodLength:
Exclude:

View file

@ -41,6 +41,7 @@ gem 'carrierwave-i18n'
gem 'commonmarker'
gem 'devise'
gem 'devise-i18n'
gem 'devise_invitable'
gem 'email_address'
gem 'exception_notification'
gem 'font-awesome-rails'

View file

@ -106,6 +106,9 @@ GEM
warden (~> 1.2.3)
devise-i18n (1.8.0)
devise (>= 4.6)
devise_invitable (2.0.1)
actionmailer (>= 5.0)
devise (>= 4.6)
dotenv (2.7.2)
dotenv-rails (2.7.2)
dotenv (= 2.7.2)
@ -363,6 +366,7 @@ DEPENDENCIES
commonmarker
devise
devise-i18n
devise_invitable
dotenv-rails
ed25519
email_address

View file

@ -51,4 +51,59 @@ class UsuariesController < ApplicationController
redirect_to site_usuaries_path
end
# Poder invitar
def invite
@site = find_site
site_usuarie = SiteUsuarie.new(@site, current_usuarie)
authorize site_usuarie
@policy = policy(site_usuarie)
end
# Envía las invitaciones
def send_invitations
@site = find_site
authorize SiteUsuarie.new(@site, current_usuarie)
# Enviar la invitación si es necesario y agregar al sitio
#
# TODO: Pedir consentimiento para agregar a un sitio!
invitaciones.each do |invitacion|
# Si la cuenta no existe, envía una invitación por correo
usuarie = Usuarie.invite! email: invitacion.address
# No invitar al sitio si ya estaba en la lista!
unless @site.invitade?(usuarie) || @site.usuarie?(usuarie)
@site.send(invited_as) << usuarie
end
end
redirect_to site_usuaries_path(@site)
end
private
# Traer todas las invitaciones que al menos tengan usuarie y dominio
def invitaciones
# XXX: Podríamos usar EmailAddress pero hace chequeos más lentos
i = params[:invitaciones].split("\n").map do |m|
Mail::Address.new m
rescue Mail::Field::IncompleteParseError
nil
end.compact
i.select do |m|
m.local && m.domain
end
end
# El tipo de invitación que tenemos que enviar, si alguien mandó
# cualquier cosa, usamos el privilegio menor.
def invited_as
invited_as = params[:invited_as].try(:to_sym)
invited_as = :invitades unless %i[usuaries invitades].include?(invited_as)
invited_as
end
end

View file

@ -2,7 +2,7 @@
# Usuarie de la plataforma
class Usuarie < ApplicationRecord
devise :database_authenticatable,
devise :invitable, :database_authenticatable,
:recoverable, :rememberable, :validatable,
:confirmable, :lockable, :registerable

View file

@ -27,6 +27,14 @@ class SiteUsuariePolicy
usuarie?
end
def invite?
usuarie?
end
def send_invitations?
usuarie?
end
private
def usuarie?

View file

@ -0,0 +1,16 @@
.row.align-items-center.justify-content-center.full-height
.col-md-6.align-self-center
%h2= t 'devise.invitations.edit.header'
= form_for(resource, as: resource_name, url: invitation_path(resource_name), html: { method: :put }) do |f|
= render 'devise/shared/error_messages', resource: resource
= f.hidden_field :invitation_token, readonly: true
- if f.object.class.require_password_on_accepting
.form-group
= f.label :password
= f.password_field :password, class: 'form-control'
.form-group
= f.label :password_confirmation
= f.password_field :password_confirmation, class: 'form-control'
.actions
= f.submit t('devise.invitations.edit.submit_button'),
class: 'btn btn-lg btn-primary btn-block'

View file

@ -0,0 +1,12 @@
.row.align-items-center.justify-content-center.full-height
.col-md-6.align-self-center
%h2= t 'devise.invitations.new.header'
= form_for(resource, as: resource_name, url: invitation_path(resource_name), html: { method: :post }) do |f|
= render 'devise/shared/error_messages', resource: resource
- resource.class.invite_key_fields.each do |field|
.form-group
= f.label field
= f.text_field field, class: 'form-control'
.actions
= f.submit t('devise.invitations.new.submit_button'),
class: 'btn btn-lg btn-primary btn-block'

View file

@ -0,0 +1,6 @@
%p= t("devise.mailer.invitation_instructions.hello", email: @resource.email)
%p= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url)
%p= link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, invitation_token: @token)
- if @resource.invitation_due_at
%p= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format'))
%p= t("devise.mailer.invitation_instructions.ignore")

View file

@ -0,0 +1,6 @@
= t("devise.mailer.invitation_instructions.hello", email: @resource.email)
= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url)
= accept_invitation_url(@resource, invitation_token: @token)
- if @resource.invitation_due_at
= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format'))
= t("devise.mailer.invitation_instructions.ignore")

View file

@ -1 +0,0 @@
<%= yield %>

View file

@ -14,7 +14,15 @@
.col
-# Una tabla de usuaries y otra de invitades, con acciones
- %i[usuaries invitades].each do |u|
%h2= t(".#{u.to_s}")
%h2
= t(".#{u.to_s}")
.btn-group{role: 'group', 'aria-label': t('.actions')}
- if @policy.invite?
= link_to t('.invite.text'),
site_usuaries_invite_path(@site, invite_as: u.to_s),
class: 'btn btn-success',
data: { toggle: 'tooltip' },
title: t('.help.invite', invite_as: u.to_s)
%p= t(".help.#{u.to_s}")
%table.table.table-striped.table-condensed
%tbody
@ -23,7 +31,7 @@
-# TODO: avatares
%td= cuenta.email
%td
.btn-group{role: 'group', 'aria-label': t('.actions')}
.btn-group{role: 'group', 'aria-label': t('.individual_actions')}
- if @policy.demote? && @site.usuarie?(cuenta)
= link_to t('.demote.text'),
site_usuarie_demote_path(@site, cuenta),

View file

@ -0,0 +1,26 @@
- invite_as = t("usuaries.invite_as.#{params[:invite_as]}")
.row
.col
= render 'layouts/breadcrumb',
crumbs: [ link_to(t('sites.index'), sites_path),
@site.name,
link_to(t('posts.index'),
site_usuaries_path(@site)),
t('.title', invite_as: invite_as) ]
= render 'layouts/help', help: t('help.breadcrumbs')
.row
.col
%h1= t('.title', invite_as: invite_as)
.row
.col
= form_with url: site_usuaries_invite_path(@site) do |f|
= f.hidden_field :invite_as, value: params[:invite_as]
.form-group
= f.label :invitaciones do
= t('.invitaciones')
%small.text-muted.form-text= t('.help.invitaciones',
invite_as: invite_as)
= f.text_area :invitaciones, class: 'form-control'
.form-group
= f.submit t('.submit'), class: 'btn btn-success'

View file

@ -136,6 +136,58 @@ Devise.setup do |config|
# Send a notification email when the user's password is changed.
config.send_password_change_notification = true
# ==> Configuration for :invitable
# The period the generated invitation token is valid. After this
# period, the invited resource won't be able to accept the invitation.
# When invite_for is 0 (the default), the invitation won't expire.
config.invite_for = 2.weeks
# Number of invitations users can send.
# - If invitation_limit is nil, there is no limit for invitations,
# users can send unlimited invitations, invitation_limit column is not
# used.
# - If invitation_limit is 0, users can't send invitations by default.
# - If invitation_limit n > 0, users can send n invitations.
# You can change invitation_limit column for some users so they can
# send more or less invitations, even with global invitation_limit = 0
# Default: nil
config.invitation_limit = 0
# The key to be used to check existing users when sending an
# invitation and the regexp used to test it when validate_on_invite is
# not set.
# config.invite_key = { email: /\A[^@]+@[^@]+\z/ }
# config.invite_key = { email: /\A[^@]+@[^@]+\z/, username: nil }
# Ensure that invited record is valid.
# The invitation won't be sent if this check fails.
# Default: false
# config.validate_on_invite = true
# Resend invitation if user with invited status is invited again
# Default: true
# config.resend_invitation = false
# The class name of the inviting model. If this is nil,
# the #invited_by association is declared to be polymorphic.
# Default: nil
# config.invited_by_class_name = 'User'
# The foreign key to the inviting model (if invited_by_class_name is
# set)
# Default: :invited_by_id
# config.invited_by_foreign_key = :invited_by_id
# The column name used for counter_cache column. If this is nil,
# the #invited_by association is declared without counter_cache.
# Default: nil
# config.invited_by_counter_cache = :invitations_count
# Auto-login after the user accepts the invite. If this is false,
# the user will need to manually log in after accepting the invite.
# Default: true
# config.allow_insecure_sign_in_after_accept = false
# ==> Configuration for :confirmable
# A period that the user is allowed to access the website even without
# confirming their account. For instance, if set to 2.days, the user

View file

@ -0,0 +1,31 @@
en:
devise:
failure:
invited: "You have a pending invitation, accept it to finish creating your account."
invitations:
send_instructions: "An invitation email has been sent to %{email}."
invitation_token_invalid: "The invitation token provided is not valid!"
updated: "Your password was set successfully. You are now signed in."
updated_not_active: "Your password was set successfully."
no_invitations_remaining: "No invitations remaining"
invitation_removed: "Your invitation was removed."
new:
header: "Send invitation"
submit_button: "Send an invitation"
edit:
header: "Set your password"
submit_button: "Set my password"
mailer:
invitation_instructions:
subject: "Invitation instructions"
hello: "Hello %{email}"
someone_invited_you: "Someone has invited you to %{url}, you can accept it through the link below."
accept: "Accept invitation"
accept_until: "This invitation will be due in %{due_date}."
ignore: "If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password."
time:
formats:
devise:
mailer:
invitation_instructions:
accept_until_format: "%B %d, %Y %I:%M %p"

View file

@ -0,0 +1,31 @@
es:
devise:
failure:
invited: "Tenés una invitación pendiente, aceptala para terminar de crear tu cuenta."
invitations:
send_instructions: "Hemos enviado una invitación a %{email}."
invitation_token_invalid: "¡El código de invitación no es válido!"
updated: "Configuraste tu contraseña correctamente, a partir de ahora podés ingresar."
updated_not_active: "Configuraste tu contraseña correctamente."
no_invitations_remaining: "No te quedan invitaciones"
invitation_removed: "La invitación ha sido eliminada."
new:
header: "Invitar a alguien"
submit_button: "Enviar la invitación"
edit:
header: "Configura tu contraseña"
submit_button: "Configurar mi contraseña"
mailer:
invitation_instructions:
subject: "Invitación"
hello: "Hola %{email}"
someone_invited_you: "Alguien te ha invitado a colaborar en %{url}, podés aceptar la invitación con el enlace a continuación."
accept: "Aceptar la invitación"
accept_until: "La invitación vencerá en %{due_date}."
ignore: "Si no querés aceptar la invitación, por favor ignora este correo. Tu cuenta no será creada hasta que aceptes la invitación y configures una contraseña."
time:
formats:
devise:
mailer:
invitation_instructions:
accept_until_format: "%d/%m/%Y a las %H:%M"

View file

@ -259,6 +259,9 @@ en:
destroy: Delete
confirm_destroy: Are you sure?
usuaries:
invite_as:
usuaries: users
invitades: guests
index:
help:
self: Self-manage who has access to this site
@ -267,6 +270,7 @@ en:
invitades: 'Guests can only create new posts and edit those authored by them. Any change needs review and approval by a user.'
demote: Removes privileges for this user
promote: Gives privileges to this guest
invite: 'Invites %{invite_as} to this site'
title: Users and Guests
usuaries: Users
invitades: Guests
@ -279,3 +283,11 @@ en:
promote:
text: 'Convert to user'
confirm: 'Convert to user? They will gain full access to self-manage this site.'
invite:
text: 'Invite'
invite:
title: 'Invite %{invite_as}'
help:
invitaciones: 'You can invite new %{invite_as} by entering an email address per line. They will receive an email with your invitation and decide if they want to accept or not.'
invitaciones: Send invitations to these addresses
submit: Send invitations

View file

@ -266,6 +266,9 @@ es:
destroy: Borrar
confirm_destroy: ¿Estás segurx?
usuaries:
invite_as:
usuaries: usuaries
invitades: invitades
index:
help:
self: Gestionar quiénes tienen acceso a este sitio
@ -274,6 +277,7 @@ es:
invitades: 'Les invitades sólo pueden crear artículos nuevos y modificar los que cargaron. Todos los cambios que hagan necesitan la revisión y aprobación de une usuarie.'
demote: Quita privilegios a este usuarie
promote: Otorga privilegios a este invitade
invite: 'Invita %{invite_as} a colaborar en este sitio'
title: Usuaries e invitades
usuaries: Usuaries
invitades: Invitades
@ -286,3 +290,11 @@ es:
promote:
text: 'Convertir en usuarie'
confirm: '¿Convertir en usuarie? Ganará acceso a la gestión completa del sitio.'
invite:
text: 'Invitar'
invite:
title: 'Invitar %{invite_as}'
help:
invitaciones: 'Invita nueves %{invite_as} ingresando una dirección de correo electrónico por línea. Recibirán un correo con tu invitación y podrán aceptarla o no.'
invitaciones: Invitar a estas direcciones
submit: Enviar invitaciones

View file

@ -17,6 +17,8 @@ Rails.application.routes.draw do
get 'public/:type/:basename', to: 'sites#send_public_file'
# Gestionar usuaries
get 'usuaries/invite', to: 'usuaries#invite'
post 'usuaries/invite', to: 'usuaries#send_invitations'
resources :usuaries do
patch 'demote', to: 'usuaries#demote'
patch 'promote', to: 'usuaries#promote'

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
# Agregar gestión de invitaciones a usuaries
class DeviseInvitableAddToUsuaries < ActiveRecord::Migration[5.2]
def up
change_table :usuaries do |t|
t.string :invitation_token
t.datetime :invitation_created_at
t.datetime :invitation_sent_at
t.datetime :invitation_accepted_at
t.integer :invitation_limit
t.references :invited_by, polymorphic: true
t.integer :invitations_count, default: 0
t.index :invitations_count
t.index :invitation_token, unique: true # for invitable
t.index :invited_by_id
end
end
def down
change_table :usuaries do |t|
t.remove_references :invited_by, polymorphic: true
t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at
end
end
end

View file

@ -12,7 +12,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20_190_705_195_758) do
ActiveRecord::Schema.define(version: 20_190_705_215_536) do
create_table 'invitades_sites', id: false, force: :cascade do |t|
t.integer 'site_id'
t.integer 'usuarie_id'
@ -48,8 +48,20 @@ ActiveRecord::Schema.define(version: 20_190_705_195_758) do
t.string 'unlock_token'
t.datetime 'locked_at'
t.boolean 'acepta_politicas_de_privacidad', default: false
t.string 'invitation_token'
t.datetime 'invitation_created_at'
t.datetime 'invitation_sent_at'
t.datetime 'invitation_accepted_at'
t.integer 'invitation_limit'
t.string 'invited_by_type'
t.integer 'invited_by_id'
t.integer 'invitations_count', default: 0
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
t.index ['invitations_count'], name: 'index_usuaries_on_invitations_count'
t.index ['invited_by_id'], name: 'index_usuaries_on_invited_by_id'
t.index %w[invited_by_type invited_by_id], name: 'index_usuaries_on_invited_by_type_and_invited_by_id'
t.index ['reset_password_token'], name: 'index_usuaries_on_reset_password_token', unique: true
t.index ['unlock_token'], name: 'index_usuaries_on_unlock_token', unique: true
end

View file

@ -43,3 +43,26 @@ está en su lista de invitade. Lo mismo para usuaries.
Les usuaries pueden bloquear invitades y a otres usuaries, y sumar
usuaries e invitades a su sitio (via correo de invitación).
## Invitaciones a sitios
### Enviar invitación
Desde la gestión del sitio se puede invitar a nueves usuaries e
invitades. Se les envía un correo (y cuando tengamos sistema de
notificaciones, una notificación) donde pueden confirmar su
participación.
Si no tienen cuenta, tienen que registrarse completando los datos en el
momento, sino se pueden loguear normalmente.
Si ya están logueades, se acepta la invitación inmediatamente.
### Invitades desde la web
Al publicar la URL de invitación, les invitades se puedan registrar
por su cuenta. Solo deben completar sus datos (correo y contraseña) y
reciben un correo de confirmación.
En la configuración del sitio hay que distinguir entre invitades por
invitación o por registro automático.