Jump to content
teppo

Module: Process Changelog

Recommended Posts

Hi Teppo

Is there anyway to show the changelog table in a template? I want to show it in frontend templates.

You'll need to load up the ProcessChangelog.css file to make it styled correctly (or style yourself), but this does the trick otherwise:

$changelog = $modules->get("ProcessChangelog");
echo $changelog->execute();
  • Like 3

Share this post


Link to post
Share on other sites

Yup, thanks Adrian! Had completely missed this one, but glad it (apparently) got solved :)

  • Like 1

Share this post


Link to post
Share on other sites

I've an issue with ProcessChangelog right now which I originally thought was a 3.X issue. Actually, I'm having it across a few 2.7 sites too.

I suspect I had tried to uninstall it the wrong way in the past and that's why I'm getting the following errors

Module 'ProcessChangelogHooks' dependency not fulfilled for: ProcessChangelog

 Unable to install module 'ProcessChangelog': SQLSTATE[23000]: Integrity constraint violation: 1062... 32 secs 

Unable to install module 'ProcessChangelog': SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'changelog-22' for key 'name_parent_id'

When I try to remove it via Modules, I get

Failed to delete module 'ProcessChangelog' 

I'm relatively comfortable with directly editing the database directly via phpMyAdmin.

Is manually deleting the module rows ProcessChangelogHooks the best way forward?

Share this post


Link to post
Share on other sites

I was able to add pageviews before and now on a fresh install of PW3 without the previous successful attempt to draw from, here's what I have for ProcessChangelogHooks.module:

Spoiler

<?php

class ProcessChangelogHooks extends WireData implements Module, ConfigurableModule {

    /**
     * Return information about this module (required)
     *
     * @return array
     */
    public static function getModuleInfo() {
        return array(
            'title' => 'Changelog Hooks',
            'summary' => 'Hooks required by Process Changelog for collecting data',
            'href' => 'http://modules.processwire.com/modules/process-changelog/',
            'author' => 'Teppo Koivula',
            'version' => '1.1.3',
            'singular' => true,
            'autoload' => true,
            'requires' => 'ProcessChangelog'
        ); 
    }

    /**
     * Default configuration for this module
     *
     * The point of putting this in it's own function is so that you don't have to specify
     * these defaults more than once.
     *
     * @return array
     */
    static public function getDefaultData() {
        return array(
            'operations' => array(
                "added" => __("added"),
                "moved" => __("moved"),
                "edited" => __("edited"),
                "trashed" => __("trashed"),
                "renamed" => __("renamed"),
                "deleted" => __("deleted"),
                "restored" => __("restored"),
                "published" => __("published"),
                "unpublished" => __("unpublished"),
				"viewed" => __("viewed")
            ),
            'schema_version' => 1,
        );
    }
    
    /**
     * Name and latest schema version for database table used by this module
     *
     */
    const TABLE_NAME = 'process_changelog';
    const SCHEMA_VERSION = 2;

    /**
     * Populate the default config data
     *
     * ProcessWire will automatically overwrite it with anything the user has specifically configured.
     * This is done in construct() rather than init() because ProcessWire populates config data after
     * construct(), but before init().
     *
     */
    public function __construct() {
        foreach(self::getDefaultData() as $key => $value) {
            $this->$key = $value;
        }
    }

    /**
     * Module configuration
     *
     * @param array $data
     * @return InputfieldWrapper
     */
    static public function getModuleConfigInputfields(array $data) {

        // this is a container for fields, basically like a fieldset
        $fields = new InputfieldWrapper();

        // since this is a static function, we can't use $this->modules, so get them from the global wire() function
        $modules = wire('modules');

        // merge default config settings (custom values overwrite defaults)
        $defaults = self::getDefaultData();
        $data = array_merge($defaults, $data);

        // which operations should be tracked?
        $field = $modules->get("InputfieldCheckboxes");
        $field->name = "operations";
        $field->label = __("Operations");
        $field->addOptions($defaults['operations']);
        $field->value = ($data['operations'] === $defaults['operations']) ? array_keys($defaults['operations']) : $data['operations'];
        $field->description = __("You can choose which operations to keep track of here.");
        $field->notes = __("Note that unchecking operations later won't remove rows containing them from database. Instead new rows of those types will no longer be created and existing ones won't be visible anymore.");
        $fields->add($field);

        // should caller (script or URL that triggered this action) be logged?
        $field = $modules->get("InputfieldSelect");
        $field->name = "log_caller";
        $field->label = __("Caller logging");
        $field->description = __("Enable logging of path/URL for script that triggered action?");
        $field->addOptions(array(
            null => __("Disabled"),
            'external' => __("For external callers only (CLI and external applications)"),
            'all' => __('For all callers (CLI, external applications and ProcessWire itself)')
        ));
        $field->notes = __("This can be useful when trying to find out why certain change was triggered. On the other hand it adds to the size of your database table and slightly slows script execution, which is why it's disabled by default.");
        $field->value = isset($data[$field->name]) ? $data[$field->name] : null;
        $fields->add($field);

        // for how long should collected data be retained?
        $field = $modules->get("InputfieldSelect");
        $field->name = "data_max_age";
        $field->label = __("Data max age");
        $field->description = __("For how long should we retain collected data?");
        $field->notes = __("Automatic cleanup requires LazyCron module, which isn't currently installed.");
        if ($modules->isInstalled("LazyCron")) {
            $field->addOptions(array(
                '1 WEEK' => __('1 week'),
                '2 WEEK' => __('2 weeks'),
                '1 MONTH' => __('1 month'),
                '2 MONTH' => __('2 months'),
                '3 MONTH' => __('3 months'),
                '6 MONTH' => __('6 months'),
                '1 YEAR' => __('1 year')
            ));
            $field->notes = __("Leave empty to disable automatic cleanup.");
            $field->value = isset($data[$field->name]) ? $data[$field->name] : null;
        }
        $fields->add($field);

        return $fields;

    }

