Ignored By Dinosaurs 🦕

Notes from Mike Ryan, the Migrate guy, at the bottom. Make sure you read them before copying any of this code.


The Migrate module has some of the best documentation I've ever seen in DrupalLand, but there are still just a couple of things that I've figured out over the last month that I wish had been clearly explained to me up front. This is an attempt to explain those things to myself.

Clearly, you're going to be writing code to perform this migration.

  • There is a module – migrate_d2d – that is specifically for stuff like this. It's aware of Drupal content types' basic schema, so it'll save you a LOT of SQL writing to join each nodes' fields on to the base table.
  • You'll write a class for each migration that you need to perform.
  • You'll need to write a separate class for each content type that you have.
  • You'll need to write classes for roles, users, files, and each taxonomy vocabulary that you have on the site.
  • You'll tell Drupal about these migrations by writing an implementation of hook_cache_clear() that'll “register” the migrations and make them show up in the GUI and in drush. This looks basically like this —

function abm_migrate_flush_caches() {

  $common_arguments = array(
    'source_connection' = 'dfi', // the "source" database connection
    // there's a syntax for this that basically mirrors the way that
    // you set up database connections in settings.php
    'source_version' => 7, // a tip to the migrate_d2d module as to which 
    // version of Drupal you're migrating from
  );
 
  $arguments = array_merge($common_arguments, array(
  'machine_name' => 'AbmRole',
  'role_mappings' => array(
  'administrator' => 'administrator',
  'editorial staff' => 'editorial staff',
  'pre-authorized' => 'pre-authorized',
  'directory listee' => 'directory listee',
  'directory listee - preapproval' => 'directory listee - preapproval',
  'directory manager' => 'directory manager',
  'web production' => 'web production'
  ),
));

Migration::registerMigration('AbmRoleMigration', $arguments['machine_name'],
$arguments);

$arguments = array_merge($common_arguments, array(
  'machine_name' => 'AbmUser',
  'role_migration' => 'AbmRole', // forms the relationship between this 
  // user migration and the role migration that already happened.
  // This only works for a few specific, simple cases.
  // Relating nodes with taxonomy items, for example, happens elsewhere.
  // (in the actual migration class...)
));

Migration::registerMigration('AbmUserMigration', $arguments['machine_name'],
$arguments);
 
$arguments = array_merge($common_arguments, array(
  'machine_name' => 'AbmProdCats', // when you run "drush ms", 
  // (migration-status) this is the name that shows up
  'source_vocabulary' => 'product_categories', // yay machine names
  'destination_vocabulary' => 'product_categories'
 ));

Migration::registerMigration('AbmProdCatsMigration', $arguments['machine_name'],
$arguments);
}

  • Registering the migration also creates a set of database tables for each migration, the most interesting of which is the migrate_map_xxx, where “xxx” is the machine_name of your migration, downcased.

mysql> show tables like 'migrate%';
+------------------------------------+
| Tables_in_for (migrate%) |
+------------------------------------+
| migrate_field_mapping |
| migrate_group |
| migrate_log |
| migrate_map_abmadterms |
| migrate_map_abmappnotes |
| migrate_map_abmarticle |
| migrate_map_abmawardwinners |
| migrate_map_abmblogs |
| migrate_map_abmcompanyprofiles |
| migrate_map_abmdigitaleditions |
| migrate_map_abmevents |
| migrate_map_abmfiles |
| migrate_map_abmnews |
| migrate_map_abmpodcasts |
| migrate_map_abmprodcats |
| migrate_map_abmproductreleases |
| migrate_map_abmproducts |
| migrate_map_abmrole |
| migrate_map_abmtopics |
| migrate_map_abmuser |
| migrate_map_abmvideos |
| migrate_map_abmwebinars |
| migrate_map_abmwhitepapers |
| migrate_message_abmadterms |
| migrate_message_abmappnotes |
| migrate_message_abmarticle |
| migrate_message_abmawardwinners |
| migrate_message_abmblogs |
| migrate_message_abmcompanyprofiles |
| migrate_message_abmdigitaleditions |
| migrate_message_abmevents |
| migrate_message_abmfiles |
| migrate_message_abmnews |
| migrate_message_abmpodcasts |
| migrate_message_abmprodcats |
| migrate_message_abmproductreleases |
| migrate_message_abmproducts |
| migrate_message_abmrole |
| migrate_message_abmtopics |
| migrate_message_abmuser |
| migrate_message_abmvideos |
| migrate_message_abmwebinars |
| migrate_message_abmwhitepapers |
| migrate_status |
+------------------------------------+
44 rows in set (0.00 sec)

