Creating a custom compound field for CCK

I’m working on a project that partially involves the development of a website in Drupal to act as a directory of people who have graduated from a given University. Seems easy. I went into the project thinking it would be a trivial application of Taxonomies, or maybe some generic CCK fields.

Nope. Turns out the problem is much more difficult and complex than I initially thought.

Taxonomies won’t work, because of the need to tie a number of values together, namely the year the degree was awarded (say, “1992”), the type of degree (say, “BSc”), the specialization of the degree (say, “Zoology”), and the granting institution (say, “University of Calgary”).

That could be an easy thing to solve with CCK – just add four text fields. Done.

BUT – people can earn more than one degree. Of different types, in different years, from different institutions.

Taxonomies fail. Generic CCK fields fail.

What I came up with is a new CCK field type, cryptically named “University Degrees”, that defines the four values that describe a degree. This solves the problem quite tidily, and supports multiple values, predefined valid sets of values, and can integrate with Views to be used as filters and sorting fields.

In building this module, I leaned heavily on a couple of web pages (CCK Sample and What is the Content Construction Kit?) that describe how parts of the module should work, and provided some sample code. In the spirit of contributing back what I learned, I’m going to document the module to help others needing to do similar things.

To start with, I just created a new folder to hold the module, and called it “universitydegrees“. Into this folder, I copied the .install and .info files from another module and customized them to suit my needs (the universitydegrees.install file doesn’t actually do anything, and the universitydegrees.info file is just for listing the module in the Drupal admin page). If you want to follow along (or use the files as a starting point to make something else) the files for my lame alpha version of the module are available here.

The file where all the fun stuff happens is universitydegrees.module

The first method, universitydegrees_field_info(), defines how the field shows up in the CCK admin interface.

function universitydegrees_field_info() {
  return array(
    'universitydegrees' => array('label' => 'University Degree'),
  );
}

This method works with the universitydegrees_widget_info() method, and will list any available widgets under the “University Degree” field type. In this case, I just added a simple widget description, like this:

function universitydegrees_widget_info() {
  return array(
    'universitydegrees_text' => array(
      'label' => 'University degree earned',
      'field types' => array('universitydegrees'),
    ),
  );
}

So, a new field called “University Degree” will be available, like this:

Before actually adding the field to any content type, we need to define what data it’s going to store. This happens in the universitydegrees_field_settings() method:

function universitydegrees_field_settings($op, $field) {
  switch ($op) {

    case 'save':
      return array('year', 'degreetype', 'programme', 'institution');

    case 'database columns':
      $columns = array(
        'year' => array('type' => 'int', 'not null' => TRUE, 'default' => '2008', 'sortable' => TRUE),
        'degreetype' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => "''", 'sortable' => TRUE),
        'programme' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => "''", 'sortable' => TRUE),
        'institution' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => "''", 'sortable' => TRUE),
      );
      return $columns;

    case 'filters':
      return array(
        'default' => array(
          'list' => '_universitydegrees_filter_handler',
          'list-type' => 'list',
          'operator' => 'views_handler_operator_or',
          'value-type' => 'array',
          'extra' => array('field' => $field),
        ),
      );
  }
}

The 'save' portion defines what values will be saved. The 'database columns' portion defines the MySQL code to generate the fields in the database to store each of the values. In this case, 'year' will be an int and the remaining values will be varchar(255) text strings.

Now, to define how the widget that is used to edit the values should behave. This is the universitydegrees_widget() method.

