I want to add multiple Schema.org Event JSON objects to the head section of my front page. The start and end dates need to be modified programmatically as time moves forward. The Schema.org module does not meet my needs.
Playing around in html.html.twig, it looks like the output of the Metatag and Schema.org modules are in head-placeholder token="{{ placeholder_token }}". I think I need to append my JSON to that placeholder in template_preprocess_html.
I have only done a small amount of _preprocess modifications, so if that is correct, any hints on how to append to that placeholder? If is it not correct, can someone point me in the right direction?
I did some exploration into how the Metatag module inserted its json and came up with this. I believe it will work.
function hhc_page_attachments_alter(array &$attachments) {
$path = \Drupal::request()->getRequestUri();
//If it is not the home page, exit
if ($path != '/')
return;
$attachments['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => "JDD PUT JSON CODE HERE",
'#attributes' => ['type' => 'application/ld+json'],
],
'my_json',
];
}
Here is the complete solution I came up with. I realize this would probably be better done with a module making full use of the Drupal framework, but I do not know how to do that! Small site, small company, and just me maintaining it. This works, is live, and validates at validator.schema.org, and passed the test for rich results at search.google.com/test/rich-results.
Perhaps this will help another beginner like myself.|
Problem Statement: Company has 2 events every month that start on the 1st and 15th of each month. Want Schema.org Event JSON for the next 4 upcoming events, output on the home page. The first event must start at least two weeks after "Today".
/*
* Gets the next start date of a retreat to be used in the Schema.org Event Type
* Retreats always begin on 1st of 15th of the month.
* Retreats to display will alway begin at least two weeks in advance, so always the next month.
* To get the first upcoming retreat (first call of function), if current day is < 15,
* get 1st of next month. If 15 or >, get 15th of next month.
* For the second upcoming retreat,
*/
function nextRetreat(DateTime $now, bool $firstCall) {
$nextRetreat = new DateTime();
if ($firstCall) {
$now->modify('+1 month');
$day = ($now->format('d') < 15)? 1 : 15;
$month = $now->format('m');
$year = $now->format('Y');
$nextRetreat->setDate($year, $month, $day);
}
else {
//If second call or more, the day of $now should be 1 or 15. If one, we want the 15th of same month
//If 15, we want first day of next month.
if ($now->format('d') < 15) {
$nextRetreat = $now->modify('+14 days');
}
else {
$nextRetreat = $now->modify('first day of next month');
}
}
return $nextRetreat;
}
/*
* Builds the Schema.org Event type with a subset of hard coded values.
* This is hard coded for a 12 day retreat, and the endDate of the event
* is 11 days after the start.
*
* $start: The start date of the event
*/
function buildEventJSON(DateTime $start) {
$end = (clone $start)->modify('+11 days');
$event = [
"#context" => "https://schema.org",
"#type" => "Event",
"name" => "12 Day Retreat",
"image" =>"https://example.com/images/some-image.jpg",
"description" => "Worlds best retreat of type Retreat. You can't get a better retreat than the ones we are Retreating!",
"startDate" => $start->format('Y-m-d')."T10:00",
"endDate" => $end->format('Y-m-d')."T10:00",
"eventStatus" => "https://schema.org/EventScheduled",
"eventAttendanceMode" => "https://schema.org/OfflineEventAttendanceMode",
"location" => [
"#type" => "Place",
"name" => "Great Retreat Center",
"address" => [
"#type" => "PostalAddress",
"streetAddress" => "Main Street #226",
"addressLocality" => "Lima",
"postalCode" => "00000",
"addressCountry" => "PE"
]
],
"offers" => [
[
"#type" => "Offer",
"name" => "9 Day Retreat",
"price" => "180",
"priceCurrency" => "USD",
"validFrom" => "2022-01-01",
"url" => "https://example.com/9-day-retreats",
"availability" => "https://schema.org/InStock"
],
[
"#type" => "Offer",
"name" => "12 Day Retreat",
"price" => "220",
"priceCurrency" => "USD",
"validFrom" => "2022-02-01",
"url" => "https://example.com/12-day-retreats",
"availability" => "https://schema.org/InStock"
]
]
];
return json_encode($event, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE);
}
function hhc_page_attachments_alter(array &$attachments) {
$path = \Drupal::request()->getRequestUri();
//If it is not the home page, exit
if ($path != '/')
return;
$nextRetreatStart = nextRetreat(new DateTime(), true);
$attachments['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => buildEventJSON($nextRetreatStart),
'#attributes' => ['type' => 'application/ld+json'],
],
'my_event_1',
];
// Change the start and end date and second event.
$nextRetreatStart = nextRetreat($nextRetreatStart, false);
$attachments['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => buildEventJSON($nextRetreatStart),
'#attributes' => ['type' => 'application/ld+json'],
],
'my_event_2',
];
// Change the start and end date and third event.
$nextRetreatStart = nextRetreat($nextRetreatStart, false);
$attachments['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => buildEventJSON($nextRetreatStart),
'#attributes' => ['type' => 'application/ld+json'],
],
'my_event_3',
];
// Change the start and end date and fourth event.
$nextRetreatStart = nextRetreat($nextRetreatStart, false);
$attachments['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => buildEventJSON($nextRetreatStart),
'#attributes' => ['type' => 'application/ld+json'],
],
'my_event_4',
];
}
Related
I'm trying to write a Perl script that will update our eBay listings descriptions without having to keep logging in (running across multiple marketplaces if proving tricky to keep stock levels, descriptions etc updated). Here is what I have so far:
my $ebay = new Net::eBay( {
SiteLevel => 'prod',
DeveloperKey => 'x',
ApplicationKey => 'x',
CertificateKey => 'x',
Token => 'x',
} );
$ebay->setDefaults( { API => 2, compatibility => 900 } );
my $new_desc = q|<meta name="viewport" content="width=device-width, initial-scale=1.0">
<p>We are proud to announce our first ever badge! With an easy-to-iron
on backing, fitting couldn't be any easier! We have designed the path to
be a perfect addition to any piece of cosplay costume. Please do send
in the photos of it being used on your costumes, as we would love to
share.</p>
<p>The badge is 7 x 7 cm / 2 x 2 inches in size, and 2mm thi<br></p>|;
my $result = $ebay->submitRequest( "ReviseItem",
{
DetailLevel => "ReturnAll",
ErrorLevel => "1",
SiteId => "1",
Item => {
Description => \$new_desc,
ItemID => 253430606975
},
ItemID => 253430606975
}) || die;
print "Result: " . Dumper( $result ) . "\n";
I get an error when running it though:
'Errors' => [
{
'ShortMessage' => 'Return Policy Attribute Not Valid',
'ErrorClassification' => 'RequestError',
'ErrorCode' => '21920200',
'LongMessage' => 'Return Policy Attribute returnDescription Not Valid On This Site',
'SeverityCode' => 'Warning',
'ErrorParameters' => {
'Value' => 'returnDescription',
'ParamID' => '0'
}
},
{
'ShortMessage' => 'Description is missing.',
'ErrorClassification' => 'RequestError',
'ErrorCode' => '106',
'SeverityCode' => 'Error',
'LongMessage' => 'A description is required.'
}
],
Am I misunderstanding what gets passed in? from what I can understand, you just pass in the params you want to change?
UPDATE: As suggested by Dave, I'm giving Marketplace::Ebay a go. Just testing by trying to select one of my items:
my $ebay = Marketplace::Ebay->new(
production => 1,
site_id => 3,
developer_key => 'xx',
application_key => 'xx',
certificate_key => 'xxx',
token => 'xx',
xsd_file => 'ebaySvc.xsd',
);
my $res = $ebay->api_call('GetItem', { ItemID => 253430606975 });
print Dumper($res);
But I get some weird error:
error: element `{urn:ebay:apis:eBLBaseComponents}GiftIcon' not
processed for {urn:ebay:apis:eBLBaseComponents}GetItemResponse/Item at
//[5]/*[6] $VAR1 = undef;
Any ideas?
Ah ha - got it! The issue seemed to be around the way the HTML was being passed along. If I put it inside a CDATA tag, it works fine:
my $new_desc = q|<![CDATA[
some html etc here
]]>|;
my $result = $ebay->submitRequest( "ReviseItem",
{
DetailLevel => "ReturnAll",
ErrorLevel => "1",
SiteId => "1",
Item => {
Description => $new_desc,
ItemID => 253430606975
},
ItemID => 253430606975
}) || die;
...and updates perfectly
Kinda stuck.
We have products which we offer to companies.
so it would be an mm field which is no problem to implement, but the problem comes with date, since each product has expire date based on company.
so decided to remove product mm and put product id with date into company as one field.
but since input which we send is an array it's not possible to save it there.
How can i solve this problem ?
this is my tca for that field:
'product' => [
'exclude' => true,
'label' => 'LLL:EXT:wemessage_checklist/Resources/Private/Language/locallang_db.xlf:tx_wemessagechecklist_domain_model_company.product',
'config' => [
'type' => 'user',
'userFunc' => Wemessage\WemessageChecklist\UserFunc\Checklist::class.'->renderChecklists',
],
],
Here is function to render html:
public function renderChecklists($PA, $fObj){
$checked = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('*', 'tx_wemessagechecklist_domain_model_company', 'hidden=0 and deleted=0 and uid='.$PA['row']['uid'].' and pid='.$PA['row']['pid']);
$allProducts = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('*', 'tx_wemessagechecklist_domain_model_product', 'hidden= 0 and deleted = 0');
foreach($allProducts as $product){
$html .= '<div>';
$html .= '<input type="hidden" name="data[tx_wemessagechecklist_domain_model_company]['.$PA['row']['uid'].'][product]['.$product['uid'].']" class="product"/>';
$html .= '<input type="checkbox" class="tocheck" value="'.$product['uid'].'" /><span style="margin: 0 10px;">'.$product['name'].'</span><span style="margin: 0 10px;">Verval datum</span><input type="date" class="date" />';
$html .= '</div>';
}
$html .= '<script>
TYPO3.jQuery(".tocheck").click(function(){
var val = TYPO3.jQuery(this).val();
if(TYPO3.jQuery(this).prop("checked")){
TYPO3.jQuery(this).parent().find(".product").val(val);
}
});
TYPO3.jQuery(".date").change(function(){
var x = new Date(TYPO3.jQuery(this).val()).getTime();
var b = TYPO3.jQuery(this).parent().find(".product").val();
TYPO3.jQuery(this).parent().find(".product").val(b+":"+x);
});
</script>';
return $html;
}
probably another option is to implement AJAX calls to process data in standalone table. although if there is another solution will be glad to hear it.
You should still go for an MM relation or better inline (IRRE) connection from company to product, since these can have an own TCA too, thus making it possible to add starttime, endtime and other values to the MM relation record itself.
The concept is called "intermediate table" and is described here: https://docs.typo3.org/typo3cms/TCAReference/ColumnsConfig/Type/Inline.html#attributes-on-anti-symmetric-intermediate-table
Here is a modified TCA taken from the IRRE tutorial extension. It contains just the necessary settings for the connections and the timestamp field - feel free to add your own fields for companies and products:
$TCA["company"] = Array (
"columns" => Array (
"products" => Array (
"config" => Array (
"type" => "inline",
"foreign_table" => "company_product_intermediate",
"foreign_field" => "company_id",
"foreign_sortby" => "companysort",
"foreign_label" => "product_id",
"maxitems" => 10,
'appearance' => array(
'showSynchronizationLink' => 1,
'showAllLocalizationLink' => 1,
'showPossibleLocalizationRecords' => 1,
'showRemovedLocalizationRecords' => 1,
),
'behaviour' => array(
'localizationMode' => 'select',
),
)
),
),
);
$TCA["company_product_intermediate"] = Array (
"columns" => Array (
"company_id" => Array (
"config" => Array (
"type" => "select",
"foreign_table" => "company",
"maxitems" => 1,
)
),
"product_id" => Array (
"config" => Array (
"type" => "select",
"foreign_table" => "product",
"maxitems" => 1,
)
),
"companysort" => Array (
"config" => Array (
"type" => "passthrough",
)
),
"productsort" => Array (
"config" => Array (
"type" => "passthrough",
)
),
"timestamp" => Array (
"config" => Array (
"type" => "input",
)
),
),
);
$TCA["product"] = Array (
"columns" => Array (
"companies" => Array (
"config" => Array (
"type" => "inline",
"foreign_table" => "company_product_intermediate",
"foreign_field" => "product_id",
"foreign_sortby" => "productsort",
"foreign_label" => "company_id",
"maxitems" => 10,
'appearance' => array(
'showSynchronizationLink' => 1,
'showAllLocalizationLink' => 1,
'showPossibleLocalizationRecords' => 1,
'showRemovedLocalizationRecords' => 1,
),
'behaviour' => array(
'localizationMode' => 'select',
),
)
),
),
);
I would like to change class for one single attribute in detailview, based on a condition:
If I wouldn't want to make it conditional, it would be working like so:
[
'attribute' => 'ungueltig',
'format' => 'boolean',
'contentOptions' => [
'class' => 'danger',
]
],
I want this one to change to conditional, and I have tried a lot of different ways, e.g.:
[
'attribute' => 'ungueltig',
'format' => 'boolean',
'contentOptions' => function ($model) {
if ($model->ungueltig == 1) {
return ['class' => 'danger'];
} else {
return '';
}
},
],
(I would think this is the most logical solution, but nothing happens, so page is loading fine but without class danger at the attribute, no error message)
or
[
'attribute' => 'ungueltig',
'format' => 'boolean',
'contentOptions' => ['class' => function ($model) {
if ($model->ungueltig == 1) {
return 'danger';
} else {
return '';
}
},]
],
= error message: htmlspecialchars() expects parameter 1 to be string, object given
so I have no clue and I don't even find any help on the web. Can you please point me to the right direction? Many thanks!
You should simply try :
'contentOptions' => [
'class' => ($model->ungueltig == 1) ? 'danger' : '',
],
DetailView display only one model, you don't need any function here.
I use a MySQL database in production and a SQLite database for running tests. One part of my application is used to gather monthly statistics for a year. I've successfully done this, however it came at a cost of not being able to automate tests because I'm using MySQL specific functions when querying for the data:
my $criteria = {
status => ['Complete'],
'YEAR(completed_on)' => DateTime->now()->year(),
};
my $attributes = {
select => [ { count => 'title' }, 'completed_on' ],
as => [qw/num_completed datetime/],
group_by => [qw/MONTH(completed_on)/],
};
Notice I'm using YEAR and MONTH MySQL functions.
I know one way I can substitute the where clause to eliminate the use of MySQLs YEAR function, something like this:
my $dtf = $schema->storage->datetime_parser;
my $begin_date = DateTime->from_day_of_year( year => DateTime->now()->year(), day_of_year => 1 ); #inception o_O
my $end_date = DateTime->from_day_of_year( year => DateTime->now()->year(), day_of_year => 36[56] );
my $criteria = {
status => ['Complete'],
completed_on =>
-between => [
$dtf->format_datetime($begin_date),
$dtf->format_datetime($end_date),
]
};
Using the recommended way to query date fields using DBIC
But I'm stumped as to what to do with the group_by clause and how to make the grouping of this fields date value by month database agnostic as well. Wondering if anyone has any ideas?
Thanks!
Sometimes you will have to make engine specific code in DBIx::Class if you're trying to do special things. You can use $schema->storage->sqlt_type to make different SQL.
Note you can also use substr(completed_on,1,4) to get the year in SQLite.
This will solve your problem:
my $type = $schema->storage->sqlt_type;
my $criteria;
my $attributes;
if ($type eq 'MySQL') {
$criteria = {
status => ['Complete'],
'YEAR(completed_on)' => DateTime->now()->year(),
};
$attributes = {
select => [ { count => 'title' }, 'completed_on' ],
as => [qw/num_completed datetime/],
group_by => [qw/MONTH(completed_on)/],
};
}
elsif ($type eq 'SQLite') {
my $dtf = $schema->storage->datetime_parser;
my $begin_date = DateTime->from_day_of_year( year => DateTime->now()->year(), day_of_year => 1 ); #inception o_O
my $end_date = DateTime->from_day_of_year( year => DateTime->now()->year() + 1, day_of_year => 1 )->add( seconds => -1 );
$criteria = {
status => ['Complete'],
completed_on => {
-between => [
$dtf->format_datetime($begin_date),
$dtf->format_datetime($end_date),
]
}
};
$attributes = {
select => [ { count => 'title' }, 'completed_on' ],
as => [qw/num_completed datetime/],
group_by => ['substr(completed_on,6,2)'],
};
}
I have two Array of Hashes: The first contains values for a current time interval and the second contains values for a previous time interval.
#AoHcurrent=
( { node => "ABC",
link => "DEF",
time => "10:00",
value => "100",
},
{
node => "FGH",
link => "IJK",
time => "10:00",
value => "200",
},
);
#AoHprevious=
( { node => "ABC",
link => "DEF",
time => "09:45",
value => "10",
},
{ node => "FGH",
link => "IJK",
time => "09:45",
value => "50",
},
);
I want to now use HTML-Template to present this data. Something like :
NODE LINK VALUE
---------------------
ABC DEF 100(10)
FGH IJK 200 (50)
the values in brackets represent the previous value.
my %html_template_parameters =
( AOHCURRENT => \#AoHcurrent,
AOHPREVIOUS => \#AoHprevious, );
my $html_template=qq{Report.tmpl};
my $html_output=qq{Report.html};
htmlReport($html_template,$html_output,\%html_template_parameters);
where htmlReport is a function that generates the report
I require guidance on defining the Report.tmpl file.
Thanks you in advance
see also http://www.perlmonks.org/?node_id=972954
I gave an example there how this can be solved with HTML::Template::Compiled.
Basically you would navigate through the parameter stash like this:
[%= expr=".AOHPREVIOUS[__index__]{'value'}" %]
or with the classic syntax:
<TMPL_VAR expr=".AOHPREVIOUS[__index__]{'value'}" >
You can't do that with 2 separate lists just with HTML::Template. And trying to do it with HTML::Template::Expr would be a nightmare to maintain. Try collapsing them into a single list where the hash data is merged.