|
| 1 | +defmodule Gallium.Members do |
| 2 | + @moduledoc """ |
| 3 | + The Members context. |
| 4 | + """ |
| 5 | + |
| 6 | + import Ecto.Query, warn: false |
| 7 | + |
| 8 | + alias Gallium.Accounts.Scope |
| 9 | + alias Gallium.Members.CesiumMember |
| 10 | + alias Gallium.Repo |
| 11 | + alias NimbleCSV.RFC4180, as: CSV |
| 12 | + |
| 13 | + @doc """ |
| 14 | + Subscribes to scoped notifications about any cesium_member changes. |
| 15 | +
|
| 16 | + The broadcasted messages match the pattern: |
| 17 | +
|
| 18 | + * {:created, %CesiumMember{}} |
| 19 | + * {:updated, %CesiumMember{}} |
| 20 | + * {:deleted, %CesiumMember{}} |
| 21 | +
|
| 22 | + """ |
| 23 | + def subscribe_cesium_members(%Scope{} = scope) do |
| 24 | + key = scope.user.id |
| 25 | + |
| 26 | + Phoenix.PubSub.subscribe(Gallium.PubSub, "user:#{key}:cesium_members") |
| 27 | + end |
| 28 | + |
| 29 | + defp broadcast_cesium_member(%Scope{} = scope, message) do |
| 30 | + key = scope.user.id |
| 31 | + |
| 32 | + Phoenix.PubSub.broadcast(Gallium.PubSub, "user:#{key}:cesium_members", message) |
| 33 | + end |
| 34 | + |
| 35 | + @doc """ |
| 36 | + Returns the list of cesium_members. |
| 37 | +
|
| 38 | + ## Examples |
| 39 | +
|
| 40 | + iex> list_cesium_members(scope) |
| 41 | + [%CesiumMember{}, ...] |
| 42 | +
|
| 43 | + """ |
| 44 | + def list_cesium_members(%Scope{} = scope) do |
| 45 | + Repo.all_by(CesiumMember, user_id: scope.user.id) |
| 46 | + end |
| 47 | + |
| 48 | + @doc """ |
| 49 | + Gets a single cesium_member. |
| 50 | +
|
| 51 | + Raises `Ecto.NoResultsError` if the Cesium member does not exist. |
| 52 | +
|
| 53 | + ## Examples |
| 54 | +
|
| 55 | + iex> get_cesium_member!(scope, 123) |
| 56 | + %CesiumMember{} |
| 57 | +
|
| 58 | + iex> get_cesium_member!(scope, 456) |
| 59 | + ** (Ecto.NoResultsError) |
| 60 | +
|
| 61 | + """ |
| 62 | + def get_cesium_member!(%Scope{} = scope, id) do |
| 63 | + Repo.get_by!(CesiumMember, id: id, user_id: scope.user.id) |
| 64 | + end |
| 65 | + |
| 66 | + @doc """ |
| 67 | + Creates a cesium_member. |
| 68 | +
|
| 69 | + ## Examples |
| 70 | +
|
| 71 | + iex> create_cesium_member(scope, %{field: value}) |
| 72 | + {:ok, %CesiumMember{}} |
| 73 | +
|
| 74 | + iex> create_cesium_member(scope, %{field: bad_value}) |
| 75 | + {:error, %Ecto.Changeset{}} |
| 76 | +
|
| 77 | + """ |
| 78 | + def create_cesium_member(%Scope{} = scope, attrs) do |
| 79 | + with {:ok, cesium_member = %CesiumMember{}} <- |
| 80 | + %CesiumMember{} |
| 81 | + |> CesiumMember.changeset(attrs, scope) |
| 82 | + |> Repo.insert() do |
| 83 | + broadcast_cesium_member(scope, {:created, cesium_member}) |
| 84 | + {:ok, cesium_member} |
| 85 | + end |
| 86 | + end |
| 87 | + |
| 88 | + @doc """ |
| 89 | + Updates a cesium_member. |
| 90 | +
|
| 91 | + ## Examples |
| 92 | +
|
| 93 | + iex> update_cesium_member(scope, cesium_member, %{field: new_value}) |
| 94 | + {:ok, %CesiumMember{}} |
| 95 | +
|
| 96 | + iex> update_cesium_member(scope, cesium_member, %{field: bad_value}) |
| 97 | + {:error, %Ecto.Changeset{}} |
| 98 | +
|
| 99 | + """ |
| 100 | + def update_cesium_member(%Scope{} = scope, %CesiumMember{} = cesium_member, attrs) do |
| 101 | + true = cesium_member.user_id == scope.user.id |
| 102 | + |
| 103 | + with {:ok, cesium_member = %CesiumMember{}} <- |
| 104 | + cesium_member |
| 105 | + |> CesiumMember.changeset(attrs, scope) |
| 106 | + |> Repo.update() do |
| 107 | + broadcast_cesium_member(scope, {:updated, cesium_member}) |
| 108 | + {:ok, cesium_member} |
| 109 | + end |
| 110 | + end |
| 111 | + |
| 112 | + @doc """ |
| 113 | + Deletes a cesium_member. |
| 114 | +
|
| 115 | + ## Examples |
| 116 | +
|
| 117 | + iex> delete_cesium_member(scope, cesium_member) |
| 118 | + {:ok, %CesiumMember{}} |
| 119 | +
|
| 120 | + iex> delete_cesium_member(scope, cesium_member) |
| 121 | + {:error, %Ecto.Changeset{}} |
| 122 | +
|
| 123 | + """ |
| 124 | + def delete_cesium_member(%Scope{} = scope, %CesiumMember{} = cesium_member) do |
| 125 | + true = cesium_member.user_id == scope.user.id |
| 126 | + |
| 127 | + with {:ok, cesium_member = %CesiumMember{}} <- |
| 128 | + Repo.delete(cesium_member) do |
| 129 | + broadcast_cesium_member(scope, {:deleted, cesium_member}) |
| 130 | + {:ok, cesium_member} |
| 131 | + end |
| 132 | + end |
| 133 | + |
| 134 | + @doc """ |
| 135 | + Returns an `%Ecto.Changeset{}` for tracking cesium_member changes. |
| 136 | +
|
| 137 | + ## Examples |
| 138 | +
|
| 139 | + iex> change_cesium_member(scope, cesium_member) |
| 140 | + %Ecto.Changeset{data: %CesiumMember{}} |
| 141 | +
|
| 142 | + """ |
| 143 | + def change_cesium_member(%Scope{} = scope, %CesiumMember{} = cesium_member, attrs \\ %{}) do |
| 144 | + true = cesium_member.user_id == scope.user.id |
| 145 | + |
| 146 | + CesiumMember.changeset(cesium_member, attrs, scope) |
| 147 | + end |
| 148 | + |
| 149 | + @doc """ |
| 150 | + Imports a list of cesium_members from a CSV file. |
| 151 | + The CSV must have the headers: id_socio, numero_aluno, nome |
| 152 | + Since the file serves as a sync, existing members for this scope are deleted. |
| 153 | + """ |
| 154 | + def import_cesium_members(%Scope{} = scope, file_path) do |
| 155 | + unless File.exists?(file_path) do |
| 156 | + {:error, "Ficheiro CSV não encontrado."} |
| 157 | + end |
| 158 | + |
| 159 | + try do |
| 160 | + stream = |
| 161 | + file_path |
| 162 | + |> File.stream!() |
| 163 | + |> Stream.map(&String.replace(&1, "\uFEFF", "")) |
| 164 | + |> CSV.parse_stream(skip_headers: false) |
| 165 | + |
| 166 | + format = detect_csv_format(stream) |
| 167 | + import_from_stream(scope, stream, format) |
| 168 | + rescue |
| 169 | + NimbleCSV.ParseError -> |
| 170 | + {:error, |
| 171 | + "Formato CSV inválido. Certifique-se de que o separador é um um ponto e vírgula (;) e não uma (,)."} |
| 172 | + end |
| 173 | + end |
| 174 | + |
| 175 | + defp detect_csv_format(stream) do |
| 176 | + case Enum.take(stream, 1) do |
| 177 | + [[col1, col2, col3]] |
| 178 | + when col1 == "id_socio" and col2 == "numero_aluno" and col3 == "nome" -> |
| 179 | + :id_socio_first |
| 180 | + |
| 181 | + [[col1, col2, col3]] |
| 182 | + when col1 == "nome" and col2 == "id_socio" and col3 == "numero_aluno" -> |
| 183 | + :nome_first |
| 184 | + |
| 185 | + _ -> |
| 186 | + {:error, "O CSV não tem a estrutura requerida (id_socio, numero_aluno, nome)."} |
| 187 | + end |
| 188 | + end |
| 189 | + |
| 190 | + defp import_from_stream(%Scope{} = _scope, _stream, {:error, _} = error), do: error |
| 191 | + |
| 192 | + defp import_from_stream(%Scope{} = scope, stream, format) do |
| 193 | + # Entire operation is atomic - all or nothing |
| 194 | + Repo.transaction(fn -> |
| 195 | + # Remove existing members for this scope completely inserting the new ones |
| 196 | + Repo.delete_all(from(c in CesiumMember)) |
| 197 | + |
| 198 | + stream |
| 199 | + |> Stream.drop(1) |
| 200 | + |> Enum.each(&process_row(scope, format, &1)) |
| 201 | + |
| 202 | + "Sócios importados com sucesso!" |
| 203 | + end) |
| 204 | + |> case do |
| 205 | + {:ok, msg} -> {:ok, msg} |
| 206 | + {:error, reason} -> {:error, "Erro a importar sócios: #{inspect(reason)}"} |
| 207 | + end |
| 208 | + end |
| 209 | + |
| 210 | + defp process_row(scope, format, row) do |
| 211 | + attrs = format_row_attrs(format, row) |
| 212 | + if attrs, do: create_cesium_member(scope, attrs) |
| 213 | + end |
| 214 | + |
| 215 | + defp format_row_attrs(:id_socio_first, [id_socio, numero_aluno, nome]) do |
| 216 | + %{member_id: id_socio, student_number: numero_aluno, name: nome} |
| 217 | + end |
| 218 | + |
| 219 | + defp format_row_attrs(:nome_first, [nome, id_socio, numero_aluno]) do |
| 220 | + %{name: nome, member_id: id_socio, student_number: numero_aluno} |
| 221 | + end |
| 222 | + |
| 223 | + defp format_row_attrs(_, _), do: nil |
| 224 | + |
| 225 | + def cesium_member?(student_number) do |
| 226 | + Repo.exists?( |
| 227 | + from m in CesiumMember, |
| 228 | + where: fragment("LOWER(TRIM(?))", m.student_number) == ^student_number |
| 229 | + ) |
| 230 | + end |
| 231 | +end |
0 commit comments