Se crea el parser para customfilter. Agregamos variables de entorno para manejar url de adjuntos con o sin clave de encriptacion.
This commit is contained in:
		
							
								
								
									
										7
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								.env
									
									
									
									
									
								
							| @ -4,7 +4,6 @@ DATABASE_URL=ecto://postgres:1nf0rm3@127.0.0.1/dicomscp | ||||
| CHECK_ORIGIN=https://estudios.nobissalud.com.ar | ||||
| PHX_PORT=4001 | ||||
| PHX_HOST=127.0.0.1 | ||||
| #PHX_HOST=pacientes.sdlc.com.ar | ||||
| ROOT_PATH=/api | ||||
| SCHEME=https | ||||
| #si es true debe estar escrito siempre en mayuscula (por que llega como string no como boolean) | ||||
| @ -16,8 +15,10 @@ PYTHON_SCRIPT=/home/informemedico/impacs/cgi/soffice/generar_pdf_api.py | ||||
| TEMPLATE_NAME=Con Membrete | ||||
| pr2_statusname=FINAL | ||||
| IDSITE=86 | ||||
| #ESCANEADOS=https://cedialcom.informemedico.com.ar:4443/cgi-bin/imageasjpeg.bf/imageasjpeg/ | ||||
| ESCANEADOS=https://estudios.nobissalud.com.ar/# | ||||
| ESCANEADOS=https://estudios.nobissalud.com.ar/ | ||||
| # ESCANEADOS_CLAVE= | ||||
| ADJUNTOS=https://estudios.nobissalud.com.ar/api/attachment/ | ||||
| ADJUNTOS_CLAVE=1nf0rm3 | ||||
| acceso_ed=IDSTUDY | ||||
| IDENTIFIERFIELD=IDSTUDY | ||||
|  | ||||
|  | ||||
| @ -3,14 +3,23 @@ defmodule Api.Studies do | ||||
|   alias Api.Repo | ||||
|  | ||||
|   def studies_sql_query(filters) do | ||||
|     customfilter = | ||||
|       case Map.get(filters, "customfilter") do | ||||
|         nil -> dynamic([_], true) | ||||
|         "" -> dynamic([_], true) | ||||
|         expr -> | ||||
|           case ExpressionParser.parse(expr) do | ||||
|             {:ok, ast} -> ExpressionToEcto.to_dynamic(ast) | ||||
|             {:error, _reason} -> raise "Error al parsear customfilter" | ||||
|           end | ||||
|       end | ||||
|  | ||||
|     page = filters["page"] || 1 | ||||
|     size = filters["size"] || 24 | ||||
|  | ||||
|     filter = filters["filter"] || [] | ||||
|     sort = filters["sort"] || [%{"dir" => "desc", "field" => "studydate"}] | ||||
|  | ||||
|  | ||||
|  | ||||
|     # Construcción de condiciones de filtro dinámicas | ||||
|     filter_conditions = | ||||
|       Enum.reduce(filter, dynamic(true), fn f, acc -> | ||||
| @ -23,7 +32,7 @@ defmodule Api.Studies do | ||||
|               dynamic([s], ilike(s.modality, ^"%#{value}%")) | ||||
|  | ||||
|             "idstudy" -> | ||||
|               dynamic([s], s.idstudy == ^String.to_integer(value)) | ||||
|               dynamic([s], like(fragment("CAST(? AS TEXT)", s.idstudy), ^"#{value}%")) | ||||
|  | ||||
|             "studydate" -> | ||||
|               dynamic([s], fragment("CAST(? AS DATE)::text LIKE ?", s.studydate, ^"%#{value}%")) | ||||
| @ -82,35 +91,37 @@ defmodule Api.Studies do | ||||
|         {direction, field} | ||||
|       end) | ||||
|  | ||||
|     combined_filter = | ||||
|       dynamic([q], ^filter_conditions and ^customfilter) | ||||
|  | ||||
|       query = | ||||
|       from s in "study", | ||||
|         join: p in "patient", | ||||
|         on: p.idpatient == s.idpatient, | ||||
|         left_join: sr in "studyreport", | ||||
|         on: sr.idstudy == s.idstudy, | ||||
|         left_join: st in "statuses", | ||||
|         on: st.idstatus == sr.idstudyreport, | ||||
|         where: ^filter_conditions, | ||||
|         select: %{ | ||||
|           recordstotal: fragment("count(*) over()"), | ||||
|           idstudy: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", s.idstudy), | ||||
|           #idstudy: s.idstudy, | ||||
|           accessionnumber: s.accessionnumber, | ||||
|           studydate: s.studydate, | ||||
|           studytime: s.studytime, | ||||
|           patientname: fragment("select replace(?, '^', ' ')", p.patientname), | ||||
|           proceduredescription: s.studydescription, | ||||
|           modality: s.modality, | ||||
|           sitename: s.institutionname, | ||||
|           insurer: s.insurer, | ||||
|           nrodocumento: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", p.patientid), | ||||
|           #nrodocumento: p.patientid, | ||||
|           hasaudio: fragment("CASE WHEN ? IS NOT NULL THEN true ELSE false END", s.audiofile) | ||||
|         }, | ||||
|         order_by: ^sort_conditions, | ||||
|         limit: ^size, | ||||
|         offset: ^((page - 1) * size) | ||||
|         from s in "study", | ||||
|           join: p in "patient", | ||||
|           on: p.idpatient == s.idpatient, | ||||
|           left_join: sr in "studyreport", | ||||
|           on: sr.idstudy == s.idstudy, | ||||
|           left_join: st in "statuses", | ||||
|           on: st.idstatus == sr.idstudyreport, | ||||
|           where: ^combined_filter, | ||||
|           select: %{ | ||||
|             recordstotal: fragment("count(*) over()"), | ||||
|             idstudy: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", s.idstudy), | ||||
|             #idstudy: s.idstudy, | ||||
|             accessionnumber: s.accessionnumber, | ||||
|             studydate: s.studydate, | ||||
|             studytime: s.studytime, | ||||
|             patientname: fragment("select replace(?, '^', ' ')", p.patientname), | ||||
|             proceduredescription: s.studydescription, | ||||
|             modality: s.modality, | ||||
|             sitename: s.institutionname, | ||||
|             insurer: s.insurer, | ||||
|             nrodocumento: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", p.patientid), | ||||
|             #nrodocumento: p.patientid, | ||||
|             hasaudio: fragment("CASE WHEN ? IS NOT NULL THEN true ELSE false END", s.audiofile) | ||||
|           }, | ||||
|           order_by: ^sort_conditions, | ||||
|           limit: ^size, | ||||
|           offset: ^((page - 1) * size) | ||||
|  | ||||
|  | ||||
|     result = Repo.one( | ||||
|  | ||||
| @ -9,7 +9,6 @@ defmodule Api.Study do | ||||
|  | ||||
|     idsite = Envar.get("IDSITE") |> String.to_integer | ||||
|     domain = Envar.get("CHECK_ORIGIN") | ||||
|     escaneados = Envar.get("ESCANEADOS") | ||||
|  | ||||
|     id_study = if Envar.get("IDENTIFIERFIELD") == "ACCESSIONNUMBER" do | ||||
|       query = | ||||
| @ -25,15 +24,6 @@ defmodule Api.Study do | ||||
|  | ||||
|     id_study = if is_integer(id_study), do: id_study, else: String.to_integer(id_study) | ||||
|  | ||||
|  | ||||
|     # accession_fragment = | ||||
|     #   if Envar.get("IDENTIFIERFIELD") == "ACCESSIONNUMBER" do | ||||
|     #     fragment("?::text", ^accession) | ||||
|     #   else | ||||
|     #     fragment("?::integer", ^id_study) | ||||
|     #   end | ||||
|  | ||||
|  | ||||
|     Logger.info("id_study -------> #{inspect(id_study)}") | ||||
|  | ||||
|     # Las siguientes consultas obtienen la información completa | ||||
| @ -70,56 +60,109 @@ defmodule Api.Study do | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|  | ||||
|     # escaneados | ||||
|     subquery2 = subquery( | ||||
|       from s in "study", | ||||
|         join: ss in "studyscans", on: ss.idstudy == s.idstudy, | ||||
|         join: sc in "scanclasses", on: sc.idscanclass == ss.idscanclass, | ||||
|         join: p in "patient", on: s.idpatient == p.idpatient, | ||||
|         where: s.idstudy == ^id_study, | ||||
|         select: %{ | ||||
|           idsite: type(^idsite, :integer), | ||||
|     escaneados_clave = Envar.get("ESCANEADOS_CLAVE") | ||||
|     base_escaneados = Envar.get("ESCANEADOS") | ||||
|  | ||||
|     escaneados_expr = | ||||
|       if escaneados_clave do | ||||
|         dynamic([_, ss, _, _], | ||||
|           fragment( | ||||
|             "concat(?::text, substring(encrypt(?::text::bytea, ?, 'aes')::text from 3))", | ||||
|             ^base_escaneados, | ||||
|             ss.idstudyscan, | ||||
|             ^escaneados_clave | ||||
|           ) | ||||
|         ) | ||||
|       else | ||||
|         dynamic([_, ss, _, _], | ||||
|           fragment( | ||||
|             "concat(?::text, ?::text)", | ||||
|             ^base_escaneados, | ||||
|             ss.idstudyscan | ||||
|           ) | ||||
|         ) | ||||
|       end | ||||
|  | ||||
|     select_expr_es = | ||||
|       dynamic([s, ss, sc, p], %{ | ||||
|         idsite: type(^idsite, :integer), | ||||
|           iddocument: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", ss.idstudyscan), | ||||
|           document_name: sc.scanclass, | ||||
|           document_type: "url", | ||||
|           url: fragment( | ||||
|             "concat(?::text, substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3))", | ||||
|             ^escaneados, | ||||
|             ss.idstudyscan | ||||
|           ), | ||||
|           url: ^escaneados_expr, | ||||
|           patientname: p.patientname, | ||||
|           proceduredescription: s.studydescription, | ||||
|           studydate: fragment("TO_CHAR(?, 'YYYY-MM-DD')", s.studydate), | ||||
|           studytime: fragment("TO_CHAR(?, 'HH24:MI:SS')", s.studytime), | ||||
|           accessionnumber: s.idstudy, | ||||
|           patientid: p.patientid | ||||
|         } | ||||
|       }) | ||||
|  | ||||
|     subquery2 = subquery( | ||||
|       from s in "study", | ||||
|         join: ss in "studyscans", on: ss.idstudy == s.idstudy, | ||||
|         join: sc in "scanclasses", on: sc.idscanclass == ss.idscanclass, | ||||
|         join: p in "patient", on: s.idpatient == p.idpatient, | ||||
|         where: s.idstudy == ^id_study, | ||||
|         select: ^select_expr_es | ||||
|     ) | ||||
|  | ||||
|     # adjuntos | ||||
|     subquery3 = | ||||
|     adjuntos_clave = Envar.get("ADJUNTOS_CLAVE") | ||||
|     base_adjuntos = Envar.get("ADJUNTOS") | ||||
|  | ||||
|     adjuntos_expr = | ||||
|       if adjuntos_clave do | ||||
|         dynamic([_, sa, _], | ||||
|           fragment( | ||||
|             "concat(?::text, substring(encrypt(?::text::bytea, ?, 'aes')::text from 3))", | ||||
|             ^base_adjuntos, | ||||
|             sa.idstudyattachment, | ||||
|             ^adjuntos_clave | ||||
|           ) | ||||
|         ) | ||||
|       else | ||||
|         dynamic([_, sa, _], | ||||
|           fragment( | ||||
|             "concat(?::text, ?::text)", | ||||
|             ^base_adjuntos, | ||||
|             sa.idstudyattachment | ||||
|           ) | ||||
|         ) | ||||
|       end | ||||
|  | ||||
|     select_expr_ad = | ||||
|       dynamic([s, sa, p], %{ | ||||
|         idsite: type(^idsite, :integer), | ||||
|         iddocument: | ||||
|           fragment( | ||||
|             "substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", | ||||
|             sa.idstudyattachment | ||||
|           ), | ||||
|         document_name: sa.name, | ||||
|         document_type: | ||||
|           fragment( | ||||
|             "CASE WHEN ? NOT IN ('jpg', 'jpeg', 'png') THEN 'attachment' ELSE 'url' END", | ||||
|             sa.format | ||||
|           ), | ||||
|         url: ^adjuntos_expr, | ||||
|         patientname: p.patientname, | ||||
|         proceduredescription: s.studydescription, | ||||
|         studydate: fragment("TO_CHAR(?, 'YYYY-MM-DD')", s.studydate), | ||||
|         studytime: fragment("TO_CHAR(?, 'HH24:MI:SS')", s.studytime), | ||||
|         accessionnumber: s.idstudy, | ||||
|         patientid: "" | ||||
|       }) | ||||
|  | ||||
|     subquery3 = subquery( | ||||
|       from s in "study", | ||||
|         join: sa in "studyattachments", on: sa.idstudy == s.idstudy, | ||||
|         join: p in "patient", on: p.idpatient == s.idpatient, | ||||
|         where: s.idstudy == ^id_study, | ||||
|         select: %{ | ||||
|           idsite: type(^idsite, :integer), | ||||
|           iddocument: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", sa.idstudyattachment), | ||||
|           document_name: sa.name, | ||||
|           document_type: fragment("CASE WHEN ? NOT IN ('jpg', 'jpeg', 'png') THEN 'attachment' ELSE 'url' END", sa.format), | ||||
|           url: fragment( | ||||
|             "concat(?::text, ?::text, substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3))", | ||||
|             ^domain, | ||||
|             "/api/attachment/", | ||||
|             sa.idstudyattachment | ||||
|           ), | ||||
|           patientname: p.patientname, | ||||
|           proceduredescription: s.studydescription, | ||||
|           studydate: fragment("TO_CHAR(?, 'YYYY-MM-DD')", s.studydate), | ||||
|           studytime: fragment("TO_CHAR(?, 'HH24:MI:SS')", s.studytime), | ||||
|           accessionnumber: s.idstudy, | ||||
|           patientid: "" | ||||
|         } | ||||
|         select: ^select_expr_ad | ||||
|     ) | ||||
|  | ||||
|     # Si no encontró informes, ni adjuntos, ni nada | ||||
|     # se traen sólo los datos básicos del estudio | ||||
|  | ||||
| @ -10,7 +10,7 @@ defmodule Api.Application do | ||||
|     children = [ | ||||
|       ApiWeb.Telemetry, | ||||
|       Api.Repo, | ||||
|         {Api.Autosender, 1000 * 62 * 4}, | ||||
|       # {Api.Autosender, 1000 * 60}, | ||||
|         # {DNSCluster, query: Application.get_env(:api, :dns_cluster_query) || :ignore}, | ||||
|       {Phoenix.PubSub, name: Api.PubSub}, | ||||
|       # Start the Finch HTTP client for sending emails | ||||
|  | ||||
| @ -13,6 +13,7 @@ defmodule Api.Emailtosend do | ||||
|     field :errormsg, :string | ||||
|     field :forcereprocess, :boolean | ||||
|     field :sentdatetime, :naive_datetime | ||||
|     field :registered, :utc_datetime | ||||
|   end | ||||
|  | ||||
|   def changeset(emailtosend, attrs) do | ||||
|  | ||||
							
								
								
									
										124
									
								
								lib/api/parser/expression_parser.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								lib/api/parser/expression_parser.ex
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | ||||
| defmodule ExpressionParser do | ||||
|   import NimbleParsec | ||||
|  | ||||
|   whitespace = ignore(optional(ascii_string([?\s, ?\t, ?\n, ?\r], min: 1))) | ||||
|  | ||||
|   identifier = | ||||
|     ascii_string([?a..?z, ?A..?Z, ?_, ?0..?9], min: 1) | ||||
|     |> label("identifier") | ||||
|     |> unwrap_and_tag(:field) | ||||
|  | ||||
|   quoted_string = | ||||
|     ignore(string("\"")) | ||||
|     |> repeat(utf8_char(not: ?")) | ||||
|     |> reduce({List, :to_string, []}) | ||||
|     |> ignore(string("\"")) | ||||
|  | ||||
|   number = | ||||
|     ascii_string([?0..?9], min: 1) | ||||
|     |> map({String, :to_integer, []}) | ||||
|  | ||||
|   atom_value = | ||||
|     ignore(string("'")) | ||||
|     |> ascii_string([?a..?z, ?A..?Z], min: 1) | ||||
|     |> ignore(string("'")) | ||||
|     |> map({__MODULE__, :identity, []}) | ||||
|  | ||||
|   list_item = | ||||
|     choice([ | ||||
|       number, | ||||
|       atom_value, | ||||
|       quoted_string |> unwrap_and_tag(:string) | ||||
|     ]) | ||||
|  | ||||
|   atom_list = | ||||
|     ignore(string("[")) | ||||
|     |> optional(whitespace) | ||||
|     |> repeat( | ||||
|       list_item | ||||
|       |> optional(ignore(string(","))) | ||||
|       |> optional(whitespace) | ||||
|     ) | ||||
|     |> ignore(string("]")) | ||||
|     |> tag(:list) | ||||
|  | ||||
|  | ||||
|  | ||||
|   operator = | ||||
|     choice([ | ||||
|       string("ilike") |> replace(:ilike), | ||||
|       string(">=") |> replace(:>=), | ||||
|       string("<=") |> replace(:<=), | ||||
|       string("!=") |> replace(:!=), | ||||
|       string(">") |> replace(:>), | ||||
|       string("<") |> replace(:<), | ||||
|       string("=") |> replace(:==), | ||||
|       string("in") |> replace(:in) | ||||
|     ]) | ||||
|  | ||||
|   value = | ||||
|     choice([ | ||||
|       quoted_string |> unwrap_and_tag(:string), | ||||
|       number |> unwrap_and_tag(:number), | ||||
|       atom_list | ||||
|     ]) | ||||
|  | ||||
|   comparison = | ||||
|     identifier | ||||
|     |> ignore(whitespace) | ||||
|     |> concat(operator) | ||||
|     |> ignore(whitespace) | ||||
|     |> concat(value) | ||||
|     |> tag(:comparison) | ||||
|  | ||||
|   defcombinatorp :expr, parsec(:logic_expr) | ||||
|  | ||||
|   paren_expr = | ||||
|     ignore(string("(")) | ||||
|     |> ignore(whitespace) | ||||
|     |> parsec(:expr) | ||||
|     |> ignore(whitespace) | ||||
|     |> ignore(string(")")) | ||||
|  | ||||
|   logic_term = | ||||
|     choice([ | ||||
|       paren_expr, | ||||
|       comparison | ||||
|     ]) | ||||
|  | ||||
|   logical_op = | ||||
|     ignore(whitespace) | ||||
|     |> choice([ | ||||
|       string("and") |> replace(:and), | ||||
|       string("or") |> replace(:or) | ||||
|     ]) | ||||
|     |> ignore(whitespace) | ||||
|  | ||||
|   logic_expr = | ||||
|     logic_term | ||||
|     |> repeat( | ||||
|       logical_op | ||||
|       |> concat(logic_term) | ||||
|     ) | ||||
|     |> reduce({__MODULE__, :reduce_logic, []}) | ||||
|  | ||||
|   defparsec :logic_expr, logic_expr | ||||
|  | ||||
|   def parse(input) do | ||||
|     case logic_expr(input) do | ||||
|       {:ok, result, "", _, _, _} -> {:ok, result} | ||||
|       {:ok, _, rest, _, _, _} -> {:error, "Unexpected remaining input: #{inspect(rest)}"} | ||||
|       {:error, reason, _, _, _, _} -> {:error, reason} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def reduce_logic([head | tail]) do | ||||
|     Enum.chunk_every(tail, 2) | ||||
|     |> Enum.reduce(head, fn [op, right], acc -> | ||||
|       {op, acc, right} | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   def identity(value), do: value | ||||
|  | ||||
| end | ||||
							
								
								
									
										59
									
								
								lib/api/parser/expression_to_ecto.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								lib/api/parser/expression_to_ecto.ex
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| defmodule ExpressionToEcto do | ||||
|   import Ecto.Query | ||||
|  | ||||
|   def to_dynamic([]), do: true | ||||
|  | ||||
|   def to_dynamic([single]), do: build_dynamic(single) | ||||
|  | ||||
|   def to_dynamic(ast), do: build_dynamic(ast) | ||||
|  | ||||
|   # Operadores lógicos | ||||
|   defp build_dynamic({:or, left, right}) do | ||||
|     dynamic([q], ^build_dynamic(left) or ^build_dynamic(right)) | ||||
|   end | ||||
|  | ||||
|   defp build_dynamic({:and, left, right}) do | ||||
|     dynamic([q], ^build_dynamic(left) and ^build_dynamic(right)) | ||||
|   end | ||||
|  | ||||
|   # Comparaciones con número | ||||
|   defp build_dynamic({:comparison, [{:field, field}, op, {:number, val}]}) do | ||||
|     field_atom = String.to_atom(field) | ||||
|  | ||||
|     case op do | ||||
|       :== -> dynamic([q], field(q, ^field_atom) == ^val) | ||||
|       :!= -> dynamic([q], field(q, ^field_atom) != ^val) | ||||
|       :>  -> dynamic([q], field(q, ^field_atom) > ^val) | ||||
|       :<  -> dynamic([q], field(q, ^field_atom) < ^val) | ||||
|       :>= -> dynamic([q], field(q, ^field_atom) >= ^val) | ||||
|       :<= -> dynamic([q], field(q, ^field_atom) <= ^val) | ||||
|       other -> raise "Operador no soportado: #{inspect(other)}" | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # Comparaciones con string | ||||
|   defp build_dynamic({:comparison, [{:field, field}, op, {:string, val}]}) when op in [:==, :!=] do | ||||
|     field_atom = String.to_atom(field) | ||||
|       case op do | ||||
|         :== -> dynamic([q], field(q, ^field_atom) == ^val) | ||||
|         :!= -> dynamic([q], field(q, ^field_atom) != ^val) | ||||
|       end | ||||
|   end | ||||
|  | ||||
|   #  ILIKE | ||||
|   defp build_dynamic({:comparison, [{:field, field}, :ilike, {:string, val}]}) do | ||||
|     field_atom = String.to_atom(field) | ||||
|     pattern = "#{val}" | ||||
|     dynamic([q], ilike(field(q, ^field_atom), ^pattern)) | ||||
|   end | ||||
|  | ||||
|   # IN | ||||
|   defp build_dynamic({:comparison, [{:field, field}, :in, {:list, vals}]}) do | ||||
|     field_atom = String.to_atom(field) | ||||
|     dynamic([q], field(q, ^field_atom) in ^vals) | ||||
|   end | ||||
|  | ||||
|   defp build_dynamic(other) do | ||||
|     raise "AST no soportado o mal formado: #{inspect(other)}" | ||||
|   end | ||||
| end | ||||
		Reference in New Issue
	
	Block a user