Jump to content

Module: Version Control For Text Fields


teppo
 Share

Recommended Posts

Hi Teppo,

I can click on "Compare with current" but I don't see any diff. I get this on JS console: Uncaught TypeError: Cannot read property 'value' of null

I am on Mac OS X with both Safari and Chrome. 

Link to comment
Share on other sites

I've been experimenting with an addition to VersionControlForTextFields.

Let's say we have a Page with some revision data.

$soup = wire('pages')->get('/specials/soup/');
echo "<br/>Current soup: " . $soup->title;

//Doing this gives Page a new method
wire('modules')->get('ProcessRevisionHistoryForTextFields');

//What kind of soup were we serving last week?
$soup->snapshot('-1 week');
echo "<br/>Old soup: " . $soup->title;

After the call to snapshot() any version controlled fields of the Page will have the values they had at the time specified.

The changes to make this work are made to the ProcessRevisionHistoryForTextFields.module file.

Add one line to the init()

    public function init() {
        parent::init();
        $this->addHook('Page::snapshot', $this, 'pagesnapshot'); //add this line
    } 

Add these:

    public function pagesnapshot($event) {   
        $page = $event->data['object'];
        if ($data = $this->snapshot($page, $event->arguments(0))) foreach($data as $k=>$v) $page->$k = $v;
    }

    public function snapshot($page, $time='') {   
        $id = $page->id;
        if (!$id) throw new WireException("Missing required param id");
        if (!is_integer($time)) $time = strtotime($time);
        if (empty($time)) $time = time();
        
        // how many of this page's fields do we keep history for? (configured for each template)
        $ct = count($page->template->versionControlFields);    

        // find values
        $sql = "
        SELECT m.fields_id, m.pages_id, d.*
        FROM " . self::TABLE_NAME . " AS m, " . self::DATA_TABLE_NAME . " AS d
        WHERE m.pages_id = $id AND d." . self::TABLE_NAME . "_id = m.id
        AND m.timestamp <= FROM_UNIXTIME($time) ORDER BY m.timestamp DESC
        ";
        $result = $this->db->query($sql);

        // generate data (associative array)
        $data = array();
        if ($result->num_rows) {
            while ($row = mysqli_fetch_assoc($result)) {
                $field = $this->fields->get($row['fields_id']);
                if (!array_key_exists($field->name, $data)) $data[$field->name] = $row['data'];
                if (count($data) >= $ct) break;
            }
        }
        return $data;
    }

  • Like 3
Link to comment
Share on other sites

@SteveB: this looks very interesting. I had planned something similar (in the form of changesets, which would've been a bit bigger concept really), but your solution seems a *lot* simpler than what I had in mind.

Would it be OK if I reviewed this, possibly made some small(ish) changes and implemented it as a part of the original module? Of course I'll add @author information to make it (as much as possible) obvious what part was written by whom.. :)

Please note that I've just added LICENSE file, @license phpdoc tags etc. to clarify that this module is licensed under GPLv2. That has always been my intention, but it wasn't obvious earlier. This becomes especially important if code from other authors ends up in the module; that way their code included with the module would also be under GPLv2 from that point on.



Hi Teppo,

I can click on "Compare with current" but I don't see any diff. I get this on JS console: Uncaught TypeError: Cannot read property 'value' of null

I am on Mac OS X with both Safari and Chrome. 

Thanks for reporting this, I'll have to take a closer look at the issue later.

Link to comment
Share on other sites

Would it be OK if I...

Yes, that would be great.  FYI, the first thing I had done was to simply modify ___execute() so I could pass it a page id and it would return the $data array of revisions. Then I wrote the snapshot function. Then I got the idea to hook it to pages.

A couple more thoughts...

Normally the revision data will be limited by data_max_age or data_row_limit settings. If you give the snapshot method a timestamp older than the oldest revision data you get current Page data and don't know whether that's really what the page was like at the specified time or not. The simple change below helps but doesn't tell you the difference between a Page which never changed and a Page with changes that are no longer in the database.

public function pagesnapshot($event) {
 	$page = $event->data['object'];
 	if ($data = $this->snapshot($page, $event->arguments(0))) foreach($data as $k=>$v) $page->$k = $v;
 	else return false; //ADDED THIS
}

Is there a simple way to have the snapshot capability automatically hooked into pages which have tracked fields? I'm wondering if the VersionControlForTextFields gather method could set that up.

I noticed after I deleted a page that the version info for it did not go away. If data_max_age hasn't been set it won't age out and get cleaned up.

  • Like 3
Link to comment
Share on other sites

I noticed after I deleted a page that the version info for it did not go away. If data_max_age hasn't been set it won't age out and get cleaned up.

The module may need a Pages::deleted hook:

public function init() {
  $this->pages->addHookAfter('deleted', $this, 'hookPageDeleted'); 
}

public function hookPageDeleted($event) {
  $page = $event->arguments(0); 
  $this->db->query("DELETE FROM " . self::TABLE_NAME . " WHERE pages_id=" . (int) $page->id); 
}
  • Like 1
Link to comment
Share on other sites

  • 4 weeks later...

I've just pushed to GitHub new version of this module.

This update includes some minor improvements, such as storing version data for pages starting from the moment they're added, and as a bigger addition $page->snapshot() feature exactly as described by @SteveB earlier:

echo "Page title now: {$page->title}<br />";

$page->snapshot("-1 week");
echo "Page title last week: {$page->title}<br />";

I've been working on and off this for a while now and actually had to write a bunch of PHPUnit tests just to make sure that everything works as expected (too many moving parts to keep track of manually.) There's still some work to do here and especially performance-wise more tests to be made, but at least this shouldn't disrupt how other parts of the module work :)

  • Like 9
Link to comment
Share on other sites

  • 3 weeks later...

Update: version 1.3.0, just pushed to GitHub, adds support for repeaters -- or, to be more precise, support for saving revision data for fields that are within repeaters.

Repeaters being pages after all, it seemed most logical to treat them as such. If repeater field added to a template for which version control has been enabled contains fields that are also under version control, values of those fields will be stored just like they would be for the main page (page containing the repeater field).

I'm not confident that my explanation made any sense, so let's just say that this should be self-evident once you try it. Main point is that instead of saving repeater values on per repeater basis the module is treating individual repeater fields (or repeater field fields..) separately :)

