Tag Archives: Drupal

Guide to migrate a Drupal website to Django after the release of Drupal 8

I maintain a news website written in Drupal since 2007. It is a Drupal 6, before was a 5. I made many Drupal 7 installations in these years and I went to three Drupal local conventions. This is a guide on how to abandon Drupal if you already knows some basics of Django and Python.

Drupal on LAMP: lessons learned

  • PHP is for (not so) fast development¬†but¬†maintainability can be a pain.
  • Drupal try to overcome PHP limits, with mixed results.
  • Apache cannot stands heavy¬†traffic without an accelerator like Varnish and time-consuming ad-hoc configurations. If traffic increases, Apache cannot stand it at all.
  • Drupal¬†contrib modules are¬†a mix of high quality tools (like Webform or Views Datasource) and bad written¬†projects. The more module are enabled, the more the project¬†lose in maintainability. It is not so evident if you don’t see any other open source project.

This is not the only real truth, this is my experience in these 8 years. I feel a more confident Python programmer than PHP programmer having spent less than one-third of the years working on it. At the end of the article I cite a list of article written for programmers feeling the same uneasiness of mine working on PHP and Drupal after trying other tools.

Django experiences

In the last years with Drupal still paying most of my bills I used the Django MVC framework written in Python for three project: an e-mail application, a real estate catalog  and a custom-made CRM. One of this is a porting of something written in PHP on Drupal 5. In all of these three project I was very happy with the maintainability, clearness of the code and high-level, well written packages I found while exploring it like Tastypie and many python packages found on cake shop.

Even considering I’m the only developer of these, I haven’t experienced the frustration I feel on Drupal when trying to make something work as I design or trying to fix some code I write time ago. I know that a CMS is at higher level than a framework, simply some projects are not suited for Drupal and I found more comfortable with Python than PHP in these days.

At the time I write¬†Drupal 8 is out as Release Candidate. I made migrations from 5 to 6 and from 6 to 7 on some websites in the past. Migrating to a new major¬†it’s not a science, it’s a sort of mystical art. When the Drupal 8 will be out, Drupal 6 will be automatically unsupported after 3 months Drupal 8 is out as of Drupal announcement since only the current and previous version are supported, 8.x and 7.x when 8 is out. Keeping a Drupal 6 running after that term will be risky.

Choosing the stack

Back to the news website I maintain, the choice is between a platform I already know well and it proves stable and maintainable for small/one-person team and another I have to learn. Plus,¬†Django will be the natural choice to avoid the problems I’ve listed above and use the¬†solutions I used on past django projects exploring new tools in the meanwhile.

Here the choices I made:

I decided to use gunicorn because it’s very easy to run and maintain¬†for a django project and you haven’t to make wsgi run on nginx. Nginx is in front of gunicorn, serving static files and sending right requests to it. Memcached is used inside Django and it will store cached pages from views¬†on volatile memory avoiding to read from the database any time a page is requested. I try to avoid using Varnish even if is a very good tool because I want to keep the stack¬†as simple as I can and I’m confident Varnish and Memcache will speed up the website enough. Now is the time to rewrite the Drupal-hosted website into a Django application.

Write the E-R model

If you are here probably you have a running Drupal website you want to port to Django. Browse it like an user, and then open your Content types list to identify the Entities and the Relationships as of the E-R model suggests. If your website is running for a long time you probably want to redesign some parts, adding, removing or fusing entities into another.

Take my news website¬†for example. I have 15 content types + 12 vocabularies (27 entities) on Drupal. After rewriting the E-R I’ve 14 models (entities), including the core ones. On the database side it translates into a 199 tables for Drupal and 25 for Django¬†since it¬†usually make an entity property into a database column. I trash some entities and fuse 4 entities into one.

From entities to models: understanding relationships

When you establish a relation between your re-designed entities you can have N:1¬†relations, N:N relations and 1:1 relations. A Drupal node “Article” that accepts a single term for a vocabulary named “Cheese type” translates into a N:1 relationship between the model Article¬†(N)¬†and the¬†model¬†CheeseType (1).¬†It is a simple case since you can translate it into a ForeignKey¬†field on your model since Article will get a ForeignKey field named author referencing to the Author model.

