Drupal Planet

Talking Drupal: Skills Upgrade #8

2 days 2 hours ago

Welcome back to “Skills Upgrade” a Talking Drupal mini-series following the journey of a D7 developer learning D10. This is episode 8.

Topics Resources

Chad's Drupal 10 Learning Curriclum & Journal Chad's Drupal 10 Learning Notes

The Linux Foundation is offering a discount of 30% off e-learning courses, certifications and bundles with the code, all uppercase DRUPAL24 and that is good until June 5th https://training.linuxfoundation.org/certification-catalog/

Hosts

AmyJune Hineline - @volkswagenchick

Guests

Chad Hester - chadkhester.com @chadkhest Mike Anello - DrupalEasy.com @ultimike

Drupal StackExchange

Published checkbox only available for administrator

2 days 1 hour ago

Only the administrator can publish a node of content type Report. It is not using content moderation or groups though other content types are. All permissions are set for the "site admin" to create/edit own/others of this type. The only buttons available are "Save" and "Preview" and the published checkbox doesn't show for non-administrator users. Save sets as "Draft" even though it's not using content moderation.

shelane

How do I create a queue derivative?

2 days 1 hour ago

I would like to create a queue derivative, where I can use a single QueueWorker class to process multiple queues, but I am not able to achieve it. Any help would be greatly appreciated.

The module structure is the following.

queue_examples/ ├── queue_examples.info.yml ├── queue_examples.routing.yml └── src ├── Form │ └── QueueDataImportForm.php └── Plugin ├── Derivative │ └── QueueDataProcessorDerivative.php └── QueueWorker └── QueueDataProcessor.php

The code I am using is the following.

