Initial work on New page

This commit is contained in:
Chris 2018-06-30 12:51:52 +01:00
parent 0b2e7b691d
commit 49387912c2
13 changed files with 625 additions and 6 deletions

279
blocks/ORB/New.pm Normal file
View File

@ -0,0 +1,279 @@
## @file
# This file contains the implementation of the new 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::New;
use strict;
use parent qw(ORB); # This class extends the ORB block class
use experimental qw(smartmatch);
use v5.14;
# How many ingredient rows should appear in the empty form?
use constant DEFAULT_INGREDIENT_COUNT => 5;
## @method private $ _build_timereq($seconds)
# Given a time requirement in seconds, generate a string representing
# the time required in the form "X days, Y hours and Z minues",
# optionally dropping parts of the string depending on whether
# X, Y, or Z are zero.
#
# @param seconds The number of seconds required to make the recipe.
# @return A string representing the seconds.
sub _build_timereq {
my $self = shift;
my $seconds = shift;
my $days = int($seconds / (24 * 60 * 60));
my $hours = ($seconds / (60 * 60)) % 24;
my $mins = ($seconds / 60) % 60;
# localisation needed...
my @parts = ();
push(@parts, "$days days") if($days);
push(@parts, "$hours hours") if($hours);
push(@parts, "$mins minutes") if($mins);
my $count = scalar(@parts);
if($count == 3) {
return $parts[0].", ".$parts[1]." and ".$parts[2];
} elsif($count == 2) {
return $parts[0]." and ".$parts[1];
} elsif($count == 1) {
return $parts[0];
}
return "";
}
sub _build_temptypes {
my $self = shift;
my $default = shift;
# Supported types are in the column enum list
my $tempenum = $self -> get_enum_values($self -> {"settings"} -> {"database"} -> {"recipes"}, "temptype");
return $tempenum
unless(ref($tempenum) eq "ARRAY");
# convert to something build_optionlist will understand
map { $_ = { name => $_, value => $_ } } @{$tempenum};
return $self -> {"template"} -> build_optionlist($tempenum, $default);
}
sub _get_units {
my $self = shift;
return $self -> {"units"}
if($self -> {"units"});
$self -> {"units"} = [
{ value => "None", name => "None" },
@{ $self -> {"system"} -> {"entities"} -> {"units"} -> as_options(1) }
];
return $self -> {"units"};
}
sub _build_ingredients {
my $self = shift;
my $args = shift;
my $units = shift;
my $preps = shift;
my @ingreds = ();
# If any ingredients are present in the argument list, push them into templated strings
if($args -> {"ingredients"} && scalar(@{$args -> {"ingredients"}})) {
foreach my $ingred (@{$args -> {"ingredients"}}) {
# Ensure we never try to deal with undef elements in the array
next unless($ingred);
# Which template to use depends on whether this is a separator
my $template = $ingred -> {"separator"} ? "new/separator.tem" : "new/ingredient.tem";
my $unitopts = $self -> {"template"} -> build_optionlist($units, $args -> {"units"});
my $prepopts = $self -> {"template"} -> build_optionlist($preps, $args -> {"prep"});
push(@ingreds,
$self -> {"template"} -> load_template($template,
{ "%(quantity)s" => $ingred -> {"quantity"},
"%(name)s" => $ingred -> {"name"},
"%(notes)s" => $ingred -> {"notes"},
"%(units)s" => $unitopts,
"%(preps)s" => $prepopts,
}));
}
# if the ingredient list is empty, generate some empties
} else {
# Only need to calculate these once for the empty ingredients
my $unitopts = $self -> {"template"} -> build_optionlist($units);
my $prepopts = $self -> {"template"} -> build_optionlist($preps);
for(my $i = 0; $i < DEFAULT_INGREDIENT_COUNT; ++$i) {
push(@ingreds,
$self -> {"template"} -> load_template("new/ingredient.tem",
{ "%(quantity)s" => "",
"%(name)s" => "",
"%(notes)s" => "",
"%(units)s" => $unitopts,
"%(preps)s" => $prepopts,
}));
}
}
return join("", @ingreds);
}
sub _generate_new {
my $self = shift;
my ($args, $errors);
if($errors) {
$self -> log("new", "Errors detected in addition: $errors");
my $errorlist = $self -> {"template"} -> load_template("error/error_list.tem", {"%(message)s" => "{L_NEW_ERRORS}",
"%(errors)s" => $errors });
$errors = $self -> {"template"} -> load_template("error/page_error.tem", { "%(message)s" => $errorlist });
}
# Prebuild arrays for units and prep methods
my $units = $self -> _get_units();
my $preps = $self -> {"system"} -> {"entities"} -> {"prep"} -> as_options(1);
# 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, $units, $preps);
# Build up the type and status data
my $typeopts = $self -> {"template"} -> build_optionlist($self -> {"system"} -> {"entities"} -> {"types"} -> as_options(),
$args -> {"type"});
my $statusopts = $self -> {"template"} -> build_optionlist($self -> {"system"} -> {"entities"} -> {"states"} -> as_options(0, visible => {value => 1}),
$args -> {"status"});
# Convert the time fields
my ($timemins, $timesecs) = ("", 0);
if($args -> {"timemins"}) {
$timesecs = $args -> {"timemins"} * 60;
$timemins = $self -> _build_timereq($timesecs);
}
# And squirt out the page content
my $body = $self -> {"template"} -> load_template("new/content.tem",
{
"%(errors)s" => $errors,
"%(name)s" => $args -> {"name"} // "",
"%(source)s" => $args -> {"source"} // "",
"%(yield)s" => $args -> {"yield"} // "",
"%(timereq)s" => $args -> {"timereq"} // "",
"%(timemins)s" => $timemins,
"%(timesecs)s" => $timesecs,
"%(temp)s" => $args -> {"temp"} // "",
"%(temptypes)s" => $self -> _build_temptypes($args -> {"temptype"}),
"%(types)s" => $typeopts,
"%(units)s" => $unitopts,
"%(preps)s" => $prepopts,
"%(status)s" => $statusopts,
"%(ingreds)s" => $ingredients,
"%(method)s" => $args -> {"method"} // "",
"%(notes)s" => $args -> {"notes"} // "",
});
return ($self -> {"template"} -> replace_langvar("NEW_TITLE"),
$body,
$self -> {"template"} -> load_template("new/extrahead.tem"),
$self -> {"template"} -> load_template("new/extrajs.tem"));
}
# ============================================================================
# UI handler/dispatcher functions
## @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 ($title, $body, $extrahead, $extrajs) = $self -> _generate_new();
# 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 => 'summary');
}
# ============================================================================
# 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;

