Skip to content
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

Merged
merged 25 commits into from
Apr 8, 2024

Conversation

matheus-fvp
Copy link
Contributor

Descrição

Descrição do que é esperado deste PR

Stories relacionadas (Shortcut)

  • [sc-xxxx]

Pontos para atenção

  • Listar pontos para atenção no review
  • Listar pontos para atenção nos testes

Possui novas configurações?

  • Descrever as configurações alteradas ou novas

Possui migrations?

  • Se a feature adicionou alguma migration e como faz para rodar

@matheus-fvp matheus-fvp requested a review from zoedsoupe February 5, 2024 15:07
Copy link
Member

@zoedsoupe zoedsoupe left a 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?

Comment on lines 49 to 52
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
Copy link
Member

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

Comment on lines 14 to 19
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
Copy link
Member

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

Comment on lines 21 to 30
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
Copy link
Member

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.

Comment on lines 32 to 41
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
Copy link
Member

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

Comment on lines 38 to 53
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]
Copy link
Member

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}

lib/pescarte/modulo_pesquisa/relatorio_compiler.ex Outdated Show resolved Hide resolved
lib/pescarte/modulo_pesquisa/relatorio_compiler.ex Outdated Show resolved Hide resolved
lib/pescarte/modulo_pesquisa/relatorio_compiler.ex Outdated Show resolved Hide resolved
lib/pescarte/modulo_pesquisa/relatorio_compiler.ex Outdated Show resolved Hide resolved
lib/pescarte_web/templates/relatorio_html.ex Outdated Show resolved Hide resolved
Comment on lines 11 to 16
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
Copy link
Member

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
Suggested change
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

Comment on lines 18 to 25
defp enviar_pdf_response(conn, id, pdf_file) do
send_download(
conn,
{:binary, pdf_file},
content_type: "application/pdf",
filename: "relatorios-#{id}.pdf"
)
end
Copy link
Member

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

Comment on lines 27 to 34
def enviar_zip_response(conn, zip_file) do
send_download(
conn,
{:binary, zip_file},
content_type: "application/zip",
filename: "relatorios-compilados.zip"
)
end
Copy link
Member

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

Comment on lines 6 to 9
def download_pdf(conn, %{"id" => id}) do
pdf_binary = RelatorioCompiler.gerar_pdf(id)
enviar_pdf_response(conn, id, pdf_binary)
end
Copy link
Member

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

Suggested change
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

Comment on lines 31 to 40
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
Copy link
Member

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:

Suggested change
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_filenamerelatorio_final.zip"
@zipped_filenamerelatorio_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

Comment on lines 17 to 24
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
Copy link
Member

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

Suggested change
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

Comment on lines 5 to 14
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
Copy link
Member

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)

Suggested change
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

Comment on lines 27 to 29
defp find_relatorio(relatorio_id) do
Repository.fetch_relatorio_pesquisa_by_id(relatorio_id)
end
Copy link
Member

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
relatorio = find_relatorio(relatorio_id)
relatorio = Repository.fetch_relatorio_pesquisa_by_id(relatorio_id)

@zoedsoupe zoedsoupe force-pushed the feat/download-relatorio-pdf branch from deee227 to 910dcca Compare February 28, 2024 16:16
* main:
  update deps
  fix: texarea relatórios
  feat: move code from toast to flash file (#149)
  fix: many hotfix for reports (#151)
…pescarte-plataforma into feat/download-relatorio-pdf

* 'feat/download-relatorio-pdf' of github.com:peapescarte/pescarte-plataforma:
  Refactor/mudancas model (#146)
@zoedsoupe zoedsoupe merged commit 90e3b9b into main Apr 8, 2024
2 of 3 checks passed
@zoedsoupe zoedsoupe deleted the feat/download-relatorio-pdf branch April 8, 2024 17:18
zoedsoupe added a commit that referenced this pull request Apr 11, 2024
* 'main' of github.com:peapescarte/pescarte-plataforma:
  feats - download e compilação de relatórios (#145)
  Refactor/mudancas model (#146)
douglastofoli added a commit that referenced this pull request Apr 17, 2024
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Status: Completo
Development

Successfully merging this pull request may close these issues.

2 participants