mysql> describe migrate_map_abmblogs;
+-----------------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+---------------------+------+-----+---------+-------+
| sourceid1 | int(10) unsigned | NO | PRI | NULL | |
| destid1 | int(10) unsigned | YES | | NULL | |
| needs_update | tinyint(3) unsigned | NO | | 0 | |
| rollback_action | tinyint(3) unsigned | NO | | 0 | |
| last_imported | int(10) unsigned | NO | | 0 | |
| hash | varchar(32) | YES | | NULL | |
+-----------------+---------------------+------+-----+---------+-------+
6 rows in set (0.00 sec)

mysql> describe migrate_message_abmblogs;
+-----------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+------------------+------+-----+---------+----------------+
| msgid | int(10) unsigned | NO | PRI | NULL | auto_increment |
| sourceid1 | int(10) unsigned | NO | MUL | NULL | |
| level | int(10) unsigned | NO | | 1 | |
| message | mediumtext | NO | | NULL | |
+-----------+------------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

[!note] Since Migrate is an OOP thing, you can write a parent class for a generic “Node” migration that all of the other specific content types can inherit from. Most of the node migration classes that I wrote look like this, due to most of the fields being set up in the parent class —


class AbmBlogsMigration extends AbmNodeMigration {
  public function __construct(array $arguments) {
    parent::__construct($arguments);
    $this-addSimpleMappings(['field_pipes_flag']); // this is the only 
    // blogs specific field that existed on this content type, all of the fields
    // that are common among all content types are mapped in the parent class - 
    // AbmNodeMigration - in exactly the same manner. Except for some that aren't...
  }
}

[!info] Most fields in a Drupal to Drupal migration will come over easily with Migration::addSimpleMappings(), but some require a little more coddling. These are often fields that represent a relationship to another entity – Taxonomy term references, other node references, etc. These will require something like this —


php

abstract class AbmNodeMigration extends DrupalNode7Migration {
  public function __construct(array $arguments) {
  parent::__construct($arguments);

  $this-addFieldMapping('field_taxonomy', 'field_taxonomy') // sets up the mapping
  ->sourceMigration('AbmTopics'); // tells you which migration to reference.
  // This makes it look to the migrate_map_xxx table to pull out the NEW 
  // destination primary keys. Without this bit, it'll try to bring over the
  // related entity IDs verbatim, which will either fail because there is no
  // taxonomy term/node/whatever with that ID, or it'll just relate it to the
  // wrong entity. Either way, bad. 

  $this->addFieldMapping('field_taxonomy:source_type') // I wish I could tell you more 
  ->defaultValue('tid'); // about what this part means, but I just don't know yet. 
  // All I know is that is the previous lines are not enough to make it work
 }