from django.db import models
from tinymce import models as tinymce_models
# Authors
class Author(models.Model):
    alias       = models.CharField(max_length=100)
    name        = models.CharField(max_length=100, null=True, blank=True)
    surname     = models.CharField(max_length=100, null=True, blank=True)
# Articles
class Article(models.Model):
    author      = models.ForeignKey('Author')
    title       = models.CharField(max_length=250,null=False, blank=False)
    body        = tinymce_models.HTMLField(blank=True, default='')
# Attachments to an Article
class Attachment(models.Model):
    article       = models.ForeignKey('Article', blank=True, null=True)
    file          = models.FileField(upload_to='attachment_dir', max_length=255, blank=True, null=True)
    description   = models.TextField(null=True, blank=True)
    weight        = models.PositiveSmallIntegerField()

In the case of a list of attachments to Article, you have a 1:N relationship between the Article model (1) and the Attachment model (N). Since the relationship is reversed, in the usual Django admin interface you cannot see the attachments in the article as is since you have to create an Attachment and then choose an article from a dropdown where attach it to.

For this case, Django provides an handy administration interface called inline to include entities in reversed relationship. This approach fix by design something that in Drupal world costs a lot of effort, with dozen of modules like Field Collection or workaround like this I write of in the past and it keep aligned your E-R design with your models. Plus, a list of all Attachment are available for free.

Exporting the data from Drupal

JSON is a pretty good interchange format: very fast to encode and decode, very well supported. I’m fascinated with YAML format but since I’ve to export thousands of articles I need pure speed and solid import/export modules on both Django and Drupal side.

There are many export module in the Drupal world. I’m very fond of Views Datasource and here how I used it:

  1. Install Views Json (part of Views Datasource): it is available for Drupal 6 and 7 and very solid
  2. Create a new view with your published nodes with the JSON Data style
    1. Field output: Normal
    2. Without Plain text (you need HTML)
    3. Json data format: Simple
    4. Without Views API mode
    5. application/json as Mime type
    6. Remove all parent / children tag name so you will have only arrays and objects
  3. Choose a path for your view
  4. Limit the view to a large number of elements, e.g. 1000
  5. Sort by node id, ascendent
  6. Add an exposed filter “greater than” Nid with a custom Filter identifier (e.g. nid)
  7. Add any field you need to import and any filter you need to limit the results
  8. Avoid caching the view
  9. Limit the access to the view if you don’t want to expose sensible contents¬†(optional)
  10. Install a plugin like JsonView (chrome) or JsonView (firefox) to look at the data on your browser

You will get something like that:

{
  [
    {
      {nid: "30004",
      domainsourceid: "1",
      nodepath: "http://example.com/path/here",
      postdate: "2014-09-17T22:18:42+0200",
      nodebody: "HTML TEXT HERE",
      nodetype: "drupal type",
      nodetitle: "Title here",
      nodeauthor: "monty",
      nodetags: "Drupal, basketball, paintball"
      }
    },
    ...
  ]
}

Now you can reach the view appending ?nid=0 to your path. It means that any node with id greater than 0 will be listed. With nid=0 a max of 1000 elements are listed. To get other nodes you have simply to get the nid from the last record (e.g. 2478) and use it as value for the nid parameter obtaining something like http://example.com/myview?nid=2478.

