Drupal

Migrations on the road

As many people in the Drupal community know, I travel a lot. Today I'm in Paris having just completed a migration in-flight during my trip from Montréal. At the end of August I was in San José, Costa Rica where I presented both of the migrate talks my colleague Novella and I have worked on over the past year (unfortunately she could not make it as she moved to Detroit that week). It was fun to present talks I've worked with for so long... familiar. Low-stress. All the notes are now safely stored in markdown format on GitHub. Bliss.

Since they are both in an accessible format I have posted them here for others to access. Of coruse, particularly with the project management talk, the real fun in the presentation is the examples we cite as we talk through each issue, so you'll have to live without that. Here they are:

Migration Shotgun Tour (Montréal 2016, Costa Rica 2016) - in markdown and as HTML/JS slides.

Project Planning a Migration Project (Montréal 2016, Costa Rica 2016, Ottawa 2015) - in markdown and as HTML/JS slides.

...unfortunately there are no recordings of these talks. I tried to capture audio of one of them and did not realize the disk was full. :-/

Prior to these two talks I have done many different variations of these presentations at other camps and summits going back as far as 2013 when I presented commerce_migrate_ubercart to the Toronto Drupal community. It was my first-ever Drupal talk. Since then I became a maintainer of that module and done many talks and countless migrations since! I'm hoping to get back into Commerce migrations when 2.x hits beta, which I hear will be coming soon.

My next upcoming migrate talk will be a full-day training at BadCamp 2016 in Berkeley, California. It is already sold out (sorry!).

See you at DrupalCon Dublin?

Before BadCamp 2016 I will be in Dublin for DrupalCon... sorta! Specifically I will be at the Business Summit on Monday and the Friday Sprints, but NOT at the conference itself (ie, Tuesday/Wednesday/Thursday). Obviously I don't have a talk at the conference this time around... but I will still be in Dublin for that time so I may still see some of you at the parties. If you want to get lunch outside the conference venue or join me for some sight seeing let me know!

I'm on twitter (and yes DMs will ding my phone, even when I travel, so long as I'm paying attention to it). If it is work related, you can also reach out to me through the contact form at kafei.ca.

Migrating Drupal 8 in Europe

This week we're in Europe for DrupalCon Amsterdam! This is starting to feel suspiciously close to a beta so it is time to dive into Migrate again so you can start working on your new sites with real-world data. Let's begin!

What's up with migrate?

Migrate in the Drupal context means running a migration from the new Drupal 8 site and pulling data from an existing site. It replaces the old upgrade-in-place system that was used in prior versoins of Drupal. So do a fresh install of Drupal 8 and have an old Drupal 6 site on the same host. After you've logged into Drupal 8 you can connect to your Drupal 6 site.

How is Drupal 8 migrate different from Drupal 7 migrate (and migrate_d2d)?

In the older versions of Migrate the process invovled defining your field mappings and manually creating the new content types in the D7 system. This is no longer necessary.

Drupal 8 migrations will automatically create the needed content types and establish the mappings between the old and new fields by default. Therefore much less configuration is needed.

For times when it is necessary to customizae your migration you can use the included hooks or you can use the configuration schema to use the included plugins with your custom data set. The hooks go further than in D7, allowing you to alter data in the prepareRow stage without having to create a custom migration to deal with the data.

Migrate from Drupal 6? What about from Drupal 7?

Migrate frees us from the need to update each sequential version. You can leafrog versions! With Drupal 6 being close to end-of-life it is important to have a pathway for these users. For this reason, the D6 to D8 path was built first.

For Drupal 7: soon. This is now in-progress now that we are finalizing the Drupal 6 code.

Requirements

  • Drush! The UI is still in a contrib sandbox, so for now you must use Drush. The latest version - from Github.
  • Composer. It is needed to install Drush. Go into the drush folder and run "composer install". Already installed? "composer update"
  • D6 Database. Have it on the same host as the new Drupal 8 install.
  • D6 Files. Probably a good idea to be on the same host. Can be file path or http(s) address.
  • D8 Database. A new, empty database. Use the old Creating the database howto if new to this.
  • D8 Files. Check out the git repo... unless of course a beta becomes available. Then use that.

Using Drupal Tools

If you do not currently have a Drupal 8 install one route to get there is to use the Drupal Tools for your platform. It includes all the software you need and the correct versions. It is available for Linux, Mac, and Windows.

