Skip to content

Commit

Permalink
UI Polish, end to end submissions and voting
Browse files Browse the repository at this point in the history
* Users who aren't signed in can't vote.
* Signed in users can vote, vote detail is written to Riak.
* Vote queries now come out of Riak thanks to 2i and map/red.
* Sorting of submissions on the user page now works.

TODO: just a little bit of polishing of code, commenting things
and tidying up bits here and there. Then it's time to start work
on the blog post which describes all the changes... there are a
LOT!
  • Loading branch information
OJ committed May 1, 2012
1 parent cf7d583 commit acf46cb
Show file tree
Hide file tree
Showing 16 changed files with 560 additions and 84 deletions.
61 changes: 59 additions & 2 deletions apps/csd_core/src/csd_core_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
%% API Function Exports
%% ------------------------------------------------------------------

-export([start_link/0, get_snippet/1, save_snippet/1, list_snippets/1]).
-export([start_link/0]).
-export([get_snippet/1, save_snippet/1, list_snippets/1]).
-export([get_user/1, save_user/1]).
-export([get_vote/1, save_vote/1, vote_count_for_snippet/1, vote_count_for_snippet/2]).

%% ------------------------------------------------------------------
%% gen_server Function Exports
Expand All @@ -18,7 +21,7 @@
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

%% ------------------------------------------------------------------
%% API Function Definitions
%% Snippet API Function Definitions
%% ------------------------------------------------------------------

start_link() ->
Expand All @@ -33,6 +36,32 @@ get_snippet(SnippetKey) ->
list_snippets(UserId) ->
gen_server:call(?SERVER, {list_snippets, UserId}, infinity).

%% ------------------------------------------------------------------
%% User API Function Definitions
%% ------------------------------------------------------------------

get_user(UserId) ->
gen_server:call(?SERVER, {get_user, UserId}, infinity).

save_user(User) ->
gen_server:call(?SERVER, {save_user, User}, infinity).

%% ------------------------------------------------------------------
%% Vote API Function Definitions
%% ------------------------------------------------------------------

get_vote(VoteId) ->
gen_server:call(?SERVER, {get_vote, VoteId}, infinity).

save_vote(Vote) ->
gen_server:call(?SERVER, {save_vote, Vote}, infinity).

vote_count_for_snippet(SnippetId) ->
gen_server:call(?SERVER, {vote_count_for_snippet, SnippetId}, infinity).

vote_count_for_snippet(SnippetId, UserId) ->
gen_server:call(?SERVER, {vote_count_for_snippet, SnippetId, UserId}, infinity).

%% ------------------------------------------------------------------
%% gen_server Function Definitions
%% ------------------------------------------------------------------
Expand All @@ -52,6 +81,34 @@ handle_call({list_snippets, UserId}, _From, State) ->
Snippet = pooler:use_member(fun(RiakPid) -> csd_snippet_store:list_for_user(RiakPid, UserId) end),
{reply, Snippet, State};

handle_call({save_user, User}, _From, State) ->
SavedUser = pooler:use_member(fun(RiakPid) -> csd_user_store:save(RiakPid, User) end),
{reply, SavedUser, State};

handle_call({get_user, UserId}, _From, State) ->
User = pooler:use_member(fun(RiakPid) -> csd_user_store:fetch(RiakPid, UserId) end),
{reply, User, State};

handle_call({get_vote, VoteId}, _From, State) ->
Vote = pooler:use_member(fun(RiakPid) -> csd_vote_store:fetch(RiakPid, VoteId) end),
{reply, Vote, State};

handle_call({save_vote, Vote}, _From, State) ->
SavedVote = pooler:use_member(fun(RiakPid) -> csd_vote_store:save(RiakPid, Vote) end),
{reply, SavedVote, State};

handle_call({vote_count_for_snippet, SnippetId}, _From, State) ->
VoteCount = pooler:use_member(fun(RiakPid) ->
csd_vote_store:count_for_snippet(RiakPid, SnippetId)
end),
{reply, VoteCount, State};

handle_call({vote_count_for_snippet, SnippetId, UserId}, _From, State) ->
VoteCount = pooler:use_member(fun(RiakPid) ->
csd_vote_store:count_for_snippet(RiakPid, SnippetId, UserId)
end),
{reply, VoteCount, State};