47
lang/en/new.lang Normal file
View File

@ -0,0 +1,47 @@
NEW_TITLE = Create Recipe
NEW_NAME = Name
NEW_NAME_DOC = The name of the recipe
NEW_NAME_PH = Recipe name
NEW_SOURCE = Source
NEW_SOURCE_DOC = Information about the source this recipe was based on
NEW_SOURCE_PH = http://source.url
NEW_YIELD = Yield
NEW_YIELD_DOC = How many servings does this recipe make?
NEW_YIELD_PH = X servings
NEW_PREPINFO = Prep info
NEW_PREPINFO_DOC = How much time each step of the recipe take?
NEW_PREPINFO_PH = 10 min prep + 20 min cook
NEW_TIMEREQ = Time required
NEW_TIMEREQ_DOC = How long does this recipe take in total?
NEW_TIMEREQ_PH = 1 hour 10 minutes
NEW_OVENTEMP = Oven preheat
NEW_OVENTEMP_DOC = Initial oven temperature (show changes in method)
NEW_OVENTEMP_PH = None
NEW_TYPE = Type
NEW_STATUS = Status
NEW_TAGS = Tags
NEW_ADD_SEP = Add Separator
NEW_ADD_INGRED = Add Ingredient
NEW_ADD_INGRED5 = Add 5 Ingredients
NEW_ADD_INGRED10 = Add 10 Ingredients
NEW_INGREDIENTS = Ingredients
NEW_ING_QUANT_PH = Quantity
NEW_ING_ING_PH = Ingredient
NEW_ING_NOTE_PH = Notes
NEW_ING_SEP_PH = Separator text
NEW_ING_DELETE = Delete
NEW_METHOD = Method
NEW_NOTES = Notes
NEW_CREATE = Add Recipe

View File

@ -16,3 +16,5 @@ BLOCK_VALIDATE_RANGEMAX = The value provided for '***field***' is out of range
BLOCK_ERROR_TITLE = Fatal System Error
BLOCK_ERROR_SUMMARY = The system has encountered an unrecoverable error.
BLOCK_ERROR_TEXT = A serious error has been encountered while processing your request. The following information was generated by the system, please contact moodlesupport@cs.man.ac.uk about this, including this error and a description of what you were doing when it happened!<br /><br /><span class="error">***error***</span>
BLOCK_ERROR_BADENUM = Unable to fetch enum values from ***table***.***col***: ***errstr***

View File

