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:
@@ -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
|
||||
|
||||
35
lib/web/controllers/export_controller.ex
Normal file
35
lib/web/controllers/export_controller.ex
Normal 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
154
lib/web/templates/export/event_participants.html.heex
Normal file
154
lib/web/templates/export/event_participants.html.heex
Normal 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>
|
||||
17
lib/web/views/export_view.ex
Normal file
17
lib/web/views/export_view.ex
Normal 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
|
||||
Reference in New Issue
Block a user