If you install the Drupal Tools package it is not necessary to install Git, Drush, or Composer.

The installation should be current as it includes a version of Drush which must be up-to-date. So if you had installed Tools before, double check the version if you have any trouble.

Install Drupal8

Using the database credentials you created, install your new Drupal 8 site.

If you need to rebuild your site remember to delete your ENTIRE files folder and old settings.php. Reinstalling without doing this step will cause problems.

Install Composer / Drush

Make sure you have run the Composer installer. You should be able to type which composer and get a result in your terminal.

Check out the latest Drush. Go into the Drush folder and run composer install.

Next time you git pull (or git fetch) in the Drush folder, make sure to run composer update.

Find an isuse to test

  • Go to the Drupal project issue queue and filter down by Component: migration system, Version: 8.x.
  • Pick an issue that is not already fixed.
  • If you are sprinting with lots of people, pick something further down the list so you are not working on the same thing as someone else.
  • Read the posts. If it is easy enough you think you can handle it, post a comment to say you are doing some work on it.
  • Post the results of your tests.

Time to Migrate

Put the manifest.yml file with the migrations you wish to run in your D8 site root. Then go there on the command line and run the following command, using your D6 database credentials.

You can install Drupal 8 at this stage. If you do, be sure to enable all of the necessary modules. For example, if you use the Book module, it is not enabled by default, so you should enable it now or your book nodes will simply become a regular content type.

When Drupal 8 is no longer in beta the manifest.yml file will not be necessary unless you are doing some custom work. In most cases all that will be necessary is to put in the database credentials and the system will run all the migrations that the system knows about by default.

You will find a manifest.yml file attached to many Migrate issues that will enable you to begin the migration. Here is a sample of what I am using... I have added together many different issues and I run them all at the same time:

To get the complete list of available migrations run drush config-list|grep migrate

# user
- d6_user
- d6_user_profile_field
- d6_user_profile_field_instance
- d6_user_profile_entity_display
- d6_user_profile_entity_form_display
- d6_profile_values:user
- d6_filter_format
- d6_user_role
- d6_user_picture_entity_display
- d6_user_picture_entity_form_display
- d6_user_picture_file
- d6_user_picture_field
- d6_user_picture_field_instance
# taxonomy
- d6_taxonomy_vocabulary
- d6_taxonomy_settings
- d6_taxonomy_term
# nodes
- d6_node
- d6_node_revision
- d6_node_type
- d6_view_modes
- d6_filter_format
- d6_field_instance_per_form_display
- d6_field_instance_widget_settings
- d6_field_formatter_settings
- d6_field_instance
- d6_field
- d6_field_settings
- d6_node_settings
- d6_cck_field_values:*
- d6_cck_field_revision:*
# taxonomy fields
- d6_term_node_revision
- d6_term_node
- d6_vocabulary_entity_display
- d6_vocabulary_entity_form_display
- d6_vocabulary_field_instance
- d6_vocabulary_field
# blocks
- d6_block
- d6_menu
# custom blocks
- d6_custom_block
- d6_filter_format
# book
- d6_book
- d6_book_settings
# files
- d6_file:
  source:
    conf_path: sites/assets
  destination:
    source_base_path: destination/base/path
    destination_path_property: uri

Now that you have created a manifest.yml file in the Drupal 8 root directory you can run the migration.

drush migrate-manifest --legacy-db-url=mysql://d6user:d6pass@localhost/d6 manifest.yml

If you are using Drupal Tools (Acquia Dev Desktop), and if you have also put your D6 site into Dev Desktop, you will need to specify the port number. You can find your database settings by creating an AcquiaDevDesktop terminal and typing drush status to get the exact settings for your D6 site. The result should look something like this:

drush migrate-manifest --legacy-db-url=mysql://drupaluser:@127.0.0.1:33067/drupal_6 manifest.yml

Note that the database user for D6 is called "drupaluser" and it uses the local IP address and port number rather than the host name. Again, run drush status if you are having trouble connecting to verify these values.

Results

After you have run the migration check your work. Did things do what you expected? Post the results of your findings to the issue queue you of the item you were working on.

Was the result successful? If so, post the result.

Did something fail? Post the result.

Post your results! Don't be afraid to comment on Drupal.org. If you provide examples of your tests you will help the migration path improve.