QueueDataImportForm.php namespace Drupal\queue_examples\Form; use Drupal\Core\Batch\BatchBuilder; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\ClientInterface; use Drupal\Core\Url; use Drupal\Core\Link; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Component\Serialization\Json; use Drupal\Core\Queue\QueueWorkerManager; use Drupal\Core\Queue\QueueFactory; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Component\Datetime\Time; use Drupal\Component\Utility\Unicode; /** * Configure queue_examples settings for this site. */ class QueueDataImportForm extends FormBase { /** * The HTTP client to fetch the feed data with. * * @var \GuzzleHttp\ClientInterface */ protected $httpClient; /** * Batch Builder. * * @var \Drupal\Core\Batch\BatchBuilder */ protected $batchBuilder; /** * Drupal\Component\Datetime\Time instance. * * @var \Drupal\Component\Datetime\Time */ public $time; /** * Drupal\Core\Queue\QueueFactory instance. * * @var \Drupal\Core\Queue\QueueFactory */ protected $queueFactory; /** * The module handler service. * * @var \Drupal\Core\Extension\ModuleHandlerInterface */ protected $moduleHandler; /** * LocationsBatchImportForm constructor. */ public function __construct(ClientInterface $http_client, QueueFactory $queue_factory, QueueWorkerManager $queue_manager, ModuleHandlerInterface $module_handler, Time $time) { $this->httpClient = $http_client; $this->batchBuilder = new BatchBuilder(); $this->queue_factory = $queue_factory; $this->queue_manager = $queue_manager; $this->moduleHandler = $module_handler; $this->time = $time; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('http_client'), $container->get('queue'), $container->get('plugin.manager.queue_worker'), $container->get('module_handler'), $container->get('datetime.time') ); } /** * {@inheritdoc} */ public function getFormId() { return 'queue_examples_batch_form'; } /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { $form['help'] = [ '#markup' => $this->t('This form is used to get the location data from remote URL and push data to queue in Batches.'), ]; $sourceUrl = 'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/master/countries.json'; $link = Link::fromTextAndUrl('here', Url::fromUri($sourceUrl, ['attributes' => ['target' => '_blank']])); $locations = [ 'countries' => $this->t('Countries'), 'states' => $this->t('States'), 'cities' => $this->t('Cities'), 'cities_one' => $this->t('Cities One'), ]; $form['locations_list'] = [ '#title' => $this->t('Select Location Type.'), '#type' => 'select', '#options' => $locations, '#empty_option' => $this->t('Choose a Location'), '#required' => TRUE, ]; $form['actions'] = ['#type' => 'actions']; $form['actions']['run'] = [ '#type' => 'submit', '#value' => $this->t('Import Locations'), '#button_type' => 'primary', ]; return $form; } /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { // Check whether the Location Type is valid or not. $location = $form_state->getValue('locations_list'); if (!in_array($location, ['countries', 'states', 'cities'], TRUE)) { $form_state->setErrorByName('locations_list', $this->t('The selected location type %location is invalid.', ['%location' => $location])); } } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { $data = []; $link = Link::fromTextAndUrl('here', Url::fromRoute('queue_examples.queue_data_import_form')); $location = $form_state->getValue('locations_list'); if (!empty($location)) { $remoteUrl = 'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/master/' . $location . '.json'; $data = $this->fetchData($remoteUrl); if (!empty($data)) { $decodedData = Json::decode($data); $this->batchBuilder ->setTitle($this->t('Processing @location Batch...', ['@location' => Unicode::ucfirst($location)])) ->setInitMessage($this->t('Initializing Batch...')) ->setProgressMessage($this->t('Completed @current of @total. Estimated remaining time: @estimate.')) ->setErrorMessage($this->t('An error has occurred.Click @link to return to form', ['@link' => $link->toString()])); $this->batchBuilder->addOperation([$this, 'processItems'], [$location, $decodedData]); $this->batchBuilder->setFinishCallback([$this, 'finished']); batch_set($this->batchBuilder->toArray()); } } } /** * {@inheritdoc} */ public function fetchData(string $url) { $data = []; try { // @todo check whether implementation of 'timeout' => 600 // is correct or not in httpClient. $response = $this->httpClient->get($url, ['headers' => ['Content-Type' => 'application/hal+json']]); if ($response->getStatusCode() == 200) { $data = (string) $response->getBody(); } } catch (RequestException $exception) { $this->messenger()->addError('Failed to download JSON data from URL due to an error check logs for more details.'); $this->logger('queue_examples')->warning('Failed to download JSON data from URL due to "%error".', ['%error' => $exception->getMessage()]); } return $data; } /** * Processor for batch operations. */ public function processItems($location, $items, array &$context) { // Elements per operation. $limit = 50; // Set default progress values. if (empty($context['sandbox']['progress'])) { $context['sandbox']['progress'] = 0; $context['sandbox']['max'] = count($items); } // Save items to array which will be changed during processing. if (empty($context['sandbox']['items'])) { $context['sandbox']['items'] = $items; } $counter = 0; if (!empty($context['sandbox']['items'])) { // Remove already processed items. if ($context['sandbox']['progress'] != 0) { array_splice($context['sandbox']['items'], 0, $limit); } foreach ($context['sandbox']['items'] as $item) { if ($counter != $limit) { $this->processItem($location, $item); $counter++; $context['sandbox']['progress']++; $context['message'] = $this->t('Now Processing Data Item :progress of :count', [ ':progress' => $context['sandbox']['progress'], ':count' => $context['sandbox']['max'], ]); // Increment total processed item values. Will be used in finished // callback. $context['results']['processed'] = $context['sandbox']['progress']; } } } // If not finished // If not finished all tasks, we count percentage of process. 1 = 100%. if ($context['sandbox']['progress'] != $context['sandbox']['max']) { $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; } } /** * Process single item. * * @param string $location * Location Type. * @param array $item * Single Location Data. */ public function processItem(string $location, array $item) { if (!empty($item)) { // Create new queue item. $queue = $this->queue_factory->get('queue_data_processor:' . $location); $queue->createItem($item); // $queue->deleteItem($data); $this->logger('queue_examples') ->notice($this->t('@location Data Item Pushed to Queue: @data', [ '@data' => implode(',', $item), '@location' => Unicode::ucfirst($location), ]) ); } } /** * Finished callback for batch. */ public function finished($success, $results, $operations) { $message = $this->t('Number of Items processed by batch: @count', [ '@count' => $results['processed'], ]); $this->messenger()->addStatus($message); $this->logger('queue_examples')->info($message); } } QueueDataProcessorDerivative.php namespace Drupal\queue_examples\Plugin\Derivative; use Drupal\Component\Plugin\Derivative\DeriverBase; /** * The simple derivative example for Queue. */ class QueueDataProcessorDerivative extends DeriverBase { /** * {@inheritdoc} */ public function getDerivativeDefinitions($base_plugin_definition) { $locations = ['countries', 'states', 'cities']; foreach ($locations as $location) { $this->derivatives[$location] = $base_plugin_definition; $this->derivatives[$location]['title'] = $location . 'Queue'; } return $this->derivatives; } }

### QueueDataProcessor.php

namespace Drupal\queue_examples\Plugin\QueueWorker; use Drupal\Core\Queue\QueueWorkerBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Database\Connection; use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Processes Locations Data. * * @QueueWorker( * id = "queue_data_processor", * title = @Translation("Queue Data Processor."), * cron = {"time" = 10}, * deriver = "Drupal\queue_examples\Plugin\Derivative\QueueDataProcessorDerivative", * ) */ class QueueDataProcessor extends QueueWorkerBase implements ContainerFactoryPluginInterface { use StringTranslationTrait; /** * The Messenger service. * * @var \Drupal\Core\Messenger\MessengerInterface */ protected $messenger; /** * Logger service. * * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface */ protected $logger; /** * Database service. * * @var \Drupal\Core\Database\Connection */ protected $database; /** * {@inheritdoc} */ public function __construct(LoggerChannelFactoryInterface $logger, MessengerInterface $messenger, Connection $connection) { $this->logger = $logger->get('queue_examples'); $this->messenger = $messenger; $this->database = $connection; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $container->get('logger.factory'), $container->get('messenger'), $container->get('database') ); } /** * {@inheritdoc} */ public function processItem($data) { $id = $this->getDerivativeId(); // @todo if id = countries insert data into countries table. // if id = states insert data into states table. // if id = cities insert data into cities table. $this->logger->info($id . 'Queue Processed.'); if (!empty($data)) { // $query = $this->database->insert($id); // $query->fields($data); // $query->execute(); $this->logger->info($this->t('Data available to Process.')); } else { $this->logger->warning($this->t('No Data available to Process.')); } } }

What's wrong in the code I am using?

I would like to insert countries data into countries table, states data into states table, and cities data into cities table. All using single queue processor file using derivatives.

I know of other way where you can conditionally check data and process the data accordingly, but I would like to achieve it using queue derivative.

I can see 3 Queues in the list when I run drush queue:list.

------------------------------------ ------- --------------------------------- Queue Items Class ------------------------------------ ------- --------------------------------- queue_data_processor:countries 0 Drupal\Core\Queue\DatabaseQueue queue_data_processor:states 0 Drupal\Core\Queue\DatabaseQueue queue_data_processor:cities 0 Drupal\Core\Queue\DatabaseQueue ------------------------------------ ------- ---------------------------------

In case it helps, I got this idea reading Drupal 8: Derivatives — множественные экземпляры плагина.

Edit 1:

The issue I am having is Id is Null in QueueDataProcessor::processItem($data)

miststudent2011

caching views based on arguments as taxonomy term id

2 days 1 hour ago

Hi need to enable caching based on taxonomy term id from the url which the views takes as an argument

1)simple cache returns empty results

2)views_arg_cache module also empty

