Skip to content

Commit

Permalink
added simple undo history feature, closes #6
Browse files Browse the repository at this point in the history
  • Loading branch information
ecly committed Jun 16, 2018
1 parent ba5b573 commit ba14cf4
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 39 deletions.
21 changes: 16 additions & 5 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,28 @@ label {
font-weight:normal;
}

#body {
text-align: center;
}

.tag_buttons {
border-style: solid;
border-width: 1px;
margin-left: 10px;
margin-right: 10px;
height: 10vh;
}

#buttons {
display: table;
margin: 0 auto;
position: relative;
}

.button {
height: 100px;
border-style: solid;
border-width: 1px;
margin: 10px;
#undo {
height: 10vh;
margin-top: 10px;
position: relative;
}

#images {
Expand Down
19 changes: 18 additions & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ class App {
var $good_button = $("#good")
var $bad_button = $("#bad")
var $next_button = $("#next")
var $undo_button = $("#undo")
var $images_left = $("#images_left")
var $reviewed_count = $("#reviewed_count")
var $online = $("#online")
var $overlay = $("#overlay")
var $overlay_image = $("#overlay_image")
var good_overlay_image = "images/good_overlay.png"
var bad_overlay_image = "images/banned_overlay.png"
var undo_overlay_image = "images/undo_overlay.png"

socket.onOpen( ev => console.log("SOCKET OPEN", ev) )
socket.onError( ev => console.log("SOCKET ERROR", ev) )
Expand Down Expand Up @@ -70,10 +72,15 @@ class App {
$reviewed_count.text(value);
}

var decrement_reviewed = function() {
var current = parseInt($reviewed_count.text(), 10);
var new_value = current <= 0 ? 0 : current - 1;
$reviewed_count.text(new_value);
}

var review = function(rating) {
var poll_next = $('#auto_next').is(":checked")
chan.push("submit_review", {review:rating, auto_next:poll_next});
increment_reviewed();
}

var poll_image= function() {
Expand All @@ -88,6 +95,8 @@ class App {
$bad_button.click();
} else if (e.keyCode == 39 || e.keyCode == 78) { // right arrow or n
$next_button.click();
} else if (e.keyCode == 37 || e.keyCode == 85) { // left arrow or u
$undo_button.click();
} else {
return true;
}
Expand All @@ -110,11 +119,19 @@ class App {
$good_button.click(function() {
show_overlay_image(good_overlay_image);
review("good");
increment_reviewed();
});

$bad_button.click(function() {
show_overlay_image(bad_overlay_image);
review("bad");
increment_reviewed();
});

$undo_button.click(function() {
show_overlay_image(undo_overlay_image);
chan.push("undo", {})
decrement_reviewed();
});

$next_button.click(function() { poll_image(); });
Expand Down
Binary file added assets/static/images/undo_button.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/static/images/undo_overlay.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion config/prod.secret.example.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ config :ex_aws,
access_key_id: ["", :instance_role],
secret_access_key: ["", :instance_role]

# The amount of reviews we allow the user to undo at most.
config :image_tagger, history_size: 5
config :image_tagger, update_interval_seconds: 1
config :image_tagger, bucket_name: "bucket"

config :image_tagger, image_folder: "to_review"

# Various tags. The tag should atom should correspond
Expand Down
20 changes: 20 additions & 0 deletions lib/image_tagger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ defmodule ImageTagger do
ExAws.S3.presigned_url(config, :get, bucket, image)
end

@doc """
Undoes the last review associated with the given reviewer.
A result tuple is returned contanining a presigned_url of the
image for which the tag was undone if any reviews are in the history
of the given reviwer, otherwise an error is returned.
## Examples
iex> ImageTagger.undo_last_review("reviewer_id")
{:ok, "www.s3.amazon.com/some_key/some_image.png"}
iex> ImageTagger.undo_last_review("reviewer_id")
{:error, "no images in history for given reviewer"}
"""
def undo_last_review(reviewer) do
case ReviewServer.undo_last_review(reviewer) do
{:ok, image} -> get_public_url(image)
{:error, reason} -> {:error, reason}
end
end


@doc """
Fetches an image to review for the given reviewer.
The image is polled from the ImageServer and added
Expand Down
117 changes: 101 additions & 16 deletions lib/image_tagger/review_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ defmodule ImageTagger.ReviewServer do
Server keeping track of all the images
that are currently being reviewed.
Implemented as a map of {<reviewer id> => image}
Implemented as a map of %{<reviewer id> => {current, history}},
where current is the path to the image currently being reviewed,
and history is a keyword list with the last X images of the form {image, tag}.
"""
alias ExAws
alias ImageTagger.ImageServer
Expand Down Expand Up @@ -45,7 +47,6 @@ defmodule ImageTagger.ReviewServer do
move_image_to_folder(image, folder)
end


@doc """
Adds an image to the ReviewServer signifying that it
is currently being reviewed. If the Reviewer is already associated
Expand All @@ -55,10 +56,16 @@ defmodule ImageTagger.ReviewServer do
"""
def handle_call({:add_image, reviewer, image}, _from, state) do
if Map.has_key?(state, reviewer) do
:ok = ImageServer.add_image(state[reviewer])
end
{current, history} = state[reviewer]

{:reply, :ok, Map.put(state, reviewer, image)}
if current != nil do
:ok = ImageServer.add_image(current)
end

{:reply, :ok, Map.put(state, reviewer, {image, history})}
else
{:reply, :ok, Map.put(state, reviewer, {image, []})}
end
end

@doc """
Expand All @@ -71,29 +78,84 @@ defmodule ImageTagger.ReviewServer do
Returns: :ok
"""
def handle_call({:review_image, reviewer, review}, _from, state) do
if Map.has_key?(state, reviewer) do
image = state[reviewer]
archive_image(image, review)
{:reply, :ok, Map.delete(state, reviewer)}
else
{:reply, :ok, state}
def handle_call({:review_image, reviewer, tag}, _from, state) do
case Map.get(state, reviewer) do
nil ->
{:reply, :ok, state}

{nil, history} ->
{:reply, :ok, state}

{current, history} ->
max_history = Application.fetch_env!(:image_tagger, :history_size)

review = {current, tag}

# If history exceeds max_history, we archive the oldest image
# in the history.
if length(history) >= max_history do
[{oldest_img, oldest_tag} | rest] = history
archive_image(oldest_img, oldest_tag)
new_reviewer_value = {nil, rest ++ [review]}
{:reply, :ok, Map.put(state, reviewer, new_reviewer_value)}
else
new_reviewer_value = {nil, history ++ [review]}
{:reply, :ok, Map.put(state, reviewer, new_reviewer_value)}
end
end
end

@doc """
Returns the size of the state.
Returns the size of the state, in the form of the amount of reviewers
currently stored by the ReviewServer.
"""
def handle_call(:get_count, _from, state) do
{:reply, map_size(state), state}
end

@doc """
Returns the values of the state, meaning all the images
associated with the currently connected reviewers.
associated with the currently connected reviewers. This includes
both the image currently being reviewed by each reviewer and their history.
"""
def handle_call(:get_images, _from, state) do
{:reply, Map.values(state), state}
current_images =
state
|> Map.values()
|> Enum.map(&elem(&1, 0))
|> Enum.filter(&(&1 != nil))

history_images =
state
|> Map.values()
|> Enum.flat_map(fn {_, history} -> Keyword.keys(history) end)

{:reply, current_images ++ history_images, state}
end

@doc """
Returns the values of the state, meaning all the images
associated with the currently connected reviewers. This includes
both the image currently being reviewed by each reviewer and their history.
"""
def handle_call({:undo_last_review, reviewer}, _from, state) do
case Map.get(state, reviewer) do
nil ->
{:reply, {:error, "no reviewer with given id"}, state}

{_, []} ->
{:reply, {:error, "no images in history for the given reviewer"}, state}

{current, history} ->
if current != nil do
:ok = ImageServer.add_image(current)
end

{undone_img, _tag} = List.last(history)
new_history = Enum.drop(history, -1)
new_state = Map.put(state, reviewer, {undone_img, new_history})
{:reply, {:ok, undone_img}, new_state}
end
end

@doc """
Expand All @@ -103,7 +165,13 @@ defmodule ImageTagger.ReviewServer do
"""
def handle_cast({:remove_reviewer, reviewer}, state) do
if Map.has_key?(state, reviewer) do
:ok = ImageServer.add_image(state[reviewer])
{current, history} = state[reviewer]

if current != nil do
:ok = ImageServer.add_image(current)
end

Enum.each(history, fn {img, tag} -> archive_image(img, tag) end)
end

{:noreply, Map.delete(state, reviewer)}
Expand Down Expand Up @@ -162,6 +230,23 @@ defmodule ImageTagger.ReviewServer do
GenServer.call(__MODULE__, {:review_image, reviewer, review})
end

@doc """
Adds a review for an image.
The image is removed from the ReviewServer and moved to
a folder according to the reivew.
## Examples
iex> ImageTagger.ReviewServer.undo_last_review("some_user_id")
{:ok, "to_review/some_image.png"}
:ok
iex> ImageTagger.ReviewServer.undo_last_review("some_user_id")
{:error, "no images in history for given reviewer"}
"""
def undo_last_review(reviewer) do
GenServer.call(__MODULE__, {:undo_last_review, reviewer})
end

@doc """
Associates a reviewer with an image.
currently being reviewed. If the reviewer is currently
Expand Down
27 changes: 19 additions & 8 deletions lib/image_tagger_web/channels/reviewer_channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,20 @@ defmodule ImageTaggerWeb.ReviewerChannel do
{:noreply, socket}
end


defp push_image_url_to_socket(socket, url) do
count = ImageTagger.images_left()
online = ImageTagger.reviewers_online()
msg = %{"url" => url, "count" => count, "online" => online}
push(socket, @new_image_event, msg)
end

# Pushes an image associated with the given socket if one is available
# in the ImageServer. Otherwise does nothing.
defp try_push_image_to_socket(socket) do
case ImageTagger.fetch_image_to_review(socket.assigns.id) do
{:ok, url} ->
count = ImageTagger.images_left()
online = ImageTagger.reviewers_online()
response = %{"url" => url, "count" => count, "online" => online}
push(socket, @new_image_event, response)

_otherwise ->
{:noreply, socket}
{:ok, url} -> push_image_url_to_socket(socket, url)
_otherwise -> nil
end
end

Expand All @@ -49,6 +51,15 @@ defmodule ImageTaggerWeb.ReviewerChannel do
{:noreply, socket}
end

@doc false
def handle_in("undo", _msg, socket) do
case ImageTagger.undo_last_review(socket.assigns.id) do
{:ok, url} -> push_image_url_to_socket(socket, url)
_otherwise -> nil
end
{:noreply, socket}
end

@doc false
def handle_in("submit_review", %{"review" => review_string, "auto_next" => get_next}, socket) do
# expected to be either :good or :bad
Expand Down
19 changes: 11 additions & 8 deletions lib/image_tagger_web/templates/page/index.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
<img id="overlay_image" src=""/>
</div>
</div>
</br><hr>
<div id="buttons">
<input class="button" type="image" id="bad" src="images/bad_button.jpg">
<input class="button" type="image" id="good" src="images/good_button.png">
<hr>
<div id="tag_buttons">
<input class="tag_buttons" type="image" id="bad" src="images/bad_button.jpg">
<input class="tag_buttons" type="image" id="good" src="images/good_button.png">
<br>
<input class="button" type="image" id="undo" src="images/undo_button.png">
</div>
<br>

Expand All @@ -17,10 +19,11 @@
<br>
</div>

<div style="position:fixed;top:5px;right:5px;">
<span>Emoji, up or h for non-cheat images</span><br>
<span>Hammer, down or j for cheat images</span><br>
<div style="position:fixed;top:5px;right:5px;text-align:right">
<span>Emoji, up or h for non-cheat image</span><br>
<span>Hammer, down or j for cheat image</span><br>
<span>Next, right or n for next image</span><br>
<span>Undo, left or u for previous image</span><br>
</div>

<div style="position:fixed;bottom:5px;right:5px;">
Expand All @@ -29,7 +32,7 @@
<input type="checkbox" id="auto_next" checked></input>
</div>

<div style="position:fixed;bottom:5px;left:5px;">
<div style="position:fixed;bottom:5px;left:5px;text-align:left">
<label for="online">Reviewers online:</label>
<b><span id="online">1</span></b>
<br>
Expand Down

0 comments on commit ba14cf4

Please sign in to comment.