diff --git a/plugins/hc-suggestions/.distignore b/plugins/hc-suggestions/.distignore deleted file mode 100644 index 1649ea423..000000000 --- a/plugins/hc-suggestions/.distignore +++ /dev/null @@ -1,30 +0,0 @@ -# A set of files you probably don't want in your WordPress.org distribution -.distignore -.editorconfig -.git -.gitignore -.gitlab-ci.yml -.travis.yml -.DS_Store -Thumbs.db -behat.yml -bin -circle.yml -composer.json -composer.lock -Gruntfile.js -package.json -phpunit.xml -phpunit.xml.dist -multisite.xml -multisite.xml.dist -phpcs.xml -phpcs.xml.dist -README.md -wp-cli.local.yml -tests -vendor -node_modules -*.sql -*.tar.gz -*.zip diff --git a/plugins/hc-suggestions/.editorconfig b/plugins/hc-suggestions/.editorconfig deleted file mode 100644 index 79207a40c..000000000 --- a/plugins/hc-suggestions/.editorconfig +++ /dev/null @@ -1,22 +0,0 @@ -# This file is for unifying the coding style for different editors and IDEs -# editorconfig.org - -# WordPress Coding Standards -# https://make.wordpress.org/core/handbook/coding-standards/ - -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true -indent_style = tab -indent_size = 4 - -[{.jshintrc,*.json,*.yml}] -indent_style = space -indent_size = 2 - -[{*.txt,wp-config-sample.php}] -end_of_line = crlf diff --git a/plugins/hc-suggestions/.gitignore b/plugins/hc-suggestions/.gitignore deleted file mode 100644 index 0e2b791dd..000000000 --- a/plugins/hc-suggestions/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -.DS_Store -Thumbs.db -wp-cli.local.yml -node_modules/ -*.sql -*.tar.gz -*.zip -vendor/ -composer.lock diff --git a/plugins/hc-suggestions/.travis.yml b/plugins/hc-suggestions/.travis.yml deleted file mode 100644 index de1aebe64..000000000 --- a/plugins/hc-suggestions/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: php -php: -- 7.0 -- 7.1 -- 7.2 -env: -- WP_VERSION=4.9 -- WP_VERSION=latest -before_script: -- composer global require "phpunit/phpunit:6.*" -- composer global require wp-coding-standards/wpcs -- export PATH="$HOME/.composer/vendor/bin:$PATH" -- phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs -- bash bin/install-wp-tests.sh $WP_VERSION -script: -- phpcs -- phpunit diff --git a/plugins/hc-suggestions/Gruntfile.js b/plugins/hc-suggestions/Gruntfile.js deleted file mode 100644 index 6f4bc941d..000000000 --- a/plugins/hc-suggestions/Gruntfile.js +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = function( grunt ) { - - 'use strict'; - var banner = '/**\n * <%= pkg.homepage %>\n * Copyright (c) <%= grunt.template.today("yyyy") %>\n * This file is generated automatically. Do not edit.\n */\n'; - // Project configuration - grunt.initConfig( { - - pkg: grunt.file.readJSON( 'package.json' ), - - addtextdomain: { - options: { - textdomain: 'hc-suggestions', - }, - update_all_domains: { - options: { - updateDomains: true - }, - src: [ '*.php', '**/*.php', '!node_modules/**', '!php-tests/**', '!bin/**' ] - } - }, - - wp_readme_to_markdown: { - your_target: { - files: { - 'README.md': 'readme.txt' - } - }, - }, - - makepot: { - target: { - options: { - domainPath: '/languages', - mainFile: 'hc-suggestions.php', - potFilename: 'hc-suggestions.pot', - potHeaders: { - poedit: true, - 'x-poedit-keywordslist': true - }, - type: 'wp-plugin', - updateTimestamp: true - } - } - }, - } ); - - grunt.loadNpmTasks( 'grunt-wp-i18n' ); - grunt.loadNpmTasks( 'grunt-wp-readme-to-markdown' ); - grunt.registerTask( 'i18n', ['addtextdomain', 'makepot'] ); - grunt.registerTask( 'readme', ['wp_readme_to_markdown'] ); - - grunt.util.linefeed = '\n'; - -}; diff --git a/plugins/hc-suggestions/README.md b/plugins/hc-suggestions/README.md deleted file mode 100644 index d6894ce72..000000000 --- a/plugins/hc-suggestions/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# HC Suggestions - -[![Build Status](https://travis-ci.org/mlaa/hc-suggestions.svg)](https://travis-ci.org/mlaa/hc-suggestions) - -Widget to suggest relevant content to [Humanities Commons](https://hcommons.org) members based on their academic interests. diff --git a/plugins/hc-suggestions/bin/install-wp-tests.sh b/plugins/hc-suggestions/bin/install-wp-tests.sh deleted file mode 100755 index 90c5a0023..000000000 --- a/plugins/hc-suggestions/bin/install-wp-tests.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -# WordPress >= 4.0 -WP_VERSION=${1-latest} -# BuddyPress >= 2.1 -BP_VERSION=${2-latest} - -WP_DIR=/tmp/wordpress -BP_DIR=/tmp/buddypress -WP_SVN=https://develop.svn.wordpress.org -BP_SVN=https://buddypress.svn.wordpress.org -MYSQLI_PLUGIN_URL=https://raw.github.com/markoheijnen/wp-mysqli/master/db.php - -# Don't change these values; they are for the test database and are hard-coded -# into wp-tests-config.php. This assumes the database is running on localhost -# and the user running this script can connect to MySQL without a password. -DB_NAME=youremptytestdbnamehere -DB_USER=yourusernamehere -DB_PASS=yourpasswordhere -DB_HOST="127.0.0.1" - -# Set SVN paths. -if [ "$WP_VERSION" = "latest" ]; then - WP_DIR="$WP_DIR/latest" - WP_SVN="$WP_SVN/trunk" -else - WP_DIR="$WP_DIR/$WP_VERSION" - WP_SVN="$WP_SVN/tags/$WP_VERSION" -fi -if [ "$BP_VERSION" = "latest" ]; then - BP_DIR="$BP_DIR/latest" - BP_SVN="$BP_SVN/trunk" -else - BP_DIR="$BP_DIR/$BP_VERSION" - BP_SVN="$BP_SVN/tags/$BP_VERSION" -fi - -# Create directories. -mkdir -p $WP_DIR $BP_DIR - -# Install WordPress and test suite. Replace ABSPATH definition with a constant -# that we will supply in our bootstrap.php. -if [ ! -f $WP_DIR/src/wp-content/db.php ]; then - svn co --quiet $WP_SVN $WP_DIR - wget -nv -O $WP_DIR/src/wp-content/db.php $MYSQLI_PLUGIN_URL -fi -sed -e "s:'ABSPATH', *dirname( __FILE__ ) . '/src/':'ABSPATH', getenv('WP_ABSPATH'):" -e "s:localhost:127.0.0.1:" $WP_DIR/wp-tests-config-sample.php > $WP_DIR/wp-tests-config.php - -# Install BuddyPress and test suite. -if [ ! -d $BP_DIR/src ]; then - svn co --quiet $BP_SVN $BP_DIR -fi - -# Create WP database. -mysql -u root << EOF -create database IF NOT EXISTS $DB_NAME; -grant usage on $DB_NAME.* to $DB_USER@$DB_HOST identified by "$DB_PASS"; -grant all privileges on $DB_NAME.* to $DB_USER@$DB_HOST; -EOF diff --git a/plugins/hc-suggestions/classes/class-hc-suggestions-rest-controller.php b/plugins/hc-suggestions/classes/class-hc-suggestions-rest-controller.php deleted file mode 100644 index 2269a4754..000000000 --- a/plugins/hc-suggestions/classes/class-hc-suggestions-rest-controller.php +++ /dev/null @@ -1,287 +0,0 @@ -namespace = 'hc-suggestions/v1'; - } - - /** - * Registers the routes for the objects of the controller - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/query', - [ - 'methods' => 'GET', - 'callback' => [ $this, 'query' ], - ] - ); - register_rest_route( - $this->namespace, - '/hide', - [ - 'methods' => 'POST', - 'callback' => [ $this, 'hide' ], - ] - ); - } - - /** - * Hide a post from appearing in suggestions for the current user - * - * @param WP_REST_Request $data request data. Expected to contain "post_id" & "post_type" params. - * @return WP_REST_Response - */ - public function hide( WP_REST_Request $data ) { - /** - * The global $current_user isn't populated here, have to do it ourselves. - * This won't work without shibd setting this header. - */ - wp_set_current_user( get_user_by( 'login', $_SERVER['HTTP_EMPLOYEENUMBER'] ) ); - - $params = $data->get_query_params(); - - $user_hidden_posts = $this->_get_user_hidden_posts(); - - $user_hidden_posts[ $params['post_type'] ] = array_unique( - array_merge( - isset( $user_hidden_posts[ $params['post_type'] ] ) ? $user_hidden_posts[ $params['post_type'] ] : [], - [ $params['post_id'] ] - ) - ); - - // Note $result will be false if $user_hidden_posts is the same as the old value. - $result = update_user_meta( get_current_user_id(), self::META_KEY_USER_HIDDEN_POSTS, $user_hidden_posts ); - - $response = new WP_REST_Response; - - $response->set_status( $result ? 200 : 500 ); - - return $response; - } - - /** - * Helper function to define a WP_Query from WP_REST_Request params. - * - * @param array $params WP_REST_Request params. - * @return WP_Query Query from request params. - */ - protected function get_wp_query( array $params ) { - global $wpdb; - - /** - * $_REQUEST param names are hardcoded to be parsed by elasticpress-buddypress, - * (and possibly elsewhere) so the names must match here - */ - $wp_query_params = [ - 'ep_integrate' => true, - 'post_type' => $params['post_type'], - 's' => $params['s'], - 'paged' => isset( $params['paged'] ) ? $params['paged'] : 1, - ]; - - if ( is_user_logged_in() ) { - switch ( $params['post_type'] ) { - case 'user': - // Exclude self. - $exclude_user_ids = [ get_current_user_id() ]; - - // Exclude users already being followed by the current user. - $exclude_user_ids = array_merge( - $exclude_user_ids, - (array) bp_follow_get_following( - [ - 'user_id' => get_current_user_id(), - ] - ) - ); - - $wp_query_params['post__not_in'] = array_unique( $exclude_user_ids ); - break; - case 'bp_group': - // Exclude groups already joined by the current user. - $exclude_group_ids = array_keys( - bp_get_user_groups( - get_current_user_id(), - [ - 'is_admin' => null, - 'is_mod' => null, - ] - ) - ); - - // Exclude groups on society networks the current user does not belong to. - $current_user_memberships = (array) bp_get_member_type( get_current_user_id(), false ); - $non_member_society_groups = groups_get_groups( - [ - 'group_type__not_in' => $current_user_memberships, - 'per_page' => 999, // TODO This won't scale well. - ] - ); - foreach ( $non_member_society_groups['groups'] as $group ) { - $exclude_group_ids[] = $group->id; - } - - // Exclude private groups. - // TODO should do this here, but there's no 'status' param to groups_get_groups until bp 2.9. - // For now, check in the loop below and just exclude there. - $wp_query_params['post__not_in'] = array_unique( $exclude_group_ids ); - break; - case 'humcore_deposit': - /** - * Exclude posts authored by the current user. - * There's a bug, possibly in elasticpress-buddypress, that breaks 'author__not_in'. - * This is a workaround to avoid using that param. - */ - $sql = []; - foreach ( get_networks() as $network ) { - switch_to_blog( $network->blog_id ); - $sql[] = sprintf( - "SELECT ID FROM %s %s", - $wpdb->posts, - get_posts_by_author_sql( 'humcore_deposit', true, get_current_user_id() ) - ); - restore_current_blog(); - } - $author_post_ids = $wpdb->get_col( implode( ' UNION ', $sql ) ); - - $wp_query_params['post__not_in'] = array_unique( $author_post_ids ); - break; - default: - break; - } - - // Exclude user-hidden posts. - $user_hidden_posts = $this->_get_user_hidden_posts(); - if ( isset( $user_hidden_posts[ $params['post_type'] ] ) ) { - $existing_post__not_in = isset( $wp_query_params['post__not_in'] ) ? $wp_query_params['post__not_in'] : []; - - $wp_query_params['post__not_in'] = array_unique( - array_merge( - $existing_post__not_in, - $user_hidden_posts[ $params['post_type'] ] - ) - ); - } - } - - /** - * By default, ElasticPress converts the 's' search parameter into a - * phrase-type ElasticSearch query. The recommendations widget passes a - * user's academic interests as a concactinated string, so we need to do - * a normal match search. - * - * @see https://www.elasticpress.io/blog/2019/02/custom-search-with-elasticpress-how-to-limit-results-to-full-text-matches/ - * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/match-multi-word.html - */ - add_filter( 'ep_formatted_args', function( $formatted_args, $args ) { - if ( ! empty( $formatted_args['query']['bool']['should'] && - is_array( $formatted_args['query']['bool']['should'] ) ) ) { - foreach ( $formatted_args['query']['bool']['should'] as &$es_search ) { - unset( $es_search['multi_match']['type'] ); - $es_search['multi_match']['operator'] = 'or'; - } - } - return $formatted_args; - }, 10, 2 ); - - $query = new WP_Query( $wp_query_params ); - return $query; - } - - /** - * Query ElasticPress for relevant content - * - * @param WP_REST_Request $data request data. Expected to contain "s" & "post_type" params. - * @return WP_REST_Response - */ - public function query( WP_REST_Request $data ) { - $response_data = []; - $params = $data->get_query_params(); - - $hcs_query = $this->get_wp_query( $params ); - - while ( $hcs_query->have_posts() ) { - $hcs_query->the_post(); - - // TODO once BP is upgraded to 2.9, move this to the switch above. - if ( 'bp_group' === $params['post_type'] ) { - $group = groups_get_group( get_the_ID() ); - if ( ! $group || 'public' !== $group->status ) { - continue; - } - } - - // Skip humcore_deposit results that have parents (are attachments). - if ( 'humcore_deposit' === $params['post_type'] ) { - if ( $hcs_query->post->post_parent ) { - continue; - } - } - - - $response_data[ get_the_ID() ] = $this->_get_formatted_post(); - } - - $response = new WP_REST_Response; - - $response->set_data( - [ - 'results' => $response_data, - ] - ); - - return $response; - } - - /** - * Format a search result for output - * - * @global $post current post in the search results loop - * @return string formatted post markup - */ - public function _get_formatted_post() { - global $post; - - ob_start(); - - bp_get_template_part( 'suggestions/' . str_replace( '_', '-', $post->post_type ) ); - - return ob_get_clean(); - } - - /** - * Fetch user-hidden posts for exclusion from query() or updating in hide() - * - * @return array multidimensional array e.g. [ 'user' => [ 1, 2 ], 'humcore_deposit => [ 1 ] ] - */ - function _get_user_hidden_posts() { - $retval = []; - - $meta = get_user_meta( get_current_user_id(), self::META_KEY_USER_HIDDEN_POSTS, true ); - - if ( $meta ) { - $retval = $meta; - } - - return $retval; - } -} diff --git a/plugins/hc-suggestions/classes/class-hc-suggestions-widget.php b/plugins/hc-suggestions/classes/class-hc-suggestions-widget.php deleted file mode 100644 index 5a019ffa8..000000000 --- a/plugins/hc-suggestions/classes/class-hc-suggestions-widget.php +++ /dev/null @@ -1,208 +0,0 @@ - label. - * - * Would be nice to pull labels from an authoritative source rather than hardcode, - * but that doesn't exist for fake post types anyway. - * - * @var array - */ - public $post_types = [ - 'user' => 'Members', - 'bp_group' => 'Groups', - 'humcore_deposit' => 'Scholarship', - ]; - - /** - * Constructor - */ - public function __construct() { - parent::__construct( - 'HC_Suggestions_Widget', - 'HC Suggestions Widget', - [ - 'classname' => 'HC_Suggestions_Widget', - 'description' => 'Suggest content to members based on selected terms.', - ] - ); - } - - /** - * Outputs the content of the widget - * - * @param array $args Display arguments including 'before_title', 'after_title', - * 'before_widget', and 'after_widget'. - * @param array $instance The settings for the particular instance of the widget. - */ - public function widget( $args, $instance ) { - if ( - isset( $instance['show_when_logged_in'] ) && - ! $instance['show_when_logged_in'] && - is_user_logged_in() - ) { - return; - } - - if ( - isset( $instance['show_when_logged_out'] ) && - ! $instance['show_when_logged_out'] && - ! is_user_logged_in() - ) { - return; - } - - $tab_id_prefix = 'hc-suggestions-tab-'; - - $user_terms = wpmn_get_object_terms( - get_current_user_id(), - self::TAXONOMY, - [ - 'fields' => 'names', - ] - ); - - echo '
'; // close hc-suggestions-widget. - } - - /** - * Outputs the options form on admin - * - * @param array $instance The widget options. - */ - public function form( $instance ) { - $defaults = array( - 'title' => 'Recommended for You', - 'description' => '', - 'show_when_logged_in' => true, - 'show_when_logged_out' => true, - 'user_tab_enabled' => true, - 'bp_group_tab_enabled' => true, - 'humcore_deposit_tab_enabled' => true, - ); - $instance = wp_parse_args( (array) $instance, $defaults ); - - $title = strip_tags( $instance['title'] ); - $description = strip_tags( $instance['description'] ); - $show_when_logged_in = (bool) $instance['show_when_logged_in']; - $show_when_logged_out = (bool) $instance['show_when_logged_out']; - $user_tab_enabled = (bool) $instance['user_tab_enabled']; - $bp_group_tab_enabled = (bool) $instance['bp_group_tab_enabled']; - $humcore_deposit_tab_enabled = (bool) $instance['humcore_deposit_tab_enabled']; - ?> - - - - - -No results.
' ); - - $( target ).find( '.btn.more' ).remove(); - - if ( Object.keys( data.results ).length > 0 ) { - html = ''; - - $.each( data.results, function( i, result ) { - // only append result if it's not already listed - if ( 0 === target.find( '.result[data-post-id="' + i + '"]' ).length ) { - html += result; - } - } ); - - $( html ).appendTo( target ); - - $( target ).find( '.hide' ).on( 'click', hc_suggestions.handle_hide_click ); - - $( 'More results' ) - .appendTo( target ) - .on( 'click', function( e ) { - e.preventDefault(); - params.paged = 1 + ( params.paged || 1 ); - hc_suggestions.load_results( params, target ); - } ); - } else if ( ! $( target ).is( ':contains(' + no_results_markup.html() + ')' ) ) { - no_results_markup.appendTo( target ); - } - - $( target ).find( loader_img ).remove(); - } ); - }, - - handle_resize: function( e ) { - var widget = $( '.' + hc_suggestions.widget_container_class ); - - if ( 450 > widget.outerWidth() ) { - widget.addClass( 'narrow' ); - } else { - widget.removeClass( 'narrow' ); - } - }, - - /** - * Initialize widget tab ui & load results for each tab - */ - init: function() { - var widget = $( '.' + hc_suggestions.widget_container_class ); - if ( widget && typeof widget.tabs === 'function' ) { - widget - .tabs() - .on( 'resize', hc_suggestions.handle_resize ) - .find( 'div' ).each( function( i, el ) { - hc_suggestions.load_results( - { - s: $( el ).attr( 'data-hc-suggestions-query' ), - post_type: $( el ).attr( 'data-hc-suggestions-type' ), - }, - $( el ) - ); - } ); - } - } -} - -jQuery( hc_suggestions.init ); diff --git a/plugins/hc-suggestions/readme.txt b/plugins/hc-suggestions/readme.txt deleted file mode 100644 index 6d8a08fa3..000000000 --- a/plugins/hc-suggestions/readme.txt +++ /dev/null @@ -1,114 +0,0 @@ -=== HC Suggestions === -Contributors: (this should be a list of wordpress.org userid's) -Donate link: http://example.com/ -Tags: comments, spam -Requires at least: 4.4 -Tested up to: 4.7.5 -Stable tag: 0.1.0 -License: GPLv2 or later -License URI: http://www.gnu.org/licenses/gpl-2.0.html - -Here is a short description of the plugin. This should be no more than 150 characters. No markup here. - -== Description == - -This is the long description. No limit, and you can use Markdown (as well as in the following sections). - -For backwards compatibility, if this section is missing, the full length of the short description will be used, and -Markdown parsed. - -A few notes about the sections above: - -* "Contributors" is a comma separated list of wp.org/wp-plugins.org usernames -* "Tags" is a comma separated list of tags that apply to the plugin -* "Requires at least" is the lowest version that the plugin will work on -* "Tested up to" is the highest version that you've *successfully used to test the plugin*. Note that it might work on -higher versions... this is just the highest one you've verified. -* Stable tag should indicate the Subversion "tag" of the latest stable version, or "trunk," if you use `/trunk/` for -stable. - - Note that the `readme.txt` of the stable tag is the one that is considered the defining one for the plugin, so -if the `/trunk/readme.txt` file says that the stable tag is `4.3`, then it is `/tags/4.3/readme.txt` that'll be used -for displaying information about the plugin. In this situation, the only thing considered from the trunk `readme.txt` -is the stable tag pointer. Thus, if you develop in trunk, you can update the trunk `readme.txt` to reflect changes in -your in-development version, without having that information incorrectly disclosed about the current stable version -that lacks those changes -- as long as the trunk's `readme.txt` points to the correct stable tag. - - If no stable tag is provided, it is assumed that trunk is stable, but you should specify "trunk" if that's where -you put the stable version, in order to eliminate any doubt. - -== Installation == - -This section describes how to install the plugin and get it working. - -e.g. - -1. Upload `plugin-name.php` to the `/wp-content/plugins/` directory -1. Activate the plugin through the 'Plugins' menu in WordPress -1. Place `` in your templates - -== Frequently Asked Questions == - -= A question that someone might have = - -An answer to that question. - -= What about foo bar? = - -Answer to foo bar dilemma. - -== Screenshots == - -1. This screen shot description corresponds to screenshot-1.(png|jpg|jpeg|gif). Note that the screenshot is taken from -the /assets directory or the directory that contains the stable readme.txt (tags or trunk). Screenshots in the /assets -directory take precedence. For example, `/assets/screenshot-1.png` would win over `/tags/4.3/screenshot-1.png` -(or jpg, jpeg, gif). -2. This is the second screen shot - -== Changelog == - -= 1.0 = -* A change since the previous version. -* Another change. - -= 0.5 = -* List versions from most recent at top to oldest at bottom. - -== Upgrade Notice == - -= 1.0 = -Upgrade notices describe the reason a user should upgrade. No more than 300 characters. - -= 0.5 = -This version fixes a security related bug. Upgrade immediately. - -== Arbitrary section == - -You may provide arbitrary sections, in the same format as the ones above. This may be of use for extremely complicated -plugins where more information needs to be conveyed that doesn't fit into the categories of "description" or -"installation." Arbitrary sections will be shown below the built-in sections outlined above. - -== A brief Markdown Example == - -Ordered list: - -1. Some feature -1. Another feature -1. Something else about the plugin - -Unordered list: - -* something -* something else -* third thing - -Here's a link to [WordPress](http://wordpress.org/ "Your favorite software") and one to [Markdown's Syntax Documentation][markdown syntax]. -Titles are optional, naturally. - -[markdown syntax]: http://daringfireball.net/projects/markdown/syntax - "Markdown is what the parser uses to process much of the readme file" - -Markdown uses email style notation for blockquotes and I've been told: -> Asterisks for *emphasis*. Double it up for **strong**. - -`` diff --git a/plugins/hc-suggestions/templates/suggestions/bp-group.php b/plugins/hc-suggestions/templates/suggestions/bp-group.php deleted file mode 100644 index 4d6874e46..000000000 --- a/plugins/hc-suggestions/templates/suggestions/bp-group.php +++ /dev/null @@ -1,64 +0,0 @@ -ID ); - -/** - * The return value of bp_core_fetch_avatar() can contain badges and other markup. - * We only want the . - */ -$bp_avatar = bp_core_fetch_avatar( - [ - 'item_id' => $group->id, - 'avatar_dir' => 'group-avatars', - 'object' => 'group', - ] -); -preg_match( '/