Plugin Directory

Changeset 2700272


Ignore:
Timestamp:
03/27/2022 05:11:35 PM (4 years ago)
Author:
eighty20results
Message:

Update to version 8.6 from GitHub

Location:
e20r-members-list
Files:
4 added
2 deleted
32 edited
1 copied

Legend:

Unmodified
Added
Removed
  • e20r-members-list/tags/8.6/CHANGELOG.md

    r2672951 r2700272  
    66
    77## [Unreleased]
     8
     9## v8.6 - 2022-03-27
     10- BUG FIX: Revert codecov upload step (Eighty/20Results Bot on Github)
     11- BUG FIX: Set permissions for coverage output directory (Eighty/20Results Bot on Github)
     12- BUG FIX: More debug output (Eighty/20Results Bot on Github)
     13- BUG FIX: Typo in Makefile (Eighty/20Results Bot on Github)
     14- BUG FIX: Adding more debug text to makefile (Eighty/20Results Bot on Github)
     15- BUG FIX: Adding debug text to makefile (Eighty/20Results Bot on Github)
     16- BUG FIX: Adding PHP 8.1 to test matrix (Eighty/20Results Bot on Github)
     17- BUG FIX: Integration tests weren't doing much (Eighty/20Results Bot on Github)
     18- BUG FIX: Add PHP 8.1 to test matrix and revert code coverage upload (Eighty/20Results Bot on Github)
     19- BUG FIX: Reverted codecov upload step and adding php 8.1 to test matrix (Eighty/20Results Bot on Github)
     20- BUG FIX: Be explicit about the file to load in action (Eighty/20Results Bot on Github)
     21- BUG FIX: Different config for codecov-action (Eighty/20Results Bot on Github)
     22- BUG FIX: Try to fix setup for codecov/codecov-action (Eighty/20Results Bot on Github)
     23- BUG FIX: Update codecov configuration and rename the config file (Eighty/20Results Bot on Github)
     24- BUG FIX: Revert removal of PMPro dependency for PHPStan tests (Eighty/20Results Bot on Github)
     25- BUG FIX: Attempting to enable code coverage uploads (Eighty/20Results Bot on Github)
     26- BUG FIX: Adding the codecov.yml config file (Eighty/20Results Bot on Github)
     27- BUG FIX: PHPDoc entry for failed class variable was wrong (Eighty/20Results Bot on Github)
     28- BUG FIX: More unit test coverage for the bulk operations including fix for members_to_process check (Eighty/20Results Bot on Github)
     29- BUG FIX: Checksum for composer wasn't a make variable (Eighty/20Results Bot on Github)
     30- BUG FIX: Exception for when the array of users/levels to perform bulk operations is incorrect (Eighty/20Results Bot on Github)
     31- BUG FIX: Make coverage a local only thing (for now?) (Eighty/20Results Bot on Github)
     32- BUG FIX: Don't create unneeded coverage subdirectories (Eighty/20Results Bot on Github)
     33- BUG FIX: Fix workflow config (Eighty/20Results Bot on Github)
     34- BUG FIX: Use remote coverage service (Eighty/20Results Bot on Github)
     35- BUG FIX: Problem with coverage artifacts (Eighty/20Results Bot on Github)
     36- BUG FIX: Bumped version number to 8.6 (Eighty/20Results Bot on Github)
     37- BUG FIX: Clean up message in get_members() -> InvalidSQL exception (Eighty/20Results Bot on Github)
     38- BUG FIX: Not re-running workflow when PR is updated/edited (Eighty/20Results Bot on Github)
     39- BUG FIX: Add support for Brain\faker() mocking for WP and the ext-mysqli extension for integration testing purposes (Eighty/20Results Bot on Github)
     40- BUG FIX: Transition to mocking PMPro functions for Integration tests (probably moving actual PMPro integration to acceptance testing) (Eighty/20Results Bot on Github)
     41- BUG FIX: Make sure Integration/Acceptance test container uses PHP v7.4 (Eighty/20Results Bot on Github)
     42- BUG FIX: Added PHP8/Paid Memberships Pro incompatibility as a 'Known Issue' (Eighty/20Results Bot on Github)
     43- BUG FIX: Updated tags for Wodby docker4wordpress (Eighty/20Results Bot on Github)
     44- BUG FIX: Wrong number of placeholders and data in sprintf() (Eighty/20Results Bot on Github)
     45- BUG FIX: Clean up Known Issues text (Eighty/20Results Bot on Github)
     46- BUG FIX: Refactored option HTML and changed to using gmdate() (Eighty/20Results Bot on Github)
     47- BUG FIX: Adding support for listing multiple memberships (still have TODOs, not releasing yet) (Eighty/20Results Bot on Github)
     48- BUG FIX: Added a known issue (Eighty/20Results Bot on Github)
     49- BUG FIX: Cleaned up namespace path (Eighty/20Results Bot on Github)
     50- BUG FIX: No longer using singleton pattern in E20R_Members_List() class and fixed PHPCS warnings (Eighty/20Results Bot on Github)
     51- BUG FIX: Null value disallowed for str_replace() (Eighty/20Results Bot on Github)
     52- BUG FIX: Bulk-exported all users, not just the selected ones and fixed PHP Deprecated messages for bulk action checks (Eighty/20Results Bot on Github)
     53- BUG FIX: Adding tests with data for column_last() method (Eighty/20Results Bot on Github)
     54- BUG FIX: Fix column_last() to avoid PHP Warning messages and refactor, make the definition of an 'empty' enddate filtered and add helper methods. Added exceptions and handlers (Eighty/20Results Bot on Github)
     55- BUG FIX: Add e20r_members_list_empty_date_values filter documentation (Eighty/20Results Bot on Github)
     56- BUG FIX: Cleanup patchwork.json (Eighty/20Results Bot on Github)
     57- BUG FIX: Variables rely on tests/_env/.env.testing (Eighty/20Results Bot on Github)
     58- BUG FIX: Run install for WP and set only DEBUG variable (Eighty/20Results Bot on Github)
     59- BUG FIX: Make sure we have the needed database tables for testing (Eighty/20Results Bot on Github)
     60- BUG FIX: Actual path to source files for e20r utilities module is src/E20R/... (Eighty/20Results Bot on Github)
     61- BUG FIX: PMPro doesn't check if constants are defined before use triggering test failures for us (Eighty/20Results Bot on Github)
     62- BUG FIX: Load fixtures to load/clear the DB (Eighty/20Results Bot on Github)
     63- BUG FIX: Lacked DB records for PMPro and User in integration tests (Eighty/20Results Bot on Github)
     64- BUG FIX: Wrong path to test specific _bootstrap.php file (Eighty/20Results Bot on Github)
     65- BUG FIX: Didn't load test specific _bootstrap.php files (Eighty/20Results Bot on Github)
     66- BUG FIX: Initial commit for column_last integration test (Eighty/20Results Bot on Github)
     67- BUG FIX: Add WP_DEBUG logging for test container(s) (Eighty/20Results Bot on Github)
     68- BUG FIX: Use E20R_PLUGIN_NAME env variable to set PROJECT_NAME variable (with default value set to plugin slug) (Eighty/20Results Bot on Github)
     69- BUG FIX: Upgraded wodby container environments (Eighty/20Results Bot on Github)
     70- BUG FIX: Fixed install-hooks for pre-commit git hook (Eighty/20Results Bot on Github)
     71- BUG FIX: Path to documentation (Eighty/20Results Bot on Github)
     72- BUG FIX: Make sure we strip away the test file (Eighty/20Results Bot on Github)
    873
    974## v8.5 - 2022-02-04
  • e20r-members-list/tags/8.6/README.md

    r2672951 r2700272  
    33`Tags: paid memberships pro, members, memberships, pmpro enhancements, better members list, members list, addon` <br />
    44`Requires at least: 4.9` <br />
    5 `Tested up to: 5.9` <br />
     5`Tested up to: 5.9.2` <br />
    66`Requires PHP: 7.1` <br />
    7 `Stable tag: 8.5` <br />
     7`Stable tag: 8.6` <br />
    88`License: GPLv2` <br />
    99`License URI: http://www.gnu.org/licenses/gpl` <br />
     
    3636See [ACTIONS.md](https://github.com/eighty20results.com/e20r-members-list/blob/main/docs/ACTIONS.md)
    3737
     38
    3839### Known Issues
    39 No known issues at this time
     40PHP 8.0 and later introduces warning messages for certain behaviors that were ignored prior to v8.0. Because of this, and the fact that this plugin relies on functionality from Paid Memberships Pro, the "end date" column may print messages indicating problems with the `trim()` function. Until Paid Memberships Pro updates their plugin to support PHP8.x, these messages will need to be disabled in your web server configuration (suppressed).
     41
     42Setting the "Members per page" in the "Options" drop-down on the Members List page to a number greater than 50 can result in unexpected errors/warnings. The default value is 20. One symptom is seeing the PHP warning: "Warning: Unknown: Input variables exceeded 2000. To increase the limit change max_input_vars in php.ini. in Unknown on line 0"
    4043
    4144### Changelog
  • e20r-members-list/tags/8.6/README.txt

    r2672951 r2700272  
    33Tags: paid memberships pro, members, memberships, pmpro enhancements, better members list, members list, addon
    44Requires at least: 4.9
    5 Tested up to: 5.9
     5Tested up to: 5.9.2
    66Requires PHP: 7.1
    7 Stable tag: 8.5
     7Stable tag: 8.6
    88License: GPLv2
    99License URI: http://www.gnu.org/licenses/gpl
     
    3838
    3939== Known Issues ==
    40 No known issues at this time
     40PHP 8.0 and later introduces warning messages for certain behaviors that were ignored prior to v8.0. Because of this, and the fact that this plugin relies on functionality from Paid Memberships Pro, the "end date" column may print messages indicating problems with the `trim()` function. Until Paid Memberships Pro updates their plugin to support PHP8.x, these messages will need to be disabled in your web server configuration (suppressed).
     41
     42Setting the "Members per page" in the "Options" drop-down on the Members List page to a number greater than 50 can result in unexpected errors/warnings. The default value is 20. One symptom is seeing the PHP warning: "Warning: Unknown: Input variables exceeded 2000. To increase the limit change max_input_vars in php.ini. in Unknown on line 0"
    4143
    4244== Changelog ==
    4345See the official [CHANGELOG.md](https://github.com/eighty20results.com/e20r-members-list/blob/main/CHANGELOG.md) file
    44 
  • e20r-members-list/tags/8.6/class-e20r-members-list.php

    r2672951 r2700272  
    44Plugin URI: https://wordpress.org/plugins/e20r-members-list
    55Description: Extensible, sortable & bulk action capable members listing + export to CSV tool for Paid Memberships Pro.
    6 Version: 8.5
     6Version: 8.6
    77Author: Thomas Sjolshagen @ Eighty / 20 Results by Wicked Strong Chicks, LLC <[email protected]>
    88Author URI: https://eighty20results.com/thomas-sjolshagen/
     
    3939use E20R\Metrics\Exceptions\MissingDependencies;
    4040use E20R\Metrics\MixpanelConnector;
     41use E20R\Utilities\ActivateUtilitiesPlugin;
    4142use E20R\Utilities\Cache;
    4243use E20R\Utilities\Utilities;
     
    5253
    5354if ( ! defined( 'E20R_MEMBERSLIST_VER' ) ) {
    54     define( 'E20R_MEMBERSLIST_VER', '8.5' );
     55    define( 'E20R_MEMBERSLIST_VER', '8.6' );
    5556}
    5657
     
    6263
    6364        /**
    64          * Instance of the Member List controller
    65          *
    66          * @var null|E20R_Members_List
    67          */
    68         private static $instance = null;
    69 
    70         /**
    7165         * The E20R Utilities Module class instance
    7266         *
     
    9892         */
    9993        public function __construct( $ml_page = null, $utils = null, $mixpanel = null ) {
    100             self::$instance = $this;
    10194
    10295            if ( empty( $utils ) ) {
     
    139132         * @throws InvalidSettingsKey Raised when an invalid class property is specified for the get() method
    140133         */
    141         public function get( $property = 'instance' ) {
     134        public function get( $property = 'utils' ) {
    142135
    143136            if ( ! property_exists( $this, $property ) ) {
     
    152145            return $this->{$property};
    153146        }
    154         /**
    155          * Get or instantiate and get the current class
    156          *
    157          * @return E20R_Members_List|null
    158          *
    159          * @test E20R_Members_ListTest::test_get_instance()
    160          */
    161         public static function get_instance() {
    162 
    163             if ( is_null( self::$instance ) ) {
    164                 self::$instance = new self();
    165             }
    166 
    167             return self::$instance;
     147
     148        /**
     149         * Fetch the Page object
     150         *
     151         * @return Members_List_Page|null
     152         */
     153        public function get_page() {
     154            return $this->page;
    168155        }
    169156
     
    181168        public function load_hooks( $utils = null ) {
    182169
    183             if ( ! method_exists( '\\E20R\\Utilities\\Utilities', 'get_instance' ) ) {
     170            if ( ! method_exists( '\E20R\Utilities\Utilities', 'get_instance' ) ) {
    184171                $msg = esc_attr__( 'The E20R Utilities Module is missing/inactive!', 'e20r-members-list' );
    185172                throw new MissingUtilitiesModule( $msg );
     
    354341                    'filters'       => sprintf(
    355342                        '<a href="%1$s" title="%2$s">%3$s</a>',
    356                         esc_url_raw( plugin_dir_url( __FILE__ ) . '../docs/FILTERS.md' ),
     343                        esc_url_raw( plugin_dir_url( __FILE__ ) . '/docs/FILTERS.md' ),
    357344                        esc_attr__( 'View the Filter documentation', 'e20r-members-list' ),
    358345                        esc_attr__( 'Filters', 'e20r-members-list' )
     
    360347                    'actions'       => sprintf(
    361348                        '<a href="%1$s" title="%2$s">%3$s</a>',
    362                         esc_url_raw( plugin_dir_url( __FILE__ ) . '../docs/ACTIONS.md' ),
     349                        esc_url_raw( plugin_dir_url( __FILE__ ) . '/docs/ACTIONS.md' ),
    363350                        esc_attr__( 'View the Actions documentation', 'e20r-members-list' ),
    364351                        esc_attr__( 'Actions', 'e20r-members-list' )
     
    395382    $required_plugin = 'Better Members List for Paid Memberships Pro';
    396383
    397     if ( false === \E20R\Utilities\ActivateUtilitiesPlugin::attempt_activation() ) {
     384    if ( false === ActivateUtilitiesPlugin::attempt_activation() ) {
    398385        add_action(
    399386            'admin_notices',
    400387            function () use ( $required_plugin ) {
    401                 \E20R\Utilities\ActivateUtilitiesPlugin::plugin_not_installed( $required_plugin );
     388                ActivateUtilitiesPlugin::plugin_not_installed( $required_plugin );
    402389            }
    403390        );
  • e20r-members-list/tags/8.6/docs/FILTERS.md

    r2672951 r2700272  
    250250);
    251251```
     252
     253### e20r_members_list_empty_date_values
     254
     255Modifies: Array of the date values that we (and PMPro) consider to be the equivalent of an 'empty' (not configured) date value
     256
     257Purpose: To modify or update the list of date values we'll think of as 'empty'. Anything goes here. Including integer values representing seconds since epoch start.
     258
     259Default:
     260```php
     261array(
     262    '',
     263    null,
     264    0,
     265    '0',
     266    '0000-00-00 00:00:00',
     267    '0000-00-00',
     268    '00:00:00',
     269);
     270```
     271
     272Dependencies: N/A
     273
     274Example:
     275```php
     276add_filter(
     277    'e20r_members_list_empty_date_values',
     278    function( $values ) {
     279        // Also include DD-MM-YYYY at midnight as a valid 'empty' value
     280        return $values + array(
     281        '00-00-0000 00:00:00',
     282        '00-00-00 00:00:00',
     283        );
     284    },
     285    10,
     286    1
     287);
     288```
  • e20r-members-list/tags/8.6/inc/autoload.php

    r2672951 r2700272  
    55require_once __DIR__ . '/composer/autoload_real.php';
    66
    7 return ComposerAutoloaderInit40868d3a6d59e2d6945ded47ea627a6d::getLoader();
     7return ComposerAutoloaderInit8968a46d1d5402e6dbb4601beede8829::getLoader();
  • e20r-members-list/tags/8.6/inc/composer/autoload_real.php

    r2672951 r2700272  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInit40868d3a6d59e2d6945ded47ea627a6d
     5class ComposerAutoloaderInit8968a46d1d5402e6dbb4601beede8829
    66{
    77    private static $loader;
     
    2525        require __DIR__ . '/platform_check.php';
    2626
    27         spl_autoload_register(array('ComposerAutoloaderInit40868d3a6d59e2d6945ded47ea627a6d', 'loadClassLoader'), true, true);
     27        spl_autoload_register(array('ComposerAutoloaderInit8968a46d1d5402e6dbb4601beede8829', 'loadClassLoader'), true, true);
    2828        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
    29         spl_autoload_unregister(array('ComposerAutoloaderInit40868d3a6d59e2d6945ded47ea627a6d', 'loadClassLoader'));
     29        spl_autoload_unregister(array('ComposerAutoloaderInit8968a46d1d5402e6dbb4601beede8829', 'loadClassLoader'));
    3030
    3131        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
     
    3333            require __DIR__ . '/autoload_static.php';
    3434
    35             call_user_func(\Composer\Autoload\ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d::getInitializer($loader));
     35            call_user_func(\Composer\Autoload\ComposerStaticInit8968a46d1d5402e6dbb4601beede8829::getInitializer($loader));
    3636        } else {
    3737            $map = require __DIR__ . '/autoload_namespaces.php';
     
    5454
    5555        if ($useStaticLoader) {
    56             $includeFiles = Composer\Autoload\ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d::$files;
     56            $includeFiles = Composer\Autoload\ComposerStaticInit8968a46d1d5402e6dbb4601beede8829::$files;
    5757        } else {
    5858            $includeFiles = require __DIR__ . '/autoload_files.php';
    5959        }
    6060        foreach ($includeFiles as $fileIdentifier => $file) {
    61             composerRequire40868d3a6d59e2d6945ded47ea627a6d($fileIdentifier, $file);
     61            composerRequire8968a46d1d5402e6dbb4601beede8829($fileIdentifier, $file);
    6262        }
    6363
     
    7171 * @return void
    7272 */
    73 function composerRequire40868d3a6d59e2d6945ded47ea627a6d($fileIdentifier, $file)
     73function composerRequire8968a46d1d5402e6dbb4601beede8829($fileIdentifier, $file)
    7474{
    7575    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
  • e20r-members-list/tags/8.6/inc/composer/autoload_static.php

    r2672951 r2700272  
    55namespace Composer\Autoload;
    66
    7 class ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d
     7class ComposerStaticInit8968a46d1d5402e6dbb4601beede8829
    88{
    99    public static $files = array (
     
    9191    {
    9292        return \Closure::bind(function () use ($loader) {
    93             $loader->prefixLengthsPsr4 = ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d::$prefixLengthsPsr4;
    94             $loader->prefixDirsPsr4 = ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d::$prefixDirsPsr4;
    95             $loader->classMap = ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d::$classMap;
     93            $loader->prefixLengthsPsr4 = ComposerStaticInit8968a46d1d5402e6dbb4601beede8829::$prefixLengthsPsr4;
     94            $loader->prefixDirsPsr4 = ComposerStaticInit8968a46d1d5402e6dbb4601beede8829::$prefixDirsPsr4;
     95            $loader->classMap = ComposerStaticInit8968a46d1d5402e6dbb4601beede8829::$classMap;
    9696
    9797        }, null, ClassLoader::class);
  • e20r-members-list/tags/8.6/languages/e20r-members-list.pot

    r2672951 r2700272  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Better Members List for Paid Memberships Pro 8.5\n"
     5"Project-Id-Version: Better Members List for Paid Memberships Pro 8.6\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/e20r-members-list\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
     
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2022-02-04T13:27:08+00:00\n"
     12"POT-Creation-Date: 2022-03-27T16:56:55+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.6.0\n"
     
    3535msgstr ""
    3636
    37 #: class-e20r-members-list.php:145
     37#: class-e20r-members-list.php:138
    3838msgid "The specified E20R_Members_List() class property does not exist!"
    3939msgstr ""
    4040
    41 #: class-e20r-members-list.php:184
     41#: class-e20r-members-list.php:171
    4242msgid "The E20R Utilities Module is missing/inactive!"
    4343msgstr ""
    4444
    45 #: class-e20r-members-list.php:215
     45#: class-e20r-members-list.php:202
    4646msgid "Could not clear all of the cached members list data. Check error logs for more information."
    4747msgstr ""
    4848
    49 #: class-e20r-members-list.php:342
     49#: class-e20r-members-list.php:329
    5050msgid "Donate to support updates, maintenance and tech support for this plugin"
    5151msgstr ""
    5252
    53 #: class-e20r-members-list.php:346
     53#: class-e20r-members-list.php:333
    5454msgid "Donate"
    5555msgstr ""
    5656
     57#: class-e20r-members-list.php:338
     58msgid "View the documentation"
     59msgstr ""
     60
     61#: class-e20r-members-list.php:339
     62msgid "Docs"
     63msgstr ""
     64
     65#: class-e20r-members-list.php:344
     66msgid "View the Filter documentation"
     67msgstr ""
     68
     69#: class-e20r-members-list.php:345
     70msgid "Filters"
     71msgstr ""
     72
     73#: class-e20r-members-list.php:350
     74msgid "View the Actions documentation"
     75msgstr ""
     76
    5777#: class-e20r-members-list.php:351
    58 msgid "View the documentation"
    59 msgstr ""
    60 
    61 #: class-e20r-members-list.php:352
    62 msgid "Docs"
     78msgid "Actions"
     79msgstr ""
     80
     81#: class-e20r-members-list.php:356
     82msgid "Visit the support forum"
    6383msgstr ""
    6484
    6585#: class-e20r-members-list.php:357
    66 msgid "View the Filter documentation"
    67 msgstr ""
    68 
    69 #: class-e20r-members-list.php:358
    70 msgid "Filters"
     86msgid "Support"
     87msgstr ""
     88
     89#: class-e20r-members-list.php:362
     90msgid "Report issues with this plugin"
    7191msgstr ""
    7292
    7393#: class-e20r-members-list.php:363
    74 msgid "View the Actions documentation"
    75 msgstr ""
    76 
    77 #: class-e20r-members-list.php:364
    78 msgid "Actions"
    79 msgstr ""
    80 
    81 #: class-e20r-members-list.php:369
    82 msgid "Visit the support forum"
    83 msgstr ""
    84 
    85 #: class-e20r-members-list.php:370
    86 msgid "Support"
    87 msgstr ""
    88 
    89 #: class-e20r-members-list.php:375
    90 msgid "Report issues with this plugin"
    91 msgstr ""
    92 
    93 #: class-e20r-members-list.php:376
    9494msgid "Report Issues"
    9595msgstr ""
     
    101101
    102102#: src/E20R/members-list/admin/bulk/Bulk_Cancel.php:145
    103 msgid "Cannot find the pmpro_cancelMembershipLevel() function!"
     103msgid "The pmpro_cancelMembershipLevel() function is not defined. Is Paid Memberships Pro activated on this site?"
     104msgstr ""
     105
     106#: src/E20R/members-list/admin/bulk/Bulk_Operations.php:106
     107msgid "The specified variable is not an array of user IDs"
    104108msgstr ""
    105109
    106110#. translators: %1$s - The parameter supplied, %2$s - The class name where we expect the supplied parameter to exist,
    107 #: src/E20R/members-list/admin/bulk/Bulk_Operations.php:98
     111#: src/E20R/members-list/admin/bulk/Bulk_Operations.php:120
    108112msgid "Invalid parameter \"%1$s\" supplied for %2$s"
    109113msgstr ""
    110114
    111115#. translators: %1$s - The class name where we expect the supplied parameter to exist.
    112 #: src/E20R/members-list/admin/bulk/Bulk_Operations.php:123
     116#: src/E20R/members-list/admin/bulk/Bulk_Operations.php:145
    113117msgid "Invalid parameter supplied for %1$s"
    114118msgstr ""
     
    154158
    155159#: src/E20R/members-list/admin/bulk/Bulk_Update.php:408
     160#: src/E20R/members-list/Members_List.php:2163
     161#: src/E20R/members-list/Members_List.php:2177
    156162msgid "Unknown"
    157163msgstr ""
    158164
    159 #: src/E20R/members-list/admin/export/Export_Members.php:1005
     165#: src/E20R/members-list/admin/export/Export_Members.php:1006
    160166msgid "Error - Undefined HTTP headers. Exiting!"
    161167msgstr ""
    162168
    163 #: src/E20R/members-list/admin/export/Export_Members.php:1014
     169#: src/E20R/members-list/admin/export/Export_Members.php:1015
    164170msgid "Cannot transmit export file. Review web server error logs for notices/warnings/errors. Exiting!"
    165171msgstr ""
    166172
    167 #: src/E20R/members-list/admin/export/Export_Members.php:1026
     173#: src/E20R/members-list/admin/export/Export_Members.php:1027
    168174msgid "Error: No export data found to transmit..."
    169175msgstr ""
     
    251257msgstr ""
    252258
    253 #: src/E20R/members-list/Members_List.php:300
     259#: src/E20R/members-list/Members_List.php:317
    254260msgid "member"
    255261msgstr ""
    256262
    257 #: src/E20R/members-list/Members_List.php:301
     263#: src/E20R/members-list/Members_List.php:318
    258264msgid "members"
    259265msgstr ""
    260266
    261 #: src/E20R/members-list/Members_List.php:407
     267#: src/E20R/members-list/Members_List.php:446
    262268msgid "Error: Invalid list of tables & joins for member list!"
    263269msgstr ""
    264270
    265 #: src/E20R/members-list/Members_List.php:415
     271#: src/E20R/members-list/Members_List.php:454
    266272msgid "Error: No FROM table specified for member list!"
    267273msgstr ""
    268274
    269275#. translators: %1$s is the per-page value supplied by the e20r_memberslist_per_page filter.
    270 #: src/E20R/members-list/Members_List.php:568
     276#: src/E20R/members-list/Members_List.php:607
    271277msgid "Error: Invalid 'per_page' value (need an integer): %1$s"
    272278msgstr ""
    273279
    274280#. translators: %1$s - The error message returned by the exception thrown
    275 #: src/E20R/members-list/Members_List.php:710
     281#: src/E20R/members-list/Members_List.php:749
    276282msgid "Cannot create a valid Database Query. Error message: %1$s"
    277283msgstr ""
    278284
    279285#. translators: %1$s - The error message returned by the exception thrown
    280 #: src/E20R/members-list/Members_List.php:735
     286#: src/E20R/members-list/Members_List.php:774
    281287msgid "Cannot execute database query. Error message: %1$s"
    282288msgstr ""
    283289
    284 #: src/E20R/members-list/Members_List.php:1009
     290#: src/E20R/members-list/Members_List.php:1051
    285291msgid "Insecure action denied."
    286292msgstr ""
    287293
    288 #. translators: Error message from the execption thrown
    289 #: src/E20R/members-list/Members_List.php:1048
    290 msgid "Cannot export data!. Error message: %1$s"
    291 msgstr ""
    292 
    293 #: src/E20R/members-list/Members_List.php:1102
    294 #: src/E20R/members-list/Members_List.php:1111
     294#. translators: %1$s - Error message from exception
     295#: src/E20R/members-list/Members_List.php:1085
     296msgid "Bulk Cancel: %1$s"
     297msgstr ""
     298
     299#: src/E20R/members-list/Members_List.php:1146
     300#: src/E20R/members-list/Members_List.php:1155
    295301msgid "Error cancelling membership(s)"
    296302msgstr ""
    297303
    298304#. translators: %1$s - Error message from thrown exception(s)
    299 #: src/E20R/members-list/Members_List.php:1163
    300 #: src/E20R/members-list/Members_List.php:1178
     305#: src/E20R/members-list/Members_List.php:1207
     306#: src/E20R/members-list/Members_List.php:1222
    301307msgid "Cannot export member data! Error message: %1$s"
    302308msgstr ""
    303309
    304310#. translators: %1$s query string.
    305 #: src/E20R/members-list/Members_List.php:1264
     311#: src/E20R/members-list/Members_List.php:1320
    306312msgid "Error processing Members List database query: %1$s"
    307313msgstr ""
    308314
    309315#. translators: %1$s - The class parameter, %2$s - The class name where we expect said parameter to exist.
    310 #: src/E20R/members-list/Members_List.php:1298
     316#: src/E20R/members-list/Members_List.php:1354
    311317msgid "%1$s is not a member variable in %2$s"
    312318msgstr ""
    313319
    314 #: src/E20R/members-list/Members_List.php:1380
     320#: src/E20R/members-list/Members_List.php:1436
    315321msgid "Error: Invalid database configuration!!!"
    316322msgstr ""
    317323
    318 #: src/E20R/members-list/Members_List.php:1406
     324#: src/E20R/members-list/Members_List.php:1462
    319325msgid "Missing the \"FROM\" statement"
    320326msgstr ""
    321327
    322 #: src/E20R/members-list/Members_List.php:1417
     328#: src/E20R/members-list/Members_List.php:1473
    323329msgid "Missing the minimum expected number of \"JOIN\" statements"
    324330msgstr ""
    325331
    326332#. translators: %1$d the number of JOIN entries in the Meembers_List::table_list definition
    327 #: src/E20R/members-list/Members_List.php:1435
     333#: src/E20R/members-list/Members_List.php:1491
    328334msgid "Have %1$d JOIN entries, but need at least 3!"
    329335msgstr ""
    330336
    331 #: src/E20R/members-list/Members_List.php:1456
     337#: src/E20R/members-list/Members_List.php:1512
    332338msgid "Does not include the \"JOIN\" to support membership level search, but specified a level"
    333339msgstr ""
    334340
    335 #: src/E20R/members-list/Members_List.php:1468
     341#: src/E20R/members-list/Members_List.php:1524
    336342msgid "Unexpected condition value for the 4th \"JOIN\" element"
    337343msgstr ""
    338344
    339345#. translators: %1$s table name defined by PMPro, %2$s Unexpected table name from user/developer
    340 #: src/E20R/members-list/Members_List.php:1482
     346#: src/E20R/members-list/Members_List.php:1538
    341347msgid "Unexpected table name value for the 4th \"JOIN\" element. Want: \"%1$s\", got: \"%2$s\""
    342348msgstr ""
    343349
    344 #: src/E20R/members-list/Members_List.php:1645
    345 #: src/E20R/members-list/Members_List.php:2057
    346 #: src/E20R/members-list/Members_List.php:2154
     350#: src/E20R/members-list/Members_List.php:1701
     351#: src/E20R/members-list/Members_List.php:2097
     352#: src/E20R/members-list/Members_List.php:2233
    347353msgid "Cancel"
    348354msgstr ""
    349355
    350 #: src/E20R/members-list/Members_List.php:1646
    351 #: src/E20R/members-list/Members_List.php:1720
     356#: src/E20R/members-list/Members_List.php:1702
     357#: src/E20R/members-list/Members_List.php:1776
    352358msgid "Update"
    353359msgstr ""
    354360
    355 #: src/E20R/members-list/Members_List.php:1647
     361#: src/E20R/members-list/Members_List.php:1703
    356362msgid "Export"
    357363msgstr ""
    358364
    359 #: src/E20R/members-list/Members_List.php:1719
     365#: src/E20R/members-list/Members_List.php:1775
    360366msgid "Update member info"
    361367msgstr ""
    362368
    363 #: src/E20R/members-list/Members_List.php:1818
    364 #: src/E20R/members-list/Members_List.php:1835
     369#: src/E20R/members-list/Members_List.php:1874
     370#: src/E20R/members-list/Members_List.php:1891
    365371msgid "Not found"
    366372msgstr ""
    367373
    368 #: src/E20R/members-list/Members_List.php:1838
     374#: src/E20R/members-list/Members_List.php:1894
     375#: src/E20R/members-list/Members_List.php:2168
     376#: src/E20R/members-list/Members_List.php:2181
    369377msgid "N/A"
    370378msgstr ""
    371379
    372 #: src/E20R/members-list/Members_List.php:1877
    373 #: src/E20R/members-list/modules/Multiple_Memberships.php:93
     380#: src/E20R/members-list/Members_List.php:1940
     381#: src/E20R/members-list/Members_List.php:2306
     382#: src/E20R/members-list/modules/Multiple_Memberships.php:124
     383msgid "Reset"
     384msgstr ""
     385
     386#: src/E20R/members-list/Members_List.php:2010
     387msgid "Free"
     388msgstr ""
     389
     390#: src/E20R/members-list/Members_List.php:2062
     391msgid "Invalid"
     392msgstr ""
     393
     394#: src/E20R/members-list/Members_List.php:2128
    374395msgid "No levels found. Paid Memberships Pro is inactive!"
    375396msgstr ""
    376397
    377 #: src/E20R/members-list/Members_List.php:1901
    378 #: src/E20R/members-list/Members_List.php:2229
    379 #: src/E20R/members-list/modules/Multiple_Memberships.php:117
    380 msgid "Reset"
    381 msgstr ""
    382 
    383 #: src/E20R/members-list/Members_List.php:1970
    384 msgid "Free"
    385 msgstr ""
    386 
    387 #: src/E20R/members-list/Members_List.php:2022
    388 msgid "Invalid"
    389 msgstr ""
    390 
    391 #: src/E20R/members-list/Members_List.php:2086
    392 msgid "Never"
    393 msgstr ""
    394 
    395 #. translators: %1$s HTML %2$s formatted payment amount, %3$s HTML.
    396 #: src/E20R/members-list/Members_List.php:2103
     398#. translators: %1$s - HTML %2$s - next payment date, %3$s - HTML.
     399#: src/E20R/members-list/Members_List.php:2184
    397400msgid "N/A (%1$sNext Payment: %2$s%3$s)"
    398401msgstr ""
    399402
    400 #: src/E20R/members-list/Members_List.php:2159
    401 msgid "Bulk update membership end/expiration date"
    402 msgstr ""
    403 
    404 #: src/E20R/members-list/Members_List.php:2234
     403#: src/E20R/members-list/Members_List.php:2238
     404msgid "Update membership end/expiration date"
     405msgstr ""
     406
     407#: src/E20R/members-list/Members_List.php:2311
    405408msgid "Update the member's membership status"
    406409msgstr ""
    407410
    408 #: src/E20R/members-list/Members_List.php:2362
     411#: src/E20R/members-list/Members_List.php:2439
    409412msgid "No members found"
    410413msgstr ""
    411414
    412 #: src/E20R/members-list/Members_List.php:2366
     415#: src/E20R/members-list/Members_List.php:2443
    413416msgid "It's possible the information you're looking for can be found in one of the following categories:"
    414417msgstr ""
    415418
    416419#. translators: %1$s HTML, %2%s HTML.
    417 #: src/E20R/members-list/Members_List.php:2373
     420#: src/E20R/members-list/Members_List.php:2450
    418421msgid "Repeat search: %1$sActive Members list%2$s"
    419422msgstr ""
    420423
    421424#. translators: %1$s HTML, %2%s HTML.
    422 #: src/E20R/members-list/Members_List.php:2386
     425#: src/E20R/members-list/Members_List.php:2463
    423426msgid "Repeat search: %1$sAll Members list%2$s"
    424427msgstr ""
    425428
    426429#. translators: %1$s HTML, %2$s HTML.
    427 #: src/E20R/members-list/Members_List.php:2398
     430#: src/E20R/members-list/Members_List.php:2475
    428431msgid "Repeat search: %1$sCancelled Members list%2$s"
    429432msgstr ""
    430433
    431434#. translators: %1$s HTML, %2$s HTML.
    432 #: src/E20R/members-list/Members_List.php:2413
     435#: src/E20R/members-list/Members_List.php:2490
    433436msgid "Repeat search: %1$sExpired Members list%2$s"
    434437msgstr ""
    435438
    436439#. translators: %1$s HTML, %2$s HTML.
    437 #: src/E20R/members-list/Members_List.php:2425
     440#: src/E20R/members-list/Members_List.php:2502
    438441msgid "Repeat search: %1$sOld Members list%2$s"
    439442msgstr ""
    440443
    441444#. translators: %1$s HTML, %2$s HTML.
    442 #: src/E20R/members-list/Members_List.php:2436
     445#: src/E20R/members-list/Members_List.php:2513
    443446msgid "Repeat search: %1$sAll Users list%2$s"
    444447msgstr ""
     448
     449#: src/E20R/members-list/modules/Multiple_Memberships.php:84
     450msgid "primary"
     451msgstr ""
     452
     453#: src/E20R/members-list/modules/Multiple_Memberships.php:133
     454msgid "Click to edit primary membership level"
     455msgstr ""
  • e20r-members-list/tags/8.6/src/E20R/members-list/Members_List.php

    r2672951 r2700272  
    3636use E20R\Utilities\Message;
    3737use E20R\Utilities\Utilities;
     38use stdClass;
    3839use WP_List_Table;
     40use WP_User;
    3941
    4042if ( ! defined( 'ABSPATH' ) && ! defined( 'PLUGIN_PHPUNIT' ) ) {
     
    4648}
    4749
    48 if ( ! class_exists( '\\E20R\\Members_List\\Members_List' ) ) {
     50if ( ! class_exists( 'E20R\Members_List\Members_List' ) ) {
    4951
    5052    /**
     
    272274
    273275        /**
     276         * List of date values we consider 'empty' (not confiured/set/defined)
     277         *
     278         * @var mixed|void|null $empty_date_values
     279         */
     280        private $empty_date_values = array();
     281
     282        /**
    274283         * Members_List constructor.
    275284         *
     
    288297            $this->action      = $this->utils->get_variable( 'action', '' );
    289298            $this->date_format = get_option( 'date_format' );
     299
     300            // Default settings for what we consider 'empty' (not set) date values
     301            add_filter(
     302                'e20r_members_list_empty_date_values',
     303                array( $this, 'set_empty_date_values' ),
     304                -1,
     305                1
     306            );
    290307
    291308            if ( empty( $page ) ) {
     
    326343            );
    327344
     345            // Configure the defaults for the empty/not set date values
     346            $this->empty_date_values = apply_filters( 'e20r_members_list_empty_date_values', array() );
     347
    328348            if ( $this->is_module_enabled( 'pmpro-multiple-memberships-per-user/pmpro-multiple-memberships-per-user.php' ) ) {
    329349                // TODO: Add actions to process data for the PMPro Multiple Memberships Per User add-on
    330350                $this->utils->log( 'Enable the MMPU extensions' );
    331351            }
     352
    332353
    333354            /**
     
    341362        }
    342363
     364        /**
     365         * Define the list of 'empty' values for the date check(s)
     366         *
     367         * @param array $values Received list of values
     368         *
     369         * @return array
     370         */
     371        public function set_empty_date_values( $values = array() ) {
     372            return $values + array(
     373                '',
     374                null,
     375                0,
     376                '0',
     377                '0000-00-00 00:00:00',
     378                '0000-00-00',
     379                '00:00:00',
     380            );
     381        }
    343382        /**
    344383         * Generate the SQL and set the sql_query member variable to use the query
     
    9611000                // Some of the default columns are sortable.
    9621001                switch ( $col ) {
     1002                    case 'display_name':
    9631003                    case 'user_login':
    9641004                    case 'user_email':
     
    9671007                    case 'status':
    9681008                    case 'last':
    969                     case 'last_name':
    9701009                        $sortable_columns[ $col ] = array( $col, false );
    9711010                        break;
     
    9971036
    9981037            // Are we processing a bulk action?
    999             if ( 1 === preg_match( '/bulk-/', $a ) || 1 === preg_match( '/bulk-/', $a2 ) ) {
     1038            if (
     1039                ( null !== $a && 1 === preg_match( '/bulk-/', $a ) ) ||
     1040                ( null !== $a2 && 1 === preg_match( '/bulk-/', $a2 ) )
     1041            ) {
    10001042
    10011043                $this->utils->log( 'Processing a bulk action' );
     
    10181060                $action           = $this->current_action();
    10191061                $data             = array();
    1020                 $selected_members = $this->utils->get_variable( 'member_user_id', array() );
     1062                $selected_members = $this->utils->get_variable( 'member_user_ids', array() );
    10211063
    10221064                foreach ( $selected_members as $key => $user_id ) {
     
    10341076
    10351077                if ( in_array( 'bulk-cancel', $bulk_actions, true ) ) {
    1036                     $this->cancel = new Bulk_Cancel( $data, $this->utils );
    1037                     $this->cancel->execute();
     1078                    try {
     1079                        $this->cancel = new Bulk_Cancel( $data, $this->utils );
     1080                        $this->cancel->execute();
     1081                    } catch ( InvalidProperty $e ) {
     1082                        $this->utils->add_message(
     1083                            sprintf(
     1084                                // translators: %1$s - Error message from exception
     1085                                esc_attr__( 'Bulk Cancel: %1$s', 'e20r-members-list' ),
     1086                                $e->getMessage()
     1087                            ),
     1088                            'error',
     1089                            'backend'
     1090                        );
     1091                    }
     1092
    10381093                    return;
    10391094
     
    10411096
    10421097                    $this->utils->log( 'Requested Export of members!' );
    1043                     try {
    1044                         $this->export_members();
    1045                     } catch ( DBQueryError | InvalidSQL $e ) {
    1046                         $msg = sprintf(
    1047                                 // translators: Error message from the execption thrown
    1048                             esc_attr__( 'Cannot export data!. Error message: %1$s', 'e20r-members-list' ),
    1049                             $e->getMessage()
    1050                         );
    1051                         $this->utils->add_message( $msg, 'error', 'backend' );
    1052                         return;
    1053                     }
     1098                    $this->export_members();
    10541099
    10551100                    // To push the export file to the browser, we have to terminate execution of this process.
     
    10781123                $this->utils->log( 'Single action for the Members List...' );
    10791124
    1080                 $user_id                     = $this->utils->get_variable( 'member_user_id', array() );
     1125                $user_id                     = $this->utils->get_variable( 'member_user_ids', array() );
    10811126                $level_id                    = $this->utils->get_variable( 'membership_id', array() );
    10821127                $action                      = $this->current_action();
     
    10971142                            $this->cancel = new Bulk_Cancel( $user_ids, $this->utils );
    10981143                        }
    1099 
    11001144
    11011145                        if ( false === $this->cancel->execute() ) {
     
    12051249         * @throws InvalidSQL Raised when there's a problem with the SQL we generated.
    12061250         * @throws DBQueryError Raised if the DB Query reports a problem
    1207          * @throws BadOperation|InvalidSettingsKey Raised when the Cache() class tries something unexpected
    12081251         */
    12091252        public function get_members( $per_page = null, $page_number = null, $return_count = false ) {
     
    12131256            }
    12141257
    1215             $result             = null;
    12161258            $result_cache_group = $this->page->get( 'result_cache_group' );
    1217             $cache_timeout      = ( (int) $this->page->get( 'cache_timeout' ) * self::MINUTE_IN_SECONDS );
     1259            try {
     1260                $cache_timeout = ( (int) $this->page->get( 'cache_timeout' ) * self::MINUTE_IN_SECONDS );
     1261            } catch ( InvalidSettingsKey $e ) {
     1262                $this->utils->log( 'Error: cache_timeout is not a valid settings key!' );
     1263                $cache_timeout = 10 * self::MINUTE_IN_SECONDS;
     1264            }
    12181265
    12191266            global $wpdb;
     
    12211268            if ( empty( $this->sql_query ) || 1 !== preg_match_all( '/SELECT\s+.*\s+FROM(\s+(.*)){1,}/im', $this->sql_query ) ) {
    12221269                $this->utils->log( 'Error in/Missing SQL statement! "' . $this->sql_query . '"' );
    1223                 throw new InvalidSQL( 'Error: Attempting to fetch data without a valid SQL query!' );
     1270                throw new InvalidSQL( 'Attempting to fetch data without a valid SQL query!' );
    12241271            }
    12251272
    12261273            // Generate the Cache key based on
    12271274            $lookup_cache_key = md5( $this->sql_query );
    1228             $result           = Cache::get( $lookup_cache_key, $result_cache_group );
     1275            try {
     1276                $result = Cache::get( $lookup_cache_key, $result_cache_group );
     1277            } catch ( BadOperation $e ) {
     1278                $this->utils->log( 'Error returning cached results for ' . $lookup_cache_key );
     1279                $result = null;
     1280            }
    12291281
    12301282            if ( null === $result ) {
     
    12511303                // Save the result to the cache based on the cache specific key
    12521304                $this->utils->log( "Attempting to cache the results for {$lookup_cache_key}" );
    1253                 if ( false === Cache::set( $lookup_cache_key, $result, $cache_timeout, $result_cache_group ) ) {
    1254                     $this->utils->log( 'Error saving the result to cache!' );
     1305                try {
     1306                    if ( false === Cache::set( $lookup_cache_key, $result, $cache_timeout, $result_cache_group ) ) {
     1307                        $this->utils->log( 'Error saving the result to cache!' );
     1308                    }
     1309                } catch ( BadOperation $e ) {
     1310                    $this->utils->log( 'Error: Unable to save to cache for ' . $lookup_cache_key );
    12551311                }
    12561312            } else {
     
    15671623            $this->utils->log( 'Requesting (active/old/etc) member export' );
    15681624
    1569             $member_ids  = $this->utils->get_variable( 'member_user_id', array() );
     1625            $member_ids  = $this->utils->get_variable( 'member_user_ids', array() );
    15701626            $added_where = null;
    15711627
     
    16771733         */
    16781734        public function column_user_login( $item ) {
    1679             $user = new \WP_User( $item['ID'] );
     1735            $user = new WP_User( $item['ID'] );
    16801736
    16811737            $edit_url = add_query_arg(
     
    18321888
    18331889            if ( empty( $address ) ) {
    1834 
     1890                $this->utils->log( "User {$item['ID']} has no billing address" );
    18351891                $address = esc_attr__( 'Not found', 'e20r-members-list' );
    18361892
     
    18581914                <input type="hidden" value="%3$s" class="e20r-members-list-membership_id-label" name="e20r-members-list-membership_label_%2$s" />
    18591915                <input type="hidden" value="%1$d" class="e20r-members-list-db-membership_id" name="e20r-members-list-db_membership_id_%2$s" />
     1916                <input type="hidden" value=""     class="e20r-members-list-db-membership_level_ids" name="e20r-members-list-db_membership_level_ids_%2$s" />
    18601917                <input type="hidden" value="%5$d" class="e20r-members-list-db_record_id" name="e20r-members-list-db_record_id_%2$s" />
    18611918                <input type="hidden" value="%4$s" class="e20r-members-list-field-name" name="e20r-members-list-field_name_%2$s" />',
     
    18671924            );
    18681925
    1869             $options = '';
    1870             if ( function_exists( 'pmpro_getAllLevels' ) ) {
    1871                 $levels = pmpro_getAllLevels( true, true );
    1872             } else {
    1873 
    1874                 // Default info if PMPro is disabled.
    1875                 $null_level           = new \stdClass();
    1876                 $null_level->level_id = 0;
    1877                 $null_level->name     = esc_attr__( 'No levels found. Paid Memberships Pro is inactive!', 'e20r-members-list' );
    1878                 $levels               = array( $null_level );
    1879             }
    1880 
    1881             foreach ( $levels as $level ) {
    1882                 $options .= sprintf(
    1883                     '<option value="%1$s" %2$s>%3$s</option>',
    1884                     $level->id,
    1885                     selected( $level->id, $item['membership_id'], false ),
    1886                     $level->name
    1887                 ) . "\n";
    1888             }
     1926            $options = self::build_option_string( $item['membership_id'] );
     1927
    18891928            $new_membershiplevel_input = sprintf(
    18901929                '<div class="ml-row-settings clearfix">
     
    19311970            if ( true === $this->is_module_enabled( 'pmpro-multiple-memberships-per-user/pmpro-multiple-memberships-per-user.php' ) ) {
    19321971                $mmpu = new Multiple_Memberships();
    1933                 return $mmpu->column_name( $item );
     1972                $this->utils->log( 'MMPU support is deactivated pending support for adding/removing MMPU memberships' );
     1973                // return $mmpu->multiple_membership_column( $item ); FIXME: Actually enable MMPU support
    19341974            }
    19351975
     
    19792019         * @param array $item The record to process.
    19802020         *
    1981          * @return \stdClass
     2021         * @return stdClass
    19822022         */
    19832023        public function column_code( $item ) {
     
    20182058        public function column_startdate( $item ) {
    20192059
    2020             if ( '0000-00-00 00:00:00' === $item['startdate'] || empty( $item['startdate'] ) ) {
     2060            if ( $this->has_empty_date( $item['startdate'] ) ) {
    20212061                $date_value  = null;
    20222062                $start_label = esc_attr__( 'Invalid', 'e20r-members-list' );
    20232063            } else {
    2024                 $date_value  = ! empty( $item['startdate'] ) ? date_i18n( 'Y-m-d', strtotime( $item['startdate'], time() ) ) : null;
    2025                 $start_label = date_i18n( $this->date_format, strtotime( $item['startdate'], time() ) );
    2026             }
    2027 
    2028             $min_val = empty( $item['startdate'] ) ? sprintf( 'min="%s"', date_i18n( 'Y-m-d', time() ) ) : null;
     2064                $date_value  = ! empty( $item['startdate'] ) ? gmdate( 'Y-m-d', strtotime( $item['startdate'], time() ) ) : null;
     2065                $start_label = gmdate( $this->date_format, strtotime( $item['startdate'], time() ) );
     2066            }
     2067
     2068            $min_val = empty( $item['startdate'] ) ? sprintf( 'min="%s"', gmdate( 'Y-m-d', time() ) ) : null;
    20292069
    20302070            $startdate_input = sprintf(
     
    20692109
    20702110        /**
     2111         * Generate the HTML for the membership level options
     2112         *
     2113         * @param int $level_id The membership level ID to highlight
     2114         *
     2115         * @return string
     2116         */
     2117        public static function build_option_string( $level_id ) {
     2118
     2119            $options = '';
     2120
     2121            if ( function_exists( 'pmpro_getAllLevels' ) ) {
     2122                $levels = pmpro_getAllLevels( true, true );
     2123            } else {
     2124
     2125                // Default info if PMPro is disabled.
     2126                $null_level           = new stdClass();
     2127                $null_level->level_id = 0;
     2128                $null_level->name     = esc_attr__( 'No levels found. Paid Memberships Pro is inactive!', 'e20r-members-list' );
     2129                $levels               = array( $null_level );
     2130            }
     2131
     2132            foreach ( $levels as $level ) {
     2133                $options .= sprintf(
     2134                    '<option value="%1$s" %2$s>%3$s</option>',
     2135                    $level->id,
     2136                    selected( $level->id, $level_id, false ),
     2137                    $level->name
     2138                ) . "\n";
     2139            }
     2140
     2141            return $options;
     2142        }
     2143        /**
     2144         * Does the received value represent an 'empty' date value
     2145         *
     2146         * @param string|int|null $date The value to check
     2147         *
     2148         * @return bool
     2149         */
     2150        public function has_empty_date( $date ) {
     2151            return in_array( $date, $this->empty_date_values, true );
     2152        }
     2153        /**
    20712154         * Create the last column for the default Members_List table (Expiration date)
    20722155         *
     
    20772160        public function column_last( $item ) {
    20782161
    2079             $enddate = null;
    2080 
    2081             // No billing amount (not recurring) and no end date (i.e. a free, limit less membership)
    2082             if (
    2083                     empty( $item['billing_amount'] ) &&
    2084                 ( empty( $item['enddate'] ) || '0000-00-00 00:00:00' === $item['enddate'] )
    2085             ) {
    2086                 $enddate_label = esc_attr__( 'Never', 'e20r-members-list' );
    2087                 $enddate_label = null;
    2088             } else {
    2089                 $enddate       = date_i18n(
    2090                     'Y-m-d',
     2162            $no_enddate    = false;
     2163            $html_label    = esc_attr__( 'Unknown', 'e20r-members-list' );
     2164            $enddate_label = '';
     2165
     2166            // The end-date field is empty/not configured (never ending membership)
     2167            if ( ! isset( $item['enddate'] ) || in_array( $item['enddate'], $this->empty_date_values, true ) ) {
     2168                $enddate_label = esc_attr__( 'N/A', 'e20r-members-list' );
     2169                $html_label    = $enddate_label;
     2170                $no_enddate    = true;
     2171            }
     2172
     2173            // We have a user with a recurring billing membership.
     2174            if ( true === $no_enddate && ! empty( $item['billing_amount'] ) && ! empty( $item['cycle_number'] ) ) {
     2175                $next_payment = pmpro_next_payment( $item['ID'], 'success', 'timestamp' );
     2176                if ( false === $next_payment ) {
     2177                    $next_payment_date = esc_attr__( 'Unknown', 'e20r-members-list' );
     2178                } else {
     2179                    $next_payment_date = gmdate( $this->date_format, $next_payment );
     2180                }
     2181                $enddate_label = esc_attr__( 'N/A', 'e20r-members-list' );
     2182                $html_label    = sprintf(
     2183                    // translators: %1$s - HTML %2$s - next payment date, %3$s - HTML.
     2184                    esc_attr__( 'N/A (%1$sNext Payment: %2$s%3$s)', 'e20r-members-list' ),
     2185                    '<span class="e20r-members-list-small" style="font-size: 10px; font-style: italic;">',
     2186                    $next_payment_date,
     2187                    '</span>'
     2188                );
     2189            }
     2190
     2191            // Setting the date label only if the value is non-empty _AND_ the value is _NOT_ ''
     2192            if ( false === $no_enddate ) {
     2193                $enddate_label = gmdate(
     2194                    $this->date_format,
    20912195                    strtotime( $item['enddate'], time() )
    20922196                );
    2093                 $enddate_label = date_i18n( $this->date_format, strtotime( $item['enddate'], time() ) );
    2094             }
    2095 
    2096             // The membership level has recurring payment.
    2097             if (
    2098                     ( empty( $item['enddate'] ) || '0000-00-00 00:00:00' === $item['enddate'] ) &&
    2099                     ! empty( $item['billing_amount'] ) && ! empty( $item['cycle_number'] )
    2100             ) {
    2101                 $enddate_label = sprintf(
    2102                         // translators: %1$s HTML %2$s formatted payment amount, %3$s HTML.
    2103                     esc_attr__( 'N/A (%1$sNext Payment: %2$s%3$s)', 'e20r-members-list' ),
    2104                     '<span class="e20r-members-list-small" style="font-size: 10px; font-style: italic;">',
    2105                     date_i18n(
    2106                         $this->date_format,
    2107                         pmpro_next_payment(
    2108                             $item['ID'],
    2109                             'success',
    2110                             'timestamp'
    2111                         )
    2112                     ),
    2113                     '</span>'
    2114                 );
    2115                 $enddate = null;
    2116             }
    2117 
    2118             $enddate = apply_filters( 'e20r_members_list_enddate_col_result', $enddate, $item );
     2197                $html_label    = $enddate_label;
     2198            }
    21192199
    21202200            // BUG FIX: Didn't set the date format to match the WP setting as documented.
    2121             $date_value = ! (
    2122                     empty( $item['enddate'] ) ||
    2123                     '0000-00-00 00:00:00' === $item['enddate'] ) ?
    2124                     date_i18n( $this->date_format, strtotime( $item['enddate'], time() ) ) :
    2125                     null;
     2201            $date_value = ( false === $no_enddate ) ?
     2202                strtotime( $item['enddate'], time() ) :
     2203                null;
     2204
     2205            $date_value = apply_filters( 'e20r_members_list_enddate_col_result', $date_value, $item );
    21262206
    21272207            // These are used to configure the enddate with JavaScript.
     
    21332213                <input type="hidden" value="%4$s" class="e20r-members-list-db-enddate" name="e20r-members-list-db_enddate_%2$s" />
    21342214                <input type="hidden" value="%5$d" class="e20r-members-list-db_record_id" name="e20r-members-list-db_record_id_%2$s" />
    2135                 <input type="hidden" value="%6$s" class="e20r-members-list-field-name" name="e20r-members-list-field_name_%2$s" />',
     2215                <input type="hidden" value="enddate" class="e20r-members-list-field-name" name="e20r-members-list-field_name_%2$s" />',
    21362216                $item['membership_id'],
    21372217                $item['ID'],
     
    21392219                $date_value,
    21402220                $item['record_id'],
    2141                 'enddate'
    21422221            );
    21432222
     
    21462225                        %1$s
    21472226                        <input type="date" placeholder="YYYY-MM-DD" pattern="(?:19|20)[0-9]{2}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9])|(?:(?!02)(?:0[1-9]|1[0-2])-(?:30))|(?:(?:0[13578]|1[02])-31))" title="Enter a date in this format YYYY-MM-DD" name="e20r-members-list-new_enddate_%2$s" class="e20r-members-list-input-enddate" value="%3$s"/>
    2148                         <br>
     2227                        <br />
    21492228                        <a href="#" class="e20r-members-list-cancel e20r-members-list-list-link">%4$s</a>
    21502229                    </div>',
    21512230                $enddate_input,
    21522231                $item['ID'],
    2153                 $date_value,
     2232                $date_value ? gmdate( 'Y-m-d', $date_value ) : $date_value,
    21542233                esc_attr__( 'Cancel', 'e20r-members-list' )
    21552234            );
    21562235
    2157             $value = sprintf(
    2158                 '<a href="#" class="e20r-members-list_enddate e20r-members-list-editable" title="%1$s">%2$s<span class="dashicons dashicons-edit"></a></span>%3$s',
    2159                 esc_attr__( 'Bulk update membership end/expiration date', 'e20r-members-list' ),
    2160                 $enddate_label,
     2236            return sprintf(
     2237                '<a href="#" class="e20r-members-list_enddate e20r-members-list-editable" title="%1$s">%2$s<span class="dashicons dashicons-edit"></span></a>%3$s',
     2238                esc_attr__( 'Update membership end/expiration date', 'e20r-members-list' ),
     2239                $html_label,
    21612240                $new_date_input
    21622241            );
    2163 
    2164             return $value;
    21652242        }
    21662243
     
    22212298                        %3$s
    22222299                        </select>
    2223                         <br>
     2300                        <br />
    22242301                        <a href="#" class="e20r-members-list-cancel e20r-members-list-link">%4$s</a>
    22252302                    </div>',
  • e20r-members-list/tags/8.6/src/E20R/members-list/admin/bulk/Bulk_Cancel.php

    r2672951 r2700272  
    2222namespace E20R\Members_List\Admin\Bulk;
    2323
     24use E20R\Members_List\Admin\Exceptions\InvalidMemberList;
    2425use E20R\Members_List\Admin\Exceptions\InvalidProperty;
    2526use E20R\Members_List\Admin\Exceptions\PMProNotActive;
    2627use E20R\Utilities\Message;
    2728use E20R\Utilities\Utilities;
     29use function pmpro_cancelMembershipLevel;
    2830
    2931if ( ! defined( 'ABSPATH' ) && ! defined( 'PLUGIN_PHPUNIT' ) ) {
     
    3133}
    3234
    33 if ( ! class_exists( '\\E20R\\Members_List\\Admin\\Bulk\\Bulk_Cancel' ) ) {
     35if ( ! class_exists( 'E20R\Members_List\Admin\Bulk\Bulk_Cancel' ) ) {
    3436
    3537    /**
     
    4143         * Bulk_Cancel constructor (singleton)
    4244         *
    43          * @param array[]|int[]|null $members_to_update The array of member IDs to perform the bulk cancel operation against
    44          * @param Utilities|null     $utils Instance of the E20R Utilities Module class
     45         * @param array[]|null  $members_to_update The array of member IDs to perform the bulk cancel operation against
     46         * @param Utilities|null $utils Instance of the E20R Utilities Module class
    4547         *
    4648         * @access public
    4749         * @throws InvalidProperty Thrown when the class or base class lacks the specified set() property
     50         * @throws InvalidMemberList Thrown when the supplied array isn't null or has the wrong format
    4851         */
    49         public function __construct( $members_to_update = array(), $utils = null ) {
     52        public function __construct( $members_to_update = null, $utils = null ) {
    5053
    5154            if ( empty( $utils ) ) {
     
    5457            }
    5558
    56             parent::__construct( $utils );
     59            parent::__construct( $members_to_update, $utils );
    5760
    5861            $this->set( 'operation', 'cancel' );
    59             $this->set( 'members_to_update', $members_to_update );
    6062        }
    6163
     
    6870        public function execute() {
    6971
     72            if ( ! $this->pmpro_is_active() ) {
     73                return false;
     74            }
     75
    7076            // Process all User & level ID for the single action.
    7177            $this->failed = array();
     
    7480            // Process all selected records/members
    7581            foreach ( $this->members_to_update as $key => $cancel_info ) {
    76                 try {
    77                     if ( false === $this->cancel_member( $cancel_info['user_id'], $cancel_info['level_id'] ) ) {
    78                         if ( ! isset( $this->failed[ $cancel_info['user_id'] ] ) ) {
    79                             $this->failed[ $cancel_info['user_id'] ] = array();
    80                         }
    81                         $this->failed[ $cancel_info['user_id'] ][] = $cancel_info['level_id']; // FIXME: Add level info for multiple membership levels
     82                if ( false === $this->cancel_member( $cancel_info['user_id'], $cancel_info['level_id'] ) ) {
     83                    if ( ! isset( $this->failed[ $cancel_info['user_id'] ] ) ) {
     84                        $this->failed[ $cancel_info['user_id'] ] = array();
    8285                    }
    83                 } catch ( PMProNotActive $e ) {
    84                     $this->utils->add_message( $e->getMessage(), 'error', 'backend' );
    85                     $this->failed[] = $cancel_info['user_id'];
    86                     return false;
     86                    $this->failed[ $cancel_info['user_id'] ][] = $cancel_info['level_id']; // FIXME: Add level info for multiple membership levels
    8787                }
    8888            }
     
    144144                throw new PMProNotActive(
    145145                    esc_attr__(
    146                         'Cannot find the pmpro_cancelMembershipLevel() function!',
     146                        'The pmpro_cancelMembershipLevel() function is not defined. Is Paid Memberships Pro activated on this site?',
    147147                        'e20r-members-list'
    148148                    )
    149149                );
    150150            }
    151             $this->utils->log( "Cancelling membership level {$id} for user {$id}" );
    152             return \pmpro_cancelMembershipLevel( $level_id, $id, 'admin_cancelled' );
     151            $this->utils->log( "Cancelling membership level {$level_id} for user {$id}" );
     152            return pmpro_cancelMembershipLevel( $level_id, $id, 'admin_cancelled' );
     153        }
     154
     155        /**
     156         * Check if PMPro is active and throw exception if it isn't
     157         *
     158         * @return bool
     159         */
     160        private function pmpro_is_active() {
     161            return function_exists( 'pmpro_cancelMembershipLevel' );
    153162        }
    154163    }
  • e20r-members-list/tags/8.6/src/E20R/members-list/admin/bulk/Bulk_Operations.php

    r2672951 r2700272  
    2222namespace E20R\Members_List\Admin\Bulk;
    2323
     24use E20R\Members_List\Admin\Exceptions\InvalidMemberList;
    2425use E20R\Members_List\Admin\Exceptions\InvalidProperty;
    2526use E20R\Utilities\Message;
     
    4748         * Array of WP_User IDs where the cancel operation failed
    4849         *
    49          * @var null|int[] $failed
     50         * @var array[][] $failed
    5051         */
    51         protected $failed = null;
     52        protected $failed = array();
    5253
    5354        /**
    5455         * Array of members to update where the memebr date is represented as an array per member
    5556         *
    56          * @var array[]|int[]|null
     57         * @var array[]
    5758         */
    5859        protected $members_to_update = array();
     
    6869         * Bulk_Cancel constructor (singleton)
    6970         *
     71         * @param array          $members_to_update The list of members and level IDs to process
    7072         * @param Utilities|null $utils Instance of the E20R Utilities Module class
    7173         *
    7274         * @access public
    7375         */
    74         public function __construct( $utils = null ) {
     76        public function __construct( $members_to_update = null, $utils = null ) {
    7577
    7678            if ( empty( $utils ) ) {
     
    7981            }
    8082
     83            if ( null !== $members_to_update ) {
     84                $this->set( 'members_to_update', $members_to_update );
     85            }
    8186            $this->utils = $utils;
    8287        }
     
    8994         *
    9095         * @throws InvalidProperty Raised if the user supplies an invalid class parameter
     96         * @throws InvalidMemberList Raised if we're attempting to set the 'members_to_update' property and its value isn't appropriate
    9197         */
    9298        public function set( string $param, $value ) {
     99
     100            if (
     101                'members_to_update' === $param &&
     102                null !== $value &&
     103                false === $this->valid_member_array( $value )
     104            ) {
     105                throw new InvalidMemberList(
     106                    esc_attr__(
     107                        'The specified variable is not an array of user IDs',
     108                        'e20r-members-list'
     109                    )
     110                );
     111            } elseif ( 'members_to_update' === $param && null === $value ) {
     112                $value = array();
     113            }
     114
    93115            // Make sure we let the caller know there's a problem if the variable doesn't exist.
    94116            if ( ! property_exists( $this, $param ) ) {
     
    131153
    132154        /**
     155         * Make sure the array is a valid user/membership level array
     156         *
     157         * @param mixed $members Variable (array) to test
     158         *
     159         * @return bool
     160         */
     161        protected function valid_member_array( $members ) {
     162
     163            if ( ! is_array( $members ) ) {
     164                return false;
     165            }
     166
     167            if ( empty( $members ) ) {
     168                return true;
     169            }
     170
     171            return array_reduce(
     172                $members,
     173                function ( $result, $item ) {
     174                    return $result &&
     175                        ( isset( $item['user_id'] ) && is_int( $item['user_id'] ) ) &&
     176                        ( isset( $item['level_id'] ) && is_int( $item['level_id'] ) );
     177                },
     178                true
     179            );
     180        }
     181
     182        /**
    133183         * Execute the Bulk Operation
    134184         *
  • e20r-members-list/tags/8.6/src/E20R/members-list/admin/bulk/Bulk_Update.php

    r2672951 r2700272  
    2222namespace E20R\Members_List\Admin\Bulk;
    2323
     24use DateTime;
     25use E20R\Members_List\Admin\Exceptions\InvalidMemberList;
    2426use E20R\Members_List\Admin\Exceptions\InvalidProperty;
    2527use E20R\Members_List\Admin\Exceptions\PMProNotActive;
     
    4143         * Bulk_Update constructor
    4244         *
    43          * @param array          $members Array of member information to update
     45         * @param null|array[]   $members Array of member information to update
    4446         * @param null|Utilities $utils Instance of the E20R Utilities Module class
    4547         *
    46          * @throws InvalidProperty Raised if the specified class parameter for some reason is missing
     48         * @throws InvalidMemberList Raised if the supplied $members variable isn't a list of integers
    4749         */
    48         public function __construct( $members, $utils = null ) {
    49 
     50        public function __construct( $members = null, $utils = null ) {
    5051            if ( empty( $utils ) ) {
    5152                $message = new Message();
    5253                $utils   = new Utilities( $message );
    5354            }
    54 
    55             parent::__construct( $utils );
    56             $this->set( 'members_to_update', $members );
    57             $this->set( 'operation', 'cancel' );
     55            $this->utils = $utils;
     56            parent::__construct( $members, $this->utils );
     57            $this->set( 'operation', 'update' );
    5858        }
    5959
     
    495495
    496496        /**
    497          * Return the list of members being updated
    498          *
    499          * @return array[]
    500          */
    501         public function get_members() {
    502             return $this->members_to_update;
    503         }
    504 
    505         /**
    506497         * Test the date supplied for MySQL compliance
    507498         *
     
    515506        private function validate_date_format( $date, $format = 'Y-m-d' ) {
    516507
    517             $check_date = \DateTime::createFromFormat( $format, $date );
     508            $check_date = DateTime::createFromFormat( $format, $date );
    518509
    519510            return $check_date && $check_date->format( $format ) === $date;
  • e20r-members-list/tags/8.6/src/E20R/members-list/admin/export/Export_Members.php

    r2672951 r2700272  
    3030}
    3131
    32 if ( ! class_exists( '\\E20R\\Members_List\\Admin\\Export\\Export_Members' ) ) {
     32if ( ! class_exists( 'E20R\Members_List\Admin\Export\Export_Members' ) ) {
    3333
    3434    /**
     
    662662         */
    663663        private function enclose( $text ) {
     664            $text = empty( $text ) ? '' : $text;
    664665            return '"' . str_replace( '"', '\\"', $text ) . '"';
    665666        }
  • e20r-members-list/tags/8.6/src/E20R/members-list/js/e20r-memberslist-page.js

    r2672951 r2700272  
    22 * License:
    33
    4     Copyright 2016-2021 - Eighty / 20 Results by Wicked Strong Chicks, LLC ([email protected])
     4    Copyright 2016-2022 - Eighty / 20 Results by Wicked Strong Chicks, LLC ([email protected])
    55
    66    This program is free software; you can redistribute it and/or modify
     
    2323
    2424    let e20rMembersList_Page = {
    25         init: function () {
    26 
    27             // this.levels_dropdown = $('select#e20r-pmpro-memberslist-levels');
    28             // this.enddate_lnk = $('a.e20r-members-list_enddate');
    29             // this.cancelMemberLnk = $('a.e20r-cancel-member');
    30             // this.updateBtn = $('a.e20r-members-list-save');
    31             this.memberslist_form = $('.e20r-pmpro-memberslist-page form#posts-filter');
    32             this.edit_lnk = $('a.e20r-members-list-editable');
    33             this.resetBtn = $('a.e20r-members-list-cancel');
    34             this.updateMemberLnk = $('a.e20r-update-member');
    35             this.exportBtn = $('a.e20r-memberslist-export');
    36             this.changed_input = $('input[class^="e20r-members-list-input-"]');
    37             this.changed_select = $( 'select[class^="e20r-members-list-select-"]');
    38             this.levels_dropdown = $('#e20r-pmpro-memberslist-levels');
    39             this.bulkUpdate = $('#doaction, #doaction2');
    40             this.updateListBtn = $('#e20r-update-list');
    41             this.search_field = $('#post-search-input');
    42             this.dateFields = $('.e20r-members-list-input-enddate, .e20r-members-list-input-startdate');
    43             this.dataSearchBtn = $('#e20r-memberslist-search-data');
    44             this.bulkActionSelectTop = $('select#bulk-action-selector-top');
    45             this.bulkActionSelectBottom = $('select#bulk-action-selector-bottom');
    46 
    47             this.dateOpts = {
    48                 year: '2-digit', month: 'short',
    49                 day: 'numeric'
    50             };
    51 
    52             let self = this;
    53 
    54             self.dateFields.datepicker({
    55                 dateFormat: "yy-mm-dd"
     25      init: function () {
     26        // this.levels_dropdown = $('select#e20r-pmpro-memberslist-levels');
     27        // this.enddate_lnk = $('a.e20r-members-list_enddate');
     28        // this.cancelMemberLnk = $('a.e20r-cancel-member');
     29        // this.updateBtn = $('a.e20r-members-list-save');
     30        this.memberslist_form = $('.e20r-pmpro-memberslist-page form#posts-filter');
     31        this.edit_lnk = $('a.e20r-members-list-editable');
     32        this.resetBtn = $('a.e20r-members-list-cancel');
     33        this.updateMemberLnk = $('a.e20r-update-member');
     34        this.exportBtn = $('a.e20r-memberslist-export');
     35        this.changed_input = $('input[class^="e20r-members-list-input-"]');
     36        this.changed_select = $( 'select[class^="e20r-members-list-select-"]');
     37        this.levels_dropdown = $('#e20r-pmpro-memberslist-levels');
     38        this.bulkUpdate = $('#doaction, #doaction2');
     39        this.updateListBtn = $('#e20r-update-list');
     40        this.search_field = $('#post-search-input');
     41        this.dateFields = $('.e20r-members-list-input-enddate, .e20r-members-list-input-startdate');
     42        this.dataSearchBtn = $('#e20r-memberslist-search-data');
     43        this.bulkActionSelectTop = $('select#bulk-action-selector-top');
     44        this.bulkActionSelectBottom = $('select#bulk-action-selector-bottom');
     45
     46        this.dateOpts = {
     47            year: '2-digit', month: 'short',
     48            day: 'numeric'
     49        };
     50
     51        let self = this;
     52
     53        self.dateFields.datepicker({
     54            dateFormat: "yy-mm-dd"
     55        });
     56
     57        self.search_field.unbind('keypress').on('keypress', function(event){
     58            let keycode = (event.key ? event.key : event.keyCode);
     59            if ('Enter' !== keycode) {
     60              return;
     61            }
     62            self.dataSearchBtn.click();
     63        });
     64
     65        // self.changed_input.unbind('blur').on('blur', function(ev) {
     66        self.changed_input.unbind('blur').on('blur', function() {
     67            self.set_update( this );
     68        });
     69
     70      // self.updateListBtn.unbind('click').on('click', function(ev) {
     71        self.updateListBtn.unbind('click').on('click', function(ev) {
     72          $('#post-search-input').val(null);
     73
     74          if ( 'Clear Search' === self.updateListBtn.val() ) {
     75              ev.preventDefault();
     76              window.console.log("We're clearing the search...");
     77              window.console.log(e20rml.url); //jshint ignore:line
     78              window.location = e20rml.url; //jshint ignore:line
     79          }
     80        });
     81
     82        // Trigger search whenever the levels drop-down is changed
     83        self.levels_dropdown.unbind('change').on('change', function() {
     84            self.dataSearchBtn.click();
     85        });
     86
     87        /**
     88         * Search operation initiated (a few other things simulate clicking the button)
     89         */
     90        self.dataSearchBtn.unbind('click').on('click', function(event) {
     91          let $search_string = $( '#post-search-input' ).val();
     92          let $level_string  = $( '#e20r-pmpro-memberslist-levels' ).val();
     93          let $uri             = window.location.toString();
     94
     95          // If we have a string in the search box we'll append it to the URL
     96          if ($search_string) {
     97            event.preventDefault();
     98
     99            // URL Encode the search string and add or replace it for the URI
     100            $uri = self.add_to_uri( $uri, 'find', encodeURIComponent( $search_string ) );
     101            $uri = self.add_to_uri( $uri, 'level', encodeURIComponent( $level_string ) );
     102            window.console.log( 'New URI should be: ' + $uri );
     103
     104            // Now we trigger the search
     105            window.location = $uri;
     106
     107            // Clear the search field - Possible FIXME if user's do not want clearing the search field to happen
     108            // $( '#post-search-input' ).val( null );
     109          }
     110        });
     111
     112        self.changed_select.unbind('change').on('change', function() {
     113          let current_select = $(this);
     114          let current_select_info = current_select.val();
     115          let $field_name = current_select.closest('div.ml-row-settings').find('.e20r-members-list-field-name').val();
     116          let previous_select_info = current_select.closest('div.ml-row-settings').find('input.e20r-members-list-db_' + $field_name ).val();
     117          let enddate = $('#the-list').find('.last .e20r-members-list-db-enddate').val();
     118
     119          window.console.log("Previous value: " + previous_select_info );
     120          window.console.log("New value: " + current_select_info );
     121
     122          if ( previous_select_info !== current_select_info && '' !== enddate ) {
     123              if ( ! window.confirm(e20rml.lang.clearing_enddate) ) { //jshint ignore:line
     124                  return false;
     125              }
     126          }
     127
     128          self.set_update( this );
     129        });
     130
     131        self.bulkUpdate.unbind('click').on('click', function (ev) {
     132
     133            ev.preventDefault();
     134
     135            if ('bulk-export' === self.bulkActionSelectTop.val() || 'bulk-export' === self.bulkActionSelectBottom.val()) {
     136                self.export_data(self);
     137                return true;
     138            }
     139
     140            self.memberslist_form.submit();
     141        });
     142
     143        self.updateMemberLnk.unbind('click').on('click', function (ev) {
     144
     145            ev.preventDefault();
     146            let btn = $(this);
     147            let row = btn.closest('tr');
     148
     149            let membership_col = row.find('td.column-membership');
     150            let membership_settings = membership_col.find('div.ml-row-settings');
     151            let membership_label = membership_col.find('a.e20r-members-list-editable');
     152
     153            let startdate_col = row.find('td.column-startdate');
     154            let startdate_settings = startdate_col.find('div.ml-row-settings');
     155            let startdate_label = startdate_col.find('a.e20r-members-list-editable');
     156
     157            let enddate_col = row.find('td.column-last');
     158            let enddate_settings = enddate_col.find('div.ml-row-settings');
     159            let enddate_label = enddate_col.find('a.e20r-members-list-editable');
     160
     161            let status_col = row.find( 'td.column-status');
     162            let status_settings = status_col.find('div.ml-row-settings');
     163            let status_label = status_col.find('a.e20r-members-list-editable');
     164
     165            membership_label.toggle();
     166            membership_settings.toggle();
     167
     168            status_label.toggle();
     169            status_settings.toggle();
     170
     171            $('.column-joindate').each(function () {
     172                $(this).toggle();
    56173            });
    57174
    58             self.search_field.unbind('keypress').on('keypress', function(event){
    59                 let keycode = (event.key ? event.key : event.keyCode);
    60                 if ('Enter' !== keycode) {
    61                     return;
    62                 }
    63 
    64                 self.dataSearchBtn.click();
    65             });
    66 
    67             // self.changed_input.unbind('blur').on('blur', function(ev) {
    68             self.changed_input.unbind('blur').on('blur', function() {
    69                 self.set_update( this );
    70             });
    71 
    72             // self.updateListBtn.unbind('click').on('click', function(ev) {
    73             self.updateListBtn.unbind('click').on('click', function(ev) {
    74                 $('#post-search-input').val(null);
    75 
    76                 if ( 'Clear Search' === self.updateListBtn.val() ) {
    77                     ev.preventDefault();
    78                     window.console.log("We're clearing the search...");
    79                     window.console.log(e20rml.url); //jshint ignore:line
    80                     window.location = e20rml.url; //jshint ignore:line
    81                 }
    82             });
    83 
    84             // Trigger search whenever the levels drop-down is changed
    85             self.levels_dropdown.unbind('change').on('change', function() {
    86                 self.dataSearchBtn.click();
    87             });
    88 
    89             /**
    90              * Search operation initiated (a few other things simulate clicking the button)
    91              */
    92 
    93             self.dataSearchBtn.unbind('click').on('click', function(event) {
    94 
    95                 let $search_string = $( '#post-search-input' ).val();
    96                 let $level_string  = $( '#e20r-pmpro-memberslist-levels' ).val();
    97                 let $uri           = window.location.toString();
    98 
    99                 // If we have a string in the search box we'll append it to the URL
    100                 if ($search_string) {
    101                     event.preventDefault();
    102 
    103                     // URL Encode the search string and add or replace it for the URI
    104                     $uri = self.add_to_uri( $uri, 'find', encodeURIComponent( $search_string ) );
    105                     $uri = self.add_to_uri( $uri, 'level', encodeURIComponent( $level_string ) );
    106                     window.console.log( 'New URI should be: ' + $uri );
    107 
    108                     // Now we trigger the search
    109                     window.location = $uri;
    110 
    111                     // Clear the search field - Possible FIXME if user's do not want clearing the search field to happen
    112                     // $( '#post-search-input' ).val( null );
    113                 }
    114             });
    115 
    116             self.changed_select.unbind('change').on('change', function() {
    117 
    118                 let current_select = $(this);
    119                 let current_select_info = current_select.val();
    120                 let $field_name = current_select.closest('div.ml-row-settings').find('.e20r-members-list-field-name').val();
    121                 let previous_select_info = current_select.closest('div.ml-row-settings').find('input.e20r-members-list-db_' + $field_name ).val();
    122                 let enddate = $('#the-list').find('.last .e20r-members-list-db-enddate').val();
    123 
    124                 window.console.log("Previous value: " + previous_select_info );
    125                 window.console.log("New value: " + current_select_info );
    126 
    127                 if ( previous_select_info !== current_select_info && '' !== enddate ) {
    128                     if ( ! window.confirm(e20rml.lang.clearing_enddate) ) { //jshint ignore:line
    129                         return false;
    130                     }
    131                 }
    132 
    133                 self.set_update( this );
    134             });
    135 
    136             self.bulkUpdate.unbind('click').on('click', function (ev) {
    137 
    138                 ev.preventDefault();
    139 
    140                 if ('bulk-export' === self.bulkActionSelectTop.val() || 'bulk-export' === self.bulkActionSelectBottom.val()) {
    141                     self.export_data(self);
    142                     return true;
    143                 }
    144 
    145                 self.memberslist_form.submit();
    146             });
    147 
    148             self.updateMemberLnk.unbind('click').on('click', function (ev) {
    149 
    150                 ev.preventDefault();
    151                 let btn = $(this);
    152                 let row = btn.closest('tr');
    153 
    154                 let membership_col = row.find('td.column-membership');
    155                 let membership_settings = membership_col.find('div.ml-row-settings');
    156                 let membership_label = membership_col.find('a.e20r-members-list-editable');
    157 
    158                 let startdate_col = row.find('td.column-startdate');
    159                 let startdate_settings = startdate_col.find('div.ml-row-settings');
    160                 let startdate_label = startdate_col.find('a.e20r-members-list-editable');
    161 
    162                 let enddate_col = row.find('td.column-last');
    163                 let enddate_settings = enddate_col.find('div.ml-row-settings');
    164                 let enddate_label = enddate_col.find('a.e20r-members-list-editable');
    165 
    166                 let status_col = row.find( 'td.column-status');
    167                 let status_settings = status_col.find('div.ml-row-settings');
    168                 let status_label = status_col.find('a.e20r-members-list-editable');
    169 
    170                 membership_label.toggle();
    171                 membership_settings.toggle();
    172 
    173                 status_label.toggle();
    174                 status_settings.toggle();
    175 
    176                 $('.column-joindate').each(function () {
    177                     $(this).toggle();
    178                 });
    179 
    180                 startdate_label.toggle();
    181                 startdate_settings.toggle();
    182 
    183                 enddate_label.toggle();
    184                 enddate_settings.toggle();
    185             });
    186 
    187             // Process edit link
    188             self.edit_lnk.unbind('click').on('click', function ($event) {
    189 
    190                 $event.preventDefault();
    191 
    192                 let $edit = $(this);
    193                 let $input = $edit.next('div.ml-row-settings');
    194 
    195                 $input.toggle();
    196                 $edit.toggle();
    197 
    198             });
    199 
    200             /**
    201              * Cancel membership link handler
    202              */
    203             self.resetBtn.unbind('click').on('click', function (ev) {
    204 
    205                 ev.preventDefault();
    206 
    207                 let $btn = $(this);
    208                 let $settings = $btn.closest('div.ml-row-settings');
    209                 let $label = $settings.prev('a.e20r-members-list-editable');
    210                 let $field_name = $settings.find('.e20r-members-list-field-name').val();
    211 
    212                 // Return the select value (or input value) to its original value...
    213                 let $original = $settings.find('.e20r-members-list-db-' + $field_name ).val();
    214                 let $field_input_key = 'e20r-members-list-new_' + $field_name;
    215 
    216                 $settings.find('select[name^="' + $field_input_key + '"], input[name^="'+ $field_input_key + '"]').val( $original );
    217 
    218                 $settings.toggle();
    219                 $label.toggle();
    220             });
    221 
    222             self.exportBtn.unbind('click').on('click', function (ev) {
    223                 ev.preventDefault();
    224                 window.console.log("Export button clicked!");
    225 
    226                 self.export_data( self );
    227             });
    228         },
    229         add_to_uri: function(url, paramName, paramValue) {
    230 
    231             let pattern = new RegExp('\\b('+paramName+'=).*?(&|#|$)');
    232 
    233             if (paramValue === null) {
    234                 paramValue = '';
    235             }
    236 
    237             if (url.search(pattern)>=0) {
    238                 return url.replace(pattern,'$1' + paramValue + '$2');
    239             }
    240 
    241             url = url.replace(/[?#]$/,'');
    242             return url + (url.indexOf('?')>0 ? '&' : '?') + paramName + '=' + paramValue;
    243         },
    244         export_data: function( self ) {
    245 
    246             $("#overlay").fadeIn(300);
    247             let inputs = $('.e20r-search-arguments input, .e20r-search-arguments textarea, .e20r-search-arguments select')
    248                 .not(':input[type=button], :input[type=submit], input[type=reset]');
    249             let export_args = self.prepare_export(self, inputs);
    250 
    251             // Open a new tab to trigger the AJAX request for the download
    252             let file_name = window.prompt( 'Filename for data to save/export', 'members_list' );
    253             let extension = '.csv';
    254             if ( file_name.toLowerCase().endsWith( '.csv' ) ) {
    255                 extension = '';
    256             }
    257             self.download_csv(file_name + extension, export_args, ); //jshint ignore:line
    258         },
    259         /**
    260          * Set the request variables based on the members list page
    261          *
    262          * @param self Instance of this object
    263          * @param inputs The HTML inputs
    264          * @returns Object
    265          */
    266         prepare_export: function( self, inputs ) {
    267             let request_args = {
    268                 '_wpnonce':  $('#_wpnonce').val(),
    269                 'showDebugTrace': true,
    270                 'action': 'e20rml_export_records',
    271             };
    272             request_args.find = $( '#post-search-input' ).val();
    273             inputs.each(function () {
    274                 let input = $(this);
    275                 let name = input.attr('name');
    276                 let value = input.val();
    277                 if (false === self.is_empty(value)) {
    278                     window.console.log(name + " contains " + value);
    279                     request_args[name] = value;
    280                 }
    281             });
    282 
    283             let is_checked = false;
    284             let selected_ids = [];
    285             $('input[name^="member_user_id"]').each(function () {
    286                 if ($(this).is(':checked')) {
    287                     is_checked = true;
    288                     selected_ids.push($(this).val());
    289                 }
    290             });
    291             if (selected_ids.length > 0) {
    292                 request_args.member_id = selected_ids;
    293             }
    294             // return self.build_export_url( export_args );
    295             return request_args;
    296         },
    297         /**
    298          * Download the exported .CSV data
    299          * @param filename The file name to use for download
    300          * @param download_args The POST argument(s)
    301          */
    302         download_export: function(filename, download_args) {
    303             fetch(e20rml.ajax_url, download_args) // jshint ignore:line
    304                 .then(response => {
    305                     if (!response.ok) {
    306                         throw new Error('Request failed.' + response);
    307                     }
    308                 })
    309                 .then(response => response.blob())
    310                 .then(blob => {
    311                     const link = window.document.createElement("a");
    312                     link.href = URL.createObjectURL(blob);
    313                     link.download = filename;
    314                     link.click();
    315                 })
    316                 .catch(window.console.error);
    317         },
    318         download_csv: function(file_name, attributes) {
    319             let URL = e20rml.ajax_url; // jshint ignore:line
    320             window.console.log( 'AJAX URL: ' + URL);
    321             window.console.log( attributes );
    322             $.ajax({
    323                 url: URL,
    324                 cache: false,
    325                 method: 'POST',
    326                 data: attributes,
    327                 accepts: 'text/csv',
    328                 xhr: function () {
    329                     var xhr = new XMLHttpRequest();
    330                     xhr.onreadystatechange = function () {
    331                         if (xhr.readyState === 2) {
    332                             if (xhr.status === 200) {
    333                                 xhr.responseType = "blob";
    334                             } else {
    335                                 xhr.responseType = "text";
    336                             }
    337                         }
    338                     };
    339                     return xhr;
    340                 },
    341                 success: function (data) {
    342                     //Convert the Byte Data to BLOB object.
    343                     let blob = new Blob([data], { type: "text/csv" });
    344 
    345                     //Check the Browser type and download the File.
    346                     let isIE = !!document.documentMode;
    347                     if (isIE) {
    348                         window.navigator.msSaveBlob(blob, file_name);
    349                     } else {
    350                         let url = window.URL || window.webkitURL;
    351                         let link = url.createObjectURL(blob);
    352                         let body = $('body');
    353                         let a = $("<a />");
    354                         a.attr("download", file_name);
    355                         a.attr("href", link);
    356                         body.append(a);
    357                         a[0].click();
    358                         body.remove(a);
    359                     }
    360                 },
    361                 error: function (jqXHR, textStatus, errorThrown) {
    362                     window.console.log( 'Status: ' + textStatus + ', Error thrown: ' + errorThrown );
    363                 }
    364             }).done(function() {
    365                 setTimeout(function(){
    366                     $("#overlay").fadeOut(300);
    367                 },500);
    368             });
    369         },
    370         set_update: function( $element ) {
    371             let self = this;
    372 
    373             let element = $($element);
    374             let $settings = element.closest('div.ml-row-settings');
    375             // let users_id = $settings.find('input.e20r-members-list-user-id').val();
    376             let field_name = $settings.find('input.e20r-members-list-field-name').val();
    377             let $label = $settings.prev('a.e20r-members-list-' + field_name + '-label');
    378             // let $new_value_field = $settings.find('input.e20r-members-list-db-' + field_name);
    379             let select = $settings.find('.e20r-members-list-select-' + field_name);
    380 
    381             let $checkbox = element.closest('tr').find('th.check-column input[type="checkbox"]');
    382 
    383             let $date = $settings.find('.e20r-members-list-input-' + field_name).val();
    384             let select_val = select.val();
    385             let test_checked = false;
    386 
    387             window.console.log("Value: ", $date, select_val);
    388 
    389             if ('undefined' !== select_val) {
    390 
    391                 $label.text(select.find('option:selected').text());
    392                 test_checked = true;
    393             }
    394 
    395             if (null !== $date) {
    396 
    397                 let date = new Date($date);
    398 
    399                 window.console.log("Date info? ", date);
    400 
    401                 if (Object.prototype.toString.call(date) === "[object Date]") {
    402                     $label.text(date.toLocaleDateString(e20rml.locale, self.dateOpts)); //jshint ignore:line
    403                 }
    404 
    405                 test_checked = true;
    406             }
    407 
    408             if (true === test_checked && false === $checkbox.is('checked')) {
    409                 window.console.log("Checkbox not checked. Fixing!");
    410                 $checkbox.prop('checked', true);
    411 
    412                 self.bulkActionSelectBottom.val('bulk-update');
    413                 self.bulkActionSelectTop.val('bulk-update');
    414                 $('#doaction').val(e20rml.lang.save_btn_text); //jshint ignore:line
    415                 $('#doaction2').val(e20rml.lang.save_btn_text); //jshint ignore:line
    416             }
    417             /*
     175            startdate_label.toggle();
     176            startdate_settings.toggle();
     177
     178            enddate_label.toggle();
     179            enddate_settings.toggle();
     180        });
     181
     182        // Process edit link
     183        self.edit_lnk.unbind('click').on('click', function ($event) {
     184
     185            $event.preventDefault();
     186
     187            let $edit = $(this);
     188            let $input = $edit.next('div.ml-row-settings');
     189
     190            $input.toggle();
     191            $edit.toggle();
     192
     193        });
     194
     195        /**
     196         * Cancel membership link handler
     197         */
     198        self.resetBtn.unbind('click').on('click', function (ev) {
     199
     200            ev.preventDefault();
     201
     202            let $btn = $(this);
     203            let $settings = $btn.closest('div.ml-row-settings');
     204            let $label = $settings.prev('a.e20r-members-list-editable');
     205            let $field_name = $settings.find('.e20r-members-list-field-name').val();
     206
     207            // Return the select value (or input value) to its original value...
     208            let $original = $settings.find('.e20r-members-list-db-' + $field_name ).val();
     209            let $field_input_key = 'e20r-members-list-new_' + $field_name;
     210
     211            $settings.find('select[name^="' + $field_input_key + '"], input[name^="'+ $field_input_key + '"]').val( $original );
     212
    418213            $settings.toggle();
    419214            $label.toggle();
    420             */
    421         },
    422         /**
    423          * Build the URL used to export the data
    424          *
    425          * @param export_attrs The attributes to use for the AJAX URL (export)
    426          */
    427         build_export_url: function (export_attrs) {
    428             window.console.log("About to transmit: ", export_attrs);
    429             let export_arguments = '';
    430             Object.keys(export_attrs).forEach(function(key){
    431                 let value = export_attrs[key];
    432                 if ( value instanceof Array) {
    433                     value = value.join(',');
    434                 }
    435                 export_arguments = export_arguments + key + '=' + encodeURI( value ) + '&';
    436             });
    437 
    438             let location = e20rml.ajax_url + '?' + export_arguments; // jshint ignore:line
    439             window.console.log( location );
    440             return location;
    441         },
    442         is_empty: function (data) {
    443 
    444             if (typeof(data) === 'number' || typeof(data) === 'boolean') {
    445                 return false;
     215        });
     216
     217        self.exportBtn.unbind('click').on('click', function (ev) {
     218            ev.preventDefault();
     219            window.console.log("Export button clicked!");
     220
     221            self.export_data( self );
     222        });
     223      },
     224          add_to_uri: function(url, paramName, paramValue) {
     225
     226        let pattern = new RegExp('\\b('+paramName+'=).*?(&|#|$)');
     227
     228        if (paramValue === null) {
     229          paramValue = '';
     230        }
     231
     232        if (url.search(pattern)>=0) {
     233          return url.replace(pattern,'$1' + paramValue + '$2');
     234        }
     235
     236        url = url.replace(/[?#]$/,'');
     237        return url + (url.indexOf('?')>0 ? '&' : '?') + paramName + '=' + paramValue;
     238          },
     239          export_data: function( self ) {
     240
     241        $("#overlay").fadeIn(300);
     242        let inputs = $('.e20r-search-arguments input, .e20r-search-arguments textarea, .e20r-search-arguments select')
     243          .not(':input[type=button], :input[type=submit], input[type=reset]');
     244        let export_args = self.prepare_export(self, inputs);
     245
     246        // Open a new tab to trigger the AJAX request for the download
     247        let file_name = window.prompt( 'Filename for data to save/export', 'members_list' );
     248        let extension = '.csv';
     249        if ( file_name.toLowerCase().endsWith( '.csv' ) ) {
     250          extension = '';
     251        }
     252        self.download_csv(file_name + extension, export_args, ); //jshint ignore:line
     253          },
     254      /**
     255       * Set the request variables based on the members list page
     256       *
     257       * @param self Instance of this object
     258       * @param inputs The HTML inputs
     259       * @returns Object
     260       */
     261      prepare_export: function( self, inputs ) {
     262        let request_args = {
     263          '_wpnonce':  $('#_wpnonce').val(),
     264          'showDebugTrace': true,
     265          'action': 'e20rml_export_records',
     266        };
     267        request_args.find = $( '#post-search-input' ).val();
     268        inputs.each(function () {
     269          let input = $(this);
     270          let name = input.attr('name');
     271          let value = input.val();
     272          if (false === self.is_empty(value)) {
     273            window.console.log(name + " contains " + value);
     274            request_args[name] = value;
     275          }
     276        });
     277
     278        let is_checked = false;
     279        let selected_ids = [];
     280        $('input[name^="member_user_id"]').each(function () {
     281          if ($(this).is(':checked')) {
     282            is_checked = true;
     283            selected_ids.push($(this).val());
     284          }
     285        });
     286        if (selected_ids.length > 0) {
     287          request_args.member_user_ids = selected_ids;
     288        }
     289        // return self.build_export_url( export_args );
     290        return request_args;
     291      },
     292      /**
     293       * Download the exported .CSV data
     294       *
     295       * @param file_name The file name to use for download
     296       * @param attributes The POST argument(s)
     297       */
     298      download_csv: function(file_name, attributes) {
     299        let URL = e20rml.ajax_url; // jshint ignore:line
     300        $.ajax({
     301          url: URL,
     302          cache: false,
     303          method: 'POST',
     304          data: attributes,
     305          accepts: 'text/csv',
     306          xhr: function () {
     307            var xhr = new XMLHttpRequest();
     308            xhr.onreadystatechange = function () {
     309              if (xhr.readyState === 2) {
     310                if (xhr.status === 200) {
     311                  xhr.responseType = "blob";
     312                } else {
     313                  xhr.responseType = "text";
     314                }
     315              }
     316            };
     317            return xhr;
     318          },
     319          success: function (data) {
     320            //Convert the Byte Data to BLOB object.
     321            let blob = new Blob([data], { type: "text/csv" });
     322
     323            //Check the Browser type and download the File.
     324            let isIE = !!document.documentMode;
     325            if (isIE) {
     326              window.navigator.msSaveBlob(blob, file_name);
     327            } else {
     328              let url = window.URL || window.webkitURL;
     329              let link = url.createObjectURL(blob);
     330              let body = $('body');
     331              let a = $("<a />");
     332              a.attr("download", file_name);
     333              a.attr("href", link);
     334              body.append(a);
     335              a[0].click();
     336              body.remove(a);
    446337            }
    447 
    448             if (typeof(data) === 'undefined' || data === null) {
    449                 return true;
    450             }
    451 
    452             if (typeof(data.length) !== 'undefined') {
    453                 return data.length === 0;
    454             }
    455 
    456             let count = 0;
    457 
    458             for (let i in data) {
    459 
    460                 if (data.hasOwnProperty(i)) {
    461                     count++;
    462                 }
    463             }
    464 
    465             return count === 0;
    466         }
     338          },
     339          error: function (jqXHR, textStatus, errorThrown) {
     340            window.console.log( 'Status: ' + textStatus + ', Error thrown: ' + errorThrown );
     341          }
     342        }).done(function() {
     343          setTimeout(function(){
     344            $("#overlay").fadeOut(300);
     345          },500);
     346        });
     347      },
     348      set_update: function( $element ) {
     349        let self = this;
     350        let element = $($element);
     351        let $settings = element.closest('div.ml-row-settings');
     352        // let users_id = $settings.find('input.e20r-members-list-user-id').val();
     353        let field_name = $settings.find('input.e20r-members-list-field-name').val();
     354        let $label = $settings.prev('a.e20r-members-list-' + field_name + '-label');
     355        // let $new_value_field = $settings.find('input.e20r-members-list-db-' + field_name);
     356        let select = $settings.find('.e20r-members-list-select-' + field_name);
     357        let $checkbox = element.closest('tr').find('th.check-column input[type="checkbox"]');
     358        let $date = $settings.find('.e20r-members-list-input-' + field_name).val();
     359        let select_val = select.val();
     360        let test_checked = false;
     361
     362        window.console.log("Value: ", $date, select_val);
     363
     364        if ('undefined' !== select_val) {
     365          $label.text(select.find('option:selected').text());
     366          test_checked = true;
     367        }
     368
     369        if (null !== $date) {
     370          let date = new Date($date);
     371          window.console.log("Date info? ", date);
     372          if (Object.prototype.toString.call(date) === "[object Date]") {
     373              $label.text(date.toLocaleDateString(e20rml.locale, self.dateOpts)); //jshint ignore:line
     374          }
     375          test_checked = true;
     376        }
     377
     378        if (true === test_checked && false === $checkbox.is('checked')) {
     379          window.console.log("Checkbox not checked. Fixing!");
     380          $checkbox.prop('checked', true);
     381
     382          self.bulkActionSelectBottom.val('bulk-update');
     383          self.bulkActionSelectTop.val('bulk-update');
     384          $('#doaction').val(e20rml.lang.save_btn_text); //jshint ignore:line
     385          $('#doaction2').val(e20rml.lang.save_btn_text); //jshint ignore:line
     386        }
     387        /*
     388        $settings.toggle();
     389        $label.toggle();
     390        */
     391      },
     392      /**
     393       * Build the URL used to export the data
     394       *
     395       * @param export_attrs The attributes to use for the AJAX URL (export)
     396       */
     397      build_export_url: function (export_attrs) {
     398              window.console.log("About to transmit: ", export_attrs);
     399        let export_arguments = '';
     400        Object.keys(export_attrs).forEach(function(key){
     401          let value = export_attrs[key];
     402          if ( value instanceof Array) {
     403            value = value.join(',');
     404          }
     405          export_arguments = export_arguments + key + '=' + encodeURI( value ) + '&';
     406        });
     407
     408        let location = e20rml.ajax_url + '?' + export_arguments; // jshint ignore:line
     409        window.console.log( location );
     410        return location;
     411          },
     412      is_empty: function (data) {
     413
     414          if (typeof(data) === 'number' || typeof(data) === 'boolean') {
     415              return false;
     416          }
     417
     418          if (typeof(data) === 'undefined' || data === null) {
     419              return true;
     420          }
     421
     422          if (typeof(data.length) !== 'undefined') {
     423              return data.length === 0;
     424          }
     425
     426          let count = 0;
     427
     428          for (let i in data) {
     429
     430              if (data.hasOwnProperty(i)) {
     431                  count++;
     432              }
     433          }
     434
     435          return count === 0;
     436      }
    467437    };
    468438
     
    470440        e20rMembersList_Page.init();
    471441    });
    472 
    473442})(jQuery);
  • e20r-members-list/tags/8.6/src/E20R/members-list/modules/Multiple_Memberships.php

    r2672951 r2700272  
    2222namespace E20R\Members_List\Admin\Modules;
    2323
     24use E20R\Members_List\Members_List;
     25
    2426if ( ! defined( 'ABSPATH' ) && ! defined( 'PLUGIN_PHPUNIT' ) ) {
    2527    die( 'WordPress not loaded. Naughty, naughty!' );
    2628}
    2729
    28 if ( ! class_exists( '\\E20R\\Members_List\\Admin\\Modules\\Multiple_Memberships' ) ) {
     30if ( ! class_exists( 'E20R\Members_List\Admin\Modules\Multiple_Memberships' ) ) {
    2931    /**
    3032     * Support for PHP's Multiple Memberships plugin
     
    6062         * @return string
    6163         */
    62         public function column_name( $item ) {
     64        public function multiple_membership_column( $item ) {
    6365
    64             // FIXME: Update this to support MMPU
    6566            // FIXME: Allow adding more levels and changing the "primary" level.
     67            if ( ! function_exists( 'pmpro_getMembershipLevelsForUser' ) ) {
     68                return '';
     69            }
     70
     71            $level_list    = pmpro_getMembershipLevelsForUser( $item['ID'] );
     72            $current_level = isset( $item['membership_id'] ) ? (int) $item['membership_id'] : null;
     73            $level_ids     = array();
     74            $level_names   = array();
     75
     76            foreach ( $level_list as $level ) {
     77                $level_ids[] = $level->id;
     78
     79                // Generate HTML for the membership levels the user is assigned to
     80                $level_names[] = ( (int) $level->id === $current_level ?
     81                    sprintf(
     82                        '<span class="e20r-members-list-level-name e20r-members-list-primary-level">%1$s (%2$s)</span>',
     83                        esc_attr( $level->name ),
     84                        esc_attr__( 'primary', 'e20r-members-list' )
     85                    ) :
     86                    sprintf( '<span class="e20r-members-list-level-name">%1$s</span>', $level->name )
     87                );
     88            }
    6689
    6790            // These are used to configure the membership level with JavaScript.
     
    7598                <input type="hidden" value="%5$d" class="e20r-members-list-db_record_id" name="e20r-members-list-db_record_id_%2$s" />
    7699                <input type="hidden" value="%4$s" class="e20r-members-list-field-name" name="e20r-members-list-field_name_%2$s" />',
    77                 $item['membership_id'],
    78                 $item['ID'],
    79                 $item['name'],
     100                (int) $item['membership_id'],
     101                (int) $item['ID'],
     102                esc_attr( $item['name'] ),
    80103                'membership_id',
    81                 $item['record_id'],
    82                 $item['membership_level_ids']
     104                (int) $item['record_id'],
     105                empty( $item['membership_level_ids'] ) ?
     106                    implode( ',', $level_ids ) :
     107                    esc_attr( $item['membership_level_ids'] )
    83108            );
    84109
    85             $options = '';
    86             if ( function_exists( 'pmpro_getAllLevels' ) ) {
    87                 $levels = pmpro_getAllLevels( true, true );
    88             } else {
     110            $options = Members_List::build_option_string( $item['membership_id'] );
    89111
    90                 // Default info if PMPro is disabled.
    91                 $null_level           = new \stdClass();
    92                 $null_level->level_id = 0;
    93                 $null_level->name     = esc_attr__( 'No levels found. Paid Memberships Pro is inactive!', 'e20r-members-list' );
    94                 $levels               = array( $null_level );
    95             }
    96 
    97             foreach ( $levels as $level ) {
    98                 $options .= sprintf(
    99                     '<option value="%1$s" %2$s>%3$s</option>',
    100                     $level->id,
    101                     selected( $level->id, $item['membership_id'], false ),
    102                     $level->name
    103                 ) . "\n";
    104             }
    105             $new_membershiplevel_input = sprintf(
     112            $new_level_input = sprintf(
    106113                '<div class="ml-row-settings clearfix">
    107114                        %1$s
     
    113120                    </div>',
    114121                $membership_input,
    115                 $item['ID'],
     122                (int) $item['ID'],
    116123                $options,
    117124                esc_attr__( 'Reset', 'e20r-members-list' )
    118125            );
    119126
    120             $value = sprintf(
     127            $level_info = sprintf(
     128                '<div class="e20r-members-list-level-names">%1$s</div>',
     129                implode( '<br class="e20r-members-list-level-name-spacer" />', $level_names )
     130            );
     131            return sprintf(
    121132                '<a href="#" class="e20r-members-list_membership_id e20r-members-list-editable" title="%1$s">%2$s<span class="dashicons dashicons-edit"></a>%3$s',
    122                 esc_attr__( 'Click to edit membership level', 'e20rapp' ),
    123                 $item['name'],
    124                 $new_membershiplevel_input
     133                esc_attr__( 'Click to edit primary membership level', 'e20r-members-list' ),
     134                $new_level_input,
     135                $level_info
    125136            );
    126 
    127             return '';
    128137        }
    129138    }
  • e20r-members-list/trunk/CHANGELOG.md

    r2672951 r2700272  
    66
    77## [Unreleased]
     8
     9## v8.6 - 2022-03-27
     10- BUG FIX: Revert codecov upload step (Eighty/20Results Bot on Github)
     11- BUG FIX: Set permissions for coverage output directory (Eighty/20Results Bot on Github)
     12- BUG FIX: More debug output (Eighty/20Results Bot on Github)
     13- BUG FIX: Typo in Makefile (Eighty/20Results Bot on Github)
     14- BUG FIX: Adding more debug text to makefile (Eighty/20Results Bot on Github)
     15- BUG FIX: Adding debug text to makefile (Eighty/20Results Bot on Github)
     16- BUG FIX: Adding PHP 8.1 to test matrix (Eighty/20Results Bot on Github)
     17- BUG FIX: Integration tests weren't doing much (Eighty/20Results Bot on Github)
     18- BUG FIX: Add PHP 8.1 to test matrix and revert code coverage upload (Eighty/20Results Bot on Github)
     19- BUG FIX: Reverted codecov upload step and adding php 8.1 to test matrix (Eighty/20Results Bot on Github)
     20- BUG FIX: Be explicit about the file to load in action (Eighty/20Results Bot on Github)
     21- BUG FIX: Different config for codecov-action (Eighty/20Results Bot on Github)
     22- BUG FIX: Try to fix setup for codecov/codecov-action (Eighty/20Results Bot on Github)
     23- BUG FIX: Update codecov configuration and rename the config file (Eighty/20Results Bot on Github)
     24- BUG FIX: Revert removal of PMPro dependency for PHPStan tests (Eighty/20Results Bot on Github)
     25- BUG FIX: Attempting to enable code coverage uploads (Eighty/20Results Bot on Github)
     26- BUG FIX: Adding the codecov.yml config file (Eighty/20Results Bot on Github)
     27- BUG FIX: PHPDoc entry for failed class variable was wrong (Eighty/20Results Bot on Github)
     28- BUG FIX: More unit test coverage for the bulk operations including fix for members_to_process check (Eighty/20Results Bot on Github)
     29- BUG FIX: Checksum for composer wasn't a make variable (Eighty/20Results Bot on Github)
     30- BUG FIX: Exception for when the array of users/levels to perform bulk operations is incorrect (Eighty/20Results Bot on Github)
     31- BUG FIX: Make coverage a local only thing (for now?) (Eighty/20Results Bot on Github)
     32- BUG FIX: Don't create unneeded coverage subdirectories (Eighty/20Results Bot on Github)
     33- BUG FIX: Fix workflow config (Eighty/20Results Bot on Github)
     34- BUG FIX: Use remote coverage service (Eighty/20Results Bot on Github)
     35- BUG FIX: Problem with coverage artifacts (Eighty/20Results Bot on Github)
     36- BUG FIX: Bumped version number to 8.6 (Eighty/20Results Bot on Github)
     37- BUG FIX: Clean up message in get_members() -> InvalidSQL exception (Eighty/20Results Bot on Github)
     38- BUG FIX: Not re-running workflow when PR is updated/edited (Eighty/20Results Bot on Github)
     39- BUG FIX: Add support for Brain\faker() mocking for WP and the ext-mysqli extension for integration testing purposes (Eighty/20Results Bot on Github)
     40- BUG FIX: Transition to mocking PMPro functions for Integration tests (probably moving actual PMPro integration to acceptance testing) (Eighty/20Results Bot on Github)
     41- BUG FIX: Make sure Integration/Acceptance test container uses PHP v7.4 (Eighty/20Results Bot on Github)
     42- BUG FIX: Added PHP8/Paid Memberships Pro incompatibility as a 'Known Issue' (Eighty/20Results Bot on Github)
     43- BUG FIX: Updated tags for Wodby docker4wordpress (Eighty/20Results Bot on Github)
     44- BUG FIX: Wrong number of placeholders and data in sprintf() (Eighty/20Results Bot on Github)
     45- BUG FIX: Clean up Known Issues text (Eighty/20Results Bot on Github)
     46- BUG FIX: Refactored option HTML and changed to using gmdate() (Eighty/20Results Bot on Github)
     47- BUG FIX: Adding support for listing multiple memberships (still have TODOs, not releasing yet) (Eighty/20Results Bot on Github)
     48- BUG FIX: Added a known issue (Eighty/20Results Bot on Github)
     49- BUG FIX: Cleaned up namespace path (Eighty/20Results Bot on Github)
     50- BUG FIX: No longer using singleton pattern in E20R_Members_List() class and fixed PHPCS warnings (Eighty/20Results Bot on Github)
     51- BUG FIX: Null value disallowed for str_replace() (Eighty/20Results Bot on Github)
     52- BUG FIX: Bulk-exported all users, not just the selected ones and fixed PHP Deprecated messages for bulk action checks (Eighty/20Results Bot on Github)
     53- BUG FIX: Adding tests with data for column_last() method (Eighty/20Results Bot on Github)
     54- BUG FIX: Fix column_last() to avoid PHP Warning messages and refactor, make the definition of an 'empty' enddate filtered and add helper methods. Added exceptions and handlers (Eighty/20Results Bot on Github)
     55- BUG FIX: Add e20r_members_list_empty_date_values filter documentation (Eighty/20Results Bot on Github)
     56- BUG FIX: Cleanup patchwork.json (Eighty/20Results Bot on Github)
     57- BUG FIX: Variables rely on tests/_env/.env.testing (Eighty/20Results Bot on Github)
     58- BUG FIX: Run install for WP and set only DEBUG variable (Eighty/20Results Bot on Github)
     59- BUG FIX: Make sure we have the needed database tables for testing (Eighty/20Results Bot on Github)
     60- BUG FIX: Actual path to source files for e20r utilities module is src/E20R/... (Eighty/20Results Bot on Github)
     61- BUG FIX: PMPro doesn't check if constants are defined before use triggering test failures for us (Eighty/20Results Bot on Github)
     62- BUG FIX: Load fixtures to load/clear the DB (Eighty/20Results Bot on Github)
     63- BUG FIX: Lacked DB records for PMPro and User in integration tests (Eighty/20Results Bot on Github)
     64- BUG FIX: Wrong path to test specific _bootstrap.php file (Eighty/20Results Bot on Github)
     65- BUG FIX: Didn't load test specific _bootstrap.php files (Eighty/20Results Bot on Github)
     66- BUG FIX: Initial commit for column_last integration test (Eighty/20Results Bot on Github)
     67- BUG FIX: Add WP_DEBUG logging for test container(s) (Eighty/20Results Bot on Github)
     68- BUG FIX: Use E20R_PLUGIN_NAME env variable to set PROJECT_NAME variable (with default value set to plugin slug) (Eighty/20Results Bot on Github)
     69- BUG FIX: Upgraded wodby container environments (Eighty/20Results Bot on Github)
     70- BUG FIX: Fixed install-hooks for pre-commit git hook (Eighty/20Results Bot on Github)
     71- BUG FIX: Path to documentation (Eighty/20Results Bot on Github)
     72- BUG FIX: Make sure we strip away the test file (Eighty/20Results Bot on Github)
    873
    974## v8.5 - 2022-02-04
  • e20r-members-list/trunk/README.md

    r2672951 r2700272  
    33`Tags: paid memberships pro, members, memberships, pmpro enhancements, better members list, members list, addon` <br />
    44`Requires at least: 4.9` <br />
    5 `Tested up to: 5.9` <br />
     5`Tested up to: 5.9.2` <br />
    66`Requires PHP: 7.1` <br />
    7 `Stable tag: 8.5` <br />
     7`Stable tag: 8.6` <br />
    88`License: GPLv2` <br />
    99`License URI: http://www.gnu.org/licenses/gpl` <br />
     
    3636See [ACTIONS.md](https://github.com/eighty20results.com/e20r-members-list/blob/main/docs/ACTIONS.md)
    3737
     38
    3839### Known Issues
    39 No known issues at this time
     40PHP 8.0 and later introduces warning messages for certain behaviors that were ignored prior to v8.0. Because of this, and the fact that this plugin relies on functionality from Paid Memberships Pro, the "end date" column may print messages indicating problems with the `trim()` function. Until Paid Memberships Pro updates their plugin to support PHP8.x, these messages will need to be disabled in your web server configuration (suppressed).
     41
     42Setting the "Members per page" in the "Options" drop-down on the Members List page to a number greater than 50 can result in unexpected errors/warnings. The default value is 20. One symptom is seeing the PHP warning: "Warning: Unknown: Input variables exceeded 2000. To increase the limit change max_input_vars in php.ini. in Unknown on line 0"
    4043
    4144### Changelog
  • e20r-members-list/trunk/README.txt

    r2672951 r2700272  
    33Tags: paid memberships pro, members, memberships, pmpro enhancements, better members list, members list, addon
    44Requires at least: 4.9
    5 Tested up to: 5.9
     5Tested up to: 5.9.2
    66Requires PHP: 7.1
    7 Stable tag: 8.5
     7Stable tag: 8.6
    88License: GPLv2
    99License URI: http://www.gnu.org/licenses/gpl
     
    3838
    3939== Known Issues ==
    40 No known issues at this time
     40PHP 8.0 and later introduces warning messages for certain behaviors that were ignored prior to v8.0. Because of this, and the fact that this plugin relies on functionality from Paid Memberships Pro, the "end date" column may print messages indicating problems with the `trim()` function. Until Paid Memberships Pro updates their plugin to support PHP8.x, these messages will need to be disabled in your web server configuration (suppressed).
     41
     42Setting the "Members per page" in the "Options" drop-down on the Members List page to a number greater than 50 can result in unexpected errors/warnings. The default value is 20. One symptom is seeing the PHP warning: "Warning: Unknown: Input variables exceeded 2000. To increase the limit change max_input_vars in php.ini. in Unknown on line 0"
    4143
    4244== Changelog ==
    4345See the official [CHANGELOG.md](https://github.com/eighty20results.com/e20r-members-list/blob/main/CHANGELOG.md) file
    44 
  • e20r-members-list/trunk/class-e20r-members-list.php

    r2672951 r2700272  
    44Plugin URI: https://wordpress.org/plugins/e20r-members-list
    55Description: Extensible, sortable & bulk action capable members listing + export to CSV tool for Paid Memberships Pro.
    6 Version: 8.5
     6Version: 8.6
    77Author: Thomas Sjolshagen @ Eighty / 20 Results by Wicked Strong Chicks, LLC <[email protected]>
    88Author URI: https://eighty20results.com/thomas-sjolshagen/
     
    3939use E20R\Metrics\Exceptions\MissingDependencies;
    4040use E20R\Metrics\MixpanelConnector;
     41use E20R\Utilities\ActivateUtilitiesPlugin;
    4142use E20R\Utilities\Cache;
    4243use E20R\Utilities\Utilities;
     
    5253
    5354if ( ! defined( 'E20R_MEMBERSLIST_VER' ) ) {
    54     define( 'E20R_MEMBERSLIST_VER', '8.5' );
     55    define( 'E20R_MEMBERSLIST_VER', '8.6' );
    5556}
    5657
     
    6263
    6364        /**
    64          * Instance of the Member List controller
    65          *
    66          * @var null|E20R_Members_List
    67          */
    68         private static $instance = null;
    69 
    70         /**
    7165         * The E20R Utilities Module class instance
    7266         *
     
    9892         */
    9993        public function __construct( $ml_page = null, $utils = null, $mixpanel = null ) {
    100             self::$instance = $this;
    10194
    10295            if ( empty( $utils ) ) {
     
    139132         * @throws InvalidSettingsKey Raised when an invalid class property is specified for the get() method
    140133         */
    141         public function get( $property = 'instance' ) {
     134        public function get( $property = 'utils' ) {
    142135
    143136            if ( ! property_exists( $this, $property ) ) {
     
    152145            return $this->{$property};
    153146        }
    154         /**
    155          * Get or instantiate and get the current class
    156          *
    157          * @return E20R_Members_List|null
    158          *
    159          * @test E20R_Members_ListTest::test_get_instance()
    160          */
    161         public static function get_instance() {
    162 
    163             if ( is_null( self::$instance ) ) {
    164                 self::$instance = new self();
    165             }
    166 
    167             return self::$instance;
     147
     148        /**
     149         * Fetch the Page object
     150         *
     151         * @return Members_List_Page|null
     152         */
     153        public function get_page() {
     154            return $this->page;
    168155        }
    169156
     
    181168        public function load_hooks( $utils = null ) {
    182169
    183             if ( ! method_exists( '\\E20R\\Utilities\\Utilities', 'get_instance' ) ) {
     170            if ( ! method_exists( '\E20R\Utilities\Utilities', 'get_instance' ) ) {
    184171                $msg = esc_attr__( 'The E20R Utilities Module is missing/inactive!', 'e20r-members-list' );
    185172                throw new MissingUtilitiesModule( $msg );
     
    354341                    'filters'       => sprintf(
    355342                        '<a href="%1$s" title="%2$s">%3$s</a>',
    356                         esc_url_raw( plugin_dir_url( __FILE__ ) . '../docs/FILTERS.md' ),
     343                        esc_url_raw( plugin_dir_url( __FILE__ ) . '/docs/FILTERS.md' ),
    357344                        esc_attr__( 'View the Filter documentation', 'e20r-members-list' ),
    358345                        esc_attr__( 'Filters', 'e20r-members-list' )
     
    360347                    'actions'       => sprintf(
    361348                        '<a href="%1$s" title="%2$s">%3$s</a>',
    362                         esc_url_raw( plugin_dir_url( __FILE__ ) . '../docs/ACTIONS.md' ),
     349                        esc_url_raw( plugin_dir_url( __FILE__ ) . '/docs/ACTIONS.md' ),
    363350                        esc_attr__( 'View the Actions documentation', 'e20r-members-list' ),
    364351                        esc_attr__( 'Actions', 'e20r-members-list' )
     
    395382    $required_plugin = 'Better Members List for Paid Memberships Pro';
    396383
    397     if ( false === \E20R\Utilities\ActivateUtilitiesPlugin::attempt_activation() ) {
     384    if ( false === ActivateUtilitiesPlugin::attempt_activation() ) {
    398385        add_action(
    399386            'admin_notices',
    400387            function () use ( $required_plugin ) {
    401                 \E20R\Utilities\ActivateUtilitiesPlugin::plugin_not_installed( $required_plugin );
     388                ActivateUtilitiesPlugin::plugin_not_installed( $required_plugin );
    402389            }
    403390        );
  • e20r-members-list/trunk/docs/FILTERS.md

    r2672951 r2700272  
    250250);
    251251```
     252
     253### e20r_members_list_empty_date_values
     254
     255Modifies: Array of the date values that we (and PMPro) consider to be the equivalent of an 'empty' (not configured) date value
     256
     257Purpose: To modify or update the list of date values we'll think of as 'empty'. Anything goes here. Including integer values representing seconds since epoch start.
     258
     259Default:
     260```php
     261array(
     262    '',
     263    null,
     264    0,
     265    '0',
     266    '0000-00-00 00:00:00',
     267    '0000-00-00',
     268    '00:00:00',
     269);
     270```
     271
     272Dependencies: N/A
     273
     274Example:
     275```php
     276add_filter(
     277    'e20r_members_list_empty_date_values',
     278    function( $values ) {
     279        // Also include DD-MM-YYYY at midnight as a valid 'empty' value
     280        return $values + array(
     281        '00-00-0000 00:00:00',
     282        '00-00-00 00:00:00',
     283        );
     284    },
     285    10,
     286    1
     287);
     288```
  • e20r-members-list/trunk/inc/autoload.php

    r2672951 r2700272  
    55require_once __DIR__ . '/composer/autoload_real.php';
    66
    7 return ComposerAutoloaderInit40868d3a6d59e2d6945ded47ea627a6d::getLoader();
     7return ComposerAutoloaderInit8968a46d1d5402e6dbb4601beede8829::getLoader();
  • e20r-members-list/trunk/inc/composer/autoload_real.php

    r2672951 r2700272  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInit40868d3a6d59e2d6945ded47ea627a6d
     5class ComposerAutoloaderInit8968a46d1d5402e6dbb4601beede8829
    66{
    77    private static $loader;
     
    2525        require __DIR__ . '/platform_check.php';
    2626
    27         spl_autoload_register(array('ComposerAutoloaderInit40868d3a6d59e2d6945ded47ea627a6d', 'loadClassLoader'), true, true);
     27        spl_autoload_register(array('ComposerAutoloaderInit8968a46d1d5402e6dbb4601beede8829', 'loadClassLoader'), true, true);
    2828        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
    29         spl_autoload_unregister(array('ComposerAutoloaderInit40868d3a6d59e2d6945ded47ea627a6d', 'loadClassLoader'));
     29        spl_autoload_unregister(array('ComposerAutoloaderInit8968a46d1d5402e6dbb4601beede8829', 'loadClassLoader'));
    3030
    3131        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
     
    3333            require __DIR__ . '/autoload_static.php';
    3434
    35             call_user_func(\Composer\Autoload\ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d::getInitializer($loader));
     35            call_user_func(\Composer\Autoload\ComposerStaticInit8968a46d1d5402e6dbb4601beede8829::getInitializer($loader));
    3636        } else {
    3737            $map = require __DIR__ . '/autoload_namespaces.php';
     
    5454
    5555        if ($useStaticLoader) {
    56             $includeFiles = Composer\Autoload\ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d::$files;
     56            $includeFiles = Composer\Autoload\ComposerStaticInit8968a46d1d5402e6dbb4601beede8829::$files;
    5757        } else {
    5858            $includeFiles = require __DIR__ . '/autoload_files.php';
    5959        }
    6060        foreach ($includeFiles as $fileIdentifier => $file) {
    61             composerRequire40868d3a6d59e2d6945ded47ea627a6d($fileIdentifier, $file);
     61            composerRequire8968a46d1d5402e6dbb4601beede8829($fileIdentifier, $file);
    6262        }
    6363
     
    7171 * @return void
    7272 */
    73 function composerRequire40868d3a6d59e2d6945ded47ea627a6d($fileIdentifier, $file)
     73function composerRequire8968a46d1d5402e6dbb4601beede8829($fileIdentifier, $file)
    7474{
    7575    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
  • e20r-members-list/trunk/inc/composer/autoload_static.php

    r2672951 r2700272  
    55namespace Composer\Autoload;
    66
    7 class ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d
     7class ComposerStaticInit8968a46d1d5402e6dbb4601beede8829
    88{
    99    public static $files = array (
     
    9191    {
    9292        return \Closure::bind(function () use ($loader) {
    93             $loader->prefixLengthsPsr4 = ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d::$prefixLengthsPsr4;
    94             $loader->prefixDirsPsr4 = ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d::$prefixDirsPsr4;
    95             $loader->classMap = ComposerStaticInit40868d3a6d59e2d6945ded47ea627a6d::$classMap;
     93            $loader->prefixLengthsPsr4 = ComposerStaticInit8968a46d1d5402e6dbb4601beede8829::$prefixLengthsPsr4;
     94            $loader->prefixDirsPsr4 = ComposerStaticInit8968a46d1d5402e6dbb4601beede8829::$prefixDirsPsr4;
     95            $loader->classMap = ComposerStaticInit8968a46d1d5402e6dbb4601beede8829::$classMap;
    9696
    9797        }, null, ClassLoader::class);
  • e20r-members-list/trunk/languages/e20r-members-list.pot

    r2672951 r2700272  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Better Members List for Paid Memberships Pro 8.5\n"
     5"Project-Id-Version: Better Members List for Paid Memberships Pro 8.6\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/e20r-members-list\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
     
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2022-02-04T13:27:08+00:00\n"
     12"POT-Creation-Date: 2022-03-27T16:56:55+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.6.0\n"
     
    3535msgstr ""
    3636
    37 #: class-e20r-members-list.php:145
     37#: class-e20r-members-list.php:138
    3838msgid "The specified E20R_Members_List() class property does not exist!"
    3939msgstr ""
    4040
    41 #: class-e20r-members-list.php:184
     41#: class-e20r-members-list.php:171
    4242msgid "The E20R Utilities Module is missing/inactive!"
    4343msgstr ""
    4444
    45 #: class-e20r-members-list.php:215
     45#: class-e20r-members-list.php:202
    4646msgid "Could not clear all of the cached members list data. Check error logs for more information."
    4747msgstr ""
    4848
    49 #: class-e20r-members-list.php:342
     49#: class-e20r-members-list.php:329
    5050msgid "Donate to support updates, maintenance and tech support for this plugin"
    5151msgstr ""
    5252
    53 #: class-e20r-members-list.php:346
     53#: class-e20r-members-list.php:333
    5454msgid "Donate"
    5555msgstr ""
    5656
     57#: class-e20r-members-list.php:338
     58msgid "View the documentation"
     59msgstr ""
     60
     61#: class-e20r-members-list.php:339
     62msgid "Docs"
     63msgstr ""
     64
     65#: class-e20r-members-list.php:344
     66msgid "View the Filter documentation"
     67msgstr ""
     68
     69#: class-e20r-members-list.php:345
     70msgid "Filters"
     71msgstr ""
     72
     73#: class-e20r-members-list.php:350
     74msgid "View the Actions documentation"
     75msgstr ""
     76
    5777#: class-e20r-members-list.php:351
    58 msgid "View the documentation"
    59 msgstr ""
    60 
    61 #: class-e20r-members-list.php:352
    62 msgid "Docs"
     78msgid "Actions"
     79msgstr ""
     80
     81#: class-e20r-members-list.php:356
     82msgid "Visit the support forum"
    6383msgstr ""
    6484
    6585#: class-e20r-members-list.php:357
    66 msgid "View the Filter documentation"
    67 msgstr ""
    68 
    69 #: class-e20r-members-list.php:358
    70 msgid "Filters"
     86msgid "Support"
     87msgstr ""
     88
     89#: class-e20r-members-list.php:362
     90msgid "Report issues with this plugin"
    7191msgstr ""
    7292
    7393#: class-e20r-members-list.php:363
    74 msgid "View the Actions documentation"
    75 msgstr ""
    76 
    77 #: class-e20r-members-list.php:364
    78 msgid "Actions"
    79 msgstr ""
    80 
    81 #: class-e20r-members-list.php:369
    82 msgid "Visit the support forum"
    83 msgstr ""
    84 
    85 #: class-e20r-members-list.php:370
    86 msgid "Support"
    87 msgstr ""
    88 
    89 #: class-e20r-members-list.php:375
    90 msgid "Report issues with this plugin"
    91 msgstr ""
    92 
    93 #: class-e20r-members-list.php:376
    9494msgid "Report Issues"
    9595msgstr ""
     
    101101
    102102#: src/E20R/members-list/admin/bulk/Bulk_Cancel.php:145
    103 msgid "Cannot find the pmpro_cancelMembershipLevel() function!"
     103msgid "The pmpro_cancelMembershipLevel() function is not defined. Is Paid Memberships Pro activated on this site?"
     104msgstr ""
     105
     106#: src/E20R/members-list/admin/bulk/Bulk_Operations.php:106
     107msgid "The specified variable is not an array of user IDs"
    104108msgstr ""
    105109
    106110#. translators: %1$s - The parameter supplied, %2$s - The class name where we expect the supplied parameter to exist,
    107 #: src/E20R/members-list/admin/bulk/Bulk_Operations.php:98
     111#: src/E20R/members-list/admin/bulk/Bulk_Operations.php:120
    108112msgid "Invalid parameter \"%1$s\" supplied for %2$s"
    109113msgstr ""
    110114
    111115#. translators: %1$s - The class name where we expect the supplied parameter to exist.
    112 #: src/E20R/members-list/admin/bulk/Bulk_Operations.php:123
     116#: src/E20R/members-list/admin/bulk/Bulk_Operations.php:145
    113117msgid "Invalid parameter supplied for %1$s"
    114118msgstr ""
     
    154158
    155159#: src/E20R/members-list/admin/bulk/Bulk_Update.php:408
     160#: src/E20R/members-list/Members_List.php:2163
     161#: src/E20R/members-list/Members_List.php:2177
    156162msgid "Unknown"
    157163msgstr ""
    158164
    159 #: src/E20R/members-list/admin/export/Export_Members.php:1005
     165#: src/E20R/members-list/admin/export/Export_Members.php:1006
    160166msgid "Error - Undefined HTTP headers. Exiting!"
    161167msgstr ""
    162168
    163 #: src/E20R/members-list/admin/export/Export_Members.php:1014
     169#: src/E20R/members-list/admin/export/Export_Members.php:1015
    164170msgid "Cannot transmit export file. Review web server error logs for notices/warnings/errors. Exiting!"
    165171msgstr ""
    166172
    167 #: src/E20R/members-list/admin/export/Export_Members.php:1026
     173#: src/E20R/members-list/admin/export/Export_Members.php:1027
    168174msgid "Error: No export data found to transmit..."
    169175msgstr ""
     
    251257msgstr ""
    252258
    253 #: src/E20R/members-list/Members_List.php:300
     259#: src/E20R/members-list/Members_List.php:317
    254260msgid "member"
    255261msgstr ""
    256262
    257 #: src/E20R/members-list/Members_List.php:301
     263#: src/E20R/members-list/Members_List.php:318
    258264msgid "members"
    259265msgstr ""
    260266
    261 #: src/E20R/members-list/Members_List.php:407
     267#: src/E20R/members-list/Members_List.php:446
    262268msgid "Error: Invalid list of tables & joins for member list!"
    263269msgstr ""
    264270
    265 #: src/E20R/members-list/Members_List.php:415
     271#: src/E20R/members-list/Members_List.php:454
    266272msgid "Error: No FROM table specified for member list!"
    267273msgstr ""
    268274
    269275#. translators: %1$s is the per-page value supplied by the e20r_memberslist_per_page filter.
    270 #: src/E20R/members-list/Members_List.php:568
     276#: src/E20R/members-list/Members_List.php:607
    271277msgid "Error: Invalid 'per_page' value (need an integer): %1$s"
    272278msgstr ""
    273279
    274280#. translators: %1$s - The error message returned by the exception thrown
    275 #: src/E20R/members-list/Members_List.php:710
     281#: src/E20R/members-list/Members_List.php:749
    276282msgid "Cannot create a valid Database Query. Error message: %1$s"
    277283msgstr ""
    278284
    279285#. translators: %1$s - The error message returned by the exception thrown
    280 #: src/E20R/members-list/Members_List.php:735
     286#: src/E20R/members-list/Members_List.php:774
    281287msgid "Cannot execute database query. Error message: %1$s"
    282288msgstr ""
    283289
    284 #: src/E20R/members-list/Members_List.php:1009
     290#: src/E20R/members-list/Members_List.php:1051
    285291msgid "Insecure action denied."
    286292msgstr ""
    287293
    288 #. translators: Error message from the execption thrown
    289 #: src/E20R/members-list/Members_List.php:1048
    290 msgid "Cannot export data!. Error message: %1$s"
    291 msgstr ""
    292 
    293 #: src/E20R/members-list/Members_List.php:1102
    294 #: src/E20R/members-list/Members_List.php:1111
     294#. translators: %1$s - Error message from exception
     295#: src/E20R/members-list/Members_List.php:1085
     296msgid "Bulk Cancel: %1$s"
     297msgstr ""
     298
     299#: src/E20R/members-list/Members_List.php:1146
     300#: src/E20R/members-list/Members_List.php:1155
    295301msgid "Error cancelling membership(s)"
    296302msgstr ""
    297303
    298304#. translators: %1$s - Error message from thrown exception(s)
    299 #: src/E20R/members-list/Members_List.php:1163
    300 #: src/E20R/members-list/Members_List.php:1178
     305#: src/E20R/members-list/Members_List.php:1207
     306#: src/E20R/members-list/Members_List.php:1222
    301307msgid "Cannot export member data! Error message: %1$s"
    302308msgstr ""
    303309
    304310#. translators: %1$s query string.
    305 #: src/E20R/members-list/Members_List.php:1264
     311#: src/E20R/members-list/Members_List.php:1320
    306312msgid "Error processing Members List database query: %1$s"
    307313msgstr ""
    308314
    309315#. translators: %1$s - The class parameter, %2$s - The class name where we expect said parameter to exist.
    310 #: src/E20R/members-list/Members_List.php:1298
     316#: src/E20R/members-list/Members_List.php:1354
    311317msgid "%1$s is not a member variable in %2$s"
    312318msgstr ""
    313319
    314 #: src/E20R/members-list/Members_List.php:1380
     320#: src/E20R/members-list/Members_List.php:1436
    315321msgid "Error: Invalid database configuration!!!"
    316322msgstr ""
    317323
    318 #: src/E20R/members-list/Members_List.php:1406
     324#: src/E20R/members-list/Members_List.php:1462
    319325msgid "Missing the \"FROM\" statement"
    320326msgstr ""
    321327
    322 #: src/E20R/members-list/Members_List.php:1417
     328#: src/E20R/members-list/Members_List.php:1473
    323329msgid "Missing the minimum expected number of \"JOIN\" statements"
    324330msgstr ""
    325331
    326332#. translators: %1$d the number of JOIN entries in the Meembers_List::table_list definition
    327 #: src/E20R/members-list/Members_List.php:1435
     333#: src/E20R/members-list/Members_List.php:1491
    328334msgid "Have %1$d JOIN entries, but need at least 3!"
    329335msgstr ""
    330336
    331 #: src/E20R/members-list/Members_List.php:1456
     337#: src/E20R/members-list/Members_List.php:1512
    332338msgid "Does not include the \"JOIN\" to support membership level search, but specified a level"
    333339msgstr ""
    334340
    335 #: src/E20R/members-list/Members_List.php:1468
     341#: src/E20R/members-list/Members_List.php:1524
    336342msgid "Unexpected condition value for the 4th \"JOIN\" element"
    337343msgstr ""
    338344
    339345#. translators: %1$s table name defined by PMPro, %2$s Unexpected table name from user/developer
    340 #: src/E20R/members-list/Members_List.php:1482
     346#: src/E20R/members-list/Members_List.php:1538
    341347msgid "Unexpected table name value for the 4th \"JOIN\" element. Want: \"%1$s\", got: \"%2$s\""
    342348msgstr ""
    343349
    344 #: src/E20R/members-list/Members_List.php:1645
    345 #: src/E20R/members-list/Members_List.php:2057
    346 #: src/E20R/members-list/Members_List.php:2154
     350#: src/E20R/members-list/Members_List.php:1701
     351#: src/E20R/members-list/Members_List.php:2097
     352#: src/E20R/members-list/Members_List.php:2233
    347353msgid "Cancel"
    348354msgstr ""
    349355
    350 #: src/E20R/members-list/Members_List.php:1646
    351 #: src/E20R/members-list/Members_List.php:1720
     356#: src/E20R/members-list/Members_List.php:1702
     357#: src/E20R/members-list/Members_List.php:1776
    352358msgid "Update"
    353359msgstr ""
    354360
    355 #: src/E20R/members-list/Members_List.php:1647
     361#: src/E20R/members-list/Members_List.php:1703
    356362msgid "Export"
    357363msgstr ""
    358364
    359 #: src/E20R/members-list/Members_List.php:1719
     365#: src/E20R/members-list/Members_List.php:1775
    360366msgid "Update member info"
    361367msgstr ""
    362368
    363 #: src/E20R/members-list/Members_List.php:1818
    364 #: src/E20R/members-list/Members_List.php:1835
     369#: src/E20R/members-list/Members_List.php:1874
     370#: src/E20R/members-list/Members_List.php:1891
    365371msgid "Not found"
    366372msgstr ""
    367373
    368 #: src/E20R/members-list/Members_List.php:1838
     374#: src/E20R/members-list/Members_List.php:1894
     375#: src/E20R/members-list/Members_List.php:2168
     376#: src/E20R/members-list/Members_List.php:2181
    369377msgid "N/A"
    370378msgstr ""
    371379
    372 #: src/E20R/members-list/Members_List.php:1877
    373 #: src/E20R/members-list/modules/Multiple_Memberships.php:93
     380#: src/E20R/members-list/Members_List.php:1940
     381#: src/E20R/members-list/Members_List.php:2306
     382#: src/E20R/members-list/modules/Multiple_Memberships.php:124
     383msgid "Reset"
     384msgstr ""
     385
     386#: src/E20R/members-list/Members_List.php:2010
     387msgid "Free"
     388msgstr ""
     389
     390#: src/E20R/members-list/Members_List.php:2062
     391msgid "Invalid"
     392msgstr ""
     393
     394#: src/E20R/members-list/Members_List.php:2128
    374395msgid "No levels found. Paid Memberships Pro is inactive!"
    375396msgstr ""
    376397
    377 #: src/E20R/members-list/Members_List.php:1901
    378 #: src/E20R/members-list/Members_List.php:2229
    379 #: src/E20R/members-list/modules/Multiple_Memberships.php:117
    380 msgid "Reset"
    381 msgstr ""
    382 
    383 #: src/E20R/members-list/Members_List.php:1970
    384 msgid "Free"
    385 msgstr ""
    386 
    387 #: src/E20R/members-list/Members_List.php:2022
    388 msgid "Invalid"
    389 msgstr ""
    390 
    391 #: src/E20R/members-list/Members_List.php:2086
    392 msgid "Never"
    393 msgstr ""
    394 
    395 #. translators: %1$s HTML %2$s formatted payment amount, %3$s HTML.
    396 #: src/E20R/members-list/Members_List.php:2103
     398#. translators: %1$s - HTML %2$s - next payment date, %3$s - HTML.
     399#: src/E20R/members-list/Members_List.php:2184
    397400msgid "N/A (%1$sNext Payment: %2$s%3$s)"
    398401msgstr ""
    399402
    400 #: src/E20R/members-list/Members_List.php:2159
    401 msgid "Bulk update membership end/expiration date"
    402 msgstr ""
    403 
    404 #: src/E20R/members-list/Members_List.php:2234
     403#: src/E20R/members-list/Members_List.php:2238
     404msgid "Update membership end/expiration date"
     405msgstr ""
     406
     407#: src/E20R/members-list/Members_List.php:2311
    405408msgid "Update the member's membership status"
    406409msgstr ""
    407410
    408 #: src/E20R/members-list/Members_List.php:2362
     411#: src/E20R/members-list/Members_List.php:2439
    409412msgid "No members found"
    410413msgstr ""
    411414
    412 #: src/E20R/members-list/Members_List.php:2366
     415#: src/E20R/members-list/Members_List.php:2443
    413416msgid "It's possible the information you're looking for can be found in one of the following categories:"
    414417msgstr ""
    415418
    416419#. translators: %1$s HTML, %2%s HTML.
    417 #: src/E20R/members-list/Members_List.php:2373
     420#: src/E20R/members-list/Members_List.php:2450
    418421msgid "Repeat search: %1$sActive Members list%2$s"
    419422msgstr ""
    420423
    421424#. translators: %1$s HTML, %2%s HTML.
    422 #: src/E20R/members-list/Members_List.php:2386
     425#: src/E20R/members-list/Members_List.php:2463
    423426msgid "Repeat search: %1$sAll Members list%2$s"
    424427msgstr ""
    425428
    426429#. translators: %1$s HTML, %2$s HTML.
    427 #: src/E20R/members-list/Members_List.php:2398
     430#: src/E20R/members-list/Members_List.php:2475
    428431msgid "Repeat search: %1$sCancelled Members list%2$s"
    429432msgstr ""
    430433
    431434#. translators: %1$s HTML, %2$s HTML.
    432 #: src/E20R/members-list/Members_List.php:2413
     435#: src/E20R/members-list/Members_List.php:2490
    433436msgid "Repeat search: %1$sExpired Members list%2$s"
    434437msgstr ""
    435438
    436439#. translators: %1$s HTML, %2$s HTML.
    437 #: src/E20R/members-list/Members_List.php:2425
     440#: src/E20R/members-list/Members_List.php:2502
    438441msgid "Repeat search: %1$sOld Members list%2$s"
    439442msgstr ""
    440443
    441444#. translators: %1$s HTML, %2$s HTML.
    442 #: src/E20R/members-list/Members_List.php:2436
     445#: src/E20R/members-list/Members_List.php:2513
    443446msgid "Repeat search: %1$sAll Users list%2$s"
    444447msgstr ""
     448
     449#: src/E20R/members-list/modules/Multiple_Memberships.php:84
     450msgid "primary"
     451msgstr ""
     452
     453#: src/E20R/members-list/modules/Multiple_Memberships.php:133
     454msgid "Click to edit primary membership level"
     455msgstr ""
  • e20r-members-list/trunk/src/E20R/members-list/Members_List.php

    r2672951 r2700272  
    3636use E20R\Utilities\Message;
    3737use E20R\Utilities\Utilities;
     38use stdClass;
    3839use WP_List_Table;
     40use WP_User;
    3941
    4042if ( ! defined( 'ABSPATH' ) && ! defined( 'PLUGIN_PHPUNIT' ) ) {
     
    4648}
    4749
    48 if ( ! class_exists( '\\E20R\\Members_List\\Members_List' ) ) {
     50if ( ! class_exists( 'E20R\Members_List\Members_List' ) ) {
    4951
    5052    /**
     
    272274
    273275        /**
     276         * List of date values we consider 'empty' (not confiured/set/defined)
     277         *
     278         * @var mixed|void|null $empty_date_values
     279         */
     280        private $empty_date_values = array();
     281
     282        /**
    274283         * Members_List constructor.
    275284         *
     
    288297            $this->action      = $this->utils->get_variable( 'action', '' );
    289298            $this->date_format = get_option( 'date_format' );
     299
     300            // Default settings for what we consider 'empty' (not set) date values
     301            add_filter(
     302                'e20r_members_list_empty_date_values',
     303                array( $this, 'set_empty_date_values' ),
     304                -1,
     305                1
     306            );
    290307
    291308            if ( empty( $page ) ) {
     
    326343            );
    327344
     345            // Configure the defaults for the empty/not set date values
     346            $this->empty_date_values = apply_filters( 'e20r_members_list_empty_date_values', array() );
     347
    328348            if ( $this->is_module_enabled( 'pmpro-multiple-memberships-per-user/pmpro-multiple-memberships-per-user.php' ) ) {
    329349                // TODO: Add actions to process data for the PMPro Multiple Memberships Per User add-on
    330350                $this->utils->log( 'Enable the MMPU extensions' );
    331351            }
     352
    332353
    333354            /**
     
    341362        }
    342363
     364        /**
     365         * Define the list of 'empty' values for the date check(s)
     366         *
     367         * @param array $values Received list of values
     368         *
     369         * @return array
     370         */
     371        public function set_empty_date_values( $values = array() ) {
     372            return $values + array(
     373                '',
     374                null,
     375                0,
     376                '0',
     377                '0000-00-00 00:00:00',
     378                '0000-00-00',
     379                '00:00:00',
     380            );
     381        }
    343382        /**
    344383         * Generate the SQL and set the sql_query member variable to use the query
     
    9611000                // Some of the default columns are sortable.
    9621001                switch ( $col ) {
     1002                    case 'display_name':
    9631003                    case 'user_login':
    9641004                    case 'user_email':
     
    9671007                    case 'status':
    9681008                    case 'last':
    969                     case 'last_name':
    9701009                        $sortable_columns[ $col ] = array( $col, false );
    9711010                        break;
     
    9971036
    9981037            // Are we processing a bulk action?
    999             if ( 1 === preg_match( '/bulk-/', $a ) || 1 === preg_match( '/bulk-/', $a2 ) ) {
     1038            if (
     1039                ( null !== $a && 1 === preg_match( '/bulk-/', $a ) ) ||
     1040                ( null !== $a2 && 1 === preg_match( '/bulk-/', $a2 ) )
     1041            ) {
    10001042
    10011043                $this->utils->log( 'Processing a bulk action' );
     
    10181060                $action           = $this->current_action();
    10191061                $data             = array();
    1020                 $selected_members = $this->utils->get_variable( 'member_user_id', array() );
     1062                $selected_members = $this->utils->get_variable( 'member_user_ids', array() );
    10211063
    10221064                foreach ( $selected_members as $key => $user_id ) {
     
    10341076
    10351077                if ( in_array( 'bulk-cancel', $bulk_actions, true ) ) {
    1036                     $this->cancel = new Bulk_Cancel( $data, $this->utils );
    1037                     $this->cancel->execute();
     1078                    try {
     1079                        $this->cancel = new Bulk_Cancel( $data, $this->utils );
     1080                        $this->cancel->execute();
     1081                    } catch ( InvalidProperty $e ) {
     1082                        $this->utils->add_message(
     1083                            sprintf(
     1084                                // translators: %1$s - Error message from exception
     1085                                esc_attr__( 'Bulk Cancel: %1$s', 'e20r-members-list' ),
     1086                                $e->getMessage()
     1087                            ),
     1088                            'error',
     1089                            'backend'
     1090                        );
     1091                    }
     1092
    10381093                    return;
    10391094
     
    10411096
    10421097                    $this->utils->log( 'Requested Export of members!' );
    1043                     try {
    1044                         $this->export_members();
    1045                     } catch ( DBQueryError | InvalidSQL $e ) {
    1046                         $msg = sprintf(
    1047                                 // translators: Error message from the execption thrown
    1048                             esc_attr__( 'Cannot export data!. Error message: %1$s', 'e20r-members-list' ),
    1049                             $e->getMessage()
    1050                         );
    1051                         $this->utils->add_message( $msg, 'error', 'backend' );
    1052                         return;
    1053                     }
     1098                    $this->export_members();
    10541099
    10551100                    // To push the export file to the browser, we have to terminate execution of this process.
     
    10781123                $this->utils->log( 'Single action for the Members List...' );
    10791124
    1080                 $user_id                     = $this->utils->get_variable( 'member_user_id', array() );
     1125                $user_id                     = $this->utils->get_variable( 'member_user_ids', array() );
    10811126                $level_id                    = $this->utils->get_variable( 'membership_id', array() );
    10821127                $action                      = $this->current_action();
     
    10971142                            $this->cancel = new Bulk_Cancel( $user_ids, $this->utils );
    10981143                        }
    1099 
    11001144
    11011145                        if ( false === $this->cancel->execute() ) {
     
    12051249         * @throws InvalidSQL Raised when there's a problem with the SQL we generated.
    12061250         * @throws DBQueryError Raised if the DB Query reports a problem
    1207          * @throws BadOperation|InvalidSettingsKey Raised when the Cache() class tries something unexpected
    12081251         */
    12091252        public function get_members( $per_page = null, $page_number = null, $return_count = false ) {
     
    12131256            }
    12141257
    1215             $result             = null;
    12161258            $result_cache_group = $this->page->get( 'result_cache_group' );
    1217             $cache_timeout      = ( (int) $this->page->get( 'cache_timeout' ) * self::MINUTE_IN_SECONDS );
     1259            try {
     1260                $cache_timeout = ( (int) $this->page->get( 'cache_timeout' ) * self::MINUTE_IN_SECONDS );
     1261            } catch ( InvalidSettingsKey $e ) {
     1262                $this->utils->log( 'Error: cache_timeout is not a valid settings key!' );
     1263                $cache_timeout = 10 * self::MINUTE_IN_SECONDS;
     1264            }
    12181265
    12191266            global $wpdb;
     
    12211268            if ( empty( $this->sql_query ) || 1 !== preg_match_all( '/SELECT\s+.*\s+FROM(\s+(.*)){1,}/im', $this->sql_query ) ) {
    12221269                $this->utils->log( 'Error in/Missing SQL statement! "' . $this->sql_query . '"' );
    1223                 throw new InvalidSQL( 'Error: Attempting to fetch data without a valid SQL query!' );
     1270                throw new InvalidSQL( 'Attempting to fetch data without a valid SQL query!' );
    12241271            }
    12251272
    12261273            // Generate the Cache key based on
    12271274            $lookup_cache_key = md5( $this->sql_query );
    1228             $result           = Cache::get( $lookup_cache_key, $result_cache_group );
     1275            try {
     1276                $result = Cache::get( $lookup_cache_key, $result_cache_group );
     1277            } catch ( BadOperation $e ) {
     1278                $this->utils->log( 'Error returning cached results for ' . $lookup_cache_key );
     1279                $result = null;
     1280            }
    12291281
    12301282            if ( null === $result ) {
     
    12511303                // Save the result to the cache based on the cache specific key
    12521304                $this->utils->log( "Attempting to cache the results for {$lookup_cache_key}" );
    1253                 if ( false === Cache::set( $lookup_cache_key, $result, $cache_timeout, $result_cache_group ) ) {
    1254                     $this->utils->log( 'Error saving the result to cache!' );
     1305                try {
     1306                    if ( false === Cache::set( $lookup_cache_key, $result, $cache_timeout, $result_cache_group ) ) {
     1307                        $this->utils->log( 'Error saving the result to cache!' );
     1308                    }
     1309                } catch ( BadOperation $e ) {
     1310                    $this->utils->log( 'Error: Unable to save to cache for ' . $lookup_cache_key );
    12551311                }
    12561312            } else {
     
    15671623            $this->utils->log( 'Requesting (active/old/etc) member export' );
    15681624
    1569             $member_ids  = $this->utils->get_variable( 'member_user_id', array() );
     1625            $member_ids  = $this->utils->get_variable( 'member_user_ids', array() );
    15701626            $added_where = null;
    15711627
     
    16771733         */
    16781734        public function column_user_login( $item ) {
    1679             $user = new \WP_User( $item['ID'] );
     1735            $user = new WP_User( $item['ID'] );
    16801736
    16811737            $edit_url = add_query_arg(
     
    18321888
    18331889            if ( empty( $address ) ) {
    1834 
     1890                $this->utils->log( "User {$item['ID']} has no billing address" );
    18351891                $address = esc_attr__( 'Not found', 'e20r-members-list' );
    18361892
     
    18581914                <input type="hidden" value="%3$s" class="e20r-members-list-membership_id-label" name="e20r-members-list-membership_label_%2$s" />
    18591915                <input type="hidden" value="%1$d" class="e20r-members-list-db-membership_id" name="e20r-members-list-db_membership_id_%2$s" />
     1916                <input type="hidden" value=""     class="e20r-members-list-db-membership_level_ids" name="e20r-members-list-db_membership_level_ids_%2$s" />
    18601917                <input type="hidden" value="%5$d" class="e20r-members-list-db_record_id" name="e20r-members-list-db_record_id_%2$s" />
    18611918                <input type="hidden" value="%4$s" class="e20r-members-list-field-name" name="e20r-members-list-field_name_%2$s" />',
     
    18671924            );
    18681925
    1869             $options = '';
    1870             if ( function_exists( 'pmpro_getAllLevels' ) ) {
    1871                 $levels = pmpro_getAllLevels( true, true );
    1872             } else {
    1873 
    1874                 // Default info if PMPro is disabled.
    1875                 $null_level           = new \stdClass();
    1876                 $null_level->level_id = 0;
    1877                 $null_level->name     = esc_attr__( 'No levels found. Paid Memberships Pro is inactive!', 'e20r-members-list' );
    1878                 $levels               = array( $null_level );
    1879             }
    1880 
    1881             foreach ( $levels as $level ) {
    1882                 $options .= sprintf(
    1883                     '<option value="%1$s" %2$s>%3$s</option>',
    1884                     $level->id,
    1885                     selected( $level->id, $item['membership_id'], false ),
    1886                     $level->name
    1887                 ) . "\n";
    1888             }
     1926            $options = self::build_option_string( $item['membership_id'] );
     1927
    18891928            $new_membershiplevel_input = sprintf(
    18901929                '<div class="ml-row-settings clearfix">
     
    19311970            if ( true === $this->is_module_enabled( 'pmpro-multiple-memberships-per-user/pmpro-multiple-memberships-per-user.php' ) ) {
    19321971                $mmpu = new Multiple_Memberships();
    1933                 return $mmpu->column_name( $item );
     1972                $this->utils->log( 'MMPU support is deactivated pending support for adding/removing MMPU memberships' );
     1973                // return $mmpu->multiple_membership_column( $item ); FIXME: Actually enable MMPU support
    19341974            }
    19351975
     
    19792019         * @param array $item The record to process.
    19802020         *
    1981          * @return \stdClass
     2021         * @return stdClass
    19822022         */
    19832023        public function column_code( $item ) {
     
    20182058        public function column_startdate( $item ) {
    20192059
    2020             if ( '0000-00-00 00:00:00' === $item['startdate'] || empty( $item['startdate'] ) ) {
     2060            if ( $this->has_empty_date( $item['startdate'] ) ) {
    20212061                $date_value  = null;
    20222062                $start_label = esc_attr__( 'Invalid', 'e20r-members-list' );
    20232063            } else {
    2024                 $date_value  = ! empty( $item['startdate'] ) ? date_i18n( 'Y-m-d', strtotime( $item['startdate'], time() ) ) : null;
    2025                 $start_label = date_i18n( $this->date_format, strtotime( $item['startdate'], time() ) );
    2026             }
    2027 
    2028             $min_val = empty( $item['startdate'] ) ? sprintf( 'min="%s"', date_i18n( 'Y-m-d', time() ) ) : null;
     2064                $date_value  = ! empty( $item['startdate'] ) ? gmdate( 'Y-m-d', strtotime( $item['startdate'], time() ) ) : null;
     2065                $start_label = gmdate( $this->date_format, strtotime( $item['startdate'], time() ) );
     2066            }
     2067
     2068            $min_val = empty( $item['startdate'] ) ? sprintf( 'min="%s"', gmdate( 'Y-m-d', time() ) ) : null;
    20292069
    20302070            $startdate_input = sprintf(
     
    20692109
    20702110        /**
     2111         * Generate the HTML for the membership level options
     2112         *
     2113         * @param int $level_id The membership level ID to highlight
     2114         *
     2115         * @return string
     2116         */
     2117        public static function build_option_string( $level_id ) {
     2118
     2119            $options = '';
     2120
     2121            if ( function_exists( 'pmpro_getAllLevels' ) ) {
     2122                $levels = pmpro_getAllLevels( true, true );
     2123            } else {
     2124
     2125                // Default info if PMPro is disabled.
     2126                $null_level           = new stdClass();
     2127                $null_level->level_id = 0;
     2128                $null_level->name     = esc_attr__( 'No levels found. Paid Memberships Pro is inactive!', 'e20r-members-list' );
     2129                $levels               = array( $null_level );
     2130            }
     2131
     2132            foreach ( $levels as $level ) {
     2133                $options .= sprintf(
     2134                    '<option value="%1$s" %2$s>%3$s</option>',
     2135                    $level->id,
     2136                    selected( $level->id, $level_id, false ),
     2137                    $level->name
     2138                ) . "\n";
     2139            }
     2140
     2141            return $options;
     2142        }
     2143        /**
     2144         * Does the received value represent an 'empty' date value
     2145         *
     2146         * @param string|int|null $date The value to check
     2147         *
     2148         * @return bool
     2149         */
     2150        public function has_empty_date( $date ) {
     2151            return in_array( $date, $this->empty_date_values, true );
     2152        }
     2153        /**
    20712154         * Create the last column for the default Members_List table (Expiration date)
    20722155         *
     
    20772160        public function column_last( $item ) {
    20782161
    2079             $enddate = null;
    2080 
    2081             // No billing amount (not recurring) and no end date (i.e. a free, limit less membership)
    2082             if (
    2083                     empty( $item['billing_amount'] ) &&
    2084                 ( empty( $item['enddate'] ) || '0000-00-00 00:00:00' === $item['enddate'] )
    2085             ) {
    2086                 $enddate_label = esc_attr__( 'Never', 'e20r-members-list' );
    2087                 $enddate_label = null;
    2088             } else {
    2089                 $enddate       = date_i18n(
    2090                     'Y-m-d',
     2162            $no_enddate    = false;
     2163            $html_label    = esc_attr__( 'Unknown', 'e20r-members-list' );
     2164            $enddate_label = '';
     2165
     2166            // The end-date field is empty/not configured (never ending membership)
     2167            if ( ! isset( $item['enddate'] ) || in_array( $item['enddate'], $this->empty_date_values, true ) ) {
     2168                $enddate_label = esc_attr__( 'N/A', 'e20r-members-list' );
     2169                $html_label    = $enddate_label;
     2170                $no_enddate    = true;
     2171            }
     2172
     2173            // We have a user with a recurring billing membership.
     2174            if ( true === $no_enddate && ! empty( $item['billing_amount'] ) && ! empty( $item['cycle_number'] ) ) {
     2175                $next_payment = pmpro_next_payment( $item['ID'], 'success', 'timestamp' );
     2176                if ( false === $next_payment ) {
     2177                    $next_payment_date = esc_attr__( 'Unknown', 'e20r-members-list' );
     2178                } else {
     2179                    $next_payment_date = gmdate( $this->date_format, $next_payment );
     2180                }
     2181                $enddate_label = esc_attr__( 'N/A', 'e20r-members-list' );
     2182                $html_label    = sprintf(
     2183                    // translators: %1$s - HTML %2$s - next payment date, %3$s - HTML.
     2184                    esc_attr__( 'N/A (%1$sNext Payment: %2$s%3$s)', 'e20r-members-list' ),
     2185                    '<span class="e20r-members-list-small" style="font-size: 10px; font-style: italic;">',
     2186                    $next_payment_date,
     2187                    '</span>'
     2188                );
     2189            }
     2190
     2191            // Setting the date label only if the value is non-empty _AND_ the value is _NOT_ ''
     2192            if ( false === $no_enddate ) {
     2193                $enddate_label = gmdate(
     2194                    $this->date_format,
    20912195                    strtotime( $item['enddate'], time() )
    20922196                );
    2093                 $enddate_label = date_i18n( $this->date_format, strtotime( $item['enddate'], time() ) );
    2094             }
    2095 
    2096             // The membership level has recurring payment.
    2097             if (
    2098                     ( empty( $item['enddate'] ) || '0000-00-00 00:00:00' === $item['enddate'] ) &&
    2099                     ! empty( $item['billing_amount'] ) && ! empty( $item['cycle_number'] )
    2100             ) {
    2101                 $enddate_label = sprintf(
    2102                         // translators: %1$s HTML %2$s formatted payment amount, %3$s HTML.
    2103                     esc_attr__( 'N/A (%1$sNext Payment: %2$s%3$s)', 'e20r-members-list' ),
    2104                     '<span class="e20r-members-list-small" style="font-size: 10px; font-style: italic;">',
    2105                     date_i18n(
    2106                         $this->date_format,
    2107                         pmpro_next_payment(
    2108                             $item['ID'],
    2109                             'success',
    2110                             'timestamp'
    2111                         )
    2112                     ),
    2113                     '</span>'
    2114                 );
    2115                 $enddate = null;
    2116             }
    2117 
    2118             $enddate = apply_filters( 'e20r_members_list_enddate_col_result', $enddate, $item );
     2197                $html_label    = $enddate_label;
     2198            }
    21192199
    21202200            // BUG FIX: Didn't set the date format to match the WP setting as documented.
    2121             $date_value = ! (
    2122                     empty( $item['enddate'] ) ||
    2123                     '0000-00-00 00:00:00' === $item['enddate'] ) ?
    2124                     date_i18n( $this->date_format, strtotime( $item['enddate'], time() ) ) :
    2125                     null;
     2201            $date_value = ( false === $no_enddate ) ?
     2202                strtotime( $item['enddate'], time() ) :
     2203                null;
     2204
     2205            $date_value = apply_filters( 'e20r_members_list_enddate_col_result', $date_value, $item );
    21262206
    21272207            // These are used to configure the enddate with JavaScript.
     
    21332213                <input type="hidden" value="%4$s" class="e20r-members-list-db-enddate" name="e20r-members-list-db_enddate_%2$s" />
    21342214                <input type="hidden" value="%5$d" class="e20r-members-list-db_record_id" name="e20r-members-list-db_record_id_%2$s" />
    2135                 <input type="hidden" value="%6$s" class="e20r-members-list-field-name" name="e20r-members-list-field_name_%2$s" />',
     2215                <input type="hidden" value="enddate" class="e20r-members-list-field-name" name="e20r-members-list-field_name_%2$s" />',
    21362216                $item['membership_id'],
    21372217                $item['ID'],
     
    21392219                $date_value,
    21402220                $item['record_id'],
    2141                 'enddate'
    21422221            );
    21432222
     
    21462225                        %1$s
    21472226                        <input type="date" placeholder="YYYY-MM-DD" pattern="(?:19|20)[0-9]{2}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9])|(?:(?!02)(?:0[1-9]|1[0-2])-(?:30))|(?:(?:0[13578]|1[02])-31))" title="Enter a date in this format YYYY-MM-DD" name="e20r-members-list-new_enddate_%2$s" class="e20r-members-list-input-enddate" value="%3$s"/>
    2148                         <br>
     2227                        <br />
    21492228                        <a href="#" class="e20r-members-list-cancel e20r-members-list-list-link">%4$s</a>
    21502229                    </div>',
    21512230                $enddate_input,
    21522231                $item['ID'],
    2153                 $date_value,
     2232                $date_value ? gmdate( 'Y-m-d', $date_value ) : $date_value,
    21542233                esc_attr__( 'Cancel', 'e20r-members-list' )
    21552234            );
    21562235
    2157             $value = sprintf(
    2158                 '<a href="#" class="e20r-members-list_enddate e20r-members-list-editable" title="%1$s">%2$s<span class="dashicons dashicons-edit"></a></span>%3$s',
    2159                 esc_attr__( 'Bulk update membership end/expiration date', 'e20r-members-list' ),
    2160                 $enddate_label,
     2236            return sprintf(
     2237                '<a href="#" class="e20r-members-list_enddate e20r-members-list-editable" title="%1$s">%2$s<span class="dashicons dashicons-edit"></span></a>%3$s',
     2238                esc_attr__( 'Update membership end/expiration date', 'e20r-members-list' ),
     2239                $html_label,
    21612240                $new_date_input
    21622241            );
    2163 
    2164             return $value;
    21652242        }
    21662243
     
    22212298                        %3$s
    22222299                        </select>
    2223                         <br>
     2300                        <br />
    22242301                        <a href="#" class="e20r-members-list-cancel e20r-members-list-link">%4$s</a>
    22252302                    </div>',
  • e20r-members-list/trunk/src/E20R/members-list/admin/bulk/Bulk_Cancel.php

    r2672951 r2700272  
    2222namespace E20R\Members_List\Admin\Bulk;
    2323
     24use E20R\Members_List\Admin\Exceptions\InvalidMemberList;
    2425use E20R\Members_List\Admin\Exceptions\InvalidProperty;
    2526use E20R\Members_List\Admin\Exceptions\PMProNotActive;
    2627use E20R\Utilities\Message;
    2728use E20R\Utilities\Utilities;
     29use function pmpro_cancelMembershipLevel;
    2830
    2931if ( ! defined( 'ABSPATH' ) && ! defined( 'PLUGIN_PHPUNIT' ) ) {
     
    3133}
    3234
    33 if ( ! class_exists( '\\E20R\\Members_List\\Admin\\Bulk\\Bulk_Cancel' ) ) {
     35if ( ! class_exists( 'E20R\Members_List\Admin\Bulk\Bulk_Cancel' ) ) {
    3436
    3537    /**
     
    4143         * Bulk_Cancel constructor (singleton)
    4244         *
    43          * @param array[]|int[]|null $members_to_update The array of member IDs to perform the bulk cancel operation against
    44          * @param Utilities|null     $utils Instance of the E20R Utilities Module class
     45         * @param array[]|null  $members_to_update The array of member IDs to perform the bulk cancel operation against
     46         * @param Utilities|null $utils Instance of the E20R Utilities Module class
    4547         *
    4648         * @access public
    4749         * @throws InvalidProperty Thrown when the class or base class lacks the specified set() property
     50         * @throws InvalidMemberList Thrown when the supplied array isn't null or has the wrong format
    4851         */
    49         public function __construct( $members_to_update = array(), $utils = null ) {
     52        public function __construct( $members_to_update = null, $utils = null ) {
    5053
    5154            if ( empty( $utils ) ) {
     
    5457            }
    5558
    56             parent::__construct( $utils );
     59            parent::__construct( $members_to_update, $utils );
    5760
    5861            $this->set( 'operation', 'cancel' );
    59             $this->set( 'members_to_update', $members_to_update );
    6062        }
    6163
     
    6870        public function execute() {
    6971
     72            if ( ! $this->pmpro_is_active() ) {
     73                return false;
     74            }
     75
    7076            // Process all User & level ID for the single action.
    7177            $this->failed = array();
     
    7480            // Process all selected records/members
    7581            foreach ( $this->members_to_update as $key => $cancel_info ) {
    76                 try {
    77                     if ( false === $this->cancel_member( $cancel_info['user_id'], $cancel_info['level_id'] ) ) {
    78                         if ( ! isset( $this->failed[ $cancel_info['user_id'] ] ) ) {
    79                             $this->failed[ $cancel_info['user_id'] ] = array();
    80                         }
    81                         $this->failed[ $cancel_info['user_id'] ][] = $cancel_info['level_id']; // FIXME: Add level info for multiple membership levels
     82                if ( false === $this->cancel_member( $cancel_info['user_id'], $cancel_info['level_id'] ) ) {
     83                    if ( ! isset( $this->failed[ $cancel_info['user_id'] ] ) ) {
     84                        $this->failed[ $cancel_info['user_id'] ] = array();
    8285                    }
    83                 } catch ( PMProNotActive $e ) {
    84                     $this->utils->add_message( $e->getMessage(), 'error', 'backend' );
    85                     $this->failed[] = $cancel_info['user_id'];
    86                     return false;
     86                    $this->failed[ $cancel_info['user_id'] ][] = $cancel_info['level_id']; // FIXME: Add level info for multiple membership levels
    8787                }
    8888            }
     
    144144                throw new PMProNotActive(
    145145                    esc_attr__(
    146                         'Cannot find the pmpro_cancelMembershipLevel() function!',
     146                        'The pmpro_cancelMembershipLevel() function is not defined. Is Paid Memberships Pro activated on this site?',
    147147                        'e20r-members-list'
    148148                    )
    149149                );
    150150            }
    151             $this->utils->log( "Cancelling membership level {$id} for user {$id}" );
    152             return \pmpro_cancelMembershipLevel( $level_id, $id, 'admin_cancelled' );
     151            $this->utils->log( "Cancelling membership level {$level_id} for user {$id}" );
     152            return pmpro_cancelMembershipLevel( $level_id, $id, 'admin_cancelled' );
     153        }
     154
     155        /**
     156         * Check if PMPro is active and throw exception if it isn't
     157         *
     158         * @return bool
     159         */
     160        private function pmpro_is_active() {
     161            return function_exists( 'pmpro_cancelMembershipLevel' );
    153162        }
    154163    }
  • e20r-members-list/trunk/src/E20R/members-list/admin/bulk/Bulk_Operations.php

    r2672951 r2700272  
    2222namespace E20R\Members_List\Admin\Bulk;
    2323
     24use E20R\Members_List\Admin\Exceptions\InvalidMemberList;
    2425use E20R\Members_List\Admin\Exceptions\InvalidProperty;
    2526use E20R\Utilities\Message;
     
    4748         * Array of WP_User IDs where the cancel operation failed
    4849         *
    49          * @var null|int[] $failed
     50         * @var array[][] $failed
    5051         */
    51         protected $failed = null;
     52        protected $failed = array();
    5253
    5354        /**
    5455         * Array of members to update where the memebr date is represented as an array per member
    5556         *
    56          * @var array[]|int[]|null
     57         * @var array[]
    5758         */
    5859        protected $members_to_update = array();
     
    6869         * Bulk_Cancel constructor (singleton)
    6970         *
     71         * @param array          $members_to_update The list of members and level IDs to process
    7072         * @param Utilities|null $utils Instance of the E20R Utilities Module class
    7173         *
    7274         * @access public
    7375         */
    74         public function __construct( $utils = null ) {
     76        public function __construct( $members_to_update = null, $utils = null ) {
    7577
    7678            if ( empty( $utils ) ) {
     
    7981            }
    8082
     83            if ( null !== $members_to_update ) {
     84                $this->set( 'members_to_update', $members_to_update );
     85            }
    8186            $this->utils = $utils;
    8287        }
     
    8994         *
    9095         * @throws InvalidProperty Raised if the user supplies an invalid class parameter
     96         * @throws InvalidMemberList Raised if we're attempting to set the 'members_to_update' property and its value isn't appropriate
    9197         */
    9298        public function set( string $param, $value ) {
     99
     100            if (
     101                'members_to_update' === $param &&
     102                null !== $value &&
     103                false === $this->valid_member_array( $value )
     104            ) {
     105                throw new InvalidMemberList(
     106                    esc_attr__(
     107                        'The specified variable is not an array of user IDs',
     108                        'e20r-members-list'
     109                    )
     110                );
     111            } elseif ( 'members_to_update' === $param && null === $value ) {
     112                $value = array();
     113            }
     114
    93115            // Make sure we let the caller know there's a problem if the variable doesn't exist.
    94116            if ( ! property_exists( $this, $param ) ) {
     
    131153
    132154        /**
     155         * Make sure the array is a valid user/membership level array
     156         *
     157         * @param mixed $members Variable (array) to test
     158         *
     159         * @return bool
     160         */
     161        protected function valid_member_array( $members ) {
     162
     163            if ( ! is_array( $members ) ) {
     164                return false;
     165            }
     166
     167            if ( empty( $members ) ) {
     168                return true;
     169            }
     170
     171            return array_reduce(
     172                $members,
     173                function ( $result, $item ) {
     174                    return $result &&
     175                        ( isset( $item['user_id'] ) && is_int( $item['user_id'] ) ) &&
     176                        ( isset( $item['level_id'] ) && is_int( $item['level_id'] ) );
     177                },
     178                true
     179            );
     180        }
     181
     182        /**
    133183         * Execute the Bulk Operation
    134184         *
  • e20r-members-list/trunk/src/E20R/members-list/admin/bulk/Bulk_Update.php

    r2672951 r2700272  
    2222namespace E20R\Members_List\Admin\Bulk;
    2323
     24use DateTime;
     25use E20R\Members_List\Admin\Exceptions\InvalidMemberList;
    2426use E20R\Members_List\Admin\Exceptions\InvalidProperty;
    2527use E20R\Members_List\Admin\Exceptions\PMProNotActive;
     
    4143         * Bulk_Update constructor
    4244         *
    43          * @param array          $members Array of member information to update
     45         * @param null|array[]   $members Array of member information to update
    4446         * @param null|Utilities $utils Instance of the E20R Utilities Module class
    4547         *
    46          * @throws InvalidProperty Raised if the specified class parameter for some reason is missing
     48         * @throws InvalidMemberList Raised if the supplied $members variable isn't a list of integers
    4749         */
    48         public function __construct( $members, $utils = null ) {
    49 
     50        public function __construct( $members = null, $utils = null ) {
    5051            if ( empty( $utils ) ) {
    5152                $message = new Message();
    5253                $utils   = new Utilities( $message );
    5354            }
    54 
    55             parent::__construct( $utils );
    56             $this->set( 'members_to_update', $members );
    57             $this->set( 'operation', 'cancel' );
     55            $this->utils = $utils;
     56            parent::__construct( $members, $this->utils );
     57            $this->set( 'operation', 'update' );
    5858        }
    5959
     
    495495
    496496        /**
    497          * Return the list of members being updated
    498          *
    499          * @return array[]
    500          */
    501         public function get_members() {
    502             return $this->members_to_update;
    503         }
    504 
    505         /**
    506497         * Test the date supplied for MySQL compliance
    507498         *
     
    515506        private function validate_date_format( $date, $format = 'Y-m-d' ) {
    516507
    517             $check_date = \DateTime::createFromFormat( $format, $date );
     508            $check_date = DateTime::createFromFormat( $format, $date );
    518509
    519510            return $check_date && $check_date->format( $format ) === $date;
  • e20r-members-list/trunk/src/E20R/members-list/admin/export/Export_Members.php

    r2672951 r2700272  
    3030}
    3131
    32 if ( ! class_exists( '\\E20R\\Members_List\\Admin\\Export\\Export_Members' ) ) {
     32if ( ! class_exists( 'E20R\Members_List\Admin\Export\Export_Members' ) ) {
    3333
    3434    /**
     
    662662         */
    663663        private function enclose( $text ) {
     664            $text = empty( $text ) ? '' : $text;
    664665            return '"' . str_replace( '"', '\\"', $text ) . '"';
    665666        }
  • e20r-members-list/trunk/src/E20R/members-list/js/e20r-memberslist-page.js

    r2672951 r2700272  
    22 * License:
    33
    4     Copyright 2016-2021 - Eighty / 20 Results by Wicked Strong Chicks, LLC ([email protected])
     4    Copyright 2016-2022 - Eighty / 20 Results by Wicked Strong Chicks, LLC ([email protected])
    55
    66    This program is free software; you can redistribute it and/or modify
     
    2323
    2424    let e20rMembersList_Page = {
    25         init: function () {
    26 
    27             // this.levels_dropdown = $('select#e20r-pmpro-memberslist-levels');
    28             // this.enddate_lnk = $('a.e20r-members-list_enddate');
    29             // this.cancelMemberLnk = $('a.e20r-cancel-member');
    30             // this.updateBtn = $('a.e20r-members-list-save');
    31             this.memberslist_form = $('.e20r-pmpro-memberslist-page form#posts-filter');
    32             this.edit_lnk = $('a.e20r-members-list-editable');
    33             this.resetBtn = $('a.e20r-members-list-cancel');
    34             this.updateMemberLnk = $('a.e20r-update-member');
    35             this.exportBtn = $('a.e20r-memberslist-export');
    36             this.changed_input = $('input[class^="e20r-members-list-input-"]');
    37             this.changed_select = $( 'select[class^="e20r-members-list-select-"]');
    38             this.levels_dropdown = $('#e20r-pmpro-memberslist-levels');
    39             this.bulkUpdate = $('#doaction, #doaction2');
    40             this.updateListBtn = $('#e20r-update-list');
    41             this.search_field = $('#post-search-input');
    42             this.dateFields = $('.e20r-members-list-input-enddate, .e20r-members-list-input-startdate');
    43             this.dataSearchBtn = $('#e20r-memberslist-search-data');
    44             this.bulkActionSelectTop = $('select#bulk-action-selector-top');
    45             this.bulkActionSelectBottom = $('select#bulk-action-selector-bottom');
    46 
    47             this.dateOpts = {
    48                 year: '2-digit', month: 'short',
    49                 day: 'numeric'
    50             };
    51 
    52             let self = this;
    53 
    54             self.dateFields.datepicker({
    55                 dateFormat: "yy-mm-dd"
     25      init: function () {
     26        // this.levels_dropdown = $('select#e20r-pmpro-memberslist-levels');
     27        // this.enddate_lnk = $('a.e20r-members-list_enddate');
     28        // this.cancelMemberLnk = $('a.e20r-cancel-member');
     29        // this.updateBtn = $('a.e20r-members-list-save');
     30        this.memberslist_form = $('.e20r-pmpro-memberslist-page form#posts-filter');
     31        this.edit_lnk = $('a.e20r-members-list-editable');
     32        this.resetBtn = $('a.e20r-members-list-cancel');
     33        this.updateMemberLnk = $('a.e20r-update-member');
     34        this.exportBtn = $('a.e20r-memberslist-export');
     35        this.changed_input = $('input[class^="e20r-members-list-input-"]');
     36        this.changed_select = $( 'select[class^="e20r-members-list-select-"]');
     37        this.levels_dropdown = $('#e20r-pmpro-memberslist-levels');
     38        this.bulkUpdate = $('#doaction, #doaction2');
     39        this.updateListBtn = $('#e20r-update-list');
     40        this.search_field = $('#post-search-input');
     41        this.dateFields = $('.e20r-members-list-input-enddate, .e20r-members-list-input-startdate');
     42        this.dataSearchBtn = $('#e20r-memberslist-search-data');
     43        this.bulkActionSelectTop = $('select#bulk-action-selector-top');
     44        this.bulkActionSelectBottom = $('select#bulk-action-selector-bottom');
     45
     46        this.dateOpts = {
     47            year: '2-digit', month: 'short',
     48            day: 'numeric'
     49        };
     50
     51        let self = this;
     52
     53        self.dateFields.datepicker({
     54            dateFormat: "yy-mm-dd"
     55        });
     56
     57        self.search_field.unbind('keypress').on('keypress', function(event){
     58            let keycode = (event.key ? event.key : event.keyCode);
     59            if ('Enter' !== keycode) {
     60              return;
     61            }
     62            self.dataSearchBtn.click();
     63        });
     64
     65        // self.changed_input.unbind('blur').on('blur', function(ev) {
     66        self.changed_input.unbind('blur').on('blur', function() {
     67            self.set_update( this );
     68        });
     69
     70      // self.updateListBtn.unbind('click').on('click', function(ev) {
     71        self.updateListBtn.unbind('click').on('click', function(ev) {
     72          $('#post-search-input').val(null);
     73
     74          if ( 'Clear Search' === self.updateListBtn.val() ) {
     75              ev.preventDefault();
     76              window.console.log("We're clearing the search...");
     77              window.console.log(e20rml.url); //jshint ignore:line
     78              window.location = e20rml.url; //jshint ignore:line
     79          }
     80        });
     81
     82        // Trigger search whenever the levels drop-down is changed
     83        self.levels_dropdown.unbind('change').on('change', function() {
     84            self.dataSearchBtn.click();
     85        });
     86
     87        /**
     88         * Search operation initiated (a few other things simulate clicking the button)
     89         */
     90        self.dataSearchBtn.unbind('click').on('click', function(event) {
     91          let $search_string = $( '#post-search-input' ).val();
     92          let $level_string  = $( '#e20r-pmpro-memberslist-levels' ).val();
     93          let $uri             = window.location.toString();
     94
     95          // If we have a string in the search box we'll append it to the URL
     96          if ($search_string) {
     97            event.preventDefault();
     98
     99            // URL Encode the search string and add or replace it for the URI
     100            $uri = self.add_to_uri( $uri, 'find', encodeURIComponent( $search_string ) );
     101            $uri = self.add_to_uri( $uri, 'level', encodeURIComponent( $level_string ) );
     102            window.console.log( 'New URI should be: ' + $uri );
     103
     104            // Now we trigger the search
     105            window.location = $uri;
     106
     107            // Clear the search field - Possible FIXME if user's do not want clearing the search field to happen
     108            // $( '#post-search-input' ).val( null );
     109          }
     110        });
     111
     112        self.changed_select.unbind('change').on('change', function() {
     113          let current_select = $(this);
     114          let current_select_info = current_select.val();
     115          let $field_name = current_select.closest('div.ml-row-settings').find('.e20r-members-list-field-name').val();
     116          let previous_select_info = current_select.closest('div.ml-row-settings').find('input.e20r-members-list-db_' + $field_name ).val();
     117          let enddate = $('#the-list').find('.last .e20r-members-list-db-enddate').val();
     118
     119          window.console.log("Previous value: " + previous_select_info );
     120          window.console.log("New value: " + current_select_info );
     121
     122          if ( previous_select_info !== current_select_info && '' !== enddate ) {
     123              if ( ! window.confirm(e20rml.lang.clearing_enddate) ) { //jshint ignore:line
     124                  return false;
     125              }
     126          }
     127
     128          self.set_update( this );
     129        });
     130
     131        self.bulkUpdate.unbind('click').on('click', function (ev) {
     132
     133            ev.preventDefault();
     134
     135            if ('bulk-export' === self.bulkActionSelectTop.val() || 'bulk-export' === self.bulkActionSelectBottom.val()) {
     136                self.export_data(self);
     137                return true;
     138            }
     139
     140            self.memberslist_form.submit();
     141        });
     142
     143        self.updateMemberLnk.unbind('click').on('click', function (ev) {
     144
     145            ev.preventDefault();
     146            let btn = $(this);
     147            let row = btn.closest('tr');
     148
     149            let membership_col = row.find('td.column-membership');
     150            let membership_settings = membership_col.find('div.ml-row-settings');
     151            let membership_label = membership_col.find('a.e20r-members-list-editable');
     152
     153            let startdate_col = row.find('td.column-startdate');
     154            let startdate_settings = startdate_col.find('div.ml-row-settings');
     155            let startdate_label = startdate_col.find('a.e20r-members-list-editable');
     156
     157            let enddate_col = row.find('td.column-last');
     158            let enddate_settings = enddate_col.find('div.ml-row-settings');
     159            let enddate_label = enddate_col.find('a.e20r-members-list-editable');
     160
     161            let status_col = row.find( 'td.column-status');
     162            let status_settings = status_col.find('div.ml-row-settings');
     163            let status_label = status_col.find('a.e20r-members-list-editable');
     164
     165            membership_label.toggle();
     166            membership_settings.toggle();
     167
     168            status_label.toggle();
     169            status_settings.toggle();
     170
     171            $('.column-joindate').each(function () {
     172                $(this).toggle();
    56173            });
    57174
    58             self.search_field.unbind('keypress').on('keypress', function(event){
    59                 let keycode = (event.key ? event.key : event.keyCode);
    60                 if ('Enter' !== keycode) {
    61                     return;
    62                 }
    63 
    64                 self.dataSearchBtn.click();
    65             });
    66 
    67             // self.changed_input.unbind('blur').on('blur', function(ev) {
    68             self.changed_input.unbind('blur').on('blur', function() {
    69                 self.set_update( this );
    70             });
    71 
    72             // self.updateListBtn.unbind('click').on('click', function(ev) {
    73             self.updateListBtn.unbind('click').on('click', function(ev) {
    74                 $('#post-search-input').val(null);
    75 
    76                 if ( 'Clear Search' === self.updateListBtn.val() ) {
    77                     ev.preventDefault();
    78                     window.console.log("We're clearing the search...");
    79                     window.console.log(e20rml.url); //jshint ignore:line
    80                     window.location = e20rml.url; //jshint ignore:line
    81                 }
    82             });
    83 
    84             // Trigger search whenever the levels drop-down is changed
    85             self.levels_dropdown.unbind('change').on('change', function() {
    86                 self.dataSearchBtn.click();
    87             });
    88 
    89             /**
    90              * Search operation initiated (a few other things simulate clicking the button)
    91              */
    92 
    93             self.dataSearchBtn.unbind('click').on('click', function(event) {
    94 
    95                 let $search_string = $( '#post-search-input' ).val();
    96                 let $level_string  = $( '#e20r-pmpro-memberslist-levels' ).val();
    97                 let $uri           = window.location.toString();
    98 
    99                 // If we have a string in the search box we'll append it to the URL
    100                 if ($search_string) {
    101                     event.preventDefault();
    102 
    103                     // URL Encode the search string and add or replace it for the URI
    104                     $uri = self.add_to_uri( $uri, 'find', encodeURIComponent( $search_string ) );
    105                     $uri = self.add_to_uri( $uri, 'level', encodeURIComponent( $level_string ) );
    106                     window.console.log( 'New URI should be: ' + $uri );
    107 
    108                     // Now we trigger the search
    109                     window.location = $uri;
    110 
    111                     // Clear the search field - Possible FIXME if user's do not want clearing the search field to happen
    112                     // $( '#post-search-input' ).val( null );
    113                 }
    114             });
    115 
    116             self.changed_select.unbind('change').on('change', function() {
    117 
    118                 let current_select = $(this);
    119                 let current_select_info = current_select.val();
    120                 let $field_name = current_select.closest('div.ml-row-settings').find('.e20r-members-list-field-name').val();
    121                 let previous_select_info = current_select.closest('div.ml-row-settings').find('input.e20r-members-list-db_' + $field_name ).val();
    122                 let enddate = $('#the-list').find('.last .e20r-members-list-db-enddate').val();
    123 
    124                 window.console.log("Previous value: " + previous_select_info );
    125                 window.console.log("New value: " + current_select_info );
    126 
    127                 if ( previous_select_info !== current_select_info && '' !== enddate ) {
    128                     if ( ! window.confirm(e20rml.lang.clearing_enddate) ) { //jshint ignore:line
    129                         return false;
    130                     }
    131                 }
    132 
    133                 self.set_update( this );
    134             });
    135 
    136             self.bulkUpdate.unbind('click').on('click', function (ev) {
    137 
    138                 ev.preventDefault();
    139 
    140                 if ('bulk-export' === self.bulkActionSelectTop.val() || 'bulk-export' === self.bulkActionSelectBottom.val()) {
    141                     self.export_data(self);
    142                     return true;
    143                 }
    144 
    145                 self.memberslist_form.submit();
    146             });
    147 
    148             self.updateMemberLnk.unbind('click').on('click', function (ev) {
    149 
    150                 ev.preventDefault();
    151                 let btn = $(this);
    152                 let row = btn.closest('tr');
    153 
    154                 let membership_col = row.find('td.column-membership');
    155                 let membership_settings = membership_col.find('div.ml-row-settings');
    156                 let membership_label = membership_col.find('a.e20r-members-list-editable');
    157 
    158                 let startdate_col = row.find('td.column-startdate');
    159                 let startdate_settings = startdate_col.find('div.ml-row-settings');
    160                 let startdate_label = startdate_col.find('a.e20r-members-list-editable');
    161 
    162                 let enddate_col = row.find('td.column-last');
    163                 let enddate_settings = enddate_col.find('div.ml-row-settings');
    164                 let enddate_label = enddate_col.find('a.e20r-members-list-editable');
    165 
    166                 let status_col = row.find( 'td.column-status');
    167                 let status_settings = status_col.find('div.ml-row-settings');
    168                 let status_label = status_col.find('a.e20r-members-list-editable');
    169 
    170                 membership_label.toggle();
    171                 membership_settings.toggle();
    172 
    173                 status_label.toggle();
    174                 status_settings.toggle();
    175 
    176                 $('.column-joindate').each(function () {
    177                     $(this).toggle();
    178                 });
    179 
    180                 startdate_label.toggle();
    181                 startdate_settings.toggle();
    182 
    183                 enddate_label.toggle();
    184                 enddate_settings.toggle();
    185             });
    186 
    187             // Process edit link
    188             self.edit_lnk.unbind('click').on('click', function ($event) {
    189 
    190                 $event.preventDefault();
    191 
    192                 let $edit = $(this);
    193                 let $input = $edit.next('div.ml-row-settings');
    194 
    195                 $input.toggle();
    196                 $edit.toggle();
    197 
    198             });
    199 
    200             /**
    201              * Cancel membership link handler
    202              */
    203             self.resetBtn.unbind('click').on('click', function (ev) {
    204 
    205                 ev.preventDefault();
    206 
    207                 let $btn = $(this);
    208                 let $settings = $btn.closest('div.ml-row-settings');
    209                 let $label = $settings.prev('a.e20r-members-list-editable');
    210                 let $field_name = $settings.find('.e20r-members-list-field-name').val();
    211 
    212                 // Return the select value (or input value) to its original value...
    213                 let $original = $settings.find('.e20r-members-list-db-' + $field_name ).val();
    214                 let $field_input_key = 'e20r-members-list-new_' + $field_name;
    215 
    216                 $settings.find('select[name^="' + $field_input_key + '"], input[name^="'+ $field_input_key + '"]').val( $original );
    217 
    218                 $settings.toggle();
    219                 $label.toggle();
    220             });
    221 
    222             self.exportBtn.unbind('click').on('click', function (ev) {
    223                 ev.preventDefault();
    224                 window.console.log("Export button clicked!");
    225 
    226                 self.export_data( self );
    227             });
    228         },
    229         add_to_uri: function(url, paramName, paramValue) {
    230 
    231             let pattern = new RegExp('\\b('+paramName+'=).*?(&|#|$)');
    232 
    233             if (paramValue === null) {
    234                 paramValue = '';
    235             }
    236 
    237             if (url.search(pattern)>=0) {
    238                 return url.replace(pattern,'$1' + paramValue + '$2');
    239             }
    240 
    241             url = url.replace(/[?#]$/,'');
    242             return url + (url.indexOf('?')>0 ? '&' : '?') + paramName + '=' + paramValue;
    243         },
    244         export_data: function( self ) {
    245 
    246             $("#overlay").fadeIn(300);
    247             let inputs = $('.e20r-search-arguments input, .e20r-search-arguments textarea, .e20r-search-arguments select')
    248                 .not(':input[type=button], :input[type=submit], input[type=reset]');
    249             let export_args = self.prepare_export(self, inputs);
    250 
    251             // Open a new tab to trigger the AJAX request for the download
    252             let file_name = window.prompt( 'Filename for data to save/export', 'members_list' );
    253             let extension = '.csv';
    254             if ( file_name.toLowerCase().endsWith( '.csv' ) ) {
    255                 extension = '';
    256             }
    257             self.download_csv(file_name + extension, export_args, ); //jshint ignore:line
    258         },
    259         /**
    260          * Set the request variables based on the members list page
    261          *
    262          * @param self Instance of this object
    263          * @param inputs The HTML inputs
    264          * @returns Object
    265          */
    266         prepare_export: function( self, inputs ) {
    267             let request_args = {
    268                 '_wpnonce':  $('#_wpnonce').val(),
    269                 'showDebugTrace': true,
    270                 'action': 'e20rml_export_records',
    271             };
    272             request_args.find = $( '#post-search-input' ).val();
    273             inputs.each(function () {
    274                 let input = $(this);
    275                 let name = input.attr('name');
    276                 let value = input.val();
    277                 if (false === self.is_empty(value)) {
    278                     window.console.log(name + " contains " + value);
    279                     request_args[name] = value;
    280                 }
    281             });
    282 
    283             let is_checked = false;
    284             let selected_ids = [];
    285             $('input[name^="member_user_id"]').each(function () {
    286                 if ($(this).is(':checked')) {
    287                     is_checked = true;
    288                     selected_ids.push($(this).val());
    289                 }
    290             });
    291             if (selected_ids.length > 0) {
    292                 request_args.member_id = selected_ids;
    293             }
    294             // return self.build_export_url( export_args );
    295             return request_args;
    296         },
    297         /**
    298          * Download the exported .CSV data
    299          * @param filename The file name to use for download
    300          * @param download_args The POST argument(s)
    301          */
    302         download_export: function(filename, download_args) {
    303             fetch(e20rml.ajax_url, download_args) // jshint ignore:line
    304                 .then(response => {
    305                     if (!response.ok) {
    306                         throw new Error('Request failed.' + response);
    307                     }
    308                 })
    309                 .then(response => response.blob())
    310                 .then(blob => {
    311                     const link = window.document.createElement("a");
    312                     link.href = URL.createObjectURL(blob);
    313                     link.download = filename;
    314                     link.click();
    315                 })
    316                 .catch(window.console.error);
    317         },
    318         download_csv: function(file_name, attributes) {
    319             let URL = e20rml.ajax_url; // jshint ignore:line
    320             window.console.log( 'AJAX URL: ' + URL);
    321             window.console.log( attributes );
    322             $.ajax({
    323                 url: URL,
    324                 cache: false,
    325                 method: 'POST',
    326                 data: attributes,
    327                 accepts: 'text/csv',
    328                 xhr: function () {
    329                     var xhr = new XMLHttpRequest();
    330                     xhr.onreadystatechange = function () {
    331                         if (xhr.readyState === 2) {
    332                             if (xhr.status === 200) {
    333                                 xhr.responseType = "blob";
    334                             } else {
    335                                 xhr.responseType = "text";
    336                             }
    337                         }
    338                     };
    339                     return xhr;
    340                 },
    341                 success: function (data) {
    342                     //Convert the Byte Data to BLOB object.
    343                     let blob = new Blob([data], { type: "text/csv" });
    344 
    345                     //Check the Browser type and download the File.
    346                     let isIE = !!document.documentMode;
    347                     if (isIE) {
    348                         window.navigator.msSaveBlob(blob, file_name);
    349                     } else {
    350                         let url = window.URL || window.webkitURL;
    351                         let link = url.createObjectURL(blob);
    352                         let body = $('body');
    353                         let a = $("<a />");
    354                         a.attr("download", file_name);
    355                         a.attr("href", link);
    356                         body.append(a);
    357                         a[0].click();
    358                         body.remove(a);
    359                     }
    360                 },
    361                 error: function (jqXHR, textStatus, errorThrown) {
    362                     window.console.log( 'Status: ' + textStatus + ', Error thrown: ' + errorThrown );
    363                 }
    364             }).done(function() {
    365                 setTimeout(function(){
    366                     $("#overlay").fadeOut(300);
    367                 },500);
    368             });
    369         },
    370         set_update: function( $element ) {
    371             let self = this;
    372 
    373             let element = $($element);
    374             let $settings = element.closest('div.ml-row-settings');
    375             // let users_id = $settings.find('input.e20r-members-list-user-id').val();
    376             let field_name = $settings.find('input.e20r-members-list-field-name').val();
    377             let $label = $settings.prev('a.e20r-members-list-' + field_name + '-label');
    378             // let $new_value_field = $settings.find('input.e20r-members-list-db-' + field_name);
    379             let select = $settings.find('.e20r-members-list-select-' + field_name);
    380 
    381             let $checkbox = element.closest('tr').find('th.check-column input[type="checkbox"]');
    382 
    383             let $date = $settings.find('.e20r-members-list-input-' + field_name).val();
    384             let select_val = select.val();
    385             let test_checked = false;
    386 
    387             window.console.log("Value: ", $date, select_val);
    388 
    389             if ('undefined' !== select_val) {
    390 
    391                 $label.text(select.find('option:selected').text());
    392                 test_checked = true;
    393             }
    394 
    395             if (null !== $date) {
    396 
    397                 let date = new Date($date);
    398 
    399                 window.console.log("Date info? ", date);
    400 
    401                 if (Object.prototype.toString.call(date) === "[object Date]") {
    402                     $label.text(date.toLocaleDateString(e20rml.locale, self.dateOpts)); //jshint ignore:line
    403                 }
    404 
    405                 test_checked = true;
    406             }
    407 
    408             if (true === test_checked && false === $checkbox.is('checked')) {
    409                 window.console.log("Checkbox not checked. Fixing!");
    410                 $checkbox.prop('checked', true);
    411 
    412                 self.bulkActionSelectBottom.val('bulk-update');
    413                 self.bulkActionSelectTop.val('bulk-update');
    414                 $('#doaction').val(e20rml.lang.save_btn_text); //jshint ignore:line
    415                 $('#doaction2').val(e20rml.lang.save_btn_text); //jshint ignore:line
    416             }
    417             /*
     175            startdate_label.toggle();
     176            startdate_settings.toggle();
     177
     178            enddate_label.toggle();
     179            enddate_settings.toggle();
     180        });
     181
     182        // Process edit link
     183        self.edit_lnk.unbind('click').on('click', function ($event) {
     184
     185            $event.preventDefault();
     186
     187            let $edit = $(this);
     188            let $input = $edit.next('div.ml-row-settings');
     189
     190            $input.toggle();
     191            $edit.toggle();
     192
     193        });
     194
     195        /**
     196         * Cancel membership link handler
     197         */
     198        self.resetBtn.unbind('click').on('click', function (ev) {
     199
     200            ev.preventDefault();
     201
     202            let $btn = $(this);
     203            let $settings = $btn.closest('div.ml-row-settings');
     204            let $label = $settings.prev('a.e20r-members-list-editable');
     205            let $field_name = $settings.find('.e20r-members-list-field-name').val();
     206
     207            // Return the select value (or input value) to its original value...
     208            let $original = $settings.find('.e20r-members-list-db-' + $field_name ).val();
     209            let $field_input_key = 'e20r-members-list-new_' + $field_name;
     210
     211            $settings.find('select[name^="' + $field_input_key + '"], input[name^="'+ $field_input_key + '"]').val( $original );
     212
    418213            $settings.toggle();
    419214            $label.toggle();
    420             */
    421         },
    422         /**
    423          * Build the URL used to export the data
    424          *
    425          * @param export_attrs The attributes to use for the AJAX URL (export)
    426          */
    427         build_export_url: function (export_attrs) {
    428             window.console.log("About to transmit: ", export_attrs);
    429             let export_arguments = '';
    430             Object.keys(export_attrs).forEach(function(key){
    431                 let value = export_attrs[key];
    432                 if ( value instanceof Array) {
    433                     value = value.join(',');
    434                 }
    435                 export_arguments = export_arguments + key + '=' + encodeURI( value ) + '&';
    436             });
    437 
    438             let location = e20rml.ajax_url + '?' + export_arguments; // jshint ignore:line
    439             window.console.log( location );
    440             return location;
    441         },
    442         is_empty: function (data) {
    443 
    444             if (typeof(data) === 'number' || typeof(data) === 'boolean') {
    445                 return false;
     215        });
     216
     217        self.exportBtn.unbind('click').on('click', function (ev) {
     218            ev.preventDefault();
     219            window.console.log("Export button clicked!");
     220
     221            self.export_data( self );
     222        });
     223      },
     224          add_to_uri: function(url, paramName, paramValue) {
     225
     226        let pattern = new RegExp('\\b('+paramName+'=).*?(&|#|$)');
     227
     228        if (paramValue === null) {
     229          paramValue = '';
     230        }
     231
     232        if (url.search(pattern)>=0) {
     233          return url.replace(pattern,'$1' + paramValue + '$2');
     234        }
     235
     236        url = url.replace(/[?#]$/,'');
     237        return url + (url.indexOf('?')>0 ? '&' : '?') + paramName + '=' + paramValue;
     238          },
     239          export_data: function( self ) {
     240
     241        $("#overlay").fadeIn(300);
     242        let inputs = $('.e20r-search-arguments input, .e20r-search-arguments textarea, .e20r-search-arguments select')
     243          .not(':input[type=button], :input[type=submit], input[type=reset]');
     244        let export_args = self.prepare_export(self, inputs);
     245
     246        // Open a new tab to trigger the AJAX request for the download
     247        let file_name = window.prompt( 'Filename for data to save/export', 'members_list' );
     248        let extension = '.csv';
     249        if ( file_name.toLowerCase().endsWith( '.csv' ) ) {
     250          extension = '';
     251        }
     252        self.download_csv(file_name + extension, export_args, ); //jshint ignore:line
     253          },
     254      /**
     255       * Set the request variables based on the members list page
     256       *
     257       * @param self Instance of this object
     258       * @param inputs The HTML inputs
     259       * @returns Object
     260       */
     261      prepare_export: function( self, inputs ) {
     262        let request_args = {
     263          '_wpnonce':  $('#_wpnonce').val(),
     264          'showDebugTrace': true,
     265          'action': 'e20rml_export_records',
     266        };
     267        request_args.find = $( '#post-search-input' ).val();
     268        inputs.each(function () {
     269          let input = $(this);
     270          let name = input.attr('name');
     271          let value = input.val();
     272          if (false === self.is_empty(value)) {
     273            window.console.log(name + " contains " + value);
     274            request_args[name] = value;
     275          }
     276        });
     277
     278        let is_checked = false;
     279        let selected_ids = [];
     280        $('input[name^="member_user_id"]').each(function () {
     281          if ($(this).is(':checked')) {
     282            is_checked = true;
     283            selected_ids.push($(this).val());
     284          }
     285        });
     286        if (selected_ids.length > 0) {
     287          request_args.member_user_ids = selected_ids;
     288        }
     289        // return self.build_export_url( export_args );
     290        return request_args;
     291      },
     292      /**
     293       * Download the exported .CSV data
     294       *
     295       * @param file_name The file name to use for download
     296       * @param attributes The POST argument(s)
     297       */
     298      download_csv: function(file_name, attributes) {
     299        let URL = e20rml.ajax_url; // jshint ignore:line
     300        $.ajax({
     301          url: URL,
     302          cache: false,
     303          method: 'POST',
     304          data: attributes,
     305          accepts: 'text/csv',
     306          xhr: function () {
     307            var xhr = new XMLHttpRequest();
     308            xhr.onreadystatechange = function () {
     309              if (xhr.readyState === 2) {
     310                if (xhr.status === 200) {
     311                  xhr.responseType = "blob";
     312                } else {
     313                  xhr.responseType = "text";
     314                }
     315              }
     316            };
     317            return xhr;
     318          },
     319          success: function (data) {
     320            //Convert the Byte Data to BLOB object.
     321            let blob = new Blob([data], { type: "text/csv" });
     322
     323            //Check the Browser type and download the File.
     324            let isIE = !!document.documentMode;
     325            if (isIE) {
     326              window.navigator.msSaveBlob(blob, file_name);
     327            } else {
     328              let url = window.URL || window.webkitURL;
     329              let link = url.createObjectURL(blob);
     330              let body = $('body');
     331              let a = $("<a />");
     332              a.attr("download", file_name);
     333              a.attr("href", link);
     334              body.append(a);
     335              a[0].click();
     336              body.remove(a);
    446337            }
    447 
    448             if (typeof(data) === 'undefined' || data === null) {
    449                 return true;
    450             }
    451 
    452             if (typeof(data.length) !== 'undefined') {
    453                 return data.length === 0;
    454             }
    455 
    456             let count = 0;
    457 
    458             for (let i in data) {
    459 
    460                 if (data.hasOwnProperty(i)) {
    461                     count++;
    462                 }
    463             }
    464 
    465             return count === 0;
    466         }
     338          },
     339          error: function (jqXHR, textStatus, errorThrown) {
     340            window.console.log( 'Status: ' + textStatus + ', Error thrown: ' + errorThrown );
     341          }
     342        }).done(function() {
     343          setTimeout(function(){
     344            $("#overlay").fadeOut(300);
     345          },500);
     346        });
     347      },
     348      set_update: function( $element ) {
     349        let self = this;
     350        let element = $($element);
     351        let $settings = element.closest('div.ml-row-settings');
     352        // let users_id = $settings.find('input.e20r-members-list-user-id').val();
     353        let field_name = $settings.find('input.e20r-members-list-field-name').val();
     354        let $label = $settings.prev('a.e20r-members-list-' + field_name + '-label');
     355        // let $new_value_field = $settings.find('input.e20r-members-list-db-' + field_name);
     356        let select = $settings.find('.e20r-members-list-select-' + field_name);
     357        let $checkbox = element.closest('tr').find('th.check-column input[type="checkbox"]');
     358        let $date = $settings.find('.e20r-members-list-input-' + field_name).val();
     359        let select_val = select.val();
     360        let test_checked = false;
     361
     362        window.console.log("Value: ", $date, select_val);
     363
     364        if ('undefined' !== select_val) {
     365          $label.text(select.find('option:selected').text());
     366          test_checked = true;
     367        }
     368
     369        if (null !== $date) {
     370          let date = new Date($date);
     371          window.console.log("Date info? ", date);
     372          if (Object.prototype.toString.call(date) === "[object Date]") {
     373              $label.text(date.toLocaleDateString(e20rml.locale, self.dateOpts)); //jshint ignore:line
     374          }
     375          test_checked = true;
     376        }
     377
     378        if (true === test_checked && false === $checkbox.is('checked')) {
     379          window.console.log("Checkbox not checked. Fixing!");
     380          $checkbox.prop('checked', true);
     381
     382          self.bulkActionSelectBottom.val('bulk-update');
     383          self.bulkActionSelectTop.val('bulk-update');
     384          $('#doaction').val(e20rml.lang.save_btn_text); //jshint ignore:line
     385          $('#doaction2').val(e20rml.lang.save_btn_text); //jshint ignore:line
     386        }
     387        /*
     388        $settings.toggle();
     389        $label.toggle();
     390        */
     391      },
     392      /**
     393       * Build the URL used to export the data
     394       *
     395       * @param export_attrs The attributes to use for the AJAX URL (export)
     396       */
     397      build_export_url: function (export_attrs) {
     398              window.console.log("About to transmit: ", export_attrs);
     399        let export_arguments = '';
     400        Object.keys(export_attrs).forEach(function(key){
     401          let value = export_attrs[key];
     402          if ( value instanceof Array) {
     403            value = value.join(',');
     404          }
     405          export_arguments = export_arguments + key + '=' + encodeURI( value ) + '&';
     406        });
     407
     408        let location = e20rml.ajax_url + '?' + export_arguments; // jshint ignore:line
     409        window.console.log( location );
     410        return location;
     411          },
     412      is_empty: function (data) {
     413
     414          if (typeof(data) === 'number' || typeof(data) === 'boolean') {
     415              return false;
     416          }
     417
     418          if (typeof(data) === 'undefined' || data === null) {
     419              return true;
     420          }
     421
     422          if (typeof(data.length) !== 'undefined') {
     423              return data.length === 0;
     424          }
     425
     426          let count = 0;
     427
     428          for (let i in data) {
     429
     430              if (data.hasOwnProperty(i)) {
     431                  count++;
     432              }
     433          }
     434
     435          return count === 0;
     436      }
    467437    };
    468438
     
    470440        e20rMembersList_Page.init();
    471441    });
    472 
    473442})(jQuery);
  • e20r-members-list/trunk/src/E20R/members-list/modules/Multiple_Memberships.php

    r2672951 r2700272  
    2222namespace E20R\Members_List\Admin\Modules;
    2323
     24use E20R\Members_List\Members_List;
     25
    2426if ( ! defined( 'ABSPATH' ) && ! defined( 'PLUGIN_PHPUNIT' ) ) {
    2527    die( 'WordPress not loaded. Naughty, naughty!' );
    2628}
    2729
    28 if ( ! class_exists( '\\E20R\\Members_List\\Admin\\Modules\\Multiple_Memberships' ) ) {
     30if ( ! class_exists( 'E20R\Members_List\Admin\Modules\Multiple_Memberships' ) ) {
    2931    /**
    3032     * Support for PHP's Multiple Memberships plugin
     
    6062         * @return string
    6163         */
    62         public function column_name( $item ) {
     64        public function multiple_membership_column( $item ) {
    6365
    64             // FIXME: Update this to support MMPU
    6566            // FIXME: Allow adding more levels and changing the "primary" level.
     67            if ( ! function_exists( 'pmpro_getMembershipLevelsForUser' ) ) {
     68                return '';
     69            }
     70
     71            $level_list    = pmpro_getMembershipLevelsForUser( $item['ID'] );
     72            $current_level = isset( $item['membership_id'] ) ? (int) $item['membership_id'] : null;
     73            $level_ids     = array();
     74            $level_names   = array();
     75
     76            foreach ( $level_list as $level ) {
     77                $level_ids[] = $level->id;
     78
     79                // Generate HTML for the membership levels the user is assigned to
     80                $level_names[] = ( (int) $level->id === $current_level ?
     81                    sprintf(
     82                        '<span class="e20r-members-list-level-name e20r-members-list-primary-level">%1$s (%2$s)</span>',
     83                        esc_attr( $level->name ),
     84                        esc_attr__( 'primary', 'e20r-members-list' )
     85                    ) :
     86                    sprintf( '<span class="e20r-members-list-level-name">%1$s</span>', $level->name )
     87                );
     88            }
    6689
    6790            // These are used to configure the membership level with JavaScript.
     
    7598                <input type="hidden" value="%5$d" class="e20r-members-list-db_record_id" name="e20r-members-list-db_record_id_%2$s" />
    7699                <input type="hidden" value="%4$s" class="e20r-members-list-field-name" name="e20r-members-list-field_name_%2$s" />',
    77                 $item['membership_id'],
    78                 $item['ID'],
    79                 $item['name'],
     100                (int) $item['membership_id'],
     101                (int) $item['ID'],
     102                esc_attr( $item['name'] ),
    80103                'membership_id',
    81                 $item['record_id'],
    82                 $item['membership_level_ids']
     104                (int) $item['record_id'],
     105                empty( $item['membership_level_ids'] ) ?
     106                    implode( ',', $level_ids ) :
     107                    esc_attr( $item['membership_level_ids'] )
    83108            );
    84109
    85             $options = '';
    86             if ( function_exists( 'pmpro_getAllLevels' ) ) {
    87                 $levels = pmpro_getAllLevels( true, true );
    88             } else {
     110            $options = Members_List::build_option_string( $item['membership_id'] );
    89111
    90                 // Default info if PMPro is disabled.
    91                 $null_level           = new \stdClass();
    92                 $null_level->level_id = 0;
    93                 $null_level->name     = esc_attr__( 'No levels found. Paid Memberships Pro is inactive!', 'e20r-members-list' );
    94                 $levels               = array( $null_level );
    95             }
    96 
    97             foreach ( $levels as $level ) {
    98                 $options .= sprintf(
    99                     '<option value="%1$s" %2$s>%3$s</option>',
    100                     $level->id,
    101                     selected( $level->id, $item['membership_id'], false ),
    102                     $level->name
    103                 ) . "\n";
    104             }
    105             $new_membershiplevel_input = sprintf(
     112            $new_level_input = sprintf(
    106113                '<div class="ml-row-settings clearfix">
    107114                        %1$s
     
    113120                    </div>',
    114121                $membership_input,
    115                 $item['ID'],
     122                (int) $item['ID'],
    116123                $options,
    117124                esc_attr__( 'Reset', 'e20r-members-list' )
    118125            );
    119126
    120             $value = sprintf(
     127            $level_info = sprintf(
     128                '<div class="e20r-members-list-level-names">%1$s</div>',
     129                implode( '<br class="e20r-members-list-level-name-spacer" />', $level_names )
     130            );
     131            return sprintf(
    121132                '<a href="#" class="e20r-members-list_membership_id e20r-members-list-editable" title="%1$s">%2$s<span class="dashicons dashicons-edit"></a>%3$s',
    122                 esc_attr__( 'Click to edit membership level', 'e20rapp' ),
    123                 $item['name'],
    124                 $new_membershiplevel_input
     133                esc_attr__( 'Click to edit primary membership level', 'e20r-members-list' ),
     134                $new_level_input,
     135                $level_info
    125136            );
    126 
    127             return '';
    128137        }
    129138    }
Note: See TracChangeset for help on using the changeset viewer.