handle_call(_Request, _From, State) ->
{noreply, ok, State}.

Expand Down
70 changes: 50 additions & 20 deletions apps/csd_core/src/csd_riak.erl
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,21 @@
update/2,
get_value/1,
save/2,
get_index_int/2,
set_index_int/3,
get_index/3,
set_index/4,
set_indexes/2,
mapred/3,
get_mapred_phase_input_index_int/3,
get_mapred_phase_input_index/4,
get_mapred_phase_map_js/1,
get_mapred_phase_map_js/2,
get_mapred_phase_map_js/3,
get_mapred_phase_reduce_js/1,
get_mapred_phase_reduce_js/2,
get_mapred_phase_reduce_js/3,
get_mapred_reduce_sort_js/1,
get_mapred_reduce_sort_js/2
get_mapred_reduce_sort_js/2,
get_mapred_reduce_sum_js/0,
get_mapred_reduce_sum_js/1
]).

%% helper functions for generating unique keys.
Expand Down Expand Up @@ -79,26 +82,39 @@ update(RiakObj, NewValue) ->
NewRiakObj = riakc_obj:update_value(RiakObj, NewValue),
NewRiakObj.

%% @spec set_index_int(riakc_obj(), string, term()) -> riakc_obj()
%% @spec set_index_int(riakc_obj(), string, int) -> riakc_obj()
%% @doc Adds an integer index of the given name to the object's
%% metadata and returns the updated object.
set_index_int(RiakObj, Name, Value) ->
set_index(RiakObj, Type, Name, Value) ->
Meta = riakc_obj:get_update_metadata(RiakObj),
Index = case dict:find(?INDEX_KEY, Meta) of
error -> [];
I -> I
{ok, I} -> I
end,
NewIndex = dict:to_list(dict:store(int_index(Name), Value, dict:from_list(Index))),
NewIndex = dict:to_list(dict:store(index(Type, Name), value(Value), dict:from_list(Index))),
riakc_obj:update_metadata(RiakObj, dict:store(?INDEX_KEY, NewIndex, Meta)).

set_indexes(RiakObj, Indexes) ->
Meta = riakc_obj:get_update_metadata(RiakObj),
Index = case dict:find(?INDEX_KEY, Meta) of
error -> [];
{ok, I} -> I
end,
UpdatedIndexes = lists:foldl(fun({T, N, V}, I) ->
dict:store(index(T, N), value(V), I)
end,
dict:from_list(Index), Indexes),
NewIndex = dict:to_list(UpdatedIndexes),
riakc_obj:update_metadata(RiakObj, dict:store(?INDEX_KEY, NewIndex, Meta)).

%% @spec get_index_int(riakc_obj(), string) -> int
%% @doc Queries the object meta data to pull out an index of
%% integer type. Assumes that the index exists, expect
%% failure when querying when metadata/index missing.
get_index_int(RiakObj, Name) ->
get_index(RiakObj, Type, Name) ->
Meta = riakc_obj:get_metadata(RiakObj),
Indexes = dict:fetch(?INDEX_KEY, Meta),
proplists:get_value(int_index(Name), Indexes).
proplists:get_value(index(Type, Name), Indexes).

%% @spec get_value(riakc_obj()) -> term()
%% @doc Retrieves the stored value from within the riakc
Expand All @@ -117,12 +133,12 @@ mapred(RiakPid, Input, Phases) when is_list(Phases) ->
Result = riakc_pb_socket:mapred(RiakPid, Input, Phases),
Result.

get_mapred_phase_input_index_int(Bucket, Index, Value) when is_integer(Value) ->
get_mapred_phase_input_index_int(Bucket, Index, integer_to_list(Value));
get_mapred_phase_input_index_int(Bucket, Index, Value) when is_list(Value) ->
get_mapred_phase_input_index_int(Bucket, Index, list_to_binary(Value));
get_mapred_phase_input_index_int(Bucket, Index, Value) when is_binary(Value) ->
{index, Bucket, list_to_binary(int_index(Index)), Value}.
get_mapred_phase_input_index(Bucket, Type, Index, Value) when is_integer(Value) ->
get_mapred_phase_input_index(Bucket, Type, Index, integer_to_list(Value));
get_mapred_phase_input_index(Bucket, Type, Index, Value) when is_list(Value) ->
get_mapred_phase_input_index(Bucket, Type, Index, list_to_binary(Value));
get_mapred_phase_input_index(Bucket, Type, Index, Value) when is_binary(Value) ->
{index, Bucket, index(Type, Index), Value}.

