diff --git a/config/config.exs b/config/config.exs index 8a431b66..d3a3bd7e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,10 +8,9 @@ use Mix.Config config :philomena, - ecto_repos: [Philomena.Repo] - -config :philomena, - password_pepper: "dn2e0EpZrvBLoxUM3gfQveBhjf0bG/6/bYhrOyq3L3hV9hdo/bimJ+irbDWsuXLP" + ecto_repos: [Philomena.Repo], + password_pepper: "dn2e0EpZrvBLoxUM3gfQveBhjf0bG/6/bYhrOyq3L3hV9hdo/bimJ+irbDWsuXLP", + image_url_root: "/img" config :philomena, :pow, user: Philomena.Users.User, diff --git a/config/prod.secret.exs b/config/prod.secret.exs index 76b24d67..0e250c91 100644 --- a/config/prod.secret.exs +++ b/config/prod.secret.exs @@ -15,12 +15,8 @@ config :bcrypt_elixir, log_rounds: String.to_integer(System.get_env("BCRYPT_ROUNDS") || "12") config :philomena, - password_pepper: - System.get_env("PASSWORD_PEPPER") || - raise(""" - environment variable PASSWORD_PEPPER is missing. - You can generate one by calling: mix phx.gen.secret - """) + password_pepper: System.get_env("PASSWORD_PEPPER"), + image_url_root: System.get_env("IMAGE_URL_ROOT") config :philomena, Philomena.Repo, # ssl: true, diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex new file mode 100644 index 00000000..631fb49f --- /dev/null +++ b/lib/philomena/images.ex @@ -0,0 +1,104 @@ +defmodule Philomena.Images do + @moduledoc """ + The Images context. + """ + + import Ecto.Query, warn: false + alias Philomena.Repo + + alias Philomena.Images.Image + + @doc """ + Returns the list of images. + + ## Examples + + iex> list_images() + [%Image{}, ...] + + """ + def list_images do + Repo.all(Image |> where(hidden_from_users: false) |> limit(25)) + end + + @doc """ + Gets a single image. + + Raises `Ecto.NoResultsError` if the Image does not exist. + + ## Examples + + iex> get_image!(123) + %Image{} + + iex> get_image!(456) + ** (Ecto.NoResultsError) + + """ + def get_image!(id), do: Repo.get!(Image, id) + + @doc """ + Creates a image. + + ## Examples + + iex> create_image(%{field: value}) + {:ok, %Image{}} + + iex> create_image(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_image(attrs \\ %{}) do + %Image{} + |> Image.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a image. + + ## Examples + + iex> update_image(image, %{field: new_value}) + {:ok, %Image{}} + + iex> update_image(image, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_image(%Image{} = image, attrs) do + image + |> Image.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a Image. + + ## Examples + + iex> delete_image(image) + {:ok, %Image{}} + + iex> delete_image(image) + {:error, %Ecto.Changeset{}} + + """ + def delete_image(%Image{} = image) do + Repo.delete(image) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking image changes. + + ## Examples + + iex> change_image(image) + %Ecto.Changeset{source: %Image{}} + + """ + def change_image(%Image{} = image) do + Image.changeset(image, %{}) + end +end diff --git a/lib/philomena/images/image.ex b/lib/philomena/images/image.ex new file mode 100644 index 00000000..d725fbfd --- /dev/null +++ b/lib/philomena/images/image.ex @@ -0,0 +1,82 @@ +defmodule Philomena.Images.Image do + use Ecto.Schema + import Ecto.Changeset + + schema "images" do + belongs_to :user, Philomena.Users.User + belongs_to :deleter, Philomena.Users.User, source: :deleted_by_id + + field :image, :string + field :image_name, :string + field :image_width, :integer + field :image_height, :integer + field :image_size, :integer + field :image_format, :string + field :image_mime_type, :string + field :image_aspect_ratio, :float + field :ip, EctoNetwork.INET + field :fingerprint, :string + field :user_agent, :string, default: "" + field :referrer, :string, default: "" + field :anonymous, :boolean, default: false + field :score, :integer, default: 0 + field :faves_count, :integer, default: 0 + field :upvotes_count, :integer, default: 0 + field :downvotes_count, :integer, default: 0 + field :votes_count, :integer, default: 0 + field :source_url, :string + field :description, :string, default: "" + field :image_sha512_hash, :string + field :image_orig_sha512_hash, :string + field :deletion_reason, :string + field :duplicate_id, :integer + field :comments_count, :integer, default: 0 + field :processed, :boolean, default: false + field :thumbnails_generated, :boolean, default: false + field :duplication_checked, :boolean, default: false + field :hidden_from_users, :boolean, default: false + field :tag_editing_allowed, :boolean, default: true + field :description_editing_allowed, :boolean, default: true + field :commenting_allowed, :boolean, default: true + field :is_animated, :boolean + field :first_seen_at, :naive_datetime + field :destroyed_content, :boolean + field :hidden_image_key, :string + field :scratchpad, :string + field :hides_count, :integer, default: 0 + + # todo: can probably remove these now + # field :tag_list_cache, :string + # field :tag_list_plus_alias_cache, :string + # field :file_name_cache, :string + + timestamps(inserted_at: :created_at) + end + + @doc false + def changeset(image, attrs) do + image + |> cast(attrs, []) + |> validate_required([]) + end + + def thumb_url(image, show_hidden, name) do + %{year: year, month: month, day: day} = image.created_at + deleted = image.hidden_from_users + format = image.image_format + root = image_url_root() + + id_fragment = + if deleted and show_hidden do + "#{image.id}-#{image.hidden_image_Key}" + else + "#{image.id}" + end + + "#{root}/#{year}/#{month}/#{day}/#{id_fragment}/#{name}.#{format}" + end + + defp image_url_root do + Application.get_env(:philomena, :image_url_root) + end +end diff --git a/lib/philomena_web/controllers/image_controller.ex b/lib/philomena_web/controllers/image_controller.ex new file mode 100644 index 00000000..acba3904 --- /dev/null +++ b/lib/philomena_web/controllers/image_controller.ex @@ -0,0 +1,62 @@ +defmodule PhilomenaWeb.ImageController do + use PhilomenaWeb, :controller + + alias Philomena.Images + alias Philomena.Images.Image + + def index(conn, _params) do + images = Images.list_images() + render(conn, "index.html", images: images) + end + + def new(conn, _params) do + changeset = Images.change_image(%Image{}) + render(conn, "new.html", changeset: changeset) + end + + def create(conn, %{"image" => image_params}) do + case Images.create_image(image_params) do + {:ok, image} -> + conn + |> put_flash(:info, "Image created successfully.") + |> redirect(to: Routes.image_path(conn, :show, image)) + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, "new.html", changeset: changeset) + end + end + + def show(conn, %{"id" => id}) do + image = Images.get_image!(id) + render(conn, "show.html", image: image) + end + + def edit(conn, %{"id" => id}) do + image = Images.get_image!(id) + changeset = Images.change_image(image) + render(conn, "edit.html", image: image, changeset: changeset) + end + + def update(conn, %{"id" => id, "image" => image_params}) do + image = Images.get_image!(id) + + case Images.update_image(image, image_params) do + {:ok, image} -> + conn + |> put_flash(:info, "Image updated successfully.") + |> redirect(to: Routes.image_path(conn, :show, image)) + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, "edit.html", image: image, changeset: changeset) + end + end + + def delete(conn, %{"id" => id}) do + image = Images.get_image!(id) + {:ok, _image} = Images.delete_image(image) + + conn + |> put_flash(:info, "Image deleted successfully.") + |> redirect(to: Routes.image_path(conn, :index)) + end +end diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 59a8ac87..d4a953f2 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -24,6 +24,8 @@ defmodule PhilomenaWeb.Router do pipe_through :browser get "/", PageController, :index + + resources "/images", ImageController, only: [:index, :show] end # Other scopes may use custom stacks. diff --git a/lib/philomena_web/templates/image/_image_box.html.slime b/lib/philomena_web/templates/image/_image_box.html.slime new file mode 100644 index 00000000..f07af525 --- /dev/null +++ b/lib/philomena_web/templates/image/_image_box.html.slime @@ -0,0 +1,18 @@ +.media-box data-image-id=@image.id + .media-box__header.media-box__header--link-row data-image-id=@image.id + a.interaction--fave href="#" rel="nofollow" data-image-id=@image.id + span.fave-span title='Fave!' + i.fa.fa-star + span.favorites title='Favorites' data-image-id=@image.id = @image.faves_count + a.interaction--upvote href="#" rel="nofollow" data-image-id=@image.id + i.fa.fa-arrow-up title='Yay!' + span.score title='Score' data-image-id=@image.id = @image.score + a.interaction--downvote href="#" rel="nofollow" data-image-id=@image.id + i.fa.fa-arrow-down title='Neigh!' + a.interaction--comments href="/#{@image.id}#comments" title='Comments' + i.fa.fa-comments + span.comments_count data-image-id=@image.id = @image.comments_count + a.interaction--hide href="#" rel="nofollow" data-image-id=@image.id + i.fa.fa-eye-slash title='Hide' + .media-box__content.media-box__content--large.flex.flex--centered.flex--center-distributed + = render PhilomenaWeb.ImageView, "_image_container.html", image: @image diff --git a/lib/philomena_web/templates/image/_image_container.html.slime b/lib/philomena_web/templates/image/_image_container.html.slime new file mode 100644 index 00000000..cec90171 --- /dev/null +++ b/lib/philomena_web/templates/image/_image_container.html.slime @@ -0,0 +1,20 @@ +.image-container.thumb + = cond do + - @image.duplicate_id -> + .media-box__overlay + strong Marked Duplicate + - @image.destroyed_content -> + .media-box__overlay + strong Destroyed Content + - @image.hidden_from_users -> + .media-box__overlay + strong Deleted: + =< @image.deletion_reason + - true -> + + .media-box__overlay.js-spoiler-info-overlay + a href="/#{@image.id}" + = if @image.thumbnails_generated do + picture: img src=thumb_url(@image, false, :thumb) + - else + | Thumbnails not yet generated diff --git a/lib/philomena_web/templates/image/edit.html.eex b/lib/philomena_web/templates/image/edit.html.eex new file mode 100644 index 00000000..595c1aac --- /dev/null +++ b/lib/philomena_web/templates/image/edit.html.eex @@ -0,0 +1,5 @@ +

