%% 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}].