<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Valentine</title>
    <description>The latest articles on Forem by Valentine (@vallka).</description>
    <link>https://forem.com/vallka</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F811628%2Fa477a594-e502-4687-b453-466c71c0663b.jpeg</url>
      <title>Forem: Valentine</title>
      <link>https://forem.com/vallka</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/vallka"/>
    <language>en</language>
    <item>
      <title>Google Analytics 4 (GA4) - querying raw data with SQL</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Sat, 11 Mar 2023 13:55:29 +0000</pubDate>
      <link>https://forem.com/vallka/google-analytics-4-ga4-querying-raw-data-with-sql-126a</link>
      <guid>https://forem.com/vallka/google-analytics-4-ga4-querying-raw-data-with-sql-126a</guid>
      <description>&lt;p&gt;Google Analytics 4 (GA4) is the newest version of Google Analytics, designed to provide businesses with a more comprehensive view of customer interactions across multiple platforms and devices. GA4 provides businesses with valuable insights that can help improve their online presence and increase conversions.&lt;/p&gt;

&lt;p&gt;While GA4 reports can be confusing for some, it is possible to query raw data using SQL to gain insights that are relevant to your business. In this article, we will discuss how to use GA4 and Google BigQuery to query raw data and gain valuable insights.&lt;/p&gt;

&lt;p&gt;Connecting Google Analytics 4 to Google BigQuery&lt;/p&gt;

&lt;p&gt;The first step in using GA4 to query raw data is to connect it to Google BigQuery. This can be done easily by following these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new BigQuery project or use an existing one.&lt;/li&gt;
&lt;li&gt;In the GA4 admin panel, select the Data Streams tab.&lt;/li&gt;
&lt;li&gt;Select the data stream that you want to export to BigQuery and click on the BigQuery Export tab.&lt;/li&gt;
&lt;li&gt;Click on the Link to BigQuery button and follow the prompts to link your GA4 account to your BigQuery project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once your GA4 account is linked to your BigQuery project, you can begin querying raw data using SQL.&lt;/p&gt;

&lt;p&gt;Querying Raw Data using SQL&lt;/p&gt;

&lt;p&gt;One of the most powerful features of GA4 is its ability to export raw data to BigQuery. This allows you to create custom queries using SQL and gain insights that are not available in GA4 reports.&lt;/p&gt;

&lt;p&gt;For example, if you want to know the details of users who made purchases during this year, you can use the following SQL query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; SELECT 
  event_date, 
  event_timestamp, 
  p.value.string_value AS order_id, 
  device.mobile_model_name, 
  device.operating_system_version, 
  traffic_source.name AS campaign, 
  traffic_source.medium, 
  traffic_source.source  
 FROM 
  `ga4-gellifique-uk.analytics_287630133.events_*`, 
  `ga4-gellifique-uk.analytics_287630133.events_*`.event_params p 
 WHERE 
  event_name='purchase' 
  AND key='transaction_id' 
  AND event_date&amp;gt;='20230101' 
 ORDER BY 
  event_timestamp 
 LIMIT 
  1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this SQL query, we are selecting the event_date, event_timestamp, transaction_id, mobile_model_name, operating_system_version, name, medium, and source from the raw data. We are also filtering the results to only show events with the event name "purchase" and a transaction ID, and only events that occurred after January 1st, 2023.&lt;/p&gt;

&lt;p&gt;Here is the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.vallka.com/media/markdownx/2023/03/10/33ffbe65-0041-4735-9a57-71eb36781ff6.png" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.vallka.com%2Fmedia%2Fmarkdownx%2F2023%2F03%2F10%2F33ffbe65-0041-4735-9a57-71eb36781ff6.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next SQL query provides an example of how to use Google Analytics 4 (GA4) and Google BigQuery to gain insights into the efficiency of your campaigns based on the number of purchases.&lt;/p&gt;

&lt;p&gt;In this query, we are first selecting the event_date, event_timestamp, transaction_id, mobile_model_name, operating_system_version, name, medium, and source from the raw data. We are also filtering the results to only show events with the event name "purchase" and a transaction ID, and only events that occurred after January 1st, 2023.&lt;/p&gt;

&lt;p&gt;Next, we are using a subquery to count the number of purchases for each campaign. We are grouping the results by campaign name and ordering the results by the count of purchases in descending order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT 
  name AS campaign, 
  COUNT(name) AS count 
FROM 
  (
    SELECT 
      event_date, 
      event_timestamp, 
      p.value.string_value AS transaction_id, 
      device.mobile_model_name, 
      device.operating_system_version, 
      traffic_source.name, 
      traffic_source.medium, 
      traffic_source.source 
    FROM 
      `ga4-gellifique-uk.analytics_287630133.events_*`, 
      `ga4-gellifique-uk.analytics_287630133.events_*`.event_params p 
    WHERE 
      event_name='purchase' 
      AND key='transaction_id' 
      AND event_date&amp;gt;='20230101' 
    ORDER BY 
      event_timestamp 
    LIMIT 
      1000
  )
GROUP BY 
  name 
ORDER BY 
  count(name) DESC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.vallka.com/media/markdownx/2023/03/10/3340a260-8719-429d-b2b0-699c5517c06a.png" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.vallka.com%2Fmedia%2Fmarkdownx%2F2023%2F03%2F10%2F3340a260-8719-429d-b2b0-699c5517c06a.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By using this query, you can gain insights into which campaigns are driving the most purchases and adjust your marketing strategies accordingly.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;While GA4 reports can be overwhelming, connecting GA4 to Google BigQuery and querying raw data using SQL can provide valuable insights for your business. By using custom queries, you can gain insights that are relevant to your business and help you make data-driven decisions.&lt;/p&gt;

</description>
      <category>google</category>
      <category>ga4</category>
      <category>sql</category>
    </item>
    <item>
      <title>textbanner - a module for Prestashop</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Wed, 22 Feb 2023 14:20:33 +0000</pubDate>
      <link>https://forem.com/vallka/textbanner-a-module-for-prestashop-228f</link>
      <guid>https://forem.com/vallka/textbanner-a-module-for-prestashop-228f</guid>
      <description>&lt;p&gt;This is a Prestashop module that displays a text banner on the store front-end instead of using an image banner. It is much easier and practical to use simple HTML text with predefined colors. The module was developed by Vallka and is released under the Academic Free License (AFL 3.0).&lt;/p&gt;

&lt;p&gt;The module extends the Module class and implements the WidgetInterface. It also has a private variable $write_log that can be used for logging.&lt;/p&gt;

&lt;p&gt;The constructor sets the basic properties of the module such as name, tab, version, author, etc. It also sets the display name and description of the module that appears on the back office. Additionally, the minimum and maximum versions of Prestashop with which the module is compatible are set.&lt;/p&gt;

&lt;p&gt;The install() method registers the displayBanner, actionObjectLanguageAddAfter, and backOfficeHeader hooks and installs fixtures. The fixtures are used to populate the TEXTBANNER_LINK and TEXTBANNER_DESC configuration variables. The backOfficeHeader() hook is used to add a JavaScript file to the back office, and the actionObjectLanguageAddAfter() hook is used to install the fixtures for each language.&lt;/p&gt;

&lt;p&gt;The uninstall() method removes the TEXTBANNER_LINK and TEXTBANNER_DESC configuration variables when the module is uninstalled.&lt;/p&gt;

&lt;p&gt;The postProcess() method is used to update the TEXTBANNER_LINK and TEXTBANNER_DESC configuration variables when the user saves the settings. The method updates the values for each language and clears the cache.&lt;/p&gt;

&lt;p&gt;The hookBackOfficeHeader() method in the textbanner module is responsible for showing all input fields for all enabled languages simultaneously in the back office. Specifically, it adds the back.js file to the back office header when the configure parameter is set to the name of the textbanner module.&lt;/p&gt;

&lt;p&gt;The purpose of back.js is to show all input fields for all enabled languages at once, on the same screen. This helps to remind the administrator to update all languages when making an update, and saves time by not having to switch between different languages to make the same change.&lt;/p&gt;

&lt;p&gt;To achieve this, back.js simply changes the visibility of all input fields for all enabled languages to 'show'. This feature improves the user experience for administrators by providing a more efficient and convenient way to manage input fields for multiple languages.&lt;/p&gt;

&lt;p&gt;Enjoy on &lt;a href="https://github.com/vallka/prestamodule_textbanner" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>welcome</category>
    </item>
    <item>
      <title>Social links for customers in Prestashop</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Tue, 21 Feb 2023 20:13:21 +0000</pubDate>
      <link>https://forem.com/vallka/social-links-for-customers-in-prestashop-46hc</link>
      <guid>https://forem.com/vallka/social-links-for-customers-in-prestashop-46hc</guid>
      <description>&lt;h2&gt;
  
  
  PrestaShop 1.7 Module
&lt;/h2&gt;

&lt;p&gt;This is a module for PrestaShop, an open-source eCommerce platform, that adds extra fields to the customer registration and account pages. The module adds fields for social media account information and preferred communication method.&lt;/p&gt;

&lt;p&gt;The module is licensed under the Academic Free License (AFL 3.0) and was developed by vallka. The module's version is 1.0.0, and it is compatible with PrestaShop 1.7 and above.&lt;/p&gt;

&lt;p&gt;The module installs a new database table named extra_customer_fields to store the additional information. The module also registers several hooks to integrate with the customer registration and account pages. The additionalCustomerFormFields hook adds the extra fields to the customer registration form. The validateCustomerFormFields hook validates the extra fields before saving them to the database. The actionObjectCustomerUpdateAfter and actionObjectCustomerAddAfter hooks update and add the extra fields to the database when a customer is updated or added. Finally, the actionCustomerFormBuilderModifier hook modifies the customer account form to include the extra fields.&lt;/p&gt;

&lt;p&gt;The module has two methods for reading and writing the extra field values: readModuleValues and writeModuleValues. The readModuleValues method reads the extra field values from the database for a given customer ID, and the writeModuleValues method writes the extra field values to the database for a given customer ID.&lt;/p&gt;

&lt;p&gt;Here is the link to &lt;a href="https://github.com/vallka/extracustomerfields" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; - feel free to use!&lt;/p&gt;