Rolling back (starting over)

  • It is possible to re-run the migration. This can be helpful if you forgot to run a component, or if you have new items in the source site that you would like to add to the Drpual8 site.
  • To completely "roll-back" you really need to reinstall Drupal 8. To do this you must do three things: (1) empty your database, (2) delete settings.php, (3) remove the files folder completely.

Files

Le sigh!

One problem you will run into at the moment is that using drush there is no way to set the path of the files directory from d6.

UPDATE: use the new manifest format in the code block shown above to set your file paths. It is the ONLY settings you will need in your manifest file.

The other solution is to use the migrate_upgrade contrib module (which will hopefully be part of 8.1). This module provides a URL at /upgrade which allows you to put database settings and file path. It does not let you select which migratiosn to run - they ALL run. So if you must test file migrations, this is the way (for now) but note that it is not as focused as the other testing methods.

Currently migrate_upgrade has some bugs! Look at the issue queue for the critical patches until the next update to migrate_upgrade is ready (at the moment the last version of that module dates back to June 2014).

Going further

Migrating into Drupal 8

These are the presentation notes from a "Migrating into Drupal 8" presentation I did at DrupalCamp Toronto 2014.

This discussion is focused on introducing the new concepts behind the new migration system that replaces the historic upgrade-in-place system we have used in previous versions.

Since many of these items are works in progress they are subject to change!

If you are working with D8 migrate now, keep in mind you will probably have to re-migrate at some point until the migration path is finalized. This may not happen until 8.1.x or even 8.2.x. It depends greatly on testing.

It is critical that as many people test the migration path as possible. Especially with sites that have been upgraded previously as those are likely to have the most problematic edge cases.

Currently the migration path in Drupal core is D6→D8, with D7→D8 in progress. The UI needs a patch and/or the "imp sandbox" version of D8. The UI will probably be in place around the time of DrupalCon Austin in June 2014.

Links to the initiative and places you can contribute are listed in the presentation.

AttachmentSize
PDF icon Slides in PDF format898.47 KB
File Source LibreOffice presentation1013.11 KB

Getting content into Drupal with Migrate : Montréal (v.0.4)

This year I'm presenting on Migrate at Montréal DrupalCamp! It will be the best one so far! I refactored the presentation yet again.

Recently I have published a new module, migrate_webform, that is a good example of the items in the talk. I also refactored commerce_migrate_ubercart to work with migrate version 2.6, which is the one with the amazing new UI config.

My company, Kafei Interactive, is a sponsor this year. We also sponsored Toronto DrupalCamp. I welcome Git tip if you want to buy me a coffee.

AttachmentSize
File migrate_montreal.odp600.7 KB
PDF icon migrate_montreal.pdf584.13 KB

Migrate all the things!

This is a presentation I did for the 2013 Toronto DrupalCamp.

Migrate allows you to bring content into your new Drupal site from a variety of data sources including past versions of Drupal. In this session we will talk about why you should consider using Migrate over other methods such as upgrading Drupal, using feeds, node export, copy and paste, etc. We will look at what data sources can be imported, how to create migration classes and where to find starter templates you can use to build your own custom migrations.

At the end of the presentation we walked through my previous blog post for code examples and more detail. http://www.verbosity.ca/working-drupals-migrate-module

EDIT: the prepareRow and prepare functions had their names inverted on the slide titles.

Working with Drupal's Migrate module

UPDATE: if you're interested in this article you should check out the slides from my 4th migrate presentation. The registration system in migrate has changed ("automatic" registration is gone) and I've explained it all in better detail in the slides. I even cover the pathway of the variables if you're new to OOP. ...and yes, I'm available for support and/or workshops.

I have had the opportunity to work on two Drupal sites that involved the Migrate module. I have in the past worked fairly extensively with the Feeds module to do this type of thing. Feeds is a great and powerful module, but at a certain point you will probably find that you want to use Migrate.

Why use Migrate rather than Feeds?

  • Rollbacks. The migrate module makes it damn easy to rollback and re-import.
  • Rules in code. Any changes you do to your input file are stored in a coded template.

The downside as you have probably already guessed is that you're going to spend a lot of time writing custom code here. On the bright side, there isn't that much complicated stuff once you know what everything is trying to do.

Example Migrate Templates