    /**
     * Initialization function
     *
     * This function attachs required hooks.
     *
     */
    public function init() {

        // update database schema (if not the latest one yet)
        if ($this->schema_version < self::SCHEMA_VERSION) {
            $this->updateDatabaseSchema();
        }

        // remove expired rows daily
        $this->addHook("LazyCron::everyDay", $this, 'cleanup');

        // add hooks that gather information and trigger insert
        $this->pages->addHook('added', $this, 'logPageEvent'); 
        $this->pages->addHook('moved', $this, 'logPageEvent'); 
        $this->pages->addHook('renamed', $this, 'logPageEvent'); 
        $this->pages->addHook('deleted', $this, 'logPageEvent'); 
        $this->pages->addHook('saveReady', $this, 'logPageEvent');
		$this->pages->addHook('viewed', $this, 'logPageEvent');

    }

    /**
     * Initialization when $page is known
     *
     * Attach hook to ProcessPageEdit::buildFormSettings
     *
     */
    public function ready() {
        if ($this->page->template == 'admin' && $this->page->process == 'ProcessPageEdit') {
            $this->addHookAfter('ProcessPageEdit::buildFormSettings', $this, 'hookPageEdit');
        }
    }

    /**
     * Adds a Changelog section to the Settings tab in the page editor
     *
     */
    public function hookPageEdit(HookEvent $event) {
        $form = $event->return;
        $process = $event->object;
        $editPage = $process->getPage();
        try {
            $query = $this->database->prepare("SELECT COUNT(*), MIN(timestamp) FROM " . self::TABLE_NAME . " WHERE pages_id=:id");
            $query->bindValue(':id', $editPage->id, PDO::PARAM_INT); 
            $query->execute();
            list($num_edits, $timestamp) = $query->fetch(PDO::FETCH_NUM);
        } catch(Exception $e) {
            $this->error($e->getMessage());
            $num_edits = 0;
        }
        if ($num_edits) {
            $field = $this->modules->get('InputfieldMarkup');
            $processPage = $this->pages->get('process=' . $this->modules->getModuleID('ProcessChangelog'));
            $field->label = $this->_('Changelog'); // Changelog field label
            $out = sprintf($this->_n('%d edit since %s', '%d edits since %s', $num_edits), $num_edits, $timestamp);
            if ($this->user->hasPermission('changelog')) $out .= " - <a href='$processPage->url?pages_id=$editPage->id'>"
                                                  . $this->_('View History') . "</a>";
            $field->attr('value', "<p>$out</p>");
            $form->append($field);
        }
    }
    
    /**
     * Delete data older than given interval
     *
     * @param string|HookEvent $interval Interval, defaults to data_max_age setting
     */
    public function cleanup($interval = null) {

        if ($interval instanceof HookEvent) $interval = $this->data_max_age;
        if (is_null($interval) && is_null($this->data_max_age)) return;
        else if (is_null($interval)) $interval = $this->data_max_age;

        // @todo check if $interval can be a bound param (not sure it can)
        $interval = $this->database->escapeStr($interval);
        $sql = "DELETE FROM " . self::TABLE_NAME . " WHERE timestamp < DATE_SUB(NOW(), INTERVAL $interval)";
        try {
            $this->database->exec($sql);
        } catch(Exception $e) {
            $this->error($e->getMessage());
        }
    }