Another thing to note is that snapshot feature added in previous update is now module called PageSnapshots. It's still bundled with VersionControlForTextFields and initiated (and automatically installed) by VersionControlForTextFields init() method, so this shouldn't change anything. I'm simply trying to keep the "core" version control module as lean as possible.

Once again, I'd suggest making sure that things work properly before putting this update into real world use. There have been a lot of changes and something could've broken. I've tried to write and run tests vigorously, but those definitely won't catch all issues.. yet :)

  • Like 10
Link to comment
Share on other sites

Version 0.9.2 of this module (just pushed to GitHub) introduces support for multi-language fields. See commit details for more info.

I've also brought in some minor fixes etc. during this weekend, so if you've installed this module you should consider updating.

I installed this today in a PW 2.4 site, but non-default language changes don't really show.

Link to comment
Share on other sites

Q: How difficult would it be to create a daily cron job, to send out a daily email with all changes done in the last 24 hours? (in an abbreviated form, not too geeky)

Quickly scanning the module code, I see you're using three extra DB tables. In the __data table, some entries in the property field look like this: data10141015 or data101410151016. I guess these relate to non-default languages, the number being the language id? If I only edit one single language field value, why does it store multiple languages though?

Link to comment
Share on other sites

I installed this today in a PW 2.4 site, but non-default language changes don't really show.

Could you specify what kind of field this is that's not saving, TextareaLanguage or something else? Based on your second post I'm guessing that data still gets saved to db tables, is that right? Are you using the default theme ("new" or old) or something else?

I'm currently running a fresh test site where this seems to work properly, so any additional information would be helpful.

Q: How difficult would it be to create a daily cron job, to send out a daily email with all changes done in the last 24 hours? (in an abbreviated form, not too geeky)

I'd probably approach this directly via database. Table version_control_for_text_fields contains all changes and timestamps for those, so it would be easy to grab changes made during last 24 hours.

Rest depends very much on what you want to send, i.e. do you want to also describe how content in each field changed and so on, so there's really no simple answer for this one.

Quickly scanning the module code, I see you're using three extra DB tables. In the __data table, some entries in the property field look like this: data10141015 or data101410151016. I guess these relate to non-default languages, the number being the language id? If I only edit one single language field value, why does it store multiple languages though?

Two tables, actually, and you're right in that those are language values. All values are stored mostly because that's what ProcessWire itself does -- it updates all language values simultaneously. You're right in that it feels a bit weird, especially here where rows are stored potentially for a very long time.

I'm not yet sure how to get more specific data from ProcessWire (which language version has changed), so setting these values separately might require another method for handling data. This could also make certain database queries a lot more complicated.

