/src/models/m_category.erl

https://code.google.com/p/zotonic/ · Erlang · 703 lines · 549 code · 82 blank · 72 comment · 12 complexity · 86c775d9b8b7ef153df626d7a48e869a MD5 · raw file

  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @copyright 2009 Marc Worrell
  3. %% Date: 2009-04-08
  4. %%
  5. %% @doc Model for categories. Add, change and re-order categories.
  6. %% Copyright 2009 Marc Worrell
  7. %%
  8. %% Licensed under the Apache License, Version 2.0 (the "License");
  9. %% you may not use this file except in compliance with the License.
  10. %% You may obtain a copy of the License at
  11. %%
  12. %% http://www.apache.org/licenses/LICENSE-2.0
  13. %%
  14. %% Unless required by applicable law or agreed to in writing, software
  15. %% distributed under the License is distributed on an "AS IS" BASIS,
  16. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. %% See the License for the specific language governing permissions and
  18. %% limitations under the License.
  19. -module(m_category).
  20. -author("Marc Worrell <marc@worrell.nl").
  21. -behaviour(gen_model).
  22. %% interface functions
  23. -export([
  24. m_find_value/3,
  25. m_to_list/2,
  26. m_value/2,
  27. get/2,
  28. get_by_name/2,
  29. get_by_parent/2,
  30. get_root/1,
  31. get_range/2,
  32. get_range_by_name/2,
  33. get_path/2,
  34. last_modified/2,
  35. is_a/2,
  36. is_a/3,
  37. insert/4,
  38. get_page_count/2,
  39. delete/3,
  40. image/2,
  41. name_to_id/2,
  42. name_to_id_check/2,
  43. id_to_name/2,
  44. move_below/3,
  45. move_end/2,
  46. move_before/3,
  47. update_sequence/2,
  48. all_flat/1,
  49. all_flat/2,
  50. all_flat_meta/1,
  51. ranges/2,
  52. tree/1,
  53. tree/2,
  54. tree_depth/2,
  55. tree_depth/3,
  56. renumber/1,
  57. renumber_pivot_task/2,
  58. enumerate/1,
  59. boundaries/2
  60. ]).
  61. -include_lib("zotonic.hrl").
  62. %% @doc Fetch the value for the key from a model source
  63. %% @spec m_find_value(Key, Source, Context) -> term()
  64. m_find_value(tree, #m{value=undefined}, Context) ->
  65. tree(Context);
  66. m_find_value(tree1, #m{value=undefined}, Context) ->
  67. get_root(Context);
  68. m_find_value(tree2, #m{value=undefined}, Context) ->
  69. tree_depth(2, Context);
  70. m_find_value(all_flat, #m{value=undefined}, Context) ->
  71. all_flat(Context);
  72. m_find_value(all_flat_meta, #m{value=undefined}, Context) ->
  73. all_flat_meta(Context);
  74. m_find_value(Index, #m{value=undefined} = M, Context) ->
  75. case name_to_id(Index, Context) of
  76. {ok, Id} -> M#m{value={cat, Id}};
  77. {error, _} -> undefined
  78. end;
  79. m_find_value(tree, #m{value={cat, Id}}, Context) ->
  80. tree(Id, Context);
  81. m_find_value(tree1, #m{value={cat, Id}}, Context) ->
  82. get_by_parent(Id, Context);
  83. m_find_value(tree2, #m{value={cat, Id}}, Context) ->
  84. tree_depth(Id, 2, Context);
  85. m_find_value(path, #m{value={cat, Id}}, Context) ->
  86. get_path(Id, Context);
  87. m_find_value(image, #m{value={cat, Id}}, Context) ->
  88. image(Id, Context);
  89. m_find_value(all_flat, #m{value={cat, Id}}, Context) ->
  90. all_flat(Id, Context);
  91. m_find_value(Key, #m{value={cat, Id}}, Context) ->
  92. proplists:get_value(Key, get(Id, Context));
  93. m_find_value(_Key, _Value, _Context) ->
  94. undefined.
  95. %% @doc Transform a m_config value to a list, used for template loops
  96. %% @spec m_to_list(Source, Context) -> List
  97. m_to_list(#m{value=undefined}, Context) ->
  98. tree(Context);
  99. m_to_list(#m{value={cat, Id}}, Context) ->
  100. get(Id, Context);
  101. m_to_list(_, _Context) ->
  102. [].
  103. %% @doc Transform a model value so that it can be formatted or piped through filters
  104. %% @spec m_value(Source, Context) -> term()
  105. m_value(#m{value=undefined}, Context) ->
  106. tree(Context);
  107. m_value(#m{value=#m{value={cat, Id}}}, Context) ->
  108. get(Id, Context).
  109. get(Name, Context) when not is_integer(Name) ->
  110. get_by_name(Name, Context);
  111. get(Id, Context) ->
  112. F = fun() ->
  113. z_db:assoc_props_row("
  114. select c.*, r.name
  115. from category c join rsc r on r.id = c.id
  116. where c.id = $1", [Id], Context)
  117. end,
  118. z_depcache:memo(F, {category, Id}, ?WEEK, [category], Context).
  119. get_by_name(Name, Context) ->
  120. F = fun() ->
  121. z_db:assoc_props_row("
  122. select c.*, r.name
  123. from category c join rsc r on r.id = c.id
  124. where r.name = $1", [Name], Context)
  125. end,
  126. z_depcache:memo(F, {category_by_name, Name}, ?WEEK, [category], Context).
  127. get_root(Context) ->
  128. F = fun() ->
  129. z_db:assoc_props("
  130. select c.*, r.name
  131. from category c join rsc r on c.id = r.id
  132. where c.parent_id is null
  133. order by c.nr", Context)
  134. end,
  135. z_depcache:memo(F, {category_root}, ?WEEK, [category], Context).
  136. get_by_parent(Id, Context) ->
  137. F = fun() ->
  138. case Id of
  139. undefined ->
  140. get_root(Context);
  141. _ ->
  142. z_db:assoc_props("
  143. select c.*, r.name
  144. from category c join rsc r on r.id = c.id
  145. where c.parent_id = $1 order by c.nr", [Id], Context)
  146. end
  147. end,
  148. z_depcache:memo(F, {category_parent, Id}, ?WEEK, [category], Context).
  149. get_range(Id, Context) ->
  150. F = fun() ->
  151. case z_db:q("
  152. select lft, rght
  153. from category
  154. where id = $1", [Id], Context) of
  155. [Row] -> Row;
  156. _ -> {1, 0} % empty range
  157. end
  158. end,
  159. z_depcache:memo(F, {category_range, Id}, ?WEEK, [category], Context).
  160. get_range_by_name(Name, Context) ->
  161. F = fun() ->
  162. case z_db:q("
  163. select c.lft, c.rght
  164. from category c join rsc r on r.id = c.id
  165. where r.name = $1", [Name], Context) of
  166. [Row] -> Row;
  167. _ -> {1, 0} % empty range
  168. end
  169. end,
  170. z_depcache:memo(F, {category_range_name, Name}, ?WEEK, [category], Context).
  171. get_page_count(Id, Context) ->
  172. z_db:q1("select count(*) from rsc where category_id = $1", [Id], Context).
  173. name_to_id({Id}, _Context) when is_integer(Id) ->
  174. {ok, Id};
  175. name_to_id(Id, _Context) when is_integer(Id) ->
  176. {ok, Id};
  177. name_to_id(Name, Context) ->
  178. case z_depcache:get({category_name_to_id, Name}, Context) of
  179. {ok, Result} ->
  180. Result;
  181. undefined ->
  182. Result = case z_db:q1("
  183. select r.id
  184. from rsc r join category c on r.id = c.id
  185. where r.name = $1", [Name], Context) of
  186. undefined -> {error, {unknown_category, Name}};
  187. Id -> {ok, Id}
  188. end,
  189. case Result of
  190. {ok, ResultId} ->
  191. z_depcache:set({category_name_to_id, Name}, Result, ?WEEK, [category, ResultId], Context);
  192. {error, _Error} ->
  193. z_depcache:set({category_name_to_id, Name}, Result, ?WEEK, [category], Context)
  194. end,
  195. Result
  196. end.
  197. name_to_id_check(Name, Context) ->
  198. {ok, Id} = name_to_id(Name, Context),
  199. Id.
  200. id_to_name(Name, Context) when is_atom(Name); is_binary(Name); is_list(Name) ->
  201. F = fun() ->
  202. Nm = z_db:q1("select r.name from rsc r join category c on r.id = c.id where r.name = $1", [Name], Context),
  203. z_convert:to_atom(Nm)
  204. end,
  205. z_depcache:memo(F, {category_id_to_name, Name}, ?WEEK, [category], Context);
  206. id_to_name(Id, Context) when is_integer(Id) ->
  207. F = fun() ->
  208. Nm = z_db:q1("select r.name from rsc r join category c on r.id = c.id where r.id = $1", [Id], Context),
  209. z_convert:to_atom(Nm)
  210. end,
  211. z_depcache:memo(F, {category_id_to_name, Id}, ?WEEK, [category], Context).
  212. %% @doc Return the last modification date of the category. Returns false
  213. %% @spec last_modified(Cat::term(), Context) -> {ok, {{Y,M,D},{Hour,Min,Sec}}} | {error, Reason}
  214. last_modified(Cat, Context) ->
  215. case name_to_id(Cat, Context) of
  216. {ok, CatId} ->
  217. {Left, Right} = get_range(CatId, Context),
  218. case z_db:q1("select max(modified) from rsc where pivot_category_nr >= $1 and pivot_category_nr <= $2", [Left, Right], Context) of
  219. false -> {error, {no_rsc_in_cat, CatId}};
  220. Date -> {ok, Date}
  221. end;
  222. {error, Reason} ->
  223. {error, Reason}
  224. end.
  225. %% @doc Move the category below another category, placing it at the end of the children of that category.
  226. %% @spec move_below(CatId::int(), NewParentId::int(), Context) -> ok | {error, Reason}
  227. move_below(Id, ParentId, Context) ->
  228. case z_acl:is_allowed(update, Id, Context) of
  229. true ->
  230. PathParentId = [ParentId | get_path(ParentId, Context)],
  231. case lists:member(Id, PathParentId) of
  232. false ->
  233. F = fun(Ctx) ->
  234. z_db:q("update category set parent_id = $1, seq = 10000 where id = $2", [ParentId, Id], Context),
  235. renumber(Ctx)
  236. end,
  237. z_db:transaction(F, Context),
  238. z_depcache:flush(category, Context);
  239. true ->
  240. {error, cycle}
  241. end;
  242. false ->
  243. {error, eacces}
  244. end.
  245. %% @doc Move the category to the end of all categories, making it a top category in the process
  246. %% @spec move_end(CatId::int(), Context) -> ok | {error, Reason}
  247. move_end(Id, Context) ->
  248. case z_acl:is_allowed(update, Id, Context) of
  249. true ->
  250. F = fun(Ctx) ->
  251. z_db:q("update category set parent_id = null, seq = 10000 where id = $1", [Id], Context),
  252. renumber(Ctx)
  253. end,
  254. z_db:transaction(F, Context),
  255. z_depcache:flush(category, Context);
  256. false ->
  257. {error, eacces}
  258. end.
  259. %% @doc Move a category in front of another category, resetting the parent of the moved category to
  260. %% the parent of the other category.
  261. %% @spec move_before(CatId::int(), BeforeCatId::int(), Context) -> ok | {error, Reason}
  262. move_before(Id, BeforeId, Context) ->
  263. case z_acl:is_allowed(update, Id, Context) of
  264. true ->
  265. F = fun(Ctx) ->
  266. {ParentId, Seq} = z_db:q_row("select parent_id, seq from category where id = $1", [BeforeId], Context),
  267. PathParentId = [ParentId | get_path(ParentId, Context)],
  268. case lists:member(Id, PathParentId) of
  269. false ->
  270. case ParentId of
  271. undefined ->
  272. z_db:q("update category set seq = seq+1 where parent_id is null and seq >= $1", [Seq], Context);
  273. _ ->
  274. z_db:q("update category set seq = seq+1 where parent_id = $2 and seq >= $1", [Seq, ParentId], Context)
  275. end,
  276. z_db:q("update category set parent_id = $1, seq = $2 where id = $3", [ParentId, Seq, Id], Context),
  277. renumber(Ctx);
  278. true ->
  279. {error, cycle}
  280. end
  281. end,
  282. case z_db:transaction(F, Context) of
  283. ok ->
  284. z_depcache:flush(category, Context),
  285. ok;
  286. {error, Reason} ->
  287. {error, Reason}
  288. end;
  289. false ->
  290. {error, eacces}
  291. end.
  292. update_sequence(Ids, Context) ->
  293. case z_acl:is_allowed(insert, category, Context) of
  294. true ->
  295. F = fun(Ctx) ->
  296. z_db:update_sequence(category, Ids, Ctx),
  297. renumber(Ctx)
  298. end,
  299. z_db:transaction(F, Context),
  300. z_depcache:flush(category, Context);
  301. false ->
  302. {error, eacces}
  303. end.
  304. %% @doc Delete the category, move referring pages to another category. Fails when the transfer id is not a category.
  305. %% @spec delete(Id::int(), TransferId::int(), Context) -> ok | {error, Reason}
  306. delete(Id, TransferId, Context) ->
  307. % fail when deleting 'other' or 'category'
  308. case z_db:q("select name from rsc where id = $1", [Id], Context) of
  309. N when N == <<"other">>; N == <<"category">> -> {error, is_system_category};
  310. _ ->
  311. case z_acl:is_allowed(delete, Id, Context) of
  312. true ->
  313. F = fun(Ctx) ->
  314. ToId = case TransferId of
  315. undefined ->
  316. case z_db:q1("select parent_id from category where id = $1", [Id], Ctx) of
  317. undefined ->
  318. %% The removed category is a top-category, move all content to 'other'
  319. case z_db:q1("
  320. select c.id
  321. from rsc r join category c on c.id = r.id
  322. where r.name = 'other'", Context) of
  323. N when is_integer(N) -> N
  324. end;
  325. N ->
  326. N
  327. end;
  328. N when is_integer(N) ->
  329. N = z_db:q1("select id from category where id = $1", [TransferId], Ctx)
  330. end,
  331. _RscRows = z_db:q("update rsc set category_id = $1 where category_id = $2", [ToId, Id], Ctx),
  332. case Id of
  333. undefined ->
  334. z_db:q("update category set parent_id = $1 where parent_id is null", [ToId], Ctx);
  335. _ ->
  336. z_db:q("update category set parent_id = $1 where parent_id = $2", [ToId, Id], Ctx)
  337. end,
  338. ok = m_rsc_update:delete_nocheck(Id, Ctx),
  339. ok = renumber(Ctx)
  340. end,
  341. case z_db:transaction(F, Context) of
  342. ok -> z_depcache:flush(Context);
  343. {error, Reason} -> {error, Reason}
  344. end;
  345. false ->
  346. {error, eacces}
  347. end
  348. end.
  349. image(Id, Context) ->
  350. F = fun() ->
  351. #search_result{result=Result1} = z_search:search({media_category_image, [{cat,Id}]}, Context),
  352. #search_result{result=Result2} = z_search:search({media_category_depiction, [{cat,Id}]}, Context),
  353. Result1 ++ Result2
  354. end,
  355. Files = z_depcache:memo(F, {category_image, Id}, ?DAY, [category], Context),
  356. case Files of
  357. [] -> undefined;
  358. _ -> lists:nth(z_ids:number(length(Files)), Files)
  359. end.
  360. %% @doc Return the path from a root to the category (excluding the category itself)
  361. %% @spec get_path(Id, Context) -> [CatId]
  362. get_path(undefined, _Context) ->
  363. [];
  364. get_path(Id, Context) ->
  365. Cat = get(Id, Context),
  366. case proplists:get_value(path, Cat) of
  367. {ok, Path} -> Path;
  368. _ -> []
  369. end.
  370. %% @doc Return the list of categories (as atoms) that the category is part of
  371. %% @spec is_a(int(), Context) -> atomlist()
  372. is_a(Id, Context) ->
  373. F = fun() ->
  374. case m_category:name_to_id(Id, Context) of
  375. {ok, CatId} ->
  376. Path = m_category:get_path(CatId, Context),
  377. [ z_convert:to_atom(m_category:id_to_name(C, Context)) || C <- Path ++ [CatId]];
  378. {error, _} ->
  379. []
  380. end
  381. end,
  382. z_depcache:memo(F, {category_is_a, Id}, ?DAY, [category], Context).
  383. %% @doc Check if the id is within another category.
  384. %% @spec is_a(int(), Cat, Context) -> atomlist()
  385. is_a(Id, Cat, Context) ->
  386. CatName = m_category:id_to_name(Cat, Context),
  387. lists:member(CatName, is_a(Id, Context)).
  388. %% @doc Given a list of category ids, return the list of numeric ranges they cover.
  389. %% @spec ranges(CatList, Context) -> RangeList
  390. ranges(Cat, Context) when is_atom(Cat); is_integer(Cat); is_binary(Cat) ->
  391. ranges([Cat], Context);
  392. ranges(CatList0, Context) ->
  393. CatList = case z_string:is_string(CatList0) of
  394. true -> [CatList0];
  395. false -> CatList0
  396. end,
  397. F = fun(Nm, Acc) ->
  398. case name_to_id(Nm, Context) of
  399. {ok, CId} ->
  400. case get(CId, Context) of
  401. undefined -> [{-100,-100} | Acc];
  402. Props -> [{proplists:get_value(lft, Props), proplists:get_value(rght, Props)} | Acc]
  403. end;
  404. _ -> Acc
  405. end
  406. end,
  407. Ranges = lists:sort(lists:foldl(F, [], flatten_string(CatList, []))),
  408. merge_ranges(Ranges, []).
  409. %% Flatten the list of cats, but do not flatten strings
  410. flatten_string([], Acc) ->
  411. Acc;
  412. flatten_string([[A|_]=L|T], Acc) when is_list(A); is_atom(A); is_binary(A); is_tuple(A) ->
  413. Acc1 = flatten_string(L, Acc),
  414. flatten_string(T, Acc1);
  415. flatten_string([H|T], Acc) ->
  416. flatten_string(T, [H|Acc]).
  417. merge_ranges([], Acc) ->
  418. Acc;
  419. merge_ranges([{A,B},{C,D}|T], Acc) when C =< B+1 ->
  420. merge_ranges([{A,max(B,D)}|T], Acc);
  421. merge_ranges([H|T], Acc) ->
  422. merge_ranges(T, [H|Acc]).
  423. max(A,B) when A > B -> A;
  424. max(_,B) -> B.
  425. %% @doc Return a flattened representation of the complete category tree. Can be used for overviews or select boxes.
  426. %% The "meta" categories of predicate, category and group are suppressed.
  427. all_flat(Context) ->
  428. all_flat1(Context, false).
  429. all_flat_meta(Context) ->
  430. all_flat1(Context, true).
  431. all_flat1(Context, ShowMeta) ->
  432. F = fun() ->
  433. z_db:q("select c.id, c.lvl, r.name, c.props from category c join rsc r on r.id = c.id order by c.nr", Context)
  434. end,
  435. All = z_depcache:memo(F, {category_flat}, ?WEEK, [category], Context),
  436. All1 = case ShowMeta of
  437. true -> All;
  438. false -> lists:filter(fun is_not_meta/1, All)
  439. end,
  440. [ {Id, Lvl, string:copies("&nbsp;&nbsp;&nbsp;&nbsp;", Lvl-1), flat_title(Name, Props)} || {Id, Lvl, Name, Props} <- All1 ].
  441. flat_title(Name, Props) ->
  442. case proplists:get_value(title, Props) of
  443. undefined -> Name;
  444. Title -> Title
  445. end.
  446. is_not_meta({_Id, _Lvl, Name, _Props}) ->
  447. Name /= <<"meta">> andalso Name /= <<"predicate">> andalso Name /= <<"category">> andalso Name /= <<"acl_role">>.
  448. all_flat(CatId, Context) ->
  449. F = fun() ->
  450. {L,R} = boundaries(CatId, Context),
  451. z_db:q("select c.id, c.lvl, r.name, c.props from category c join rsc r on r.id = c.id where $1 <= c.nr and c.nr <= $2 order by c.nr", [L, R], Context)
  452. end,
  453. All = z_depcache:memo(F, {category_flat, CatId}, ?WEEK, [category], Context),
  454. [ {Id, Lvl, string:copies("&nbsp;&nbsp;&nbsp;&nbsp;", Lvl-1), flat_title(Name, Props)} || {Id, Lvl, Name, Props} <- All ].
  455. %% @doc Return the tree of all categories
  456. %% @spec tree(Context) -> Tree
  457. tree(Context) ->
  458. F = fun() ->
  459. CatTuples = z_db:q("
  460. select c.id, c.parent_id, c.lvl, r.name, c.props
  461. from category c join rsc r on r.id = c.id
  462. order by c.nr", Context),
  463. build_tree(CatTuples, [])
  464. end,
  465. z_depcache:memo(F, {category_tree}, ?WEEK, [category], Context).
  466. %% @doc Return the tree of all categories till a certain depth
  467. %% @spec tree_depth(Depth, Context) -> Tree
  468. tree_depth(Depth, Context) ->
  469. F = fun() ->
  470. CatTuples = z_db:q("
  471. select c.id, c.parent_id, c.lvl, r.name, c.props
  472. from category c join rsc r on r.id = c.id
  473. where c.lvl <= $1
  474. order by c.nr", [Depth], Context),
  475. build_tree(CatTuples, [])
  476. end,
  477. z_depcache:memo(F, {category_tree_depth, Depth}, ?WEEK, [category], Context).
  478. %% @doc Return the tree of all categories below a category id
  479. %% @spec tree(CatId, Context) -> TreeNode
  480. tree(CatId, Context) ->
  481. F = fun() ->
  482. CatTuples = z_db:q("
  483. select a.id, a.parent_id, a.lvl, r.name, a.props
  484. from category a join rsc r on a.id = r.id, category p
  485. where p.id = $1
  486. and a.nr <= p.rght
  487. and a.nr >= p.lft
  488. order by a.nr", [CatId], Context),
  489. case build_tree(CatTuples, []) of
  490. [TreeNode] -> TreeNode;
  491. [] -> []
  492. end
  493. end,
  494. z_depcache:memo(F, {category_tree_cat, CatId}, ?WEEK, [category], Context).
  495. %% @doc Return the tree of all categories below a category id till a certain depth
  496. %% @spec tree_depth(CatId, Depth, Context) -> TreeNode
  497. tree_depth(CatId, Depth, Context) ->
  498. F = fun() ->
  499. CatTuples = z_db:q("
  500. select a.id, a.parent_id, a.lvl, r.name, a.props
  501. from category a join rsc r on a.id = r.id, category p
  502. where p.id = $1
  503. and a.nr <= p.rght
  504. and a.nr >= p.lft
  505. and a.lvl <= p.lvl + $2
  506. order by a.nr", [CatId, Depth], Context),
  507. case build_tree(CatTuples, []) of
  508. [TreeNode] -> TreeNode;
  509. [] -> []
  510. end
  511. end,
  512. z_depcache:memo(F, {category_tree_cat_depth, CatId, Depth}, ?WEEK, [category], Context).
  513. build_tree([], Acc) ->
  514. lists:reverse(Acc);
  515. build_tree([{_Id, _Parent, _Lvl, _Name, _Props} = C|Rest], Acc) ->
  516. {C1, Rest1} = build_tree(C, [], Rest),
  517. build_tree(Rest1, [C1|Acc]).
  518. build_tree({Id, _Parent, _Lvl, _Name, _Props} = P, Acc, [{_Id2, Parent2, _Lvl2, _Name2, _Props2} = C|Rest])
  519. when Id == Parent2 ->
  520. {C1, Rest1} = build_tree(C, [], Rest),
  521. build_tree(P, [C1|Acc], Rest1);
  522. build_tree({Id, Parent, Lvl, Name, Props}, Acc, Rest) ->
  523. Props1 = case Props of
  524. <<>> -> [];
  525. _ -> Props
  526. end,
  527. {[{id,Id}, {parent_id,Parent}, {level,Lvl}, {children, {ok, lists:reverse(Acc)}}, {name, Name} | Props1], Rest}.
  528. %% @doc Renumber all categories so that the left/right and level indices are correct.
  529. %% @spec renumber(Context) -> ok
  530. renumber(Context) ->
  531. ok = z_db:transaction(fun renumber_transaction/1, Context),
  532. z_depcache:flush(category, Context),
  533. ok.
  534. renumber_transaction(Context) ->
  535. CatTuples = z_db:q("select id, parent_id, seq from category order by seq,id", Context),
  536. Enums = enumerate(CatTuples),
  537. [
  538. z_db:q("update category
  539. set nr = $2,
  540. seq = $2,
  541. lvl = $3,
  542. lft = $4,
  543. rght = $5,
  544. props = $6
  545. where id = $1",
  546. [CatId, Nr, Level, Left, Right, [{path, {ok, Path}}]]
  547. , Context)
  548. || {CatId, Nr, Level, Left, Right, Path} <- Enums
  549. ],
  550. z_pivot_rsc:insert_task(?MODULE, renumber_pivot_task, "m_category:renumber", [1], Context),
  551. ok.
  552. %% @doc Resync all ids that have their category nr changed.
  553. renumber_pivot_task(LowId, Context) ->
  554. Nrs = z_db:q("select r.id, c.nr
  555. from rsc r, category c
  556. where r.id >= $1
  557. and c.id = r.category_id
  558. and (r.pivot_category_nr is null or r.pivot_category_nr <> c.nr)
  559. order by r.id
  560. limit 500", [LowId], Context),
  561. case Nrs of
  562. [] ->
  563. case LowId of
  564. 1 -> ok;
  565. _ -> m_category:renumber_pivot_task(1, Context)
  566. end;
  567. Ids ->
  568. ok = z_db:transaction(fun(Ctx) ->
  569. [
  570. z_db:q("update rsc set pivot_category_nr = $2 where id = $1", [Id, CatNr], Ctx)
  571. || {Id,CatNr} <- Ids
  572. ],
  573. ok
  574. end, Context),
  575. {HighestId,_} = lists:last(Ids),
  576. z_pivot_rsc:insert_task(?MODULE, renumber_pivot_task, "m_category:renumber", [HighestId+1], Context)
  577. end.
  578. %% @doc Take a category list and make it into a tree, recalculating the left/right and lvl nrs
  579. %% @spec enumerate([Cat]) -> [Sort]
  580. %% Cat = {CatId, Parent, NodeSeq}
  581. %% Sort = {CatId, Nr, Level, Left, Right, Path}
  582. enumerate(Cats) ->
  583. % Fetch all the roots of our forest
  584. {Roots, Rest} = lists:partition(fun({_Id, Parent, _Seq}) -> Parent == undefined end, Cats),
  585. % Make the trees from the roots down
  586. Trees = [ make_tree(Root, Rest, 1, []) || Root <- Roots],
  587. % Flatten the trees, enumerating all nodes depth-first
  588. {Flatten,_Nr} = lists:foldl(fun(Tree, {Acc,Nr}) -> flatten_tree(Tree, Acc, Nr) end, {[],1}, Trees),
  589. Flatten.
  590. make_tree({NodeId,_Parent,NodeSeq} = Node, Nodes, Level, Path) ->
  591. SubNodes = lists:filter(fun ({_,Parent,_}) -> Parent == NodeId end, Nodes),
  592. SubTrees = [ make_tree(SubNode, Nodes, Level+1, [NodeId|Path]) || SubNode <- SubNodes ],
  593. {Level, NodeSeq, Node, lists:keysort(2, SubTrees), lists:reverse(Path)}.
  594. flatten_tree({Level, _NodeSeq, {NodeId,_Parent,_Seq}, SubTrees, Path}, NodesAcc, NodeNr) ->
  595. {NodesAcc1, NodeNr1} = lists:foldl(fun(Tree, {Acc,Nr}) -> flatten_tree(Tree, Acc, Nr) end, {NodesAcc,NodeNr+1}, SubTrees),
  596. {[ {NodeId, NodeNr, Level, NodeNr, NodeNr1-1, Path} | NodesAcc1], NodeNr1}.
  597. %% Insert a category
  598. insert(ParentId, Name, Props, Context) ->
  599. {ok, CatId} = name_to_id("category", Context),
  600. F = fun(Ctx) ->
  601. {ok, Id} = m_rsc_update:insert(Props ++ [{name, Name}, {category_id, CatId}], Ctx),
  602. case ParentId of
  603. undefined ->
  604. Id;
  605. _ -> move_below(Id, ParentId, Ctx),
  606. Id
  607. end
  608. end,
  609. F(Context).
  610. %%z_db:transaction(F, Context).
  611. %%
  612. %% Return the left/right boundaries of the given category.
  613. %% @spec boundaries(Id, C) -> {Left, Right}
  614. boundaries(CatId, Context) ->
  615. F = fun() ->
  616. case z_db:q_row("SELECT lft, rght FROM category WHERE id = $1", [CatId], Context) of
  617. {_,_} = LR -> LR;
  618. _ -> {-100,-100}
  619. end
  620. end,
  621. z_depcache:memo(F, {category_bounds, CatId}, ?WEEK, [CatId, category], Context).