function universitydegrees_widget($op, &$node, $field, &$items) {
    $options_year = _universitydegrees_values_year();
	$options_degreetype = _universitydegrees_values_degreetype();
	$options_institution = _universitydegrees_values_institution();

   switch ($op) {

      case 'form':
        $form = array();
        $form[$field['field_name']] = array('#tree' => TRUE);

        if ($field['multiple']) {
          $form[$field['field_name']]['#type'] = 'fieldset';
          $form[$field['field_name']]['#description'] = t('Degrees Earned');
          $delta = 0;
          foreach (range($delta, $delta + 2) as $delta) {
			$item = $items[$delta];
			$form[$field['field_name']][$delta]['#type'] = 'fieldset';
            $form[$field['field_name']][$delta]['year'] = array(
              '#type' => 'select',
              '#title' => t('Year'),
			  '#default_value' => array_search($items[$delta]['year'], $options_year),
			  '#options' => $options_year,
            );
            $form[$field['field_name']][$delta]['degreetype'] = array(
              '#type' => 'select',
              '#title' => t('Degree Type'),
			  '#default_value' => array_search($items[$delta]['degreetype'], $options_degreetype),
			  '#options' => $options_degreetype,
              '#required' => ($delta == 0) ? $field['required'] : FALSE,
            );
            $form[$field['field_name']][$delta]['programme'] = array(
              '#type' => 'textfield',
              '#title' => t('Degree, major or concentration'),
			  '#default_value' => $items[$delta]['programme'],
              '#required' => ($delta == 0) ? $field['required'] : FALSE,
            );
            $form[$field['field_name']][$delta]['institution'] = array(
              '#type' => 'select',
              '#title' => t('School'),
			  '#default_value' => array_search($items[$delta]['institution'], $options_institution),
			  '#options' => $options_institution,
              '#required' => ($delta == 0) ? $field['required'] : FALSE,
            );
          }
        }
        else {
			$form[$field['field_name']][0]['#type'] = 'fieldset';
            $form[$field['field_name']][0]['year'] = array(
              '#type' => 'select',
              '#title' => t('Year'),
			  '#default_value' => array_search($items[0]['year'], $options_year),
			  '#options' => $options_year,
			  '#required' => $field['required'],
            );
            $form[$field['field_name']][$delta]['degreetype'] = array(
              '#type' => 'select',
              '#title' => t('Degree Type'),
			  '#default_value' => array_search($items[0]['degreetype'], $options_degreetype),
			  '#options' => $options_degreetype,
              '#required' => $field['required'],
            );
            $form[$field['field_name']][0]['programme'] = array(
              '#type' => 'select',
              '#title' => t('Degree, major or concentration'),
			  '#default_value' => $items[0]['programme'],
              '#required' => $field['required'],
            );
            $form[$field['field_name']][0]['institution'] = array(
              '#type' => 'select',
              '#title' => t('School'),
			  '#default_value' => array_search($items[0]['institution'], $options_institution),
			  '#options' => $options_institution,
              '#required' => $field['required'],
            );
        }
        return $form;

      case 'process form values':


        foreach ($items as $delta => $item) {
         
			// don't store empty stuff.
			if (empty($items[$delta]['year']) && $delta > 0 ) {
				unset($items[$delta]);
			} else {
				// do an array lookup to store the actual value of the selection, not just the number of its index position.
				$items[$delta]['year'] = $options_year[$items[$delta]['year']];
				$items[$delta]['degreetype'] = $options_degreetype[$items[$delta]['degreetype']];
				$items[$delta]['institution'] = $options_institution[$items[$delta]['institution']];
			}
        }
  }
}

The $options_year, $options_degreetype, and $options_institution variables are just calling methods that return arrays of values. I did this initially to separate the options from the various places they are used so I could change where the data comes from more easily (it’d be really cool to tie these into taxonomies or something more user-editable…)

The 'form' portion generates the portion of the form that is used to edit the content associated with an instance of the field. It has two sub-portions, one for handling multiple values, and one for single values. I’m really not sure why that isn’t collapsed into a single chunk that can grok both single and multiple values, but I was following a recipe and left it that way. For now… The only portion I really cared about was the multivalue stuff anyway, because that’s how I’ll be using the content type.

The code for that portion defines a fieldset, called “Degrees Earned” and starts pumping out chunks of forms to present editors for each value. It creates a nested fieldset for each value, and presents a Select field for “Year”, “Degree Type” and “Institution” – and a textfield for “Degree, major or concentration”.

