Export participants to different formats

* CSV
* PDF (requires Python dependency `weasyprint`)
* ODS (requires Python dependency `pyexcel_ods3`)

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2021-10-04 18:59:41 +02:00
parent 5dd24e1c9e
commit 0c667b13ae
121 changed files with 10817 additions and 6872 deletions

View File

@@ -53,28 +53,30 @@ defmodule Mobilizon.Web.Auth.Guardian do
end
end
@spec on_verify(any(), any(), any()) :: {:ok, any()}
@spec on_verify(any(), any(), any()) :: {:ok, map()} | {:error, :token_not_found}
def on_verify(claims, token, _options) do
with {:ok, _} <- Guardian.DB.on_verify(claims, token) do
{:ok, claims}
end
end
@spec on_revoke(any(), any(), any()) :: {:ok, any()}
@spec on_revoke(any(), any(), any()) :: {:ok, map()} | {:error, :could_not_revoke_token}
def on_revoke(claims, token, _options) do
with {:ok, _} <- Guardian.DB.on_revoke(claims, token) do
{:ok, claims}
end
end
@spec on_refresh({any(), any()}, {any(), any()}, any()) :: {:ok, {any(), any()}, {any(), any()}}
@spec on_refresh({any(), any()}, {any(), any()}, any()) ::
{:ok, {String.t(), map()}, {String.t(), map()}} | {:error, any()}
def on_refresh({old_token, old_claims}, {new_token, new_claims}, _options) do
with {:ok, _, _} <- Guardian.DB.on_refresh({old_token, old_claims}, {new_token, new_claims}) do
{:ok, {old_token, old_claims}, {new_token, new_claims}}
end
end
@spec on_exchange(any(), any(), any()) :: {:ok, {any(), any()}, {any(), any()}}
@spec on_exchange(any(), any(), any()) ::
{:ok, {String.t(), map()}, {String.t(), map()}} | {:error, any()}
def on_exchange(old_stuff, new_stuff, options), do: on_refresh(old_stuff, new_stuff, options)
# def build_claims(claims, _resource, opts) do

View File