Try it on your browser simulating what a procedure will do for you: check the response size and adapt the number of elements (#4) accordingly to avoid to overload your server, hit the timeout or simply storing too much data into the memory when parsing. When the view response is¬†empty you’ve listed all nodes matching your filters and the parsing is complete.

In this example I’ve talked about nodes but you can do the same with files, using fid as id to pass as parameter and to sort your rows. In the case of files you have to move the files as well but it’s pretty simple to import these on a custom model on Django as you will see.

Importing data to Django

Django comes with some nice export (dumpdata)¬†¬†and import¬†(loaddata) commands. I’ve used a lot the YAML format to migrate and backup data from models but Json and SQL are other supported formats you can try. However in this migration I choose¬†custom admin command to do the job. It’s fast: in less than 10 minutes the procedure¬†imported 15k+ articles writing on a custom model some logging information on both error and¬†success.

All the import code in my case, comments and import included, is about 300 lines of python code. The core of the import function for nodes willing to become Articles is that:

import json, urllib
# ...
sid = int(options['start'].pop())
reading = True
while reading:
    url = "http://mydrupalwebsite.example.com/myview?nid=%d" % (sid,)
    print url
    response = urllib.urlopen(url)
    data = json.loads(response.read())
    data = data['']
    # no data received, empty view result, quit
    if not data:
        reading = False
        break
    for n, record in enumerate(data):
        sid = int(record['']['nid'])
        # ... do something with data ...

In this cycle, sid is the start argument passed to the admin command via command line. Next, sid will be set to the last read record so, when record finishes, a new request to myview starting from the last read element will be made.

All input and output is UTF-8 in my case. JSON View quotes strings and you have to decode them before saving in Django:

from myapp.models import Article
import HTMLParser
hp = HTMLParser.HTMLParser()
authors = Author.objects.all()
...
for n, record in enumerate(data):
    try:
        art = Article(
            title = hp.unescape(record['']['nodetitle']),
            body = record['']['nodebody'],
            author = authors.get(alias=record['']['nodeauthor'])
        )
        # run the same validation of an admin interface submit
        art.full_clean()
        art.save()
    except ValidationError as e:
      # cannot save the element
      # inside e all the error data you can save into
      # a custom log model or print to screen
    except:
      # any other exception
      pass

On line 9 a new article is declared. The title in Json source is named nodetitle. On line 10 the title from json is unescaped and assigned to title CharField of Article. The nodebody  is set as it is since the destination field is a TextField with HTML. On line 11 username nodeauthor from Json is used as key to associate the already imported user to the ForeignKey field author, where username is saved as Author.alias.

Performance gains

Here the download time graph from Google Search Console after some months:
downloadtime

You can clearly see the results in speed, expressed in milliseconds, between 2015 (old Drupal 6 platform) and 2016 (new Django platform).

Conclusion

Here the very basics on how to prepare a migration from Django to Drupal using Views Datasource module and a custom admin command. I described why I choose Django after years of Drupal development for this migration suggesting some tools to do the job and introducing some basic concepts for Drupal developer who wants to try Django.

I’ve read about Drupal enthusiasts that suffers the same uneasiness of mine after long-time Drupal / PHP development. I talk about reasons to leave Drupal on another post.

Epilogue

  • I quit my Drupal job and I’m programming mostly with Python.
  • On October 2016 Django (Software) surpassed Drupal (Software) in Google Trends. Django gained 4 points from then, Drupal lost 2 points continuing its decline in popularity on Google search.

    Django vs Drupal on Google Trends

    Django vs Drupal on Google Trends. Django surpassed Drupal on October 2016.

Advertisements

Apache CentOS 6 cannot send email and Drupal get HTTP request status fails

I’m installing a Pressflow 6 on a new machine running CentOS 6. I’m using Apache MPM Worker with FastCGI. Then I get the classical e-mail error:

Unable to send e-mail. Please contact the site administrator if the problem persists.

Then I try to use sendmail:

sendmail -v yourmail@example.com < testmail

Where testmail is a file containing these lines:

Subject: test mail Ozu
Yasujiro Ozu
[blank line here]

And i get the message. PHP cannot send email through apache!

Trying a simple php script to send mail like drupal core do I got this error:

sendmail: fatal: chdir /var/spool/postfix: Permission denied

Then I check this variable following this awesome post:

# /usr/sbin/getsebool httpd_can_sendmail
httpd_can_sendmail --> off

Enable httpd_can_sendmail solve this issue:

setsebool -P httpd_can_sendmail 1

And wait. It will be a long wait using the -P option. And then PHP and Drupal can send mail.

Then check again the variable:

# /usr/sbin/getsebool httpd_can_sendmail
httpd_can_sendmail --> on

Now httpd can send mail. Try your script again.

The SMTP Authentication Support module is not working. This is another of these variables, the same that causes Drupal to show the “HTTP request status fails” message.

setsebool -P httpd_can_network_connect 1

And wait again. Both the SMTP module and the base Drupal networking are now working and Status report is all green.

Localize date format using i18n

Tested on:

  1. Drupal 6.16+
  2. Date API 6.x-2.4
  3. Internationalization 6.x-1.3

Any date format is stored as system variable (on the global $conf variable).

Since Internationalization module allows to declare some system variables as Multilingual, you could add to your $conf[‘i18n_variables’] on settings.php these lines to use different date format for different languages:

$conf['i18n_variables'] = array(
// Other variables
// bla bla bla
// Date variables
'date_format_long',
'date_format_medium',
'date_format_short',
'date_first_day',
);

date_format variables are Long, Medium and Short date format, used in many places (including Views).

date_first_day is the first day displayed on calendars (e.g. Sunday for English, Monday for Italian).

Note that you have to save the value twice via:

http://example.com/it/admin/settings/date-time
http://example.com/en/admin/settings/date-time

And one more time:

http://example.com/it/admin/settings/date-time

After the first time, you can change format as you like without double checking.

See also:

Site off-line error after changing mysql to mysqli on Drupal

Sometimes Drupal try to access MySQL using a wrong socket, i.e. /tmp/mysql.sock.

There are two solutions: creating a symbolic link from the wrong location to the right location, or change the php.ini (es. /etc/php.ini) to point to the right socket:

mysqli.default_socket = /var/lib/mysql/mysql.sock

This solution is more reliable, since the symbolic link to socket should be recreated at any system boot on solution #1.

See also:

How to automatically translate your Drupal module

You’ve created your module. But how to translate it into different languages?

Tested with:

  • Translation template extractor 6.x-3.0
  • Drupal 6.x
  • English default + Italian translation

Prerequisites:

  • Another language active apart default (English)
  • Use t() function for all translatable string, including ones on my_funny_module.admin.inc (Administration interface).

If you use t() function correctly on your module, you can create your own translation using the handy Translation template extractor module.

  1. Download and install Translation template extractor module.
  2. Create a directory named “translations” within my_funny_module directory (your module directory)
  3. Go to admin/build/translate/extract
  4. Select your module from Directory lists
  5. Select “Language independent template” and click “Extract”
  6. Save file to my_funny_module/translations directory as my_funny_module.pot
  7. In the same screen, select “Template file for Italiano translations” (where Italiano is your destination language)
  8. If you’ve already translated some strings into Italiano language, check “Include translations” to include these strings
  9. Click “Extract”, and save file to my_funny_module/translations directory as it.po, where “it” is the ISO 639-2 code for Italiano language
  10. You can add information about translation changing the first part of both files (translator mail, name, etc.)

Now, when you install your module translation strings will be added automatically. If you apply some changes to these files, and in any case the first time you complete this procedure on an active module, you have to refresh translation cache. To do this, go to admin/build/translate/refresh and use Refresh strings and Update translations after you’ve checked all boxes. If problem persists (strings are not updated or you got some weird errors), try to reinstall your module.

Disable upload and comment for a new content type programmatically

Following code is useful when installing a module that create a new content type programmatically on Drupal 6.x.

Basically, it adds two variables setting default values for comments (core Comment module) and attachments (core Upload module).

Code to write on my_funny_module/my_funny_module.install.

function my_funny_module_install() {
  // Disable attachments
  // Read http://api.drupal.org/api/function/upload_nodeapi/6 on "load"
  variable_set("upload_my_content_type", 0);

  // Disable comments for this content type
  // Read http://api.drupal.org/api/function/comment_form_alter/6
  variable_set('comment_my_content_type', COMMENT_NODE_DISABLED);

  // Install schema as usual (if any)
  drupal_install_schema('my_funny_module');
}

Note that this code assign only default values for my_content_type: as any content type, this value could be later changed via GUI.

Node import and domain access

If you are using Node import 1.x-rc4 or below with Domain Access, you can get this error on each row to be imported:

An illegal choice has been detected. Please contact the site administrator.

This error in this case is presented when Domain Access try to import a node without assigning it to a domain. Node import 1.x-rc4 and below  lacks Domain Access support on 1.x-rc4.

Domain Access support will be available on Node import by 1.0 RC5 version, you have to dowload the -dev version to have it now.

After that, the error should disappear. For more information, read the first lines of node_import/node_import.inc , where this error is explained.