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 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.

      Detailed records
      View mode "Detailed records" 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.

      Cached visitor records
      View modes other than "Detailed records" are cached visitor counts which will be collected on a daily basis from the detailed records. This procedure ensures a faster display even with a large number of data records. Another advantage is that the detailed records can be deleted while the cache remains. The cache can be updated manually or automatically in a specified time period. Multiple visits from the same IP address on the same day are counted as a single visitor.

      Upgrade from older versions
      Cached visitor counts is new in version 1.0.8. If you just upgraded from an older version you might expire a delay or even an error 500 if you display cached visitor counts. The reason for this is that the cache has to be created from the records. This can take longer if your database contains many records. Sometimes it might hit the maximally execution time. Don't worry about that and keep reloading the page until the cache is completely created.
      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 (-).

      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.
    • By Mike Rockett
      TextformatterTypographer
      A ProcessWire wrapper for the awesome PHP Typography class, originally authored by KINGdesk LLC and enhanced by Peter Putzer in wp-Typography. Like Smartypants, it supercharges text fields with enhanced typography and typesetting, such as smart quotations, hyphenation in 59 languages, ellipses, copyright-, trade-, and service-marks, math symbols, and more.
      It's based on the PHP-Typography library found over at wp-Typography, which is more frequently updated and feature rich that its original by KINGdesk LLC.
      The module itself is fully configurable. I haven't done extensive testing, but there is nothing complex about this, and so I only envisage a typographical bug here and there, if any.
    • By robert
      I often had the need for an overview of all used fields and their contents for a specific page/template while developing new websites without switching to the backend, so I made a small module which lists all the needed information in a readable manner (at least for me):
      Debug Page Fields
      https://github.com/robertweiss/ProcessDebugPageFields
      It adds two new properties to all pages:
      $page->debugFieldValues – returns an object with all (sub-)fields, their labels, fieldtypes and values $page->debugFieldTypes – returns an object with all fieldtypes and their corresponding fields // List all values of a pages $page->debugFieldValues // List a specific field $page->debugFieldValues->fieldname // List all used fieldtypes of a page $page->debugFieldTypes I recommend using it in combination with Tracy Debugger, Ray, Xdebug etc. as it returns an object and is only meant for developing/debugging uses. 
      For now, the fieldtype support includes mostly fieldtypes I use in my projects, but can easily be extended by adding a new FieldtypeFIELDNAME method to the module. I use it with five different client installations (all PW 3.0.*), but of course there might be some (or more) field configurations which are not covered correctly yet.
      Supported fieldtypes
      Button Checkbox Color Combo Datetime Email FieldsetPage * File FontIconPicker Functional Image ImageReference MapMarker Multiplier Mystique Options Page PageIDs PageTitle Radio Repeater * RepeaterMatrix * RockAwesome SeoMaestro Table Text Textarea Textareas Toggle URL * The fields with complete subfield-support also list their corresponding subfields.
      Installation
      Download the zip file at Github or clone the repo into your site/modules directory. If you downloaded the zip file, extract it in your sites/modules directory. In your admin, go to Modules > Refresh, then Modules > New, then click on the Install button for this module. As this is my first ›public‹ module, I hope I did not miss any important things to mention here.
    • By horst
      Wire Mail SMTP

      An extension to the (new) WireMail base class that uses SMTP-transport

      This module integrates EmailMessage, SMTP and SASL php-libraries from Manuel Lemos into ProcessWire. I use this continously evolved libraries for about 10 years now and there was never a reason or occasion not to do so. I use it nearly every day in my office for automated composing and sending personalized messages with attachments, requests for Disposition Notifications, etc. Also I have used it for sending personalized Bulkmails many times.

      The WireMailSmtp module extends the new email-related WireMail base class introduced in ProcessWire 2.4.1 (while this writing, the dev-branch only).
       
      Here are Ryans announcement.



      Current Version 0.6.0
      Changelog: https://github.com/horst-n/WireMailSmtp/blob/master/CHANGELOG.md
      get it from the Modules Directory Install and Configure

      Download the module into your site/modules/ directory and install it.

      In the config page you fill in settings for the SMTP server and optionaly the (default) sender, like email address, name and signature.
      You can test the smtp settings directly there. If it says "SUCCESS! SMTP settings appear to work correctly." you are ready to start using it in templates, modules or bootstrap scripts.


      Usage Examples
      The simplest way to use it:
      $numSent = wireMail($to, $from, $subject, $textBody); $numSent = wireMail($to, '', $subject, $textBody); // or with a default sender emailaddress on config page This will send a plain text message to each recipient.
       
      You may also use the object oriented style:
      $mail = wireMail(); // calling an empty wireMail() returns a wireMail object $mail->to($toEmail, $toName); $mail->from = $yourEmailaddress; // if you don't have set a default sender in config // or if you want to override that $mail->subject($subject); $mail->body($textBody); $numSent = $mail->send(); Or chained, like everywhere in ProcessWire:
      $mail = wireMail(); $numSent = $mail->to($toEmail)->subject($subject)->body($textBody)->send(); Additionaly to the basics there are more options available with WireMailSmtp. The main difference compared to the WireMail BaseClass is the sendSingle option. With it you can set only one To-Recipient but additional CC-Recipients.
      $mail = wireMail(); $mail->sendSingle(true)->to($toEmail, $toName)->cc(array('person1@example.com', 'person2@example.com', 'person3@example.com')); $numSent = $mail->subject($subject)->body($textBody)->send(); The same as function call with options array:
      $options = array( 'sendSingle' => true, 'cc' => array('person1@example.com', 'person2@example.com', 'person3@example.com') ); $numSent = wireMail($to, '', $subject, $textBody, $options); There are methods to your disposal to check if you have the right WireMail-Class and if the SMTP-settings are working:
      $mail = wireMail(); if($mail->className != 'WireMailSmtp') { // Uups, wrong WireMail-Class: do something to inform the user and quit echo "<p>Couldn't get the right WireMail-Module (WireMailSmtp). found: {$mail->className}</p>"; return; } if(!$mail->testConnection()) { // Connection not working: echo "<p>Couldn't connect to the SMTP server. Please check the {$mail->className} modules config settings!</p>"; return; }  
      A MORE ADVANCED DEBUG METHOD!
      You can add some debug code into a template file and call a page with it:
      $to = array('me@example.com'); $subject = 'Wiremail-SMTP Test ' . date('H:i:s') . ' äöü ÄÖÜ ß'; $mail = wireMail(); if($mail->className != 'WireMailSmtp') { echo "<p>Couldn't get the right WireMail-Module (WireMailSmtp). found: {$mail->className}</p>"; } else { $mail->from = '--INSERT YOUR SENDER ADDRESS HERE --'; // <--- !!!! $mail->to($to); $mail->subject($subject); $mail->sendSingle(true); $mail->body("Titel\n\ntext text TEXT text text\n"); $mail->bodyHTML("<h1>Titel</h1><p>text text <strong>TEXT</strong> text text</p>"); $dump = $mail->debugSend(1); } So, in short, instead of using $mail->send(), use $mail->debugSend(1) to get output on a frontend testpage.
      The output is PRE formatted and contains the areas: SETTINGS, RESULT, ERRORS and a complete debuglog of the server connection, like this one:
       
      Following are a ...


      List of all options and features


      testConnection () - returns true on success, false on failures


      sendSingle ( true | false ) - default is false

      sendBulk ( true | false ) - default is false, Set this to true if you have lots of recipients (50+)


      to ($recipients) - one emailaddress or array with multiple emailaddresses

      cc ($recipients) - only available with mode sendSingle, one emailaddress or array with multiple emailaddresses

      bcc ($recipients) - one emailaddress or array with multiple emailaddresses

       
      from = 'person@example.com' - emailaddress, can be set in module config (called Sender Emailaddress) but it can be overwritten here

      fromName = 'Name Surname' - optional, can be set in module config (called Sender Name) but it can be overwritten here


      priority (3) - 1 = Highest | 2 = High | 3 = Normal | 4 = Low | 5 = Lowest

      dispositionNotification () or notification () - request a Disposition Notification


      subject ($subject) - subject of the message

      body ($textBody) - use this one alone to create and send plainText emailmessages

      bodyHTML ($htmlBody) - use this to create a Multipart Alternative Emailmessage (containing a HTML-Part and a Plaintext-Part as fallback)

      addSignature ( true | false ) - the default-behave is selectable in config screen, this can be overridden here
      (only available if a signature is defined in the config screen)

      attachment ($filename, $alternativeBasename = "") - add attachment file, optionally alternative basename


      send () - send the message(s) and return number of successful sent messages


      debugSend(1) - returns and / or outputs a (pre formatted) dump that contains the areas: SETTINGS, RESULT, ERRORS and a complete debuglog of the server connection. (See above the example code under ADVANCED DEBUG METHOD for further instructions!)


      getResult () - returns a dump (array) with all recipients (to, cc, bcc) and settings you have selected with the message, the message subject and body, and lists of successfull addresses and failed addresses,


      logActivity ($logmessage) - you may log success if you want

      logError ($logmessage) - you may log warnings, too. - Errors are logged automaticaly
       
       
      useSentLog (true | false) - intended for usage with e.g. third party newsletter modules - tells the send() method to make usage of the sentLog-methods - the following three sentLog methods are hookable, e.g. if you don't want log into files you may provide your own storage, or add additional functionality here

      sentLogReset ()  - starts a new LogSession - Best usage would be interactively once when setting up a new Newsletter

      sentLogGet ()  - is called automaticly within the send() method - returns an array containing all previously used emailaddresses

      sentLogAdd ($emailaddress)  - is called automaticly within the send() method
      Changelog: https://github.com/horst-n/WireMailSmtp/blob/master/CHANGELOG.md
       
       
    • By Cybermano
      Food Allergens Module
      A simple List of Food Allergens
      My needs were to provide a simple list of food allergens for our clients with restaurant related activity.
      The idea was to simply output the list (to speed up the data entry) without leaving the food menu editing, eg. opening another page in new tab or window.
      This isn't a perfect solution, but it works fine for my needs and I decided to share the base idea.
      This could also be easily used to show little notes or short "vademecum", not only for the list of food allergens.
      ---
      Main features
      The basis
      All moves from a short editing of the module in this tutorial: How to create custom admin pages by @bernhard
      First of all it creates an empty admin page, with a dedicated permission to let safe-user to see it (this permission has to be created as a new ones, manually or by the module).
      Once the page is created, I have hooked its behaviour into the ready.php, to show the content (basically a list).
      A step further
      With the tips of  @bernhard, @Soma (and many others), see here , the magic happens. 
      The new page will be shown as a panel, so editors will not abandon their data entry to have a quick view to the list.
      A little further
      Why scroll to the top of the page to click a link?
      The next step was to create a sticky button only in the food menu pages.
      Again with a @bernhard tip I moved into the customization of this simple module and the related hook.
      ---
      How to use this module
      After installed, it creates the page /admin/page/allergens/ and the module is to be setted up. The first field is a CKEditor with multi-language. This is the place where to write the informations that will be shown into the page. The next field is a simply text-area where to place a bit of JS that will be appended to the markup of the text (omit the 'script' tags). I also putted a checkbox with a silly statement: this to think at least twice on the safety of the written JS. Now comes the first way to display the link to the page
      Field Note with Link. Enable and save it. The module will display a new row with 4 selects (1 standard and 3 ASM):
      View mode (to show the page as Panel or as Modal PopUp); Templates to select: select one or more and save before proceed, so the  asm-select of the pages will be populated showing all the pages of the selected templates. Pages to select: also here select at least one and save before proceed to populate the asm-select for fields only with the ones that belong to the selected pages. Select the fields where to place the note and save again. That's all: now you will find into the notes of the selected fields the link "See the List of Allergens".
      At the same way, the option for the sticky button, but with a plus
      The field select is obviously unnecessary, but you could play with the last row: the inline styles to fix your sticky button where you like. Here you could set the sticky position of the <div> and the absolute ones of the <a>.

      Video Explanation
      In these screencasts you could see a custom JS that show a "copy" button near a "hanna-code" call.
      This because I've set a specific one for each allergen to show up a tooltip in the front end.

      Registrazione #33.mp4  

      Registrazione #34.mp4 ---
      Last but not the least
      Actually it works fine for my needs, even if it's much improvable: I'm working on the permissions creation, the uninstall section, a separate configs and defaults and how to include the hook into the module leaving free the ready.php. According to a simpler uninstall. Also I would make the link text as a dynamic text field, so it will be more flexible.
      I always learn a lot here, so I would share my code for whom it could be interested.
      I removed the hanna code references, but I provide you the html list of the allergens, English and Italian too, so you can paste them into the "source" of the CKEditor field to have a ready to use module.
      Obviously you are free to modify the code as per your needs.
      Please, keep in mind that I'm not a pro coder and I beg your pardon for my verbosity (speaking and coding). 😉
      I hope be helpful or for inspiration.
      Bye
      ready.phpList-ITA.htmlList-ENG.htmlAllergens.module
      README.md
×
×
  • Create New...