@ -0,0 +1,27 @@
#ingredients {
margin: 0px;
padding: 0px;
}
#ingredients li {
list-style: none;
margin-bottom: 0.5rem;
padding-left: 1rem;
background-repeat: no-repeat;
background-image: url('../images/draghandle.png');
background-position: left center;
}
#ingredients li input,
#ingredients li select,
#ingredients li button
{
margin: 0px;
height: auto;
}
#ingredients .ui-state-highlight {
height: calc(2.75rem - 2px);
background-image: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

View File

@ -0,0 +1,74 @@
function add_separator()
{
var $new = $('#templates li.separator').clone(true);
$new.hide().appendTo($('#ingredients')).fadeIn(300);
}
function add_ingredient(count)
{
for(i = 0; i < count; ++i) {
var $new = $('#templates li.ingred').clone(true);
$new.hide().appendTo($('#ingredients')).fadeIn(300);
$new.find(".ingredient").autocomplete({
source: api.ingredients,
minLength: 2
});
}
}
$(function() {
$('#timemins').timeDurationPicker({
lang: 'en_US',
seconds: false,
minutes: true,
hours: true,
days: true,
months: false,
years: false,
onSelect: function(element, seconds, humanDuration) {
$('#timemins').val(humanDuration);
$('#timesecs').val(seconds);
console.log(seconds, humanDuration);
}
});
$('#tags').select2({
theme: "foundation",
tags: true,
tokenSeparators: [','],
minimumInputLength: 2,
multiple: true,
ajax: {
delay: 250,
dataType: 'json',
url: api.tags
}
});
CKEDITOR.replace('method');
CKEDITOR.replace('notes');
$('#ingredients').sortable({
placeholder: "ui-state-highlight"
});
$("#ingredients .ingredient").autocomplete({
source: api.ingredients,
minLength: 2
});
// Handle addition of separators and ingredients
$('#addsep').on('click', function() { add_separator(); });
$('.adding').on('click', function() { add_ingredient($(this).data('count')); });
// Handle removal of separators and ingredients
$('.deletectrl').on('click', function() {
$(this).parents('li').fadeOut(300, function() { $(this).remove(); });
});
});

View File

@ -0,0 +1,142 @@
%(errors)s
<div class="small-8 small-offset-2 cell">
<form class="nomargin" method="POST" id="newgrade">
<h4 class="underscore">{L_NEW_TITLE}</h4>
<div>
<label>{L_NEW_NAME}
<input maxlength="80" data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_NEW_NAME_DOC}" type="text" id="name" name="name" value="%(name)s" placeholder="{L_NEW_NAME_PH}" required />
</label>
</div>
<div>
<label>{L_NEW_SOURCE}
<input maxlength="255" data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_NEW_SOURCE_DOC}" type="text" id="source" name="source" value="%(source)s" placeholder="{L_NEW_SOURCE_PH}" required />
</label>
</div>
<div>
<label>{L_NEW_YIELD}
<input maxlength="80" data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_NEW_YIELD_DOC}" type="text" id="yeild" name="yield" value="%(yield)s" placeholder="{L_NEW_YIELD_PH}" />
</label>
</div>
<div>
<label>{L_NEW_PREPINFO}
<input maxlength="255" data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_NEW_PREPINFO_DOC}" type="text" id="timereq" name="timereq" value="%(timereq)s" placeholder="{L_NEW_PREPINFO_PH}" required />
</label>
</div>
<div>
<label>{L_NEW_TIMEREQ}
<input data-tooltip aria-haspopup="true" class="has-tip top" data-disable-hover="false" title="{L_NEW_TIMEREQ_DOC}" type="text" id="timemins" name="timemins" value="%(timemins)s" placeholder="{L_NEW_TIMEREQ_PH}" />
<input type="hidden" name="timesecs" value="0" id="timesecs" value="%(timesecs)s" />
</label>
</div>
<div>
<label>{L_NEW_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_NEW_OVENTEMP_DOC}" type="number" id="temp" name="temp" value="%(temp)s" placeholder="{L_NEW_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_NEW_TYPE}
<select id="type" name="type">
%(types)s
</select>
</label>
</div>
<div class="medium-6 cell form-right">
<label>{L_NEW_STATUS}
<select id="status" name="status">
%(status)s
</select>
</label>
</div>
</div>
<div class="spacer">
<label>{L_NEW_TAGS}
<select id="tags" name="tags" size="1" multiple="multiple">
%(tags)s
</select>
</label>
</div>
<ul id="ingredients">
%(ingreds)s
</ul>
<div class="button-group">
<button type="button" class="button" id="addsep" >{L_NEW_ADD_SEP}</button>
<button type="button" class="button adding" data-count="1">{L_NEW_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_NEW_ADD_INGRED5}</button></li>
<li><button type="button" class="adding" data-count="10" data-toggle="count-dropdown">{L_NEW_ADD_INGRED10}</button></li>
</ul>
</div>
</div>
<div class="spacer">
<label>{L_NEW_METHOD}
<textarea id="method" name="method">
%(method)s
</textarea>
</label>
</div>
<div class="spacer">
<label>{L_NEW_NOTES}
<textarea id="notes" name="notes">
%(notes)s
</textarea>
</label>
</div>
<div class="clearfix">
<input type="submit" name="newgrade" class="button float-right" value="{L_NEW_CREATE}" />
</div>
</form>
</div>
<ul class="hide" id="templates">
<li class="ingred">
<div class="grid-x">
<div class="small-1 cell">
<input type="text" placeholder="Quantity">
</div>
<div class="small-2 cell">
<select>
%(units)s
</select>
</div>
<div class="small-2 cell">
<select>
%(preps)s
</select>
</div>
<div class="small-3 cell">
<input type="text" class="ingredient" placeholder="Ingredient">
</div>
<div class="small-3 cell">
<input type="text" placeholder="Notes">
</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>
<li class="separator">
<div class="grid-x">
<div class="small-11 cell">
<input type="text" 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',
};

