/src/support/z_pivot_rsc.erl

http://github.com/zotonic/zotonic · Erlang · 909 lines · 660 code · 121 blank · 128 comment · 7 complexity · 77079ea753c00fada2e6e15ffaa27009 MD5 · raw file

  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @copyright 2009-2015 Marc Worrell
  3. %% @doc Pivoting server for the rsc table. Takes care of full text indices. Polls the pivot queue for any changed resources.
  4. %% Copyright 2009-2015 Marc Worrell
  5. %%
  6. %% Licensed under the Apache License, Version 2.0 (the "License");
  7. %% you may not use this file except in compliance with the License.
  8. %% You may obtain a copy of the License at
  9. %%
  10. %% http://www.apache.org/licenses/LICENSE-2.0
  11. %%
  12. %% Unless required by applicable law or agreed to in writing, software
  13. %% distributed under the License is distributed on an "AS IS" BASIS,
  14. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. %% See the License for the specific language governing permissions and
  16. %% limitations under the License.
  17. -module(z_pivot_rsc).
  18. -author("Marc Worrell <marc@worrell.nl").
  19. -behaviour(gen_server).
  20. %% gen_server exports
  21. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
  22. -export([start_link/1]).
  23. %% interface functions
  24. -export([
  25. poll/1,
  26. pivot/2,
  27. pivot_delay/1,
  28. pivot_resource_update/4,
  29. queue_all/1,
  30. insert_queue/2,
  31. get_pivot_title/1,
  32. get_pivot_title/2,
  33. insert_task/3,
  34. insert_task/4,
  35. insert_task/5,
  36. insert_task_after/6,
  37. get_task/1,
  38. get_task/2,
  39. get_task/3,
  40. get_task/4,
  41. delete_task/3,
  42. delete_task/4,
  43. pivot_resource/2,
  44. stemmer_language/1,
  45. stemmer_language_config/1,
  46. cleanup_tsv_text/1,
  47. pg_lang/1,
  48. pg_lang_extra/1,
  49. % get_pivot_data/2,
  50. define_custom_pivot/3,
  51. lookup_custom_pivot/4
  52. ]).
  53. -include("zotonic.hrl").
  54. % Interval (in seconds) to check if there are any items to be pivoted.
  55. -define(PIVOT_POLL_INTERVAL_FAST, 2).
  56. -define(PIVOT_POLL_INTERVAL_SLOW, 20).
  57. % Number of queued ids taken from the queue at one go
  58. -define(POLL_BATCH, 50).
  59. %% Minimum day, inserted for date start search ranges
  60. -define(EPOCH_START, {{-4000,1,1},{0,0,0}}).
  61. -record(state, {site, is_pivot_delay = false}).
  62. %% @doc Poll the pivot queue for the database in the context
  63. %% @spec poll(Context) -> void()
  64. poll(Context) ->
  65. gen_server:cast(Context#context.pivot_server, poll).
  66. %% @doc An immediate pivot request for a resource
  67. -spec pivot(integer(), #context{}) -> ok.
  68. pivot(Id, Context) ->
  69. gen_server:cast(Context#context.pivot_server, {pivot, Id}).
  70. %% @doc Delay the next pivot, useful when performing big updates
  71. -spec pivot_delay(#context{}) -> ok.
  72. pivot_delay(Context) ->
  73. gen_server:cast(Context#context.pivot_server, pivot_delay).
  74. %% @doc Return a modified property list with fields that need immediate pivoting on an update.
  75. pivot_resource_update(Id, UpdateProps, RawProps, Context) ->
  76. Props = lists:foldl(fun(Key, All) ->
  77. case proplists:is_defined(Key, UpdateProps) of
  78. false ->
  79. [{Key, proplists:get_value(Key, RawProps)}|All];
  80. true ->
  81. All
  82. end
  83. end, UpdateProps, [date_start, date_end, title]),
  84. {DateStart, DateEnd} = pivot_date(Props),
  85. PivotTitle = truncate(get_pivot_title(Props), 100),
  86. Props1 = [
  87. {pivot_date_start, DateStart},
  88. {pivot_date_end, DateEnd},
  89. {pivot_date_start_month_day, month_day(DateStart)},
  90. {pivot_date_end_month_day, month_day(DateEnd)},
  91. {pivot_title, PivotTitle}
  92. | Props
  93. ],
  94. z_notifier:foldr(#pivot_update{id=Id, raw_props=RawProps}, Props1, Context).
  95. month_day(undefined) -> undefined;
  96. month_day(?EPOCH_START) -> undefined;
  97. month_day(?ST_JUTTEMIS) -> undefined;
  98. month_day({{_Y,M,D}, _}) -> M*100+D.
  99. %% @doc Rebuild the search index by queueing all resources for pivot.
  100. queue_all(Context) ->
  101. erlang:spawn(fun() ->
  102. queue_all(0, Context)
  103. end).
  104. queue_all(FromId, Context) ->
  105. case z_db:q("select id from rsc where id > $1 order by id limit 1000", [FromId], Context) of
  106. [] ->
  107. done;
  108. Ids ->
  109. F = fun(Ctx) ->
  110. [ insert_queue(Id, Ctx) || {Id} <- Ids ]
  111. end,
  112. z_db:transaction(F, Context),
  113. {LastId} = lists:last(Ids),
  114. queue_all(LastId, Context)
  115. end.
  116. %% @doc Insert a rsc_id in the pivot queue
  117. insert_queue(Id, Context) ->
  118. insert_queue(Id, calendar:universal_time(), Context).
  119. %% @doc Insert a rsc_id in the pivot queue for a certain date
  120. -spec insert_queue(integer(), calendar:date(), #context{}) -> ok | {error, eexist}.
  121. insert_queue(Id, Date, Context) when is_integer(Id), is_tuple(Date) ->
  122. z_db:transaction(
  123. fun(Ctx) ->
  124. case z_db:q("update rsc_pivot_queue
  125. set serial = serial + 1,
  126. due = $2
  127. where rsc_id = $1", [Id, Date], Ctx) of
  128. 1 ->
  129. ok;
  130. 0 ->
  131. try
  132. z_db:q("insert into rsc_pivot_queue (rsc_id, due, is_update) values ($1, $2, true)",
  133. [Id, Date],
  134. Ctx),
  135. ok
  136. catch
  137. throw:{error, {error,error,<<"23503">>, _, _}} ->
  138. {error, eexist}
  139. end
  140. end
  141. end,
  142. Context).
  143. %% @doc Insert a slow running pivot task. For example syncing category numbers after an category update.
  144. insert_task(Module, Function, Context) ->
  145. insert_task(Module, Function, undefined, [], Context).
  146. %% @doc Insert a slow running pivot task. Use the UniqueKey to prevent double queued tasks.
  147. insert_task(Module, Function, UniqueKey, Context) ->
  148. insert_task(Module, Function, UniqueKey, [], Context).
  149. %% @doc Insert a slow running pivot task with unique key and arguments.
  150. insert_task(Module, Function, undefined, Args, Context) ->
  151. insert_task(Module, Function, binary_to_list(z_ids:id()), Args, Context);
  152. insert_task(Module, Function, UniqueKey, Args, Context) ->
  153. insert_task_after(undefined, Module, Function, UniqueKey, Args, Context).
  154. %% @doc Insert a slow running pivot task with unique key and arguments that should start after Seconds seconds.
  155. insert_task_after(SecondsOrDate, Module, Function, UniqueKey, Args, Context) ->
  156. z_db:transaction(fun(Ctx) -> insert_transaction(SecondsOrDate, Module, Function, UniqueKey, Args, Ctx) end, Context).
  157. insert_transaction(SecondsOrDate, Module, Function, UniqueKey, Args, Context) ->
  158. Due = to_utc_date(SecondsOrDate),
  159. UniqueKeyBin = z_convert:to_binary(UniqueKey),
  160. Fields = [
  161. {module, Module},
  162. {function, Function},
  163. {key, UniqueKeyBin},
  164. {args, Args},
  165. {due, Due}
  166. ],
  167. case z_db:q1("select id
  168. from pivot_task_queue
  169. where module = $1 and function = $2 and key = $3",
  170. [Module, Function, UniqueKeyBin],
  171. Context)
  172. of
  173. undefined ->
  174. z_db:insert(pivot_task_queue, Fields, Context);
  175. Id when is_integer(Id) ->
  176. case Due of
  177. undefined -> nop;
  178. _ -> z_db:update(pivot_task_queue, Id, Fields, Context)
  179. end,
  180. {ok, Id}
  181. end.
  182. get_task(Context) ->
  183. z_db:assoc("
  184. select *
  185. from pivot_task_queue",
  186. Context).
  187. get_task(Module, Context) ->
  188. z_db:assoc("
  189. select *
  190. from pivot_task_queue
  191. where module = $1",
  192. [Module],
  193. Context).
  194. get_task(Module, Function, Context) ->
  195. z_db:assoc("
  196. select *
  197. from pivot_task_queue
  198. where module = $1 and function = $2",
  199. [Module, Function],
  200. Context).
  201. get_task(Module, Function, UniqueKey, Context) ->
  202. UniqueKeyBin = z_convert:to_binary(UniqueKey),
  203. z_db:assoc_row("
  204. select *
  205. from pivot_task_queue
  206. where module = $1 and function = $2 and key = $3",
  207. [Module, Function, UniqueKeyBin],
  208. Context).
  209. to_utc_date(undefined) ->
  210. undefined;
  211. to_utc_date(N) when is_integer(N) ->
  212. calendar:gregorian_seconds_to_datetime(
  213. calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + N);
  214. to_utc_date({Y,M,D} = YMD) when is_integer(Y), is_integer(M), is_integer(D) ->
  215. {YMD,{0,0,0}};
  216. to_utc_date({{Y,M,D},{H,I,S}} = Date) when is_integer(Y), is_integer(M), is_integer(D), is_integer(H), is_integer(I), is_integer(S) ->
  217. Date.
  218. delete_task(Module, Function, Context) ->
  219. z_db:q("delete from pivot_task_queue where module = $1 and function = $2",
  220. [Module, Function],
  221. Context).
  222. delete_task(Module, Function, UniqueKey, Context) ->
  223. UniqueKeyBin = z_convert:to_binary(UniqueKey),
  224. z_db:q("delete from pivot_task_queue where module = $1 and function = $2 and key = $3",
  225. [Module, Function, UniqueKeyBin],
  226. Context).
  227. %%====================================================================
  228. %% API
  229. %%====================================================================
  230. %% @spec start_link(SiteProps) -> {ok,Pid} | ignore | {error,Error}
  231. %% @doc Starts the server
  232. start_link(SiteProps) ->
  233. {site, Site} = proplists:lookup(site, SiteProps),
  234. Name = z_utils:name_for_site(?MODULE, Site),
  235. gen_server:start_link({local, Name}, ?MODULE, Site, []).
  236. %%====================================================================
  237. %% gen_server callbacks
  238. %%====================================================================
  239. %% @spec init(Args) -> {ok, State} |
  240. %% {ok, State, Timeout} |
  241. %% ignore |
  242. %% {stop, Reason}
  243. %% @doc Initiates the server.
  244. init(Site) ->
  245. lager:md([
  246. {site, Site},
  247. {module, ?MODULE}
  248. ]),
  249. timer:send_after(?PIVOT_POLL_INTERVAL_SLOW*1000, poll),
  250. {ok, #state{site=Site, is_pivot_delay=false}}.
  251. %% @spec handle_call(Request, From, State) -> {reply, Reply, State} |
  252. %% {reply, Reply, State, Timeout} |
  253. %% {noreply, State} |
  254. %% {noreply, State, Timeout} |
  255. %% {stop, Reason, Reply, State} |
  256. %% {stop, Reason, State}
  257. %% @doc Trap unknown calls
  258. handle_call(Message, _From, State) ->
  259. {stop, {unknown_call, Message}, State}.
  260. %% @spec handle_cast(Msg, State) -> {noreply, State} |
  261. %% {noreply, State, Timeout} |
  262. %% {stop, Reason, State}
  263. %% @doc Poll the queue for the default host
  264. handle_cast(poll, State) ->
  265. do_poll(z_context:new(State#state.site)),
  266. {noreply, State};
  267. %% @doc Poll the queue for a particular database
  268. handle_cast({pivot, Id}, State) ->
  269. do_pivot(Id, z_context:new(State#state.site)),
  270. {noreply, State};
  271. %% @doc Delay the next pivot, useful when performing big updates
  272. handle_cast(pivot_delay, State) ->
  273. {noreply, State#state{is_pivot_delay=true}};
  274. %% @doc Trap unknown casts
  275. handle_cast(Message, State) ->
  276. {stop, {unknown_cast, Message}, State}.
  277. %% @spec handle_info(Info, State) -> {noreply, State} |
  278. %% {noreply, State, Timeout} |
  279. %% {stop, Reason, State}
  280. %% @doc Handling all non call/cast messages
  281. handle_info(poll, #state{is_pivot_delay=true} = State) ->
  282. timer:send_after(?PIVOT_POLL_INTERVAL_SLOW*1000, poll),
  283. {noreply, State#state{is_pivot_delay=false}};
  284. handle_info(poll, State) ->
  285. case do_poll(z_context:new(State#state.site)) of
  286. true -> timer:send_after(?PIVOT_POLL_INTERVAL_FAST*1000, poll);
  287. false -> timer:send_after(?PIVOT_POLL_INTERVAL_SLOW*1000, poll)
  288. end,
  289. {noreply, State};
  290. handle_info(_Info, State) ->
  291. {noreply, State}.
  292. %% @spec terminate(Reason, State) -> void()
  293. %% @doc This function is called by a gen_server when it is about to
  294. %% terminate. It should be the opposite of Module:init/1 and do any necessary
  295. %% cleaning up. When it returns, the gen_server terminates with Reason.
  296. %% The return value is ignored.
  297. terminate(_Reason, _State) ->
  298. ok.
  299. %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
  300. %% @doc Convert process state when code is changed
  301. code_change(_OldVsn, State, _Extra) ->
  302. {ok, State}.
  303. %%====================================================================
  304. %% support functions
  305. %%====================================================================
  306. %% @doc Poll a database for any queued updates.
  307. do_poll(Context) ->
  308. DidTask = do_poll_task(Context),
  309. do_poll_queue(Context) or DidTask.
  310. do_poll_task(Context) ->
  311. case poll_task(Context) of
  312. {TaskId, Module, Function, _Key, Args} ->
  313. try
  314. case erlang:apply(Module, Function, z_convert:to_list(Args) ++ [Context]) of
  315. {delay, Delay} ->
  316. Due = if
  317. is_integer(Delay) ->
  318. calendar:gregorian_seconds_to_datetime(
  319. calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + Delay);
  320. is_tuple(Delay) ->
  321. Delay
  322. end,
  323. z_db:q("update pivot_task_queue set due = $1 where id = $2", [Due, TaskId], Context);
  324. {delay, Delay, NewArgs} ->
  325. Due = if
  326. is_integer(Delay) ->
  327. calendar:gregorian_seconds_to_datetime(
  328. calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + Delay);
  329. is_tuple(Delay) ->
  330. Delay
  331. end,
  332. Fields = [
  333. {due, Due},
  334. {args, NewArgs}
  335. ],
  336. z_db:update(pivot_task_queue, TaskId, Fields, Context);
  337. _OK ->
  338. z_db:q("delete from pivot_task_queue where id = $1", [TaskId], Context)
  339. end
  340. catch
  341. error:undef ->
  342. ?zWarning(io_lib:format("Undefined task, aborting: ~p:~p(~p) ~p~n",
  343. [Module, Function, Args, erlang:get_stacktrace()]),
  344. Context),
  345. z_db:q("delete from pivot_task_queue where id = $1", [TaskId], Context);
  346. Error:Reason ->
  347. ?zWarning(io_lib:format("Task failed(~p:~p): ~p:~p(~p) ~p~n",
  348. [Error, Reason, Module, Function, Args, erlang:get_stacktrace()]),
  349. Context)
  350. end,
  351. true;
  352. empty ->
  353. false
  354. end.
  355. do_poll_queue(Context) ->
  356. case fetch_queue(Context) of
  357. [] ->
  358. false;
  359. Qs ->
  360. F = fun(Ctx) ->
  361. [ {Id, catch pivot_resource(Id, Ctx)} || {Id,_Serial} <- Qs]
  362. end,
  363. case z_db:transaction(F, Context) of
  364. {rollback, PivotError} ->
  365. lager:error("[~p] Pivot error: ~p: ~p~n",
  366. [z_context:site(Context), PivotError, Qs]);
  367. L when is_list(L) ->
  368. lists:map(fun({Id, _Serial}) ->
  369. IsA = m_rsc:is_a(Id, Context),
  370. z_notifier:notify(#rsc_pivot_done{id=Id, is_a=IsA}, Context),
  371. % Flush the resource, as some synthesized attributes might depend on the pivoted fields.
  372. % @todo Only do this if some fields are changed
  373. m_rsc_update:flush(Id, Context)
  374. end, Qs),
  375. lists:map(fun({_Id, ok}) -> ok;
  376. ({Id,Error}) -> log_error(Id, Error, Context) end,
  377. L),
  378. delete_queue(Qs, Context)
  379. end,
  380. true
  381. end.
  382. log_error(Id, Error, Context) ->
  383. ?zWarning(io_lib:format("Pivot error ~p: ~p", [Id, Error]), Context).
  384. %% @doc Fetch the next task uit de task queue, if any.
  385. poll_task(Context) ->
  386. case z_db:q_row("select id, module, function, key, props
  387. from pivot_task_queue
  388. where due is null
  389. or due < current_timestamp
  390. order by due asc
  391. limit 1", Context)
  392. of
  393. {Id,Module,Function,Key,Props} ->
  394. Args = case Props of
  395. [{args,Args0}] -> Args0;
  396. _ -> []
  397. end,
  398. %% @todo We delete the task right now, this needs to be changed to a deletion after the task has been running.
  399. {Id, z_convert:to_atom(Module), z_convert:to_atom(Function), Key, Args};
  400. undefined ->
  401. empty
  402. end.
  403. %% @doc Pivot a specific id, delete its queue record if present
  404. do_pivot(Id, Context) ->
  405. Serial = fetch_queue_id(Id, Context),
  406. pivot_resource(Id, Context),
  407. delete_queue(Id, Serial, Context).
  408. %% @doc Fetch the next batch of ids from the queue. Remembers the serials, as a new
  409. %% pivot request might come in while we are pivoting.
  410. %% @spec fetch_queue(Context) -> [{Id,Serial}]
  411. fetch_queue(Context) ->
  412. z_db:q("select rsc_id, serial from rsc_pivot_queue where due < current_timestamp - '10 second'::interval order by is_update, due limit $1", [?POLL_BATCH], Context).
  413. %% @doc Fetch the serial of id's queue record
  414. fetch_queue_id(Id, Context) ->
  415. z_db:q1("select serial from rsc_pivot_queue where rsc_id = $1", [Id], Context).
  416. %% @doc Delete the previously queued ids iff the queue entry has not been updated in the meanwhile
  417. delete_queue(Qs, Context) ->
  418. F = fun(Ctx) ->
  419. [ z_db:q("delete from rsc_pivot_queue where rsc_id = $1 and serial = $2", [Id,Serial], Ctx) || {Id,Serial} <- Qs ]
  420. end,
  421. z_db:transaction(F, Context).
  422. %% @doc Delete a specific id/serial combination
  423. delete_queue(_Id, undefined, _Context) ->
  424. ok;
  425. delete_queue(Id, Serial, Context) ->
  426. z_db:q("delete from rsc_pivot_queue where rsc_id = $1 and serial = $2", [Id,Serial], Context).
  427. -spec pivot_resource(integer(), #context{}) -> ok | {error, eexist}.
  428. pivot_resource(Id, Context0) ->
  429. Lang = stemmer_language_config(Context0),
  430. Context = z_context:set_language(Lang,
  431. z_context:set_tz(<<"UTC">>,
  432. z_acl:sudo(Context0))),
  433. case m_rsc:exists(Id, Context) of
  434. true ->
  435. RscProps = get_pivot_rsc(Id, Context),
  436. Vars = #{
  437. id => Id,
  438. props => RscProps,
  439. z_language => Lang
  440. },
  441. {ok, Template} = z_template_compiler_runtime:map_template({cat, <<"pivot/pivot.tpl">>}, Vars, Context),
  442. TextA = render_block(a, Template, Vars, Context),
  443. TextB = render_block(b, Template, Vars, Context),
  444. TextC = render_block(c, Template, Vars, Context),
  445. TextD = render_block(d, Template, Vars, Context),
  446. TsvIds = render_block(related_ids, Template, Vars, Context),
  447. Title = render_block(title, Template, Vars, Context),
  448. Street = render_block(address_street, Template, Vars, Context),
  449. City = render_block(address_city, Template, Vars, Context),
  450. Postcode = render_block(address_postcode, Template, Vars, Context),
  451. State = render_block(address_state, Template, Vars, Context),
  452. Country = render_block(address_country, Template, Vars, Context),
  453. NameFirst = render_block(name_first, Template, Vars, Context),
  454. NameSurname = render_block(name_surname, Template, Vars, Context),
  455. Gender = render_block(gender, Template, Vars, Context),
  456. DateStart = to_datetime(render_block(date_start, Template, Vars, Context)),
  457. DateEnd = to_datetime(render_block(date_end, Template, Vars, Context)),
  458. DateStartMonthDay = to_integer(render_block(date_start_month_day, Template, Vars, Context)),
  459. DateEndMonthDay = to_integer(render_block(date_end_month_day, Template, Vars, Context)),
  460. LocationLat = to_float(render_block(location_lat, Template, Vars, Context)),
  461. LocationLng = to_float(render_block(location_lng, Template, Vars, Context)),
  462. % Make psql tsv texts from the A..D blocks
  463. StemmerLanguage = stemmer_language(Context),
  464. {SqlA, ArgsA} = to_tsv(TextA, $A, [], StemmerLanguage),
  465. {SqlB, ArgsB} = to_tsv(TextB, $B, ArgsA, StemmerLanguage),
  466. {SqlC, ArgsC} = to_tsv(TextC, $C, ArgsB, StemmerLanguage),
  467. {SqlD, ArgsD} = to_tsv(TextD, $D, ArgsC, StemmerLanguage),
  468. % Make the text and object-ids vectors for the pivot
  469. TsvSql = [SqlA, " || ", SqlB, " || ", SqlC, " || ", SqlD],
  470. Tsv = z_db:q1(iolist_to_binary(["select ", TsvSql]), ArgsD, Context),
  471. Rtsv = z_db:q1("select to_tsvector($1)", [TsvIds], Context),
  472. KVs = [
  473. {pivot_tsv, Tsv},
  474. {pivot_rtsv, Rtsv},
  475. {pivot_street, truncate(Street, 120)},
  476. {pivot_city, truncate(City, 100)},
  477. {pivot_postcode, truncate(Postcode, 30)},
  478. {pivot_state, truncate(State, 50)},
  479. {pivot_country, truncate(Country, 80)},
  480. {pivot_first_name, truncate(NameFirst, 100)},
  481. {pivot_surname, truncate(NameSurname, 100)},
  482. {pivot_gender, truncate(Gender, 1)},
  483. {pivot_date_start, DateStart},
  484. {pivot_date_end, DateEnd},
  485. {pivot_date_start_month_day, DateStartMonthDay},
  486. {pivot_date_end_month_day, DateEndMonthDay},
  487. {pivot_title, truncate(Title, 100)},
  488. {pivot_location_lat, LocationLat},
  489. {pivot_location_lng, LocationLng}
  490. ],
  491. KVs1= z_notifier:foldr(#pivot_fields{id=Id, rsc=RscProps}, KVs, Context),
  492. update_changed(Id, KVs1, RscProps, Context),
  493. pivot_resource_custom(Id, Context),
  494. case to_datetime(render_block(date_repivot, Template, Vars, Context)) of
  495. undefined -> ok;
  496. DateRepivot -> insert_queue(Id, DateRepivot, Context)
  497. end,
  498. ok;
  499. false ->
  500. {error, eexist}
  501. end.
  502. render_block(Block, Template, Vars, Context) ->
  503. {Output, _} = z_template:render_block_to_iolist(Block, Template, Vars, Context),
  504. iolist_to_binary(Output).
  505. %% @doc Check which pivot fields are changed, update only those
  506. update_changed(Id, KVs, RscProps, Context) ->
  507. case lists:filter(
  508. fun
  509. ({K,V}) when is_list(V) ->
  510. proplists:get_value(K, RscProps) =/= iolist_to_binary(V);
  511. ({K,undefined}) ->
  512. proplists:get_value(K, RscProps) =/= undefined;
  513. ({K,V}) when is_atom(V) ->
  514. proplists:get_value(K, RscProps) =/= z_convert:to_binary(V);
  515. ({K,V}) ->
  516. proplists:get_value(K, RscProps) =/= V
  517. end,
  518. KVs)
  519. of
  520. [] ->
  521. % No fields changed, nothing to do
  522. nop;
  523. KVsChanged ->
  524. % Make Sql update statement for the changed fields
  525. {Sql, Args} = lists:foldl(fun({K,V}, {Sq,As}) ->
  526. {[Sq,
  527. case As of [] -> []; _ -> $, end,
  528. z_convert:to_list(K),
  529. " = $", integer_to_list(length(As)+1)
  530. ],
  531. [V|As]}
  532. end,
  533. {"update rsc set ",[]},
  534. KVsChanged),
  535. z_db:q1(iolist_to_binary([Sql, " where id = $", integer_to_list(length(Args)+1)]),
  536. lists:reverse([Id|Args]),
  537. Context)
  538. end.
  539. pivot_resource_custom(Id, Context) ->
  540. CustomPivots = z_notifier:map(#custom_pivot{id=Id}, Context),
  541. lists:foreach(
  542. fun
  543. (undefined) -> ok;
  544. (none) -> ok;
  545. ({error, _} = Error) ->
  546. lager:error("[~p] Error return from custom pivot of ~p, error: ~p",
  547. [z_context:site(Context), Id, Error]);
  548. (Res) ->
  549. update_custom_pivot(Id, Res, Context)
  550. end,
  551. CustomPivots),
  552. ok.
  553. to_datetime(Text) ->
  554. case z_string:trim(Text) of
  555. <<>> -> undefined;
  556. Text1 -> check_datetime(z_datetime:to_datetime(Text1))
  557. end.
  558. check_datetime({{Y,M,D},{H,I,S}} = Date)
  559. when is_integer(Y), is_integer(M), is_integer(D),
  560. is_integer(H), is_integer(I), is_integer(S) ->
  561. Date;
  562. check_datetime({Y,M,D} = Date)
  563. when is_integer(Y), is_integer(M), is_integer(D) ->
  564. {Date, {0,0,0}};
  565. check_datetime(_) ->
  566. undefined.
  567. %% Make the setweight(to_tsvector()) parts of the update statement
  568. to_tsv(Text, Level, Args, StemmingLanguage) when is_binary(Text) ->
  569. case cleanup_tsv_text(z_html:unescape(z_html:strip(Text))) of
  570. <<>> ->
  571. {"tsvector('')", Args};
  572. TsvText ->
  573. N = length(Args) + 1,
  574. Args1 = Args ++ [TsvText],
  575. {["setweight(to_tsvector('pg_catalog.",StemmingLanguage,"', $",integer_to_list(N),"), '",Level,"')"], Args1}
  576. end.
  577. to_float(undefined) ->
  578. undefined;
  579. to_float(Text) ->
  580. case z_string:trim(Text) of
  581. <<>> -> undefined;
  582. Text1 -> z_convert:to_float(Text1)
  583. end.
  584. to_integer(undefined) ->
  585. undefined;
  586. to_integer(Text) ->
  587. case z_string:trim(Text) of
  588. <<>> -> undefined;
  589. Text1 -> z_convert:to_integer(Text1)
  590. end.
  591. cleanup_tsv_text(Text) when is_binary(Text) ->
  592. Text1 = binary:replace(Text, <<"-">>, <<" ">>, [global]),
  593. Text2 = binary:replace(Text1, <<"/">>, <<" ">>, [global]),
  594. z_string:trim(Text2).
  595. truncate(undefined, _Len) -> undefined;
  596. truncate(S, Len) -> iolist_to_binary(
  597. z_string:trim(
  598. z_string:to_lower(
  599. truncate_1(S, Len, Len)))).
  600. truncate_1(_S, 0, _Bytes) ->
  601. "";
  602. truncate_1(S, Utf8Len, Bytes) ->
  603. case z_string:truncate(S, Utf8Len, "") of
  604. T when length(T) > Bytes -> truncate_1(T, Utf8Len-1, Bytes);
  605. L -> L
  606. end.
  607. %% @doc Fetch the date range from the record
  608. pivot_date(R) ->
  609. DateStart = z_datetime:undefined_if_invalid_date(proplists:get_value(date_start, R)),
  610. DateEnd = z_datetime:undefined_if_invalid_date(proplists:get_value(date_end, R)),
  611. pivot_date1(DateStart, DateEnd).
  612. pivot_date1(S, E) when not is_tuple(S) andalso not is_tuple(E) ->
  613. {undefined, undefined};
  614. pivot_date1(S, E) when not is_tuple(S) andalso is_tuple(E) ->
  615. { ?EPOCH_START, E};
  616. pivot_date1(S, E) when is_tuple(S) andalso not is_tuple(E) ->
  617. {S, ?ST_JUTTEMIS};
  618. pivot_date1(S, E) when is_tuple(S) andalso is_tuple(E) ->
  619. {S, E}.
  620. %% @doc Fetch the first title from the record for sorting.
  621. get_pivot_title(Id, Context) ->
  622. z_string:to_lower(get_pivot_title([{title, m_rsc:p(Id, title, Context)}])).
  623. get_pivot_title(Props) ->
  624. case proplists:get_value(title, Props) of
  625. {trans, []} ->
  626. "";
  627. {trans, [{_, Text}|_]} ->
  628. z_string:to_lower(Text);
  629. T ->
  630. z_string:to_lower(T)
  631. end.
  632. %% @doc Return the raw resource data for the pivoter
  633. get_pivot_rsc(Id, Context) ->
  634. FullRecord = z_db:assoc_props_row("select * from rsc where id = $1", [Id], Context),
  635. z_notifier:foldl(pivot_rsc_data, FullRecord, Context).
  636. %% @doc Translate a language to a language string as used by
  637. %% postgresql. This language list is the intersection of the default
  638. %% catalogs of postgres with the languages supported by
  639. %% mod_translation.
  640. pg_lang(dk) -> "danish";
  641. pg_lang(nl) -> "dutch";
  642. pg_lang(en) -> "english";
  643. pg_lang(fi) -> "finnish";
  644. pg_lang(fr) -> "french";
  645. pg_lang(de) -> "german";
  646. pg_lang(hu) -> "hungarian";
  647. pg_lang(it) -> "italian";
  648. pg_lang(no) -> "norwegian";
  649. pg_lang(ro) -> "romanian";
  650. pg_lang(ru) -> "russian";
  651. pg_lang(es) -> "spanish";
  652. pg_lang(se) -> "swedish";
  653. pg_lang(tr) -> "turkish";
  654. pg_lang(<<"dk">>) -> "danish";
  655. pg_lang(<<"nl">>) -> "dutch";
  656. pg_lang(<<"en">>) -> "english";
  657. pg_lang(<<"fi">>) -> "finnish";
  658. pg_lang(<<"fr">>) -> "french";
  659. pg_lang(<<"de">>) -> "german";
  660. pg_lang(<<"hu">>) -> "hungarian";
  661. pg_lang(<<"it">>) -> "italian";
  662. pg_lang(<<"no">>) -> "norwegian";
  663. pg_lang(<<"ro">>) -> "romanian";
  664. pg_lang(<<"ru">>) -> "russian";
  665. pg_lang(<<"es">>) -> "spanish";
  666. pg_lang(<<"se">>) -> "swedish";
  667. pg_lang(<<"tr">>) -> "turkish";
  668. pg_lang(_) -> "english".
  669. %% Map extra languages, these are from the i18n.language_stemmer configuration and not
  670. %% per default installed in PostgreSQL
  671. pg_lang_extra(Iso) ->
  672. case iso639:lc2lang(z_convert:to_list(Iso)) of
  673. <<"">> ->
  674. pg_lang(Iso);
  675. Lang ->
  676. lists:takewhile(fun
  677. (C) when C >= $a, C =< $z -> true;
  678. (_) -> false
  679. end,
  680. z_convert:to_list(z_string:to_lower(Lang)))
  681. end.
  682. % Default stemmers in a Ubuntu psql install:
  683. %
  684. % pg_catalog | danish_stem | snowball stemmer for danish language
  685. % pg_catalog | dutch_stem | snowball stemmer for dutch language
  686. % pg_catalog | english_stem | snowball stemmer for english language
  687. % pg_catalog | finnish_stem | snowball stemmer for finnish language
  688. % pg_catalog | french_stem | snowball stemmer for french language
  689. % pg_catalog | german_stem | snowball stemmer for german language
  690. % pg_catalog | hungarian_stem | snowball stemmer for hungarian language
  691. % pg_catalog | italian_stem | snowball stemmer for italian language
  692. % pg_catalog | norwegian_stem | snowball stemmer for norwegian language
  693. % pg_catalog | portuguese_stem | snowball stemmer for portuguese language
  694. % pg_catalog | romanian_stem | snowball stemmer for romanian language
  695. % pg_catalog | russian_stem | snowball stemmer for russian language
  696. % pg_catalog | simple | simple dictionary: just lower case and check for stopword
  697. % pg_catalog | spanish_stem | snowball stemmer for spanish language
  698. % pg_catalog | swedish_stem | snowball stemmer for swedish language
  699. % pg_catalog | turkish_stem | snowball stemmer for turkish language
  700. %% @doc Return the language used for stemming the full text index.
  701. %% We use a single stemming to prevent having seperate indexes per language.
  702. -spec stemmer_language(#context{}) -> string().
  703. stemmer_language(Context) ->
  704. StemmingLanguage = m_config:get_value(i18n, language_stemmer, Context),
  705. case z_utils:is_empty(StemmingLanguage) of
  706. true -> pg_lang(z_trans:default_language(Context));
  707. false -> pg_lang_extra(StemmingLanguage)
  708. end.
  709. -spec stemmer_language_config(#context{}) -> atom().
  710. stemmer_language_config(Context) ->
  711. StemmingLanguage = m_config:get_value(i18n, language_stemmer, Context),
  712. case z_utils:is_empty(StemmingLanguage) of
  713. true ->
  714. z_trans:default_language(Context);
  715. false ->
  716. case z_trans:to_language_atom(StemmingLanguage) of
  717. {ok, LangAtom} -> LangAtom;
  718. {error, not_a_language} -> z_trans:default_language(Context)
  719. end
  720. end.
  721. %% @spec define_custom_pivot(Module, columns(), Context) -> ok
  722. %% @doc Let a module define a custom pivot
  723. %% columns() -> [column()]
  724. %% column() -> {ColumName::atom(), ColSpec::string()} | {atom(), string(), options::list()}
  725. define_custom_pivot(Module, Columns, Context) ->
  726. TableName = "pivot_" ++ z_convert:to_list(Module),
  727. case z_db:table_exists(TableName, Context) of
  728. true ->
  729. ok;
  730. false ->
  731. ok = z_db:transaction(
  732. fun(Ctx) ->
  733. Fields = custom_columns(Columns),
  734. Sql = "CREATE TABLE " ++ TableName ++ "(" ++
  735. "id int NOT NULL," ++ Fields ++ " primary key(id))",
  736. [] = z_db:q(lists:flatten(Sql), Ctx),
  737. [] = z_db:q("ALTER TABLE " ++ TableName ++
  738. " ADD CONSTRAINT fk_" ++ TableName ++ "_id " ++
  739. " FOREIGN KEY (id) REFERENCES rsc(id) ON UPDATE CASCADE ON DELETE CASCADE", Ctx),
  740. Indexable = lists:filter(fun({_,_}) -> true;
  741. ({_,_,Opts}) -> not lists:member(noindex, Opts)
  742. end,
  743. Columns),
  744. Idx = [
  745. begin
  746. K = element(1,Col),
  747. "CREATE INDEX " ++ z_convert:to_list(K) ++ "_key ON "
  748. ++ TableName ++ "(" ++ z_convert:to_list(K) ++ ")"
  749. end
  750. || Col <- Indexable
  751. ],
  752. lists:foreach(
  753. fun(Sql1) ->
  754. [] = z_db:q(Sql1, Ctx)
  755. end,
  756. Idx),
  757. ok
  758. end,
  759. Context),
  760. z_db:flush(Context),
  761. ok
  762. end.
  763. custom_columns(Cols) ->
  764. custom_columns(Cols, []).
  765. custom_columns([], Acc) ->
  766. lists:reverse(Acc);
  767. custom_columns([{Name, Spec}|Rest], Acc) ->
  768. custom_columns(Rest, [ [z_convert:to_list(Name), " ", Spec, ","] | Acc]);
  769. custom_columns([{Name, Spec, _Opts}|Rest], Acc) ->
  770. custom_columns(Rest, [ [z_convert:to_list(Name), " ", Spec, ","] | Acc]).
  771. update_custom_pivot(Id, {Module, Columns}, Context) ->
  772. TableName = "pivot_" ++ z_convert:to_list(Module),
  773. case z_db:select(TableName, Id, Context) of
  774. {ok, []} ->
  775. {ok, _} = z_db:insert(TableName, [{id, Id}|Columns], Context);
  776. {ok, _} ->
  777. {ok, _} = z_db:update(TableName, Id, Columns, Context)
  778. end.
  779. %% @doc Lookup a custom pivot; give back the Id based on a column. Will always return the first Id found.
  780. %% @spec lookup_custom_pivot(Module, Column, Value, Context) -> Id | undefined
  781. lookup_custom_pivot(Module, Column, Value, Context) ->
  782. TableName = "pivot_" ++ z_convert:to_list(Module),
  783. Column1 = z_convert:to_list(Column),
  784. Query = "SELECT id FROM " ++ TableName ++ " WHERE " ++ Column1 ++ " = $1",
  785. case z_db:q(Query, [Value], Context) of
  786. [] -> undefined;
  787. [{Id}|_] -> Id
  788. end.