zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

domain.ex (7047B)


      1 # Zenflows is designed to implement the Valueflows vocabulary,
      2 # written and maintained by srfsh <info@dyne.org>.
      3 # Copyright (C) 2021-2023 Dyne.org foundation <foundation@dyne.org>.
      4 #
      5 # This program is free software: you can redistribute it and/or modify
      6 # it under the terms of the GNU Affero General Public License as published by
      7 # the Free Software Foundation, either version 3 of the License, or
      8 # (at your option) any later version.
      9 #
     10 # This program is distributed in the hope that it will be useful,
     11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 # GNU Affero General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU Affero General Public License
     16 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
     17 
     18 defmodule Zenflows.SWPass.Domain do
     19 @moduledoc """
     20 Domain logic of interacting with softwarepassport instances over
     21 HTTP.
     22 """
     23 
     24 alias Zenflows.DB.Repo
     25 alias Zenflows.VF.{
     26 	Agent,
     27 	EconomicEvent,
     28 	EconomicResource,
     29 	Person,
     30 	Process,
     31 	ResourceSpecification,
     32 	Unit,
     33 }
     34 alias Ecto.Multi
     35 
     36 # TODO: not the best piece of code, but will do okay for now.
     37 @doc """
     38 Import repos by a URL to `/repositories` route of a softwarepassport
     39 instance, and generate necessary things to link Person Agents to
     40 EconomicResource Procjets.
     41 """
     42 @spec import_repos(String.t()) :: {:ok, term()} | {:error, term()}
     43 def import_repos(url) do
     44 	url = to_charlist(url)
     45 	hdrs = [
     46 		{'user-agent', useragent()},
     47 		{'accept', 'application/json'},
     48 	]
     49 	http_opts = [
     50 		{:timeout, 30_000}, # 30 seconds
     51 		{:connect_timeout, 5000}, # 5 seconds
     52 		{:autoredirect, false},
     53 	]
     54 	with {:ok, {{_, 200, _}, _, body_charlist}} <-
     55 				:httpc.request(:get, {url, hdrs}, http_opts, []),
     56 			{:ok, map} <- body_charlist |> to_string() |> Jason.decode() do
     57 		proj = get_emails(map)
     58 
     59 		result = Enum.map(proj, fn {url, emails} ->
     60 			now = DateTime.utc_now()
     61 			mult =
     62 				Multi.new()
     63 				|> Multi.run(:commit_spec, fn repo, _ ->
     64 					params = %{name: "committing"}
     65 
     66 					case repo.get_by(ResourceSpecification, params) do
     67 						nil ->
     68 							params
     69 							|> ResourceSpecification.changeset()
     70 							|> repo.insert()
     71 						res_spec -> {:ok, res_spec}
     72 					end
     73 				end)
     74 				|> Multi.run(:proj_spec, fn repo, _ ->
     75 					params = %{name: "project"}
     76 
     77 					case repo.get_by(ResourceSpecification, params) do
     78 						nil ->
     79 							params
     80 							|> ResourceSpecification.changeset()
     81 							|> repo.insert()
     82 						res_spec -> {:ok, res_spec}
     83 					end
     84 				end)
     85 				|> Multi.run(:unit, fn repo, _ ->
     86 					params = %{label: "one", symbol: "one"}
     87 
     88 					case repo.get_by(Unit, params) do
     89 						nil ->
     90 							params
     91 							|> Unit.changeset()
     92 							|> repo.insert()
     93 						unit -> {:ok, unit}
     94 					end
     95 				end)
     96 				|> Multi.run(:proc, fn repo, _ ->
     97 					params = %{name: url}
     98 
     99 					case repo.get_by(Process, params) do
    100 						nil ->
    101 							params
    102 							|> Process.changeset()
    103 							|> repo.insert()
    104 						proc -> {:ok, proc}
    105 					end
    106 				end)
    107 			Enum.reduce(emails, mult, fn e, mult ->
    108 				mult
    109 				|> Multi.run("per:#{e}", fn repo, _ ->
    110 					params = %{email: e, user: e, name: e}
    111 
    112 					case repo.get_by(Person, params) do
    113 						nil ->
    114 							params
    115 							|> Person.changeset()
    116 							|> repo.insert()
    117 						per -> {:ok, per}
    118 					end
    119 				end)
    120 				|> Multi.run("evt:#{e}", fn repo, %{proc: proc, commit_spec: commit_spec} = chgs ->
    121 					per = Map.fetch!(chgs, "per:#{e}")
    122 					params = %{
    123 						action_id: "deliverService",
    124 						input_of_id: proc.id,
    125 						provider_id: per.id,
    126 						receiver_id: per.id,
    127 						resource_conforms_to_id: commit_spec.id,
    128 						note: url,
    129 					}
    130 
    131 					case repo.get_by(EconomicEvent, params) do
    132 						nil ->
    133 							params
    134 							|> Map.put(:has_point_in_time, now)
    135 							|> EconomicEvent.changeset()
    136 							|> repo.insert()
    137 						evt -> {:ok, evt}
    138 					end
    139 				end)
    140 			end)
    141 			|> Multi.merge(fn changes ->
    142 				first_email = emails |> MapSet.to_list() |> List.first()
    143 				first_person = Map.fetch!(changes, "per:#{first_email}")
    144 				Multi.put(Multi.new(), :org, first_person) # TODO: make this really an organization
    145 			end)
    146 			|> Multi.run(:produce, fn repo, %{org: org, proc: proc, proj_spec: proj_spec, unit: unit} ->
    147 				params = %{
    148 					action_id: "produce",
    149 					output_of_id: proc.id,
    150 					provider_id: org.id,
    151 					receiver_id: org.id,
    152 					resource_conforms_to_id: proj_spec.id,
    153 					note: url,
    154 				}
    155 
    156 				case repo.get_by(EconomicEvent,
    157 							params
    158 							|> Map.put(:resource_quantity_has_unit_id, unit.id)
    159 							|> Map.put(:resource_quantity_has_numerical_value, 1)
    160 						) do
    161 					nil ->
    162 						params
    163 						|> Map.put(:resource_quantity, %{
    164 							has_unit_id: unit.id,
    165 							has_numerical_value: 1,
    166 						})
    167 						|> Map.put(:has_point_in_time, now)
    168 						|> EconomicEvent.changeset()
    169 						|> repo.insert()
    170 					evt -> {:ok, evt}
    171 				end
    172 			end)
    173 			|> Multi.run(:resource, fn repo, %{unit: unit, org: org, proj_spec: proj_spec} ->
    174 				params = %{
    175 					name: url,
    176 					primary_accountable_id: org.id,
    177 					custodian_id: org.id,
    178 					conforms_to_id: proj_spec.id,
    179 					accounting_quantity_has_unit_id: unit.id,
    180 					accounting_quantity_has_numerical_value: 1,
    181 					onhand_quantity_has_unit_id: unit.id,
    182 					onhand_quantity_has_numerical_value: 1,
    183 				}
    184 
    185 				case repo.get_by(EconomicResource, params) do
    186 					nil ->
    187 						params
    188 						|> EconomicResource.changeset()
    189 						|> repo.insert()
    190 					res -> {:ok, res}
    191 				end
    192 			end)
    193 			|> Multi.run(:update_produce_evt, fn repo, %{produce: evt, resource: res} ->
    194 				Ecto.Changeset.change(evt, resource_inventoried_as_id: res.id) |> repo.update()
    195 			end)
    196 			|> Repo.transaction()
    197 			|> case do
    198 				{:ok, res} -> {:ok, res}
    199 				{:error, op, val, chng} -> {:error, op, val, chng}
    200 			end
    201 		end)
    202 
    203 		if Enum.all?(result, &match?({:ok, _}, &1)) do
    204 			{:ok, result}
    205 		else
    206 			{:error, result}
    207 		end
    208 	else
    209 		{:ok, {{_, stat, _}, _, body_charlist}} ->
    210 			{:error, "the http call result in non-200 status code #{stat}: #{to_string(body_charlist)}"}
    211 
    212 		other -> other
    213 	end
    214 end
    215 
    216 @spec project_agents(String.t()) :: [Agent.t()]
    217 def project_agents(url) do
    218 	import Ecto.Query
    219 
    220 	from(p in Process,
    221 		join: e in assoc(p, :inputs),
    222 		join: a in assoc(e, :provider),
    223 		where: p.name == ^url,
    224 		select: a)
    225 	|> Repo.all()
    226 end
    227 
    228 # Get emails from the softwarepassport repositories output.
    229 @spec get_emails(map()) :: %{String.t() => MapSet.t()}
    230 defp get_emails(map) do
    231 	Enum.reduce(map, %{}, fn p, acc ->
    232 		# the scanning might be in proccess, so it might be `nil`
    233 		if p["scancode_report"] do
    234 			emails =
    235 				Enum.reduce(p["scancode_report"]["files"], MapSet.new(), fn f, acc ->
    236 					Enum.reduce(f["emails"], acc, fn e, acc ->
    237 						MapSet.put(acc, String.downcase(e["email"]))
    238 					end)
    239 				end)
    240 			if emails == MapSet.new() do # empty?
    241 				acc
    242 			else
    243 				Map.put(acc, p["url"], emails)
    244 			end
    245 		else
    246 			acc
    247 		end
    248 	end)
    249 end
    250 
    251 # Return the useragent to be used by the HTTP client, this module.
    252 @spec useragent() :: charlist()
    253 defp useragent() do
    254 	'zenflows/' ++ Application.spec(:zenflows, :vsn)
    255 end
    256 end