Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 04/28/2022 in all areas

  1. Triasima, a portfolio management company, asked the agency Contractuelle to revamp its web image. Spiria was the contractor for the site integration. While seemingly straightforward, there were a few challenges ahead, including managing three distinct regions that shared a lot of content: Canada, the United States and the Rest of the World. Two of these regions were assigned a sub-domain (us. and world.). We did not need a multi-site module. Within each region, the visitor has to choose a type of investment profile. One of these profiles could only be selected by the Canadian region. Each profile shares some content but is identified by colour. In short, a small puzzle solved with very little code and CSS. We made extensive use of field repeaters to create a nice administration interface. We have designed "presentation scenarios" that allow the administrator to set up pages as they wish. Only the header and footer are fixed. https://triasima.com
    5 points
  2. When I need to shift data over from an old site I normally export the data to a csv file and then just write a PHP script to loop through that csv and add pages. Once you get the idea it's a very flexible way to import data. Here's some completely untested code as an example of how I'd approach it: // bootstrap PW include("index.php"); $filename='data_to_import.csv'; // loop through this CSV if (($handle = fopen($filename, "r")) !== FALSE) { while (($data = fgetcsv($handle, 2000, ",")) !== FALSE) { // say we have a csv that is in the form // 'artist_name', 'artwork_name', 'artwork_dscription' // (I'm assuming this csv doesn't have a header row) $artist_name=$data[0]; $artwork_name=$data[1]; $artwork_description=$data[2]; // see if we have a page for this artist already $ap=$pages->get("template=artist,name={$artist_name}"); // if not then add this artist - in this example // using the template 'artist' and the parent '/artists/' if($ap->id < 1){ $ap = $pages->add('artist', '/artists/', [ 'title' => $artist_name ]); } // now add our new artwork page using the artist_page as a parent $artwork_page = $pages->add('artwork', $ap, [ 'title' => $artwork_name, 'description' => $artwork_description ]); } fclose($handle); }else{ echo 'cant open csv file for import'; }
    2 points
  3. Fieldtype modules Not really a tutorial, but a bunch of stuff I learnt in building a fieldtype module (FieldtypeMeasurement). That module is used as a starting point for many of the examples. Happy to take corrections and improvements ? Basics For a (full-featured) module, you actually need two module files: FieldtypeModuleName.module; and InputfieldModuleName.module The Fieldtype module defines the general settings for the fieldtype (how it appears in the setup->field page), together with how it interacts with the database, while the Inputfield module defines how the field appears when editing it in a page. In addition, for complex fields, you can define a class to hold your field values in an object. This allows you to provide custom methods for use in the API. Otherwise you can store field values as any existing type or ProcessWire object The chart below summarises the interactions of these elements and the subsequent sections describe the methods in more detail. Fieldtype Module The important methods are described below. __construct() Generally not much is required here (apart from parent::__construct();). If you have a php script with the field object then include it (require_once() ). SQL database interaction getDatabaseSchema(Field $field) This is essential. It states what data will be saved to the SQL database (via the sleepValue function - see below). An example is: public function getDatabaseSchema(Field $field) { $schema = parent::getDatabaseSchema($field); $schema['data'] = 'double NOT NULL'; // value in base units $schema['magnitude'] = 'varchar(64)'; // value in current units - needs to be text to store composite values $schema['unit'] = 'text NOT NULL'; $schema['quantity'] = 'text NOT NULL'; return $schema; } 'data' is required and is a primary key field which means that 'text' cannot be used, although varchar(64) is OK. Often (as here) it would be a numeric field of some type. Other items can be defined as required. ___sleepValue(Page $page, Field $field, $value) This determines how the ProcesssWire field object is mapped to the SQL schema. You need to return an array where the keys are the elements defined in the schema, e.g.: $sleepValue = array( 'data' => $data, 'magnitude' => $magnitude, 'unit' => $value->unit, 'quantity' => $value->quantity ); return $sleepValue; ___wakeupValue(Page $page, Field $field, $value) This is basically the inverse of sleepValue - mapping the array from the database into the field object. In the example below, the field object (Measurement) extends WireData. Properties in WireData objects can be stored in the 'data' property via 'get' and 'set' methods. getBlankValue() (see next section) performs the initial setting of these - config values for the field (see below) can be set, but otherwise the settings are just placeholders. Properties set to a WireData object can also be set or accessed directly, like $item->property or using array access like $item[$property] public function ___wakeupValue(Page $page, Field $field, $value) { // if for some reason we already get a valid value, then just return it if($value instanceof Measurement) return $value; // start a blank value to be populated $measurement = $this->getBlankValue($page, $field); // if we were given a blank value, then we've got nothing to do: just return a blank Measurement object if(empty($value) || !is_array($value)) return $measurement; // create new Measurement object $measurement->quantity = $value['quantity']; // ... custom processing ... $measurement->baseMagnitude = $value['data']; if($value['unit']) { $measurement->unit = $value['unit']; $units = $measurement->getUnits(); if(array_key_exists($value['unit'], $units) && isset($value['magnitude'])) { $measurement->magnitude = explode('|', $value['magnitude']); } else { $measurement->magnitude = $measurement->baseMagnitude; $measurement->unit = $measurement->units->base; $this->error('... error msg ...'); } } else { //... } if(!is_array($measurement->magnitude)) $measurement->magnitude = [$measurement->magnitude]; return $measurement; } getBlankValue(Page $page, Field $field) This should return an empty item of the appropriate type. For instance, if your field object is an array, just return array(); If the field type is an object then you will need to return a 'new ObjectClassName()'. Pre-fill any config values from the Fieldtype settings but leave blank those which are set in the Inputfield, In the above example, the field object data was set as follows: public function getBlankValue(Page $page, Field $field) { /* @var $field FieldtypeMeasurement */ $measurement = new Measurement($field->quantity, null, []); if ($field->quantity) $measurement->set('quantity', $field->quantity); $measurement->set('magnitude', []); $measurement->set('shortLabel', null); $measurement->set('plural', null); return $measurement; } If your object has configurable fields that can be modified according to context (as defined in getConfigAllowContext() - see below), then you will need to deal with this in getBlankValue too, e.g. : public function getBlankValue(Page $page, Field $field): Measurement { //NB Field details may differ between templates so we need to get the field in context $context = ($page && $page->id) ? $field->getContext($page->template) : $field; ... $measurement = new Measurement($context->quantity, null, []); ... return $measurement; } But note that this does not completely deal with the situation where the field is in repeater matrix items where the types might have different contexts - there you might need: public function getBlankValue(Page $page, Field $field): Measurement { //NB Field details may differ between templates so we need to get the field in context $context = ($page && $page->id) ? $field->getContext($page->template) : $field; ... // if($page instanceof RepeaterMatrixPage) { // This does not always work - see edit note if($page->template->pageClass == 'RepeaterMatrixPage') { if($page->getField($field->name)) { $context = $page->fieldgroup->getFieldContext($field, "matrix$page->repeater_matrix_type"); } } ... $measurement = new Measurement($context->quantity, null, []); ... return $measurement; } See this post for more details. Configuration ___getConfigInputfields(Field $field) This defines how the Details tab in the field setup page will look. The best thing to do here is to find a fieldtype module that is similar to the one you want if you are uncertain. Broadly the process is: define the config object - $inputfields = parent::___getConfigInputfields($field); for each config item, use the appropriate input field, e.g . $f = $this->modules->get("InputfieldSelect"); assign the relevant attributes. $f->name = $f_name; is important as it enables the item to be subsequently referred to as $field->f_name in, for example, getBlankValue(). append each item ($inputfields->append($f);) and return $inputfields; ___getConfigAllowContext(Field $field) This determines if the above input fields are allowed to have unique values per Fieldgroup assignment enabling the user to configure them independently per template in the admin, rather than sharing the same setting globally. E.g. public function ___getConfigAllowContext(Field $field) { $a = array('quantity', 'units', 'hide_quantity', 'show_update'); return array_merge(parent::___getConfigAllowContext($field), $a); } In this example the settings 'quantity', 'units', 'hide_quantity' and 'show_update' can be varied in different template contexts. Link with the Inputfield module This is done with getInputfield(Page $page, Field $field) e.g.: public function getInputfield(Page $page, Field $field) { $inputfield = $this->wire('modules')->get('InputfieldMeasurement'); $inputfield->setField($field); return $inputfield; } If you want to reference the current page in the inputfield, you will also need to include $inputfield->setPage($page); If your fieldtype is an object and you want full context flexibility including for different repeater matrix item types, then you may need to use this: public function getInputfield(Page $page, Field $field): Inputfield { $inputfield = $this->wire('modules')->get('InputfieldMeasurement'); if($page->template->pageClass == 'RepeaterMatrixPage') { if($page->getField($field->name)) { $field_in_context = $page->fieldgroup->getFieldContext($field, "matrix$page->repeater_matrix_type"); if($field_in_context) { $field = $field_in_context; } } } $inputfield->setField($field); $inputfield->setPage($page); return $inputfield; } Inputfield Module __construct() Generally not much is required here (apart from parent::__construct();). If you have a php script with the field object then include it (require_once() ). Configuration ___getConfigInputfields() This is pretty much exactly the same construction as the similar method in the Fieldtype class. The only difference is that these settings will appear in the 'input' tab of the fieldtype settings, rather than the 'details' tab. ___getConfigAllowContext(Field $field) This is the equivalent to the Fieldtype::getConfigAllowContext() method, but for the "Input" tab rather than the "Details" tab. Input and output The key methods for this module are to render it from the fieldtype and database and to process the user inputs. ___render() $field = $this->field will have the field config settings from the fieldtype module. $this->attr('value') will have the current values for the field. If there is no current values then, if using a field object, you will need to create a new object, e.g.: if($this->attr('value')) $value = $this->attr('value'); // Measurement object else { $value = new Measurement(); } You can then use $field and $value to display the inputfield (which might be a fieldset) as required using the appropriate pre-existing inputfield modules. (Again, find an existing module that is similar, if you are uncertain). renderValue() This is required where the field is locked (not editable) and therefore render() does not apply. Get the value with $value = $this->attr('value'); and then apply the required formatting, returning the output string. ___processInput(WireInputData $input) Here you take the inputs and update the field values. As in render(), set $value = $this->attr('value') ; and then modify $value for the inputs. For example, set $name = $this->attr('name'); and then assign the inputs thus: $input_names = array( 'magnitude' => "{$name}_magnitude", 'unit' => "{$name}_unit", 'quantity' => "{$name}_quantity", 'update' => "{$name}_update" ); You can then loop through the inputs and carry out the required updates. The example below is slightly convoluted but illustrates this: foreach($input_names as $key => $name) { if(isset($input->$name) && $value->$key != $input->$name) { if($key == 'magnitude') { $input->$name = trim($input->$name); if(!is_numeric($input->$name)) { $magnitude = explode('|', $input->$name); $magnitude = array_filter($magnitude, 'is_numeric'); $value->set($key, $magnitude); } else { $value->set($key, [$input->$name]); } } else { $value->set($key, $input->$name); } $this->trackChange('value'); } } When all is done, return $this; Custom classes As mentioned earlier, for complex field types it may be useful to set up custom classes to hold the data. Typically a custom class would extend WireData, which is ProcessWire's class designed for runtime data storage. It provides this primarily through the built-in get() and set() methods for getting and setting named properties to WireData objects. The most common example of a WireData object is Page, the type used for all pages in ProcessWire. Properties set to a WireData object can also be set or accessed directly, like $item->property or using array access like $item[$property]. If you foreach() a WireData object, the default behaviour is to iterate all of the properties/values present within it. Do not declare any such properties in your class (or declare properties with the same name) otherwise you will end up with two properties, one in the 'data' array and one outside it and endless confusion will result. It is advisable to put any such classes in your own namespace. In that case, you will need to include 'use' statements in your script - e.g. use ProcessWire\{FieldtypeMeasurement, WireData}; use function ProcessWire\{wire, __}; and also include use statements in your module scripts, e.g. use MetaTunes\MeasurementClasses\Measurement;
    1 point
  4. This week on the dev branch we've got a good mix of updates and issue resolutions, though not quite yet enough yet to bump the version number just yet. But here's a few highlights: There's a PR from Adrian committed which adds configurable columns for Selector fields. System update 20 was added which fixes the "created user" for several system pages. WireHttp was updated to support sending cookies in GET/POST requests powered by CURL. Lister was updated to recognize non-sortable fields, preventing unnecessary error messages. There were also 3 issue resolutions this week. We are getting down to the smaller stuff in terms of updates, which means it's about time to get that next master version out. I know I've been talking about it for awhile, but think we'll likely have one more dev branch version (3.0.199) and then it's looking like our 200th 3.x version will tentatively be the next master version (3.0.200). Thanks for reading, Happy Earth Day, and have a great weekend!
    1 point
  5. This would be easier to explain in your context if you had included all the relevant field and template names in your post. So here is an example that you can adapt to your scenario... I have a Page Reference field named "select_colour" and generally the selectable pages are those that have the template "colour". I add this field to the template "animal", so that on animal pages I have a field to choose the colour of the animal. But in the select_colour field I only want to show colours that have not been selected on any other animal page. So in the settings for field select_colour I only use the "Custom PHP code" option for "Selectable pages" and add the following code to /site/ready.php: $wire->addHookAfter('InputfieldPage::getSelectablePages', function($event) { $page = $event->arguments(0); // Define selectable pages for the select_colour field if($event->object->hasField == 'select_colour') { /** @var Pages $pages */ $pages = $event->wire()->pages; // Find colours that are already selected on another animal page using "owner" selector // https://processwire.com/blog/posts/processwire-3.0.95-core-updates/ $already_selected_on_another_page = $pages->find("template=colour, select_colour.owner.template=animal, select_colour.owner.id!=$page->id"); // Return colour pages that aren't already selected on another page $event->return = $event->pages->find("template=colour, id!=$already_selected_on_another_page"); } }); Another possible approach is to loop over all the colour pages and remove those that are referenced on another page, but this is less specific because it will remove colours that have been referenced in any field, not just the select_colour field. $wire->addHookAfter('InputfieldPage::getSelectablePages', function($event) { $page = $event->arguments(0); // Define selectable pages for the select_colour field if($event->object->hasField == 'select_colour') { /** @var Pages $pages */ $pages = $event->wire()->pages; // All colours $colours = $pages->find("template=colour"); foreach($colours as $colour) { // Remove colour if any other pages are referencing it if($colour->numReferences) $colours->remove($colour); } // Return the filtered colours $event->return = $colours; } });
    1 point
  6. @Pixrael, good question! I thought that PW database uses InnoDB by default but it was wrong. Did an InnoDB migration without changing any code, the concurrent tests are experiencing the same troubles as before with MyISAM, but now there will be more space for experiments. Thanks!
    1 point
  7. If you want to do something if the page has not yet been created: if(!$page->id) { // Do something here }
    1 point
  8. Perhaps you can just run your code after saving? $wire->addHookAfter("Pages::saved", /* … */); Also worth noting is the hook Pages::savedPageOrField.
    1 point
  9. Just Connect to your old database and Save your entries as Pages ?? o
    1 point
  10. You are on the right track with your present code ?. I'll have a think about adding this feature to the API. These two will not work since the fields padloper_type and padloper_categories belong to the parent product, i.e., in your case, $product. Yes. This is necessary to explicitly specify that the product will use variants. This prevents 'accidental' variants if children were created for a product by mistake. This is unlikely but this safeguard is an extra layer to prevent that. The correct code for this should be: <?php namespace ProcessWire; $product->padloper_product_settings->useVariants = 1; Alternatively, since you are creating a new product (rather than amending an existing one), you can do it as follows, given that padloper_product_settings is a WireData object. <?php namespace ProcessWire; $productSettings = new WireData(); // 'physical' | 'physical_no_shipping' | 'digital' | 'service' $productSettings->shippingType = 'physical';// product shipping type $productSettings->taxable = 1; // bool int: is product taxable? $productSettings->trackInventory = 1;// bool int: is product tracking inventory? $productSettings->useVariants = 1; // bool int: is product using variants? $productSettings->colour = '#d52f10ff'; // product colour // ------ // add to product $product->padloper_product_settings = $productSettings; // OR //$product->set('padloper_product_settings', $productSettings); $product->save(); OR <?php namespace ProcessWire; $productSettings = [ 'shippingType' => 'physical', 'taxable' => 1, 'trackInventory' => 1, 'useVariants' => 1, 'colour' => '#d52f10ff', ]; $product->padloper_product_settings->setArray($productSettings); $product->save(); This requires setting the attribute on the parent product and the combination of attribute options on the variant. For instance, the product can have the attributes Size and Colour. The variants will take combinations of options for those attributes, i.e. combo of Size and Colour, e.g. Red x Small, Red x Medium, Black x Large, Black x Small, etc. The attribute options need to be children of the attribute. Hence, Red and Black are page children of Colour and Small, Medium and Large are children of Size. Attributes and Attribute Options use different templates. As you can see, it does get a little complicated. Above does not mean that each each possible Colour and Size combo should be created. For instance, you might not have a Brown x Large variant. Just skip creating it. So, for products that have and use variants, in addition to $product->padloper_product_attributes (a multi-page reference field), you also need to populate $product_variant->padloper_product_attributes_options (a multi-page reference field). Please let me know if you need help with coding this or anything else. Looking forward to seeing it! ?.
    1 point
  11. v0.0.12 now available. This fixes a few bugs and also introduces interactive dependent-selects in the config. Now that both the config and the pages have dependent selects, I thought it would be helpful to demo how it all works. Firstly, the config. On the details tab, you select the quantity you want to measure and then choose what units you want to be selectable within a page (you can also choose whether to convert automatically, not at all, or to 'ask'): New field.mp4 You will realise that we ended that demo just saving with no quantity selected. That's because we can use the same field in different template contexts to measure different quantities. So, next, we are going to add our field to a template and choose 'volume' as the quantity: volume template.mp4 Similarly, we can add our field to a different template to measure mass: mass template.mp4 Finally, we can create a page using one of these templates. In this case, it is 'volume' and we have chosen to convert automatically: volume page.mp4 If we had chosen to 'ask', we would have got a confirmation box before doing the conversion. All of this is accomplished by the magic of htmx (and of course ProcessWire). The principles behind it are discussed at The actual code has moved on a bit from that post. For instance, I have used css transitions in the config. These work really nicely with htmx: #Inputfield_unit_set { opacity: 1; transition: opacity 200ms ease-in; } #Inputfield_unit_set.htmx-swapping { opacity: 0.1; transition: opacity 100ms ease-out; } #Inputfield_unit_set.htmx-settling { opacity: 0.1; } Now I'm getting the hang of htmx, I really like it ?
    1 point
×
×
  • Create New...