From d91f9d45de8c25d02a6abd34c2179cb109aed8d0 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 20 Sep 2016 16:55:37 +0100 Subject: [PATCH] Ensure that recipe IDs do not change between edits --- modules/ORB/System/Recipe.pm | 200 +++++++++++++++++++++++++++++++---- 1 file changed, 182 insertions(+), 18 deletions(-) diff --git a/modules/ORB/System/Recipe.pm b/modules/ORB/System/Recipe.pm index f367f1e..afe7862 100644 --- a/modules/ORB/System/Recipe.pm +++ b/modules/ORB/System/Recipe.pm @@ -86,17 +86,18 @@ sub new { # ============================================================================ -# Recipe creation and deletion +# Recipe creation and status modification ## @method $ create(%args) -# Create a new recipe in the system, or edit a recipe setting the status of -# the old version to 'edited'. The args hash can contain the following, all +# Create a new recipe in the system. The args hash can contain the following, all # fields are required unless indicated otherwise: # # - `previd`: (optional) ID of the recipe this is an edit of. If specified, # the old recipe has its state set to 'edited', and the # metadata context of the new recipe is created as a child of -# the old recipe to ensure editing works as expected. +# the old recipe to ensure editing works as expected. Generally +# this will not be specified directly; if editing a recipe, +# call edit() to have renumbering handled for you. # - `name`: The name of the recipe # - `source`: (optional) Where did the recipe come from originally? # - `timereq`: A string describing the time required for the recipe @@ -112,27 +113,34 @@ sub new { # - `ingredients`: A reference to an array of ingredient hashes. See the # documentation for _add_recipe_ingredients() for the # required hash values +# - `tags`: The tags to set for the recipe, may be either a comma +# separated string of tags, or a reference to an array +# of tags. May be undef or an empty string. # # @param args A hash, or reference to a hash, of values to use when creating # the new recipe. -# @return A reference to a hash containing the new recipe ID on success, -# undef on error. +# @return The new recipe ID on success, undef on error. sub create { my $self = shift; my $args = hash_or_hashref(@_); $self -> clear_error(); + # Get IDs for the type and status + $args -> {"typeid"} = $self -> {"system"} -> {"types"} -> get_id($args -> {"type"}) + or return $self -> self_error($self -> {"system"} -> {"types"} -> errstr()); + $args -> {"statusid"} = $self -> {"system"} -> {"states"} -> get_id($args -> {"status"}) + or return $self -> self_error($self -> {"system"} -> {"states"} -> errstr()); # We need a metadata context for the recipe my $metadataid = $self -> _create_recipe_metadata($args -> {"previd"}); # Do the insert, and fetch the ID of the new row my $newh = $self -> {"dbh"} -> prepare("INSERT INTO `".$self -> {"settings"} -> {"database"} -> {"recipes"}."` - (`metadata_id`, `prev_id`, `name`, `source`, `timereq`, `timemins`, `yield`, `temp`, `temptype`, `method`, `notes`, `type_id`, `status_id`, `creator_id`, `created`) - VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UNIX_TIMESTAMP())"); - my $result = $newh -> execute($metadataid, $args -> {"previd"}, $args -> {"name"}, $args -> {"source"}, $args -> {"timereq"}, $args -> {"timemins"}, $args -> {"yield"}, $args -> {"temp"}, $args -> {"temptype"}, $args -> {"method"}, $args -> {"notes"}, $args -> {"type_id"}, $args -> {"status_id"}, $args -> {"creatorid"}); + (`id`, `metadata_id`, `prev_id`, `name`, `source`, `timereq`, `timemins`, `yield`, `temp`, `temptype`, `method`, `notes`, `type_id`, `status_id`, `creator_id`, `created`) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UNIX_TIMESTAMP())"); + my $result = $newh -> execute($args -> {"id"}, $metadataid, $args -> {"previd"}, $args -> {"name"}, $args -> {"source"}, $args -> {"timereq"}, $args -> {"timemins"}, $args -> {"yield"}, $args -> {"temp"}, $args -> {"temptype"}, $args -> {"method"}, $args -> {"notes"}, $args -> {"typeid"}, $args -> {"statusid"}, $args -> {"creatorid"}); return $self -> self_error("Insert of recipe failed: ".$self -> {"dbh"} -> errstr) if(!$result); return $self -> self_error("No rows added when inserting recipe.") if($result eq "0E0"); @@ -145,9 +153,81 @@ sub create { $self -> {"metadata"} -> attach($metadataid) or return $self -> self_error("Error in metadata system: ".$self -> {"metadata"} -> errstr()); - # Add the ingredients for the recipe + $self -> _add_recipe_ingredients($newid, $args -> {"ingredients"}) + or return undef; + # And the tags + $self -> _add_recipe_tags($newid, $args -> {"tags"}) + or return undef; + + return $newid; +} + + +## @method $ edit(%args) +# Edit the specified recipe. This will retain edit history, so that previous +# versions of a recipe may be accessed at any time, and keep the live ID of +# the recipe the same (previous versions get moved to new IDs, then the +# updated recipe overwrites the data at the old ID). +# +# @param args This should be a reference to a hash containing the same +# elements as the args hash for create(), except previd is +# required here +# @return The recipe Id on success, undef on error. +sub edit { + my $self = shift; + my $args = hash_or_hashref(@_); + + $self -> clear_error(); + + return $self -> self_error("edit called without previous recipe ID") + unless($args -> {"previd"}); + + # Move the old recipe to the end of the table, but keep a record of its current ID + $args -> {"id"} = $args -> {"previd"}; + $args -> {"previd"} = $self -> _renumber_recipe($args -> {"previd"}); + + # Create a new one at the old ID + $self -> create($args) + or return undef; + + # Set the status of the edited recipe + $self -> set_state($args -> {"previd"}, $self -> {"settings"} -> {"config"} -> {"Recipe:status:edited"} // "edited") + or return undef; + + return $args -> {"id"}; +} + + +## @method $ set_status($recipeid, $status) +# Set the recipe status to the specified value. This will convert the provided +# status to a status ID and set that as the status if the recipe. +# +# @note The settings table may define a number of special state names, with +# the setting names 'Recipe:status:edited' and 'Recipe:status:deleted' +# +# @param recipeid The ID of the recipe to set the status for. +# @param status The status of the recipe. This should be a string, not an ID. +# @return true on success, undef on error. +sub set_status { + my $self = shift; + my $recipeid = shift; + my $status = shift; + + $self -> clear_error(); + + my $statusid = $self -> {"system"} -> {"states"} -> get_id($status) + or return $self -> self_error($self -> {"system"} -> {"states"} -> errstr()); + + my $stateh = $self -> {"dbh"} -> prepare("UPDATE `".$self -> {"settings"} -> {"database"} -> {"recipes"}."` + SET `status_id` = ? + WHERE `id` = ?"); + my $result = $stateh -> execute($statusid, $recipeid); + return $self -> self_error("Status update of recipe failed: ".$self -> {"dbh"} -> errstr) if(!$result); + return $self -> self_error("No rows modified when updating recipe state.") if($result eq "0E0"); + + return 1; } @@ -160,10 +240,15 @@ sub create { # ingredient list for the specified recipe. The ingredients are specified # as an array of hashrefs, each hash should contain the following keys: # -# - `separator`: if true, the ingredient is a separator, and `name` is set -# as the separator line title. -# - `name`: the ingredient name (or separator title if `separator` is true. -# - +# - `separator`: if true, the ingredient is a separator, `name` is set +# as the separator line title, and all the other fields +# are ignored. +# - `name`: the ingredient name (or separator title if `separator` is true. +# - `quant`: a string describing the quantity. Note that this may be +# anything from a simple numeric value to "some". +# - `units`: The units to use for the quantity. May be undef. +# - `prep`: A string describing the preparation method. +# - `notes`: Optional notes for the ingredient. # # @param recipeid The id of the recipe to add the ingredients to. # @param ingredients A reference to an array of hashes containing ingredient @@ -207,14 +292,14 @@ sub _add_recipe_ingredients { } -## @method $ add_recipe_tags($recipeid, $tags) -# Add the specified tags to a recipe, setting the provided userid as the creator for new tags. +## @method private $ _add_recipe_tags($recipeid, $tags) +# Add the specified tags to a recipe. # # @param recipeid The id of the recipe to add the tags to. # @param tags A string containing a comma-delimited list of tags, or a reference to an # array of tag names. # @return true on success, undef on error -sub add_recipe_tags { +sub _add_recipe_tags { my $self = shift; my $recipeid = shift; my $tags = shift; @@ -235,7 +320,7 @@ sub add_recipe_tags { # If $tags is a reference, it has to be an array! } elsif(ref($tags) ne "ARRAY") { - return $self -> self_error("Unsupported reference passed to add_recipe_tags(). Giving up."); + return $self -> self_error("Unsupported reference passed to _add_recipe_tags(). Giving up."); } # Now we prepare the tag insert query for action @@ -260,6 +345,84 @@ sub add_recipe_tags { } +## @method private $ _renumber_recipe($sourceid) +# Given a recipe ID, move the recipe to a new ID at the end of the recipe +# table. This will move the recipe and all relations involving it, to +# a new ID at the end of the table, leaving the source ID available for +# use by a new recipe. Note that, as the ID field of the recipe table is +# an autoincrement, reusing the ID will require explicit specification +# of the ID in the insert. +# +# @param sourceid The ID of the recipe to move. +# @return The new ID of the recipe on success, undef on error. +sub _renumber_recipe { + my $self = shift; + my $sourceid = shift; + + $self -> clear_error(); + + # Duplicate the source recipe at the end of the table + my $moveh = $self -> {"dbh"} -> prepare("INSERT INTO `".$self -> {"settings"} -> {"database"} -> {"recipes"}."` + (`metadata_id`, `prev_id`, `name`, `method`, `notes`, `source`, `yield`, `timereq`, `timemins`, `temptype`, `temp`, `type_id`, `status_id`, `creator_id`, `created`, `viewed`) + SELECT `metadata_id`, `prev_id`, `name`, `method`, `notes`, `source`, `yield`, `timereq`, `timemins`, `temptype`, `temp`, `type_id`, `status_id`, `creator_id`, `created`, `viewed` + FROM `".$self -> {"settings"} -> {"database"} -> {"recipes"}."` + WHERE `id` = ?"); + my $rows = $moveh -> execute($sourceid); + return $self -> self_error("Unable to perform recipe move: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Recipe move failed, no rows inserted") if($rows eq "0E0"); + + # Get the new ID + my $newid = $self -> {"dbh"} -> {"mysql_insertid"} + or return $self -> self_error("Unable to obtain id for new recipe"); + + $self -> _fix_recipe_relations($sourceid, $destid) + or return undef; + + # Nuke the old recipe + my $remh = $self -> {"dbh"} -> prepare("DELETE FROM `".$self -> {"settings"} -> {"database"} -> {"recipes"}."` + WHERE `id` = ?"); + $rows = $remh -> execute($sourceid); + return $self -> self_error("Unable to perform recipe move cleanup: ". $self -> {"dbh"} -> errstr) if(!$rows); + return $self -> self_error("Recipe move cleanup failed, no rows inserted") if($rows eq "0E0"); + + # Done, hand back the new ID number + return $newid; +} + + +## @method private $ _fix_recipe_relations($sourceid, $destid) +# Correct all relations to the source recipe so that they refer to the +# destination. This is used as part of the renumbering process to +# fix up any relations that use the old recipe Id to use the new one. +# +# @param sourceid The ID of the old recipe. +# @param destid The ID of the new recipe. +# @return true on success, undef on error. +sub _fix_recipe_relations { + my $self = shift; + my $sourceid = shift; + my $destid = shift; + + $self -> clear_error(); + + # Move ingredient relation IDs + $moveh = $self -> {"dbh"} -> prepare("UPDATE `".$self -> {"settings"} -> {"database"} -> {"recipeing"}."` + SET `recipe_id` = ? + WHERE `recipe_id` = ?"); + $moveh -> execute($destid, $sourceid) + or return $self -> self_error("Ingredient relation fixup failed: ".$self -> {"dbh"} -> errstr()); + + # And fix up the tag relations too + $moveh = $self -> {"dbh"} -> prepare("UPDATE `".$self -> {"settings"} -> {"database"} -> {"recipetags"}."` + SET `recipe_id` = ? + WHERE `recipe_id` = ?"); + $moveh -> execute($destid, $sourceid) + or return $self -> self_error("Ingredient relation fixup failed: ".$self -> {"dbh"} -> errstr()); + + return 1; +} + + # ============================================================================== # Metadata related @@ -287,6 +450,7 @@ sub get_recipe_metadata { return $meta -> [0]; } + ## @method private $ _create_recipe_metadata($previd) # Create a metadata context for a new recipe. This will create the new context # as a child of the metadata context for the specific previous recipe, if one