</description>
      <category>welcome</category>
      <category>community</category>
      <category>codenewbie</category>
    </item>
    <item>
      <title>Prestashop and ImageKit</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Fri, 11 Jun 2021 20:20:30 +0000</pubDate>
      <link>https://forem.com/vallka/prestashop-and-imagekit-8c8</link>
      <guid>https://forem.com/vallka/prestashop-and-imagekit-8c8</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FPvLqkGm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/06/09/e25ed155-fc6e-4d44-a691-7abeace6767d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FPvLqkGm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/06/09/e25ed155-fc6e-4d44-a691-7abeace6767d.png" alt="" width="615" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recently we found a really useful service - &lt;a href="https://imagekit.io/"&gt;imagekit.io&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Image CDN with automatic optimization, real-time transformation, and storage that you can integrate with existing setup in minutes.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What is the most amazing thing about this service, it has a free plan with very a generous set of features and allowances.&lt;/p&gt;

&lt;p&gt;Let's use it for a Prestashop theme.&lt;/p&gt;

&lt;p&gt;First of all, we need to create an account on &lt;a href="https://imagekit.io/"&gt;imagekit.io&lt;/a&gt;. After verifying your email, you'll be able to login to your account. At the very first step you will be asked for IMAGEKIT ID. By default this is a random string value. It make sense to change this random string to something meaningful, like the main portion of your website domain name. After setting all up, you will be unable to change your IMAGEKIT ID, so please do it right now.&lt;/p&gt;

&lt;p&gt;Next, the imagekit terminology becomes a somewhat confusing. We need to &lt;strong&gt;"Attach existing origin in ImageKit.io"&lt;/strong&gt;. Click on the Attach existing origin in ImageKit.io link and then click on &lt;strong&gt;ADD NEW ORIGIN&lt;/strong&gt; button. &lt;strong&gt;Origin Name&lt;/strong&gt; may be any string, for &lt;strong&gt;Origin Type&lt;/strong&gt; select &lt;em&gt;Web Folder - HTTP(S) server and Magento, Shopify, Wordpress, etc.&lt;/em&gt;. For &lt;strong&gt;Base URL&lt;/strong&gt; use &lt;strong&gt;&lt;em&gt;base&lt;/em&gt;&lt;/strong&gt; URL, like &lt;a href="https://www.example.com"&gt;https://www.example.com&lt;/a&gt; (without /images subfolder as it advise you). Leave the bottom checkboxes unchecked (unless you know what they are), and click Submit.&lt;/p&gt;

&lt;p&gt;What has confused me a little - where can you find this newly created "origin" after it has been set up? Under "URL endpoins" menu at the left. You will find  "Default URL endpoint" there. Click on it, here you can edit your "origins". You might need a new "origin", if you are using a multi-store feature of Prestashop. For example, we have 2 shops and 2 domains for these shops in our Prestashop - gellifique.co.uk for UK shop and gellifique.eu for Spanish shop. Both domains should be added to Imagekit Default URL endpoins as 2 separate "origins". One endpoint, two origins. This was somewhat confusing.&lt;/p&gt;

&lt;p&gt;Basically, all done. Now we can use ImageKit URLs instead of original URLs for images in your theme. I wanted to pinpoint all templates in Classic theme which needs to be updated, but as it happened, the Classic theme has changed a lot since we used it to create a custom theme for our webshop. In our case, I had to modify these files (under /themes/mytheme/ folder):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;templates/catalog/listing/category.tpl 
templates/catalog/_partials/product-cover-thumbnails.tpl 
templates/catalog/_partials/product-images-modal.tpl 
templates/catalog/_partials/miniatures/product.tpl 
modules/ps_imageslider/views/templates/hook/slider.tpl 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the latest version of the Classic theme some of these files became split into two or more files and some references may have moved somewhere else... So I'll show an example of what I did to my, probably  outdated version. Let's take this file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;templates/catalog/_partials/miniatures/product.tpl 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks like it didn't change much. Let's find a reference to a product image (lines 33-37):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img
  src="{$product.cover.bySize.home_default.url}"
  alt="{if !empty($product.cover.legend)}{$product.cover.legend}{else}{$product.name|truncate:30:'...'}{/if}"
  data-full-size-image-url="{$product.cover.large.url}"
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to change src attribute - with what? PHP parse_url() function will help us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src = "https://ik.imagekit.io/ourimagekitid{parse_url($product.cover.bySize.home_default.url, PHP_URL_PATH)}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where ourimagekitid is the IMAGEKIT ID we have set up earlier. If we know the exact size of the image here, we may specify it for the Imagekit, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src = "https://ik.imagekit.io/ourimagekitid/tr:w-250,h-250{parse_url($product.cover.bySize.home_default.url, PHP_URL_PATH)}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do similar exercises to remaining templates, and the task is completed! However...&lt;/p&gt;

&lt;p&gt;We had to hardcode IMAGEKIT ID into several files. Not good. Next issue - sometimes you may want to switch off ImageKit completely, for example in development environment. And the last one (at least for me) - although ImageKit gives us an ability to Purge cache, we have to specify it file by file, and even so it doesn't occure immediately. Sometimes you have to wait a few minutes before it have an effect... Would be nice to implement some feature like file versioning, like attaching ? to the end of the URL.&lt;/p&gt;

&lt;p&gt;So let's write another Prestashop module.&lt;/p&gt;

