PageRenderTime 90ms CodeModel.GetById 7ms app.highlight 76ms RepoModel.GetById 2ms app.codeStats 0ms

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