-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feats - download e compilação de relatórios #145
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ótimo! essa era realmente a ideia da funcionalidade! mandou muito bem! apenas sugeri algumas mudanças por questões de padrão do projeto, porém gostei bastante do trabalho.
creio que falta apenas trocar alguns “lorem ipsum” dos templates para dados reais, certo?
defp fetch_and_preload_relatorio(relatorio_id) do | ||
relatorio = Repository.fetch_relatorio_pesquisa_by_id(relatorio_id) | ||
Replica.preload(relatorio, pesquisador: [:usuario, :linha_pesquisa, :orientador]) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preload já deveria ser feito dentro do Repository
. o controller deveria lidar apenas com chamadas à funções do contexto interno/lógica de negócio e gerenciar as respostas e lógica da camanda web. schemas e entidades não deveriam ser expostas diretamente dentro de um controller
defp enviar_pdf_response(conn, id, pdf_file) do | ||
conn | ||
|> put_resp_content_type("application/pdf") | ||
|> put_resp_header("content-disposition", "attachment; filename=relatorios-#{id}.pdf") | ||
|> send_resp(200, pdf_file) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
creio que seja mais idiomático usar a função send_download/3
do módulo Phoenix.Controller
, que já é automaticamente importado
def compilar_relatorios(conn, params) do | ||
campos_formularios = Map.keys(params) | ||
relatorios_selecionados = Enum.filter(campos_formularios, fn id -> id != "_csrf_token" end) | ||
relatorios = Enum.map(relatorios_selecionados, &fetch_and_preload_relatorio/1) | ||
htmls = Enum.map(relatorios, &RelatorioHTML.content/1) | ||
pdfs = Enum.reduce(htmls, [], fn html, acc -> [{:html, html} | acc] end) | ||
{:ok, pdf_compilado} = ChromicPDF.print_to_pdf(pdfs) | ||
zip_file = criar_zip_file(pdf_compilado) | ||
enviar_zip_response(conn, elem(zip_file, 1)) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
muito bom! único comentário é que creio que tenha muita lógica acontecendo aqui, acho interessante criar um módulo que lida exatamente com compilação de relatórios e criação dos PDFs, o que acha? a ideia seria isolar num módulo próprio e o controller apenas chamaria a função de alto nível.
defp criar_zip_file(pdf) do | ||
{:ok, zip_file} = | ||
:zip.create( | ||
"relatorios_compilados.zip", | ||
[{~c"relatorios_compilados.pdf", Base.decode64!(pdf)}], | ||
[:memory] | ||
) | ||
|
||
zip_file | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mandou bem!
mesma ideia aqui, podemos isolar no mesmo módulo de compilação de PDF
meses = %{ | ||
1 => "Janeiro", | ||
2 => "Fevereiro", | ||
3 => "Março", | ||
4 => "Abril", | ||
5 => "Maio", | ||
6 => "Junho", | ||
7 => "Julho", | ||
8 => "Agosto", | ||
9 => "Setembro", | ||
10 => "Outubro", | ||
11 => "Novembro", | ||
12 => "Dezembro" | ||
} | ||
|
||
meses[mes] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
não precisa definir esse mapa, você pode usar diretamente a função Timex.lformat/2
. mais documentação aqui. Assim você consegue mandar a data inteira pra função e retornar apenas o nome do mês com o formato {Mfull}
def compilar_relatorios(conn, params) do | ||
campos_formularios = Map.keys(params) | ||
relatorios_selecionados = Enum.filter(campos_formularios, fn id -> id != "_csrf_token" end) | ||
zip_file = RelatorioCompiler.compilar_relatorios(relatorios_selecionados) | ||
enviar_zip_response(conn, zip_file) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
creio que fique mais claro dessa forma, pois:
- você ativa diz que não quer o campo
_csrf_token
, que é único no mapa - pega os relatorios selecionados
- compila os relatórios num zip, e devolve o nome (para não repetir uma constante) e o conteúdo do mesmo em binário
- se isso tudo der certo, dentro do
with/1/
, ai então vc manda o download numa response de sucesso
def compilar_relatorios(conn, params) do | |
campos_formularios = Map.keys(params) | |
relatorios_selecionados = Enum.filter(campos_formularios, fn id -> id != "_csrf_token" end) | |
zip_file = RelatorioCompiler.compilar_relatorios(relatorios_selecionados) | |
enviar_zip_response(conn, zip_file) | |
end | |
def compilar_relatorios(conn, params) do | |
campos_formularios = Map.delete(params, “_csrf_token") | |
relatorios_selecionados = Map.keys(campos_formularios) | |
RelatorioCompiler.compilar_relatorios(relatorios_selecionados, fn zip_filename, zip_content -> | |
conn | |
|> put_status(:ok) | |
|> send_download({:binary, zip_content}, filename: zip_name) | |
end) | |
end |
defp enviar_pdf_response(conn, id, pdf_file) do | ||
send_download( | ||
conn, | ||
{:binary, pdf_file}, | ||
content_type: "application/pdf", | ||
filename: "relatorios-#{id}.pdf" | ||
) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
não vejo necessidade de encapsular essa lógica em outra função
def enviar_zip_response(conn, zip_file) do | ||
send_download( | ||
conn, | ||
{:binary, zip_file}, | ||
content_type: "application/zip", | ||
filename: "relatorios-compilados.zip" | ||
) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
não vejo necessidade de encapsular essa lógica em outra função
def download_pdf(conn, %{"id" => id}) do | ||
pdf_binary = RelatorioCompiler.gerar_pdf(id) | ||
enviar_pdf_response(conn, id, pdf_binary) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
creio que dessa forma fique mais clara a intenção e a possibilidade de retornar um erro
def download_pdf(conn, %{"id" => id}) do | |
pdf_binary = RelatorioCompiler.gerar_pdf(id) | |
enviar_pdf_response(conn, id, pdf_binary) | |
end | |
def download_pdf(conn, %{"id" => id}) do | |
with {:ok, pdf_filename, pdf_binary} <- RelatorioCompiler.gerar_pdf(id) do | |
conn | |
|> put_status(:ok) | |
|> send_download({:binary, pdf_binary}, filename: pdf_filename) | |
end | |
end |
defp criar_zip_file(pdf) do | ||
with {:ok, zip_file} <- | ||
:zip.create( | ||
"relatorios_compilados.zip", | ||
[{~c"relatorios_compilados.pdf", Base.decode64!(pdf)}], | ||
[:memory] | ||
) do | ||
zip_file | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
acho que o ideal seria ficar assim:
defp criar_zip_file(pdf) do | |
with {:ok, zip_file} <- | |
:zip.create( | |
"relatorios_compilados.zip", | |
[{~c"relatorios_compilados.pdf", Base.decode64!(pdf)}], | |
[:memory] | |
) do | |
zip_file | |
end | |
end | |
@zip_filename “relatorio_final.zip" | |
@zipped_filename “relatorio_final.pdf" | |
defp criar_zip_file(pdf) do | |
with {:ok, pdf} <- Base.decode64(pdf), | |
files = [{String.to_charlist(@zipped_filename), pdf}], | |
{:ok, {_, binary}} <- :zip.create(@zip_filename, files [:memory]) do | |
{:ok, @zip_filename, binary} | |
end | |
end |
dessa forma, conseguimos deixar a ideia explícita numa cadeia/sequência de operações atômicas que podem falhar ou não:
- extraimos os nomes dos arquivos para constantes
- decodificamos o base64 do pdf, caso dê erro, já retornamos
- criamos a lista de arquivos a serem incluídos nos zip
- fazemos a operação de criação do zip em memória, de forma atômica
- retornamos os dados necessários apra enviar o download no controller
relatorio = find_relatorio(relatorio_id) | ||
|
||
with {:ok, pdf} <- | ||
[content: RelatorioHTML.content(relatorio), size: :a4] | ||
|> ChromicPDF.Template.source_and_options() | ||
|> ChromicPDF.print_to_pdf() do | ||
Base.decode64(pdf) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
o relatório pode não existir por algum erro ou inconsistência do sistema, logo, é recomendável que verifiquemos isso antes de quebrar em runtime
relatorio = find_relatorio(relatorio_id) | |
with {:ok, pdf} <- | |
[content: RelatorioHTML.content(relatorio), size: :a4] | |
|> ChromicPDF.Template.source_and_options() | |
|> ChromicPDF.print_to_pdf() do | |
Base.decode64(pdf) | |
end | |
if relatorio = find_relatorio(relatorio_id) do | |
with {:ok, pdf} <- | |
[content: RelatorioHTML.content(relatorio), size: :a4] | |
|> ChromicPDF.Template.source_and_options() | |
|> ChromicPDF.print_to_pdf() do | |
Base.decode64(pdf) | |
end | |
else | |
{:error, :pdf_not_found} | |
end |
def compilar_relatorios(relatorios_selecionados) do | ||
relatorios = Enum.map(relatorios_selecionados, &find_relatorio/1) | ||
htmls = Enum.map(relatorios, &RelatorioHTML.content/1) | ||
pdfs = Enum.reduce(htmls, [], fn html, acc -> [{:html, html} | acc] end) | ||
|
||
with {:ok, pdf_compilado} <- ChromicPDF.print_to_pdf(pdfs), | ||
zip_file <- criar_zip_file(pdf_compilado) do | ||
elem(zip_file, 1) | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok essa é um pouco mais complexa porque no pico serão 63 relatórios com em torno de até 10mb o que seria mais 500mb em memória… por isso optei por criar um arquivo temporário com o ChromicPDF.print_to_pdf/2
e passar um “callback” do controller pra dentro desse módulo, enviando assim o download e não tendo que manter o arquivo em memória durante todo o ciclo. (nosso servidor n é parrudo, n aguentaria tudo isso em RAM)
def compilar_relatorios(relatorios_selecionados) do | |
relatorios = Enum.map(relatorios_selecionados, &find_relatorio/1) | |
htmls = Enum.map(relatorios, &RelatorioHTML.content/1) | |
pdfs = Enum.reduce(htmls, [], fn html, acc -> [{:html, html} | acc] end) | |
with {:ok, pdf_compilado} <- ChromicPDF.print_to_pdf(pdfs), | |
zip_file <- criar_zip_file(pdf_compilado) do | |
elem(zip_file, 1) | |
end | |
end | |
def compilar_relatorios(relatorios_selecionados, continuation) do | |
with {:ok, htmls} <- parse_relatorios_html(relatorios_selecionados) do | |
ChromicPDF.print_to_pdf(htmls, output: fn tmp_path -> | |
with {:ok, zip_filename, zip_file} <- criar_zip_file(pdf_compilado) do | |
continuation.(zip_filename, zip_file) | |
end | |
end) | |
end | |
end | |
defp parse_relatorios_html(relatorios_ids) do | |
Enum.reduce_while(relatorios_ids, [], fn relatorio_id, htmls -> | |
if relatorio = Repository.fetch_relatorio_pesquisa_by_id(relatorio_id) do | |
html = RelatorioHTML.content(relatorio) | |
{:cont, [{:html, html} | htmls]} | |
else | |
{:halt, {:error, :pdf_not_found}} | |
end | |
end) | |
end |
defp find_relatorio(relatorio_id) do | ||
Repository.fetch_relatorio_pesquisa_by_id(relatorio_id) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
n vejo sentido em isolar se podemos usar diretamente
end | ||
|
||
def gerar_pdf(relatorio_id) do | ||
relatorio = find_relatorio(relatorio_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
relatorio = find_relatorio(relatorio_id) | |
relatorio = Repository.fetch_relatorio_pesquisa_by_id(relatorio_id) |
deee227
to
910dcca
Compare
…pescarte-plataforma into feat/download-relatorio-pdf * 'feat/download-relatorio-pdf' of github.com:peapescarte/pescarte-plataforma: Refactor/mudancas model (#146)
…plataforma into fix/improve-navbar * 'fix/improve-navbar' of github.com:peapescarte/pescarte-plataforma: wip: search input v2 remove storybook and add flop fix: compile warnings and text sizes config: host for working live view update dockerfile feat: script de ingestão de pesquisa feats - download e compilação de relatórios (#145) Refactor/mudancas model (#146) add gettext update deps fix: texarea relatórios feat: move code from toast to flash file (#149) fix: many hotfix for reports (#151)
Descrição
Descrição do que é esperado deste PR
Stories relacionadas (Shortcut)
Pontos para atenção
Possui novas configurações?
Possui migrations?