Created an issue for this one so I won't forget it right away..  :)

  • Like 1
Link to comment
Share on other sites

Thanks for the feedback. I'm using mainly TextareaLanguage fields. And one of the four 2.4 default PW admin themes ("classic").

I see all edits in the DB, but they're not visible when I use the watch-icon in page-edit view. I'm also using FieldLanguageTabs, if that matters.

Example:

If I edit a french text-value somewhere, save, let the page reload, and use the version history icon (french lang. tab selected), I don't see anything in the "compare" hover, and clicking on "username / date" doesn't dynamically replace the textinput with the older version, as it does with the default language.

I compared the PW DB-field names for one particular field - they look like this:

pages_id

data // default language

data1014 // en

data1015 // us-en

data1016 // fr

In your table version_control_for_text_fields__data, I see that each edit creates four DB-entries (one per language).

The "property" fields look like this:

data

data1014

data10141015 // shouldn't this be data1015?

data101410151016 // shouldn't this be 1016?

I guess it could be a JS issue too, since I'm using the multilang field tabs module, but disabling that would not fare well with my client :-|

Link to comment
Share on other sites

@dragan: thanks, that actually solved it. There was an issue with storing language versions; language ID's were getting "stacked" instead of last one being used, which resulted in useless data. Looks like I never properly tested this with multiple language versions..

I've just pushed fix to GitHub and would suggest you (and everyone else reading this) to update the module. To fix existing data you can do something like this in your database:

UPDATE version_control_for_text_fields__data SET property = "data1015" WHERE property = "data10141015";
UPDATE version_control_for_text_fields__data SET property = "data1016" WHERE property = "data101410151016";
# .. and so on
  • Like 1
Link to comment
Share on other sites

I've surely written some crappy replies earlier, but this was first one bad enough to be removed ;)

No need to restore anything, really. Main point of that reply was that this module had a bug that @dragan uncovered.

When more than two languages were in use, values were getting stacked, i.e. version_control_for_text_fields__data contained property values such as "data10141015" instead of "data1015", "data101410151016" instead of "data1016" and so on. This is fixed in current version at GitHub so I strongly suggest everyone using this module to update to that.

To fix existing data you have to replace those nonsensical values directly in your database:

UPDATE version_control_for_text_fields__data SET property = 'data1015' where property = 'data10141015';
UPDATE version_control_for_text_fields__data SET property = 'data1016' where property = 'data101410151016';
# .. and so on, depending on actual language page IDs (broken values should be easy to spot)
  • Like 3
Link to comment
Share on other sites

Just a little word of caution: The fieldtype checkbox is also in the allowed field-types. At least with multi-lang sites, this creates strange entries in the DB:

default language: 1 (OK - checked)

all other languages: some string value from another, totally unrelated field

Maybe leave out checkbox input type in the module config / settings dialogue altogether?

(I know, I'm nitpicking here... to avoid confusion, I have removed that field from the watch settings)

  • Like 1
Link to comment
Share on other sites

@dragan: that's good to know and definitely not nitpicking -- my intention at this point is to make this module support all native fieldtypes (except images and files, though I'll hopefully get there eventually), including checkboxes. I'm starting to realize that not writing specific tests for multilanguage part was a huge mistake, obviously there are quite a few weird things happening there :)

As soon as I get the time, I'll write a test case for checkboxes (for starters) and see what exactly is going on. It should work.

  • Like 3
Link to comment
Share on other sites

  • 2 weeks later...

Hello and thanks for this great module.

It seams to be working fine except that when is activated for a template, if that template contains image fields, after dragging and dropping the image, this module breaks the upload response and immediate image that was uploaded is not shown (instead is returned the return form inject function of this module). The version control is activated only for body field... Just wanted to mention this. 

Please tell me is I am doing something wrong.

Thank you.

  • Like 1
Link to comment
Share on other sites

Hello, this module looks fantastic. But unfortunately, I encounter an error while setting up the module.

I'm using ProcessWire 2.3.0 with Languages Support module. I installed the latest version of this module (1.3.1), but when I try to configure the module, it returns an error:

"Method TemplatesArray::makeCopy does not exist or is not callable in this context"

The other modules - Page Snapshot and Revision History For Text Fields are installed correctly and the configuration of Revision History is working.

Any ideas to solve this issue? Thanks in advance!

  • Like 1
Link to comment
Share on other sites

@adrianromega and @homma: thanks for reporting these issues. I've been terribly busy lately, but will take the time to process these properly (hopefully) this weekend.

Your posts are noted and much appreciated :)

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...