3)views_content_cache module aslo empty

please help if any body has tried such scenario and found the solution

Mohammed Gomma

Simplenews Scheduler - only send newsletter when attached EVA view contains results

2 days 2 hours ago

Simplenews Scheduler is working well for me to send a weekly email which contains an attached view (using EVA) of nodes.

I would like the newsletter to only send if the attached view contains nodes, so that empty newsletters are not sent.

I can see that this should be possible using the 'Additionally only create newsletter edition if the following code returns true' field on the Newsletter > Send newsletter according to schedule page. I am currently using this (which always produces a newsletter regardless of whether nodes are displayed in the view):

if(views_get_view('job_type_c_list')) { return true; }

I have tried using the overall length of the node body to try to work out if a view is present. (I don't know if the tokens can be used in this field or not):

if(views_get_view('job_type_c_list')) { $myvar = [node:body:?]; if(strlen($myvar) >= 30) { return true; } }

When i was working with PHP in the body of the node I was able to evaluate whether EVA was returning nodes by using

<?php $view = views_get_view('job_type_c_list'); $printedview = $view->preview('default'); $myvar = (string)$printedview; if(strlen($myvar) >= 1000) { print "VIEW ATTACHED"; } else if(strlen($myvar) < 1000) { print "NO VIEW ATTACHED"; } ?>

The above code is clearly not the most elegant, but it worked in the node itself. Unfortunately it didn't work in the 'Additionally only create newsletter edition if the following code returns true' Simple news scheduler field.

I have struggled with this for quite some time and would really appreciate some help. Code suggestions would be great, but failing that knowing 'If I can use tokens in the field?' and 'why does the PHP in this field behaves differently to PHP the node itself?' would also be very helpful.

I have had an issue open in the module issues page for a few days now, but it has not received any responses.

Paul Trotter

Programmatic access to metatag module fields

2 days 3 hours ago

I am wondering how to programmatically access metatag module data. For example, my node has an entry for metatag description, and I see in the generated page the meta description tag, which is great. However, it's not clear how I can access this programmatically.

Right now, I am trying the following:

$metatag_manager = \Drupal::service('metatag.manager'); $tags = $metatag_manager->tagsFromEntityWithDefaults($node); $elements = $metatag_manager->generateRawElements($tags, $node);

Inspecting the data, I see that $tags includes a 'description' entry, but $elements does not. Is there a better way of accessing this data?

The metatag module includes documentation for programmatically setting data, but unfortunately does not mention accessing the data.

Edit: Alas, it seems I was inspecting the wrong node -- one with the same title as a node with metadata entries. Working now with the correct node, it seems all I need to use is:

$metatag_manager = \Drupal::service('metatag.manager'); $tags = $metatag_manager->tagsFromEntity($node);

Which returns an array with a description key and the description value entered for that node.

Mike Godin

Pricing changes based on line item fields in the add to cart form

2 days 3 hours ago

I've been using commerce_custom_product to add custom line items options to products I'm selling in the shop. The shop is selling servers and if someone when adding a server to their cart wants a bigger hard drive for instance they select it in the "add to cart" form. I want to alter the price of the line item based on which option they select

e.g.

20gb HDD $40 40 gb HDD $70

The hard drives and other things are not really properties of the product I'm buying. They are options I'm selecting for the product. This is why I looked at commerce_custom_product.

Ultimately this question comes down to How can I set the product price based on choices made in the Add to Cart form. Is commerce_custom_product the way forward with something else bolted in? I'm very happy to code if there is a hook I could use to grab the price from the referenced entity if I was to add price to the entities that get selected in the add to cart form

Stewart Robinson

How to sort the query results?

2 days 4 hours ago

In my task I need to display the results based on "the newest first". I perform a query:

$query = \Drupal::database()->select('node_field_data', 'node'); $query->innerJoin('node__field_ils_apps', 'app', 'app.entity_id = node.nid AND app.field_ils_apps_target_id = '.$node->id()); $query->addField('app', 'entity_id', 'nid'); $query->groupBy('app.entity_id'); $query->orderBy('created', 'DESC'); $query->execute();

But it returns me an error:

SQLSTATE[42000]: Syntax error or access violation: 1055 Expression #1 of ORDER BY clause is not in GROUP BY clause and contains nonaggregated column

Could you please tell me what I do wrong?

badm

Is this the correct way to add a page with hook_menu()?

2 days 5 hours ago

I'm trying to create a page through hook_menu() so I can add my custom block with context instead.

function my_module_menu() { $items['my_page'] = array( 'title' => 'my_page', 'description' => 'my_page', 'page callback' => 'my_function', 'access callback' => true, ); $items['my_page2'] = array( 'title' => 'my_page2', 'description' => 'my_page2', 'page callback' => 'my_function', 'access callback' => true, ); return $items; } function my_function() { return ''; }

Is it the right way to do it? I have some bug from time to time, like the block goes missing on those pages. Could it be related to that?

GwenM