Compare commits

...

4 Commits

Author SHA1 Message Date
Ryan Rix 99511469a9 Merge the Definition and Ecto model in to a single module 2020-09-16 18:45:21 -07:00
Ryan Rix cb53a45498 add PokaIjo.Photo and PokaIjoWeb.Photo 2020-09-16 17:42:14 -07:00
Ryan Rix 16593ac278 add mix test.watch 2020-09-16 17:41:57 -07:00
Ryan Rix 9e929e0310 fix test for failure flash assertion 2020-09-16 17:32:39 -07:00
7 changed files with 139 additions and 2 deletions

1
.gitignore vendored
View File

@ -42,6 +42,7 @@ poka_ijo-*.tar
/lib/poka_ijo_web/templates/
/test/poka_ijo_web/controllers/
/test/poka_ijo_web/views/
/lib/poka_ijo_web/uploaders/
/test/test_helper.exs
/test/support/

View File

@ -26,6 +26,7 @@ I expect a few components to come together for this [[file:../Projects.org][Proj
- [ ] attributes: file attachments, tags
- [X] "walk" the tree of objects
- [ ] search for an object by name or metadata
- [[file:photo_uploads.org][Photo Uploads in Poka Ijo]]
- a management frontend which allows me to:
- [-] [[file:thing_controller.org][Poka Ijo Thing CRUD]] to quickly add new items, floating or contained in another.
- [ ] quickly attach photos and metadata to objects
@ -91,6 +92,8 @@ Run this with Alt-Return to disable colors and see the =IEx= shell in action. Yo
IEx.configure(colors: [enabled: false])
#+end_src
To run tests use [[shell:mix test.watch][=mix test.watch=]].
* Footnotes
[fn:1] Some restrictions apply, segments that are part of a noweb block will need to be run in the module-level block...

View File

@ -1,4 +1,7 @@
%{
"arc": {:hex, :arc, "0.11.0", "ac7a0cc03035317b6fef9fe94c97d7d9bd183a3e7ce1606aa0c175cfa8d1ba6d", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.0", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "e91a8bd676fca716f6e46275ae81fb96c0bbc7a9d5b96cac511ae190588eddd0"},
"arc_ecto": {:hex, :arc_ecto, "0.11.3", "52f278330fe3a29472ce5d9682514ca09eaed4b33453cbaedb5241a491464f7d", [:mix], [{:arc, "~> 0.11.0", [hex: :arc, repo: "hexpm", optional: false]}, {:ecto, ">= 2.1.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "24beed35003707434a778caece7d71e46e911d46d1e82e7787345264fc8e96d0"},
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
@ -8,8 +11,14 @@
"ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
"gettext": {:hex, :gettext, "0.18.1", "89e8499b051c7671fa60782faf24409b5d2306aa71feb43d79648a8bc63d0522", [:mix], [], "hexpm", "e70750c10a5f88cb8dc026fc28fa101529835026dec4a06dba3b614f2a99c7a9"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.4.17", "1b1bd4cff7cfc87c94deaa7d60dd8c22e04368ab95499483c50640ef3bd838d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a8e5d7a3d76d452bb5fb86e8b7bd115f737e4f8efe202a463d4aeb4a5809611"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.0", "4ac3300a22240a37ed54dfe6c0be1b5623304385d1a2c210a70f011d9e7af7ac", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "59e7e2a550d7ea082a665c0fc29485f06f55d1a51dd02f513aafdb9d16fc72c4"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
@ -20,5 +29,7 @@
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
}

View File

@ -58,6 +58,7 @@ Files which are provided from tangles will need to be managed here:
/lib/poka_ijo_web/templates/
/test/poka_ijo_web/controllers/
/test/poka_ijo_web/views/
/lib/poka_ijo_web/uploaders/
/test/test_helper.exs
/test/support/
@ -131,7 +132,10 @@ The project dependencies are specified here, this is a simple Phoenix web app fo
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"}
{:plug_cowboy, "~> 2.0"},
{:arc, "~> 0.11.0"},
{:arc_ecto, "~> 0.11.0"},
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false},
]
end
#+end_src
@ -218,6 +222,13 @@ Use Jason for the Phoenix JSON library, this is also a default. I've used Poison
config :phoenix, :json_library, Jason
#+end_src
For [[file:photo_uploads.org][PokaIjo.Photo]], I upload the photos to my local filesystem. If it becomes a problem I'll use B2 via S3.
#+begin_src elixir :tangle config/config.exs
config :arc,
storage: Arc.Storage.Local
#+end_src
Lastly, config.exs loads environment-specific configuration.
#+begin_src elixir :tangle config/config.exs

103
photo_uploads.org Normal file
View File

@ -0,0 +1,103 @@
#+TITLE: Photo Uploads in Poka Ijo
#+ROAM_ALIAS: "PokaIjo.Photo"
#+PROPERTY: header-args:elixir :results none
I use [[https://github.com/stavro/arc][stavro/arc]] and its ecto module to provide photo upload and cleanup capabilities. It relies on ImageMagick being installed, I'll deal with that when I write my deployment documentation!
* Ecto Model
The persistence model is a pretty simple Ecto model, it's got a single association to a thing, a reference to an object of the =Arc.Definition=, and a UUID used for the filename. Eventually I need to add a =delete_photo= call, but for now this is Good Enough, there's very little behavior to consider here.
#+begin_src elixir :noweb-ref ecto-schema-and-fns
use Ecto.Schema
use Arc.Ecto.Schema
import Ecto.Changeset
schema "photo" do
belongs_to :thing, PokaIjo.Thing
field :photo, PokaIjo.Photo.Type
field :uuid, :string
timestamps()
end
def changeset(photo, attrs) do
photo
|> Map.update(:uuid, Ecto.UUID.generate, fn val -> val || Ecto.UUID.generate end)
|> cast_attachments(attrs, [:photo])
|> validate_required([:thing])
end
@doc "attach a photo to the provided Thing"
def create_photo(%Thing{} = thing, attrs \\ %{}) do
%Photo{}
|> Photo.changeset(attrs)
|> Ecto.Changeset.put_assoc(:thing, thing)
|> Repo.insert()
end
#+end_src
* Arc Definition
This =Arc.Definition= creates a pair of stripped images for each photo uploaded; I have the metadata-rich versions saved on my phone if I need them, and I'd rather not have things laying around that have my home GPS coordinates on 'em, you know? Some folks will balk at my only providing =1000x1000= max images, we'll see if I regret that!
#+begin_src elixir :noweb-ref arc-definition
use Arc.Definition
use Arc.Ecto.Definition
# Whitelist file extensions:
def validate({file, _}) do
~w(.jpg .jpeg .gif .png .webp) |> Enum.member?(Path.extname(file.file_name))
end
# Define a thumbnail transformation:
def transform(:thumb, _) do
{:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format jpg", :jpg}
end
def transform(:large, _) do
{:convert, "-strip -thumbnail 1000x000^ -gravity center -extent 1000x1000 -format jpg", :jpg}
end
# Define the filename of the images
def filename(version, {_, scope}) do
"#{scope.uuid}_#{version}"
end
# Override the storage directory:
def storage_dir(_, _) do
"uploads/photos/"
end
#+end_src
* Ecto Migration
#+begin_src elixir :tangle priv/repo/migrations/20200916234048_create_photo.exs
defmodule PhotoGallery.Repo.Migrations.CreatePhotos do
use Ecto.Migration
def change do
create table(:photos) do
add :photo, :string
add :uuid, :string
add :thing_id, references(:things, on_delete: :delete_all)
timestamps()
end
create index(:photos, [:thing_id])
end
end
#+end_src
* Assemblage
#+begin_src elixir :tangle lib/poka_ijo/photo.ex :noweb yes
defmodule PokaIjo.Photo do
alias PokaIjo.{Thing,Photo,Repo}
<<arc-definition>>
<<ecto-schema-and-fns>>
end
#+end_src

View File

@ -106,6 +106,7 @@ defmodule PokaIjoWeb.Endpoint do
<<plug-configurations>>
<<plug-session>>
<<Plug_Static>>
<<plug-uploads>>
plug PokaIjoWeb.Router
end
@ -121,6 +122,13 @@ plug Plug.Static,
only: ~w(css fonts images js favicon.ico robots.txt)
#+end_src
#+begin_src elixir :noweb-ref plug-uploads
plug Plug.Static,
at: "/uploads",
from: Path.expand("./uploads"),
gzip: false
#+end_src
Code Reloading is enabled in local development by inserting some plugs if the =code_reloading?/0= function from =Phoenix.Endpoint= returns true based on [[id:11e43150-7556-45a9-978e-05e942d40a68][Mix configuration]].
#+begin_src elixir :noweb-ref plug-configurations

View File

@ -672,7 +672,7 @@ defmodule WebControllersThingDoMoveTest do
assert Thing.get_by(id: t.id).container_id != t.id
assert conn.status == 302
assert get_flash(conn, :alert) =~ "failed"
assert get_flash(conn, :error) =~ "failed"
end
end
#+end_src