get_mapred_phase_map_js(JsSource) ->
get_mapred_phase_map_js(JsSource, true).
Expand All @@ -131,7 +147,7 @@ get_mapred_phase_map_js(JsSource, Keep) ->
get_mapred_phase_map_js(JsSource, Keep, none).

get_mapred_phase_map_js(JsSource, Keep, Arg) ->
get_mapred_phase_map({jsanon, JsSource}, Arg, Keep).
get_mapred_phase_map({jsanon, JsSource}, Keep, Arg).

get_mapred_phase_map(Fun, Keep, Arg) ->
{map, Fun, Arg, Keep}.
Expand All @@ -143,7 +159,7 @@ get_mapred_phase_reduce_js(JsSource, Keep) ->
get_mapred_phase_reduce_js(JsSource, Keep, none).

get_mapred_phase_reduce_js(JsSource, Keep, Arg) ->
get_mapred_phase_reduce({jsanon, JsSource}, Arg, Keep).
get_mapred_phase_reduce({jsanon, JsSource}, Keep, Arg).

get_mapred_phase_reduce(Fun, Keep, Arg) ->
{reduce, Fun, Arg, Keep}.
Expand All @@ -154,6 +170,12 @@ get_mapred_reduce_sort_js(CompareFun) ->
get_mapred_reduce_sort_js(CompareFun, Keep) ->
{reduce, {jsfun, <<"Riak.reduceSort">>}, CompareFun, Keep}.

get_mapred_reduce_sum_js() ->
get_mapred_reduce_sum_js(true).

get_mapred_reduce_sum_js(Keep) ->
{reduce, {jsfun, <<"Riak.reduceSum">>}, <<"">>, Keep}.

%% @spec new_key() -> key()
%% @doc Generate an close-to-unique key that can be used to identify
%% an object in riak. This implementation is blatantly borrowed
Expand All @@ -171,5 +193,13 @@ new_key(List) ->
Hash = erlang:phash2(List),
base64:encode(<<Hash:32>>).

int_index(Name) ->
Name ++ ?INDEX_SUFFIX_INT.
index(int, Name) ->
iolist_to_binary([Name, ?INDEX_SUFFIX_INT]);
index(bin, Name) ->
iolist_to_binary([Name, ?INDEX_SUFFIX_BIN]).

value(V) when is_list(V) ->
list_to_binary(V);
value(V) ->
V.

2 changes: 2 additions & 0 deletions apps/csd_core/src/csd_snippet.erl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
-record(snippet, {
user_id,
key,
votes_left = 0,
votes_right = 0,
data
}).

Expand Down
11 changes: 6 additions & 5 deletions apps/csd_core/src/csd_snippet_store.erl
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
-module(csd_snippet_store).
-author('OJ Reeves <oj@buffered.io>').

-export([save/2, fetch/2, list_for_user/2]).

-define(BUCKET, <<"snippet">>).
-define(USERID_INDEX, "userid").
-define(LIST_MAP_JS, <<"function(v){var d = Riak.mapValuesJson(v)[0]; return [{key:d.key,title:d.title,created:d.created}];}">>).
-define(REDUCE_SORT_JS, <<"function(a,b){return new Date(a.created)-new Date(b.created);}">>).
%-define(REDUCE_SORT_JS, <<"function(v,args){v.sort(function(a,b){return new Date(a.created)-new Date(b.created);});return v;}">>).
-define(REDUCE_SORT_JS, <<"function(a,b){return a.created<b.created?1:(a.created>b.created?-1:0);}">>).