  • Speaking of that, prior to finally putting the pieces together about how related entities maintain that relationship, I did lots of clever coding to maintain the relationships between imported entities. It's not that complicated, but I was manually looking into the migrate_map_xxx tables to pull destination_ids out. This is obviously wrong abd felt wrong when I was doing it, but it didn't all click until chasing down vague error messages about “field validation errors” in later migrations. It doesn't tell you what fields are in error, it just throws an Exception on these nodes and doesn't save them. I finally ended up dumping $errors in field_attach_validate() and saw that it was always a related entity field that was erroring. It was easy to figure out after that, but it took me several weeks of getting my head around the rest of it all to be able to get to that very simple point.
  • I missed all of that for so long because the user migration has this tidy little line about 'role_migation' that establishes the relationship, so I thought it would/should be something along those lines. I spent a long time in the module code tracing down default arguments and the like before finally just doing it the hard way. This is wrong.
  • Oh, by the way, USE THE LATEST VERSION OF ALL OF THESE MODULES. Migrate finally released 2.6, years in the making apparently, a couple of weeks ago, as I was in the middle of all this. I'd been using the previous stable, which is of course missing years of work, and solves almost all problems out of the box.
  • Here's another little gem regarding files, and making those relationships tie out —

// In AbmNodeMigration::__construct()

