Jump to content

Module: Process Changelog


teppo

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
Link to comment
Share on other sites

  • 6 months later...

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?

Link to comment
Share on other sites

  • 1 month later...

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
Link to comment
Share on other sites

  • 2 weeks later...

@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
Link to comment
Share on other sites

  • 1 month later...

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
Link to comment
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
Link to comment
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
Link to comment
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
Link to comment
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
Link to comment
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.

Link to comment
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 :)

Link to comment
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
Link to comment
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
Link to comment
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
Link to comment
Share on other sites

  • 2 weeks later...

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
Link to comment
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
Link to comment
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
Link to comment
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
Link to comment
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
Link to comment
Share on other sites

Create an account or sign in to comment

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

Create an account

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

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By LuisM
      Symprowire is a PHP MVC Framework based and built on Symfony using ProcessWire 3.x as DBAL and Service-Provider
      It acts as a Drop-In Replacement Module to handle the Request/Response outside the ProcessWire Admin. Even tough Symfony or any other mature MVC Framework could be intimidating at first, Symprowire tries to abstract Configuration and Symfony Internals away as much as possible to give you a quick start and lift the heavy work for you.
      The main Goal is to give an easy path to follow an MVC Approach during development with ProcessWire and open up the available eco-system.
      You can find the GitHub Repo and more Information here: https://github.com/Luis85/symprowire
      Documentation
      The Symprowire Wiki https://github.com/Luis85/symprowire/wiki How to create a simple Blog with Symprowire https://github.com/Luis85/symprowire/wiki/Symprowire-Blog-Tutorial Last Update
      16.07.2021 // RC 1 v0.6.0 centralized ProcessWire access trough out the Application by wrapping to a Service https://github.com/Luis85/symprowire/releases/tag/v0.6.0-rc-1 Requirements
      PHP ^7.4 Fresh ProcessWire ^3.0.181 with a Blank Profile Composer 2 (v1 should work, not recommended) The usual Symfony Requirements Features
      Twig Dependency Injection Monolog for Symprowire Support for .env YAML Configuration Symfony Console and Console Commands Symfony Webprofiler Full ProcessWire access inside your Controller and Services Webpack Encore support Caveats
      Symfony is no small Framework and will come with a price in terms of Memory Usage and added Overhead. To give you a taste I installed Tracy Debugger alongside to compare ProcessWire profiling with the included Symfony Webprofiler

      So in a fresh install Symprowire would atleast add another 2MB of Memory usage and around 40ms in response time, should be less in production due to the added overhead of the Webprofiler in dev env
       
    • By FireWire
      Hello community!

      I want to share a new module I've been working on that I think could be a big boost for multi-language ProcessWire sites.

      Some background, I was looking for a way for our company website to be efficiently translated as working with human translators was pretty laborious and a lack of updating content created a divergence between languages. I, and several other devs here, have talked about translation integrations and have recognized the power that DeepL has. DeepL is an AI deep learning powered service that delivers translation quality beyond any automated service available. After access to the API was opened up to the US, I built Fluency, a DeepL translation integration for ProcessWire.
      Fluency brings automated translation to every multi-language field in the admin, and also provides a translation tool allowing the user to translate their text to any language without it being inside a template's field. With Fluency you can:
      Translate any plain textarea or text input Translate any CKEditor content (yes, with markup) Translate page names for fully localized URLs on every page Translate your in-template translation function wrapped strings Translate modules Fluency is free, and now so is DeepL
      Since this module was first built DeepL has introduced free Developer accounts that allow anyone to start using Fluency at zero cost and beginning with the version 0.3.0 release Fluency now supports free DeepL accounts. As of June 2021 DeepL supports translation to 26 languages and continues to offer more!
      Installation and usage is completely plug and play. Whether you're building a new multi-language site, need to update a site to multi-language, or simply want to stop manually translating a site and make any language a one-click deal, it could not be easier to do it. Fluency works by having you match the languages configured in ProcessWIre to DeepL's. You can have your site translating to any or all of the languages DeepL translates to in minutes (quite literally).
      Let's break out the screenshots...
      When the default language tab is shown, a message is displayed to let users know that translation is available. Clicking on each tab shows a link that says "Translate from English". Clicking it shows an animated overlay with the word "Translating..." cycling through each language and a light gradient shift. Have a CKEditor field? All good. Fluency will translated it and use DeepL's ability to translate text within HTML tags. CKEditor fields can be translated as easily and accurately as text/textarea fields.

      Repeaters and AJAX created fields also have translation enabled thanks to a JavaScript MutationObserver that searches for multi-language fields and adds translation as they're inserted into the DOM. If there's a multi-language field on the page, it will have translation added.

      Same goes for image description fields. Multi-language SEO friendly images are good to go.

      Creating a new page from one of your templates? Translate your title, and also translate your page name for native language URLs. (Not available for Russian, Chinese, or Japanese languages due to URL limitations). These can be changed in the "Settings" tab for any page as well so whether you're translating new pages or existing pages, you control the URLs everywhere.

      Language configuration pages are no different. Translate the names of your languages and search for both Site Translation Files (including all of your modules)

      Translate all of the static text in your templates as well. Notice that the placeholders are retained. DeepL is pretty good at recognizing and keeping non-translatable strings like that. If it is changed, it's easy to fix manually.

      Fluency adds a "Translate" item to the CMS header. When clicked this opens up a modal with a full translation tool that lets the user translate any language to any language. No need to leave the admin if you need to translate content from a secondary language back to the default ProcessWire language. There is also a button to get the current API usage statistics. DeepL account owners can set billing limitations via character count to control costs. This may help larger sites or sites being retrofitted keep an eye on their usage. Fluency can be used by users having roles given the fluency-translate permission.

      It couldn't be easier to add Fluency to your new or existing website. Simply add your API key and you're shown what languages are currently available for translation from/to as provided by DeepL. This list and all configuration options are taken live from the API so when DeepL releases new languages you can add them to your site without any work. No module updates, just an easy configuration. Just match the language you configured in ProcessWire to the DeepL language you want it to be associated with and you're done. Fluency also allows you to create a list of words/phrases that will not be translated which can prevent items such as brands and company names from being translated when they shouldn't

       
      Limitations:
      No "translate page" - Translating multiple fields can be done by clicking multiple translation links on multiple fields at once but engineering a "one click page translate" is not feasible from a user experience standpoint. The time it takes to translate one field can be a second or two, but cumulatively that may take much longer (CKEditor fields are slower than plain text fields). There may be a workaround in the future but it isn't currently on the roadmap. No "translate site" - Same thing goes for translating an entire website at once. It would be great, but it would be a very intense process and take a very (very) long time. There may be a workaround in the future but it isn't on the roadmap. No current support for Inline CKEditor fields - Handling for CKEditor on-demand hasn't been implemented yet, this is planned for a future release though and can be done. I just forgot about it because I've never really used that feature personally.. Alpha release - This module is in alpha. Releases should be stable and usable, but there may be edge case issues. Test the module thoroughly and please report any bugs via a Github issue on the repository or respond here. Please note that the browser plugin for Grammarly conflicts with Fluency (as it does with many web applications). To address this issue it is recommended that you disable Grammarly when using Fluency, or open the admin to edit pages in a private window where Grammarly may not be loaded. This is an issue that may not have a resolution as creating a workaround may not be possible. If you have insight as to how this may be solved please visit the Github page and file a bugfix ticket.
      Requirements:
      ProcessWire  3.0+ UIKit Admin Theme That's Fluency in a nutshell. A core effort in this module is to create it so that there is nothing DeepL related hard-coded in that would require updating it when DeepL offers new languages. I would like this to be a future-friendly module that doesn't require developer work to keep it up-to-date.
      The Module Is Free
      This is my first real module and I want to give it back to the community as thanks. This is the best CMS I've worked with (thank you Ryan & contributors) and a great community (thank you dear reader).
      DeepL Developer Accounts
      In addition to paid Pro Developer accounts, DeepL now offers no-cost free accounts. Now all ProcessWire developers and users can use Fluency at no cost.
      Learn more about free and paid accounts by visiting the DeepL website. Sign up for a Developer account, get an API key, and start using Fluency today.
      Download & Feedback
      Download the latest version here
      https://github.com/SkyLundy/Fluency-Translation/archive/main.zip
      Github repository:
      https://github.com/SkyLundy/Fluency-Translation
      File issues and feature requests here (your feedback and testing is greatly appreciated):
      https://github.com/SkyLundy/Fluency-Translation/issues
       
      Thank you! ¡Gracias! Ich danke Ihnen! Merci! Obrigado! Grazie! Dank u wel! Dziękuję! Спасибо! ありがとうございます! 谢谢你!

    • 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 (-).

      New in version 1.1.0
      A new feature comes with version 1.1.0 which offers to record user names of loggedin visitors. Just activate "Record user names" and "Record loggedin user" in the module settings.
      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 clsource
      Inertia Adapter ProcessWire Module
      Hello! Long time no see.
      I created this module so you can use Inertia.js (https://inertiajs.com/) with ProcessWire.
      Description
      Inertia allows you to create fully client-side rendered, single-page apps, without much of the complexity that comes with modern SPAs. It does this by leveraging existing server-side frameworks.
      Inertia isn’t a framework, nor is it a replacement to your existing server-side or client-side frameworks. Rather, it’s designed to work with them. Think of Inertia as glue that connects the two. Inertia comes with three official client-side adapters (React, Vue, and Svelte).
      This is an adapter for ProcessWire. Inertia replaces PHP views altogether by returning JavaScript components from controller actions. Those components can be built with your frontend framework of choice.
      Links
      - https://github.com/joyofpw/inertia
      - https://github.com/joyofpw/inertia-svelte-mix-pw
      - https://inertiajs.com/
      Screenshots


       
    • By sms77io
      Hi all,
      we made a small module for sending SMS via Sms77.io. It supports sending to one and multiple users.
      You can download it from GitHub and follow the instructions on how to install it - it is quite easy. An API key is required for sending, get yours for free @ Sms77 and receive 0,50 €.
      Hope this helps somebody and we are open for improvement suggestions!
       
      Best regards
      André
×
×
  • Create New...