%% a small shoppingcart example which tries to show
%% a variety of tricks and tacticts to display a
%% shoppingcart style page with server side state.
-module(shopcart).
-author('klacke@hyber.org').
-include("../../include/yaws_api.hrl").
-include_lib("kernel/include/inet.hrl").
-export([top/1, buy/1, index/1, loginpost/1, login/1, logout/1, formupdate/1]).
%% this is the opaque structure we pass to the
%% yaws cookie session server
-record(sess, {
user,
passwd,
addr,
items = []}).
%% this function extracts the session from the cookie
check_cookie(A) ->
H = A#arg.headers,
case yaws_api:find_cookie_val("ssid", H#headers.cookie) of
Val when Val /= [] ->
case yaws_api:cookieval_to_opaque(Val) of
{ok, Sess} ->
{ok, Sess, Val};
{error, {has_session, Sess}} ->
{ok, Sess};
Else ->
Else
end;
[] ->
{error, nocookie}
end.
%% this function is called first in all out yaws files,
%% it will autologin users that are not logged in
top(A) ->
case check_cookie(A) of
{ok, _Session, _Cookie} ->
ok;
{error, _Reason} ->
login(A)
end.
%% generate a css head the title of the page set dynamically
css_head(PageTitle) ->
Z =
[<<"
">>,
PageTitle,
<<"
">>
],
{html, Z}.
%% the little status field in the upper left corner
head_status(User) ->
{ehtml,
{table, [],
{tr, [],
[{td, [{width, "30%"}],
{table, [ {border, "1"}, {bgcolor, beige},{bordercolor, black}],
[{tr, [], {td, [], pb("User: ~s", [User])}}
]}
},
{td, [{align, right}], {img, [{src, "junk.png"}
]}}
]
}
}
}.
%% bold paragraph according to style.css
pb(Fmt, Args) ->
{p, [{class, pb}], io_lib:format(Fmt, Args)}.
%% toprow of buttons to push
toprow() ->
{ehtml,
{table, [{cellspacing, "4"},
{bgcolor, tan},
{width, "100%"}
],
[
{tr, [],
[{td, [], {a, [{href, "buy.yaws"}] , {p, [{class, toprow}], "Buy"}}},
{td, [], {a, [{href, "logout.yaws"}], {p, [{class, toprow}], "Logout"}}},
{td, [], {a, [{href, "source.html"}], {p, [{class, toprow}], "The Source"}}},
{td, [{width, "70%"}], ""}
]}
]
}
}.
%% kinda hackish since we us ehtml
bot() ->
{html, " \n \n"}.
%% This function displays the login page
login(A) ->
CSS = css_head("Shopcart"),
Head = head_status("Not logged in"),
Top = toprow(),
Login =
{ehtml,
[{h2, [], "Shopcart login"},
{form, [{method, get},
{action, "loginpost.yaws"}],
[
{p, [], "Username"},
{input, [{name, user},
{type, text},
{value, "Joe Junk shopper"},
{size, "48"}]},
{p, [], "Password"},
{input, [{name, password},
{type, text},
{value, "xyz123"},
{size, "48"}]},
{input, [{type, submit},
{value, "Login"}]},
{input, [{name, url},
{type, hidden},
{value, xpath((A#arg.req)#http_request.path, A)}]}
]
}
]},
[CSS, Head, Top, Login, bot(), break].
logout(A) ->
{ok, _Sess, Cookie} = check_cookie(A),
yaws_api:delete_cookie_session(Cookie),
{ehtml, {h3, [], "Yo, "}}.
%% This is the function that gets invoked when the
%% user has attempted to login
%% The trick used here is to pass the original URL in a hidden
%% field into this function, if the login is successful, we redirect
%% to the original URL.
loginpost(A) ->
case {yaws_api:queryvar(A, "user"),
yaws_api:queryvar(A, "url"),
yaws_api:queryvar(A, "password")} of
{{ok, User},
{ok, Url},
{ok, Pwd}} ->
%% here's the place to validate the user
%% we allow all users,
io:format("User ~p logged in ~n", [User]),
Sess = #sess{user = User,
passwd = Pwd},
Cookie = yaws_api:new_cookie_session(Sess),
[yaws_api:redirect(Url),
yaws_api:setcookie("ssid",Cookie)];
_ ->
login(A)
end.
xpath({abs_path, P}, _A) ->
P.
%% this is the function that gets the form when the user
%% hits "update Cart"
formupdate(A) ->
{ok, Sess, Cookie} = check_cookie(A),
_J = junk(),
Items = Sess#sess.items,
L = yaws_api:parse_post(A),
I2 = handle_l(L, Items),
Sess2 = Sess#sess{items = I2},
yaws_api:replace_cookie_session(Cookie, Sess2),
{redirect, "index.yaws"}. %% force browser to reload
handle_l([], Items) ->
Items;
handle_l([{Str, Num} | Tail], Items) ->
case catch list_to_integer(Num) of
Int when is_integer(Int) ->
handle_l(Tail, [{Str, Int} | lists:keydelete(Str,1, Items)]);
_ ->
handle_l(Tail, Items)
end.
ip(A) ->
S = A#arg.clisock,
case inet:peername(S) of
{ok, {Ip, _Port}} ->
case inet:gethostbyaddr(Ip) of
{ok, HE} ->
io_lib:format("~s/~s", [fmtip(Ip), HE#hostent.h_name]);
_Err ->
io_lib:format("~s", [fmtip(Ip)])
end;
_ ->
[]
end.
fmtip({A,B,C,D}) ->
io_lib:format("~w.~w.~w.~w", [A,B,C,D]).
%% generate the final "you have bought page ... "
buy(A) ->
{ok, Sess, _Cookie} = check_cookie(A),
Css = css_head("Shopcart"),
Head = head_status(Sess#sess.user),
Top = toprow(),
BROWS = b_rows(Sess#sess.items),
Res =
if
length (BROWS) > 0 ->
{ehtml,
[{h4, [], "Congratulations, you have bought"},
{table, [],BROWS},
{hr},
{p , [{class, toprow}],
io_lib:format(
"Items are at this very moment being shipped to the"
" residence of the computer with IP: ~s~n", [ip(A)])}
]
};
true ->
{ehtml,
[{h4, [], "Congratulations, you have bought nothing"}]}
end,
[Css, Head, Top, Res, bot()].
b_rows(Items) ->
J = junk(),
Desc = {tr,[],
[
{th, [], pb("Description",[])},
{th, [], pb("Quantity",[])},
{th, [], pb("Sum ",[])}]},
[Desc | b_rows(Items, J, 0, [])].
b_rows([{Desc, Num}|Tail], Junk, Ack, TRS) when Num > 0 ->
{value, {_, Price}} = lists:keysearch(Desc, 1, Junk),
A = Num * Price,
TR = {tr, [],
[{td, [], Desc},
{td, [], io_lib:format("~w", [Num])},
{td, [], io_lib:format("~w", [A])}
]},
b_rows(Tail, Junk, A+Ack, [TR|TRS]);
b_rows([{_Desc, Num}|Tail], Junk, Ack, TRS) when Num == 0 ->
b_rows(Tail, Junk, Ack, TRS);
b_rows([], _, Ack, TRS) when Ack > 0 ->
Tax = round(0.27 * Ack),
Empty = {td, [], []},
TaxRow = {tr, [],
[
{td, [], pb("Swedish VAT tax 27% ",[])},
Empty,
{td, [], pb("~w", [Tax])}
]},
Total = {tr, [],
[
{td, [], pb("Total ",[])},
Empty,
{td, [], pb("~w", [Ack + Tax])}
]},
lists:reverse([Total, TaxRow | TRS]);
b_rows(_, _,_,_) ->
[].
%% this is the main function which displays
%% the shopcart .....
%% the entire shopcart is one big form which gets posted
%% when the user updates the shopcart
index(A) ->
{ok, Sess, _Cookie} = check_cookie(A),
io:format("Inside index: ~p~n", [Sess#sess.items]),
Css = css_head("Shopcart"),
Head = head_status(Sess#sess.user),
Top = toprow(),
Cart =
{ehtml,
{form,
[{name, form},
{method,post},
{action, "shopcart_form.yaws"}],
[
{table, [{bgcolor, linen}, {border, "2"}],
rows(Sess#sess.items)},
{input, [{type, submit}, {value, "Update Cart"}]}
]
}
},
[Css, Head, Top, Cart, bot()].
%% this function gets a list of
%% {JunkString, Num} and displays the current shopcart
rows(Items) ->
Junk = junk(),
First = {tr, [],
[{th, [], pb("Num Items", [])},
{th, [], pb("Item description", [])},
{th, [], pb("Price SEK ",[])}
]},
L = lists:map(
fun({Desc, Price}) ->
{tr, [],
[{td, [],
{input, [{type, text},
{width, "4"},
{value, jval(Desc, Items)},
{name, Desc}]}},
{td, [], {p, [], Desc}},
{td, [], pb("~w ", [Price])}
]}
end, Junk),
Total = total(Items, Junk, 0),
Tax = round(0.27 * Total),
T = [{tr, [],
[{td, [], pb("Sum accumulated",[])},
{td, [{colspan, "2"}], pb("~w SEK", [Total])}
]
},
{tr, [],
[
{td, [], pb("Swedish VAT tax 27 % :-)",[])},
{td, [{colspan, "2"}], pb("~w SEK", [Tax])}
]
},
{tr, [],
[
{td, [], pb("Total",[])},
{td, [{colspan, "2"}], pb("~w SEK", [Total + Tax])}
]
}
],
_Rows = [First | L] ++ T.
%% The Items are picked up by the
%% formupdate function and set accordingly in the opaque state
%% this function recalculates the sum total
total([{Str, Num} | Tail], Junk, Ack) ->
{value, {Str, Price}} = lists:keysearch(Str, 1, Junk),
total(Tail, Junk, (Num * Price) + Ack);
total([], _,Ack) ->
Ack.
%% We need to set the value in each input field
jval(Str, Items) ->
case lists:keysearch(Str, 1, Items) of
{value, {_, Num}} when is_integer(Num) ->
integer_to_list(Num);
false ->
"0"
end.
%% the store database :-)
%% {Description, Price} tuples
junk() ->
[{"Toothbrush in rainbow colours", 18},
{"Twinset of extra soft towels", 66},
{"Hangover pill - guaranteed to work", 88},
{"Work-out kit that fits under your bed", 1900},
{"100 pack of headache pills", 7},
{"Free subscription to MS update packs", 999},
{"Toilet cleaner", 1111},
{"Body lotion 4 litres", 888},
{"Yello, a lifetime supply", 99}].