Skip to content

Honor date-only dates using the date picker (CFM-107) #49

Merged
merged 9 commits into from
Sep 13, 2022
9 changes: 9 additions & 0 deletions app/resources/locales/en_US/field.po
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ msgstr "Created"
msgid "datepicker.am"
msgstr "AM"

msgid "datepicker.chooseTime"
msgstr "Choose time"

msgid "datepicker.enterDate"
msgstr "Enter a date as YYYY-MM-DD"

msgid "datepicker.enterDateTime"
msgstr "Enter a date as YYYY-MM-DD HH:MM:SS"

msgid "datepicker.hour"
msgstr "Hour"

Expand Down
37 changes: 37 additions & 0 deletions app/src/Lib/Enum/DateTypeEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* COmanage Registry Date Type Enum, Standard Variant
*
* Portions licensed to the University Corporation for Advanced Internet
* Development, Inc. ("UCAID") under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* UCAID licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @link https://www.internet2.edu/comanage COmanage Project
* @package registry
* @since COmanage Registry v5.0.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/

declare(strict_types = 1);

namespace App\Lib\Enum;

class DateTypeEnum extends StandardEnum {
const Standard = 'standard';
const DateOnly = 'dateonly';
const FromTime = 'fromtime';
const ThroughTime = 'throughtime';
}
158 changes: 109 additions & 49 deletions app/src/View/Helper/FieldHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@

use \Cake\Utility\Inflector;
use Cake\View\Helper;
use App\Lib\Enum\DateTypeEnum;
use Cake\I18n\FrozenTime;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit, sort the App stuff together after the Cake stuff.

Copy link
Contributor Author

@arlen arlen Sep 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - I also removed a preceding "\" on one of the Cake imports for consistency.


