Ensure that recipe IDs do not change between edits

This commit is contained in:
Chris 2016-09-20 16:55:37 +01:00
parent 51e9e1e2b3
commit d91f9d45de

View File

@ -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