Add cloning support

This commit is contained in:
Chris 2019-09-28 11:55:07 +01:00
parent b080ce34ff
commit de15291388
5 changed files with 429 additions and 0 deletions

267
blocks/ORB/Clone.pm Normal file
View File

@ -0,0 +1,267 @@
## @file
# This file contains the implementation of the clone page.
#
# @author Chris Page <chris@starforge.co.uk>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see http://www.gnu.org/licenses/.
## @class
package ORB::Clone;
use strict;
use parent qw(ORB::Common); # This class extends the ORB common class
use experimental qw(smartmatch);
use v5.14;
use JSON;
## @method private $ _convert_tags($tags)
# Convert a list of tags into a string that can be shown in the recipe
# page.
#
# @param tags A reference to a list of tag names.
# @return A string containing the tag list.
sub _convert_tags {
my $self = shift;
my $tags = shift;
my @result;
foreach my $tag (@{$tags}) {
push(@result, $tag -> {"name"});
}
return join(",", @result);
}
## @method private void _convert_ingredients($args)
# Convert the ingredients list into a form that can be shown in the
# clone form. This fixes up some differences between the field names
# used in the result of get_recipe() and the ingredient generator.
#
# @param args A reference to the recipe data hash.
sub _convert_ingredients {
my $self = shift;
my $args = shift;
foreach my $ingred (@{$args -> {"ingredients"}}) {
if($ingred -> {"separator"}) {
$ingred -> {"name"} = $ingred -> {"separator"};
} else {
$ingred -> {"name"} = $ingred -> {"ingredient"};
}
$ingred -> {"prep"} = $ingred -> {"prepmethod"};
}
}
# ============================================================================
# UI handler/dispatcher functions
## @method $ _generate_clone($recipeid)
# Build the page containing the recipe clone form.
#
# @return An array containing the page title, content, extra header data, and
# extra javascript content.
sub _generate_clone {
my $self = shift;
my $recipeid = shift;
my ($args, $errors);
# Recipe ID must be purely numeric for clones
return $self -> _fatal_error("{L_EDIT_FAILED_BADID}")
unless($recipeid =~ /^\d+$/);
# Try to fetch the data.
$args = $self -> {"system"} -> {"recipe"} -> get_recipe($recipeid);
return $self -> _fatal_error("{L_EDIT_FAILED_NOTFOUND}")
unless($args -> {"id"});
$args -> {"tags"} = $self -> _convert_tags($args -> {"tags"});
$self -> _convert_ingredients($args);
# User must have recipe edit to proceed.
return $self -> _fatal_error("{L_PERMISSION_FAILED_SUMMARY}")
unless($self -> check_permission('recipe.edit', $args -> {"metadata_id"}));
if($self -> {"cgi"} -> param("clonerecipe")) {
$self -> log("recipe.clone", "User has submitted clone for recipe $recipeid");
$args = {};
# Do all the validation, and if there's no errors then add the recipe
($args, $errors) = $self -> _validate_recipe();
if(!$errors) {
# No errors, try adding the recipe
$args -> {"creatorid"} = $self -> {"session"} -> get_session_userid();
$args -> {"id"} = $self -> {"system"} -> {"recipe"} -> create($args)
or $errors = $self -> {"template"} -> load_template("error/error_item.tem",
{ "%(error)s" => $self -> {"system"} -> {"recipe"} -> errstr() });
# Did the clone work? If so, send the user to the view page for the new recipe
return $self -> redirect($self -> build_url(block => "view",
pathinfo => [ $args -> {"id"} ],
params => "",
api => []))
if(!$errors);
}
}
# Wrap the errors if there are any
if($errors) {
$self -> log("recipe.clone", "Errors detected in clone: $errors");
my $errorlist = $self -> {"template"} -> load_template("error/error_list.tem", {"%(message)s" => "{L_CLONE_ERRORS}",
"%(errors)s" => $errors });
$errors = $self -> {"template"} -> load_template("error/page_error.tem", { "%(message)s" => $errorlist });
}
# Prebuild arrays for temptypes, units, and prep methods
my $temptypes = $self -> _build_temptypes();
my $units = $self -> _get_units();
my $preps = $self -> _get_prepmethods();
# And convert them to optionlists for the later template call
my $unitopts = $self -> {"template"} -> build_optionlist($units);
my $prepopts = $self -> {"template"} -> build_optionlist($preps);
# Build the list of ingredients
my $ingredients = $self -> _build_ingredients($args);
# Build up the type and status data
my $typeopts = $self -> {"template"} -> build_optionlist($self -> {"system"} -> {"entities"} -> {"types"} -> as_options(1),
$args -> {"type"});
my $statusopts = $self -> {"template"} -> build_optionlist($self -> {"system"} -> {"entities"} -> {"states"} -> as_options(1, visible => {value => 1}),
$args -> {"status"});
# Convert the time fields
my ($preptime, $prepsecs) = ("", 0);
if($args -> {"preptime"}) {
$prepsecs = $args -> {"preptime"} * 60;
$preptime = $self -> _build_timereq($prepsecs);
}
my ($cooktime, $cooksecs) = ("", 0);
if($args -> {"cooktime"}) {
$cooksecs = $args -> {"cooktime"} * 60;
$cooktime = $self -> _build_timereq($cooksecs);
}
# Convert tags - can't use build_optionlist because all of them need to be selected.
my $taglist = "";
if($args -> {"tags"}) {
my @tags = split(/,/, $args -> {"tags"});
foreach my $tag (@tags) {
$taglist .= "<option selected=\"selected\">$tag</option>\n";
}
}
# And squirt out the page content
my $body = $self -> {"template"} -> load_template("clone/content.tem",
{
"%(errors)s" => $errors,
"%(name)s" => $args -> {"name"} // "",
"%(source)s" => $args -> {"source"} // "",
"%(yield)s" => $args -> {"yield"} // "",
"%(prepinfo)s" => $args -> {"prepinfo"} // "",
"%(preptime)s" => $preptime,
"%(prepsecs)s" => $prepsecs,
"%(cooktime)s" => $cooktime,
"%(cooksecs)s" => $cooksecs,
"%(temp)s" => $args -> {"temp"} // "",
"%(temptypes)s" => $self -> {"template"} -> build_optionlist($temptypes, $args -> {"temptype"}),
"%(types)s" => $typeopts,
"%(units)s" => $unitopts,
"%(preps)s" => $prepopts,
"%(status)s" => $statusopts,
"%(tags)s" => $taglist,
"%(ingreds)s" => $ingredients,
"%(method)s" => $args -> {"method"} // "",
"%(notes)s" => $args -> {"notes"} // "",
});
return ($self -> {"template"} -> replace_langvar("CLONE_TITLE"),
$body,
$self -> {"template"} -> load_template("clone/extrahead.tem"),
$self -> {"template"} -> load_template("clone/extrajs.tem"));
}
## @method private @ _fatal_error($error)
# Generate the tile and content for an error page.
#
# @param error A string containing the error message to display
# @return The title of the error page and an error message to place in the page.
sub _fatal_error {
my $self = shift;
my $error = shift;
return ("{L_VIEW_ERROR_FATAL}",
$self -> {"template"} -> load_template("error/page_error.tem",
{ "%(message)s" => $error,
"%(url-logout)s" => $self -> build_url(block => "login", pathinfo => ["signout"])
})
);
}
## @method private $ _dispatch_ui()
# Implements the core behaviour dispatcher for non-api functions. This will
# inspect the state of the pathinfo and invoke the appropriate handler
# function to generate content for the user.
#
# @return A string containing the page HTML.
sub _dispatch_ui {
my $self = shift;
my @pathinfo = $self -> {"cgi"} -> multi_param("pathinfo");
my ($title, $body, $extrahead, $extrajs) = $self -> _generate_clone($pathinfo[0]);
# Done generating the page content, return the filled in page template
return $self -> generate_orb_page(title => $title,
content => $body,
extrahead => $extrahead,
extrajs => $extrajs,
active => '-',
doclink => 'clone');
}
# ============================================================================
# Module interface functions
## @method $ page_display()
# Generate the page content for this module.
sub page_display {
my $self = shift;
# Is this an API call, or a normal page operation?
my $apiop = $self -> is_api_operation();
if(defined($apiop)) {
# API call - dispatch to appropriate handler.
given($apiop) {
default {
return $self -> api_response($self -> api_errorhash('bad_op',
$self -> {"template"} -> replace_langvar("API_BAD_OP")))
}
}
} else {
return $self -> _dispatch_ui();
}
}
1;