    /**
     * Based on event method and other information available this
     * method parses required data and triggers insert method.
     *
     * @param HookEvent $event
     */
        public function logPageEvent(HookEvent $event) {
        // render has no arguments
        if ($event->method == "render") $page = $event->object;
        else $page = $event->arguments[0];

        // don't log operations for repeaters or admin pages
        if ($page instanceof RepeaterPage || $page->template == "admin") return;

        // grab operation from event
        $operation = $event->method;
        if ($operation == "saveReady") $operation = "edited";
        if ($operation == "render") {
            //if ($page->template->name !== "vessel") return;
            $operation = "viewed";
        }

        // only continue if this operation is set to be logged
        if (!in_array($operation, $this->operations)) return;

        $fields_edited = array();
        if ($operation == "edited") {
            // skip new pages or pages being restored/trashed
            if (!$page->id || $page->parentPrevious) return;
            if ($page->isChanged()) {
                foreach ($page->template->fields as $field) {
                    if ($page->isChanged($field->name)) {
                        $fields_edited[] = $field->name;
                    }
                }
                // only continue if at least one field has been changed (or
                // if status has changed trigger new event for that)
                if (!count($fields_edited)) {
                    if ($page->isChanged("status")) {
                        $event->method = $page->is(Page::statusUnpublished) ? "unpublished" : "published";
                        $this->logPageEvent($event);
                    }
                    return;
                }
            } else return;
        } else if ($operation == "renamed") {
            // if previous parent is trash, page is being restored
            if ($page->parentPrevious->id == $this->config->trashPageID) return;
            // if current parent is trash, page is being trashed
            else if ($page->parent->id == $this->config->trashPageID) return;
        } else if ($operation == "moved") {
            if ($page->parent->id == $this->config->trashPageID) {
                // page is being trashed
                $operation = "trashed";
            } else if ($page->parentPrevious->id == $this->config->trashPageID) {
                // page is being restored
                $operation = "restored";
            }
        }

        // details about page being edited, trashed, moved etc.
        $details = array();

        if ($page->title) $details['Page title'] = $page->title;

        $details['Page name'] = $page->name;
        if ($page->namePrevious) {
            $details[($operation == "moved" ? 'Page name' : 'Previous page name')] = $page->namePrevious;
        }

        $details['Template name'] = $page->template->name;
        if ($page->templatePrevious) {
            $details['Previous template name'] = $page->templatePrevious->name;
        }

        $details['Page URL'] = $page->url;
        if ($page->parentPrevious && $operation != "edited") {
            // for pages being edited current or previous parent is irrelevant
            // data since changing parent will also trigger "moved" operation.
            $details['Previous page URL'] = $page->parentPrevious->url;
            if ($page->namePrevious) $details['Previous page URL'] .= $page->namePrevious."/";
            else $details['Previous page URL'] .= $page->name."/";
        }
        if ($operation = "viewed" /*&& $page->template->name == "vessel"*/) {
            $details['Page URL'] .= $event->input->urlSegment(1);
            //echo $pages->get($page)->input->urlSegment(1);//->input->urlSegment(1);
            //print_r($page);
        }

        // note: currently only "edited" operation keeps track of edited fields
        if (count($fields_edited)) $details['Fields edited'] = implode(", ", $fields_edited);

        // find out which script / URL triggered this particular action
        if ($this->log_caller && $caller = $this->getCaller()) $details['Caller'] = $caller;

        $this->insert($operation, $page->id, $page->template->id, $details);

        if ($page->isChanged('status') && !in_array($operation, array("unpublished", "published"))) {
            // if status has changed, log extra unpublished/published event
            $event->method = $page->is(Page::statusUnpublished) ? "unpublished" : "published";
            $this->logPageEvent($event);
        }

    }

 

Somehow with this I'm unable to publish or unpublish pages; if attempted on the pages admin view it just loads forever, on the page editor it reloads the editor blank and doesn't work.

Edited by teppo
Shortened excessively long post by moving code to a spoiler block

Share this post


Link to post
Share on other sites

@hellomoto, I took a quick look at your code and there are some issues there, but first things first: I'd like to suggest not going this route at all. This module is intended for change tracking, not collecting data for all page views.

I can see at least three problems you will eventually run into here:

  • the amount of data can turn your site unusable (the module is not optimized for this),
  • this module is most likely too simplified for collecting any actually useful statistics, and finally
  • this will slow your site down and make it difficult to cache, since each page load needs to be processed programmatically and results in a database write operation.

You would be much better off with a real analytics software, such as Google Analytics – which is free and provides all the features you will ever need in terms of page views. If you're looking to avoid turning your data to Google, I'd suggest taking a closer look at Piwik, which is a free option.

Now on to the code issues if you still really want to make this thing work. First of all, you're forcing $operation to be "view" by this row:

        if ($operation = "viewed" /*&& $page->template->name == "vessel"*/) {

What you probably meant to use there was "==" instead of "=".

Another issue is related to the row attaching the "viewed" hook:

		$this->pages->addHook('viewed', $this, 'logPageEvent');

There's no "viewed" method to hook into, so what you're looking for is either Page::render or ProcessPageView::execute: first one occurs when a page is being rendered (via API or web interface), second one when a page is being requested online.

Again, I'd like to stress out that in my opinion this addition is not a good idea. Consider yourself warned :)

  • Like 2

Share this post


Link to post
Share on other sites

Hey @teppo - just noticed this notice - I think it might be new since installing multi-language support on the site.

PHP Notice: Undefined index: Previous page name in .../modules/ProcessChangelog/ProcessChangelog.module:574
 

Share this post


Link to post
Share on other sites

Downloaded this yesterday for a test drive (V 1.2.15). Like it very much. Reporting back with observations and mods.

Disclaimer - I did make another change particular to my needs but I'm pretty certain it doesn't impact anything else.

If the page had a RuntimeMarkup field (kind of a pseudo-field) it was always detected as changed. See ProcessChangelogHooks.

                foreach ($page->template->fields as $field) {
                    //if ($page->isChanged($field->name)) {
                    if (empty($field->runtimeFields) && $page->isChanged($field->name)) {    //SB added condition - ignore RuntimeMarkup field
                        $fields_edited[] = $field->name;
                    }
                }

Also undefined index notices as Adrian mentioned. See ProcessChangelog.

        switch ($operation) {
            case 'renamed':
                if(!empty($details["Previous $key"])) { //SB added if
                    $target .= $details["Previous $key"] 
                        . " <strong>" . __("as") . "</strong> " // as // In context of a rename operation ("renamed old-page-name as new-page-name")
                        . $details[ucfirst($key)];
                    break;
                }
            case 'trashed':
                if(! empty($details["Previous $key"])) { //SB added if
                    $target .= $details["Previous $key"]
                        . " <em>(" . $details[ucfirst($key)] . ")</em>";
                    break;
                }
            default:
                $target .= $details[ucfirst($key)];
        }

The ProcessChangelog buildQuery method was doing things like WHERE operation = '3' instead of using the name of the operation. That made the filter not work.

                    case "operation":
                        if (!in_array($value, array_flip($this->operations))) {
                            unset($this->input->get->$key);
                            break;
                        }
                        if($value) $value = $this->operations[$value - 1];    //SB fix - want word not number
                        else break; //SB fix
                    default:
                        $where[] = "$key $operator '$value'";
                }

Thanks!