class FieldHelper extends Helper {
public $helpers = ['Form', 'Html', 'Url', 'Alert'];
Expand Down Expand Up @@ -66,25 +68,29 @@ public function banner(string $info) {
* Emit a form control.
*
* @since COmanage Registry v5.0.0
* @param string $fieldName Form field
* @param array $options FormHelper control options
* @param string $labelText Label text (fieldName language key used by default)
* @param array $config Custom FormHelper configuration options
* @param string $fieldName Form field
* @param array $options FormHelper control options
* @param string $labelText Label text (fieldName language key used by default)
* @param array $config Custom FormHelper configuration options
* @param string $ctrlCode Control code passed in from wrapper functions
* @param string $cssClass Start li css class passed in from wrapper functions
* @return string HTML for control
*/

public function control(string $fieldName,
array $options=[],
string $labelText=null,
array $config=[]){
array $config=[],
string $ctrlCode=null,
string $cssClass=''){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a return type in the function signature, eg ): string {.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - and also for consistency I carried this convention to every function in the FieldHelper.php file.

$coptions = $options;
$coptions['label'] = false;
$coptions['readonly'] = !$this->editable || (isset($options['readonly']) && $options['readonly']);
// Selects, Checkboxes, and Radio Buttons use "disabled"
$coptions['disabled'] = $coptions['readonly'];

// Generate HTML for the control itself
$liClass = "";
// Specify a class on the <li> form control wrapper
$liClass = $cssClass;

// Remove prefix from field value
if(isset($config['prefix'], $this->getView()->get('vv_obj')->$fieldName)) {
Expand All @@ -94,50 +100,20 @@ public function control(string $fieldName,
$vv_obj->$fieldName = $fieldValueTemp;
$this->getView()->set('vv_obj', $vv_obj);
}

// Handle datetime controls specially
if($fieldName == 'valid_from' || $fieldName == 'valid_through') {
// Append the timezone to the label
$label = __d('field', $fieldName.".tz", [$this->_View->get('vv_tz')]);

// A datetime field will be rendered as plain text input with adjacent date and time pickers
// that will interact with the field value. Allowing direct access to the input field is for
// accessibility purposes.
$coptions['class'] = 'form-control datepicker';
$coptions['placeholder'] = 'YYYY-MM-DD HH:MM:SS'; // TODO: test for date-only inputs and send only the date
$coptions['id'] = str_replace("_", "-", $fieldName);

$entity = $this->getView()->get('vv_obj');

$pickerDate = '';
if(!empty($entity->$fieldName)) {
// Adjust the time back to the user's timezone
$coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->getView()->get('vv_tz'));
$pickerDate = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->getView()->get('vv_tz'));
}

$date_args = [
'fieldName' => $fieldName,
'pickerDate' => $pickerDate
];
// Create a text field to hold our value.
$controlCode = $this->Form->text($fieldName, $coptions)
. $this->getView()->element('datePicker', $date_args);

$liClass = "fields-datepicker";
} else {
if($fieldName != 'status'
&& !isset($options['empty'])
&& (!isset($options['suppressBlank']) || !$options['suppressBlank'])) {
// Cause any select (except status) to render with a blank option, even
// if the field is required. This makes it clear when a value need to be set.
// Note this will be ignored for non-select controls.
$coptions['empty'] = true;
}

$controlCode = $this->Form->control($fieldName, $coptions);

if($fieldName != 'status'
&& !isset($options['empty'])
&& (!isset($options['suppressBlank']) || !$options['suppressBlank'])) {
// Cause any select (except status) to render with a blank option, even
// if the field is required. This makes it clear when a value need to be set.
// Note this will be ignored for non-select controls.
$coptions['empty'] = true;
}

// Generate the form control or pass along the markup generated in a wrapper function
$controlCode = empty($ctrlCode) ? $this->Form->control($fieldName, $coptions) : $ctrlCode;


// Required fields are usually determined by the model validator, but for
// related models the view (currently) has to pass the field as required in
// $options. For fields of the form model.0.field, if $options['required']
Expand All @@ -159,6 +135,90 @@ public function control(string $fieldName,
. $this->endLine();
}

/**
* Emit a date/time form control.
* This is a wrapper function for $this->control()
*
* @since COmanage Registry v5.0.0
* @param string $fieldName Form field
* @param string $dateType Standard, DateOnly, FromTime, ThroughTime
*
* @return string HTML for control
*/

public function dateControl(string $fieldName, string $dateType=DateTypeEnum::Standard) {
// A datetime field will be rendered as a plain text input with adjacent date and time pickers
// that will interact with the field value. Allowing direct access to the input field is for
// accessibility purposes.

$pickerType = $dateType;
// Special-case the very common "valid_from" and "valid_through" fields so we won't need
// to specify their types in fields.inc.
if($fieldName == 'valid_from') {
$pickerType = DateTypeEnum::FromTime;
}
if($fieldName == 'valid_through') {
$pickerType = DateTypeEnum::ThroughTime;
}

// Append the timezone to the label -- TODO: see that the timezone gets output to the display
$label = __d('field', $fieldName.".tz", [$this->_View->get('vv_tz')]);

// Create the options array for the (text input) form control
$coptions = [];
$coptions['class'] = 'form-control datepicker';

if($pickerType == DateTypeEnum::DateOnly) {
$coptions['placeholder'] = 'YYYY-MM-DD';
$coptions['pattern'] = '\d{4}-\d{2}-\d{2}';
$coptions['title'] = __d('field', 'datepicker.enterDate');
} else {
$coptions['placeholder'] = 'YYYY-MM-DD HH:MM:SS';
$coptions['pattern'] = '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}';
$coptions['title'] = __d('field', 'datepicker.enterDateTime');
}
$coptions['id'] = str_replace("_", "-", $fieldName);

$entity = $this->getView()->get('vv_obj');

// Default the picker date to today
$now = FrozenTime::now();
$pickerDate = $now->i18nFormat('yyyy-MM-dd');

// Get the existing values, if present
if(!empty($entity->$fieldName)) {
// Adjust the time back to the user's timezone
if($pickerType == DateTypeEnum::DateOnly) {
$coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->getView()->get('vv_tz'));
} else {
$coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->getView()->get('vv_tz'));
}
$pickerDate = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->getView()->get('vv_tz'));
}

// Set the date picker floor year value (-100 years)
$pickerDateFT = new FrozenTime($pickerDate);
$pickerDateFT = $pickerDateFT->subYears(100);
$pickerFloor = $pickerDateFT->i18nFormat("yyyy-MM-dd");

$date_picker_args = [
'fieldName' => $fieldName,
'pickerDate' => $pickerDate,
'pickerType' => $pickerType,
'pickerFloor' => $pickerFloor
];

// Create a text field to hold our value and call the datePicker
$controlCode = $this->Form->text($fieldName, $coptions)
. $this->getView()->element('datePicker', $date_picker_args);

// Specify a class on the <li> form control wrapper
$liClass = "fields-datepicker";

// Pass everything to the generic control() function
return $this->control($fieldName, $coptions, '', [], $controlCode, $liClass);
}

