MarkE
Members-
Posts
1,051 -
Joined
-
Last visited
-
Days Won
12
Everything posted by MarkE
-
RockMigrations1 - Easy migrations from dev/staging to live server
MarkE replied to bernhard's topic in Modules/Plugins
Actually it found me ? My code could probably be improved, but here is wahat I did (from around line 1190) // if the page is the home page then we need to avoid referring to its parent when saving if ($parent === 0) { $parent = ''; unset($data['parent']); } else { // make sure parent is a page and not a selector $parent = $this->pages->get((string)$parent); } // get page if it exists if ($parent and $parent->id) { $selector = [ 'name' => $name, 'template' => $template, 'parent' => $parent, ]; } else { $selector = [ 'name' => $name, 'template' => $template ]; } $page = $this->pages->get($selector); Hope that helps. -
RockMigrations1 - Easy migrations from dev/staging to live server
MarkE replied to bernhard's topic in Modules/Plugins
One (rather trivial) issue: if you attempt to migrate the home page, then PW throws an error because the parent is invalid. It seems to be necessary to unset the parent and not include 'parent' in the selector in RockMigrations::createPage(). -
+1. I am assuming there is something peculiar about options fields that makes this tricky, but it seems like this functionality was left unfinished some years ago.
-
I have a situation where $templates->setImportData() is not behaving as expected. This might be more general issue as I can't see anything odd about my situation. Basically, the problem is that it seems to return the wrong result and not update the template, but returns no error messages. RESOLVED: The problem with the changes being truncated is that Tracy appears to truncate the json string. The php variable is fine. So maybe there is a Tracy problem, but nothing wrong with the API. The problem with not saving is that setImportData is not intended to save the data. I borrowed the saveItem() method from ProcessTemplateExportImport and that does the trick. Original problem description retained below for the record. .............................................................................................................. I have tracked the results using Tracy bd(), as follows: Template data is exported as json into a file: // Get the export data $rawData = $templates->getExportData($t); // $t is a template //Encode the array into a JSON string. $encodedString = wireEncodeJSON($rawData, true, false); //Save the JSON string to a text file. file_put_contents('json_array.txt', $encodedString); //Retrieve the data from our text file. $fileContents = file_get_contents('json_array.txt'); //Convert the JSON string back into an array. $decoded = wireDecodeJSON($fileContents); bd() indicates that $rawdata and $decoded are identical. A simple mod is made to the context description of a field ('operator') in the txt file and the above is run (without the file_put_contents, so as not to revert the txt ?), followed by $result = $templates->setImportData($t, $decoded); bd($result) gives (identical) 'old' and 'new' strings for the 'summary' field only (the first after 'title', which has no contextual changes). The remaining fields (including 'operator' which would be next) are not shown. array 'fieldgroupContexts' => array 'old' => '{ "summary": { "description": "About this page", "editRoles": [ 7258 ], "flagsAdd": 224, "rows … 50 } }' 'new' => '{ "summary": { "description": "About this page", "editRoles": [ 7258 ], "flagsAdd": 224, "rows … 50 } }' 'error' => array (0) Tracking this through the wire code, everything is as expected until Fieldgroups::___setImportData() where we have the following code (with my bd() additions): $old = wireEncodeJSON($_data['contexts'], true, true); $new = wireEncodeJSON($data['contexts'], true, true); if($old !== $new) { bd($old, 'old'); bd($new, 'new'); bd(trim($old), 'trim old'); bd(trim($new), 'trim new'); $return['contexts']['old'] = trim($old); $return['contexts']['new'] = trim($new); bd($return, 'return'); $old and $new are exactly as expected, with my change for the 'operator' description. trim($old) and trim($new) are the same as $old and $new. However, $return['contexts']['old'] and $return['contexts']['new'] have truncated the $old and $new strings as described earlier. bd($return) shows: array 'fields' => array 'old' => '' 'new' => '' 'error' => array (0) 'contexts' => array 'old' => '{ "summary": { "description": "About this page", "editRoles": [ 7258 ], "flagsAdd": 224, "rows … 50 } }' 'new' => '{ "summary": { "description": "About this page", "editRoles": [ 7258 ], "flagsAdd": 224, "rows … 50 } }' 'error' => array (0) In addition to the returned changes being incomplete, the template is not being updated for the import data. Initially I thought that these issues were connected, but on inspection, it seems that the the contexts are all set, regardless of any detected changes. Later in Fieldgroups::___setImportData() we have (again with my bd() addition) foreach($data['contexts'] as $name => $context) { bd($data['contexts'][$name], 'Context for ' . $name); $field = $fieldgroup->getField($name, true); // in context if(!$field) { if(!empty($context)) $return['contexts']['error'][] = sprintf($this->_('Unable to find field to set field context: %s'), $name); continue; } $id = $field->id; $fieldContexts = $fieldgroup->getFieldContextArray(); if(isset($fieldContexts[$id]) || !empty($context)) { bd($data['contexts'][$name], 'Setting context for ' . $name . ', id = ' . $id) ; $fieldgroup->setFieldContextArray($id, $context); $fieldgroup->trackChange('fieldContexts'); } } The dumps indicate that all the data is present and correct and that the new context for operator has been passed to setFieldContextArray. However, the template itself (on inspection in the admin) appears to be unchanged. I can see that there is a saveContext() method for Fieldgroup and Fieldgroups classes, which I can't see being called, so I'm not sure how the context is actually being saved. I'm afraid that, at this point my knowledge of PW (and, to an extent, of PHP) hindered me from making further progress, but maybe I missed something obvious along the way? Do I misunderstand the setImportData() method, might there be something odd with my data, or is there a bug?
-
RockMigrations1 - Easy migrations from dev/staging to live server
MarkE replied to bernhard's topic in Modules/Plugins
Thanks @bernhard, that's really helpful. It is prompting me to re-think my whole approach to PW development. I had already moved to wholesale use of template page classes; If I understand you correctly, you are now suggesting that I use the Page Class to hold the complete template definition via migrate(). And that I use that rather than the admin tools to maintain the template. Your first comment makes even more sense in that context. You will appreciate that, of course, the original app was not developed in that way - hence my need to copy and paste out of Tracy (a quick way of doing that would make moving onto your way of doing things much easier). I will read your post again and try a few things to make sure I have an approach that works for me. Meanwhile a couple of things occur to me: The way RockMigrations has eveolved (and maybe it's still evolving...) has meant there there is not a consistent documentation of the whole approach and the related API. This is not a criticism - I appreciate that you are just sharing something and are not obliged to document it completely and also that, while it is evolving, documentation is a painful overhead. However, being forgetful, I will need to document something for myself, so happy to share if it is useful. Exploring this module and reading your comments has raised a whole bunch of questions in my mind about the direction of PW. There has been much discussion lately about front-end enhancements, but I think attention should also be paid to the back-end and in particular to the issue of maintainability which your module addresses. I think there is a place for both UI-based development via the admin and code-based development like you suggest (more like a trad PHP framework). In particular the UI-based tools are an easy entry point for new users and for prototyping, so a route from that to the code-based approach would be helpful (hence my comments re Tracy copy & paste). I'm still quite a PW newbie and a fairly inexperienced PHP user, so I'm a bit nervous about putting this into the PW roadmap discussion without having a clear idea of the implications, but it seems like that is where it belongs. If you or other key players such as @ryan or @adrian want to pick up on this then that would be great. -
RockMigrations1 - Easy migrations from dev/staging to live server
MarkE replied to bernhard's topic in Modules/Plugins
I'm assuming from the 'like' that I am thinking on the right lines. I've started building the migration module and it's coming along nicely - a dream to test it as you go along. Now for a bit of heresy ?. I wanted to migrate my Property template. I added the field website2, then I wanted to define the new state of the template with the new field, the amended widths etc. I couldn't quite remember what I changed (although not too hard to work out in this instance) but why should I? - surely I could just declare the whole thing as set out in Tracy's fieldgroupContexts for the template. But now followed a bit of awkward copying and pasting from Tracy's Request Info panel. Why not have a php script to interrogate the current (dev) state and create the required array...? Oh dear - I started by building a simple migration and now am in danger of disappearing down a rabbit hole... I suspect this could become a bigger issue than just this module. I see that @Kiwi Chris has had similar thoughts over on the thread that sparked my interest in this. -
RockMigrations1 - Easy migrations from dev/staging to live server
MarkE replied to bernhard's topic in Modules/Plugins
Hi @bernhard. Prompted by your comments here , I decided to give this module a go as it looks very useful. One thing I find rather confusing is that you seem to have changed the suggested methodology and it's not entirely clear to me what is now the suggested best practice for using the module. From what I can see, you are suggesting that users construct their own module (along the lines of your example). Installing this will then run the migration and uninstalling it will do a downgrade. For further migrations, it seems that you are suggesting just adding to this module, not creating a new module. Your previous approach seems to have been more version-based. I am trying to understand how that works in practice. I currently have 2 migrations that I want to carry out. The dev system reflects the end state of both. I want to do it in 2 stages so that I can throroughly test the first migration before applying the second (although I think there is no dependency, it is better to do them in the same order as on the dev system). My plan is therefore to create the first migration module, back up the dev database, load the live database to the dev system, install the migration module to this, then test. If that is OK, then, if I understand correctly, I should amend the migration module to include the second migration and refresh the modules to run it. I would then test again. Now, what if there is a problem with this second migration and I want to reverse it? Uninstalling the module would presumably undo both migrations (unless I change the uninstall method to be only partial, which seems inconsistent). Sure, in the dev system, I could just revert to a backup, but that would not be a solution if the problems happened in the live environment as (unknown) data will also have been updated - in that case I would need to just be able to reverse the last migration. So should I put the second migration in a separate module? In which case, this starts to look more like your initial approach (except that the use of the migrate() method allows for a more declarative style). Am I clear in explaining my confusion? If so, can you de-confuse me? By way of background, here is my first migration plan (as a set of human instructions), to give you an idea about what I am trying to do: 1. Add new fields to templates: a. Property -> operator b. Property -> website2 (new field req’d) c. BookingForm -> propertyPage d. home -> taxYear, dayMonth, month & website2 (with descriptions) 2. Amend home template for a. description of operator (& webmaster?) & website (now allowed) b. re-use of summary field as restricted access field with description/instructions (and resize) c. re-order fields as per dev system 3. Change title of home to “System” 4. Add/amend hanna codes for a. ManagementMember b. Operator 5. Edit cheque payment method to = that on the dev system 6. Add images and files pages to home 7. Change title of home 8. Re-purpose comparison field as “mailHeaders” and add to StandardMails template 9. Remove bodyTop field from StandardMails template – change body to 10 rows, summary to 3, footer to 2, update descriptions & notes in the template and update instructions in the page I think I can see that there are methods for most of that in your module, but I'm not sure about the hanna codes. The php in all my hanna codes is identical (as it just passes data to a separate script), but obviously attributes etc. are different. I don't think RockMigrations handles these, but they have convenient export/import features so that's not too much of an issue. The second migration is 1. Use findPages to detect all pages with BookingStatus in a body field (i.e bodyTop etc. as well as body – any field that can take a hanna code) – approx. 15 pages. Make appropriate changes. 2. Change the refNumber for cancelled statuses (2 pages). I can probably turn these human instructions into php (although it may be quicker just to do it in the admin, but then I definitely do not have a quick 'downgrade option!). -
Hmm. Thanks, but that just seems to just resize the unformatted column. Also, if you drag it under the Tracy bar at the bottom, then there doesn't seem to be a way of getting it back ?
-
Simple question: Is it possible to resize the field settings column when using Request Info on page? If you expand the array you get a very long thin column and I can't see how to resize. Thanks.
-
Membership app, Processwire limitations on unique records?
MarkE replied to Kiwi Chris's topic in General Support
? I was going to mention the 2 migrations modules, but they are not part of the core. I have taken a look at RockMigrations and will do again - it's just that my last migration was too messy to use as an initial attempt. I will try using it for a simple migration first, to get the hang of it. -
Membership app, Processwire limitations on unique records?
MarkE replied to Kiwi Chris's topic in General Support
I chose to use PW precisely because of its back-end capabilities - it facilitates quick and effective building of back-end applications. I've used it for a few sites now - one (a holiday cottage booking system) is an almost completely submerged "iceberg" which has hardly any front-end pages and even those are access-controlled - e.g. for use by other front-end apps. In that one, I have a similar issue to yours with customer pages. Of course, all the pages and names are unique, but that is not the real issue, which is to say precisely when a combination of fields should determine that a new page is added rather than an existing one updated . My solution is to have a hidden field (uniqueId) which is a concatenation of first name, last name, email address and postcode. So I have this in the before Pages::save hook: case 'Person' : if ($p->id) { $p->of(false); $postCode = ($p->address) ? $p->address->postal_code : ''; $p->uniqueId = wire()->sanitizer->selectorValue($p->firstName . '_' . $p->lastName . ' ' . $p->email . ' ' . $postCode); // Prevent duplicate records if ($p->parents("name=contacts")->count()) { $uniqueMatches = wire()->pages->find("has_parent=/contacts/, template=Person, uniqueId=$p->uniqueId, id!=$p->id, include=unpublished"); if ($uniqueMatches->count() > 0) { $this->warning("Change to contact record not saved - Unique id: " . $p->uniqueId . " already exists. (Use search box to find it). If you just created this record, please delete it if it is a duplicate."); $event->replace = true; $event->return; } else { $event->arguments(0, $p); // Save the unique value, overwriting any previous value } } } break; One might argue that the uniqueID is superfluous and that the key should be assigned to name instead, but my preference was for a hidden field. Another site is less of an iceberg (about 30% above the water) and is a membership system whereby members can access their records only through the front end via an email challenge protocol (like a password reset every time - a lot of the members are quite old and struggle with passwords). There are various admin access permissions for different roles within the club (membership secretary, treasurer, event organiser etc.). This one just uses the email address as the unique id - an approach not possible for the first site as not all customers book on line. I think PW is admirably suited for this type of application - I researched a whole bunch of other CMS/CMFs and nothing came close. The balance of features vs flexibility is spot on. The only issue that irks me slightly is the database vs. code issue - i.e. the database effectively contains business logic as well as data which does create maintenance issues (some careful thought is required as to what logic should be capable of being amended in the admin and what is best to put in the code if possible - for example I moved all Hanna code logic into a separate php script). I suspect the recent debate about front-end enhancements is precisely because PW is seen as a bit more lacking in that department. -
Resolved by adding the following to admin.js $('.pw-sidebar-nav').on('click', '#topPropertySelectForm', function(event) { // // as of Uikit beta 34, clicking items closes the offcanvas unless the following line is here event.stopPropagation(); }); where #topPropertySelectForm is the page select form.
-
Thanks - I had come to the same conclusion. The following appears to achieve the same result as .load() but works when the div is outside pw-content-body var id = '#wrap_topPropertySelect'; $.ajax(window.location.href, { headers: {'X-Requested-With': 'justgivemethedanghtml'}, //:) dataType: "html", type: 'GET', cache: false, success: function (result) { var val = $(result).find(id).html(); $(id).html(val); } } )
-
Thanks for the simple solution @Jan Romero Now I have the same problem with .load() - you are right that it is ProcessController.php that is causing it. I want to use .load(document.URL + ' #' + 'wrap_topPropertySelect' + '> *'); but it is outside pw-content-body so doesn't work.
-
Adding the following in the ajax request seems to fool PW into responding with the full HTML. xhr: function() { // Get new xhr object using default factory var xhr = jQuery.ajaxSettings.xhr(); // Copy the browser's native setRequestHeader method var setRequestHeader = xhr.setRequestHeader; // Replace with a wrapper xhr.setRequestHeader = function(name, value) { // Ignore the X-Requested-With header if (name == 'X-Requested-With') return; // Otherwise call the native setRequestHeader method // Note: setRequestHeader requires its 'this' to be the xhr object, // which is what 'this' is here when executed. setRequestHeader.call(this, name, value); } // pass it on to jQuery return xhr; }, Still not worked out where PW is fixing the response. I thought it might be the line in ProcessWire.php: $config->ajax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'); but commenting that out seems to have no effect.
-
Maybe I'm being a bit dim (not unusual ?) but I don't understand the ajax response I get from an admin template. A very simple example is function formTest(event) { $.ajax(window.location.href, { success: function (result) { console.log(result); } }) } If this is activated in the admin then the result seems to be not the entire html of the current page, but just the DOM under <div id=pw-content-body>. Why is this and how can I get the full HTML? Or am I being really dim....?
-
Maybe this isn't a great idea, but I wanted to place a pageselect inputfield in the admin masthead. There are several ways of doing this, but at the moment I am using a Page::render hook. That works well - see pic I'm then using js to clone that into the off-canvas menu. However, in the off-canvas menu, it is not usable - clicking on it just closes the menu. I'm assuming that there is some js in play here that I'm not aware of - any ideas? Thanks. PS I should have mentioned that this is in AdminUiKit
-
Sure. In fact, I was wrong to say that it works fine - I needed to make a small adjustment, which also enables more checking. The page object in the hook has the admin template - so we need to get the actual page being edited in order to get the override field. $this->addHookAfter('Page::render', function(HookEvent $event) { $p = $event->object; if ($p->template == 'admin') { $pId = wire()->input->get->int('id'); $page = wire('pages')->get($pId); if ($page and $page->template == 'BookingForm') { $event->return = str_replace("</body>", '<input type="hidden" id="Inputfield_override" name="override" value="' . $page->override . '"></body>', $event->return); } } });
-
No worries - thanks for the help This one works fine ? I suspect I need to mod this a bit to get it working. Ta muchly!
-
I've been using this for a while with no problems, but have just run into a slight issue. I have a checkbox field 'override' which is in a tab 'system'. The 'system' tab is only allowed for superuser. However, another field in an unrestricted tab uses a visibility condition "override!=1". It seems that, when a non-superuser is logged in, the visibility condition returns false when it should be true, presumably because override cannot be accessed. There are no restrictions on the override field itself. Any ideas @adrian? I tried a work-round with a hook, assuming that the API would work even if the tab was hidden. The one below is after renderReadyHook, but I also tried a before hook. In each case, the value is applied, but it seems like the form is unchanged. $this->addHookAfter('Inputfield::renderReadyHook', function(HookEvent $event) { $input = $event->arguments(0); if ($input and $input->id == 'Inputfield_fieldset_minimal') { bd($input, 'input'); $input->showIf = (wire()->page->overide!=1); } $event->arguments(0, $input); $event->return; });
-
Following the above discussion, I decided that it made more sense to display the title rather than the path (since it is only for display purposes). I also wanted a solution for attribute types pagelistselectmultiple, not just pagelistselect. In case it is useful to anyone else, my suggested js is: $(document).ready(function() { /* Author: Mark Evens, based on an original script by Robin Sallis */ /* This version displays the title text rather than the path name */ /* It also works for pagelistselectmultiple as well as pagelistselect */ /* Hide the text field */ /* id_title is the name of a text attribute in the hanna code*/ /* change id_title throughout the code below to match your attribute name */ $('#wrap_id_title').addClass('hide-field'); // Must use mousedown instead of click event due to frustrating event propagation issues across PW core // https://github.com/processwire/processwire-issues/issues/1028 // .PageListActionSelect is the button 'Select' with appears when a page is clicked (no normally visible - loaded by js) $(document).on('mousedown', '.PageListActionSelect a', function (event) { var id_title; // Adapt unselect label to suit your language if ($(this).text() === 'Unselect') { id_title = ''; } else { id_title = ($(this).closest('.PageListItem').children('.PageListPage').text()); // replace .text() by .attr('title') for path name } $('#id_title').val(id_title); }); // For pagelistselectmultiple, get all the selected pages when the selection is updated // This returns the contents of the label displayed in the pagelist item. This can be set in your HannaCodeDialog::buildForm hook // The default is the page title. For the path name, set $f->labelFieldName = 'path'; in your hook $(document).on('change', '.InputfieldPageListSelectMultiple', function(event) { var id_title = []; $(this).find('.InputfieldContent ol li:not(.itemTemplate)').each(function(){ id_title.push($(this).children('span.itemLabel').text()) }) $('#id_title').val(id_title); }); }); To hide the id_title attribute in the dialog, you need to add the following css: #hanna-form .hide-field { display: none !important; }
-
Just to report that this works well. I also added some css to hide the path attribute in the dialog box, so as not to confuse the user or risk inadvertant amendment. Super module , super support!
-
Brilliant - thanks. I was thinking along those lines, but obviously not hacky enough ?
-
This is one of my favourite modules and I use it a lot to provide user-customisation of, for example, email contents. In particular, I have managed to reduce the number of hanna codes used by introducing dialogs with page-select attributes (limited to certain children, specified by a hook described earlier in this thread). This works really well and avoids the need for head-scratching over tag names, but has one slight disadvantage in that the selected page is shown as an id in the text area - e.g. [[RentalPayment payment_type="5363" net_gross="Net" due="" format="£0.00" absolute=""]]. Unless the user can remember what page id 5363 is, they have to click the code to display it in the dialog. That's no great hardship, but in a text area filled with several such codes, it would be nice to display the name rather than the id. I'm wondering if there is a way to achieve a more meaningful display in the text area. I guess I could introduce a text or select attribute (unused in the code) which the user could use to document the selection. That seems a bit unreliable, however. It would be nice to fill the dummy attribute automatically, so my next idea was to use a hook - which is where I started running out of ideas: the hook would need to be on the page selection and somehow rebuild the form, whch all start to feel a bit messy. Any better ideas?
-
I have a chart that has a logarithmic y axis and I want normal numeric notation, not scientific, on the tick marks. In Chart.js v2 it seems the only way you can do this is with a callback - see https://github.com/chartjs/Chart.js/issues/3121