  • Like 3

Share this post


Link to post
Share on other sites

Quick question: is there an "official" update planned or should one try the above patch from SteveB?

Share this post


Link to post
Share on other sites

Thanks, guys! I'm just now taking a closer look at these, and there are a few things I'd like to understand first.

Regarding undefined indexes:

@adrian and @SteveB, it sounds like you were both able to reproduce the undefined index problem. How exactly did you get there? I've been testing this with a multi-language setup and a non-multi-language setup for a while now, and can't seem to figure out the steps to reproduce this.

As far as I can tell, this would mean that a page has been renamed or trashed, but there's no information about what the name used to be. Logically thinking that must be something that shouldn't be logged in the first place, or the problem is in the way I'm fetching the previous page name. Do you have any pointers here, what am I missing? :)

Regarding RuntimeMarkup:

I'm familiar with RuntimeMarkup, but haven't really used it, so my initial thought is that this doesn't sound quite right. A field should not report itself changed every single page load unless it really changes each page load, in which case logging these changes is the only sensible thing to do. Either way, one definitely shouldn't have to check for specific field types like that.

Anyway, I'll take a closer look and see if there's actually a sensible reason why this is happening.

Regarding non-working filters:

@SteveB: If I'm getting this right, selecting an operation on the Changelog page results in a numeric GET param, such as "operation=3", right? I'm not able to reproduce this either, as the values in the operations dropdown are actual operation names, not numbers. Any pointers for reproducing this one?

Thanks again for reporting these issues, and sorry for not being able to reproduce them.. :)

  • Like 2

Share this post


Link to post
Share on other sites

One more addition: testing with multi-language page name support, I'm seeing that the rename operations affecting only non-default language are not being logged as renames at all. Currently I'm thinking that this should be logged as a separate rename operation, i.e. if you change the name in multiple languages for the same page at once, it would look in the log a bit like this:

renamed basic-page what as what-2

renamed basic-page what-in-finnish as what-in-finnish-2 

Probably will implement it like that unless someone has a strong opinion on thi :)

  • Like 3

Share this post


Link to post
Share on other sites

One idea: if both have the same name (which can happen) it can be clearer that 2 different operations are covered if non default language names are added?

 

renamed basic-page news as news2

renamed basic-page news DE  as news2

(maybe a different bg color for the language part)

Edited by ceberlin
  • Like 2

Share this post


Link to post
Share on other sites

Hi Teppo, 

I've been planning to use this module but have not yet spent the time to check it out. Seeing that you are currently working on it, I thought it is hight time to install it :) 

Looks cool so far, but may I suggest a new feature? I use a similar WP plugin which can generate an RSS feed accessible via a URL with a long, "random" hashed string in it, so it is a relatively secure way to get the change info via my RSS reader every 5 minute. Is a simple but powerful tool to get informed of failed login attempts (hackers...) and also to keep an eye on clients in almost "real time" (my reader refreshes the feeds very 5 minute).

A new addition the the above mentioned WP plugin is that it also looks for updates (system, theme, plugins), which is really handy too. This WP plugin relies on WP's lazy cron, but that is generally good enough, I think.

What do you think?

Edited by szabesz
typo

Share this post


Link to post
Share on other sites

Teppo,

The undefined indexes thing was only a Notice (harmless) and only when I have debug true in my PW config file.

Re. the filters, the filters form in /setup/changelog has a select returning numeric values. The first thing I did was to put this line in right after where you define $query in buildQuery.

wire('log')->save('messages', 'changelog @' . __LINE__ . ' ' . $query);

Here's results I got (line number will not be same as yours):

No filters: 
changelog @518 SELECT process_changelog.* FROM process_changelog WHERE operation IN('added','moved','edited','trashed','renamed','deleted','restored','published','unpublished') ORDER BY timestamp DESC, id DESC LIMIT 0, 25

'Edited' filter: 
changelog @518 SELECT process_changelog.* FROM process_changelog WHERE operation = '3' ORDER BY timestamp DESC, id DESC LIMIT 0, 25

So I just used the $this->operations array to look up the string from the number the form supplied.

Share this post


Link to post
Share on other sites
On 25/09/2016 at 5:38 PM, SteveB said:

The undefined indexes thing was only a Notice (harmless) and only when I have debug true in my PW config file.

Thanks, but I'm still unable to reproduce this. Been running on debug mode, switching between multi-language and non-multi-language setup, so far no idea about this. I'm less worried about a notice and more worried about what caused it -- in this context it could be a sign of a more serious issue somewhere else, one that results in broken or malformed data.