These are the ways that do not involve using much code. The reality is that you *will* be doing code with almost any Migrate project. Before you get started the best things you can do are (1) read the manual on Drupal.org, and (2) find a reference template that is close to what you want to do.

There is one additional thing... the beer and the wine templates. These are part of the migrate_example package that is part of the main migrate module. Review these, the code is commented specifically for this purpose.

Also check out the following as possible starting points:

Additionally, there is a Wordpress conversion template, and ones for Typo3 and PhpBB. The Wordpress one has a UI and works quite flawlessly (though you will need to remap your image locations somehow).

Migrate and the Future of Drupal

Some people have suggested using Migrate as the main way to upgrade between versions of Drupal. My guess is that my team is going to use it a lot for D7->D8 migrations when the time comes, and I suspect that it will be the default way for D9.

Setting Up Your Dev Environment and Migrate Basics

One thing about Migrate that you should know is that some of the queries that will run will be enormously expensive processor-wise. Especially when things aren't working correctly. I find it is best to work from a local environment rather than a dev server until you have a working template. Additionally, it helps to have a process monitor showing the activity on the machine so you can see when things go wrong.

Another tip that you will need to know is the command drush_print_r($variable); in your PHP code that will help you to analyze the objects referenced in your code and to see what structure Drupal is expecting when you put stuff into $row->something; (prepareRow) or $entity->field_name[][0]['value']; (prepare).

Familiarize yourself with the following commands, as it is recommend to use drush to do your migration:

drush ms
drush mi $migration
drush mr $migration

You will probably need some debugging tools as well:

drush_print_r($var); - this command does what you would expect - dumps a variable to the command line. You put this in your PHP code.

drush_print_r($query->__toString()); - this will dump the MySQL query that is generated by Drupal's dynamic query engine. If you have one template that won't run, this can come in handy. You can adapt the output of this to run in MySQL directly - useful if you need to confirm that the query is causing memory problems. You can prepend the keyword EXPLAIN to the query generated for additional info from MySQL.

Anatomy of a Migrate template

The migrate module needs some basic definitions so that it will show up on the Migrate page, which is a tab on the Content list page. Most of the time "registration" of the migration classes is "automatic" but I have found that it is useful to use the tab on that page to force it to look for migrations.

In the case of Commerce Ubercart Migration module, there is also a settings tab which will appear for that. You will want to configure it to connect to the existing data set and it will create a bunch of "migrations" to show on the main Migrate page.

When your migrations are all listed, you can click one and see what fields are mapped and which are not. This is where you're going to spend a lot of your migrate time.

Typically you will have a custom module with the following:

  • custom.info file
  • custom.module file, blank... unless you want to use it for something*
    • *an optional API definition if not using automatic-registration
  • something.inc, your first migration, listed in your files in the info file
  • another.inc, your second migration, also listed in files of info file

Within your .inc files you will need to know about the following components which are almost always going to be in your migration:

  • class CommerceMigrateUbercartNodeMigration extends DynamicMigration class definition setting what migration will be used as a base. Then the following sub-components:
    • public function __construct(array $arguments); // initialize the migration, connect to source and grab the settings! Contains a reference to what will be used to map the migration (keeps track of changes to IDs)
    • public function prepareRow($row); // preprocess function, DOES NOT RUN FOR XML IMPORTS
    • function prepare($entity, stdClass $row); // last chance to do stuff. Can run SQL queries here, first param is always the target node/order/entity fully expanded, so put in your languages setting and delta values!
    • function complete($entity, stdClass $row); // rarely needed if you did things right. Can be used for doing post-migration tasks but could complicate your rollbacks, so be sure to test!

Initializing the Migration

With Migration templates you're dealing with Object Oriented Programming, not a staple in our Drupal PHP diet yet. If you're familiar with OOP you'll be right at home. Let's start out with a definition:

class CommerceMigrateUbercartNodeMigration extends DynamicMigration {
  // An array mapping D6 format names to this D7 databases formats.
  public $filter_format_mapping = array();