&lt;p&gt;Like before, we will create a module skeleton, using (&lt;a href="https://www.vallka.com/blog/prestashop-modules-programming-bcc-outgoing-emails/"&gt;see my first article&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://validator.prestashop.com/generator"&gt;https://validator.prestashop.com/generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will register two hooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function install()
{
    Configuration::updateValue('IMAGEKIT_LIVE_MODE', false);
    return parent::install() &amp;amp;&amp;amp;
            $this-&amp;gt;registerHook('actionDispatcher') &amp;amp;&amp;amp; 
            $this-&amp;gt;registerHook('backOfficeHeader');
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"backOfficeHeader" hook is needed only for attaching some js file to back office (not really important):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function hookBackOfficeHeader()
{
    if (Tools::getValue('configure') == $this-&amp;gt;name || Tools::getValue('module_name') == $this-&amp;gt;name) {
        $this-&amp;gt;context-&amp;gt;controller-&amp;gt;addJS($this-&amp;gt;_path.'views/js/back.js');
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the important one is hookActionDispatcher. It allows us to define a custom plugin for Smarty templates. This plugin can be used as a function in a Smarty template&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function hookActionDispatcher(){
    $this-&amp;gt;context-&amp;gt;smarty-&amp;gt;registerPlugin('function', 'imagekit', array('Imagekit', 'imagekitUrl'));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the function implementation construct the needed URL for ImageKit image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static function imagekitUrl($params,$smarty) {
    if ($params['url']) {
        if ( Configuration::get('IMAGEKIT_LIVE_MODE', false) &amp;amp;&amp;amp; Configuration::get('IMAGEKIT_ENDPOINT', '')) {
            $tr = $params['tr'] ? 'tr:'.$params['tr'] : '';
            $v = $params['v'] ? '?'.Configuration::get('IMAGEKIT_TS', '') : '';
              return Configuration::get('IMAGEKIT_ENDPOINT', '') . $tr . parse_url($params['url'],PHP_URL_PATH) . $v;
        }

        return $params['url'];
    }
    return '';
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It takes configuration parameters from the module configuration. IMAGEKIT_LIVE_MODE allows us to switch using ImageKit on/off with a single click. Parameters passed to the function are "url", "tr", and "v" (only "url" parameter is mandatory). "tr" set required transformation in ImageKit format, and v=1 (0 by default) append timestamp "?" querystring to the URL.&lt;/p&gt;

&lt;p&gt;So now the src attribute in templates/catalog/_partials/miniatures/product.tpl template will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src = "{imagekit tr='w-250,h-250' url=$product.cover.bySize.home_default.url}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What about "v" (versioning) parameter and why all the buzz? As I found out, the only place where it makes sense is templates/catalog/listing/category.tpl  template. This is the only place where a newer image is being saved by the same name as a previous one. If you upload a new image for a category with id 25, for example, the image URL will be /img/c/25.jpg (if you upload a jpg file). If you upload a new image, it still will have this name. Thus ImageKit will think it is the same image. You can Purge cache from ImageKit dashboard (providing you know the exact URL), wait some time and the change will happen... But for me it was easier to introduce "versioning". You have to go to my module configuration, double-click on Version (Timestamp) field, ans save configuration. And image URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/img/c/25.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;will become something like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/img/c/25.jpg?1623440107030

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and ImageKit will treat it as a new image.&lt;/p&gt;

&lt;p&gt;In all other templates Prestashop generates a unique image name, using either the actual file name, or some random values, and such problem doesn't raise.&lt;/p&gt;

&lt;p&gt;So in the file  templates/catalog/listing/category.tpl&lt;/p&gt;

&lt;p&gt;the line&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img class="replace-2x" 
    src="{$link-&amp;gt;getCatImageLink($subcategory.link_rewrite,$subcategory.id_image,'')}"
    alt="{$subcategory.name|escape:'html':'UTF-8'}"/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;became&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img class="replace-2x" 
    src="{imagekit tr='w-230,h-230' v=1 url=$link-&amp;gt;getCatImageLink($subcategory.link_rewrite,$subcategory.id_image,'')}"
    alt="{$subcategory.name|escape:'html':'UTF-8'}"/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I cannot find a similar line in the modern version of Classic theme. Maybe subcategories images are not shown at all in the modern Classic theme?&lt;/p&gt;

&lt;p&gt;Ok, everything is working now as I wanted it. Except for a small problem what I couldn't solve. Maybe some of the readers will point me to a right direction ?&lt;/p&gt;

&lt;p&gt;If, for some reason, I will unregister   or disable my module, all images will go! Because Smarty plugin "imagekit" doens't exist any more. I wanted to do something like this in my templates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img class="replace-2x" 
        {if function_exists("imagekitUrl")}
            src="{imagekit tr='w-230,h-230' v=1 url=$link-&amp;gt;getCatImageLink($subcategory.link_rewrite,$subcategory.id_image,'')}"
        {else}
            src="{$link-&amp;gt;getCatImageLink($subcategory.link_rewrite,$subcategory.id_image,'')}"
     {/if}
         /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but for some reason it doesn't seem to work. Anyone can help? (Maybe I had to use "modifer" instead of "function" in Smarty plugin, it would be easier than?)&lt;/p&gt;

&lt;p&gt;As usual, the source of the module is on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/vallka/prestamodule_imagekit.git"&gt;https://github.com/vallka/prestamodule_imagekit.git    &lt;/a&gt;&lt;/p&gt;

</description>
      <category>prestashopmodule</category>
      <category>prestashopdevelopmen</category>
      <category>imageoptimization</category>
      <category>imagekit</category>
    </item>
    <item>
      <title>Laybuy against Prestashop</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Sat, 22 May 2021 19:40:14 +0000</pubDate>
      <link>https://forem.com/vallka/laybuy-against-prestashop-193l</link>
      <guid>https://forem.com/vallka/laybuy-against-prestashop-193l</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9XYp5QVD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/05/22/09eaf1ca-38d2-4895-9cf2-ed02238f9ff5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9XYp5QVD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/05/22/09eaf1ca-38d2-4895-9cf2-ed02238f9ff5.png" alt="" width="680" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recently we subscribed to &lt;a href="https://www.laybuy.com/uk/for-merchants"&gt;Laybuy&lt;/a&gt; with our Prestashop-based eCommerce website. It provides Prestashop module out of a box. The module is easy to install and setup. However, immediately we have found a problem. In certain circumstances Laybuy module reported an error - totals didn't sum up:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fpK6dmQ9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/05/22/395b4707-a074-4093-8b51-bb3f10c5eb6a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fpK6dmQ9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/05/22/395b4707-a074-4093-8b51-bb3f10c5eb6a.png" alt="" width="680" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is the problem? Enabling a debug mode in Laybuy module (thank you developers!) shows the source of the problem: Laybuy adds a delivery cost - which should be 0 for free shipping to the total, so total totals become different. Why this is happening? It looks like there is something wrong in Prestashop itself, not in the Laybuy module.&lt;/p&gt;

&lt;p&gt;Even looking at the screenshot above one can notice something not quite right: there is a subtotal of £56.00, plus a discount of £6.95, plus free shipping, so the final total should be shown as £49.05. However, it is shown as £56.00. This is because the shown discount £6.95 as actually is a discount for free shipping - normal shipping cost is £6.95, so adding a discount of £6.95 gives customer a free shipping. But looking at the given pictures it is difficult to realize. (However, we didn't get any complaints from our customers.) I think the line "Discount(s) - £6.95" shouldn't be shown at all, or have some comment. Where is the error, in our Prestashop theme (highly customized), or in the &lt;a href="https://addons.prestashop.com/en/promotions-gifts/9129-promotions-and-discounts-3x2-sales-offers-packs.html?_ga=2.86574555.2145252126.1621709716-1707616425.1616341995"&gt;Promotions and discounts module&lt;/a&gt;, which we use? Or in the core Prestashop code? I don't know.&lt;/p&gt;

&lt;p&gt;Interesting, the situation with the Laybuy module is exactly the opposite. Laybuy gets order totals, using core Prestashop classes, and somehow gets £6.95 as a shipping cost. Which should be 0 as Free shipping discount applies to orders over £50. This discount is created using Promotions and discounts module.&lt;br&gt;
Where is the error? Most probably in the same place as the first error with showing discount to the customer, that is in Prestashop itself. I have a look at the Prestashop code… well, it doesn't seem possible to find and fix. I can only make things worse. So instead I decided to "fix" the Laybuy module, adding a simple patch to its code. If totals are not equal - make them equal by subtracting the shipping cost. Easy-peasy!&lt;/p&gt;

&lt;p&gt;Where is the error? Most probably in the same place as the first error with showing discount to customer, that is in Prestashop itself. I have a look at the Prestashop code... well, it isn't seems possible to find and fix. I can only make things worse. So instead I decided to "fix" the Laybuy module, adding a simple patch to its code. If totals are not equal - make them equal by subtract the shipping cost. Easy-peasy!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
    * Process the Order Items
    *
    * @return array
    * since 1.0.0
    */
    private function _processItems() {

        $tax = floatval($this-&amp;gt;cart-&amp;gt;getOrderTotal() - $this-&amp;gt;cart-&amp;gt;getOrderTotal(false));

        // items total
        $shipping_total = $this-&amp;gt;cart-&amp;gt;getOrderTotal(false, Cart::ONLY_SHIPPING);

        //vallka:
        $items_price = $this-&amp;gt;cart-&amp;gt;getOrderTotal(false, Cart::BOTH_WITHOUT_SHIPPING);
        $amount = $this-&amp;gt;cart-&amp;gt;getOrderTotal();

        if (Laybuy::$debug) {
            PrestaShopLogger::addLog("laybuy-vallka:$amount,$items_price,$tax,$shipping_total", 3, NULL, "Laybuy", 1);
        }

        if ($items_price+$tax == $amount) {
            $shipping_total = 0;
            PrestaShopLogger::addLog("laybuy-vallka-corrected!:$amount,$items_price,$tax,$shipping_total", 3, NULL, "Laybuy", 1);
        }
        //vallka end


        // shipping
        if ($shipping_total &amp;gt; 0) {
            $items[] = array(
                'id' =&amp;gt; 'shipping_fee#' . $this-&amp;gt;cart-&amp;gt;id,
                'description' =&amp;gt; 'Shipping fee',
                'quantity' =&amp;gt; '1',
                'price' =&amp;gt;  $shipping_total
            );
        }

        // tax
        if ($tax) {
            $items[] = array(
                'id' =&amp;gt; 'total_tax_amount_for_order#' . $this-&amp;gt;cart-&amp;gt;id,
                'description' =&amp;gt; 'Tax amount for this order',
                'quantity' =&amp;gt; '1',
                'price' =&amp;gt; $tax
            );
        }

        $items[] = [
            'id'          =&amp;gt; 'item_for_order___#' . $this-&amp;gt;cart-&amp;gt;id,
            'description' =&amp;gt; 'Items',
            'quantity'    =&amp;gt; 1,
            'price'       =&amp;gt; $this-&amp;gt;cart-&amp;gt;getOrderTotal(false, Cart::BOTH_WITHOUT_SHIPPING)
        ];

        return $items;
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it does work!&lt;/p&gt;

&lt;p&gt;What Cart::BOTH_WITHOUT_SHIPPING parameter value for getOrderTotal method is, I can only guess… Well, I cannot even guess what meaning Prestashop developers wanted to give to it….&lt;/p&gt;

&lt;p&gt;My fixed version of Laybuy module is here: &lt;a href="https://github.com/vallka/prestashop-laybuy"&gt;https://github.com/vallka/prestashop-laybuy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I will make a pull request to the original repository to let them know. Maybe my fix isn't 100% correct. Or it can be done in a more simple way. I also hope the Prestashop developers will notice this problem and at least comment my post. But for a time being the fix is working and we can use Laybuy / Prestashop / Promotions and discounts module together without errors.&lt;/p&gt;

</description>
      <category>php</category>
      <category>prestashopdevelopmen</category>
      <category>laybuy</category>
      <category>prestashopmodule</category>
    </item>
    <item>
      <title>Prestashop Module — Enhanced Product List in Admin</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Wed, 05 May 2021 20:18:36 +0000</pubDate>
      <link>https://forem.com/vallka/prestashop-module-enhanced-product-list-in-admin-3a48</link>
      <guid>https://forem.com/vallka/prestashop-module-enhanced-product-list-in-admin-3a48</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dqdJEWc8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/04/25/ff2dcc50-ee0a-4006-b247-e83833674a50.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dqdJEWc8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/04/25/ff2dcc50-ee0a-4006-b247-e83833674a50.png" alt="" width="680" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Today we are returning to Prestashop modules programming. What game shall we play today?&lt;/p&gt;

&lt;p&gt;There are a number of modules which allows us to customize catalog view - in the front end. But what if we want to customize the back office? Namely, we are missing some columns in Product List view. It would be nice to see all our EAN13 codes in the list, as well as such values like Cost Price and Weight - these values are often set being set incorrectly by users and usually it is difficult to trace. Only when you trying to post an order which surprisingly weights 0 grams and costs 0 pence to ship you are starting to realize that some values are missing in the product description.&lt;/p&gt;

&lt;p&gt;We have all the means to add columns. First of all, there is a hook &lt;a href="https://devdocs.prestashop.com/1.7/modules/concepts/hooks/list-of-hooks/"&gt;actionAdminProductsListingFieldsModifier&lt;/a&gt; - and it does what the name suggests - it allows to modify fields in the select which is used to build the product list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;actionAdminProductsListingFieldsModifier
Located in: /src/Adapter/Product/AdminProductDataProvider.php
Parameters:

&amp;lt;?php
array(
  '_ps_version' =&amp;gt; (string) PrestaShop version,
  'sql_select' =&amp;gt; &amp;amp;(array),
  'sql_table' =&amp;gt; &amp;amp;(array),
  'sql_where' =&amp;gt; &amp;amp;(array),
  'sql_order' =&amp;gt; &amp;amp;(array),
  'sql_limit' =&amp;gt; &amp;amp;(string),
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(It always helps to keep a copy of full Prestashop sources at hand. Looking at source file often helps to get the idea. In my case first came the discovery that this hook is &lt;a href="https://github.com/PrestaShop/PrestaShop/issues/14083"&gt;called twice, most probably by mistake&lt;/a&gt;. Ok, mistake or not, now we are prepared to deal with this fact.)&lt;/p&gt;

&lt;p&gt;Let's create a module skeleton, as we already did in &lt;a href="https://www.vallka.com/blog/prestashop-modules-programming-bcc-outgoing-emails/"&gt;the previous article&lt;/a&gt;. Go here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://validator.prestashop.com/generator"&gt;https://validator.prestashop.com/generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I couldn't find the needed hook in the dropdown, so we'll add it later.&lt;/p&gt;

&lt;p&gt;After downloading and unzipping our brand new module, let's add code. Let's add the needed hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function install()
{
        Configuration::updateValue('ADMINEXTRAINFO_LIVE_MODE', false);

        return parent::install() &amp;amp;&amp;amp;
            $this-&amp;gt;registerHook('backOfficeHeader') &amp;amp;&amp;amp;
            $this-&amp;gt;registerHook('actionAdminProductsListingFieldsModifier');  // added line
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in the install method, and the hook implementation at the bottom of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function hookActionAdminProductsListingFieldsModifier($param)
{
        /* Place your code here. */
        print('***==hookActionAdminProductsListingFieldsModifier==***');
        var_dump($param);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;just to make sure it works. &lt;/p&gt;

&lt;p&gt;And it does. This is what we immediately see in the page source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;***==hookActionAdminProductsListingFieldsModifier==***array(10) {
  ["_ps_version"]=&amp;gt;
  string(7) "1.7.7.3"
  ["sql_select"]=&amp;gt;
  &amp;amp;array(16) {
    ["id_product"]=&amp;gt;
    array(3) {
      ["table"]=&amp;gt;
      string(1) "p"
      ["field"]=&amp;gt;
      string(10) "id_product"
      ["filtering"]=&amp;gt;
      string(4) " %s "
    }
    ["reference"]=&amp;gt;
    array(3) {
      ["table"]=&amp;gt;
      string(1) "p"
      ["field"]=&amp;gt;
      string(9) "reference"
      ["filtering"]=&amp;gt;
      string(13) "LIKE '%%%s%%'"
    }
    ["price"]=&amp;gt;
    array(3) {
      ["table"]=&amp;gt;
      string(2) "sa"
      ["field"]=&amp;gt;
      string(5) "price"
      ["filtering"]=&amp;gt;
      string(4) " %s "
    }
        ...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scrolling down, we see that this is repeated twice - exactly what we expect from examining the code in AdminProductDataProvider.php. Ok, but the content is self-explaining. We only need to add new fields to the param in the same format. A very simple task, knowing that the fields we need are located in the same tables... And do not forget to prevent double execution. &lt;/p&gt;

&lt;p&gt;This is what we will put in the hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function hookActionAdminProductsListingFieldsModifier($param)
{
        /* Place your code here. */
        if (!$param['sql_select']['ean13']) {
                $param['sql_select']['ean13'] = array();
                $param['sql_select']['ean13']["table"] = "p";
                $param['sql_select']['ean13']["field"] = "ean13";
                $param['sql_select']['ean13']["filtering"] = "LIKE '%%%s%%'";
        }

        if (!$param['sql_select']['weight']) {
                $param['sql_select']['weight'] = array();
                $param['sql_select']['weight']["table"] = "p";
                $param['sql_select']['weight']["field"] = "weight";
                $param['sql_select']['weight']["filtering"] = ' %s ';
        }

        if (!$param['sql_select']['wholesale_price']) {
                $param['sql_select']['wholesale_price'] = array();
                $param['sql_select']['wholesale_price']["table"] = "sa";
                $param['sql_select']['wholesale_price']["field"] = "wholesale_price";
                $param['sql_select']['wholesale_price']["filtering"] = ' %s ';
        }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But obviously this is not enough. We need to display these new fields. Luckily for us, if we search Prestashop Module Development documentation, there is a page which refers &lt;a href="https://devdocs.prestashop.com/1.7/modules/concepts/templating/admin-views/"&gt;exactly to our case&lt;/a&gt;! We need to add a subtree to module's views subfolder like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├───sql
├───translations
├───upgrade
└───views
    ├───css
    ├───img
    ├───js
    ├───PrestaShop
    │   └───Admin
    │       └───Product
    │           └───CatalogPage
    │               └───Lists
    └───templates
        └───admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the content of this subdir will be taken from this subfolder of Prestashop sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prestashop_1.7.7.3/src/PrestaShopBundle/Resources/views/Admin 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initially we will override only one file,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/src/PrestaShopBundle/Resources/views/Admin/Product/CatalogPage/Lists/list.html.twig 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I never heard of .twig templates before, but what I see here looks exactly like Django templates or Jinja templates. Looks like all templating languages uses the same syntax... Good for us! (But need to notice that this syntax is slightly different from Smarty used in the front end and older parts of the back end.) &lt;/p&gt;

&lt;p&gt;We see that products are displayed inside an HTML table and list.html.twig represents a tbody row. For simplicity, lets add EAN13 field in the same column as the product id, to keep thead unchanged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; &amp;lt;td&amp;gt;
        &amp;lt;label class="form-check-label" for="bulk_action_selected_products-{{ product.id_product }}"&amp;gt;
                {{ product.id_product }}
        &amp;lt;/label&amp;gt;
        &amp;lt;br/&amp;gt;
        &amp;lt;a href="{{ product.url|default('') }}#tab-step6"&amp;gt;{{ product.ean13 }}&amp;lt;/a&amp;gt;
    &amp;lt;/td&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I copied the idea from some other field. #tab-step6 in the link refers to the tab "options" in product description, where EAN13 is being set. The same way, let's put "weight" field under product category and wholesale price under price:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;td&amp;gt;
        {{ product.name_category|default('') }}
        &amp;lt;br&amp;gt;
        &amp;lt;a href="{{ product.url|default('') }}#tab-step4"&amp;gt;{{ product.weight|round(2) }} kg&amp;lt;/a&amp;gt;
&amp;lt;/td&amp;gt;
&amp;lt;td class="text-center"&amp;gt;
        &amp;lt;a href="{{ product.url|default('') }}#tab-step2"&amp;gt;{{ product.price|default('N/A'|trans({}, 'Admin.Global')) }}&amp;lt;/a&amp;gt;
        &amp;lt;br&amp;gt;
        &amp;lt;a href="{{ product.url|default('') }}#tab-step2"&amp;gt;{{ product.wholesale_price|round(2) }}&amp;lt;/a&amp;gt;
&amp;lt;/td&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to use round(2) filter to give our numbers a good look, otherwise all 8 or so decimal digits will be displayed. Why it is not needed for product.price? Because it is done inside a php code, together with prepending (or appending) a currency sign. We would want to do the same with the wholesale price, but for simplicity let's live it as is for now. Let's also skip internationalization for 'kg' word.&lt;/p&gt;

&lt;p&gt;To change the table header, we need to override, well, products_table.html.twig template. And what I have found, trying to implement filtering and sorting by our new fields - it doesn't work. Although we have defined, and hopefully correctly, all needed attributes for the new fields to be used in filtering and sorting, we will also need to modify some javascript somewhere. Filtering and sorting are performed by ajax call, and additional programming is required. We need to teach some javascript to deal with the new fields. Let's leave it for the next exercise. (And for my client's purpose this unfinished solution is already a big help.)&lt;/p&gt;

&lt;p&gt;So below is my version of  products_table.html.twig:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% block product_catalog_form_table_header %}
    &amp;lt;tr class="column-headers"&amp;gt;
        &amp;lt;th scope="col" style="width: 2rem"&amp;gt;&amp;lt;/th&amp;gt;
        &amp;lt;th scope="col" style="width: 6%"&amp;gt;
            {{ ps.sortable_column_header("ID"|trans({}, 'Admin.Global'), 'id_product', orderBy, sortOrder) }}
        &amp;lt;/th&amp;gt;
        &amp;lt;th scope="col"&amp;gt;
            {{ "Image"|trans({}, 'Admin.Global') }}
        &amp;lt;/th&amp;gt;
        &amp;lt;th scope="col"&amp;gt;
            {{ ps.sortable_column_header("Name"|trans({}, 'Admin.Global'), 'name', orderBy, sortOrder) }}
        &amp;lt;/th&amp;gt;
        &amp;lt;th scope="col" style="width: 9%"&amp;gt;
            {{ ps.sortable_column_header("Reference"|trans({}, 'Admin.Global'), 'reference', orderBy, sortOrder) }}
        &amp;lt;/th&amp;gt;
        &amp;lt;th scope="col"&amp;gt;
            {{ ps.sortable_column_header("Category"|trans({}, 'Admin.Catalog.Feature'), 'name_category', orderBy, sortOrder) }}
        &amp;lt;/th&amp;gt;
        &amp;lt;th scope="col" class="text-center" style="width: 6%"&amp;gt;
            {{ ps.sortable_column_header("Price ex VAT"|trans({}, 'Admin.Catalog.Feature'), 'price', orderBy, sortOrder) }}
        &amp;lt;/th&amp;gt;
        &amp;lt;th scope="col" class="text-center" style="width: 6%"&amp;gt;
            {{ "Price VAT"|trans({}, 'Admin.Catalog.Feature') }}
        &amp;lt;/th&amp;gt;

        {% if 'PS_STOCK_MANAGEMENT'|configuration %}
        &amp;lt;th scope="col" class="text-center" style="width: 6%"&amp;gt;
            {{ ps.sortable_column_header("Quant"|trans({}, 'Admin.Catalog.Feature'), 'sav_quantity', orderBy, sortOrder) }}
        &amp;lt;/th&amp;gt;
        {% else %}
            &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;
        {% endif %}

        &amp;lt;th scope="col" class="text-center"&amp;gt;
            {{ ps.sortable_column_header("Status"|trans({}, 'Admin.Global'), 'active', orderBy, sortOrder) }}
        &amp;lt;/th&amp;gt;
        {% if has_category_filter == true %}
            &amp;lt;th scope="col"&amp;gt;
                {{ ps.sortable_column_header("Pos"|trans({}, 'Admin.Global'), 'position', orderBy, sortOrder) }}
            &amp;lt;/th&amp;gt;
        {% endif %}
        &amp;lt;th scope="col" class="text-right" style="width: 3rem; padding-right: 2rem"&amp;gt;
                {{ "Actions"|trans({}, 'Admin.Global') }}
        &amp;lt;/th&amp;gt;
    &amp;lt;/tr&amp;gt;
    &amp;lt;tr class="column-headers"&amp;gt;
        &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;
            {{ "EAN13"|trans({}, 'Admin.Catalog.Feature') }}
        &amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;
            {{ "Weight"|trans({}, 'Admin.Global') }}
        &amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;
            {{ "Cost Price"|trans({}, 'Admin.Catalog.Feature') }}
        &amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;
        {% if has_category_filter == true %}
            &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;
        {% endif %}
        &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;
    &amp;lt;/tr&amp;gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a Github repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/vallka/prestamodule_adminextrainfo"&gt;https://github.com/vallka/prestamodule_adminextrainfo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To be continued...&lt;/p&gt;

</description>
      <category>prestashopmodule</category>
      <category>php</category>
      <category>prestashopdevelopmen</category>
    </item>
    <item>
      <title>Pandas in the Pandaemic. (COVID-19 in Scotland — statistics) Part 3</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Wed, 14 Apr 2021 22:05:35 +0000</pubDate>
      <link>https://forem.com/vallka/pandas-in-the-pandaemic-covid-19-in-scotland-statistics-part-3-5277</link>
      <guid>https://forem.com/vallka/pandas-in-the-pandaemic-covid-19-in-scotland-statistics-part-3-5277</guid>
      <description>&lt;p&gt;&lt;a href="https://vallka.medium.com/pandas-in-the-pandaemic-covid-19-in-scotland-statistics-part-3-88ff9f9c5f6f?source=rss-b12a2c268778------2"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4U9C0su4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/680/0%2AG-jeuXG53U-NkHQS.png" alt="" width="680" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the 3rd part of an exploration of pandas package.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vallka.medium.com/pandas-in-the-pandaemic-covid-19-in-scotland-statistics-part-3-88ff9f9c5f6f?source=rss-b12a2c268778------2"&gt;Continue reading on Medium »&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;UPD (2022-02-08):&lt;br&gt;
The URL of myCovidash has changed to&lt;br&gt;
&lt;a href="https://www.vallka.com/mycovidash/"&gt;https://www.vallka.com/mycovidash/&lt;/a&gt;&lt;br&gt;
and it uses Django now instead of Flask. But still working fine for nearly a year without serious adjustments. And death rate is currently lower then ever indeed...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dailysceptic.org/2022/02/08/deaths-9-below-average-when-will-the-government-declare-the-pandemic-over/"&gt;https://dailysceptic.org/2022/02/08/deaths-9-below-average-when-will-the-government-declare-the-pandemic-over/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The graph on the above link shows stats in England and Wales, mine is for Scotland. Similar figures... Looks like the end is near?&lt;/p&gt;

</description>
      <category>plotly</category>
      <category>flask</category>
      <category>covid19</category>
      <category>pandas</category>
    </item>
    <item>
      <title>Pandas in the Pandaemic. (COVID-19 in — Scotland statistics) Part 2</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Wed, 31 Mar 2021 21:38:34 +0000</pubDate>
      <link>https://forem.com/vallka/pandas-in-the-pandaemic-covid-19-in-scotland-statistics-part-2-l94</link>
      <guid>https://forem.com/vallka/pandas-in-the-pandaemic-covid-19-in-scotland-statistics-part-2-l94</guid>
      <description>&lt;p&gt;&lt;a href="https://vallka.medium.com/pandas-in-the-pandaemic-covid-19-in-scotland-statistics-part-2-c8d41c94fb07?source=rss-b12a2c268778------2"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Q-95PFt7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/680/0%2ABdAoVR-thPf14MjX" alt="" width="680" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the 2nd part of the exploration of pandas package. Part 1 can be found here…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vallka.medium.com/pandas-in-the-pandaemic-covid-19-in-scotland-statistics-part-2-c8d41c94fb07?source=rss-b12a2c268778------2"&gt;Continue reading on Medium »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pandas</category>
      <category>python</category>
      <category>covid19</category>
      <category>datavisualization</category>
    </item>
    <item>
      <title>Leaning pandas. Data manipulation and visualization. (COVID-19 in — Scotland statistics)</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Sun, 14 Mar 2021 20:26:16 +0000</pubDate>
      <link>https://forem.com/vallka/leaning-pandas-data-manipulation-and-visualization-covid-19-in-scotland-statistics-2e1f</link>
      <guid>https://forem.com/vallka/leaning-pandas-data-manipulation-and-visualization-covid-19-in-scotland-statistics-2e1f</guid>
      <description>&lt;p&gt;&lt;a href="https://vallka.medium.com/leaning-pandas-data-manipulation-and-visualization-covid-19-in-scotland-statistics-c3bf2dbbf199?source=rss-b12a2c268778------2"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6_UCoCLC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/625/0%2ADXxNHGVWmUsW8Vmr.jpg" alt="" width="625" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s do a little bit of pandas. Pandas is (or are? :) extremely popular. Let’s just dive in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vallka.medium.com/leaning-pandas-data-manipulation-and-visualization-covid-19-in-scotland-statistics-c3bf2dbbf199?source=rss-b12a2c268778------2"&gt;Continue reading on Medium »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pandasdataframe</category>
      <category>covid19</category>
      <category>pandas</category>
      <category>python</category>
    </item>
    <item>
      <title>Prestashop Modules Programming. Bcc outgoing emails</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Sun, 28 Feb 2021 19:50:40 +0000</pubDate>
      <link>https://forem.com/vallka/prestashop-modules-programming-bcc-outgoing-emails-kb5</link>
      <guid>https://forem.com/vallka/prestashop-modules-programming-bcc-outgoing-emails-kb5</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aqwM5yoH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/050cdd89-136b-4419-8fd6-2af3f7fd2a4f.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aqwM5yoH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/050cdd89-136b-4419-8fd6-2af3f7fd2a4f.jpg" alt="" width="680" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the idea?
&lt;/h2&gt;

&lt;p&gt;Let's start programming Prestashop modules. Where to start from? Here is the documentation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devdocs.prestashop.com/1.7/modules/"&gt;https://devdocs.prestashop.com/1.7/modules/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ok. But what would be our first module? It should be very simple, and yet it should be useful. Here is the idea: add BCC to all outgoing emails, that we will have copies of all outgoing emails in out mailbox. Why would we want it? Actually this is what many people want, just try to google. For newly set up website it may be useful. You may want to see how actually the emails look like. And find all versions of emails which Prestashop sends to your customers. Probably after looking to these emails you would want to change them a bit, maybe remove a default "Powered by Prestashop" line at the bottom... You may want to keep monitoring outgoing emails to ensure that next update of Prestashop do not revert your custom emails to default ones (you probably forgot to check "Keep email templates" while updating Prestashop). Ok, let's do it.&lt;/p&gt;

&lt;p&gt;Among Prestashop hooks there is one which can be used for our purpose: actionEmailSendBefore&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;actionEmailSendBefore

Before sending an email This hook is used to filter the content 
or the metadata of an email before sending it or even prevent its sending

Located in: /classes/Mail.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://devdocs.prestashop.com/1.7/modules/concepts/hooks/list-of-hooks/"&gt;https://devdocs.prestashop.com/1.7/modules/concepts/hooks/list-of-hooks/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we have a chance to look inside /classes/Mail.php file, we will see that all the parameters are prefixed with "&amp;amp;", what means we can change them:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$hookBeforeEmailResult = Hook::exec(
        'actionEmailSendBefore',
        [
            'idLang' =&amp;gt; &amp;amp;$idLang,
            'template' =&amp;gt; &amp;amp;$template,
            'subject' =&amp;gt; &amp;amp;$subject,
            'templateVars' =&amp;gt; &amp;amp;$templateVars,
            'to' =&amp;gt; &amp;amp;$to,
            'toName' =&amp;gt; &amp;amp;$toName,
            'from' =&amp;gt; &amp;amp;$from,
            'fromName' =&amp;gt; &amp;amp;$fromName,
            'fileAttachment' =&amp;gt; &amp;amp;$fileAttachment,
            'mode_smtp' =&amp;gt; &amp;amp;$mode_smtp,
            'templatePath' =&amp;gt; &amp;amp;$templatePath,
            'die' =&amp;gt; &amp;amp;$die,
            'idShop' =&amp;gt; &amp;amp;$idShop,
            'bcc' =&amp;gt; &amp;amp;$bcc,
            'replyTo' =&amp;gt; &amp;amp;$replyTo,
        ],
        null,
        true
    );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;I couldn't find any place in Prestashop Back Office which could potentially use BCC. So we can assume (for simplicity) it is always empty and we can just set it to our own value in the hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's create a module!
&lt;/h2&gt;

&lt;p&gt;Luckily, there is a simple way to create a module skeleton. There is a module generator supplied by Prestashop itself:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://validator.prestashop.com/generator"&gt;https://validator.prestashop.com/generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wouldn't say it works perfectly but it works. Probably, it is not updated as ofter as new Prestashop versions are released, and contains some bugs. Also, as mentioned in the documentation, if you want to create a Payments module for Prestashop 1.7 you should not use this generator. But for our purpose it as just invaluable.&lt;/p&gt;

&lt;p&gt;So put some values to start a module - name, version, author name etc.:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dd9pvyH1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/b8d45a88-54e5-4c02-a242-85f58149b557.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dd9pvyH1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/b8d45a88-54e5-4c02-a242-85f58149b557.PNG" alt="" width="630" height="1050"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On next page let's select the the following - Yes to Confirm uninstall, No to Create default database table and Need instance. Compliancy min-max - 1.7.x (we do not bother about older versions).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2WZvkm6_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/9166e870-8bf9-4645-ae87-311a324ed829.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2WZvkm6_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/9166e870-8bf9-4645-ae87-311a324ed829.PNG" alt="" width="680" height="873"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, we are asked about the hooks we want to use. Unfortunately, the generator seems not to know about the hook we are after:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--b437fGXL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/88c962db-d5ca-493c-b7a5-5285d9323ba2.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b437fGXL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/88c962db-d5ca-493c-b7a5-5285d9323ba2.PNG" alt="" width="680" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ok, let's just select a random one, we will change the code later:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--n4-Mq8Va--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/922f4eed-895c-4ed2-930c-63ddda07de90.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--n4-Mq8Va--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/922f4eed-895c-4ed2-930c-63ddda07de90.PNG" alt="" width="680" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on Create! button - the generator will generate bcc_outgoing_emails.zip file. This is how we named our module.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is inside?
&lt;/h2&gt;

&lt;p&gt;Inside the zip file there is a directory with the same name -  bcc_outgoing_emails. And inside this directory:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2Hf5F4kc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/d76a4752-021d-4706-9ddd-ecfb38bee406.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2Hf5F4kc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/27/d76a4752-021d-4706-9ddd-ecfb38bee406.PNG" alt="" width="672" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Actually, we don't need most of these files. But it is interesting to look inside and see what is generated.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bcc_outgoing_emails.php 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This is the main ,php file. By default, Prestashop puts everything in this file. I would rather separate install/uninstall code, admin actions, hooks into different   .php files. But for our very simple example the default behavior is good enough.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/sql/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We have actually selected No to Create default database table option, so this directory shouldn't be here at all. We don't need any sql content, so we can simply delete this directory.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/translations/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Ok, let's keeps it. We may want to use it to translate Back Office form. We won't do it now, but it good to know that we have this ability by default.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/upgrade/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Let's keep it as is.     &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/views/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Under /views/ directory Prestashop module keeps all templates, images, javascripts and css files. For both front end and back end. Definitely we don't need any front end files, and hardly we need any styling or images for back end. Or javascript. But it is interesting to see how all these files are going to be used in case they were needed.&lt;/p&gt;

&lt;p&gt;Let's look inside the main file, bcc_outgoing_emails.php. First what we can notice - the generator correctly set author and version info in php class, but left &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*  @author    PrestaShop SA &amp;lt;contact@prestashop.com&amp;gt;
*  @copyright 2007-2021 PrestaShop SA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;in the top file comment. We may want to change the comment :)&lt;/p&gt;

&lt;p&gt;Ok. More important things. We have chosen (randomly) &lt;strong&gt;actionAdminControllerSetMedia&lt;/strong&gt; hook, but here we see 2 more hooks registered: &lt;strong&gt;header&lt;/strong&gt; and &lt;strong&gt;backOfficeHeader&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return parent::install() &amp;amp;&amp;amp;
        $this-&amp;gt;registerHook('header') &amp;amp;&amp;amp;
        $this-&amp;gt;registerHook('backOfficeHeader') &amp;amp;&amp;amp;
        $this-&amp;gt;registerHook('actionAdminControllerSetMedia');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;(lines 66-69)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;header&lt;/strong&gt; hook is used for adding js and css files to front end:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; /**
 * Add the CSS &amp;amp; JavaScript files you want to be added on the FO.
 */
public function hookHeader()
{
    $this-&amp;gt;context-&amp;gt;controller-&amp;gt;addJS($this-&amp;gt;_path.'/views/js/front.js');
    $this-&amp;gt;context-&amp;gt;controller-&amp;gt;addCSS($this-&amp;gt;_path.'/views/css/front.css');
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;(lines 213-220)&lt;/p&gt;

&lt;p&gt;We won't be touching front end at all, so we can delete all these lines together with files in /views/js/ and /css/ directories.&lt;/p&gt;

&lt;p&gt;Similarly, &lt;strong&gt;backOfficeHeader&lt;/strong&gt; hook is used to add back office js and css files:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
* Add the CSS &amp;amp; JavaScript files you want to be loaded in the BO.
*/
public function hookBackOfficeHeader()
{
    if (Tools::getValue('module_name') == $this-&amp;gt;name) {
        $this-&amp;gt;context-&amp;gt;controller-&amp;gt;addJS($this-&amp;gt;_path.'views/js/back.js');
        $this-&amp;gt;context-&amp;gt;controller-&amp;gt;addCSS($this-&amp;gt;_path.'views/css/back.css');
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;(lines 202-211)&lt;/p&gt;

&lt;p&gt;Please notice that our back office js and css will only be added to the page with our module configuration form. Yes, it may have sense in many cases, but just be aware of it. In another my project I wanted back.js file to be always available in back office and had to spend some time trying to find out and get rid of the condition: &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (Tools::getValue('module_name') == $this-&amp;gt;name) {
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Another thing to notice here. These two hooks have been added automatically behind the scenes. Accidentally we have selected for our module a hook &lt;strong&gt;actionAdminControllerSetMedia&lt;/strong&gt;. It looks like this particular hook is intended to be used instead of &lt;strong&gt;backOfficeHeader&lt;/strong&gt; for adding js and css files. This is an open question to Prestashop developers - which hook should be actually used for this purpose and did they forgot to update the generator?&lt;/p&gt;

&lt;p&gt;Anyway, both hooks seems to be working for this purpose (as I find out building another module), and we don't need any media in this project, so we can simply delete the lines :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's finally write some code!
&lt;/h2&gt;

&lt;p&gt;Let's delete all mentions of &lt;strong&gt;header&lt;/strong&gt; and &lt;strong&gt;backOfficeHeader&lt;/strong&gt; hooks and change &lt;strong&gt;actionAdminControllerSetMedia&lt;/strong&gt; to what we really need: &lt;strong&gt;actionEmailSendBefore&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function install()
{
    Configuration::updateValue('BCC_OUTGOING_EMAILS_LIVE_MODE', false);

    return parent::install() &amp;amp;&amp;amp;
        $this-&amp;gt;registerHook('actionEmailSendBefore');
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;... and below:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function hookActionEmailSendBefore($param)
{
    /* Place your code here. */
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We know that all hooks get one and only argument - an array of various parameters which differ from hook to hook. It seems like a bug that generator didn't include this $param array in the skeleton, so we have to add it ourselves.&lt;/p&gt;

&lt;p&gt;As we are going to write some real code here, it's time to look for our own configuration. Where will we take to email address to which we going bcc? And very handy, the generator did generate some basic configuration for us, and luckily, this is almost exactly what we need! The generator has created 3 configuration variables: &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'BCC_OUTGOING_EMAILS_LIVE_MODE' =&amp;gt; Configuration::get('BCC_OUTGOING_EMAILS_LIVE_MODE', true),
'BCC_OUTGOING_EMAILS_ACCOUNT_EMAIL' =&amp;gt; Configuration::get('BCC_OUTGOING_EMAILS_ACCOUNT_EMAIL', 'contact@prestashop.com'),
'BCC_OUTGOING_EMAILS_ACCOUNT_PASSWORD' =&amp;gt; Configuration::get('BCC_OUTGOING_EMAILS_ACCOUNT_PASSWORD', null),
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This is almost exactly what we need! We don't need any password, so lets delete all lines where BCC_OUTGOING_EMAILS_ACCOUNT_PASSWORD is mentioned, and for clarity change the name BCC_OUTGOING_EMAILS_ACCOUNT_EMAIL to BCC_OUTGOING_EMAILS_BCC_EMAIL. Let's even keep BCC_OUTGOING_EMAILS_LIVE_MODE variable - using it will add some professional look to our module :)&lt;/p&gt;

&lt;p&gt;Here is how the hook handle will look like:     &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function hookActionEmailSendBefore($param)
{
    if (Configuration::get('BCC_OUTGOING_EMAILS_LIVE_MODE', false)) {
        $bcc = Configuration::get('BCC_OUTGOING_EMAILS_BCC_EMAIL', '');
        if ($bcc) {
            $param['bcc'] = $param['bcc']? "{$param['bcc']},$bcc": $bcc;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;UPD. And here we have an interesting result. I assumed that multiple email addresses in bcc field should be separated by comma. But it appeared to be wrong. In reality it would be difficult to get to to the point where this problem would show. But working on this project, I had 2 copies of the same module installed (by different names) and Prestashop began to complain about wrong format of bcc parameter. It occurred that Prestashop Mail.php expects multiple email addresses in to and bcc fields as arrays. And another interesting result from investigating Mail.php - there is no possibility to set cc at all. Well, I may understand the logic, but is looks at least surprising for me...&lt;/p&gt;

&lt;p&gt;So the final version of the hook handler is:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function hookActionEmailSendBefore($param)
{
    if (Configuration::get('BCC_OUTGOING_EMAILS_LIVE_MODE', false)) {
        $bcc = Configuration::get('BCC_OUTGOING_EMAILS_BCC_EMAIL', '');
        if ($bcc) {
            if (! $param['bcc']) {
                $param['bcc'] = $bcc;
            }
            else {
                if (is_array($param['bcc'])) {
                    array_push($param['bcc'],$bcc);
                }
                else {
                    $param['bcc'] = [$param['bcc'],$bcc];
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Well, a bit more complicated than it was expected :)&lt;/p&gt;

&lt;p&gt;Anyway, the whole investigation had rather theoretical value. In practice, I think, the simplest solution to overwrite old bcc setting with a new one would satisfy 99.9% cases.&lt;/p&gt;

&lt;p&gt;Done and tested.&lt;/p&gt;

&lt;p&gt;Other thing to notice: while the generator generated three configuration variables (we got rid of one of those), in install and uninstall methods only one is mentioned. Looks like the other two were just forgotten, at least in unistall method. Let's add our second variable to uninstall: &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public function uninstall()
{
    Configuration::deleteByName('BCC_OUTGOING_EMAILS_LIVE_MODE');
    Configuration::deleteByName('BCC_OUTGOING_EMAILS_BCC_EMAIL');

    return parent::uninstall();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Let's leave install method as it is. I think the initialization of BCC_OUTGOING_EMAILS_LIVE_MODE is not needed altogether (and I have changed the default for this ver from true to false - seems more logical for me). but let's leave it alone.&lt;/p&gt;

&lt;p&gt;We didn't look at the /views/templates/admin/configure.tpl file, but it basically OK for our purpose. We may want to delete the example texts on the page, update links to the documentation when we had one, but this is straightforward. Just textual changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy and install
&lt;/h2&gt;

&lt;p&gt;First we need to create (or rather re-create) the zip file. After deleting the files and directories we didn't use, here is the structure:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DohouX_z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/28/61ed5928-8ca1-43c0-bab6-cb28b5bcf32d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DohouX_z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.vallka.com/media/markdownx/2021/02/28/61ed5928-8ca1-43c0-bab6-cb28b5bcf32d.png" alt="" width="473" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do not forget to put all these file in bcc_outgoing_emails folder, as they were in the original zip. Then go to back office in Prestashop, Modules-&amp;gt;Module Manager-&amp;gt;Upload a Module. Configuration is self-explaining.  Voilà! Our first Prestashop module is installed and working. &lt;/p&gt;

&lt;h3&gt;
  
  
  Yet another potential problem.
&lt;/h3&gt;

&lt;p&gt;When trying to test a newly installed module, first thing I went to Prestashop Email configuration-&amp;gt;Test your email configuration. I expected a test email to be bcc'd too, accordingly the module setting. Nope. It just doesn't work with Test your email configuration. So you need, for example, to register a new user to see any effect. The Welcome email will be sent to bcc address.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub repositary
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/vallka/bcc_outgoing_emails"&gt;https://github.com/vallka/bcc_outgoing_emails&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>phpdevelopment</category>
      <category>prestashopdevelopmen</category>
      <category>prestashopmodule</category>
    </item>
    <item>
      <title>How to deploy Django application to HostPresto!</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Thu, 18 Feb 2021 16:38:59 +0000</pubDate>
      <link>https://forem.com/vallka/how-to-deploy-django-application-to-hostpresto-3oln</link>
      <guid>https://forem.com/vallka/how-to-deploy-django-application-to-hostpresto-3oln</guid>
      <description>&lt;p&gt;&lt;a href="https://vallka.medium.com/how-to-deploy-django-application-to-hostpresto-9c9a3b1f6a07?source=rss-b12a2c268778------2"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--O_xBUlOl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/610/0%2AspBNC5OVwJ0N8EUa.png" alt="" width="610" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this tutorial we will deploy Django Polls project from official Django tutorial to the HostPresto!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vallka.medium.com/how-to-deploy-django-application-to-hostpresto-9c9a3b1f6a07?source=rss-b12a2c268778------2"&gt;Continue reading on Medium »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>webdev</category>
      <category>webhosting</category>
      <category>python</category>
    </item>
    <item>
      <title>How to start a Django application from scratch on HostPresto!</title>
      <dc:creator>Valentine</dc:creator>
      <pubDate>Mon, 15 Feb 2021 17:02:13 +0000</pubDate>
      <link>https://forem.com/vallka/how-to-start-a-django-application-from-scratch-on-hostpresto-2f3</link>
      <guid>https://forem.com/vallka/how-to-start-a-django-application-from-scratch-on-hostpresto-2f3</guid>
      <description>&lt;p&gt;&lt;a href="https://hostpresto.com/my/aff.php?aff=824"&gt;HostPresto!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this tutorial we will create a brand new Django application on HostPresto!&lt;/p&gt;

&lt;p&gt;Let’s start by setting up our environment:&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a subdomain:
&lt;/h3&gt;

&lt;p&gt;Suppose you have a domain registered with HostPresto! — yourname.xyz. For this tutorial, we will create a subdomain: start.youname.xyz, as you may already have an existing website sitting on the main domain. Adding a subdirectory like youname.xyz/start is less convenient, in our opinion.&lt;/p&gt;

&lt;p&gt;Log in into your cPanel and click on &lt;strong&gt;Subdomains&lt;/strong&gt; in the DOMAINS section. Enter ‘start’ into Subdomain field. Select your main domain (youname.xyz) from Domain dropdown. The Document Root field will be auto-populated with ‘start.yourname.xyz’. Let’s accept the default value. Click on  &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rEO-XAp3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/908/0%2AJaOxwKeHh8H1h02n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rEO-XAp3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/908/0%2AJaOxwKeHh8H1h02n.png" alt="" width="880" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Subdomain has now been created. If you point your browser to &lt;a href="http://start.yourname.xyz,"&gt;http://start.yourname.xyz,&lt;/a&gt; you should normally see an empty directory listing — something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dbBQrNg4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/956/0%2AXu4sPihztb6eMxN3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dbBQrNg4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/956/0%2AXu4sPihztb6eMxN3.png" alt="" width="880" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up a Python application:
&lt;/h3&gt;

&lt;p&gt;In cPanel you will then need to go to the &lt;strong&gt;SOFTWARE&lt;/strong&gt; section and select &lt;strong&gt;Setup Python App&lt;/strong&gt;. Then click on &lt;strong&gt;CREATE APPLICATION.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;HostPresto! Uses software from Passenger ( &lt;a href="https://www.phusionpassenger.com/"&gt;https://www.phusionpassenger.com/&lt;/a&gt;) to set up Python applications. The setup page isn’t very intuitive, in my opinion. But let’s use it anyway.&lt;/p&gt;

&lt;p&gt;Select the latest Python version in the first field. It is 3.7.3 at the time of writing.&lt;/p&gt;

&lt;p&gt;In Application root type in the directory name we have created in the previous step, relating to your home directory — that is, just ‘start.yourname.xyz’.&lt;/p&gt;

&lt;p&gt;Select ‘start.yourname.xyz’ as the Application URL in the next line.&lt;/p&gt;

&lt;p&gt;Leave Application startup file and Application Entry point empty — the system will create this file by its own rules and let it do it.&lt;/p&gt;

&lt;p&gt;The last thing — it’s sensible to set up a log file (Passenger log file) — enter /home/yournamexy/start.log in the Passenger Log File box, replacing “yournamexy” with your cPanel username.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XA-zvw7T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2AbONKMOfavFGsCKVH.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XA-zvw7T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2AbONKMOfavFGsCKVH.png" alt="" width="880" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on  &lt;strong&gt;CREATE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click on &lt;strong&gt;START APP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Again, point your browser to &lt;a href="http://start.yourname.xyz"&gt;http://start.yourname.xyz&lt;/a&gt; (or refresh the page if you still have it in one of the tabs). Now it should show something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rkfcRiJh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/827/0%2A3-sdYEEmseHtBHmB.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rkfcRiJh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/827/0%2A3-sdYEEmseHtBHmB.png" alt="" width="827" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This means we have successfully created a virtual environment for our Python application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing Python packages:
&lt;/h3&gt;

&lt;p&gt;Yes, we can do it directly from the web interface (by going to the ‘requirements.txt’ file — if you have this file in root subdirectory, In the ‘Configuration files’ section, click ADD, then click ‘Run Pip install’), but we won’t do it this way. Let’s do it the traditional way, via running shell commands. We will need to use shell commands anyway, so let’s go to the command line straight away.&lt;/p&gt;

&lt;p&gt;We need to make a ssh connection to our server. If you are on Windows, I recommend to use a program called PuTTY. Use your cPanel username and password (not client area password!) and change port 22 to 88 for the ssh connection.&lt;/p&gt;

&lt;p&gt;Once logged in, activate your project virtual environment and cd into the project directory. How do you do this? Actually cPanel contains a useful hint: at the top of the page the command is displayed for you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enter to the virtual environment. To enter to virtual environment, run the command: source /home/yournamexy/virtualenv/start.yourname.xyz/3.7/bin/activate &amp;amp;&amp;amp; cd /home/yournamexy/start.yourname.xyz&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Just click on the command shown on your cPanel and it will be copied to your clipboard.&lt;/p&gt;

&lt;p&gt;Paste the command to PuTTY terminal window and run it — now you are in your project directory and the virtual environment has been activated. First of all, let’s upgrade pip — run the following command:&lt;/p&gt;

&lt;p&gt;pip install --upgrade pip&lt;/p&gt;

&lt;p&gt;Next, we will install Django:&lt;/p&gt;

&lt;p&gt;pip install django&lt;/p&gt;

&lt;p&gt;We might need mysqlclient, so let’s install it:&lt;/p&gt;

&lt;p&gt;pip install mysqlclient&lt;/p&gt;

&lt;p&gt;I highly recommend to install WhiteNoise for managing static files, this will make life much easier:&lt;/p&gt;

&lt;p&gt;pip install whitenoise&lt;/p&gt;

&lt;p&gt;To finalise set up, let’s create the requirements.txt file:&lt;/p&gt;

&lt;p&gt;pip freeze &amp;gt; requirements.txt&lt;/p&gt;

&lt;p&gt;This command will create the requirements.txt file, which can be used to re-create the environment if needed. We don’t need this file right now right here, but it’s a good practice to have it at hand just in case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a project:
&lt;/h3&gt;

&lt;p&gt;Run the following command:&lt;/p&gt;

&lt;p&gt;django-admin startproject start&lt;/p&gt;

&lt;p&gt;(Please notice the dot at the end of command. This dot tells django-admin to create a project in the current directory. Otherwise, it will create a subdirectory ‘start’ in the current directory — this is not what we want.)&lt;/p&gt;

&lt;p&gt;Now let’s try to create an app:&lt;/p&gt;

&lt;p&gt;python manage.py startapp startapp&lt;/p&gt;

&lt;p&gt;Most probably, you will get a long list of errors when running the above command. The very last line contains the actual error, something like this:&lt;/p&gt;

&lt;p&gt;django.core.exceptions.ImproperlyConfigured: SQLite 3.8.3 or later is required (found 3.7.17).&lt;/p&gt;

&lt;p&gt;Actually, it’s good that we’ve run into this error in the very first step. (If not and your command completed successfully you still need to read further!).&lt;/p&gt;

&lt;p&gt;As you may remember, for local development you may have used the SQlite server, built-in Python SQL server. This is completely fine for local development, but totally unacceptable for a production server (and we are pretending to create a production server now). So luckily for us, Django doesn’t like the version of the installed SQlite server, so there is no other way than to switch to MySQL. (Traditionally Django uses PostgreSQL but in our case we have MySQL pre-installed on the server, so we will use MySQL)&lt;/p&gt;

&lt;p&gt;So let’s return to cPanel and create a MySQL database, using the available cPanel tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating MySQL database for the project:
&lt;/h3&gt;

&lt;p&gt;This is easy. In cPanel, go to &lt;strong&gt;DATABASES — MySQL Database Wizard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Type in a name for a database. Let’s call it ‘start’ in order to be consistent.&lt;/p&gt;

&lt;p&gt;cPanel automatically prefixes the database name with yournamexy_, so the full name will be ‘yournamexy_start’.&lt;/p&gt;

&lt;p&gt;Click on &lt;strong&gt;Next Step&lt;/strong&gt; , provide a name and a password for the new database user (let’s give the user the same name: ‘yournamexy_start’). &lt;em&gt;Don’t forget to make a note of the newly created password — we’ll need it in a minute.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On Step 3 check the &lt;strong&gt;ALL PRIVILEGES&lt;/strong&gt; tick box.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Next.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Django to use MySQL:
&lt;/h3&gt;

&lt;p&gt;In the cPanel file manager, navigate to /home/yournamexy/start.yourname.xyz/start. This subdirectory should contain the settings.py file. Open this file in the file editor and find the &lt;strong&gt;DATABASES&lt;/strong&gt; section. Replace the whole section with the following lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'yournamexy_start', 'USER': 'yournamexy_start', 'PASSWORD': os.environ['START_DB_PASSWORD'], 'HOST': 'localhost', } }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Replace yournamexy_start with your own name you have created in the previous step)&lt;/p&gt;

&lt;p&gt;It’s not a good idea to hard code the database password, so we will read it from the environment variable instead.&lt;/p&gt;

&lt;p&gt;So, return to the SSH console window you have open and set a variable for a password (Replace “your password” in the below command with the password you have chosen when creating a database):&lt;/p&gt;

&lt;p&gt;export START_DB_PASSWORD=[your password]&lt;/p&gt;

&lt;p&gt;Now you should be able to create an application. Run the following command again:&lt;/p&gt;

&lt;p&gt;python manage.py startapp startapp&lt;/p&gt;

&lt;p&gt;(Now I realise that I have chosen a bad name for my app — ‘startapp’. startapp is also a command in manage.py, so it is unclear what is what in the above command. It would have been better to name the app, say, ‘first_app’, and the command to create this app would then be: ‘ python manage.py startapp first_app')&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring passenger_wsgi.py:
&lt;/h3&gt;

&lt;p&gt;Return to your cPanel —  &lt;strong&gt;Setup Python App&lt;/strong&gt;  page.&lt;/p&gt;

&lt;p&gt;You will see the Application startup file and Application Entry point fields are populated now.&lt;/p&gt;

&lt;p&gt;The Application startup file is called passenger_wsgi.py. Find this file in the root folder of our project. Open it in File Manager. It should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os import sys

sys.path.insert(0, os.path.dirname( __file__ ))

def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) message = 'It works!\n' version = 'Python %s\n' % sys.version.split()[0] response = '\n'.join([message, version]) return [response.encode()]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We don’t need all of this, so delete the content and replace it with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'start.settings') application = get_wsgi_application()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last thing that we need to do before attempting to run our new Django site, is to set an environment variable for the db password in the Python web application. We did this previously via the SSH command shell, but we now need to do the same for the webserver.&lt;/p&gt;

&lt;p&gt;For this, return to the &lt;strong&gt;SOFTWARE&lt;/strong&gt; section in cPanel, go to &lt;strong&gt;Setup Python App&lt;/strong&gt; , click on the &lt;strong&gt;Edit&lt;/strong&gt; (pencil) icon, in line with our newly created application start.yourname.xyz.&lt;/p&gt;

&lt;p&gt;In the Environment variables section add a variable named &lt;strong&gt;START_DB_PASSWORD&lt;/strong&gt; with your database password. Click &lt;strong&gt;SAVE&lt;/strong&gt; at the top of the page.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;RESTART&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now we are ready to try the application. Point your browser to &lt;a href="http://start.yourname.xyz"&gt;http://start.yourname.xyz&lt;/a&gt; and refresh the page. Most likely you will see a message such as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DisallowedHost at /Invalid HTTP_HOST header: ‘start.yourname.xyz’. You may need to add ‘start.yourname.xyz’ to ALLOWED_HOSTS.&lt;/strong&gt; ALLOWED_HOSTS = []&lt;/p&gt;

&lt;p&gt;Thankfully, the message is very clear. ALLOWED_HOSTS = [] in settings.py. This default value only allows us to run a web app on the localhost. Now we need to change this. Open your settings.py file and find the line:&lt;/p&gt;

&lt;p&gt;ALLOWED_HOSTS = ['polls.yourname.xyz']&lt;/p&gt;

&lt;p&gt;Reolace it with the following, Replacing ‘yourname.xyz’ with your actual subdomain:&lt;/p&gt;

&lt;p&gt;Then &lt;strong&gt;RESTART&lt;/strong&gt; the application again.&lt;/p&gt;

&lt;p&gt;Refresh the &lt;a href="http://start.yourname.xyz"&gt;http://start.yourname.xyz&lt;/a&gt; page in your web browser, and you should then see:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0z-EMXq1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2Adl7D8YGW8aJE9eWi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0z-EMXq1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2Adl7D8YGW8aJE9eWi.png" alt="" width="880" height="679"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations! Our first Django application is up and running!&lt;/p&gt;

&lt;h3&gt;
  
  
  Further steps:
&lt;/h3&gt;

&lt;p&gt;OK, Let’s continue forward. Let’s try the admin page: &lt;a href="http://start.yourname.xyz"&gt;http://start.yourname.xyz&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Bv_wU6xq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2ADX8JvyxiN1-7PmFR.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Bv_wU6xq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2ADX8JvyxiN1-7PmFR.png" alt="" width="880" height="650"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It looks like our web application isn’t currently seeing the .css files, causing the formatting to look broken.&lt;/p&gt;

&lt;p&gt;Luckily, we already installed the WhiteNoise package, which will help us with just one line in settings.py. Open your settings.py file again in the cPanel file editor and add the below to the &lt;strong&gt;MIDDLEWARE&lt;/strong&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MIDDLEWARE = ['django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', #new 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;RESTART&lt;/strong&gt; the app once more and then refresh the page in your web browser:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--87iS8emp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2Ap5Wf1fye0hhU6s6z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--87iS8emp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2Ap5Wf1fye0hhU6s6z.png" alt="" width="880" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Much better!&lt;/p&gt;

&lt;p&gt;This is what WhiteNoise does for us — no need to run the collectstatic command. All static files are now being served directly from the Python server.&lt;/p&gt;

&lt;p&gt;python manage.py migrate&lt;/p&gt;

&lt;p&gt;The next thing we need to migrate is the database. We didn’t write a single line of code yet, but Django did a lot for us. So let’s migrate by running the below command in our SSH terminal:&lt;/p&gt;

&lt;p&gt;python manage.py createsuperuser&lt;/p&gt;

&lt;p&gt;Next, let’s create a superuser:&lt;/p&gt;

&lt;p&gt;Now you can log in to admin page via your web browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a meaningful app:
&lt;/h3&gt;

&lt;p&gt;OK, let’s create something meaningful. In this example lets create a simple Contact list.&lt;/p&gt;

&lt;p&gt;First of all, we need to create a model. Let’s open a nearly empty file in the cPanel file manager: startapp/models.py and add some content to it. A very simple Contact model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;# Create your models here. class Contact(models.Model): forename = models.CharField(max_length=100) surname = models.CharField(max_length=100) email = models.EmailField(max_length=100)

def __str__ (self): return f"{self.forename} {self.surname}: {self.email}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to update our settings.py file one more time. We need to add our app, startapp, to the &lt;strong&gt;INSTALLED_APP&lt;/strong&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles',

'startapp', #new ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s add our new model to the admin area. Add these lines to the startapp/admin.py file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.contrib import admin

# Register your models here. from .models import Contact admin.site.register(Contact)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;python manage.py makemigrations&lt;/p&gt;

&lt;p&gt;In the SSH console, run following command:&lt;/p&gt;

&lt;p&gt;python manage.py migrate&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;Next, &lt;strong&gt;RESTART&lt;/strong&gt; the app and then login to the admin interface in your web browser, using credentials you supplied to the &lt;strong&gt;createsuperuser&lt;/strong&gt; command&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--maTdOUdB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2AUjwBjea6GTK_W3vw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--maTdOUdB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2AUjwBjea6GTK_W3vw.png" alt="" width="880" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this tutorial, I won’t go further into creating views, forms and other common stuff. All this is common to any hosting provider, so you will find tons of useful tutorials on the Internet.&lt;/p&gt;

&lt;p&gt;Instead, I’d like to pay some attention to details, specific for HostPresto. Namely, testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing our app:
&lt;/h3&gt;

&lt;p&gt;Let’s create a very simple test in our startapp/tests.py file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.test import TestCase

# Create your tests here. from .models import Contact

class ContactModelTests(TestCase): def test_contact_save(self): contact = Contact() contact.save()

self.assertNotEqual(contact.id, None,'id is autocreated') self.assertEqual(len(Contact.objects.all()),1,'Exactly 1 record in db after save')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;python manage.py test&lt;/p&gt;

&lt;p&gt;If we try to run test the usual way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Got an error creating the test database: (1044, “Access denied for user ‘yournamexy_start’@’localhost’ to database ‘test_yournamexy_start’”)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most likely, we will get an error, something like this:&lt;/p&gt;

&lt;p&gt;This is because Django tries to create a new database for testing, and our database user does not have enough rights to create another database on the MySQL server.&lt;/p&gt;

&lt;p&gt;OK, we can create an empty database via cPanel — MySQL Databases, and then use the parameter — keepdb, but there is another problem: cPanel requires that the database name starts with ‘yournamexy_’. We cannot prepend ‘test_’ to the name.&lt;/p&gt;

&lt;p&gt;OK, there are still possibilities. Let’s create a database with the name ‘ &lt;strong&gt;yournamexy_start_test&lt;/strong&gt; ’. This is allowed. How should we tell Django to use this name? We need to edit the startapp/settings.py file one more time and add an option to the &lt;strong&gt;DATABASES&lt;/strong&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'yournamexy_start', 'USER': 'yournamexy_start', 'PASSWORD': os.environ['START_DB_PASSWORD'], 'HOST': 'localhost',

'TEST': { 'NAME': 'yournamexy_start_test', } } }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;python manage.py test --keepdb_Note: Don't even think of using the live db for testing - running tests on live will empty all data!_&lt;/p&gt;

&lt;p&gt;Now we can run tests with the following command:&lt;/p&gt;

&lt;h3&gt;
  
  
  Logging:
&lt;/h3&gt;

&lt;p&gt;Setting up logging in Django is really complicated. I won’t go in to detail here, but what can we do with the default configuration?&lt;/p&gt;

&lt;p&gt;Actually, it just works, although to use it is not very convenient.&lt;/p&gt;

&lt;p&gt;To demonstrate logging, let’s add some logging to our model via the startapp/models.py file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.db import models import logging logger = logging.getLogger( __name__ )

# Create your models here. class Contact(models.Model): forename = models.CharField(max_length=100) surname = models.CharField(max_length=100) email = models.EmailField(max_length=100)

def __str__ (self): return f"{self.forename} {self.surname}: {self.email}"

def save(self, *args, **kwargs): super().save(*args,**kwargs) # Call the "real" save() method. logger.error( f"Contact saved with id={self.id}.")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default logging level is set to &lt;strong&gt;ERROR&lt;/strong&gt; (despite whatever is written in Django documentation — probably Passenger or cPanel somehow interferes with it), so don’t bother using logger.debug of logger.info.&lt;/p&gt;

&lt;p&gt;Now go to your apps admin interface and create a few Contacts to see how it works.&lt;/p&gt;

&lt;p&gt;Where can we see the logs?&lt;/p&gt;

&lt;p&gt;Remember, there was a Passenger log file, of which the name we set up in the &lt;strong&gt;SOFTWARE — Setup Python App panel&lt;/strong&gt;? (in this example we called it /home/yournamexy/start.log).&lt;/p&gt;

&lt;p&gt;All server logs from our Python application go into this file, and logs generated by calling logger.error or logger.critical will also go into this file. Not very convenient, but at least we can use the default Python logging mechanism.&lt;/p&gt;

&lt;p&gt;Another useful option would be to install &lt;strong&gt;Django Debug Toolbar&lt;/strong&gt; , although it still requires some configuration for optimal use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion:
&lt;/h3&gt;

&lt;p&gt;In this tutorial, we have created a very basic Django application. This application does very little, and does not even have a proper front end — all it has is an admin area.&lt;/p&gt;

&lt;p&gt;But we covered very important details, related to the specifics of How to set up a Python environment within cPanel on HostPresto!, how to switch to MySQL database usage, how to set up a passenger_wsgi.py file, how to use logging with minimum configuration effort and even how to test our application.&lt;/p&gt;

&lt;p&gt;In most cases, using a remote server as a development server is not a very good idea. It would be better to install Python and all development tools locally and only use the remote server as a test or production environment.&lt;/p&gt;

&lt;p&gt;All files for this tutorial can be found here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/vallka/django-start"&gt;vallka/django-start&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;HostPresto! is reasonably priced and reliable hosting. What makes it outstanding — like many hosting providers they give you PHP and MySQL out of the box, but unlike most of other providers, they give you a very simple solution to host almost any kind of application, Node.js, Django, Flask, Ruby-on-Rails to mention a few. And all this zoo of applications can co-exist on the same server! What makes HostPresto! a great choice for any developer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hostpresto.com/my/aff.php?aff=824"&gt;HostPresto!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at&lt;/em&gt; &lt;a href="https://hostpresto.com/community/tutorials/how-to-start-a-django-application-from-scratch-on-hostpresto/"&gt;&lt;em&gt;https://hostpresto.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web</category>
      <category>python</category>
      <category>hosting</category>
      <category>django</category>
    </item>
  </channel>
</rss>