One thing that I didn’t like in the out-of-the-box widget behaviour was that it stored the index of the value, rather than the actual value itself, in the database. While that worked, it wasn’t ideal – if I changed the list of options, the index values would be invalid. So I modified the code to lookup the actual value selected and store that for the select widgets. (the #default_value portions of the code present the current value, if any, while editing).

The 'process form values' portion does any processing of the values prior to saving in the database. This is where I convert the stored values from plain, dumb, indexes to a more meaningful text string containing the actual selection.

Here’s what the form looks like, with the widgets in place:

Now that we have a field defined, have provided a way to save values in the database, and described how the form widgets should behave, we need to display values on the node page. I took a shortcut and only defined a single hard-coded way to present the values. I’ll eventually work in a way to customize the display using a list of formatters. In the meantime, the universitydegrees_field_formatter() method handles the conversion from raw data into displayable text.

function universitydegrees_field_formatter($field, $item, $formatter, $node) {
	$text = '';

	$text = $item['degreetype'] . ' ' . $item['programme'] . ' (' . $item['institution'] . ', ' . $item['year'] . ')';
	return $text;
}

With the formatter in place, a node with this CCK field type will look like this:

And that’s it. Now, I can add a University Degree to any CCK content type, and be able to define all four values that describe a degree awarded to an individual. This pattern could easily be generalized for other compound data types, as well.

Next, I need to expose full Views functionality, so each value can be used for filtering and sorting Views. And, I need to provide a more flexible way to display the values, using the formatters. And, I need to abstract the lists of Years, Degrees and Institutions so that they are editable by users without having to modify the source code for the module…

48 thoughts on “Creating a custom compound field for CCK

  1. Thanks for publishing all your steps, D’Arcy! I’m finding this useful as a guide for how to create a new custom module, which I have not yet had the opportunity to learn.

    Richard

  2. Hello, D’Arcy,

    I just did a read over this, and I’m sure that I’m missing something obvious, but wouldn’t a combination of taxonomy and/or nodereferences/user references, in combination with the Computed Field module, get this done?

    Each degree is a cck node type, with a user reference pointing back to the user — the computed field module could grab all associated nodes for a user, and you could also wrap the data inside the computed field in whatever divs you needed to handle formatting —

    This would also take care of the views integration, because you’re using taxonomy and cck field types that are already exposing their data through views — Views arguments, or themed filters, would then allow you to sort, or you could use the faceted search module —

    @Richard — the handbooks also have a guide for building modules — if you google “Module developers guide site:drupal.org” that should bring it up — I’d add the link, but I don’t want to trip the spam filter.

    Cheers,

    Bill

  3. @Bill – at first glance it looks really obvious. Taxonomies and a simple CCK node. The problem is that I need to be able to do things like assign a Year in which a degree was awarded. And, perhaps, if the degree was Honours or not. etc…

    A CCK/Taxonomy might work for just a single degree – have say a taxonomy for Year, one for Degree, one for Honours, etc…

    BUT

    What if a person has 3 degrees? How do you say that Year #1 is associated with Degree #1? The taxonomies get jumbled pretty quickly, unless I’ve missed something.

    The only way I see doing the level of granularity I need, is with a compound field. One with values for each piece of data pertaining to a degree earned by a person (year, type, specialization, honours, etc…) otherwise it’s difficult to make sense of the various bits of taxonomic/CCK data.

  4. At a quick glance (and little in the way of actual thought 🙂 ) here’s how I’d tackle this:

    Create a cck node for Degrees (and I’d use the autonodetitle module to eliminate the node title)

    On this Degree node, eliminate the body field, and create 5 taxonomy categories:
    Year
    Degree
    Degree Type
    Special Awards/Honors
    School

    Then, add a user reference cck field to tie the degree to a specific user. You could use the autonodetitle module to generate a title that displayed relevant information about the individual degrees.

    This one user can be tied to multiple degrees, and the degrees can be as granular as you need them to be.

    With that said, either way will work — but maintaining a compound field, creating views integration, and generalizing/exposing the admin features via a clean UI is a lot of work — to say nothing of the work involved in maintaining this code over time.

  5. right, but there could be thousands of permutations of year, degree, degreetype, honours, school, etc… so creating a new Node to combine them and associate them with a profile would be pretty unwieldy. Each person will have a different set of degrees with details, so this could mean a LOT of extra nodes to be created. with the compound field module, the extra work is in the code, so the users don’t have to worry about it (as much)…

  6. Hi,

    I may be looking at this in the wrong way, so please correct me if I am, but this seems like it should be very simple to me:

    I would create a new content type, called University Degree. Then I would add the 4 required fields (Year, Type, Degree & School) as standard CCK fields, using whatever combination of select boxes or text boxes you require. There is no reason to use taxonomy for any of this data.

    Then, each user can create an unlimited number of University Degree’s (in practice this will obviously only be 1 or 2 each).

    Using Views, you could very easily create a list of each user’s degrees or any other list based on any of the 4 fields.

    I’m sorry if I’ve mis-understood your requirements, but this type of approach has always worked for me.

    Ian

  7. Sorry, I don’t think I made it clear that each University Degree would be an individual node. So a user wanting to add 3 University Degree’s would need to create 3 separate University Degree nodes.

  8. Interesting. 2 Drupalistas got flagged as “spam” by Akismet. Sorry about that. I’m _sure_ there’s nothing nefarious going on there… 😉

    and yeah… education_field sounds great! I’ll _definitely_ be checking that out! THANKS!

  9. @Eleazan yes. sorry! I should have mentioned the Drupal version! I’m still running Drupal 5 for all of my production sites, so needed the CCK field for Drupal 5. There are probably subtle changes to the API of CCK for Drupal 6, but I haven’t looked at that yet…

  10. Has anyone bothered to make this into a configurable compound field module? Cos I’d be up for it, but I don’t want to do it if it’s already been done. And what was the point of releasing ‘education field’ as a drupal project? you may as well release a module called “education field that can only be used by tall people living in north carolina, on wednesday afternoons, or by medium to tall people on christmas day” or somethign equally niche

    Nice basis for something bigger and better I reckon.

  11. @danielb I didn’t find a generalized compound field module, or I would have used it 🙂 And this module was intended as a quick-and-dirty way to put the compound field together to meet an exact need of a client – and I thought it would come in handy to document how I did it online so others could adapt as needed (or, maybe even generalize it…)

  12. I think you just saved my ass today.

    I’ll try to make your compound work a generalized compound field module. Wish me luck.

  13. ooops!!!

    Forget about it. There’s a solution that doens’t require coding.

    Create a fieldgroup.
    Put the CCK fields you want into there.
    Create CCK fields from vocabularies if required using CCK taxonomy fields module (http://drupal.org/project/cck_taxonomy).
    Put your CCK fields into the fieldgroup.
    Get and install fieldgroup table module (http://drupal.org/project/fieldgroup_table)
    Now, edit your fieldgroup and look at the last option: “Multiple values”.

    Hey! It’s done, you can add multiple “groups” of CCK fields, properly tied in rows, so to say.

  14. @Nachenko it sounds pretty untested right now though… not sure I want to use that on a production site (but then again, I’m using a custom module on it… but at least with that one I can predict changes to it…)

  15. Yes, it’s untested, but I’ll try to get the code running. The module fieldgroup_table is about 120 lines of code, I’ll have a look at it and try to fix it tomorrow and the day after. Stay tuned! If I manage to get it working, I’ll create a patch file and upload it in the module’s project page.

    Anyway, if I can’t fix it, your code seems a good starting poing to build a general purpose module. Not yet sure on what to do, but I need a compound field for a Sports club site I’m working on, so one way or another I’ll push it to get it done.

    Wish me luck!

  16. Hi All,

    I was testing the custom module in one of my test sites. I was able to add first degree but it doesn’t let me create another degree. I think It will be done through link module but i don’t know how or there is any completely different approach??

    Can anyone guide me?? I am fairly new to drupal.

    Thanks in advance.

  17. i’m having the same issue – but instead of university degrees, its line items on a customer order.

    ubercart isnt what i’m after, as the orders for the business i’m working with come via fax and telephone… so its just a simple customer order node type , with an ability to add/remove “line items” corresponding to the “products” they are ordering.

    in my case – its web advertising slots.

  18. oh – by the way – thanks for the write up on this… i’m gonna hack/adapt this for “line items”…

    if i get it working , i’ll post it up on the drupal forums.

  19. You are my hero!!! Thank you for this tutorial and thank you for letting us download the module. I’ve been trying to create essentially the same exact thing except for rental rates for rental properties (such as hotels, etc.). I’ve been struggling trying to find good code as an example and I can’t get my module to work. I love you! (ooh is that too strong!?)

  20. The general problem is that you have to create nodes for a lot of stuff you just don’t want nodes for, and sometimes things you don’t want taxonomies for.

    And trying to tie together multiple node reference and/or taxonomy reference fields in a repeating table (there are several modules for 5.x series which will help you out) is rather cumbersome.

    Then if you want to enforce how many of each is available, you end up with a real mess.

    Example:

    A “politician” belonged to party “a” from 1995-1998, then he became an independent and in joined party “b” in 2005. In party “b” he was first a lay member for a year, then elected into the “comittee” for two years, then he became the “vice-president” the next two years until he finaly made it to the top. Of course there can only be one president, though several vice-presidents and comittee members…

    For each of those affiliations I would need to create a node; but those nodes would be available for all politicians to add to their resumes, but I would really just like to see that data available in node reference fields for just one context, that politician.

    So we end up with adding a node reference in each line item to tie it back to the original politician, create a view to use as the source of the line items, and we get a lot of CCK madness.

    That’s when I want to hire an Indian to program a whole module from scratch based on a database diagram.

  21. Not to mention that for all of those positions in the comittees I would need to have one content type, if I want a module like node relativity to help me out.

    There is some glue missing!

  22. Ah the elusive combo/compound field. I looked at the Fieldgroup Table module, but the module is for D5 (and even then there isn’t a version that works with 5.8). That’s fine, I’d rather solve this problem in D6.

    I tried myself to update this for D6 – but without any luck.

    Any chance you might be doing that in the near future?

  23. Hey D’Arcy
    There is error in your guide

    function universitydegrees_field_settings($op, $field) {
    switch ($op) {

    case ‘save’:
    return array(‘year’, ‘degreetype’, ‘programme’, ‘institution’);

    You don’t need to save these here – I think ‘save’ in this function is for saving field settings but you are saving field values

    More on topic:
    First, I tried module ‘fieldgroup table’ and it worked for me only with “text field” widget types. Select, checkboxes, radio etc won’t work. So it kind of kills all the pro’s of this module. Also there is big limitation in that you can only have 1 value per row.

    Then I tried your path for making my money commission field, and now, using your guide and modules ‘link’, ‘money’, ‘education_field’ as examples, I made my custom “commission” field type. So kudos for posting this.

    But now I face problems of removing bugs, testing and all this is hell for me ’cause I’m not experienced developer. Looking at this comment:

    With that said, either way will work — but maintaining a compound field, creating views integration, and generalizing/exposing the admin features via a clean UI is a lot of work — to say nothing of the work involved in maintaining this code over time.

    I now have feeling that compount field is probably wrong way. Making lots of nodes is not big problem (adding more hardware is easy and cheap these days ). But if I have my custom compound field, what if I want to upgrade to D6 in,say, 6-8 months ? Then I will need to upgrade my module, and if its already hell for me, it will not be easier in future…

    So for any non-experienced programmer, I would recommend to go with separate node solution, and helper modules such as addnode, nodereference or node relativity and such. This way you only develop small portion of code and most of the code is maintained and supported by community

  24. Thinking about pro’s and con’s again.
    Say, we have integer ‘age’ field in profile (for clarity, not core profile but node-based, such as Bio ) , and separate node type for education degree.
    What if we want to make view of people of certain age and having certain degrees ?
    With compound degree field, as far as I understand, this is easy

  25. Heh, for anyone reading this in future, it is possible to make views containing fields from 2 node types with “Views fusion” module. So I’m gonna try that road, wish me luck 😛

  26. This is a great tutorial! Looks very useful. Do you have any plans to port this tutorial for Drupal 6?

  27. I don’t have any plans for that right now – none of my sites run on D6 yet because I use modules that still don’t run on it…

  28. The fieldgroup table does not work for this purpose. A compound field must be able to handle multiple instances of multiple-value fields, not just making a table over a group of multiple-value fields.

    IMHO this is a deep problem with CCK and needs to be addressed by the CCK developers. For example, its interaction with view filters will be significantly more complex than for non-compound fields.

    With the currently available methods, one must use a node-reference and a separate CCK node type for each compound-type. This however creates some ugly problems with usability for creating and editing nodes. Subform, autonode, and some other stuff might help, I don’t know.

  29. @Andrey Tretyakov: It is true that with your own CCK field type in a custom module, it is up to you to convert to D6 when the time comes. However, to me that is a pro. Otherwise, I am dependent on other module authors to convert their module to D6. Some modules becomes abandoned and therefor will never be converted to D6.

    @aws: The CCK developers are currently working on that for D6:
    http://drupal.org/node/119102

  30. This writeup and dialog is very helpful.

    I’ve looked all over for a non coding solution for what could be termed subforms or child forms on the same page. I basically want to define a one-to-many relationship between two nodes and have both on the same form. This comes up all the time for me.

    The closest out of the box solution I see is: #182867

    Any current thoughts on this? Is there a need for a one_to_many_cck_form_on_a_single_page? Or is this best done with some sort of views?

  31. @bill. Thanks. This will be helpful in one project I’m working on, but not the others because of accessibility and design constraints.

    1. @Wim

      Is it still a patch right now and not yet a module? Is it merging with CCK core? I’d love to see something a little more formal – even a dev release – as a module …

  32. D’arcy, a sort of related question I wanted to throw out to the CCK users of the world: when and why use CCK?

    I’ve got a site with about fifteen different content types, and I’m starting to wonder at what point it becomes ridiculous… I mean, CCK is awesome and versatile and it works … but am I getting out of hand? 🙂

    I wanted to find out what the general average number of fields is for mid-sized Drupal sites, or if there’s a mild consensus on when to create a new CCK field vs. creating a more generic one and specifying things with taxonomy or something instead…

    I’m happy to use CCK until the cows come home, but I was just wondering if anybody else had thought about scalability etc.?

    1. I’m not sure – sorry! I haven’t been able to do anything in Drupal 6 yet because the modules I need aren’t all there yet.

Comments are closed.