@@ -0,0 +1,35 @@
defmodule Mobilizon.Web.ExportController do
@moduledoc """
Controller to serve exported files
"""
use Mobilizon.Web, :controller
plug(:put_layout, false)
action_fallback(Mobilizon.Web.FallbackController)
alias Mobilizon.Export
import Mobilizon.Service.Export.Participants.Common, only: [enabled_formats: 0]
import Mobilizon.Web.Gettext, only: [dgettext: 3]
# sobelow_skip ["Traversal.SendDownload"]
@spec export(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t()
def export(conn, %{"format" => format, "file" => file}) do
if format in enabled_formats() do
case Export.get_export(file, "event_participants", format) do
%Export{file_name: file_name, file_path: file_path} ->
local_path = "uploads/exports/#{format}/#{file_path}"
# We're using encode: false to disable escaping the filename with URI.encode_www_form/1
# but it may introduce an security issue if the event title wasn't properly sanitized
# https://github.com/phoenixframework/phoenix/pull/3344
# https://owasp.org/www-community/attacks/HTTP_Response_Splitting
send_download(conn, {:file, local_path}, filename: file_name, encode: false)
nil ->
{:error, :not_found}
end
else
{:error,
dgettext("errors", "Export to format %{format} is not enabled on this instance",
format: format
)}
end
end
end

View File

@@ -77,17 +77,6 @@ defmodule Mobilizon.Web.Endpoint do
plug(Plug.MethodOverride)
plug(Plug.Head)
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug(
Plug.Session,
store: :cookie,
key: "_mobilizon_key",
signing_salt: "F9CCTF22"
)
plug(Mobilizon.Web.Router)
@spec websocket_url :: String.t()

View File

@@ -61,6 +61,9 @@ defmodule Mobilizon.Web.Router do
plug(:accepts, ["atom", "ics", "html"])
end
pipeline :exports do
end
pipeline :browser do
plug(Plug.Static, at: "/", from: "priv/static")
@@ -78,6 +81,11 @@ defmodule Mobilizon.Web.Router do
pipeline :remote_media do
end
scope "/exports", Mobilizon.Web do
pipe_through(:browser)
get("/:format/:file", ExportController, :export)
end
scope "/api" do
pipe_through(:graphql)

View File

@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style>
table {
border: 1px solid #bdbdbd;
border-collapse: collapse;
width: 100%; }
th,
td,
table caption {
padding: 0.75rem;
text-align: left;
text-align: start;
/* 1 */ }
[dir="rtl"] th,
[dir="rtl"] td,
[dir="rtl"] table caption {
text-align: right;
text-align: start;
/* 1 */ }
td {
vertical-align: text-top;
}
th {
vertical-align: bottom; }
th[scope="col"] {
background-color: #024488;
color: #fff; }
dl {
display: flex;
flex-flow: row wrap;
}
dt {
flex-basis: 20%;
padding: 2px 4px;
text-align: right;
}
dd {
flex-basis: 70%;
flex-grow: 1;
margin: 0;
padding: 2px 4px;
}
dl dt {
font-weight: bold; }
dl dd + dt {
margin-top: 0.5em; }
dl dt + dd,
dl dd + dd {
margin-top: 0.25em; }
tr:nth-child(even) {
background-color: rgba(255, 255, 255, 0.25);
}
tr:nth-child(odd) {
background-color: rgba(0, 0, 0, 0.05);
}
th {
background-color: rgba(0, 0, 0, 0.5);
}
@media print {
body {
/*font-size: 6pt;*/
color: #000;
background-color: #fff;
background-image: none;
}
body,
main {
margin: 0;
padding: 0;
background-color: #fff;
border: none;
}
table {
page-break-inside: avoid;
}
div {
overflow: visible;
}
th {
color: #000;
background-color: #fff;
border-bottom: 1pt solid #000;
}
tr {
border-top: 1pt solid #000;
}
}
@media print and (max-width: 5in) {
caption {
color: #000;
background-color: #fff;
border-bottom: 1pt solid #000;
}
table {
page-break-inside: auto;
}
tr {
page-break-inside: avoid;
}
}
</style>
<%# <title><%= gettext("Participants for %{event}") ></title> %>
</head>
<body>
<h1><%= gettext("Participants for %{event}", event: @event.title) %></h1>
<dl>
<dt><%= gettext("Begins on") %></dt>
<dd><%= datetime_to_string(@event.begins_on, @locale, :long) %></dd>
<%= if @event.ends_on do %>
<dt><%= gettext("Ends on") %></dt>
<dd><%= datetime_to_string(@event.ends_on, @locale, :long) %></dd>
<% end %>
<%= if @event.physical_address do %>
<dt><%= gettext("Location") %></dt>
<dd><%= render_address(@event.physical_address) %></dd>
<% end %>
<dt><%= gettext("Number of participants") %></dt>
<dd><%= @event.participant_stats.participant + @event.participant_stats.moderator + @event.participant_stats.administrator + @event.participant_stats.creator %></dd>
</dl>
<table>
<thead>
<tr>
<%= for column <- @columns do %>
<th><%= column %></th>
<% end %>
</tr>
</thead>
<tbody>
<%= for line <- @data do %>
<tr>
<%= for cell <- line do %>
<td><%= cell %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</body>
</html>

View File

@@ -0,0 +1,17 @@
defmodule Mobilizon.Web.ExportView do
use Mobilizon.Web, :view
alias Mobilizon.Service.Address
alias Mobilizon.Service.DateTime, as: DateTimeRenderer
import Mobilizon.Web.Gettext
defdelegate datetime_to_string(datetime, locale \\ "en", format \\ :medium),
to: DateTimeRenderer
defdelegate datetime_to_time_string(datetime, locale \\ "en", format \\ :short),
to: DateTimeRenderer
defdelegate datetime_tz_convert(datetime, timezone), to: DateTimeRenderer
defdelegate datetime_relative(datetime, locale \\ "en"), to: DateTimeRenderer
defdelegate render_address(address), to: Address
end