Which version of ProcessWire are you using? Any other tips for reproducing this? Anything you can share would be very much appreciated :) /cc @adrian

On 25/09/2016 at 5:38 PM, SteveB said:

Re. the filters, the filters form in /setup/changelog has a select returning numeric values.

Strangely I'm not able to reproduce this either. The values in the select menu should always be operation names (strings), not numbers. Is there a chance that you might've changed something related to this? Does anyone else see this?

Thanks for clarifying these. As soon as I can figure out what's going on here, I'll be happy to apply a fix :)

Share this post


Link to post
Share on other sites
On 25/09/2016 at 2:34 PM, szabesz said:

Looks cool so far, but may I suggest a new feature? I use a similar WP plugin which can generate an RSS feed accessible via a URL with a long, "random" hashed string in it, so it is a relatively secure way to get the change info via my RSS reader every 5 minute. Is a simple but powerful tool to get informed of failed login attempts (hackers...) and also to keep an eye on clients in almost "real time" (my reader refreshes the feeds very 5 minute).

Thanks for the suggestion. To be honest I was going to turn this one down (ProcessChangelog already provides JSON feed, so this could actually be a separate module), but I've already got a rough proof of concept ready and I think this has enough value to be part of ProcessChangelog itself.

Hopefully I can finish this in the next few days (a bit busy right now).

On 25/09/2016 at 2:34 PM, szabesz said:

A new addition the the above mentioned WP plugin is that it also looks for updates (system, theme, plugins), which is really handy too.

Definitely worth considering. Currently this module is limited to keeping track of changed page data, but I'm open for the possibility of tracking more than just that. I'll see if I can find an easy way to handle this (and a sensible way to output such content).

  • Like 1

Share this post


Link to post
Share on other sites

Thanks in advance Teppo! I am more than happy to help you by testing if you think I can be of any help this way. I'm also busy-busy-busy too, but I can postpone a few things just to spare some time for this. Since I'm sloooowly moving form WP to PW, I'm looking for ways to keep my useful habits :) 

As for the "output" part of the RSS, here is a screenshot of the WP plugin (called Simple History) to give you an idea what it can look like in an RSS reader (notice the login/logout noticies too, useful to keep track of users of sites with not too many users/logins):

Spoiler

simple-history.png

 

 

  • Like 1

Share this post


Link to post
Share on other sites

Hey @teppo - not ignoring your pings, just busy and not really sure why that notice is appearing either. I saw it a couple of times today, but was too rushed to investigate, and now it's not showing, but I will keep an eye out and when I see it when I have a spare moment I'll try to debug.

  • Like 1

Share this post


Link to post
Share on other sites

Sorry, folks -- looks like I managed to overestimate the amount of time I'd have for this module. Anyway, I've just pushed version 1.3 to Github, along with a new module called ProcessChangelogRSS. Here's the gist of this update:

  • If you access the changelog via /setup/changelog/rss/ you should find an RSS feed for the module. This feed requires the same permissions as the regular changelog view, i.e. you have to be authenticated and have the "changelog" permission.
  • Since the RSS feed might have valid use cases where authentication is an issue, an optional ProcessChangelogRSS module is now included. After installing this module you should go to it's config screen (in the Modules section) and type in a key of your own. After that you can view an RSS feed of changelog events at yourdomain.com/process-changelog-rss.xml?key=your-very-long-and-complex-key.

I don't really know how important this RSS feature is going to be, so didn't want to spend too much time on it -- this is why the description of each item makes use of the same tabular output as the main changelog view itself. Originally I had planned to implement multiple keys and such, but in the end decided to strip all that away and go with the most basic implementation possible.

Feedback on these features would be appreciated, but please note that if you do enable the ProcessChangelogRSS module, the changelog RSS feed is publicly available, even if only for those who somehow gain access to your private key (or are somehow able to guess it). If you are uncomfortable with that, please leave it uninstalled.

  • Like 7

Share this post


Link to post
Share on other sites

Thanks a lot Teppo, I do appreciate the effort! I'm gonna start using it and report my findings as soon as I have enough experience.

  • Like 1

Share this post


Link to post
Share on other sites

Another update: the latest version logs multi-language name changes as renames. The implementation is a bit hacky since core doesn't trigger a renamed hook in this case and also doesn't track previous name etc. like it does for the default name field. The output also doesn't mention the language specifically, though that's something that could be added later on.

Please let me know if you have the chance to give this feature a try and happen to notice any inconsistencies. Thanks!

  • Like 6

Share this post


Link to post
Share on other sites

Thanks for the updates teppo.  I've noticed that ProcessChangeLog doesn't log when a page (or rather field) is updated from ListerPro. My guess is this has to do something to do the saveReady hook not being called within ListerPro. Ryan posted a solution to 'force' the hook.

p.s. I also posted this in the the Activity Log post by renobird, since I believe it's the same issue.

  • Like 2

Share this post


Link to post
Share on other sites

@arjen: thanks for notifying me of this. The solution posted by Ryan a bit later (hooking into both Pages::saveReady and Pages::saveFieldReady) should be relatively easy to implement, but the problem is that Pages::saveFieldReady was added in 2.5.10 and this module currently supports >= 2.2. This could mean either jumping through some extra hoops or dropping support for a couple of releases.