/**
* End a set of form controls.
*
Expand Down
4 changes: 2 additions & 2 deletions app/templates/ApiUsers/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ if($vv_action == 'add' || $vv_action == 'edit') {

print $this->Field->control('status', ['empty' => false]);

print $this->Field->control('valid_from');
print $this->Field->dateControl('valid_from');

print $this->Field->control('valid_through');
print $this->Field->dateControl('valid_through');

print $this->Field->control('remote_ip');

Expand Down
2 changes: 1 addition & 1 deletion app/templates/ExternalIdentities/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ if($vv_action == 'add' || $vv_action == 'edit') {
// XXX sync status?
print $this->Field->control('status', ['empty' => false]);

print $this->Field->control('date_of_birth');
print $this->Field->dateControl('date_of_birth', \App\Lib\Enum\DateTypeEnum::DateOnly);
}

// XXX This is a placeholder for canvas... maybe it should become a separate page
Expand Down
8 changes: 2 additions & 6 deletions app/templates/ExternalIdentityRoles/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,9 @@ if($vv_action == 'add' || $vv_action == 'edit') {

print $this->Field->control('manager_identifier', [], __d('field', 'manager'));

// XXX these need to render date pickers
// - we specifically have code in FieldHelper that checks for these two, but not date_of_birth
// - can FieldHelper introspect the date type rather than require a hard coded list of fields?
// though note valid_from/through uses special logic for 00:00:00 and 23:59:59
print $this->Field->control('valid_from');
print $this->Field->dateControl('valid_from');

print $this->Field->control('valid_through');
print $this->Field->dateControl('valid_through');
}

// XXX This is a placeholder for canvas... maybe it should become a separate page
Expand Down
5 changes: 2 additions & 3 deletions app/templates/GroupMembers/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ if($vv_action == 'add') {
print $this->Form->hidden('person_id');
}

// XXX valid from should default to 00:00:00 time, valid through to 23:59:59
print $this->Field->control('valid_from');
print $this->Field->dateControl('valid_from');

print $this->Field->control('valid_through');
print $this->Field->dateControl('valid_through');

// XXX RFE: Add links to EIS or Nesting info
2 changes: 1 addition & 1 deletion app/templates/People/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -165,5 +165,5 @@ if($vv_action == 'add') {
if($vv_action == 'add' || $vv_action == 'edit') {
print $this->Field->control('status', ['empty' => false]);

print $this->Field->control('date_of_birth');
print $this->Field->dateControl('date_of_birth', \App\Lib\Enum\DateTypeEnum::DateOnly);
}
8 changes: 2 additions & 6 deletions app/templates/PersonRoles/fields.inc
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,9 @@ if($vv_action == 'add' || $vv_action == 'edit') {
print $this->Field->statusControl($f.'_person_id', $fname, $flink, __d('field', $f));
}

// XXX these need to render date pickers
// - we specifically have code in FieldHelper that checks for these two, but not date_of_birth
// - can FieldHelper introspect the date type rather than require a hard coded list of fields?
// though note valid_from/through uses special logic for 00:00:00 and 23:59:59
print $this->Field->control('valid_from');
print $this->Field->dateControl('valid_from');

print $this->Field->control('valid_through');
print $this->Field->dateControl('valid_through');
}

// XXX This is a placeholder for canvas... maybe it should become a separate page
Expand Down
22 changes: 16 additions & 6 deletions app/templates/element/datePicker.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?php
// Get parameters
$fieldName = $fieldName ?? "";
$pickerDate = $pickerDate ?? "";
$fieldName = $fieldName ?? "";
$pickerDate = $pickerDate ?? "";
$pickerType = $pickerType ?? \App\Lib\Enum\DateTypeEnum::Standard;
$pickerFloor = $pickerFloor ?? "";

// Create a date/time picker. The yyyy-MM-dd format is set above in $pickerDate.

Expand All @@ -10,9 +12,12 @@
// like the models that use the Tree behavior and have the column parent_id
$pickerId = 'datepicker-' . str_replace("_", "-", $fieldName);
$pickerTarget = str_replace("_", "-", $fieldName);
$pickerTimed = $pickerTimed ?? true; // TODO: set false if date-only
$pickerAmPm = $pickerAmPm ?? false; // TODO: allow change between AM/PM and 24-hour mode

// Set the min and max dates to allow for a wide range of year selections in the datepicker.
$pickerDateMin = $pickerFloor; // We are passing in -100 years from FieldHelper where the values are constructed.
$pickerDateMax = ''; // If empty, the date picker will default to +10 years.

?>

<script type="module">
Expand All @@ -24,13 +29,16 @@
id: "<?= $pickerId ?>",
target: "<?= $pickerTarget ?>",
date: "<?= $pickerDate ?>",
timed: <?= ($pickerTimed ? 'true' : 'false') ?>,
datemin: "<?= $pickerDateMin ?>",
datemax: "<?= $pickerDateMax ?>",
type: "<?= $pickerType ?>",
ampm: <?= ($pickerAmPm ? 'true' : 'false') ?>,
txt: {
hour: "<?= __d('field', 'datepicker.hour') ?>",
minute: "<?= __d('field', 'datepicker.minute') ?>",
am: "<?= __d('field', 'datepicker.am') ?>",
pm: "<?= __d('field', 'datepicker.pm') ?>"
pm: "<?= __d('field', 'datepicker.pm') ?>",
choosetime: "<?= __d('field', 'datepicker.chooseTime') ?>"
}
}
},
Expand Down Expand Up @@ -63,7 +71,9 @@
:id="id"
:target="target"
:date="date"
:timed="timed"
:datemin="datemin"
:datemax="datemax"
:type="type"
:ampm="ampm"
:txt="txt">
</cm-date-time-picker>
Expand Down
Loading