 $this-addFieldMapping('field_image', 'field_image') // sets up the mapping
 ->sourceMigration('AbmFiles');
 $this->addFieldMapping('field_image:file_class') // but for some reason it doesn't 
 ->defaultValue('MigrateFileFid'); // work without this part. The answer is
 // here - https://www.drupal.org/node/1540106 - but I haven't had time to 
 // fully absorb that part yet. I glossed over this part of the documentation
 // a dozen times because file_class seems like it'd be unrelated to what you're
 // trying to do - relate nodes and files. file_class sounds like something
 // CSS related. Needless to say, it is not.

Beer shot -


A review from the guy himself —

The blog post looks like a good intro to migrate_d2d 2.0, but I'm afraid now it's a bit dated (as you point out towards the bottom).

hookflushcaches() hasn't been considered a good practice for a while (defining migrations in hookmigrateapi() and using drush migrate-register is preferred – https://www.drupal.org/node/1824884), but I see that migrated2dexample still does it – I'll need to update that before the imminent new release so people aren't misled.

Setting the sourcetype to 'tid' is covered at https://www.drupal.org/node/1224042 – by default the incoming value for a term field is assumed to be the term name, when you're making use of a separate term migration via sourceMigration, the incoming value is a tid and you need to set the sourcetype so the field handler knows what to expect.

The fileclass is similar – normally the value coming in to the file field is assumed to be a URL, but when using a separate file migration and referencing it via sourceMigration it's a fid. The “class” in fileclass is a PHP class – the name of any class implementing MigrateFileInterface can be used here.

Thanks Mike!

#drupal

This shit is all very scary and confusing to you right now. You're about to walk the plank into the unknown. This will be the last “principled” career decision that you make up until the point that I'm writing you this, and you will learn a hell of a lot from it – about yourself, about your marriage, and about life in general. Shit's about to get really difficult for you, in a way that you sense now, and that's why you were up unable to sleep at 3am last night.

There will be no gentle landing, and that hail mary pass that you're hoping to connect with that idea of yours isn't going to connect, at least not as neatly as you need it to. And certainly not as quickly as you need it to.

I'm writing you now to let you know that it's going to be alright. You have this cocky hunch that the move you're making is going to be the best move you ever made, and it will be, mostly in ways that you can't really get yet. But you're going to pay for it, too.

The investment that you're making now and over the coming years is going to come back in a big way. Don't let knowing this make you work any less hard though – it's only under this pressure that you get where you need to go. It's only by putting more time into something that's more difficult than anything you've tried to do before that you get where you need to go.

Now go.

#life

I started this blog almost 6 years ago. Looking back it was basically chronicling the beginning of the darkest years of my life. It was also, however, chronicling the beginning of the most creative years that I've ever had. Lot of shit went down for me 5 years ago, and being a nice round-number-type anniversary I've been going back over these old posts a lot lately, especially the ones where I really took a lot of time to lay it out exactly right. The creative fire is one that I wrote almost exactly 5 years ago, and it startles me now how much I knew intrinsically about the journey that lay ahead of me. It took me a couple hours and several cups of coffee in a Boulder coffee shop to transcribe that passage, by the way, prior to one of my last CO shows. You were there, IIRC.

I truly thank God for that blog post I read, wherever it was, that said something to the effect of “start a blog”.

So, my man – start a blog. I firmly believe everyone should do it. Whether it's a thing you keep doing or not, it doesn't matter. You are going through a rough period right now. I had no idea how close the two of you were, and my heart hurts for you reading what you wrote on *someone else's blog*. You are of of my most intelligent musician friends (a big part of the reason I like you so much, even though we rarely get together), and I had no idea you were so articulate in print. Not that I'm surprised...

Articulate your grief more, friend. Write it down. It's not only therapeutic to analyze how you're feeling and why, you will be profoundly glad when this period is behind you and you can look back and truly remember exactly how you felt now. Because you wrote it down.

I love you, brother. JG

#life

Hoping to land a slot in the SERPs with that title, I'm here to demonstrate today the difference between good 3rd party javascript, and bad 3rd party javascript.

First – the good, as demonstrated by Google Analytics. GA's setup and tracking code, as stepped through here.

(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-XXXX-Y', 'auto');
ga('send', 'pageview');

The ga() function is what does the magic, but in essence all that the ga() does is push its arguments into an array. All of this functionality is contained within those 4 lines. Those 4 lines also create a script tag that loads the rest of the GA lib, where the functionality to rifle through that array and report its contents back to GA's servers lives.

If that script tag fails to return, nothing happens. More importantly, nothing bad happens. The array that ga() pushes into is a vanilla JS array, it will always be there. If the GA payload doesn't come back, no worries. Your app or website will never know the difference.

This is how knowledgable developers build 3rd party libs that don't suck.

In contrast, here's what Adobe's Omniture does.

// A blob of obfuscated js hundreds and hundreds of
// lines long here. The essence of worst practices.
// All of this results in (hopefully) their lib coming
// down via a generated script tag as well. This script
// tag creates a global object - s (so artfully and helpfully
// named) - that has a giant range of methods to deal with 
// the functionality of their platform - pageviews, events, etc

Later, you track events and the like via a function call like this -

s.tl(this, 'o', 'blah'); // event
s.t() // pageview

What happens though, if their huge lib doesn't come back for some reason? Like, maybe a corporate firewall doesn't feel like letting Omniture code track its users? This would result in there being no global s object on which to call these methods. What happens then?

Your website blows up, that's what. Exceptions are thrown, and god help you if you're running a single page app, because it's toast now.

The only solution I've been able to come up with is to wrap everything in try {} catch {}, which is hideous and wasteful and prone to error when you forget to wrap every single piece of Omniture code with it.

That's why I have the utmost respect for the engineering of GA, and the utmost disdain for Omniture. Oh, and Omniture costs tons of money to use, and the API documentation is buried somewhere in the 5th level of Dante's hell.

#javascript

Simple trick for making sure that anything that you want to listen to window.onscroll doesn't eat up too many cycles while it's doing its thing. It's called “throtting”.


Throttling basically means, if you're receiving a steady stream of input from something, you don't really want to be firing stuff off based on that steady stream. This is a performance suck. Let's say you have this —-

window.addEventListener('scroll', function() {
  // Stuff that's actually kinda CPU intensive like
  // taking measurements, waiting for some element
  // to show up on the screen, for example. 
  console.log('hi!');
});

This function is going to be firing as many times a second as your computer can handle. If you're on a beefy laptop in Chrome, this will probably not be noticeable, but make no mistake — none of your users are on as good a laptop as you are. You will definitely drop frames and your perceived performance will suck wind.

What's the answer? Throttle that code. Like this.

// timeNow is the current time in milliseconds, created by
// casting new Date() to a number with +
var timeNow = +new Date();
window.addEventListener('scroll', function() {
  // if the current time in milliseconds - timeNow is
  // less than 250, abort.
  if((+new Date() - timeNow) < 250) return;
  // Else, reset timeNow to now.
  timeNow = +new Date();
  console.log('hi!');
});

This is hack-y looking because it's kind of a hack. Underscore and Lodash have this built in, but it might be a little heavier than what you need. If you find yourself using this more than once in a file, please either bring in Lodash, or rip off their implementation into your project.

#javascript

I screwed this one up pretty bad when I first got started with Fastly. That have that link that says “VCL”, as if you should download that to get started. So I did. But that's the generated VCL, and all their special macros are already blown up, so you don't want to copy that one.

And the one on their website has HTML entities encoded, so I'm putting this one here in case I need it again later.

The original

sub vcl_recv {
#FASTLY recv

  if (req.request != "HEAD" && req.request != "GET" && req.request != "FASTLYPURGE") {
    return(pass);
  }

  return(lookup);
  }

sub vcl_fetch {
#FASTLY fetch

 if ((beresp.status == 500 || beresp.status == 503) && req.restarts < 1 && (req.request == "GET" || req.request == "HEAD")) {
 restart;
 }

 if(req.restarts > 0 ) {
 set beresp.http.Fastly-Restarts = req.restarts;
 }

 if (beresp.http.Set-Cookie) {
 set req.http.Fastly-Cachetype = "SETCOOKIE";
 return (pass);
 }

 if (beresp.http.Cache-Control ~ "private") {
 set req.http.Fastly-Cachetype = "PRIVATE";
 return (pass);
 }

 if (beresp.status == 500 || beresp.status == 503) {
 set req.http.Fastly-Cachetype = "ERROR";
 set beresp.ttl = 1s;
 set beresp.grace = 5s;
 return (deliver);
 }

 if (beresp.http.Expires || beresp.http.Surrogate-Control ~ "max-age" || beresp.http.Cache-Control ~"(s-maxage|max-age)") {
 # keep the ttl here
 } else {
 # apply the default ttl
 set beresp.ttl = 3600s;
 }

 return(deliver);
}

sub vcl_hit {
#FASTLY hit

 if (!obj.cacheable) {
 return(pass);
 }
 return(deliver);
}

sub vcl_miss {
#FASTLY miss
 return(fetch);
}

sub vcl_deliver {
#FASTLY deliver
 return(deliver);
}

sub vcl_error {
#FASTLY error
}

sub vcl_pass {
#FASTLY pass
}

#varnish

So tracking pixels. They sound awful. They sort of are, but we all use them. One just fired off on you a minute ago when you loaded this page. That's how Google Analytics works its magic. But how do they work? The GA tracking code is Javascript and doesn't say anything about an image pixel.

Step inside...


Dat JS

So that javascript does a few things, primarily it creates another javascript tag that pulls down the real “payload”, which is a hackerish term for “a bigger ball of code”. I haven't analyzed that code yet, but one of the things that it does is build a profile of your browser that you're on and the page that you're looking at. Once it does that it pings GA's tracking servers with that profile which counts as a “pageview”. That's the ga('send', 'pageview') bit. But how does that work?

A tracking pixel!

Placement in the DOM, you need not...

So a pretty interesting thing about tracking pixels, and anything in the browser really is that it doesn't actually need to be put on the page to exist in memory somewhere. In fact, even if that pixel is only 1x1 in size, it could bump something out of the way enough to trigger a repaint of the webpage, which might alert you to that pixel's existence, which is something that advertisers and their ilk stringently avoid.

So basically, that ga('send', 'pageview') ends up generating a request to a server somewhere. That request looks like this

https://www.google-analytics.com/collect?v=1&_v=j29&a=806595983&t=pageview&_s=1&dl=http%3A%2F%2Fwww.ignoredbydinosaurs.com%2F2014%2F09%2Fdeconstructing-the-google-analytics-tag&ul=en-us&de=UTF-8&dt=Deconstructing%20the%20Google%20Analytics%20tag%20%7C%20Ignored%20by%20Dinosaurs&sd=24-bit&sr=1440x900&vp=1334x479&je=1&fl=15.0%20r0&_u=MACAAAQAI~&jid=&cid=1626931523.1412365384&tid=UA-8646459-1&z=962163205

In the network tab of your devTools in your favorite browser you can break down all those query string params into something a little more interesting.

v:1
_v:j29
a:806595983
t:pageview
_s:1
dl:http://www.ignoredbydinosaurs.com/2014/09/deconstructing-the-google-analytics-tag
ul:en-us
de:UTF-8
dt:Deconstructing the Google Analytics tag | Ignored by Dinosaurs
sd:24-bit
sr:1440x900
vp:1334x479
je:1
fl:15.0 r0
_u:MACAAAQAI~
jid:
cid:1626931523.1412365384
tid:UA-8646459-1
z:962163205

Some of that stuff is understandable, some of it is not. But the point is that that request actually trigger a response of a 1x1 pixel.

# Response headers

access-control-allow-origin:\*
age:66666
alternate-protocol:443:quic,p=0.01
cache-control:private, no-cache, no-cache=Set-Cookie, proxy-revalidate
content-length:35
content-type:image/gif
date:Fri, 03 Oct 2014 18:23:52 GMT
expires:Mon, 07 Aug 1995 23:30:00 GMT
last-modified:Sun, 17 May 1998 03:00:00 GMT
pragma:no-cache
server:Golfe2
status:200 OK
version:HTTP/1.1
x-content-type-options:nosniff

If this were the first time I'd visited the internet, there would almost certainly be a set-cookie header in there as well, but since they set that cookie on me a LONG time ago, it doesn't get sent.

The kinda creepy thing is that since Google Analytics is on a large number of sites, and their origin servers are on the same domain (cookies), they can follow you around the internet from site to site to site in a way that nobody else can (save perhaps the other giant analytics providers, which probably have nowhere near the reach, unless you count Facebook).


Wow, cool. So what?

So that image pixel is not really the point. It gets returned in that response, but doesn't get put on the page. It plants a cookie on you, big deal.

But what happens at Google is that the request that was made in the first place gets logged. It gets broken down by its query string params, and that's how they build the tool. That's how you know what size browser people are on, what part of the world they're from, what they looked at, and what they clicked on (if you're tracking events).

The really interesting part to me, and the part I haven't figured out yet, is how they store ALL that data on the backend to build the reports out of. Think about it — they're basically logging every request made to every website that is running their code. That's a really big number, even for Google. And then they're able to pull your report suite out of all that data, and sort it out by whatever you wanna know. Seems pretty cool, and also well beyond the capability of normal relational DBs.

A post in the future, I imagine....

#javascript #analytics

Hello there! There has been a lot of discussion in the Drupalsphere lately about a concept that has been coined “Headless Drupal”, and rightly so. It's basically theming, but throwing out the theme layer. The theme layer in Drupal is very powerful, but has always felt severely over-engineered to me, especially in contrast to pretty much any MVC framework I've played with. With those, you define view-layer variables in a controller class, and you write HTML with a little bit of code to print them out into HTML. It's more work up front, since you have to write code, but it's vastly easier once you get over that hump.

The company I work for has been doing exactly this with AngularJS since early this year, and I've yet to see a concise post about how to get started with it in the context of Drupal. I rarely write “how-to” posts, but I figured it'd be a good way to inaugurate the ABM tech blog.

Our use case (feel free to skip)

Early this calendar year, my boss' boss came to us with a business request — build us a framework on which we can make our up-until-lately desktop-only websites a little more mobile friendly. We were using a rather ungainly combination of Panels and Adaptivetheme, and though those should have given us a good base on which to build, we had managed to mess it up.

Our original themer was a designer who learned CSS on the job, and our stylesheets were an enormous mess. Absolute positioning, widths specified in pixels, subthemes that had huge amounts of repetitive CSS rules that could've been rolled up into the parent theme. Rewriting these sheets would've been prohibitively expensive, and wouldn't have gained us anything in the eyes of the business.

To add to that, the aforementioned boss' boss was really keen on what we in the biz call “HTML5 mobile apps” that felt more like a native app rather than just a website that is readable on the phone. UI patterns would include swiping to navigate between stories, storing articles to read later, offline support, etc. Basically, not things you can do in any Drupal theme that I know of.

I spent a few days in R&D mode trying to figure out how to fake these things with pre-fetching pages so they'd be rendered already when the user swiped, but it was a mess.

I knew in the back of my head that I was doing it the wrong way, but it took some prototyping to convince myself that what we indeed needed to do was to throw out the book on Drupal theming and do this another way.

I love writing Javascript, and I'd finally found a use case for which one of these JS MV* frameworks might actually fit the bill.

The Drupal setup

So, assuming you have a clean Drupal install spun up already (if you don't, may I suggest drush core-quick-drupal?), you'll want to download a couple of modules to get going.

  • Views (obviously! don't forget the ctools dependency!)
  • Views Datasource (this lets you spit out views as JSON).
  • Devel (for generating some content if you don't already have some)
  • CORS (just trust me, I'll get to this one later)

Or you can just do a drush dl views ctools views_datasource cors devel and be done with it.

Enable all these modules – drush en views views_ui devel devel_generate cors views_json -y.

Generate some content – drush generate-content 50 0 --types=article

Ok, you're ready to hop into the UI!


Pretty much all the action at this point is going to happen in Views, so navigate to admin/structure/views, and “Add new view”.

  1. Name it whatever you want, may I suggest “json articles”?
  2. You want to show “Content” of type “Articles” sorted by “Newest first”.
  3. Create a page, yes. Display format will be “JSON Data Document”. Continue and edit. Screenshot of these settings.
  4. Just add a few fields, since you only have the title so far. Add the Body, the nid, and the date in whatever format you please.
  5. You do want to create a label, but you'll be better off customizing it to be all lowercase and to have no spaces. ie. body, post_date, etc. What you should see at this point

In the preview, you should see a pretty-printed json list of your 10 most recent articles, as generated by Devel.


Congrats! The Drupal part is done! We'll be visiting the more interesting part in the next post.

#drupal #angular #javascript

On Tue, Sep 9, 2014, at 10:58 AM, San wrote:

Dear business owner of Ignoredbydinosaurs.com,

I would like to take a few minutes from your schedule and ask for your attention towards Internet marketing for Ignoredbydinosaurs.com.

As a business Owner you might be interested to gain profit by placing your website among top in search engines. Your website needs immediate improvement for some major issues with your website.

  • Low online presence for many competitive keyword phrases
  • Unorganized social media accounts
  • Not compatible with all mobile devices
  • Many bad back links to your website

Looking at the above issues and other additional improvements for your website, I would request you to give us a chance to fix those issues. Our team of Search Engine and Social Media experts are here to serve you with best inputs. If you are interested in learning more about current status of your website, we would be glad to share WEBSITE AUDIT REPORT of Ignoredbydinosaurs.com for FREE.

You will feel the difference once you get services from our company as we never let our clients expectations go down. Being at the top left of Google (#1- #3 organic positions) is the best thing you can do for your company's website traffic and online reputation. You will be happy to know that, my team is willing to guarantee you 1st page Google ranking for your targeted local keyword phrases.

If my proposal sound's interesting for your business goal, feel free to email us, or can provide me with your phone number and the best time to call you. I am also available for an online meeting to present you this website audit report.

Best Regards,

Jessica

Marketing Consultant

PS I: I am not spamming. I have studied your website and believe I can help with your business promotion. If you still want us to not contact you, you can ignore this email or ask to remove and I will not contact again.

PS II: I found your site using Google search and after having a look over your website I recommend you to implement future technologies such as HTML5 and Responsive Design to make your site more accessible in mobile phone, tablets, desktop etc.

Dear whatever your name is,

I promise I'm better at all aspects of what you're trying to sell me than anyone in your entire “company”.

Thank you, John Grubb

#life

If you're a web developer, I'm sure you've placed this snippet of code more into more than a few projects.

(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-XXXX-Y', 'auto');
ga('send', 'pageview');

Let's unpack it a little bit -

(function(i, s, o, g, r, a, m) {
 i['GoogleAnalyticsObject'] = r;
 i[r] = i[r] || function() {
 (i[r].q = i[r].q || []).push(arguments)
 }, i[r].l = 1 \* new Date();
 a = s.createElement(o),
 m = s.getElementsByTagName(o)[0];
 a.async = 1;
 a.src = g;
 m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');

Now let's make those local variables a little more clear -

(function() {
	var a, m;
	window['GoogleAnalyticsObject'] = 'ga';
	window['ga'] = window['ga'] || function() {
	(window['ga'].q = window['ga'].q || []).push(arguments)
	}, window['ga'].l = 1 \* new Date();
	a = document.createElement('script'),
	m = document.getElementsByTagName('script')[0];
	a.async = 1;
	a.src = '//www.google-analytics.com/analytics.js';
	m.parentNode.insertBefore(a, m)
})()

So if you go to your javascript console and type “GoogleAnalyticsObject”, you'll get back the string “ga”. window.ga is a function, but since functions in javascript are also objects, it has a property called q, which is just an array. This is reminiscent of the old ga.js syntax which went something like this —

var _gaq = _gaq || [];
 _gaq.push(['_setAccount', 'UA-XXXX-Y']);
 _gaq.push(['_trackPageview']);

 (function() {
 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
 })();

_gaq is/was just a plain old Javascript array, which gives it the push() method for free. This new ga.q property serves the exact same purpose, an array to push things into and wait for something to come along later and pop them off. That something that comes along later is whatever is contained in that async script that this snippet also builds.

This is super clever because it doesn't have to wait for anything, it can go ahead and do all its business the instant the page loads and even if the main tracking script doesn't come down for some reason, nothing breaks.

Back to analytics.js...

Whatever you hand as arguments to ga() gets fed into ga.q right here —

window['ga'] = window['ga'] || function() {
	(window['ga'].q = window['ga'].q || []).push(arguments)
}

If you pop open the console on the front page of this blog, and type in ga.q, you'll get this —

> ga.q
[
Arguments[3]
0: "create"
1: "UA-8646459-1"
2: "ignoredbydinosaurs.com"
callee: function (){
length: 3
__proto__: Object
, 
Arguments[2]
0: "send"
1: "pageview"
callee: function (){
length: 2
__proto__: Object
]

Those are stashed in the queue because as soon as that first bit of code is parsed out, there are two quick calls to ga(), and that's exactly what they have as their arguments. It's so simple, it's almost stupid to explain, but the script is so heavily optimized it's not at all obvious on first glance what's going on here.

Moving on, there's another property of the ga function/object – ga.l. ga.l gets initialized to a javascript timestamp (in milliseconds). new Date() returns a javscript Date object, but multiplying it by the integer 1 casts it into a number, which automatically converts it into the number of milliseconds since the epoch. Another way of writing this would be +new Date() – another, albeit less clear way, of performing the same casting to a timestamp. ga.l's purpose is to provide a time for the initial “create” and “pageview” calls to ga().

Lastly, an asynchronous javascript tag is written to make the call to fetch the ga.js script from Google's servers so the real magic can start.

Another interesting bit is that the a and m parameters are not assigned anything at the IIFE call at the end. This leaves them as undefined in the script until they are assigned the script tags toward the end of this snippet. Another way of writing the exact same thing would be to only have (i,s,o,g,r) as parameters to the function, and then declaring var a, m; somewhere in this snippet. I'm not sure off hand if this is a memory or performance optimization or if it's just a handy way to save a couple bytes over the network, but someday I'll figure it out.


Thanks for sticking with me – this is one of the most common little snippets that I've probably placed in my web development career, and I'd never totally dug in to understand what exactly it does beyond writing an async script tag. The pattern of declaring a “plain old javascript array” and then pushing your “stuff” into it as a queue for working with later is an extremely common pattern in 3rd party javascript, since you want everything to be as performant as possible, and you want to make sure you don't break anything if for some reason the rest of your payload script doesn't actually load.

#javascript #analytics