View File

@ -0,0 +1,26 @@
<li>
<div class="grid-x">
<div class="small-1 cell">
<input type="text" placeholder="{L_NEW_ING_QUANT_PH}" value="%(quantity)s" />
</div>
<div class="small-2 cell">
<select>
%(units)s
</select>
</div>
<div class="small-2 cell">
<select>
%(preps)s
</select>
</div>
<div class="small-3 cell">
<input type="text" class="ingredient" placeholder="{L_NEW_ING_ING_PH}" value="%(name)s" />
</div>
<div class="small-3 cell">
<input type="text" placeholder="{L_NEW_ING_NOTE_PH}" value="%(notes)s" />
</div>
<div class="small-1 cell">
<button class="button alert deletectrl" type="button" title="{L_NEW_ING_DELETE}"><i class="fa fa-trash" aria-hidden="true"></i></button>
</div>
</div>
</li>

View File

@ -0,0 +1,10 @@
<li class="separator">
<div class="grid-x">
<div class="small-11 cell">
<input type="text" placeholder="{L_NEW_ING_SEP_PH}" value="%(name)s" />
</div>
<div class="small-1 cell">
<button class="button alert deletectrl" type="button" title="{L_NEW_ING_DELETE}"><i class="fa fa-trash" aria-hidden="true"></i></button>
</div>
</div>
</li>

View File

@ -6,6 +6,9 @@
<title>%(title)s</title>
<link rel="stylesheet" href="{V_[templatepath]}3rdparty/jquery-ui/jquery-ui.min.css" />
<link rel="stylesheet" href="{V_[templatepath]}3rdparty/foundation/css/foundation.min.css" />
<link rel="stylesheet" href="{V_[templatepath]}3rdparty/fontawesome/css/fontawesome-all.css" />
<link rel="stylesheet" href="{V_[templatepath]}3rdparty/select2/css/select2.min.css" />
<link rel="stylesheet" href="{V_[templatepath]}3rdparty/select2-foundation/select2-foundation-theme.min.css" />
<link rel="stylesheet" href="{V_[csspath]}notebox.css" />
<link rel="stylesheet" href="{V_[csspath]}messagebox.css" />
@ -22,9 +25,9 @@
<!-- Start top bar -->
%(userbar)s
<!-- End top bar -->
<!-- Start content -->
<div class="grid-container fluid">
%(pagemenu)s
<!-- Start content -->
<div class="grid-x grid-margin-x" id="content">
%(content)s
</div>
@ -35,9 +38,9 @@
<!-- Foundation and other javascript shenanigans -->
<script src="{V_[templatepath]}3rdparty/foundation/js/vendor/jquery.js"></script>
<script src="{V_[templatepath]}3rdparty/jquery-ui/jquery-ui.min.js"></script>
<script src="{V_[templatepath]}3rdparty/jquery-ui/jquery-ui.js"></script>
<script src="{V_[templatepath]}3rdparty/foundation/js/vendor/foundation.min.js"></script>
<script src="{V_[templatepath]}3rdparty/fontawesome/js/fontawesome-all.js"></script>
<script src="{V_[templatepath]}3rdparty/select2/js/select2.full.min.js"></script>
<script src="{V_[jspath]}foundation.js"></script>
<script src="{V_[jspath]}clear-input.js"></script>
<script>