fetch(RiakPid, Key) ->
case csd_riak:fetch(RiakPid, ?BUCKET, Key) of
{ok, RiakObj} ->
SnippetJson = csd_riak:get_value(RiakObj),
Snippet = csd_snippet:from_json(SnippetJson),
UserId = csd_riak:get_index_int(RiakObj, ?USERID_INDEX),
UserId = csd_riak:get_index(RiakObj, int, ?USERID_INDEX),
{ok, csd_snippet:set_user_id(Snippet, UserId)};
{error, Reason} ->
{error, Reason}
Expand All @@ -30,7 +31,7 @@ save(RiakPid, Snippet) ->
end.

list_for_user(RiakPid, UserId) ->
Index = csd_riak:get_mapred_phase_input_index_int(?BUCKET, ?USERID_INDEX, UserId),
Index = csd_riak:get_mapred_phase_input_index(?BUCKET, int, ?USERID_INDEX, UserId),
Map = csd_riak:get_mapred_phase_map_js(?LIST_MAP_JS, false),
Sort = csd_riak:get_mapred_reduce_sort_js(?REDUCE_SORT_JS),
%Sort = csd_riak:get_mapred_phase_reduce_js(?REDUCE_SORT_JS),
Expand All @@ -42,6 +43,6 @@ list_for_user(RiakPid, UserId) ->

persist(RiakPid, RiakObj, Snippet) ->
UserId = csd_snippet:get_user_id(Snippet),
UpdatedRiakObj = csd_riak:set_index_int(RiakObj, ?USERID_INDEX, UserId),
UpdatedRiakObj = csd_riak:set_index(RiakObj, int, ?USERID_INDEX, UserId),
ok = csd_riak:save(RiakPid, UpdatedRiakObj),
{ok, Snippet}.
41 changes: 41 additions & 0 deletions apps/csd_core/src/csd_user.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-module(csd_user).
-author('OJ Reeves <oj@buffered.io>').

-export([get_id/1, fetch/1, save/1, to_user/2, from_json/1, to_json/1]).

-record(user, {
id,
name,
joined
}).

to_user(Id, Name) ->
#user{
name = Name,
id = Id,
joined = csd_date:utc_now()
}.

get_id(#user{id=Id}) ->
Id.

fetch(Id) ->
csd_core_server:get_user(Id).

save(User=#user{}) ->
csd_core_server:save_user(User).

to_json(#user{name=N, id=T, joined=J}) ->
csd_json:to_json([{name, N}, {id, T}, {joined, J}], fun is_string/1).

from_json(UserJson) ->
User = csd_json:from_json(UserJson, fun is_string/1),
#user{
id = proplists:get_value(User, id),
name = proplists:get_value(User, name),
joined = proplists:get_value(User, joined)
}.

is_string(name) -> true;
is_string(joined) -> true;
is_string(_) -> false.
37 changes: 37 additions & 0 deletions apps/csd_core/src/csd_user_store.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-module(csd_user_store).
-author('OJ Reeves <oj@buffered.io>').

-export([fetch/2, save/2]).

-define(BUCKET, <<"user">>).

fetch(RiakPid, UserId) when is_integer(UserId) ->
fetch(RiakPid, integer_to_list(UserId));
fetch(RiakPid, UserId) when is_list(UserId) ->
fetch(RiakPid, list_to_binary(UserId));
fetch(RiakPid, UserId) when is_binary(UserId) ->
case csd_riak:fetch(RiakPid, ?BUCKET, UserId) of
{ok, RiakObj} ->
UserJson = csd_riak:get_value(RiakObj),
User = csd_user:from_json(UserJson),
{ok, User};
{error, Reason} ->
{error, Reason}
end.

save(RiakPid, User) ->
IntId = csd_user:get_id(User),

% Id is int, so we need to conver to a binary
UserId = list_to_binary(integer_to_list(IntId)),

case csd_riak:fetch(RiakPid, ?BUCKET, UserId) of
{ok, RiakObj} ->
NewRiakObj = csd_riak:update(RiakObj, csd_user:to_json(User)),
ok = csd_riak:save(RiakPid, NewRiakObj),
{ok, User};
{error, notfound} ->
NewRiakObj = csd_riak:create(?BUCKET, UserId, csd_user:to_json(User)),
ok = csd_riak:save(RiakPid, NewRiakObj),
{ok, User}
end.
Loading

0 comments on commit acf46cb

Please sign in to comment.