I have a custom block module for Drupal 8. It is working on my localhost version of drupal (version 8.7.8). When I upload it to the web server (Version 8.7.11), I can enable the module, but it doesn't show up when I try to place the block on the block layout page. I don't have much control of the web server - files are uploaded via a git repository, but other modules I've added work without issues.
My module is just 2 files:
modules/custom/ischool_section_title_level_two/ischool_section_title_level_two.info.yml
name: iSchool Section Title Level Two
description: Provides a block that shows the Level Two title, or Level One if there is no Level Two.
core: 8.x
package: Custom
dependencies:
- block
type: module
modules/custom/ischool_section_title_level_two/src/plugin/block/iSchoolSectionTitlelevel_two.php
<?php
namespace Drupal\ischool_section_title_level_two\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a block that shows the Level Two section title, or Level One title if there is no level Two
*
* #Block(
* id = "ischool_section_title_level_two",
* admin_label = #Translation("iSchool Section Title Level Two"),
* category = #Translation("Custom"),
* context_definitions = {
* "node" = #ContextDefinition("entity:node", label = #Translation("Node"))
* }
* )
*/
//code adapted from http://hussainweb.me/an-easier-way-to-get-the-current-node-in-a-block-plugin-in-drupal-8/
//and https://design.briarmoon.ca/tutorials/drupal-8/getting-the-parent-node-of-a-drupal-8-node
class iSchoolSectionTitlelevel_two extends BlockBase {
public function build() {
$node = $this->getContextValue('node');
if (empty($node)) {
return [
'#markup' => "",
];
}
$L1_Title = $node->getTitle();
$L2_Title = $node->getTitle();
$currentNode = $node;
while (true) {
$parent_node = $this->getParentNode($currentNode);
if (empty($parent_node)){
break;
}
$L2_Title = $L1_Title;
$L1_Title = $parent_node->getTitle();
$currentNode = $parent_node;
}
return [
'#markup' => $L2_Title,
];
}
private function getParentNode($node){
if (empty($node)) return null;
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$links = $menu_link_manager->loadLinksByRoute('entity.node.canonical', ['node' => $node->id()]);
// Because loadLinksByRoute() returns an array keyed by a complex id
// it is simplest to just get the first result by using array_pop().
/** #var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = array_pop($links);
if (empty($link)) return null;
/** #var \Drupal\Core\Menu\MenuLinkInterface $parent */
if ($link->getParent() && $parent = $menu_link_manager->createInstance($link->getParent())) {
if (!method_exists($parent, "getUrlObject")) return null;
$urlObj = $parent->getUrlObject();
if (is_null($urlObj)) return null;
if (!method_exists($urlObj, "getRouteParameters")) return null;
$route = $urlObj->getRouteParameters();
if (empty($route)) return null;
if (!isset($route['node'])) return null;
$parent_node = \Drupal::entityManager()->getStorage('node')->load($route['node']);
return $parent_node;
}
else return null;
}
// cache this block for a definite time.
public function getCacheMaxAge() {
return 43200;
}
}
This was an issue with the capitalization of the folders.
The 2nd file should have been in the /src/Plugin/Block/ folder but instead was in the /src/plugin/block/ folder (missing the initial caps).
On the local windows machine, this didn't make any difference. On the LAMP stack machine it resulted in the block not showing.
Related
This relates to a semi-known issue listed on the GitHub page [link], where the way PhpWord generates tables causes it to set the cell width to "", which isn't valid in LibreOffice, but is fine in Word.
The issue on GitHub lists html-to-word conversion specifically, but I'm getting this issue when just going by the regular object-oriented interface also.
I've tried manually setting the cell width, but it doesn't seem to do anything.
I'm asking here to see if anyone has any workaround to this issue, as it makes it nearly impossible to debug my output on a Linux box without access to MS Word. I need to export .docx for a client.
Why not just export to .odt to test it instead? If I do that it won't render the list I have on the page either.....
Here's a rough outline of the document I'm working with (I'm not at liberty to share the exact code due to NDAs):
$document = new PhpWord\PhpWord();
$section = $document->addSection();
// This list doesn't get rendered in the ODT file
$section->addListItem('Employee ID: 98765');
$section->addListItem('First Name: John');
$section->addListItem('Last Name: Doe');
$section->addListItem('SSN: 12345');
// This table has errors in LibreOffice when exported to .docx
if ($show_adv_details) {
$table = $section->addTable();
$table->addRow();
$table->addCell()->addText('SSN');
$table->addCell()->addText('Email');
$table->addCell()->addText('Phone No');
$table->addRow();
$table->addCell()->addText('12345');
$table->addCell()->addText('jd#example.com');
$table->addCell()->addText('999 1234');
// repeat ad nauseam
}
$section->addTitle('Comment');
$section->addTitle('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.');
I'm using PHP 7.0 (ancient legacy system, can't do anything about it) on Ubuntu.
So just to reiterate the question here at the end:
Is there something I can do to make this document render correctly on all the different outputs? The documentation doesn't seem to be much help.
Well, I'm not a maintainer from the PHPOffice package but here is a workaround which I made digging in the package code and fixing your two issues:
ODT List does not work;
Tables on Word2007 Writer when opening with LibreOffice does not work.
I realized that the ListItem.php class did not exist on ODText writer, and I suppose that this is why you can not add lists to .odt files.
You need to add the widths manually to columns to make it work on LibreOffice (as Nigel said, you can go through each addCell() call and add some number, even "1" works).
How to fix both issues
We will "override" the original files with some updates that fix the issues.
In order to make it work, you should have installed PHPWord using Composer.
(This is the only option available on the installation guide, but there are other ways to do that).
Without Composer, require the custom files after PHPWord require (I think that it works). Or change directly the files on their locations (a bad idea).
Files
Create a structure like that:
Custom
├── Element
│ └── Table.php
└── Writer
└── ODText
└── Element
└── ListItem.php
Of course, you can use any other, but we're going to "override" the original package files, so I kept its structure.
We will use Autoload Files to get those files:
composer.json:
...
"autoload": {
"files": [
"Custom/Element/Table.php",
"Custom/Writer/ODText/Element/ListItem.php"
]
},
...
If there's no "autoload" key on your composer.json, you have to add it, and the same goes for the "files" key.
Run composer dump-autoload to update the changes.
Custom classes
Now, we have to add the code to our custom files.
Custom/Writer/ODText/Element/ListItem.php:
<?php
namespace PhpOffice\PhpWord\Writer\ODText\Element;
/**
* ListItem element writer
*/
class ListItem extends AbstractElement
{
/**
* Write list item element.
*/
public function write()
{
$xmlWriter = $this->getXmlWriter();
$element = $this->getElement();
if (!$element instanceof \PhpOffice\PhpWord\Element\ListItem) {
return;
}
$textObject = $element->getTextObject();
$xmlWriter->startElement('text:list');
$xmlWriter->writeAttribute('text:style-name', 'L1');
$xmlWriter->startElement('text:list-item');
$xmlWriter->startElement('text:p');
$xmlWriter->writeAttribute('text:style-name', 'P1');
$elementWriter = new Text($xmlWriter, $textObject, true);
$elementWriter->write();
$xmlWriter->endElement(); // text:list
$xmlWriter->endElement(); // text:p
$xmlWriter->endElement(); // text:list-item
}
}
The file has been adapted from the Word2007 version and fixes your first issue. Now lists will work on ODText.
Custom/Element/Table.php:
<?php
namespace PhpOffice\PhpWord\Element;
use PhpOffice\PhpWord\Style\Table as TableStyle;
/**
* Table element writer
*/
class Table extends AbstractElement
{
/**
* Table style
*
* #var \PhpOffice\PhpWord\Style\Table
*/
private $style;
/**
* Table rows
*
* #var \PhpOffice\PhpWord\Element\Row[]
*/
private $rows = array();
/**
* Table width
*
* #var int
*/
private $width = null;
/**
* Create a new table
*
* #param mixed $style
*/
public function __construct($style = null)
{
$this->style = $this->setNewStyle(new TableStyle(), $style);
}
/**
* Add a row
*
* #param int $height
* #param mixed $style
* #return \PhpOffice\PhpWord\Element\Row
*/
public function addRow($height = null, $style = null)
{
$row = new Row($height, $style);
$row->setParentContainer($this);
$this->rows[] = $row;
return $row;
}
/**
* Add a cell
*
* #param int $width
* #param mixed $style
* #return \PhpOffice\PhpWord\Element\Cell
*/
public function addCell($width = 1, $style = null)
{
$index = count($this->rows) - 1;
$row = $this->rows[$index];
$cell = $row->addCell($width, $style);
return $cell;
}
/**
* Get all rows
*
* #return \PhpOffice\PhpWord\Element\Row[]
*/
public function getRows()
{
return $this->rows;
}
/**
* Get table style
*
* #return \PhpOffice\PhpWord\Style\Table
*/
public function getStyle()
{
return $this->style;
}
/**
* Get table width
*
* #return int
*/
public function getWidth()
{
return $this->width;
}
/**
* Set table width.
*
* #param int $width
*/
public function setWidth($width)
{
$this->width = $width;
}
/**
* Get column count
*
* #return int
*/
public function countColumns()
{
$columnCount = 0;
$rowCount = count($this->rows);
for ($i = 0; $i < $rowCount; $i++) {
/** #var \PhpOffice\PhpWord\Element\Row $row Type hint */
$row = $this->rows[$i];
$cellCount = count($row->getCells());
if ($columnCount < $cellCount) {
$columnCount = $cellCount;
}
}
return $columnCount;
}
/**
* The first declared cell width for each column
*
* #return int[]
*/
public function findFirstDefinedCellWidths()
{
$cellWidths = array();
foreach ($this->rows as $row) {
$cells = $row->getCells();
if (count($cells) <= count($cellWidths)) {
continue;
}
$cellWidths = array();
foreach ($cells as $cell) {
$cellWidths[] = $cell->getWidth();
}
}
return $cellWidths;
}
}
As we are "overwriting" the file, we cannot reuse the original file extending it (at least, I don't know how). The only change here is on public function addCell($width = 1, $style = null) that makes always "1" as default value to the function. This fixes your second issue, and now you can call $table->addCell() without a value.
DEMO
require __DIR__ . '/vendor/autoload.php';
$document = new \PhpOffice\PhpWord\PhpWord();
$section = $document->addSection();
// This list **does** get rendered in the ODT file
$section->addListItem('Employee ID: 98765');
$section->addListItem('First Name: John');
$section->addListItem('Last Name: Doe');
$section->addListItem('SSN: 12345');
$table = $section->addTable();
$table->addRow();
$table->addCell()->addText('SSN');
$table->addCell()->addText('Email');
$table->addCell()->addText('Phone No');
$table->addRow();
$table->addCell()->addText('12345');
$table->addCell()->addText('jd#example.com');
$table->addCell()->addText('999 1234');
// Save the document as DOCX
$objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($document, 'Word2007');
$objWriter->save('document.docx');
// Save the document as ODT
$objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($document, 'ODText');
$objWriter->save('document.odt');
I am new at Drupal 7 and I'm creating a Block by code, following this tutorial.
So I create a new module folder at drupal/sites/all/modules and created two files:
block_square_menu.info: it has the info of the module:
name = Block Square Menu
description = Module that create a Block for Square menu, menu shown only in home page
core = 7.x
package = custom
block_square_menu.module: it contains the PHP code:
<?php
/**
* Implements hook_block_info().
*/
function block_square_block_info() {
$blocks = array();
$blocks['block_square'] = array(
'info' => t('Block Square'),
'cache' => DRUPAL_CACHE_PER_ROLE,
);
return $blocks;
}
/**
* Implements hook_block_view().
*/
function block_square_block_view($delta = '') {
$block = array();
switch ($delta) {
case 'block_square':
$block['subject'] = t('block Title');
$block['content'] = t('Hello World!');
break;
}
return $block;
}
After save the files, I go to Admin/Modules, I activate the new module and save the configuration. Now I go to Structure/Blocks and it should list my new Block, but it doesn't do.
I have followed all the tutorial steps and I cleaned Drupal cache, but I'm still having the problem.
First solve your mistake: change the function name where you implemented hook_block_view(), you need to change it as function blocks_square_block_view()
/**
* Implements hook_block_view().
*/
function blocks_square_block_view($delta = '') {
$block = array();
......
After also if not solve then remove 'cache' attribute from hook_block_info() it is optional.
Then follow 2 steps if you missed.
1) Clear all cache (/admin/config/development/performance).
2) Enable your custom module (/admin/modules).
After trying again, your block should appear in (/admin/structure/block).
Solved, the problem was the name of the functions. So the names started with "block_square" which it have the word "block" and it causes some trouble so I changed the all the names with menu_square.
So the functions are now:
menu_square_block_info()
menu_square_block_view($delta = '')
And the files are:
menu_square.info
menu_square.module
The code of the files are:
info:
name = Menu Square
description = Module that create a Block for Square menu, menu shown only in home page
core = 7.x
package = custom
module:
<?php
/**
* Implements hook_block_info().
*/
function menu_square_block_info() {
$blocks['menu_square'] = array(
'info' => t('Block Square'),
//'cache' => DRUPAL_CACHE_PER_ROLE,
);
return $blocks;
}
/**
* Implements hook_block_view().
*/
function menu_square_block_view($delta = '') {
$block = array();
switch ($delta) {
case 'menu_square':
$block['subject'] = t('block Title');
$block['content'] = t('Hello World!');
break;
}
return $block;
}
While looking into this question I came up with the following solution that is called from canDelete() in an extension to File:
protected function isFileInUse()
{
$owner = $this->getOwner();
$dataObjectSubClasses = ClassInfo::subclassesFor('DataObject');
$classesWithFileHasOne = [];
foreach ($dataObjectSubClasses as $subClass) {
$hasOnes = array_flip($subClass::create()->hasOne());
if (array_key_exists($owner->class, $hasOnes)) {
$classesWithFileHasOne[$subClass] = $hasOnes[$owner->class];
}
}
$threshold = (Director::get_current_page()->class == 'AssetAdmin') ? 1 : 2;
$uses = 0;
foreach ($classesWithFileHasOne as $class => $relation) {
$uses += count($class::get()->filter("{$relation}ID", $this->owner->ID));
if ($uses >= $threshold) {
return true;
}
}
return false;
}
There is one edge case I can't get around though. If, say, a featured image is changed on a blog post then if there is exactly one other use of the same image then with this approach it will still allow it to be deleted. This is because until the page is saved the current change doesn't count towards uses of the image.
The threshold is set differently in CMS Pages and the Media Manager to allow an image to be deleted from within the page that is using it.
Is there a way that I can access the containing page (or other element - we're using Elemental) from within my File extension to see if its associated image has changed?
This is the solution I eventually came up with. I'm not entirely happy with having to inspect the request but couldn't see any other solution:
public function canDelete($member = null)
{
return !$this->isFileInUse();
}
/**
* Check if the file is in use anywhere on the site
* #return bool True if the file is in use
*/
protected function isFileInUse()
{
$owner = $this->getOwner();
$dataObjectSubClasses = ClassInfo::subclassesFor('DataObject');
$classesWithFileHasOne = [];
foreach ($dataObjectSubClasses as $subClass) {
$hasOnes = array_flip($subClass::create()->hasOne());
if (array_key_exists($owner->class, $hasOnes)) {
$classesWithFileHasOne[$subClass] = $hasOnes[$owner->class];
}
}
$threshold = ($this->isAssetAdmin() || ($this->isFileAttach($classesWithFileHasOne))) ? 1 : 2;
$uses = 0;
foreach ($classesWithFileHasOne as $class => $relation) {
$uses += count($class::get()->filter("{$relation}ID", $this->owner->ID));
if ($uses >= $threshold) {
return true;
}
}
return false;
}
/**
* Are we in the asset manager rather than editing a Page or Element?
* #return bool
*/
protected function isAssetAdmin()
{
return 'AssetAdmin' === Director::get_current_page()->class;
}
/**
* Is the current action attaching a file to a field that we're interested in?
* #param array $classesWithFileHasOne Classes with a relationship we're interested in and the name of the
* relevant field
* #return bool
*/
protected function isFileAttach($classesWithFileHasOne)
{
$controller = Controller::curr();
$field = $controller->request->allParams()['FieldName'];
return (preg_match('/attach$/', $controller->requestParams['url']) &&
($controller->action == 'EditForm')
&& (in_array($field, array_values($classesWithFileHasOne))));
}
So, this is my first encounter with SuiteCRM or any other CRM for that matter. I need to query the db on a table that is not used by the CRM for our quote system. So, I have created the module using module builder and modified the module file so that it uses the correct table. The problem is that when the query runs, SuiteCRM still adds its default where clauses and adds the deleted = 0 condition to the query.
So, I tried using the method that is described on this SO page. That doesn't work as I get the an error that I am using an undefined variable (db) and that I am calling to a member function fetchByAssoc() on a non-object. Now, I am placing the code in the moduleName.php file. Maybe that is my issue. I don't know as I have never worked on any other CRM project. If anyone can point me in the right direction as to what I will need to do to be able to query a different table other than the default CRM table and then show the results from that query inside of a dashlet, your help will be greatly appreciated.
I got the errors fixed. They were my fault as I had not referenced the object.
So, as requested, here is some of my code. This is my php file.
<?php
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
require_once('include/Dashlets/Dashlet.php');
class FrtwQuotesDashlet extends Dashlet {
var $height = '200'; // height of the dashlet
var $quoteData = "";
/**
* Constructor
*
* #global string current language
* #param guid $id id for the current dashlet (assigned from Home module)
* #param array $def options saved for this dashlet
*/
function FrtwQuotesDashlet($id, $def) {
$this->loadLanguage('FrtwQuotesDashlet');
if(!empty($def['height'])) // set a default height if none is set
$this->height = $def['height'];
parent::Dashlet($id); // call parent constructor
$this->isConfigurable = true; // dashlet is configurable
$this->hasScript = false; // dashlet has javascript attached to it
// if no custom title, use default
if(empty($def['title'])) $this->title = $this->dashletStrings['LBL_TITLE'];
else $this->title = $def['title'];
}
/**
* Displays the dashlet
*
* #return string html to display dashlet
*/
function display() {
$sql = "SELECT QuoteNbr, crmname, ShipToCity FROM quotes.quotehdr LIMIT 10";
$result = $GLOBALS["db"]->query($sql);
$quoteData = "<table>";
while($quotes = $GLOBALS["db"]->fetchByAssoc($result)){
foreach ($quotes as $quote) {
$quoteData .="<tr><td>".$quote[0]."</td><td>".$quote[1]."</td><td>".$quote[2]."</td></tr>";
}
}
$ss = new Sugar_Smarty();
//assign variables
//$ss->assign('greeting', $this->dashletStrings['LBL_GREETING']);
$ss->assign('quoteData', $this->quoteData);
$ss->assign('height', $this->height);
$str = $ss->fetch('custom/modules/Home/FrtwQuotesDashlet/FrtwQuotesDashlet.tpl');
return parent::display().$str;
}
}
?>
The issue was with the foreach loop. I removed it and now it works fine. In analyzing the code, the while loop actually does the iterations needed. So, by adding the foreach, what was happening was that the code was iterating over each row returned from the db and then doing some weird stuff -- as in, it would only return a partial string of what each value should be. Since I am querying on 3 fields, it would also loop over each row 3 times, thereby creating 3 different rows from each row. So, for anyone with similar issue, this is how working code looks.
<?php
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
require_once('include/Dashlets/Dashlet.php');
class FrtwQuotesDashlet extends Dashlet {
var $height = '200'; // height of the dashlet
var $quoteData = "";
/**
* Constructor
*
* #global string current language
* #param guid $id id for the current dashlet (assigned from Home module)
* #param array $def options saved for this dashlet
*/
function FrtwQuotesDashlet($id, $def) {
$this->loadLanguage('FrtwQuotesDashlet');
if(!empty($def['height']))
$this->height = $def['height'];
parent::Dashlet($id);
$this->isConfigurable = true;
$this->hasScript = false;
// if no custom title, use default
if(empty($def['title'])) $this->title = $this->dashletStrings['LBL_TITLE'];
else $this->title = $def['title'];
}
/**
* Displays the dashlet
*
* #return string html to display dashlet
*/
function display() {
$sql = "SELECT QuoteNbr, revnbr, crmname, ShipToCity FROM quotes.quotehdr LIMIT 10";
$result = $GLOBALS["db"]->query($sql);
$this->quoteData = "Need headers here when we determine exact fields....";
while($quotes = $GLOBALS["db"]->fetchByAssoc($result)){
$this->quoteData .="<tr><td width = \"30%\">".$quotes["QuoteNbr"].' '.$quotes['revnbr']."</td><td width = \"30%\">".$quotes["crmname"]."</td><td width = \"30%\">".$quotes["ShipToCity"]."</td></tr>";
}
$ss = new Sugar_Smarty();
//assign variables
// $ss->assign('greeting', $this->dashletStrings['LBL_GREETING']);
$ss->assign('greeting', "This is the Greeting....");
$ss->assign('quoteData', $this->quoteData);
$ss->assign('height', $this->height);
$str = $ss->fetch('modules/Home/Dashlets/FrtwQuotesDashlet/FrtwQuotesDashlet.tpl');
return parent::display().$str; // return parent::display for title and such
}
}
?>
From what I understand, after reading documentation (especially scoring part), every field I add has the same level of importance when scoring searched results. I have following code:
protected static $_indexPath = 'tmp/search/indexes/projects';
public static function createSearchIndex()
{
$_index = new Zend_Search_Lucene(APPLICATION_PATH . self::$_indexPath, true);
$_projects_stmt = self::getProjectsStatement();
$_count = 0;
while ($row = $_projects_stmt->fetch()) {
$doc = new Zend_Search_Lucene_Document();
$doc->addField(Zend_Search_Lucene_Field::text('name', $row['name']));
$doc->addField(Zend_Search_Lucene_Field::text('description', $row['description']));
$doc->addField(Zend_Search_Lucene_Field::unIndexed('projectId', $row['id']));
$_index->addDocument($doc);
}
$_index->optimize();
$_index->commit();
}
The code is simple - I'm generating index, based on data fetched from db, and save it in the specified location.
I was looking in many places, as my desired behavior is that name field is more important than description (let's say 75% and 25%). So when I will search for some phrase, and it will be found in description of the first document, and in name of the second document, then second document will in fact have 3 times bigger score, and will show up higher on my list.
Is there any way to control scoring/ordering in this way?
I found it out basing on this documentation page. You need to create new Similarity algorithm class, and overwrite lengthNorm method. I copied this method from Default class, added $multiplier variable, and set it's value when needed (for a column I want):
class Zend_Search_Lucene_Search_Similarity_Projects extends Zend_Search_Lucene_Search_Similarity_Default
{
/**
* #param string $fieldName
* #param integer $numTerms
* #return float
*/
public function lengthNorm($fieldName, $numTerms)
{
if ($numTerms == 0) {
return 1E10;
}
$multiplier = 1;
if($fieldName == 'name') {
$multiplier = 3;
}
return 1.0/sqrt($numTerms / $multiplier);
}
}
Then the only thing you need to do (edit of code from question) is set your new Similarity algorithm class as a default method just before indexing:
protected static $_indexPath = 'tmp/search/indexes/projects';
public static function createSearchIndex()
{
Zend_Search_Lucene_Search_Similarity::setDefault(new Zend_Search_Lucene_Search_Similarity_Projects());
$_index = new Zend_Search_Lucene(APPLICATION_PATH . self::$_indexPath, true);
$_projects_stmt = self::getProjectsStatement();
$_count = 0;
while ($row = $_projects_stmt->fetch()) {
$doc = new Zend_Search_Lucene_Document();
$doc->addField(Zend_Search_Lucene_Field::text('name', $row['name']));
$doc->addField(Zend_Search_Lucene_Field::text('description', $row['description']));
$doc->addField(Zend_Search_Lucene_Field::unIndexed('projectId', $row['id']));
$_index->addDocument($doc);
}
$_index->optimize();
$_index->commit();
}
I wanted to extra boost name field, but you can do it with anyone.