  public function __construct(array $arguments) {
    $this->arguments = $arguments;
    parent::__construct();
    $this->description = t('Import product nodes from Ubercart.');
    $dependency_name = 'CommerceMigrateUbercartProduct' . ucfirst($this->arguments['type']);
    $this->dependencies = array($dependency_name);

    // Create a map object for tracking the relationships between source rows
    $this->map = new MigrateSQLMap($this->machineName,
      array(
        'nid' => array(
          'type' => 'int',
          'unsigned' => TRUE,
          'not null' => TRUE,
          'description' => 'Ubercart node ID',
        ),
      ),
      MigrateDestinationNode::getKeySchema()
    );

There is a lot of stuff going on here. Firstly, we're extending a base class. That means this will apply to any objects of that class. So we can just assume that if the base class Migrate is running, that this class will run as well. What follows is a constructor, which what is going to run at startup.

The last part of this code block is the most interesting: $this->map. Here is how Drupal is going to track the objects in your migration. It is your key for each item in your import - and it is going to BREAK if you are trying to use XML as your import source. Then you're going to have to dig around in your DB and change that type from int to something else.

Connecting to Source(s)

The most exciting part about this part of the migration template is that you have many options. In this case we're looking at a D6->D7 migration, so in our case D6 is our source! Migrate takes care of the actual connection to the database so that part is easy, you can point it right at production to grab the data. As noted above, XML can also be your source(s). When I say source(s) I mean possibly plural... the example code provided will let you cycle through many XML files during the import process. Potentially really handy if your exports were run in batches.

Note, that the switch statement in the start of this module is related to the UI of Migrate, where the author of this module has made a switch available. Most migrations don't provide a settings form for Migrate but this one does.

    // Create a MigrateSource object, which manages retrieving the input data.
    $connection = commerce_migrate_ubercart_get_source_connection();

    $query = $connection->select('node', 'n');

    switch (SOURCE_DATABASE_DRUPAL_VERSION) {
      case 'd7':
        $query->leftJoin('field_data_body', 'fdb', 'n.nid = fdb.entity_id AND n.vid = revision_id');
        $query->leftJoin('field_data_field_campaign_id', 'cid', 'n.nid = cid.entity_id');
        $query->fields('n', array('nid', 'type', 'title', 'created', 'changed'))
              ->fields('fdb', array('body_value', 'body_summary', 'body_format', 'language'))
              ->fields('cid', array('field_campaign_id_value'))
              ->condition('n.type', $arguments['type'])
              ->distinct();
        break;
      case 'd6':
        $query->leftJoin('node_revisions', 'nr', 'n.nid = nr.nid AND n.vid = nr.vid');
        $query->leftJoin('filter_formats', 'ff', 'nr.format = ff.format');
        // ... and so on ...
    }
    $this->source = new MigrateSourceSQL($query, array(), NULL, array('map_joinable' => FALSE, 'skip_count' => TRUE)); // connect!

You probably noted the D7 and D6 options have different mappings. Take note. If you're going to extend one, do the one you intend to use.

When your main Migrate page stops working...

Eventually if you start adding fields your query will silently break, and will force Migrate's listing page to hang. Usually you can still access the page for the specific migration but since the purpose of these pages is to help guide your clients through the process this is a severe deal-breaker. Fortunately, it is easily solved one of two different ways. Either (1) don't bother counting how many tasks are to be done or (2) specify a basic query that can be tabulated quickly.

Here is how you do that:

$this->source = new MigrateSourceSQL($query, array(), NULL, array('map_joinable' => FALSE, 'skip_count' => TRUE));

In your source you specify skip_count as a parameter and you set it to TRUE. Now you're going to have an X where the number of items to be migrated was... but you can at least get to that page. Whew...

Adding each of your fields

This is where start getting fun... or really boring. Your pick. We now have to go through each field that will be brought into the database and create a relationship with the place where we want it to go.

Each field in your migration is going to have the following components:

  • A field in your target content type to hold the incoming data
  • An extension to the $query object to perform a LEFT JOIN if your source is in a table that is not in the base query
  • An extension to the $query object's fields to SELECT by column name
  • An extension to $this to connect the old field to the new field
  $query->leftJoin('content_field_location', 'loc', 'n.nid = loc.nid');

  $query->fields('n', array('nid', 'type', 'title', 'created', 'changed'))
             ->fields('loc', array('field_location_value'));

  $this->addFieldMapping('body', 'body_value') // add semicolon if no options
         ->arguments($arguments)  // optional
         ->defaultValue(''); // optional

This wouldn't be Drupal without some "gotchas". As I was adding fields in each of the migrations I have done eventually the main Migrate page stops listing out my migrations. This was REALLY aggravating and I spent at least a full week debugging this issue across the two migrations I have done. The solution? Stop counting how many things you are going to process (see the section above about using skip_count when connecting to the source.

It probably goes without saying... but test EVERY field after you have defined it. This is where you'll start finding weird stuff in your data. Unless, like nobody before you, your previous database is perfection in every way. Highly doubtful. As you discover problems in your code you can deal with them in processRow or process functions.

Processing the Data

Now that we have all of our fields coming into Drupal and our Migrate page has no errors on the main list and additionally no errors on our specific migration it is time to process the data! Most of the time this will not be necessary, but sometimes you must change things as they import so it will fit the new data structure.

  public function prepareRow($row) {
    // Transform body format to something we can use if it's not already.
    if (!filter_format_exists($row->body_format)) {
      $row->body_format = $this->transformFormatToMachineName($row->body_format);
    }
    $row->temp_data = "somejunk"; // will be passed to prepare function then dropped
    $row->field_text = "XXX"; // sets the value of a text field to the string "XXX" (works for many simple fields)
  }

Within your migration class you will probably always have a prepareRow function. This is where you can make some changes, or alternatively, reject a certain row before it gets imported by return FALSE;. This step DOES NOT RUN for any XML import you do, not even to carry over temp data. Gotcha!

  function prepare($entity, stdClass $row) {
    $entity->field_text['fr'][0] = 'YYY'; // manually set field_text French version (if using entity_translation), for delta 0 (the first instance if allowing multiple values) to YYY
  }

The prepare function is your second chance to change things using the entity pre-rendered, fully expanded into Drupal's typical array syntax. If you're doing XML processing, this is where you need to make any transformations. You can also grab any data you setup in prepareRow... or just run SQL queries here. Be forewarned... if you do SQL queries here it is generally better to load all of your data earlier on (in $this->query) so that you're not hammering your database with requests or blowing out the memory on your server. Those things are hard to detect.

  function complete($entity, stdClass $row) {
  }

The complete function runs at the end. In some cases it is necessary to make an update after the data has been imported. For example: if you are importing line items that are part of an order in Commerce, you will have a task here to update the order total after each line item has been imported. Migrate uses $this->map to track the progress and ID changes between all of your migrations. This function takes that into consideration.

So... sweet, we're already done and we don't even need to use the complete function! That was easy right? Sort-of. It is not always easy. It is a long time-consuming process to verify all the data is correct and to get this far. If you're considering taking on a Migrate project be sure to consider all the possibilities of problems that will come up with the old data. Quantify all of your content types and entities that will be migrated and every individual field contained therein. Expect that each field will need a certain level of review and base your estimates/calculations on that.

Overriding Drupal7 node display with Views

Back in the Drupal6 days overriding node displays with a View was a pretty common thing for me but since using Drupal7 most of the time it is not necessary. That time has finally come, and I did a bunch of searching around for a better/automatic way but I did not find anything that did not involve over-engineering so I did it in my theme.

First, create a view, as a block (to not to have extra pages floating around).

  • Set the title to %1 to set your view title the same as the node
  • Do not use a pager
  • Display only one item
  • Add a Contextual Filter using the NID field, set the fallback behaviour to "hide"

Then, override the node.tpl.php file in your theme. If you do not have this file in your theme, find out which theme your custom theme is based on, and copy over the relevant file to the correct location for your theme.

In your node.tpl.php file, look for the following code:

print render($content);

...and replace it with a view_embed_view.

print views_embed_view('node','block_1', $node->nid);

In this example, the first parameter, node, is the machine name for my view, and block_1 is the first block in the view. When you are editing your view you will probably see both of these parameters in your URL. Finally, the last parameter is the node ID. This last parameter will probably be the exact same that you will use unless you are doing something truly crazy (like showing the wrong node on purpose? har har).

I only wanted to apply the view to nodes which use the 'article' content type so I combined the old behaviour and the new behaviour with a test for the content type:

if ($node->type == 'article') {
      print views_embed_view('node','block_1', $node->nid);
    }
    else {
      print render($content);
    }

There you have it! Now you can use Views to render your fields for a specific node type... and using Views you can customize your HTML as much as you like using the awesomeness of Views without the overhead of Panels or Display Suite.