PageRenderTime 189ms CodeModel.GetById 31ms app.highlight 136ms RepoModel.GetById 1ms app.codeStats 1ms

/src/models/m_rsc_update.erl

http://github.com/zotonic/zotonic
Erlang | 1343 lines | 1108 code | 153 blank | 82 comment | 26 complexity | 0d661c012857703b4c44dc91dd2b8585 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1%% @author Marc Worrell <marc@worrell.nl>
   2%% @copyright 2009-2015 Marc Worrell, Arjan Scherpenisse
   3%% @doc Update routines for resources.  For use by the m_rsc module.
   4
   5%% Copyright 2009-2015 Marc Worrell, Arjan Scherpenisse
   6%%
   7%% Licensed under the Apache License, Version 2.0 (the "License");
   8%% you may not use this file except in compliance with the License.
   9%% You may obtain a copy of the License at
  10%%
  11%%     http://www.apache.org/licenses/LICENSE-2.0
  12%%
  13%% Unless required by applicable law or agreed to in writing, software
  14%% distributed under the License is distributed on an "AS IS" BASIS,
  15%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16%% See the License for the specific language governing permissions and
  17%% limitations under the License.
  18
  19-module(m_rsc_update).
  20-author("Marc Worrell <marc@worrell.nl").
  21
  22%% interface functions
  23-export([
  24    insert/2,
  25    insert/3,
  26    delete/2,
  27    update/3,
  28    update/4,
  29    duplicate/3,
  30    merge_delete/3,
  31
  32    flush/2,
  33    
  34    normalize_props/3,
  35    normalize_props/4,
  36
  37    delete_nocheck/2,
  38    props_filter/3,
  39
  40    test/0
  41]).
  42
  43-include_lib("zotonic.hrl").
  44
  45-record(rscupd, {
  46    id=undefined,
  47    is_escape_texts=true,
  48    is_acl_check=true,
  49    is_import=false,
  50    is_no_touch=false,
  51    expected=[]
  52}).
  53
  54
  55%% @doc Insert a new resource. Crashes when insertion is not allowed.
  56-spec insert(list(), #context{}) -> {ok, integer()}.
  57insert(Props, Context) ->
  58    insert(Props, [{escape_texts, true}], Context).
  59
  60-spec insert(list(), list()|boolean(), #context{}) -> {ok, integer()}.
  61insert(Props, Options, Context) ->
  62    PropsDefaults = props_defaults(Props, Context),
  63    update(insert_rsc, PropsDefaults, Options, Context).
  64
  65
  66%% @doc Delete a resource
  67-spec delete(integer(), #context{}) -> ok.
  68delete(Id, Context) when is_integer(Id), Id /= 1 ->
  69    case z_acl:rsc_deletable(Id, Context) of
  70        true ->
  71            case m_rsc:is_a(Id, category, Context) of
  72                true ->
  73                    m_category:delete(Id, undefined, Context);
  74                false ->
  75                    delete_nocheck(Id, Context)
  76            end;
  77        false ->
  78            throw({error, eacces})
  79    end.
  80
  81
  82%% @doc Delete a resource, no check on rights etc is made. This is called by m_category:delete/3
  83%% @throws {error, Reason}
  84-spec delete_nocheck(integer(), #context{}) -> ok.
  85delete_nocheck(Id, Context) ->
  86    delete_nocheck(Id, undefined, Context).
  87
  88delete_nocheck(Id, OptFollowUpId, Context) ->
  89    Referrers = m_edge:subjects(Id, Context),
  90    CatList = m_rsc:is_a(Id, Context),
  91    Props = m_rsc:get(Id, Context),
  92    
  93    F = fun(Ctx) ->
  94        z_notifier:notify_sync(#rsc_delete{id=Id, is_a=CatList}, Ctx),
  95        m_rsc_gone:gone(Id, OptFollowUpId, Ctx),
  96        z_db:delete(rsc, Id, Ctx)
  97    end,
  98    {ok, _RowsDeleted} = z_db:transaction(F, Context),
  99    % Sync the caches
 100    [ z_depcache:flush(SubjectId, Context) || SubjectId <- Referrers ],
 101    flush(Id, CatList, Context),
 102    %% Notify all modules that the rsc has been deleted
 103    z_notifier:notify_sync(
 104                    #rsc_update_done{
 105                        action=delete,
 106                        id=Id,
 107                        pre_is_a=CatList,
 108                        post_is_a=[],
 109                        pre_props=Props,
 110                        post_props=[]
 111                    }, Context),
 112    z_edge_log_server:check(Context),
 113    ok.
 114
 115%% @doc Merge two resources, delete the losing resource.
 116-spec merge_delete(integer(), integer(), #context{}) -> ok | {error, term()}.
 117merge_delete(WinnerId, WinnerId, _Context) ->
 118    ok;
 119merge_delete(_WinnerId, 1, _Context) ->
 120    throw({error, eacces});
 121merge_delete(WinnerId, LoserId, Context) when is_integer(WinnerId), is_integer(LoserId) ->
 122    case z_acl:rsc_deletable(LoserId, Context)
 123        andalso z_acl:rsc_editable(WinnerId, Context)
 124    of
 125        true ->
 126            case m_rsc:is_a(WinnerId, category, Context) of
 127                true ->
 128                    m_category:delete(LoserId, WinnerId, Context);
 129                false ->
 130                    merge_delete_nocheck(WinnerId, LoserId, Context)
 131            end;
 132        false ->
 133            throw({error, eacces})
 134    end.
 135
 136%% @doc Merge two resources, delete the 'loser'
 137-spec merge_delete_nocheck(integer(), integer(), #context{}) -> ok.
 138merge_delete_nocheck(WinnerId, LoserId, Context) ->
 139    z_notifier:map(#rsc_merge{winner_id=WinnerId, looser_id=LoserId}, Context),
 140    ok = m_edge:merge(WinnerId, LoserId, Context),
 141    m_media:merge(WinnerId, LoserId, Context),
 142    m_identity:merge(WinnerId, LoserId, Context),
 143    move_creator_modifier_ids(WinnerId, LoserId, Context),
 144    PropsLooser = m_rsc:get(LoserId, Context),
 145    ok = delete_nocheck(LoserId, WinnerId, Context),
 146    case merge_copy_props(WinnerId, PropsLooser, Context) of
 147        [] -> 
 148            ok;
 149        UpdProps ->
 150            {ok, _} = update(WinnerId, UpdProps, [{escape_texts, false}], Context)
 151    end,
 152    ok.
 153
 154move_creator_modifier_ids(WinnerId, LoserId, Context) ->
 155    Ids = z_db:q("select id
 156                  from rsc 
 157                  where (creator_id = $1 or modifier_id = $1)
 158                    and id <> $1", 
 159                 [LoserId],
 160                 Context),
 161    z_db:q("update rsc set creator_id = $1 where creator_id = $2",
 162           [WinnerId, LoserId],
 163           Context,
 164           1200000),
 165    z_db:q("update rsc set modifier_id = $1 where modifier_id = $2",
 166           [WinnerId, LoserId],
 167           Context,
 168           1200000),
 169    lists:foreach(
 170            fun(Id) ->
 171                flush(Id, [], Context)
 172            end,
 173            Ids).
 174
 175merge_copy_props(WinnerId, Props, Context) ->
 176    merge_copy_props(WinnerId, Props, [], Context).
 177
 178merge_copy_props(_WinnerId, [], Acc, _Context) ->
 179    lists:reverse(Acc);
 180merge_copy_props(WinnerId, [{P,_}|Ps], Acc, Context) 
 181    when P =:= creator; P =:= creator_id; P =:= modifier; P =:= modifier_id;
 182         P =:= created; P =:= modified; P =:= version;
 183         P =:= id; P =:= is_published; P =:= is_protected; P =:= is_dependent;
 184         P =:= is_authoritative; P =:= pivot_geocode; P =:= pivot_geocode_qhash;
 185         P =:= category_id ->
 186    merge_copy_props(WinnerId, Ps, Acc, Context);
 187merge_copy_props(WinnerId, [{_,Empty}|Ps], Acc, Context)
 188    when Empty =:= []; Empty =:= <<>>; Empty =:= undefined ->
 189    merge_copy_props(WinnerId, Ps, Acc, Context);
 190merge_copy_props(WinnerId, [{P,_} = PV|Ps], Acc, Context) ->
 191    case m_rsc:p_no_acl(WinnerId, P, Context) of
 192        Empty when Empty =:= []; Empty =:= <<>>; Empty =:= undefined ->
 193            merge_copy_props(WinnerId, Ps, [PV|Acc], Context);
 194        _Value ->
 195            merge_copy_props(WinnerId, Ps, Acc, Context)
 196    end.
 197
 198
 199%% Flush all cached entries depending on this entry, one of its subjects or its categories.
 200flush(Id, Context) ->
 201    CatList = m_rsc:is_a(Id, Context),
 202    flush(Id, CatList, Context).
 203    
 204flush(Id, CatList, Context) ->
 205    z_depcache:flush(Id, Context),
 206    [ z_depcache:flush(Cat, Context) || Cat <- CatList ],
 207    ok.
 208
 209
 210%% @doc Duplicate a resource, creating a new resource with the given title.
 211%% @throws {error, Reason}
 212-spec duplicate(integer(), list(), #context{}) -> {ok, integer()}.
 213duplicate(Id, DupProps, Context) ->
 214    case z_acl:rsc_visible(Id, Context) of
 215        true ->
 216            Props = m_rsc:get_raw(Id, Context),
 217            FilteredProps = props_filter_protected(Props, #rscupd{id=insert_rsc, is_escape_texts=false}),
 218            SafeDupProps = z_sanitize:escape_props(DupProps, Context),
 219            InsProps = lists:foldl(
 220                            fun({Key, Value}, Acc) ->
 221                                z_utils:prop_replace(Key, Value, Acc)
 222                            end,
 223                            FilteredProps,
 224                            SafeDupProps ++ [
 225                                {name,undefined}, {uri,undefined}, {page_path,undefined},
 226                                {is_authoritative,true}, {is_protected,false},
 227                                {slug,undefined}
 228                            ]),
 229            {ok, NewId} = insert(InsProps, false, Context),
 230            m_edge:duplicate(Id, NewId, Context),
 231            m_media:duplicate(Id, NewId, Context),
 232            {ok, NewId};
 233        false ->
 234            throw({error, eacces})
 235    end.
 236
 237
 238%% @doc Update a resource
 239%% @spec update(Id, Props, Context) -> {ok, Id}
 240%% @throws {error, Reason}
 241-spec update(integer()|insert_rsc, list(), #context{}) -> {ok, integer()} | {error, term()}.
 242update(Id, Props, Context) ->
 243    update(Id, Props, [], Context).
 244
 245-spec update(integer()|insert_rsc, list(), list()|boolean(), #context{}) -> {ok, integer()} | {error, term()}.
 246update(Id, Props, false, Context) ->
 247    update(Id, Props, [{escape_texts, false}], Context);
 248update(Id, Props, true, Context) ->
 249    update(Id, Props, [{escape_texts, true}], Context);
 250
 251%% @doc Resource updater function
 252%% [Options]: {escape_texts, true|false (default: true}, {acl_check: true|false (default: true)}
 253%% {escape_texts, false} checks if the texts are escaped, and if not then it will escape. This prevents "double-escaping" of texts.
 254update(Id, Props, Options, Context) when is_integer(Id) orelse Id =:= insert_rsc ->
 255    RscUpd = #rscupd{
 256        id = Id,
 257        is_escape_texts = proplists:get_value(escape_texts, Options, true),
 258        is_acl_check = proplists:get_value(acl_check, Options, true),
 259        is_import = proplists:get_value(is_import, Options, false),
 260        is_no_touch = proplists:get_value(no_touch, Options, false)
 261                        andalso z_acl:is_admin(Context),
 262        expected = proplists:get_value(expected, Options, [])
 263    },
 264    update_imported_check(RscUpd, Props, Context).
 265
 266update_imported_check(#rscupd{is_import=true, id=Id} = RscUpd, Props, Context) when is_integer(Id) ->
 267    case m_rsc:exists(Id, Context) of
 268        false ->
 269            {ok, CatId} = m_category:name_to_id(other, Context), 
 270            1 = z_db:q("insert into rsc (id, creator_id, is_published, category_id)
 271                        values ($1, $2, false, $3)", 
 272                       [Id, z_acl:user(Context), CatId], 
 273                       Context);
 274        true -> 
 275            ok
 276    end,
 277    update_editable_check(RscUpd, Props, Context);
 278update_imported_check(RscUpd, Props, Context) ->
 279    update_editable_check(RscUpd, Props, Context).
 280
 281
 282update_editable_check(#rscupd{id=Id, is_acl_check=true} = RscUpd, Props, Context) when is_integer(Id) ->
 283    case z_acl:rsc_editable(Id, Context) of
 284        true ->
 285            update_normalize_props(RscUpd, Props, Context);
 286        false ->
 287            E = case m_rsc:p(Id, is_authoritative, Context) of
 288                false -> {error, non_authoritative};
 289                true -> {error, eacces}
 290            end,
 291            throw(E)
 292    end;
 293update_editable_check(RscUpd, Props, Context) ->
 294    update_normalize_props(RscUpd, Props, Context).
 295
 296update_normalize_props(#rscupd{id=Id} = RscUpd, Props, Context) when is_list(Props) ->
 297    AtomProps = normalize_props(Id, Props, Context),
 298    update_transaction(RscUpd, fun(_, _, _) -> {ok, AtomProps} end, Context);
 299update_normalize_props(RscUpd, Func, Context) when is_function(Func) ->
 300    update_transaction(RscUpd, Func, Context).
 301
 302update_transaction(RscUpd, Func, Context) ->
 303    Result = z_db:transaction(
 304                    fun (Ctx) ->
 305                        update_transaction_fun_props(RscUpd, Func, Ctx)
 306                    end,
 307                    Context),
 308    update_result(Result, RscUpd, Context).
 309
 310update_result({ok, NewId, notchanged}, _RscUpd, _Context) ->
 311    {ok, NewId};
 312update_result({ok, NewId, OldProps, NewProps, OldCatList, IsCatInsert}, #rscupd{id=Id}, Context) ->
 313    % Flush some low level caches
 314    case proplists:get_value(name, NewProps) of
 315        undefined -> nop;
 316        Name -> z_depcache:flush({rsc_name, z_string:to_name(Name)}, Context)
 317    end,
 318    case proplists:get_value(uri, NewProps) of
 319        undefined -> nop;
 320        Uri -> z_depcache:flush({rsc_uri, z_convert:to_list(Uri)}, Context)
 321    end,
 322
 323    % Flush category caches if a category is inserted.
 324    case IsCatInsert of
 325        true -> m_category:flush(Context);
 326        false -> nop
 327    end,
 328
 329    % Flush all cached content that is depending on one of the updated categories
 330    z_depcache:flush(NewId, Context),
 331    NewCatList = m_rsc:is_a(NewId, Context),
 332    Cats = lists:usort(NewCatList ++ OldCatList),
 333    [ z_depcache:flush(Cat, Context) || Cat <- Cats ],
 334
 335     % Notify that a new resource has been inserted, or that an existing one is updated
 336    Note = #rsc_update_done{
 337        action= case Id of insert_rsc -> insert; _ -> update end,
 338        id=NewId,
 339        pre_is_a=OldCatList,
 340        post_is_a=NewCatList,
 341        pre_props=OldProps,
 342        post_props=NewProps
 343    },
 344    z_notifier:notify_sync(Note, Context),
 345
 346    % Return the updated or inserted id
 347    {ok, NewId};
 348update_result({rollback, {_Why, _} = Er}, _RscUpd, _Context) ->
 349    throw(Er);
 350update_result({error, _} = Error, _RscUpd, _Context) ->
 351    throw(Error).
 352
 353
 354%% @doc This is running inside the rsc update db transaction
 355update_transaction_fun_props(#rscupd{id=Id} = RscUpd, Func, Context) ->
 356    Raw = get_raw_lock(Id, Context),
 357    case Func(Id, Raw, Context) of
 358        {ok, UpdateProps} ->
 359            EditableProps = props_filter_protected(
 360                                props_filter(
 361                                    props_trim(UpdateProps), [], Context),
 362                                RscUpd),
 363            AclCheckedProps = case z_acl:rsc_update_check(Id, EditableProps, Context) of
 364                                 L when is_list(L) -> L;
 365                                 {error, Reason} -> throw({error, Reason})
 366                              end,
 367            AutogeneratedProps = props_autogenerate(Id, AclCheckedProps, Context),
 368            SafeProps = case RscUpd#rscupd.is_escape_texts of
 369                            true -> z_sanitize:escape_props(AutogeneratedProps, Context);
 370                            false -> z_sanitize:escape_props_check(AutogeneratedProps, Context)
 371                        end,
 372            ok = preflight_check(Id, SafeProps, Context),
 373            throw_if_category_not_allowed(Id, SafeProps, RscUpd#rscupd.is_acl_check, Context),
 374            update_transaction_fun_insert(RscUpd, SafeProps, Raw, UpdateProps, Context);
 375        {error, _} = Error ->
 376            {rollback, Error}
 377    end.
 378
 379get_raw_lock(insert_rsc, _Context) -> [];
 380get_raw_lock(Id, Context) -> m_rsc:get_raw_lock(Id, Context).
 381
 382update_transaction_fun_insert(#rscupd{id=insert_rsc} = RscUpd, Props, _Raw, UpdateProps, Context) ->
 383     % Allow the initial insertion props to be modified.
 384    CategoryId = z_convert:to_integer(proplists:get_value(category_id, Props)),
 385    InsProps = z_notifier:foldr(#rsc_insert{}, [{category_id, CategoryId}, {version, 0}], Context),
 386
 387    % Check if the user is allowed to create the resource
 388    InsertId = case proplists:get_value(creator_id, UpdateProps) of
 389                    self ->
 390                        {ok, InsId} = z_db:insert(rsc, [{creator_id, undefined} | InsProps], Context),
 391                        1 = z_db:q("update rsc set creator_id = id where id = $1", [InsId], Context),
 392                        InsId;
 393                    CreatorId when is_integer(CreatorId) ->
 394                        {ok, InsId} = z_db:insert(rsc, [{creator_id, CreatorId} | InsProps], Context),
 395                        InsId;
 396                    undefined ->
 397                        {ok, InsId} = z_db:insert(rsc, [{creator_id, z_acl:user(Context)} | InsProps], Context),
 398                        InsId
 399                end,
 400
 401    % Insert a category record for categories. Categories are so low level that we want
 402    % to make sure that all categories always have a category record attached.
 403    IsA = m_category:is_a(CategoryId, Context),
 404    IsCatInsert = case lists:member(category, IsA) of
 405                      true ->
 406                            m_hierarchy:append('$category', [InsertId], Context),
 407                            true;
 408                      false ->
 409                            false
 410                  end,
 411     % Place the inserted properties over the update properties, replacing duplicates.
 412    Props1 = lists:foldl(
 413                    fun
 414                        ({version, _}, Acc) -> Acc;
 415                        ({creator_id, _}, Acc) -> Acc;
 416                        ({P,_} = V, Acc) -> [ V | proplists:delete(P, Acc) ]
 417                    end,
 418                    Props,
 419                    InsProps),
 420    update_transaction_fun_db(RscUpd, InsertId, Props1, InsProps, [], IsCatInsert, Context);
 421update_transaction_fun_insert(#rscupd{id=Id} = RscUpd, Props, Raw, UpdateProps, Context) ->
 422    Props1 = proplists:delete(creator_id, Props),
 423    Props2 = case z_acl:is_admin(Context) of
 424                true ->
 425                    case proplists:get_value(creator_id, UpdateProps) of
 426                        self ->
 427                            [{creator_id, Id} | Props1];
 428                        CreatorId when is_integer(CreatorId) ->
 429                            [{creator_id, CreatorId} | Props1 ];
 430                        undefined -> 
 431                            Props1
 432                    end;
 433                false ->
 434                    Props1
 435            end,
 436    IsA = m_rsc:is_a(Id, Context),
 437    update_transaction_fun_expected(RscUpd, Id, Props2, Raw, IsA, false, Context).
 438
 439update_transaction_fun_expected(#rscupd{expected=Expected} = RscUpd, Id, Props1, Raw, IsA, false, Context) ->
 440    case check_expected(Raw, Expected, Context) of
 441        ok ->
 442            update_transaction_fun_db(RscUpd, Id, Props1, Raw, IsA, false, Context);
 443        {error, _} = Error ->
 444            Error
 445    end.
 446
 447
 448check_expected(_Raw, [], _Context) ->
 449    ok;
 450check_expected(Raw, [{Key,F}|Es], Context) when is_function(F) ->
 451    case F(Key, Raw, Context) of
 452        true -> check_expected(Raw, Es, Context);
 453        false -> {error, {expected, Key, proplists:get_value(Key, Raw)}}
 454    end;
 455check_expected(Raw, [{Key,Value}|Es], Context) ->
 456    case proplists:get_value(Key, Raw) of
 457        Value -> check_expected(Raw, Es, Context);
 458        Other -> {error, {expected, Key, Other}}
 459    end.
 460
 461
 462update_transaction_fun_db(RscUpd, Id, Props, Raw, IsABefore, IsCatInsert, Context) ->
 463    {version, Version} = proplists:lookup(version, Raw),
 464    UpdateProps = [ {version, Version+1} | proplists:delete(version, Props) ],
 465    UpdateProps1 = set_if_normal_update(RscUpd, modified, erlang:universaltime(), UpdateProps),
 466    UpdateProps2 = set_if_normal_update(RscUpd, modifier_id, z_acl:user(Context), UpdateProps1),
 467    {IsChanged, UpdatePropsN} = z_notifier:foldr(#rsc_update{
 468                                            action=case RscUpd#rscupd.id of 
 469                                                        insert_rsc -> insert; 
 470                                                        _ -> update
 471                                                   end,
 472                                            id=Id, 
 473                                            props=Raw
 474                                        }, 
 475                                        {false, UpdateProps2},
 476                                        Context),
 477
 478    % Pre-pivot of the category-id to the category sequence nr.
 479    UpdatePropsN1 = case proplists:get_value(category_id, UpdatePropsN) of
 480                        undefined ->
 481                            UpdatePropsN;
 482                        CatId ->
 483                            CatNr = z_db:q1("select nr
 484                                             from hierarchy 
 485                                             where id = $1
 486                                               and name = '$category'",
 487                                            [CatId],
 488                                            Context),
 489                            [ {pivot_category_nr, CatNr} | UpdatePropsN]
 490                    end,
 491
 492    case RscUpd#rscupd.id =:= insert_rsc orelse IsChanged orelse is_changed(Raw, UpdatePropsN1) of
 493        true ->
 494            UpdatePropsPrePivoted = z_pivot_rsc:pivot_resource_update(Id, UpdatePropsN1, Raw, Context),
 495            {ok, 1} = z_db:update(rsc, Id, UpdatePropsPrePivoted, Context),
 496            ok = update_page_path_log(Id, Raw, UpdatePropsN, Context),
 497            {ok, Id, Raw, UpdatePropsN, IsABefore, IsCatInsert};
 498        false ->
 499            {ok, Id, notchanged}
 500    end.
 501
 502%% @doc Recombine all properties from the ones that are posted by a form.
 503normalize_props(Id, Props, Context) ->
 504    normalize_props(Id, Props, [], Context).
 505
 506normalize_props(Id, Props, Options, Context) ->
 507    DateProps = recombine_dates(Id, Props, Context),
 508    TextProps = recombine_languages(DateProps, Context),
 509    BlockProps = recombine_blocks(TextProps, Props, Context),
 510    IsImport = proplists:get_value(is_import, Options, false),
 511    [ {map_property_name(IsImport, P), V} || {P, V} <- BlockProps ].
 512
 513
 514set_if_normal_update(#rscupd{} = RscUpd, K, V, Props) ->
 515    set_if_normal_update_1(
 516            is_normal_update(RscUpd),
 517            K, V, Props).
 518
 519set_if_normal_update_1(false, _K, _V, Props) ->
 520    Props;
 521set_if_normal_update_1(true, K, V, Props) ->
 522    [ {K,V} | proplists:delete(K, Props) ].
 523
 524is_normal_update(#rscupd{is_import=true}) -> false;
 525is_normal_update(#rscupd{is_no_touch=true}) -> false;
 526is_normal_update(#rscupd{}) -> true.
 527
 528
 529%% @doc Check if the update will change the data in the database
 530%% @spec is_changed(Current, Props) -> bool()
 531is_changed(Current, Props) ->
 532    is_prop_changed(Props, Current).
 533
 534is_prop_changed([], _Current) ->
 535    false;
 536is_prop_changed([{version, _}|Rest], Current) ->
 537    is_prop_changed(Rest, Current);
 538is_prop_changed([{modifier_id, _}|Rest], Current) ->
 539    is_prop_changed(Rest, Current);
 540is_prop_changed([{modified, _}|Rest], Current) ->
 541    is_prop_changed(Rest, Current);
 542is_prop_changed([{pivot_category_nr, _}|Rest], Current) ->
 543    is_prop_changed(Rest, Current);
 544is_prop_changed([{Prop, Value}|Rest], Current) ->
 545    case is_equal(Value, proplists:get_value(Prop, Current)) of
 546        true -> is_prop_changed(Rest, Current);
 547        false -> true  % The property Prop has been changed.
 548    end.
 549
 550is_equal(A, A) -> true;
 551is_equal(_, undefined) -> false;
 552is_equal(undefined, _) -> false;
 553is_equal(A,B) -> z_utils:are_equal(A, B).
 554
 555
 556%% @doc Check if all props are acceptable. Examples are unique name, uri etc.
 557%% @spec preflight_check(Id, Props, Context) -> ok | {error, Reason}
 558preflight_check(insert_rsc, Props, Context) ->
 559    preflight_check(-1, Props, Context);
 560preflight_check(_Id, [], _Context) ->
 561    ok;
 562preflight_check(Id, [{name, Name}|T], Context) when Name =/= undefined ->
 563    case z_db:q1("select count(*) from rsc where name = $1 and id <> $2", [Name, Id], Context) of
 564        0 -> 
 565            preflight_check(Id, T, Context);
 566        _N ->
 567            lager:warning("[~p] Trying to insert duplicate name ~p", 
 568                          [z_context:site(Context), Name]), 
 569            throw({error, duplicate_name})
 570    end;
 571preflight_check(Id, [{page_path, Path}|T], Context) when Path =/= undefined ->
 572    case z_db:q1("select count(*) from rsc where page_path = $1 and id <> $2", [Path, Id], Context) of
 573        0 ->
 574            preflight_check(Id, T, Context);
 575        _N ->
 576            lager:warning("[~p] Trying to insert duplicate page_path ~p", 
 577                          [z_context:site(Context), Path]), 
 578            throw({error, duplicate_page_path})
 579    end;
 580preflight_check(Id, [{uri, Uri}|T], Context) when Uri =/= undefined ->
 581    case z_db:q1("select count(*) from rsc where uri = $1 and id <> $2", [Uri, Id], Context) of
 582        0 ->
 583            preflight_check(Id, T, Context);
 584        _N ->
 585            lager:warning("[~p] Trying to insert duplicate uri ~p", 
 586                          [z_context:site(Context), Uri]), 
 587            throw({error, duplicate_uri})
 588    end;
 589preflight_check(Id, [{'query', Query}|T], Context) ->
 590    Valid = case m_rsc:is_a(Id, 'query', Context) of
 591                true ->
 592                    try
 593                        search_query:search(search_query:parse_query_text(z_html:unescape(Query)), Context), 
 594                        true
 595                    catch
 596                        _: {error, {_, _}} ->
 597                            false
 598                    end;
 599                false -> true
 600            end,
 601    case Valid of
 602        true -> preflight_check(Id, T, Context);
 603        false -> throw({error, invalid_query})
 604    end;
 605preflight_check(Id, [_H|T], Context) ->
 606    preflight_check(Id, T, Context).
 607
 608
 609throw_if_category_not_allowed(_Id, _SafeProps, false, _Context) ->
 610    ok;
 611throw_if_category_not_allowed(insert_rsc, SafeProps, _True, Context) ->
 612    case proplists:get_value(category_id, SafeProps) of
 613        undefined -> 
 614            throw({error, nocategory});
 615        CatId ->
 616            throw_if_category_not_allowed_1(undefined, CatId, Context)
 617    end;
 618throw_if_category_not_allowed(Id, SafeProps, _True, Context) ->
 619    case proplists:get_value(category_id, SafeProps) of
 620        undefined ->
 621            ok;
 622        CatId ->
 623            PrevCatId = z_db:q1("select category_id from rsc where id = $1", [Id], Context),
 624            throw_if_category_not_allowed_1(PrevCatId, CatId, Context)
 625    end.
 626
 627throw_if_category_not_allowed_1(CatId, CatId, _Context) ->
 628    ok;
 629throw_if_category_not_allowed_1(_PrevCatId, CatId, Context) ->
 630    CategoryName = m_category:id_to_name(CatId, Context),
 631    case z_acl:is_allowed(insert, #acl_rsc{category=CategoryName}, Context) of
 632        true -> ok;
 633        _False -> throw({error, eaccess})
 634    end.
 635
 636
 637%% @doc Remove whitespace around some predefined fields
 638props_trim(Props) ->
 639    [
 640        case is_trimmable(P,V) of
 641            true -> {P, z_string:trim(V)};
 642            false -> {P,V}
 643        end
 644        || {P,V} <- Props
 645    ].
 646
 647
 648%% @doc Remove properties the user is not allowed to change and convert some other to the correct data type
 649%% @spec props_filter(Props1, Acc, Context) -> Props2
 650props_filter([], Acc, _Context) ->
 651    Acc;
 652
 653props_filter([{uri, Uri}|T], Acc, Context) ->
 654    case Uri of
 655        Empty when Empty == undefined; Empty == []; Empty == <<>> ->
 656            props_filter(T, [{uri, undefined} | Acc], Context);
 657        _ ->
 658            props_filter(T, [{uri, z_sanitize:uri(Uri)} | Acc], Context)
 659    end;
 660
 661props_filter([{name, Name}|T], Acc, Context) ->
 662    case z_acl:is_allowed(use, mod_admin, Context) of
 663        true ->
 664            case Name of
 665                Empty when Empty == undefined; Empty == []; Empty == <<>> ->
 666                    props_filter(T, [{name, undefined} | Acc], Context);
 667                _ ->
 668                    props_filter(T, [{name, z_string:to_name(Name)} | Acc], Context)
 669            end;
 670        false ->
 671            props_filter(T, Acc, Context)
 672    end;
 673
 674props_filter([{page_path, Path}|T], Acc, Context) ->
 675    case z_acl:is_allowed(use, mod_admin, Context) of
 676        true ->
 677            case Path of
 678                Empty when Empty == undefined; Empty == []; Empty == <<>> ->
 679                    props_filter(T, [{page_path, undefined} | Acc], Context);
 680                _ ->
 681                    P = [ $/ | string:strip(z_utils:url_path_encode(Path), both, $/) ],
 682                    props_filter(T, [{page_path, P} | Acc], Context)
 683            end;
 684        false ->
 685            props_filter(T, Acc, Context)
 686    end;
 687props_filter([{slug, undefined}|T], Acc, Context) ->
 688    props_filter(T, [{slug, []} | Acc], Context);
 689props_filter([{slug, <<>>}|T], Acc, Context) ->
 690    props_filter(T, [{slug, []} | Acc], Context);
 691props_filter([{slug, ""}|T], Acc, Context) ->
 692    props_filter(T, [{slug, []} | Acc], Context);
 693props_filter([{slug, Slug}|T], Acc, Context) ->
 694    props_filter(T, [{slug, to_slug(Slug, Context)} | Acc], Context);
 695props_filter([{custom_slug, P}|T], Acc, Context) ->
 696    props_filter(T, [{custom_slug, z_convert:to_bool(P)} | Acc], Context);
 697
 698props_filter([{B, P}|T], Acc, Context) 
 699    when  B =:= is_published; B =:= is_featured; B=:= is_protected;
 700          B =:= is_dependent; B =:= is_query_live; B =:= date_is_all_day;
 701          B =:= is_website_redirect; B =:= is_page_path_multiple;
 702          B =:= is_authoritative ->
 703    props_filter(T, [{B, z_convert:to_bool(P)} | Acc], Context);
 704
 705props_filter([{P, DT}|T], Acc, Context) 
 706    when P =:= created; P =:= modified; 
 707         P =:= date_start; P =:= date_end;
 708         P =:= publication_start; P =:= publication_end  ->
 709    props_filter(T, [{P,z_datetime:to_datetime(DT)}|Acc], Context);
 710
 711props_filter([{P, Id}|T], Acc, Context) 
 712    when P =:= creator_id; P =:= modifier_id ->
 713    case m_rsc:rid(Id, Context) of
 714        undefined ->
 715            props_filter(T, Acc, Context);
 716        RId ->
 717            props_filter(T, [{P,RId}|Acc], Context)
 718    end;
 719
 720props_filter([{visible_for, Vis}|T], Acc, Context) ->
 721    VisibleFor = z_convert:to_integer(Vis),
 722    case VisibleFor of
 723        N when N >= 0 ->
 724            props_filter(T, [{visible_for, N} | Acc], Context);
 725        _ ->
 726            props_filter(T, Acc, Context)
 727    end;
 728
 729props_filter([{category, CatName}|T], Acc, Context) ->
 730    props_filter([{category_id, m_category:name_to_id_check(CatName, Context)} | T], Acc, Context);
 731props_filter([{category_id, CatId}|T], Acc, Context) ->
 732    CatId1 = m_rsc:rid(CatId, Context),
 733    case m_rsc:is_a(CatId1, category, Context) of
 734        true ->
 735            props_filter(T, [{category_id, CatId1}|Acc], Context);
 736        false ->
 737            lager:error("[~p] Ignoring unknown category '~p' in update, using 'other' instead.",
 738                        [z_context:site(Context), CatId]),
 739            props_filter(T, [{category_id,m_rsc:rid(other, Context)}|Acc], Context)
 740    end;
 741
 742props_filter([{content_group, undefined}|T], Acc, Context) ->
 743    props_filter(T, [{content_group_id, undefined}|Acc], Context);
 744props_filter([{content_group, CgName}|T], Acc, Context) ->
 745    case m_rsc:rid(CgName, Context) of
 746        undefined ->
 747            lager:error("[~p] Ignoring unknown content group '~p' in update.",
 748                        [z_context:site(Context), CgName]),
 749            props_filter(T, Acc, Context);
 750        CgId ->
 751            props_filter([{content_group_id, CgId}|T], Acc, Context)
 752    end;
 753props_filter([{content_group_id, undefined}|T], Acc, Context) ->
 754    props_filter(T, [{content_group_id, undefined}|Acc], Context);
 755props_filter([{content_group_id, CgId}|T], Acc, Context) ->
 756    CgId1 = m_rsc:rid(CgId, Context),
 757    case m_rsc:is_a(CgId1, content_group, Context) 
 758        orelse m_rsc:is_a(CgId1, acl_collaboration_group, Context)
 759    of
 760        true ->
 761            props_filter(T, [{content_group_id, CgId1}|Acc], Context);
 762        false ->
 763            lager:error("[~p] Ignoring unknown content group '~p' in update.",
 764                        [z_context:site(Context), CgId]),
 765            props_filter(T, Acc, Context)
 766    end;
 767
 768props_filter([{Location, P}|T], Acc, Context) 
 769    when Location =:= location_lat; Location =:= location_lng ->
 770    X = try
 771            z_convert:to_float(P)
 772        catch
 773            _:_ -> undefined
 774        end,
 775    props_filter(T, [{Location, X} | Acc], Context);
 776
 777props_filter([{pref_language, Lang}|T], Acc, Context) ->
 778    Lang1 = case z_trans:to_language_atom(Lang) of
 779                {ok, LangAtom} -> LangAtom;
 780                {error, not_a_language} -> undefined
 781            end,
 782    props_filter(T, [{pref_language, Lang1} | Acc], Context);
 783
 784props_filter([{language, Langs}|T], Acc, Context) ->
 785    props_filter(T, [{language, filter_languages(Langs)}|Acc], Context);
 786
 787props_filter([{_Prop, _V}=H|T], Acc, Context) ->
 788    props_filter(T, [H|Acc], Context).
 789
 790
 791%% Filter all given languages, drop unknown languages.
 792%% Ensure that the languages are a list of atoms.
 793filter_languages([]) -> [];
 794filter_languages(<<>>) -> [];
 795filter_languages(Lang) when is_binary(Lang); is_atom(Lang) ->
 796    filter_languages([Lang]);
 797filter_languages([C|_] = Lang) when is_integer(C) ->
 798    filter_languages([Lang]);
 799filter_languages([L|_] = Langs) when is_list(L); is_binary(L); is_atom(L) ->
 800    lists:foldr(
 801            fun(Lang, Acc) ->
 802                case z_trans:to_language_atom(Lang) of
 803                    {ok, LangAtom} -> [LangAtom|Acc];
 804                    {error, not_a_language} -> Acc
 805                end
 806            end,
 807            [],
 808            Langs).
 809
 810%% @doc Automatically modify some props on update.
 811%% @spec props_autogenerate(Id, Props1, Context) -> Props2
 812props_autogenerate(Id, Props, Context) ->
 813    %% When title is updating, check the rsc 'custom_slug' field to see if we need to update the slug or not.
 814    Props1 = case proplists:get_value(title, Props) of
 815                 undefined -> Props;
 816                 Title ->
 817                     case {proplists:get_value(custom_slug, Props), m_rsc:p(custom_slug, Id, Context)} of
 818                         {true, _} -> Props;
 819                         {_, true} -> Props;
 820                         _X ->
 821                             %% Determine the slug from the title.
 822                             [{slug, to_slug(Title, Context)} | proplists:delete(slug, Props)]
 823                     end
 824             end,
 825    Props1.
 826
 827
 828%% @doc Fill in some defaults for empty props on insert.
 829%% @spec props_defaults(Props1, Context) -> Props2
 830props_defaults(Props, Context) ->
 831    % Generate slug from the title (when there is a title)
 832    Props1 = case proplists:get_value(slug, Props) of
 833                undefined ->
 834                    case proplists:get_value(title, Props) of
 835                        undefined ->
 836                            Props;
 837                        Title ->
 838                            lists:keystore(slug, 1, Props, {slug, to_slug(Title, Context)})
 839                    end;
 840                _ ->
 841                    Props
 842             end,
 843    % Assume content is authoritative, unless stated otherwise
 844    case proplists:get_value(is_authoritative, Props1) of
 845        undefined -> [{is_authoritative, true}|Props1];
 846        _ -> Props
 847    end.
 848
 849
 850props_filter_protected(Props, RscUpd) ->
 851    IsNormal = is_normal_update(RscUpd),
 852    lists:filter(fun
 853                    ({K, _}) -> not is_protected(K, IsNormal) 
 854                 end,
 855                 Props).
 856
 857
 858to_slug(undefined, _Context) -> undefined;
 859to_slug({trans, _} = Tr, Context) -> to_slug(z_trans:lookup_fallback(Tr, en, Context), Context);
 860to_slug(B, _Context) when is_binary(B) -> truncate_slug(z_string:to_slug(B)); 
 861to_slug(X, Context) -> to_slug(z_convert:to_binary(X), Context).
 862
 863truncate_slug(<<Slug:78/binary, _/binary>>) -> Slug;
 864truncate_slug(Slug) -> Slug.
 865
 866%% @doc Map property names to an atom, fold pivot and computed fields together for later filtering.
 867map_property_name(IsImport, P) when not is_list(P) -> map_property_name(IsImport, z_convert:to_list(P));
 868map_property_name(_IsImport, "computed_"++_) -> computed_xxx;
 869map_property_name(_IsImport, "pivot_"++_) -> pivot_xxx;
 870map_property_name(false, P) when is_list(P) -> erlang:list_to_existing_atom(P);
 871map_property_name(true,  P) when is_list(P) -> erlang:list_to_atom(P).
 872
 873
 874%% @doc Properties that can't be updated with m_rsc_update:update/3 or m_rsc_update:insert/2
 875is_protected(id, _IsNormal)            -> true;
 876is_protected(created, true)            -> true;
 877is_protected(creator_id, true)         -> true;
 878is_protected(modified, true)           -> true;
 879is_protected(modifier_id, true)        -> true;
 880is_protected(props, _IsNormal)         -> true;
 881is_protected(version, _IsNormal)       -> true;
 882is_protected(page_url, _IsNormal)      -> true;
 883is_protected(medium, _IsNormal)        -> true;
 884is_protected(pivot_xxx, _IsNormal)     -> true;
 885is_protected(computed_xxx, _IsNormal)  -> true;
 886is_protected(_, _IsNormal)             -> false.
 887
 888
 889is_trimmable(_, V) when not is_binary(V), not is_list(V) -> false;
 890is_trimmable(title, _)       -> true;
 891is_trimmable(title_short, _) -> true;
 892is_trimmable(summary, _)     -> true;
 893is_trimmable(chapeau, _)     -> true;
 894is_trimmable(subtitle, _)    -> true;
 895is_trimmable(email, _)       -> true;
 896is_trimmable(uri, _)         -> true;
 897is_trimmable(website, _)     -> true;
 898is_trimmable(page_path, _)   -> true;
 899is_trimmable(name, _)        -> true;
 900is_trimmable(slug, _)        -> true;
 901is_trimmable(custom_slug, _) -> true;
 902is_trimmable(category, _)    -> true;
 903is_trimmable(rsc_id, _)      -> true;
 904is_trimmable(_, _)           -> false.
 905
 906
 907%% @doc Combine all textual date fields into real date. Convert them to UTC afterwards.
 908recombine_dates(Id, Props, Context) ->
 909    LocalNow = z_datetime:to_local(erlang:universaltime(), Context),
 910    {Dates, Props1} = recombine_dates_1(Props, [], []),
 911    {Dates1, DateGroups} = group_dates(Dates),
 912    {DateGroups1, DatesNull} = collect_empty_date_groups(DateGroups, [], []),
 913    {Dates2, DatesNull1} = collect_empty_dates(Dates1, [], DatesNull),
 914    Dates3 = [ {Name, date_from_default(LocalNow, D)} || {Name, D} <- Dates2 ],
 915    DateGroups2 = [ {Name, dategroup_fill_parts(date_from_default(LocalNow, S), E)} || {Name, {S,E}} <- DateGroups1 ],
 916    Dates4 = lists:foldl(
 917                    fun({Name, {S, E}}, Acc) ->
 918                        [
 919                            {Name++"_start", S},
 920                            {Name++"_end", E} 
 921                            | Acc
 922                        ] 
 923                    end,
 924                    Dates3,
 925                    DateGroups2),
 926    DatesUTC = maybe_dates_to_utc(Id, Dates4, Props, Context),
 927    [
 928        {tz, z_context:tz(Context)}
 929        | DatesUTC ++ DatesNull1 ++ Props1
 930    ].
 931
 932maybe_dates_to_utc(Id, Dates, Props, Context) ->
 933    IsAllDay = is_all_day(Id, Props, Context),
 934    [ maybe_to_utc(IsAllDay, NameDT,Context) || NameDT <- Dates ].
 935
 936maybe_to_utc(true, {"date_start", _Date} = D, _Context) ->
 937    D;
 938maybe_to_utc(true, {"date_end", _Date} = D, _Context) ->
 939    D;
 940maybe_to_utc(_IsAllDay, {Name, Date}, Context) ->
 941    {Name, z_datetime:to_utc(Date, Context)}.
 942
 943is_all_day(Id, Props, Context) ->
 944    case proplists:get_value(date_is_all_day, Props) of
 945        undefined ->
 946            case proplists:get_value("date_is_all_day", Props) of
 947                undefined ->
 948                    case is_integer(Id) of
 949                        false ->
 950                            false;
 951                        true  ->
 952                            z_convert:to_bool(m_rsc:p_no_acl(Id, date_is_all_day, Context))
 953                    end;
 954                IsAllDay ->
 955                    z_convert:to_bool(IsAllDay)
 956            end;
 957        IsAllDay ->
 958            z_convert:to_bool(IsAllDay)
 959    end.
 960
 961
 962collect_empty_date_groups([], Acc, Null) ->
 963    {Acc, Null};
 964collect_empty_date_groups([{"publication", _} = R|T], Acc, Null) ->
 965    collect_empty_date_groups(T, [R|Acc], Null);
 966collect_empty_date_groups([{Name, {
 967                            {{undefined, undefined, undefined}, {undefined, undefined, undefined}},
 968                            {{undefined, undefined, undefined}, {undefined, undefined, undefined}}
 969                            }}|T], Acc, Null) ->
 970    collect_empty_date_groups(T, Acc, [{Name++"_start", undefined}, {Name++"_end", undefined} | Null]);
 971collect_empty_date_groups([H|T], Acc, Null) ->
 972    collect_empty_date_groups(T, [H|Acc], Null).
 973
 974
 975
 976collect_empty_dates([], Acc, Null) ->
 977    {Acc, Null};
 978collect_empty_dates([{Name, {{undefined, undefined, undefined}, {undefined, undefined, undefined}}}|T], Acc, Null) ->
 979    collect_empty_dates(T, Acc, [{Name, undefined}|Null]);
 980collect_empty_dates([H|T], Acc, Null) ->
 981    collect_empty_dates(T, [H|Acc], Null).
 982
 983
 984
 985recombine_dates_1([], Dates, Acc) ->
 986    {Dates, Acc};
 987recombine_dates_1([{"dt:"++K,V}|T], Dates, Acc) ->
 988    [Part, End, Name] = string:tokens(K, ":"),
 989    Dates1 = recombine_date(Part, End, Name, V, Dates),
 990    recombine_dates_1(T, Dates1, Acc);
 991recombine_dates_1([H|T], Dates, Acc) ->
 992    recombine_dates_1(T, Dates, [H|Acc]).
 993
 994    recombine_date(Part, End, Name, undefined, Dates) ->
 995        recombine_date(Part, End, Name, "", Dates);
 996    recombine_date(Part, _End, Name, V, Dates) ->
 997        Date = case proplists:get_value(Name, Dates) of
 998            undefined ->
 999                {{undefined, undefined, undefined}, {undefined, undefined, undefined}};
1000            D ->
1001                D
1002        end,
1003        Date1 = recombine_date_part(Date, Part, to_date_value(Part, string:strip(V))),
1004        lists:keystore(Name, 1, Dates, {Name, Date1}).
1005
1006    recombine_date_part({{_Y,M,D},{H,I,S}}, "y", V) -> {{V,M,D},{H,I,S}};
1007    recombine_date_part({{Y,_M,D},{H,I,S}}, "m", V) -> {{Y,V,D},{H,I,S}};
1008    recombine_date_part({{Y,M,_D},{H,I,S}}, "d", V) -> {{Y,M,V},{H,I,S}};
1009    recombine_date_part({{Y,M,D},{_H,I,S}}, "h", V) -> {{Y,M,D},{V,I,S}};
1010    recombine_date_part({{Y,M,D},{H,_I,S}}, "i", V) -> {{Y,M,D},{H,V,S}};
1011    recombine_date_part({{Y,M,D},{H,I,_S}}, "s", V) -> {{Y,M,D},{H,I,V}};
1012    recombine_date_part({{Y,M,D},{_H,_I,S}}, "hi", {H,I,_S}) -> {{Y,M,D},{H,I,S}};
1013    recombine_date_part({{Y,M,D},_Time}, "his", {_,_,_} = V) -> {{Y,M,D},V};
1014    recombine_date_part({_Date,{H,I,S}}, "ymd", {_,_,_} = V) -> {V,{H,I,S}}.
1015
1016    to_date_value(Part, V) when Part == "ymd" orelse Part == "his"->
1017        case string:tokens(V, "-/: ") of
1018            [] -> {undefined, undefined, undefined};
1019            [Y,M,D] -> {to_int(Y), to_int(M), to_int(D)}
1020        end;
1021    to_date_value("hi", V) ->
1022        case string:tokens(V, "-/: ") of
1023            [] -> {undefined, undefined, undefined};
1024            [H] -> {to_int(H), 0, undefined};
1025            [H,I] -> {to_int(H), to_int(I), undefined}
1026        end;
1027    to_date_value(_, V) ->
1028        to_int(V).
1029
1030group_dates(Dates) ->
1031    group_dates(Dates, [], []).
1032
1033    group_dates([], Groups, Acc) ->
1034        {Acc, Groups};
1035    group_dates([{Name,D}|T], Groups, Acc) ->
1036        case lists:suffix("_start", Name) of
1037            true ->
1038                Base = lists:sublist(Name, length(Name) - 6),
1039                Range = case proplists:get_value(Base, Groups) of
1040                    {_Start, End} ->
1041                        { D, End };
1042                    undefined ->
1043                        { D, {{undefined, undefined, undefined}, {undefined, undefined, undefined}} }
1044                end,
1045                Groups1 = lists:keystore(Base, 1, Groups, {Base, Range}),
1046                group_dates(T, Groups1, Acc);
1047
1048            false ->
1049                case lists:suffix("_end", Name) of
1050                    true ->
1051                        Base = lists:sublist(Name, length(Name) - 4),
1052                        Range = case proplists:get_value(Base, Groups) of
1053                            {Start, _End} ->
1054                                { Start, D };
1055                            undefined ->
1056                                { {{undefined, undefined, undefined}, {0, 0, 0}}, D }
1057                        end,
1058                        Groups1 = lists:keystore(Base, 1, Groups, {Base, Range}),
1059                        group_dates(T, Groups1, Acc);
1060
1061                    false ->
1062                        group_dates(T, Groups, [{Name,D}|Acc])
1063                end
1064        end.
1065
1066
1067dategroup_fill_parts( S, {{undefined,undefined,undefined},{undefined,undefined,undefined}} ) ->
1068    {S, ?ST_JUTTEMIS};
1069dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{undefined,Me,De},{He,Ie,Se}} ) ->
1070    dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ys,Me,De},{He,Ie,Se}} );
1071dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,undefined,De},{He,Ie,Se}} ) ->
1072    dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}} ,{{Ye,Ms,De},{He,Ie,Se}} );
1073dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,undefined},{He,Ie,Se}} ) ->
1074    dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,Ds},{He,Ie,Se}} );
1075dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{undefined,Ie,Se}} ) ->
1076    dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{23,Ie,Se}} );
1077dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,undefined,Se}} ) ->
1078    dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,59,Se}} );
1079dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,undefined}} ) ->
1080    dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,59}} );
1081dategroup_fill_parts( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,Se}} ) ->
1082    {{{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,Se}}}.
1083
1084
1085date_from_default( S, {{undefined,undefined,undefined},{undefined,undefined,undefined}} ) ->
1086    S;
1087date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{undefined,Me,De},{He,Ie,Se}} ) ->
1088    date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ys,Me,De},{He,Ie,Se}} );
1089date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,undefined,De},{He,Ie,Se}} ) ->
1090    date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Ms,De},{He,Ie,Se}} );
1091date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,undefined},{He,Ie,Se}} ) ->
1092    date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,Ds},{He,Ie,Se}} );
1093date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{undefined,Ie,Se}} ) ->
1094    date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{0,Ie,Se}} );
1095date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,undefined,Se}} ) ->
1096    date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,0,Se}} );
1097date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,undefined}} ) ->
1098    date_from_default( {{Ys,Ms,Ds},{Hs,Is,Ss}}, {{Ye,Me,De},{He,Ie,0}} );
1099date_from_default( _S, {{Ye,Me,De},{He,Ie,Se}} ) ->
1100    {{Ye,Me,De},{He,Ie,Se}}.
1101
1102to_int("") ->
1103    undefined;
1104to_int(<<>>) ->
1105    undefined;
1106to_int(A) ->
1107    try
1108        list_to_integer(A)
1109    catch
1110        _:_ -> undefined
1111    end.
1112
1113% to_datetime(undefined) ->
1114%     erlang:universaltime();
1115% to_datetime(B) ->
1116%     case z_datetime:to_datetime(B) of
1117%         undefined -> erlang:universaltime();
1118%         DT -> DT
1119%     end.
1120
1121%% @doc get all languages encoded in proplists' keys.
1122%% e.g. m_rsc_update:props_languages([{"foo$en", x}, {"bar$nl", x}]) -> ["en", "nl"]
1123props_languages(Props) ->
1124    lists:foldr(fun({Key, _}, Acc) ->
1125                        case string:tokens(z_convert:to_list(Key), [$$]) of
1126                            [_, Lang] ->
1127                                case lists:member(Lang, Acc) of
1128                                    true -> Acc;
1129                                    false -> [Lang|Acc]
1130                                end;
1131                            _ -> Acc
1132                        end
1133                end, [], Props).
1134
1135
1136%% @doc Combine language versions of texts. Assume we edit all texts or none.
1137recombine_languages(Props, Context) ->
1138    case props_languages(Props) of
1139        [] ->
1140            Props;
1141        L -> 
1142            Cfg = [ atom_to_list(Code) || Code <- config_langs(Context) ],
1143            L1 = filter_langs(edited_languages(Props, L), Cfg),
1144            {LangProps, OtherProps} = comb_lang(Props, L1, [], []),
1145            LangProps ++ [{language, [list_to_atom(Lang) || Lang <- L1]}|proplists:delete("language", OtherProps)]
1146    end.
1147
1148    %% @doc Fetch all the edited languages, from 'language' inputs or a merged 'language' property
1149    edited_languages(Props, PropLangs) ->
1150        case proplists:is_defined("language", Props) of
1151            true ->
1152                proplists:get_all_values("language", Props);
1153            false ->
1154                case proplists:get_value(language, Props) of
1155                    L when is_list(L) ->
1156                        [ z_convert:to_list(Lang) || Lang <- L ];
1157                    undefined ->
1158                        PropLangs
1159                end
1160        end.
1161
1162    comb_lang([], _L1, LAcc, OAcc) ->
1163        {LAcc, OAcc};
1164    comb_lang([{P,V}|Ps], L1, LAcc, OAcc) when is_list(P) ->
1165        case string:tokens(P, "$") of
1166            [P1,Lang] ->
1167                case lists:member(Lang, L1) of
1168                    true -> comb_lang(Ps, L1, append_langprop(P1, Lang, V, LAcc), OAcc);
1169                    false -> comb_lang(Ps, L1, LAcc, OAcc)
1170                end;
1171            _ ->
1172                comb_lang(Ps, L1, LAcc, [{P,V}|OAcc])
1173        end;
1174    comb_lang([PV|Ps], L1, LAcc, OAcc) ->
1175        comb_lang(Ps, L1, LAcc, [PV|OAcc]).
1176
1177
1178    append_langprop(P, Lang, V, Acc) ->
1179        Lang1 = list_to_atom(Lang),
1180        case proplists:get_value(P, Acc) of
1181            {trans, Tr} ->
1182                Tr1 = [{Lang1, z_convert:to_binary(V)}|Tr],
1183                [{P, {trans, Tr1}} | proplists:delete(P, Acc)];
1184            undefined ->
1185                [{P, {trans, [{Lang1,z_convert:to_binary(V)}]}}|Acc]
1186        end.
1187
1188recombine_blocks(Props, OrgProps, Context) ->
1189    Props1 = recombine_blocks_form(Props, OrgProps, Context),
1190    recombine_blocks_import(Props1, OrgProps, Context).
1191
1192recombine_blocks_form(Props, OrgProps, Context) ->
1193    {BPs, Ps} = lists:partition(fun({"block-"++ _, _}) -> true; (_) -> false end, Props),
1194    case BPs of
1195        [] ->
1196            case proplists:get_value(blocks, Props) of
1197                Blocks when is_list(Blocks) ->
1198                    Blocks1 = [ {proplists:get_value(name, B),B} || B <- Blocks ],
1199                    z_utils:prop_replace(blocks, normalize_blocks(Blocks1, Context), Props);
1200                _ ->
1201                    Props
1202            end;
1203        _ ->
1204            Keys = block_ids(OrgProps, []),
1205            Dict = lists:foldr(
1206                            fun ({"block-", _}, Acc) -> 
1207                                    Acc;
1208                                ({"block-"++Name, Val}, Acc) ->
1209                                    Ts = string:tokens(Name, "-"),
1210                                    BlockId = iolist_to_binary(tl(lists:reverse(Ts))),
1211                                    BlockField = lists:last(Ts),
1212                                    dict:append(BlockId, {BlockField, Val}, Acc)
1213                            end,
1214                            dict:new(),
1215                            BPs),
1216            Blocks = normalize_blocks([ {K, dict:fetch(K, Dict)} || K <- Keys ], Context),
1217            [{blocks, Blocks++proplists:get_value(blocks, Ps, [])} | p

Large files files are truncated, but you can click here to view the full file