Anyway, I'll take a closer look at this soon. What would've been awesome was if Pages::saveReady worked consistently across all revisions, but it is what it is :)

  • Like 4

Share this post


Link to post
Share on other sites
...
    $pwVersion = wire('config')->version;
    if(version_compare($pwVersion, '3.0.35', '>')) {
        $pages->addHookAfter('Pages::savePageOrFieldReady', $this, 'saveReady'); // Pages::savedPageOrField(Page $page, array $changes);
    } else {
        $pages->addHookAfter('Pages::saveReady', $this, 'saveReady');
        if(version_compare($pwVersion, '2.5.9', '>')) {
            $pages->addHookAfter('Pages::saveFieldReady', $this, 'saveReady');
        }
    }
...

 

Edited by horst
last core changes implemented :)
  • Like 4

Share this post


Link to post
Share on other sites

No problem @teppo. Thanks for taking a look.  It's something we ran into a while ago. Since I'm testing all the modules which are tracking changes I've ran into this again ;) Thought you should know. 

Thank you @horst for stepping in!

  • Like 2

Share this post


Link to post
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

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By David Karich
      ProcessWire InputfieldRepeaterMatrixDuplicate
      Thanks to the great ProModule "RepeaterMatrix" I have the possibility to create complex repeater items. With it I have created a quite powerful page builder. Many different content modules, with many more possible design options. The RepeaterMatrix module supports the cloning of items, but only within the same page. Now I often have the case that very design-intensive pages and items are created. If you want to use a content module on a different page (e.g. in the same design), you have to rebuild each item manually every time.
      This module extends the commercial ProModule "RepeaterMatrix" by the function to duplicate repeater items from one page to another page. The condition is that the target field is the same matrix field from which the item is duplicated. This module is currently understood as proof of concept. There are a few limitations that need to be considered. The intention of the module is that this functionality is integrated into the core of RepeaterMatrix and does not require an extra module.
      Check out the screencast
      What the module can do
      Duplicate multible repeater items from one page to another No matter how complex the item is Full support for file and image fields Multilingual support Support of Min and Max settings Live synchronization of clipboard between multiple browser tabs. Copy an item and simply switch the browser tab to the target page and you will immediately see the past button Support of multiple RepeaterMatrix fields on one page Configurable which roles and fields are excluded Configurable dialogs for copy and paste Duplicated items are automatically pasted to the end of the target field and set to hidden status so that changes are not directly published Automatic clipboard update when other items are picked Automatically removes old clipboard data if it is not pasted within 6 hours Delete clipboard itself by clicking the selected item again Benefit: unbelievably fast workflow and content replication What the module can't do
      Before an item can be duplicated in its current version, the source page must be saved. This means that if you make changes to an item and copy this, the old saved state will be duplicated Dynamic loading is currently not possible. Means no AJAX. When pasting, the target page is saved completely No support for nested repeater items. Currently only first level items can be duplicated. Means a repeater field in a repeater field cannot be duplicated. Workaround: simply duplicate the parent item Dynamic reloading and adding of repeater items cannot be registered. Several interfaces and events from the core are missing. The initialization occurs only once after the page load event Changelog
      2.0.0
      Feature: Copy multiple items at once! The fundament for copying multiple items was created by @Autofahrn - THX! Feature: Optionally you can disable the copy and/or paste dialog Bug fix: A fix suggestion when additional and normal repeater fields are present was contributed by @joshua - THX! 1.0.4
      Bug fix: Various bug fixes and improvements in live synchronization Bug fix: Items are no longer inserted when the normal save button is clicked. Only when the past button is explicitly clicked Feature: Support of multiple repeater fields in one page Feature: Support of repeater Min/Max settings Feature: Configurable roles and fields Enhancement: Improved clipboard management Enhancement: Documentation improvement Enhancement: Corrected few typos #1 1.0.3
      Feature: Live synchronization Enhancement: Load the module only in the backend Enhancement: Documentation improvement 1.0.2
      Bug fix: Various bug fixes and improvements in JS functions Enhancement: Documentation improvement Enhancement: Corrected few typos 1.0.1
      Bug fix: Various bug fixes and improvements in the duplication process 1.0.0
      Initial release Support this module
      If this module is useful for you, I am very thankful for your small donation: Donate 5,- Euro (via PayPal – or an amount of your choice. Thank you!)
      Download this module (Version 2.0.0)
      > Github: https://github.com/FlipZoomMedia/InputfieldRepeaterMatrixDuplicate
      > PW module directory: https://modules.processwire.com/modules/inputfield-repeater-matrix-duplicate/
      > Old stable version (1.0.4): https://github.com/FlipZoomMedia/InputfieldRepeaterMatrixDuplicate/releases/tag/1.0.4
    • By Robin S
      A new module that hasn't had a lot of testing yet. Please do your own testing before deploying on any production website.
      Custom Paths
      Allows any page to have a custom path/URL.
      Note: Custom Paths is incompatible with the core LanguageSupportPageNames module. I have no experience working with LanguageSupportPageNames or multi-language sites in general so I'm not in a position to work out if a fix is possible. If anyone with multi-language experience can contribute a fix it would be much appreciated!
      Screenshot

      Usage
      The module creates a field named custom_path on install. Add the custom_path field to the template of any page you want to set a custom path for. Whatever path is entered into this field determines the path and URL of the page ($page->path and $page->url). Page numbers and URL segments are supported if these are enabled for the template, and previous custom paths are managed by PagePathHistory if that module is installed.
      The custom_path field appears on the Settings tab in Page Edit by default but there is an option in the module configuration to disable this if you want to position the field among the other template fields.
      If the custom_path field is populated for a page it should be a path that is relative to the site root and that starts with a forward slash. The module prevents the same custom path being set for more than one page.
      The custom_path value takes precedence over any ProcessWire path. You can even override the Home page by setting a custom path of "/" for a page.
      It is highly recommended to set access controls on the custom_path field so that only privileged roles can edit it: superuser-only is recommended.
      It is up to the user to set and maintain suitable custom paths for any pages where the module is in use. Make sure your custom paths are compatible with ProcessWire's $config and .htaccess settings, and if you are basing the custom path on the names of parent pages you will probably want to have a strategy for updating custom paths if parent pages are renamed or moved.
      Example hooks to Pages::saveReady
      You might want to use a Pages::saveReady hook to automatically set the custom path for some pages. Below are a couple of examples.
      1. In this example the start of the custom path is fixed but the end of the path will update dynamically according to the name of the page:
      $pages->addHookAfter('saveReady', function(HookEvent $event) { $page = $event->arguments(0); if($page->template == 'my_template') { $page->custom_path = "/some-custom/path-segments/$page->name/"; } }); 2. The Custom Paths module adds a new Page::realPath method/property that can be used to get the "real" ProcessWire path to a page that might have a custom path set. In this example the custom path for news items is derived from the real ProcessWire path but a parent named "news-items" is removed:
      $pages->addHookAfter('saveReady', function(HookEvent $event) { $page = $event->arguments(0); if($page->template == 'news_item') { $page->custom_path = str_replace('/news-items/', '/', $page->realPath); } }); Caveats
      The custom paths will be used automatically for links created in CKEditor fields, but if you have the "link abstraction" option enabled for CKEditor fields (Details > Markup/HTML (Content Type) > HTML Options) then you will see notices from MarkupQA warning you that it is unable to resolve the links.
      Installation
      Install the Custom Paths module.
      Uninstallation
      The custom_path field is not automatically deleted when the module is uninstalled. You can delete it manually if the field is no longer needed.
       
      https://github.com/Toutouwai/CustomPaths
      https://modules.processwire.com/modules/custom-paths/
    • By teppo
      Hey folks!
      I'm happy to finally introduce a project I've been working on for quite a while now: it's called Wireframe, and it is an output framework for ProcessWire.
      Note that I'm posting this in the module development area, maily because this project is still in rather early stage. I've built a couple of sites with it myself, and parts of the codebase have been powering some pretty big and complex sites for many years now, but this should still be considered a soft launch 🙂
      --
      Long story short, Wireframe is a module that provides the "backbone" for building sites (and apps) with ProcessWire using an MVC (or perhaps MVVM... one of those three or four letter acronyms anyway) inspired methodology. You could say that it's an output strategy, but I prefer the term "output framework", since in my mind the word "strategy" means something less tangible. A way of doing things, rather than a tool that actually does things.
      Wireframe (the module) provides a basic implementation for some familiar MVC concepts, such as Controllers and a View layer – the latter of which consists of layouts, partials, and template-specific views. There's no "model" layer, since in this context ProcessWire is the model. As a module Wireframe is actually quite simple – not even nearly the biggest one I've built – but there's still quite a bit of stuff to "get", so I've put together a demo & documentation site for it at https://wireframe-framework.com/.
      In addition to the core module, I'm also working on a couple of site profiles based on it. My current idea is actually to keep the module very light-weight, and implement most of the "opinionated" stuff in site profiles and/or companion modules. For an example MarkupMenu (which I released a while ago) was developed as one of those "companion modules" when I needed a menu module to use on the site profiles.
      Currently there are two public site profiles based on Wireframe:
      site-wireframe-docs is the demo&docs site mentioned above, just with placeholder content replaced with placeholder content. It's not a particularly complex site, but I believe it's still a pretty nice way to dig into the Wireframe module. site-wireframe-boilerplate is a boilerplate (or starter) site profile based on the docs site. This is still very much a work in progress, but essentially I'm trying to build a flexible yet full-featured starter profile you can just grab and start building upon. There will be a proper build process for resources, it will include most of the basic features one tends to need from site to site, etc. --
      Requirements and getting started:
      Wireframe can be installed just like any ProcessWire module. Just clone or download it to your site/modules/ directory and install. It doesn't, though, do a whole lot of stuff on itself – please check out the documentation site for a step-by-step guide on setting up the directory structure, adding the "bootstrap file", etc. You may find it easier to install one of the site profiles mentioned above, but note that this process involves the use of Composer. In the case of the site profiles you can install ProcessWire as usual and download or clone the site profile directory into your setup, but after that you should run "composer install" to get all the dependencies – including the Wireframe module – in place. Hard requirements for Wireframe are ProcessWire 3.0.112 and PHP 7.1+. The codebase is authored with current PHP versions in mind, and while running it on 7.0 may be possible, anything below that definitely won't work. A feature I added just today to the Wireframe module is that in case ProcessWire has write access to your site/templates/ directory, you can use the module settings screen to create the expected directories automatically. Currently that's all, and the module won't – for an example – create Controllers or layouts for you, so you should check out the site profiles for examples on these. (I'm probably going to include some additional helper features in the near future.)
      --
      This project is loosely based on an earlier project called pw-mvc, i.e. the main concepts (such as Controllers and the View layer) are very similar. That being said, Wireframe is a major upgrade in terms of both functionality and architecture: namespaces and autoloader support are now baked in, the codebase requires PHP 7, Controllers are classes extending \Wireframe\Controller (instead of regular "flat" PHP files), implementation based on a module instead of a collection of drop-in files, etc.
      While Wireframe is indeed still in a relatively early stage (0.3.0 was launched today, in case version numbers matter) for the most part I'm happy with the way it works, and likely won't change it too drastically anytime soon – so feel free to give it a try, and if you do, please let me know how it went. I will continue building upon this project, and I am also constantly working on various side projects, such as the site profiles and a few unannounced helper modules.
      I should probably add that while Wireframe is not hard to use, it is more geared towards those interested in "software development" type methodology. With future updates to the module, the site profiles, and the docs I hope to lower the learning curve, but certain level of "developer focus" will remain. Although of course the optimal outcome would be if I could use this project to lure more folks towards that end of the spectrum... 🙂
      --
      Please let me know what you think – and thanks in advance!
    • By tcnet
      PageViewStatistic for ProcessWire is a module to log page visits of the CMS. The records including some basic information like IP-address, browser, operating system, requested page and originate page. Please note that this module doesn't claim to be the best or most accurate.
      Advantages
      One of the biggest advantage is that this module doesn't require any external service like Google Analytics or similar. You don't have to modify your templates either. There is also no JavaScript or image required.
      Disadvantages
      There is only one disadvantage. This module doesn't record visits if the browser loads the page from its browser cache. To prevent the browser from loading the page from its cache, add the following meta tags to the header of your page:
      <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Expires" content="0" /> How to use
      The records can be accessed via the Setup-menu of the CMS backend. The first dropdown control changes the view mode. There are 4 different view modes.
      View mode "Day" shows all visits of the selected day individually with IP-address, browser, operating system, requested page and originate page. Click the update button to see new added records. View mode "Month" shows the total of all visitors per day from the first to the last day of the selected month. View mode "Year" shows the total of all visitors per month from the first to the last month of the selected year. View mode "Total" shows the total of all visitors per year for all recorded years. Please note that multiple visits from the same IP address within the selected period are counted as a single visitor.
      Settings
      You can access the module settings by clicking the Configuration button at the bottom of the records page. The settings page is also available in the menu: Modules->Configure->ProcessPageViewStat.
      IP2Location
      This module uses the IP2Location database from: http://www.ip2location.com. This database is required to obtain the country from the IP address. IP2Location updates this database at the begin of every month. The settings of ProcessPageViewStat offers the ability to automatically download the database monthly. Please note, that automatically download will not work if your webspace doesn't allow allow_url_fopen.
      Dragscroll
      This module uses DragScroll. A JavaScript available from: http://github.com/asvd/dragscroll. Dragscroll adds the ability in view mode "Day" to drag the records horizontally with the mouse pointer.
      parseUserAgentStringClass
      This module uses the PHP class parseUserAgentStringClass available from: http://www.toms-world.org/blog/parseuseragentstring/. This class is required to filter out the browser type and operating system from the server request.
      Special Feature
      PageViewStatistic for ProcessWire can record the time a visitor viewed the page. This feature is deactivated by default. To activate open the module configuration page and activate "Record view time". If activated you will find a new column "S." in the records which means the time of view in seconds. With every page request, a Javascript code is inserted directly after the <body> tag. Every time the visitor switches to another tab or closes the tab, this script reports the number of seconds the tab was visible. The initial page request is recorded only as a hyphen (-).
       
    • By MoritzLost
      This module allows you to integrate hCaptcha bot / spam protection into ProcessWire forms. hCaptcha is a great alternative to Google ReCaptcha, especially if you are in the EU and need to comply with privacy regulations.

      The development of this module is sponsored by schwarzdesign.
      The module is built as an Inputfield, allowing you to integrate it into any ProcessWire form you want. It's primarily intended for frontend forms and can be added to Form Builder forms for automatic spam protection. There's a step-by-step guide for adding the hCaptcha widget to Form Builder forms in the README, as well as instructions for API usage.
      Features
      Inputfield that displays an hCaptcha widget in ProcessWire forms. The inputfield verifies the hCaptcha response upon submission, and adds a field error if it is invalid. All hCaptcha configuration options for the widget (theme, display size etc) can be changed through the inputfield configuration, as well as programmatically. hCaptcha script options can be changed through a hook. Error messages can be translated through ProcessWire's site translations. hCaptcha secret keys and site-keys can be set for each individual inputfield or globally in your config.php. Error codes and failures are logged to help you find configuration errors. Please check the README for setup instructions.
      Links
      Github Repository and documentation InputfieldHCaptcha in the module directory Screenshots (configuration)

      Screenshots (hCaptcha widget)

       
       

       
×
×
  • Create New...