View File

@ -6,6 +6,10 @@ EDIT_TITLE = Edit Recipe
EDIT_EDIT = Edit Recipe EDIT_EDIT = Edit Recipe
EDIT_ERRORS = Unable to edit recipe; the following errors have been encountered: EDIT_ERRORS = Unable to edit recipe; the following errors have been encountered:
CLONE_TITLE = Clone Recipe
CLONE_CLONE = Clone Recipe
CLONE_ERRORS = Unable to clone recipe; the following errors have been encountered:
RECIPE_NAME = Name RECIPE_NAME = Name
RECIPE_NAME_DOC = The name of the recipe RECIPE_NAME_DOC = The name of the recipe
RECIPE_NAME_PH = Recipe name RECIPE_NAME_PH = Recipe name

View File

@ -0,0 +1,149 @@
%(errors)s
<div class="small-8 small-offset-2 cell">
<form class="nomargin" method="POST" id="recipeform">
<h4 class="underscore">{L_EDIT_TITLE}</h4>
<div>
<label>{L_RECIPE_NAME}
<input maxlength="80" data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_RECIPE_NAME_DOC}" type="text" id="name" name="name" value="%(name)s" placeholder="{L_RECIPE_NAME_PH}" required />
</label>
</div>
<div>
<label>{L_RECIPE_SOURCE}
<input maxlength="255" data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_RECIPE_SOURCE_DOC}" type="text" id="source" name="source" value="%(source)s" placeholder="{L_RECIPE_SOURCE_PH}" />
</label>
</div>
<div>
<label>{L_RECIPE_YIELD}
<input maxlength="80" data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_RECIPE_YIELD_DOC}" type="text" id="yeild" name="yield" value="%(yield)s" placeholder="{L_RECIPE_YIELD_PH}" />
</label>
</div>
<div>
<label>{L_RECIPE_PREPINFO}
<input maxlength="255" data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_RECIPE_PREPINFO_DOC}" type="text" id="prepinfo" name="prepinfo" value="%(prepinfo)s" placeholder="{L_RECIPE_PREPINFO_PH}" required />
</label>
</div>
<div>
<label>{L_RECIPE_PREPTIME}
<input data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_RECIPE_PREPTIME_DOC}" type="text" id="preptime" name="preptime" value="%(preptime)s" placeholder="{L_RECIPE_PREPTIME_PH}" autocomplete="off" />
<input type="hidden" name="prepsecs" id="prepsecs" value="%(prepsecs)s" />
</label>
</div>
<div>
<label>{L_RECIPE_COOKTIME}
<input data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_RECIPE_COOKTIME_DOC}" type="text" id="cooktime" name="cooktime" value="%(cooktime)s" placeholder="{L_RECIPE_COOKTIME_PH}" autocomplete="off" />
<input type="hidden" name="cooksecs" id="cooksecs" value="%(cooksecs)s" />
</label>
</div>
<div>
<label>{L_RECIPE_OVENTEMP}
<div class="grid-x">
<div class="small-6 cell">
<input data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_RECIPE_OVENTEMP_DOC}" type="number" id="temp" name="temp" value="%(temp)s" placeholder="{L_RECIPE_OVENTEMP_PH}" />
</div>
<div class="small-6 cell">
<select id="temptype" name="temptype">
%(temptypes)s
</select>
</div>
</div>
</label>
</div>
<div class="grid-x">
<div class="medium-6 cell form-left">
<label>{L_RECIPE_TYPE}
<select id="type" name="type">
%(types)s
</select>
</label>
</div>
<div class="medium-6 cell form-right">
<label>{L_RECIPE_STATUS}
<select id="status" name="status">
%(status)s
</select>
</label>
</div>
</div>
<div class="spacer">
<label>{L_RECIPE_TAGS}
<select id="tags" name="tags" size="1" multiple="multiple">
%(tags)s
</select>
</label>
</div>
<ul id="ingredients">
%(ingreds)s
</ul>
<input type="hidden" id="ingdata" name="ingdata" />
<div class="button-group">
<button type="button" class="button" id="addsep" >{L_RECIPE_ADD_SEP}</button>
<button type="button" class="button adding" data-count="1">{L_RECIPE_ADD_INGRED}</button>
<a class="dropdown button arrow-only" data-toggle="count-dropdown">
<span class="show-for-sr">Show menu</span>
</a>
<div class="dropdown-pane bottom float-left" id="count-dropdown" data-dropdown data-auto-focus="true">
<ul class="menu vertical">
<li><button type="button" class="adding" data-count="5" data-toggle="count-dropdown">{L_RECIPE_ADD_INGRED5}</button></li>
<li><button type="button" class="adding" data-count="10" data-toggle="count-dropdown">{L_RECIPE_ADD_INGRED10}</button></li>
</ul>
</div>
</div>
<div class="spacer">
<label>{L_RECIPE_METHOD}
<textarea id="method" name="method">
%(method)s
</textarea>
</label>
</div>
<div class="spacer">
<label>{L_RECIPE_NOTES}
<textarea id="notes" name="notes">
%(notes)s
</textarea>
</label>
</div>
<div class="clearfix">
<input type="submit" name="clonerecipe" class="button float-right" value="{L_CLONE_CLONE}" />
</div>
</form>
</div>
<ul class="hide" id="templates">
<li class="ingred">
<div class="grid-x">
<div class="small-1 cell">
<input class="quantity" type="text" placeholder="{L_RECIPE_ING_QUANT_PH}" value="%(quantity)s" />
</div>
<div class="small-2 cell">
<select class="units">
%(units)s
</select>
</div>
<div class="small-2 cell">
<select class="preps">
%(preps)s
</select>
</div>
<div class="small-3 cell">
<input type="text" class="ingredient" pattern="[-\w,.:()\x26;#*\\ ]+" title="{L_RECIPE_ING_FORMAT}" placeholder="{L_RECIPE_ING_ING_PH}" />
</div>
<div class="small-3 cell">
<input type="text" class="notes" placeholder="{L_RECIPE_ING_NOTE_PH}" />
</div>
<div class="small-1 cell">
<button class="button alert deletectrl" type="button" title="{L_RECIPE_ING_DELETE}"><i class="fa fa-trash" aria-hidden="true"></i></button>
</div>
</div>
</li>
<li class="separator">
<div class="grid-x">
<div class="small-11 cell">
<input type="text" class="separator" pattern="[-\w,.:()\x26;#*\\ ]+" title="{L_RECIPE_ING_FORMAT}" placeholder="Separator text">
</div>
<div class="small-1 cell">
<button class="button alert deletectrl" type="button" title="Delete"><i class="fa fa-trash" aria-hidden="true"></i></button>
</div>
</div>
</li>
</ul>

View File

@ -0,0 +1 @@
<link rel="stylesheet" href="{V_[csspath]}new.css" />

View File

@ -0,0 +1,8 @@
</script>
<script src="{V_[templatepath]}3rdparty/timepicker/jquery-time-duration-picker.js"></script>
<script src="{V_[templatepath]}3rdparty/ckeditor/ckeditor.js"></script>
<script src="{V_[jspath]}new.js"></script>
<script>
api = { ingredients: '{V_[scriptpath]}rest/api/ingredients',
tags: '{V_[scriptpath]}rest/api/tags',
};