Edit Image

+ +<%= render "form.html", Map.put(assigns, :action, Routes.image_path(@conn, :update, @image)) %> + +<%= link "Back", to: Routes.image_path(@conn, :index) %> diff --git a/lib/philomena_web/templates/image/form.html.eex b/lib/philomena_web/templates/image/form.html.eex new file mode 100644 index 00000000..a8b33630 --- /dev/null +++ b/lib/philomena_web/templates/image/form.html.eex @@ -0,0 +1,11 @@ +<%= form_for @changeset, @action, fn f -> %> + <%= if @changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + +
+ <%= submit "Save" %> +
+<% end %> diff --git a/lib/philomena_web/templates/image/index.html.slime b/lib/philomena_web/templates/image/index.html.slime new file mode 100644 index 00000000..3084ef11 --- /dev/null +++ b/lib/philomena_web/templates/image/index.html.slime @@ -0,0 +1,4 @@ +.block#imagelist-container + .block__content.js-resizable-media-container + = for image <- @images do + = render PhilomenaWeb.ImageView, "_image_box.html", image: image diff --git a/lib/philomena_web/templates/image/new.html.eex b/lib/philomena_web/templates/image/new.html.eex new file mode 100644 index 00000000..cf0236b4 --- /dev/null +++ b/lib/philomena_web/templates/image/new.html.eex @@ -0,0 +1,5 @@ +

New Image

+ +<%= render "form.html", Map.put(assigns, :action, Routes.image_path(@conn, :create)) %> + +<%= link "Back", to: Routes.image_path(@conn, :index) %> diff --git a/lib/philomena_web/templates/image/show.html.eex b/lib/philomena_web/templates/image/show.html.eex new file mode 100644 index 00000000..7f42c6f8 --- /dev/null +++ b/lib/philomena_web/templates/image/show.html.eex @@ -0,0 +1,8 @@ +

Show Image

+ + + +<%= link "Edit", to: Routes.image_path(@conn, :edit, @image) %> +<%= link "Back", to: Routes.image_path(@conn, :index) %> diff --git a/lib/philomena_web/views/image_view.ex b/lib/philomena_web/views/image_view.ex new file mode 100644 index 00000000..6708d57d --- /dev/null +++ b/lib/philomena_web/views/image_view.ex @@ -0,0 +1,9 @@ +defmodule PhilomenaWeb.ImageView do + use PhilomenaWeb, :view + + alias Philomena.Images.Image + + def thumb_url(image, show_hidden, name) do + Image.thumb_url(image, show_hidden, name) + end +end diff --git a/lib/philomena_web/views/layout_view.ex b/lib/philomena_web/views/layout_view.ex index 336199d7..3a259536 100644 --- a/lib/philomena_web/views/layout_view.ex +++ b/lib/philomena_web/views/layout_view.ex @@ -9,7 +9,7 @@ defmodule PhilomenaWeb.LayoutView do def hostname() do {:ok, host} = :inet.gethostname() - + host |> to_string end end diff --git a/test/philomena/images_test.exs b/test/philomena/images_test.exs new file mode 100644 index 00000000..a9c269ce --- /dev/null +++ b/test/philomena/images_test.exs @@ -0,0 +1,62 @@ +defmodule Philomena.ImagesTest do + use Philomena.DataCase + + alias Philomena.Images + + describe "images" do + alias Philomena.Images.Image + + @valid_attrs %{} + @update_attrs %{} + @invalid_attrs %{} + + def image_fixture(attrs \\ %{}) do + {:ok, image} = + attrs + |> Enum.into(@valid_attrs) + |> Images.create_image() + + image + end + + test "list_images/0 returns all images" do + image = image_fixture() + assert Images.list_images() == [image] + end + + test "get_image!/1 returns the image with given id" do + image = image_fixture() + assert Images.get_image!(image.id) == image + end + + test "create_image/1 with valid data creates a image" do + assert {:ok, %Image{} = image} = Images.create_image(@valid_attrs) + end + + test "create_image/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Images.create_image(@invalid_attrs) + end + + test "update_image/2 with valid data updates the image" do + image = image_fixture() + assert {:ok, %Image{} = image} = Images.update_image(image, @update_attrs) + end + + test "update_image/2 with invalid data returns error changeset" do + image = image_fixture() + assert {:error, %Ecto.Changeset{}} = Images.update_image(image, @invalid_attrs) + assert image == Images.get_image!(image.id) + end + + test "delete_image/1 deletes the image" do + image = image_fixture() + assert {:ok, %Image{}} = Images.delete_image(image) + assert_raise Ecto.NoResultsError, fn -> Images.get_image!(image.id) end + end + + test "change_image/1 returns a image changeset" do + image = image_fixture() + assert %Ecto.Changeset{} = Images.change_image(image) + end + end +end diff --git a/test/philomena_web/controllers/image_controller_test.exs b/test/philomena_web/controllers/image_controller_test.exs new file mode 100644 index 00000000..28f1c33d --- /dev/null +++ b/test/philomena_web/controllers/image_controller_test.exs @@ -0,0 +1,89 @@ +defmodule PhilomenaWeb.ImageControllerTest do + use PhilomenaWeb.ConnCase + + alias Philomena.Images + + @create_attrs %{} + @update_attrs %{} + @invalid_attrs %{} + + def fixture(:image) do + {:ok, image} = Images.create_image(@create_attrs) + image + end + + describe "index" do + test "lists all images", %{conn: conn} do + conn = get(conn, Routes.image_path(conn, :index)) + assert html_response(conn, 200) =~ "Listing Images" + end + end + + describe "new image" do + test "renders form", %{conn: conn} do + conn = get(conn, Routes.image_path(conn, :new)) + assert html_response(conn, 200) =~ "New Image" + end + end + + describe "create image" do + test "redirects to show when data is valid", %{conn: conn} do + conn = post(conn, Routes.image_path(conn, :create), image: @create_attrs) + + assert %{id: id} = redirected_params(conn) + assert redirected_to(conn) == Routes.image_path(conn, :show, id) + + conn = get(conn, Routes.image_path(conn, :show, id)) + assert html_response(conn, 200) =~ "Show Image" + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = post(conn, Routes.image_path(conn, :create), image: @invalid_attrs) + assert html_response(conn, 200) =~ "New Image" + end + end + + describe "edit image" do + setup [:create_image] + + test "renders form for editing chosen image", %{conn: conn, image: image} do + conn = get(conn, Routes.image_path(conn, :edit, image)) + assert html_response(conn, 200) =~ "Edit Image" + end + end + + describe "update image" do + setup [:create_image] + + test "redirects when data is valid", %{conn: conn, image: image} do + conn = put(conn, Routes.image_path(conn, :update, image), image: @update_attrs) + assert redirected_to(conn) == Routes.image_path(conn, :show, image) + + conn = get(conn, Routes.image_path(conn, :show, image)) + assert html_response(conn, 200) + end + + test "renders errors when data is invalid", %{conn: conn, image: image} do + conn = put(conn, Routes.image_path(conn, :update, image), image: @invalid_attrs) + assert html_response(conn, 200) =~ "Edit Image" + end + end + + describe "delete image" do + setup [:create_image] + + test "deletes chosen image", %{conn: conn, image: image} do + conn = delete(conn, Routes.image_path(conn, :delete, image)) + assert redirected_to(conn) == Routes.image_path(conn, :index) + + assert_error_sent 404, fn -> + get(conn, Routes.image_path(conn, :show, image)) + end + end + end + + defp create_image(_) do + image = fixture(:image) + {:ok, image: image} + end +end diff --git a/vagrant/philomena-nginx.conf b/vagrant/philomena-nginx.conf index b37e1a51..782981f0 100644 --- a/vagrant/philomena-nginx.conf +++ b/vagrant/philomena-nginx.conf @@ -8,6 +8,43 @@ server { root APP_DIR/priv/static; + location ~ ^/img/view/(.+)/([0-9]+).*\.([A-Za-z]+)$ { + expires max; + add_header Cache-Control public; + alias "APP_DIR/priv/static/images/thumbs/$1/$2/full.$3"; + } + + location ~ ^/img/download/(.+)/([0-9]+).*\.([A-Za-z]+)$ { + add_header Content-Disposition "attachment"; + expires max; + add_header Cache-Control public; + alias "APP_DIR/priv/static/system/images/thumbs/$1/$2/full.$3"; + } + + location ~ ^/img/(.+) { + expires max; + add_header Cache-Control public; + alias APP_DIR/priv/static/system/images/thumbs/$1; + } + + location ~ ^/spns/(.+) { + expires max; + add_header Cache-Control public; + alias APP_DIR/priv/static/system/images/adverts/$1; + } + + location ~ ^/avatars/(.+) { + expires max; + add_header Cache-Control public; + alias APP_DIR/priv/static/system/images/avatars/$1; + } + + location ~ ^/media/(.+) { + expires max; + add_header Cache-Control public; + alias APP_DIR/priv/static/system/images/$1; + } + location / { try_files $uri @proxy; }