18 Commits

Author SHA1 Message Date
083d0320e1 Bump plugin version to 0.1.15
All checks were successful
Release Plugin Zip / release (push) Successful in 6s
Update the plugin header, runtime version constant, and package metadata for the next release.
2026-05-18 18:38:35 -05:00
518adda534 Merge branch 'feature/multi-team-schedule-exporter' 2026-05-18 18:35:36 -05:00
43033ddfb2 Add greyscale printable calendar mode
Add a black_white URL option for printable calendars and expose it in both printable URL builders.

Apply a high-legibility greyscale print style with white day cells, grey event blocks, and dark borders/text.

Condense month-cell event typography to fit more matchup text in each calendar day.

Cover black_white propagation in the printable URL helper test.
2026-05-18 18:35:08 -05:00
ac71e880a4 Clean up multi-team schedule title code
Centralize title format validation for schedule exports and reuse it from the public URL builder.

Tidy request, feed, and printable entry arrays introduced during the multi-team title work.

Clarify printable calendar render naming so split-title layout is distinct from matchup title format.
2026-05-18 18:15:24 -05:00
5fbd902c6c Support multi-team printable schedule titles
Allow the printable calendar URL builders to select multiple teams and carry the selected title_format parameter through CSV, iCal, and print URLs.

Add selected-first and matchup title modes so single-team schedules can show only the opponent while multi-team schedules consistently place a chosen team first, using vs for selected home games and at for away or selected-vs-selected games.

Keep team-layout exports available for multiple selected teams, including title, separator, and opponent columns derived from the selected-team perspective.

Adjust printable calendar matchup day styling so date numbers stay visible while event content can use the full day cell behind the badge.

Extend schedule exporter tests for multi-team team layout, selected-first titles, matchup titles, and printable URL title_format propagation.
2026-05-18 18:06:58 -05:00
635a76342e Enhance printable calendar URL controls
Add URL-driven label mode controls for printable calendars, including team_label and field_label parameters that support name, shortname, and abbreviation output. Wire those modes through the Tony's settings URL builder, printable title, opponent and matchup labels, selected field metadata, event venue labels, and footer legend labels.

Expand the printable URL builder with field selection, team/field label format dropdowns, and month-page output. Keep existing URLs compatible by normalizing old label mode values such as full_name and short_name to the new name and shortname modes.

Improve printable calendar rendering for field-filtered and combined-team schedules. Support multiple selected teams, matchup-style event cells, per-month pages, selected-field venue suppression, and neutral colors for multi-team calendars.

Fix season-scoped venue color settings persistence by merging submitted field color and team-primary flags for the active season instead of wiping other seasons. Resolve field colors so team primary is only used for single-team calendars when the venue flag is set; multi-team calendars use the saved or suggested field color.

Add PHPUnit coverage for multi-team printable URLs, label mode parameters, label resolution in printable entries, multi-team matchup entries, and suppressing venue labels when a single field is selected.
2026-05-18 10:10:11 -05:00
78a2229bb2 Bump version to 0.1.14 2026-04-30 17:47:16 -05:00
e6a336751c Add venue field page settings and sections 2026-04-30 17:46:32 -05:00
b6ca3c3d38 Remove square Open Graph image 2026-04-29 13:13:19 -05:00
10ef2dd28c bump version 2026-04-28 07:10:16 -05:00
2afe98bc99 Improve SportsPress Open Graph images 2026-04-27 12:43:26 -05:00
1df307dfbe Use WordPress timezone for webhook schedules 2026-04-27 12:13:54 -05:00
f38ceccdb3 Bump version to 0.1.11
Some checks failed
Release Plugin Zip / release (push) Has been cancelled
2026-04-24 14:39:23 -05:00
94b51c3959 Refine SportsPress webhook schedule templates 2026-04-24 14:38:38 -05:00
8f1a819a2d Fix stale plugin update notice
Some checks failed
Release Plugin Zip / release (push) Has been cancelled
2026-04-11 12:29:47 -05:00
5a5bf5acdb Bump version to 0.1.10
Some checks failed
Release Plugin Zip / release (push) Has been cancelled
2026-04-08 07:23:37 -05:00
dbe3048af7 Improve webhook testing and provider UI
Some checks failed
Release Plugin Zip / release (push) Has been cancelled
2026-04-05 14:28:17 -04:00
4ed968a045 Add configurable event webhook notifications 2026-04-05 14:14:12 -04:00
17 changed files with 7003 additions and 536 deletions

View File

@@ -0,0 +1,93 @@
Copyright © 2010 by Dharma Type.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@@ -27,10 +27,7 @@ body {
.print-shell {
margin: 0;
width: calc(100% / var(--sheet-scale));
background: #fff;
transform-origin: top left;
transform: scale(var(--sheet-scale));
}
.print-page {
@@ -48,7 +45,10 @@ body {
}
body.print-preview .print-shell {
flex: 0 0 auto;
box-shadow: 0 10px 30px rgba(17, 24, 39, 0.2);
transform-origin: top left;
transform: scale(var(--sheet-scale));
}
body.print-preview .print-shell.letter {
@@ -60,6 +60,104 @@ body {
width: 11in;
min-height: 17in;
}
body.print-preview.month-pages {
display: block;
overflow-x: auto;
}
body.print-preview.month-pages .print-shell,
body.print-preview.month-pages .print-shell.letter,
body.print-preview.month-pages .print-shell.ledger {
width: auto;
min-height: auto;
background: transparent;
box-shadow: none;
transform: none;
}
body.print-preview.month-pages .print-page {
padding: 0;
}
body.print-preview.month-pages .month-page {
box-sizing: border-box;
width: 8.5in;
min-height: 11in;
margin-right: auto;
margin-left: auto;
padding: var(--pc-page-padding);
background: #fff;
box-shadow: 0 10px 30px rgba(17, 24, 39, 0.2);
}
body.print-preview.month-pages.ledger .month-page {
width: 11in;
min-height: 17in;
}
body.print-preview.month-pages .month-page + .month-page {
margin-top: 24px;
}
body.print-preview.month-pages .month-page .header {
margin-bottom: 24px;
}
body.print-preview.month-pages .month-page .footer-meta {
margin-top: 24px;
}
body.print-preview.month-pages .sheet-grid {
display: block;
}
body.print-preview.month-pages .month {
width: 100%;
}
body.month-pages .month-title {
font-size: calc(26px * var(--month-font-scale));
padding: 4px;
}
body.month-pages .dow span {
font-size: calc(12px * var(--month-font-scale));
padding: 4px 2px;
}
body.month-pages .day-num {
font-size: calc(14px * var(--month-font-scale));
}
body.month-pages .event-name {
font-size: calc(13px * var(--month-font-scale));
line-height: 0.98;
}
body.month-pages .matchup-name {
font-size: calc(13px * var(--month-font-scale));
line-height: 0.92;
}
body.month-pages .event-time {
font-size: calc(14px * var(--month-font-scale));
}
body.month-pages .event.matchup .event-time {
font-size: calc(11px * var(--month-font-scale));
}
body.month-pages .event.matchup .event-venue {
font-size: calc(10px * var(--month-font-scale));
}
}
@media screen and (max-width: 900px) {
body.print-preview {
justify-content: flex-start;
overflow-x: auto;
}
}
.header {
@@ -235,7 +333,7 @@ body {
position: absolute;
top: var(--corner-badge-offset);
left: var(--corner-badge-offset);
z-index: 4;
z-index: 20;
width: var(--corner-badge-size);
height: var(--corner-badge-size);
display: flex;
@@ -252,12 +350,46 @@ body {
color: var(--day-num-color, #fff);
}
.day.has-matchups {
background: #fff;
border: 1px solid var(--pc-border);
}
.day.has-matchups .day-num {
position: absolute;
z-index: 20;
background: #fff;
color: var(--team-ink, #111) !important;
text-shadow: none !important;
box-shadow: 0 0 0 1px var(--pc-border);
}
body.month-pages .day .day-num {
top: 1px;
left: 1px;
width: var(--corner-badge-size);
height: var(--corner-badge-size);
border-radius: 0;
background: #fff;
color: var(--team-ink, #111);
text-shadow: none;
}
.events-stack {
position: relative;
z-index: 1;
height: 100%;
display: grid;
grid-template-rows: repeat(var(--event-count), minmax(0, 1fr));
}
.day.has-matchups .events-stack {
box-sizing: border-box;
grid-template-rows: repeat(var(--event-count), minmax(0, 1fr));
gap: 2px;
padding: 2px;
}
.event {
--event-top-band: calc(var(--corner-badge-size, 11px) + var(--corner-badge-offset, 2px));
--event-bottom-band: 26px;
@@ -275,6 +407,17 @@ body {
--event-bg: var(--team-link-color, var(--team-secondary, #8b3f1f));
}
.event.matchup {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) auto;
gap: 1px;
min-height: 0;
padding: 2px 3px;
border-radius: 2px;
overflow: hidden;
}
.event-center {
position: absolute;
top: var(--event-top-band);
@@ -293,6 +436,17 @@ body {
text-align: center;
}
.event.matchup .event-center {
position: relative;
top: auto;
left: auto;
right: auto;
bottom: auto;
min-height: 0;
align-items: center;
justify-content: center;
}
.event-center img {
width: auto;
height: var(--event-logo-height);
@@ -315,6 +469,8 @@ body {
hyphens: none;
line-height: 1.05;
font-weight: 700;
font-stretch: condensed;
font-variation-settings: "wdth" 42, "wght" 700;
opacity: 0.85;
}
@@ -322,10 +478,38 @@ body {
font-family: var(--pc-font-display);
font-weight: 700;
font-variation-settings: "wdth" 30, "wght" 700;
letter-spacing: -0.01em;
text-transform: uppercase;
}
.matchup-name {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
padding: 0;
font-size: calc(8.5px * var(--month-font-scale));
line-height: 0.98;
font-variation-settings: "wdth" 36, "wght" 800;
opacity: 1;
}
.matchup-vs {
font-size: 0.66em;
font-weight: 900;
opacity: 0.78;
}
.matchup-team {
display: block;
width: 100%;
overflow: hidden;
text-align: center;
text-overflow: clip;
white-space: nowrap;
font-stretch: condensed;
}
.ha-flag {
position: absolute;
top: var(--corner-badge-offset, 2px);
@@ -341,7 +525,7 @@ body {
font-weight: 900;
font-variation-settings: "wdth" 84, "wght" 900;
line-height: 1;
letter-spacing: -0.01em;
letter-spacing: 0;
background: #111;
color: #fff;
}
@@ -366,11 +550,26 @@ body {
overflow: hidden;
}
.event.matchup .event-meta {
position: relative;
left: auto;
right: auto;
bottom: auto;
display: grid;
grid-template-columns: minmax(0, auto) minmax(0, 1fr);
align-items: center;
justify-content: center;
column-gap: 3px;
padding: 0;
}
.event-time {
order: 1;
max-width: 100%;
font-size: calc(12px * var(--month-font-scale));
font-weight: 800;
font-stretch: condensed;
font-variation-settings: "wdth" 56, "wght" 800;
line-height: 1;
text-transform: uppercase;
text-align: center;
@@ -381,11 +580,19 @@ body {
opacity: 0.95;
}
.event.matchup .event-time {
min-width: 0;
font-size: calc(7.5px * var(--month-font-scale));
font-weight: 900;
}
.event-venue {
order: 2;
max-width: 100%;
font-size: calc(8px * var(--month-font-scale));
font-weight: 700;
font-stretch: condensed;
font-variation-settings: "wdth" 50, "wght" 700;
line-height: 1;
text-align: center;
text-transform: uppercase;
@@ -395,6 +602,12 @@ body {
opacity: 0.88;
}
.event.matchup .event-venue {
min-width: 0;
font-size: calc(7px * var(--month-font-scale));
text-align: left;
}
.empty {
padding: 16px;
border: 2px dashed #c8d2de;
@@ -472,6 +685,100 @@ body {
background: #fff;
}
body.black-white .header,
body.black-white .month-title,
body.black-white .dow span,
body.black-white .day,
body.black-white .event,
body.black-white .empty,
body.black-white .footer-meta,
body.black-white .footer-qr-image {
border-color: #2f3337 !important;
}
body.black-white .month-title,
body.black-white .ha-flag {
background: #1f2328 !important;
color: #fff !important;
text-shadow: none !important;
}
body.black-white .dow span,
body.black-white .day,
body.black-white .day.muted,
body.black-white .day.no-events,
body.black-white .day.has-matchups,
body.black-white .empty,
body.black-white .legend-item,
body.black-white .event.a .ha-flag {
background: #f7f7f7 !important;
color: #111 !important;
text-shadow: none !important;
}
body.black-white .dow span {
background: #e4e7eb !important;
}
body.black-white .day,
body.black-white .day.has-matchups,
body.black-white .legend-item {
background: #fff !important;
}
body.black-white .event,
body.black-white .event.h,
body.black-white .event.a,
body.black-white .event.matchup {
background: #e4e7eb !important;
color: #111 !important;
text-shadow: none !important;
}
body.black-white .day.muted,
body.black-white .day.no-events {
background: #f2f3f5 !important;
color: #4b5563 !important;
}
body.black-white .month-title,
body.black-white .dow span,
body.black-white .day,
body.black-white .event {
border: 1px solid #2f3337;
}
body.black-white .day-num,
body.black-white .day.has-events .day-num,
body.black-white .day.has-matchups .day-num,
body.black-white.month-pages .day .day-num {
background: #fff !important;
color: #111 !important;
box-shadow: 0 0 0 1px #2f3337 !important;
text-shadow: none !important;
}
body.black-white .ha-flag,
body.black-white .event.a .ha-flag {
box-shadow: 0 0 0 1px #2f3337;
}
body.black-white .event-name,
body.black-white .event-time,
body.black-white .event-venue,
body.black-white .matchup-vs,
body.black-white .footer-qr-label,
body.black-white .footer-qr-link,
body.black-white .meta {
color: #111 !important;
opacity: 1 !important;
}
body.black-white .meta,
body.black-white .footer-qr-label {
color: #374151 !important;
}
@media print {
body,
body.print-preview {
@@ -487,6 +794,7 @@ body {
width: auto;
min-height: auto;
box-shadow: none;
transform: none;
}
.print-page {
@@ -500,4 +808,27 @@ body {
.title {
font-size: calc(26px * var(--month-font-scale));
}
body.month-pages .sheet-grid {
display: block;
}
body.month-pages .month-page {
break-after: page;
page-break-after: always;
}
body.month-pages .month-page:last-child {
break-after: auto;
page-break-after: auto;
}
body.month-pages .month {
width: 100%;
}
body.month-pages .grid {
break-inside: avoid;
page-break-inside: avoid;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,151 @@
<?php
/*
Plugin Name: Custom Open Graph Tags with SportsPress Integration
Description: Adds custom Open Graph tags to posts based on their type, specifically handling sp_event post types with methods from the SportsPress SP_Event class.
Version: 1.0
Author: Your Name
*/
/**
* Open Graph tags for SportsPress events.
*
* @package Tonys_Sportspress_Enhancements
*/
add_action('wp_head', 'custom_open_graph_tags_with_sportspress_integration');
add_action( 'wp_head', 'custom_open_graph_tags_with_sportspress_integration' );
function asc_generate_sp_event_title( $post ) {
// See https://github.com/ThemeBoy/SportsPress/blob/770fa8c6654d7d6648791e877709c2428677635b/includes/admin/post-types/class-sp-admin-cpt-event.php#L99C40-L99C55
if ( is_numeric( $post ) ) {
$post = get_post( $post );
}
if ( ! $post || $post->post_type !== 'sp_event' ) {
return get_the_title();
/**
* Build the dynamic matchup image URL for an event.
*
* @param int|WP_Post $post Event post or ID.
* @param string $variant Image variant.
* @return string
*/
function asc_sp_event_matchup_image_url( $post, $variant = 'wide' ) {
$post = asc_sp_event_get_post( $post );
if ( ! $post ) {
return '';
}
$teams = get_post_meta( $post->ID, 'sp_team', false );
$teams = array_filter( $teams );
$args = array(
'post' => $post->ID,
);
if ( 'wide' !== $variant ) {
$args['variant'] = $variant;
}
if ( function_exists( 'asc_sp_event_image_url_version' ) ) {
$args['v'] = asc_sp_event_image_url_version();
}
return add_query_arg( $args, home_url( '/head-to-head' ) );
}
/**
* Build Open Graph image descriptors for an event.
*
* @param WP_Post $post Event post.
* @return array<int,array<string,string>>
*/
function asc_sp_event_open_graph_images( WP_Post $post ) {
return array(
array(
'url' => asc_sp_event_matchup_image_url( $post, 'wide' ),
'width' => '1200',
'height' => '628',
),
);
}
/**
* Normalize an event post argument.
*
* @param int|WP_Post|null $post Post object or ID.
* @return WP_Post|null
*/
function asc_sp_event_get_post( $post = null ) {
if ( null === $post ) {
$post = get_post();
} elseif ( is_numeric( $post ) ) {
$post = get_post( absint( $post ) );
}
if ( ! $post instanceof WP_Post || 'sp_event' !== $post->post_type ) {
return null;
}
return $post;
}
/**
* Get event team IDs in SportsPress display order.
*
* @param int|WP_Post $post Event post or ID.
* @return int[]
*/
function asc_sp_event_team_ids( $post ) {
$post = asc_sp_event_get_post( $post );
if ( ! $post ) {
return array();
}
$teams = get_post_meta( $post->ID, 'sp_team', false );
$team_ids = array();
$team_names = array();
foreach ( $teams as $team ) {
while ( is_array( $team ) ) {
$team = array_shift( array_filter( $team ) );
}
$team = absint( $team );
if ( $team > 0 ) {
$team_names[] = sp_team_short_name( $team );
$team_ids[] = $team;
}
}
$team_names = array_unique( $team_names );
$team_ids = array_values( array_unique( $team_ids ) );
if ( get_option( 'sportspress_event_reverse_teams', 'no' ) === 'yes' ) {
$team_names = array_reverse( $team_names );
if ( 'yes' === get_option( 'sportspress_event_reverse_teams', 'no' ) ) {
$team_ids = array_reverse( $team_ids );
}
return $team_ids;
}
/**
* Get a safe team short name with fallbacks for test and partial SportsPress environments.
*
* @param int $team_id Team post ID.
* @return string
*/
function asc_sp_team_short_name( $team_id ) {
$name = '';
if ( function_exists( 'sp_team_short_name' ) ) {
$name = (string) sp_team_short_name( $team_id );
}
if ( '' === trim( $name ) ) {
$name = get_the_title( $team_id );
}
return '' !== trim( $name ) ? $name : __( 'Team TBD', 'tonys-sportspress-enhancements' );
}
/**
* Generate a matchup title from event teams.
*
* @param int|WP_Post $post Event post or ID.
* @return string
*/
function asc_generate_sp_event_title( $post ) {
$post = asc_sp_event_get_post( $post );
if ( ! $post ) {
return get_the_title();
}
$team_names = array_map( 'asc_sp_team_short_name', asc_sp_event_team_ids( $post ) );
$team_names = array_values( array_filter( array_unique( $team_names ) ) );
if ( empty( $team_names ) ) {
return get_the_title( $post );
}
$delimiter = ' ' . get_option( 'sportspress_event_teams_delimiter', 'vs' ) . ' ';
@@ -41,147 +153,343 @@ function asc_generate_sp_event_title( $post ) {
return implode( $delimiter, $team_names );
}
/**
* Generate compact event date text.
*
* @param int|WP_Post $post Event post or ID.
* @param bool $withTime Include time.
* @return string
*/
function asc_generate_short_date( $post, $withTime = true ) {
$formatted_date = get_the_date('D n/j/y', $post);
$post = asc_sp_event_get_post( $post );
if (!$withTime){
return $formatted_date;
}
if ( ! $post ) {
return '';
}
if ( get_the_date('i', $post) == "00") {
$formatted_time = get_the_date('gA', $post);
} else {
$formatted_time = get_the_date('g:iA', $post);
}
return $formatted_date . " " . $formatted_time ;
$formatted_date = get_the_date( 'D n/j/y', $post );
if ( ! $withTime ) {
return $formatted_date;
}
$formatted_time = '00' === get_the_date( 'i', $post ) ? get_the_date( 'gA', $post ) : get_the_date( 'g:iA', $post );
return trim( $formatted_date . ' ' . $formatted_time );
}
/**
* Get venue name for an event.
*
* @param WP_Post $post Event post.
* @return string
*/
function asc_sp_event_venue_name( WP_Post $post ) {
$venue_terms = get_the_terms( $post->ID, 'sp_venue' );
if ( is_wp_error( $venue_terms ) || empty( $venue_terms ) ) {
return __( 'Venue TBD', 'tonys-sportspress-enhancements' );
}
return $venue_terms[0]->name;
}
/**
* Normalize event body content for meta descriptions.
*
* @param WP_Post $post Event post.
* @return string
*/
function asc_sp_event_body_excerpt( WP_Post $post ) {
$content = strip_shortcodes( $post->post_content );
$content = wp_strip_all_tags( $content, true );
$content = html_entity_decode( $content, ENT_QUOTES, get_bloginfo( 'charset' ) );
$content = preg_replace( '/\s+/', ' ', $content );
$content = trim( (string) $content );
if ( '' === $content ) {
return '';
}
return wp_trim_words( $content, 35, '' );
}
/**
* Safely instantiate a SportsPress event object.
*
* @param WP_Post $post Event post.
* @return object|null
*/
function asc_sp_event_object( WP_Post $post ) {
if ( ! class_exists( 'SP_Event' ) ) {
return null;
}
try {
return new SP_Event( $post->ID );
} catch ( Throwable $e ) {
return null;
}
}
/**
* Get the SportsPress event status with fallbacks.
*
* @param WP_Post $post Event post.
* @param object|null $event SportsPress event object.
* @return string
*/
function asc_sp_event_status( WP_Post $post, $event = null ) {
if ( $event && is_callable( array( $event, 'status' ) ) ) {
try {
$status = (string) $event->status();
if ( '' !== $status ) {
return $status;
}
} catch ( Throwable $e ) {
return '';
}
}
return 'future' === $post->post_status ? 'future' : '';
}
/**
* Get SportsPress result rows safely.
*
* @param object|null $event SportsPress event object.
* @return array
*/
function asc_sp_event_results( $event = null ) {
if ( ! $event || ! is_callable( array( $event, 'results' ) ) ) {
return array();
}
try {
$results = $event->results();
return is_array( $results ) ? $results : array();
} catch ( Throwable $e ) {
return array();
}
}
/**
* Convert a result row into outcome labels.
*
* @param array $result Result row.
* @return array
*/
function asc_sp_event_result_outcomes( array $result ) {
$result_outcome = isset( $result['outcome'] ) ? $result['outcome'] : null;
if ( ! is_array( $result_outcome ) ) {
return array();
}
$outcomes = array();
foreach ( $result_outcome as $outcome ) {
$the_outcome = get_page_by_path( $outcome, OBJECT, 'sp_outcome' );
if ( $the_outcome instanceof WP_Post ) {
$outcome_abbreviation = get_post_meta( $the_outcome->ID, 'sp_abbreviation', true );
if ( ! $outcome_abbreviation ) {
$outcome_abbreviation = function_exists( 'sp_substr' ) ? sp_substr( $the_outcome->post_title, 0, 1 ) : substr( $the_outcome->post_title, 0, 1 );
}
$outcomes[] = array(
'title' => $the_outcome->post_title,
'abbreviation' => $outcome_abbreviation,
);
}
}
return $outcomes;
}
/**
* Build a result title/description from SportsPress result data.
*
* @param WP_Post $post Event post.
* @param array $results Event results data.
* @param string $description Existing description.
* @return array{title:string,description:string}|null
*/
function asc_sp_event_result_meta( WP_Post $post, array $results, $description ) {
unset( $results[0] );
$results = array_filter( $results );
if ( count( $results ) < 2 ) {
return null;
}
if ( 'yes' === get_option( 'sportspress_event_reverse_teams', 'no' ) ) {
$results = array_reverse( $results, true );
}
$teams_result_array = array();
foreach ( $results as $team_id => $result ) {
if ( ! is_array( $result ) ) {
continue;
}
$outcomes = asc_sp_event_result_outcomes( $result );
$first_outcome = ! empty( $outcomes ) ? $outcomes[0] : array( 'title' => __( 'Result', 'tonys-sportspress-enhancements' ), 'abbreviation' => '' );
$team_name = asc_sp_team_short_name( $team_id );
$team_score = isset( $result['r'] ) && '' !== $result['r'] ? $result['r'] : null;
$team_score = null !== $team_score ? (string) $team_score : '';
if ( '' === $team_score ) {
continue;
}
$teams_result_array[] = array(
'score' => $team_score,
'outcome' => $first_outcome['title'],
'outcome_abbreviation' => $first_outcome['abbreviation'],
'team_name' => $team_name,
);
}
if ( count( $teams_result_array ) < 2 ) {
return null;
}
$special_result_suffix_abbreviation = '';
$special_result_suffix = '';
foreach ( $teams_result_array as $team ) {
$outcome_abbreviation = strtoupper( (string) $team['outcome_abbreviation'] );
if ( 'TF-W' === $outcome_abbreviation ) {
$special_result_suffix_abbreviation = 'TF-W';
$special_result_suffix = 'Technical Forfeit Win';
break;
}
if ( 'TF-L' === $outcome_abbreviation ) {
$special_result_suffix_abbreviation = 'TF';
$special_result_suffix = 'Technical Forfeit';
break;
}
if ( 'F-W' === $outcome_abbreviation || 'F-L' === $outcome_abbreviation ) {
$special_result_suffix_abbreviation = 'Forfeit';
$special_result_suffix = 'Forfeit';
break;
}
}
$publish_date = asc_generate_short_date( $post, false );
$title = sprintf(
'%1$s %2$s-%3$s %4$s%s',
$teams_result_array[0]['team_name'],
$teams_result_array[0]['score'],
$teams_result_array[1]['score'],
$teams_result_array[1]['team_name'],
$publish_date ? ' - ' . $publish_date : ''
);
if ( $special_result_suffix ) {
$title .= " ({$special_result_suffix_abbreviation})";
}
$description .= sprintf(
' %1$s (%2$s), %3$s (%4$s).',
$teams_result_array[0]['team_name'],
$teams_result_array[0]['outcome'],
$teams_result_array[1]['team_name'],
$teams_result_array[1]['outcome']
);
return array(
'title' => $title,
'description' => $description,
);
}
/**
* Build all Open Graph values for an event.
*
* @param int|WP_Post $post Event post or ID.
* @return array<string,mixed>
*/
function asc_sp_event_open_graph_data( $post ) {
$post = asc_sp_event_get_post( $post );
if ( ! $post ) {
return array();
}
$event = asc_sp_event_object( $post );
$venue_name = asc_sp_event_venue_name( $post );
$publish_date_and_time = get_the_date( 'F j, Y g:i A', $post );
$description = trim( "{$publish_date_and_time} at {$venue_name}." );
$title = asc_generate_sp_event_title( $post );
$sp_status = get_post_meta( $post->ID, 'sp_status', true );
$status = asc_sp_event_status( $post, $event );
if ( in_array( $sp_status, array( 'postponed', 'cancelled', 'tbd' ), true ) ) {
$status_label = strtoupper( $sp_status );
$description = "{$status_label} - {$description}";
$title = trim( "{$status_label} - {$title} - " . asc_generate_short_date( $post ) . " - {$venue_name}", ' -' );
} elseif ( 'future' === $status ) {
$title = trim( $title . ' - ' . asc_generate_short_date( $post ) . " - {$venue_name}", ' -' );
} elseif ( 'results' === $status ) {
$result_meta = asc_sp_event_result_meta( $post, asc_sp_event_results( $event ), $description );
if ( $result_meta ) {
$title = $result_meta['title'];
$description = $result_meta['description'];
}
}
$body_excerpt = asc_sp_event_body_excerpt( $post );
if ( '' !== $body_excerpt ) {
$description = trim( $description . ' ' . $body_excerpt );
}
return array(
'type' => 'article',
'images' => asc_sp_event_open_graph_images( $post ),
'image' => asc_sp_event_matchup_image_url( $post, 'wide' ),
'image_width' => '1200',
'image_height' => '628',
'title' => $title,
'description' => $description,
'url' => get_permalink( $post ),
);
}
/**
* Echo Open Graph meta tags for single SportsPress events.
*/
function custom_open_graph_tags_with_sportspress_integration() {
if (is_single()) {
global $post;
if ($post->post_type === 'sp_event') {
// Instantiate SP_Event object
$event = new SP_Event($post->ID);
if ( ! is_single() ) {
return;
}
// Fetch details using SP_Event methods
$publish_date = get_the_date('F j, Y', $post);
$venue_terms = get_the_terms($post->ID, 'sp_venue');
$venue_name = $venue_terms ? $venue_terms[0]->name : 'Venue TBD';
$results = $event->results(); // Using SP_Event method
$title = asc_generate_sp_event_title($post);
$sp_status = get_post_meta( $post->ID, 'sp_status', true );
$status = $event->status(); // Using SP_Event method
$publish_date_and_time = get_the_date('F j, Y g:i A', $post);
$description = "{$publish_date_and_time} at {$venue_name}.";
if ( 'postponed' == $sp_status || 'cancelled' == $sp_status || 'tbd' == $sp_status) {
$description = strtoupper($sp_status) . "" . $description;
$title = strtoupper($sp_status) . "" . $title . "" . asc_generate_short_date($post) . "" . $venue_name;
}
$post = asc_sp_event_get_post();
if ( 'future' == $status ) {
$description = $description;
$title = $title . "" . asc_generate_short_date($post) . "" . $venue_name;
}
if ( ! $post ) {
return;
}
if ( 'results' == $status ) { // checks if there is a final score
// Get event result data
$data = $event->results();
$meta = asc_sp_event_open_graph_data( $post );
// The first row should be column labels
$labels = $data[0];
if ( empty( $meta ) ) {
return;
}
// Remove the first row to leave us with the actual data
unset( $data[0] );
$data = array_filter( $data );
if ( empty( $data ) ) {
return false;
}
// Initialize
$i = 0;
$result_string = '';
$title_string = '';
// Reverse teams order if the option "Events > Teams > Order > Reverse order" is enabled.
$reverse_teams = get_option( 'sportspress_event_reverse_teams', 'no' ) === 'yes' ? true : false;
if ( $reverse_teams ) {
$data = array_reverse( $data, true );
}
$teams_result_array = [];
foreach ( $data as $team_id => $result ) :
$outcomes = array();
$result_outcome = sp_array_value( $result, 'outcome' );
if ( ! is_array( $result_outcome ) ) :
$outcomes = array( '&mdash;' );
else :
foreach ( $result_outcome as $outcome ) :
$the_outcome = get_page_by_path( $outcome, OBJECT, 'sp_outcome' );
if ( is_object( $the_outcome ) ) :
$outcomes[] = $the_outcome->post_title;
endif;
endforeach;
endif;
unset( $result['outcome'] );
$team_name = sp_team_short_name( $team_id );
$team_abbreviation = sp_team_abbreviation( $team_id );
$outcome_abbreviation = get_post_meta( $the_outcome->ID, 'sp_abbreviation', true );
if ( ! $outcome_abbreviation ) {
$outcome_abbreviation = sp_substr( $the_outcome->post_title, 0, 1 );
}
array_push($teams_result_array, [
"result" => $result,
"outcome" => $the_outcome->post_title,
"outcome_abbreviation" => $outcome_abbreviation,
"team_name" => $team_name,
"team_abbreviation" => $team_abbreviation
]
);
$i++;
endforeach;
$publish_date = asc_generate_short_date($post, false);
$special_result_suffix_abbreviation = '';
$special_result_suffix= '';
foreach ( $teams_result_array as $team ) {
$outcome_abbreviation = strtoupper( $team['outcome_abbreviation'] ); // Normalize case
if ( $outcome_abbreviation === 'TF-W' ) {
$special_result_suffix_abbreviation = 'TF-W';
$special_result_suffix = 'Technical Forfeit Win';
break;
} elseif ( $outcome_abbreviation === 'TF-L' ) {
$special_result_suffix_abbreviation = 'TF';
$special_result_suffix = 'Technical Forfeit';
break;
} elseif ( $outcome_abbreviation === 'F-W' || $outcome_abbreviation === 'F-L' ) {
$special_result_suffix_abbreviation = 'Forfeit';
$special_result_suffix = 'Forfeit';
break;
}
}
$title = "{$teams_result_array[0]['team_name']} {$teams_result_array[0]['result']['r']}-{$teams_result_array[1]['result']['r']} {$teams_result_array[1]['team_name']}{$publish_date}" . ($special_result_suffix ? "({$special_result_suffix_abbreviation})" : "");
$description .= " " . "{$teams_result_array[0]['team_name']} ({$teams_result_array[0]['outcome']}), {$teams_result_array[1]['team_name']} ({$teams_result_array[1]['outcome']})." ;
}
$description .= " " . $post->post_content;
$image = get_site_url() . "/head-to-head?post={$post->ID}";
echo '<meta property="og:type" content="article" />' . "\n";
echo '<meta property="og:image" content="'. $image . '" />' . "\n";
echo '<meta property="og:title" content="' . $title . '" />' . "\n";
echo '<meta property="og:description" content="' . $description . '" />' . "\n";
echo '<meta property="og:url" content="' . get_permalink() . '" />' . "\n";
}
}
echo '<meta property="og:type" content="' . esc_attr( $meta['type'] ) . '" />' . "\n";
foreach ( $meta['images'] as $image ) {
echo '<meta property="og:image" content="' . esc_url( $image['url'] ) . '" />' . "\n";
echo '<meta property="og:image:width" content="' . esc_attr( $image['width'] ) . '" />' . "\n";
echo '<meta property="og:image:height" content="' . esc_attr( $image['height'] ) . '" />' . "\n";
}
echo '<meta property="og:title" content="' . esc_attr( $meta['title'] ) . '" />' . "\n";
echo '<meta property="og:description" content="' . esc_attr( $meta['description'] ) . '" />' . "\n";
echo '<meta property="og:url" content="' . esc_url( $meta['url'] ) . '" />' . "\n";
}
?>

View File

@@ -62,7 +62,9 @@ function tse_sp_event_export_get_column_definitions() {
'time' => __( 'Time', 'tonys-sportspress-enhancements' ),
'season' => __( 'Season', 'tonys-sportspress-enhancements' ),
'league' => __( 'League', 'tonys-sportspress-enhancements' ),
'title' => __( 'Title', 'tonys-sportspress-enhancements' ),
'team_name' => __( 'Team', 'tonys-sportspress-enhancements' ),
'separator' => __( 'Separator', 'tonys-sportspress-enhancements' ),
'opponent_name' => __( 'Opponent', 'tonys-sportspress-enhancements' ),
'location_flag' => __( 'Home/Away', 'tonys-sportspress-enhancements' ),
'field_name' => __( 'Field Name', 'tonys-sportspress-enhancements' ),
@@ -85,12 +87,33 @@ function tse_sp_event_export_get_column_definitions() {
function tse_sp_event_export_get_default_columns( $format ) {
$defaults = array(
'matchup' => array( 'date', 'time', 'season', 'league', 'away_team', 'home_team', 'field_name' ),
'team' => array( 'label', 'date', 'time', 'season', 'league', 'opponent_name', 'location_flag', 'field_name' ),
'team' => array( 'label', 'date', 'time', 'season', 'league', 'title', 'field_name' ),
);
return isset( $defaults[ $format ] ) ? $defaults[ $format ] : $defaults['matchup'];
}
/**
* Get supported event title formats.
*
* @return string[]
*/
function tse_sp_event_export_get_title_formats() {
return array( 'selected_first', 'matchup' );
}
/**
* Sanitize event title format.
*
* @param string $format Raw title format.
* @return string
*/
function tse_sp_event_export_sanitize_title_format( $format ) {
$format = sanitize_key( (string) $format );
return in_array( $format, tse_sp_event_export_get_title_formats(), true ) ? $format : 'selected_first';
}
/**
* Sanitize an export format.
*
@@ -173,24 +196,26 @@ function tse_sp_event_export_parse_id_list( $value ) {
* @return array
*/
function tse_sp_event_export_normalize_request_args( $source = null ) {
$source = is_array( $source ) ? $source : $_GET;
$format = isset( $source['format'] ) ? tse_sp_event_export_sanitize_format( wp_unslash( $source['format'] ) ) : 'matchup';
$team_ids = isset( $source['team_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['team_id'] ) ) : array();
$season_ids = isset( $source['season_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['season_id'] ) ) : array();
$league_ids = isset( $source['league_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['league_id'] ) ) : array();
$field_ids = isset( $source['field_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['field_id'] ) ) : array();
$source = is_array( $source ) ? $source : $_GET;
$format = isset( $source['format'] ) ? tse_sp_event_export_sanitize_format( wp_unslash( $source['format'] ) ) : 'matchup';
$team_ids = isset( $source['team_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['team_id'] ) ) : array();
$season_ids = isset( $source['season_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['season_id'] ) ) : array();
$league_ids = isset( $source['league_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['league_id'] ) ) : array();
$field_ids = isset( $source['field_id'] ) ? tse_sp_event_export_parse_id_list( wp_unslash( $source['field_id'] ) ) : array();
$title_format = isset( $source['title_format'] ) ? tse_sp_event_export_sanitize_title_format( wp_unslash( $source['title_format'] ) ) : 'selected_first';
return array(
'team_id' => isset( $team_ids[0] ) ? $team_ids[0] : 0,
'team_ids' => $team_ids,
'season_id' => isset( $season_ids[0] ) ? $season_ids[0] : 0,
'season_ids'=> $season_ids,
'league_id' => isset( $league_ids[0] ) ? $league_ids[0] : 0,
'league_ids'=> $league_ids,
'field_id' => isset( $field_ids[0] ) ? $field_ids[0] : 0,
'field_ids' => $field_ids,
'format' => $format,
'columns' => isset( $source['columns'] ) ? tse_sp_event_export_sanitize_columns( $format, wp_unslash( $source['columns'] ) ) : tse_sp_event_export_get_default_columns( $format ),
'team_id' => isset( $team_ids[0] ) ? $team_ids[0] : 0,
'team_ids' => $team_ids,
'season_id' => isset( $season_ids[0] ) ? $season_ids[0] : 0,
'season_ids' => $season_ids,
'league_id' => isset( $league_ids[0] ) ? $league_ids[0] : 0,
'league_ids' => $league_ids,
'field_id' => isset( $field_ids[0] ) ? $field_ids[0] : 0,
'field_ids' => $field_ids,
'format' => $format,
'title_format' => $title_format,
'columns' => isset( $source['columns'] ) ? tse_sp_event_export_sanitize_columns( $format, wp_unslash( $source['columns'] ) ) : tse_sp_event_export_get_default_columns( $format ),
);
}
@@ -212,9 +237,6 @@ function tse_sp_event_export_validate_filters( $filters ) {
wp_die( esc_html__( 'Team format requires a team filter.', 'tonys-sportspress-enhancements' ), '', array( 'response' => 400 ) );
}
if ( count( $team_ids ) > 1 ) {
wp_die( esc_html__( 'Team format does not support multiple teams.', 'tonys-sportspress-enhancements' ), '', array( 'response' => 400 ) );
}
}
/**
@@ -294,11 +316,10 @@ function tse_sp_event_export_query_posts( $filters ) {
* @return array
*/
function tse_sp_event_export_get_events( $filters ) {
$team_id = isset( $filters['team_id'] ) ? absint( $filters['team_id'] ) : 0;
$selected_ids = isset( $filters['team_ids'] ) && is_array( $filters['team_ids'] ) ? array_values( array_filter( array_map( 'absint', $filters['team_ids'] ) ) ) : array();
$query_posts = tse_sp_event_export_query_posts( $filters );
$events = array();
$team_name = $team_id > 0 ? get_the_title( $team_id ) : '';
$title_format = tse_sp_event_export_sanitize_title_format( isset( $filters['title_format'] ) ? $filters['title_format'] : 'selected_first' );
$query_posts = tse_sp_event_export_query_posts( $filters );
$events = array();
foreach ( $query_posts as $event ) {
$event_id = $event instanceof WP_Post ? (int) $event->ID : 0;
@@ -314,21 +335,22 @@ function tse_sp_event_export_get_events( $filters ) {
$home_id = isset( $teams[0] ) ? (int) $teams[0] : 0;
$away_id = isset( $teams[1] ) ? (int) $teams[1] : 0;
$venue = tse_sp_event_export_get_primary_field( $event_id );
$context = tse_sp_event_export_get_team_context( $home_id, $away_id, $selected_ids );
if ( $team_id > 0 ) {
$location_flag = $home_id === $team_id ? 'Home' : 'Away';
$opponent_id = $home_id === $team_id ? $away_id : $home_id;
} else {
$location_flag = '';
$opponent_id = 0;
}
$context_team_id = isset( $context['team_id'] ) ? (int) $context['team_id'] : 0;
$opponent_id = isset( $context['opponent_id'] ) ? (int) $context['opponent_id'] : 0;
$separator = isset( $context['separator'] ) ? (string) $context['separator'] : '';
$location_flag = isset( $context['location_flag'] ) ? (string) $context['location_flag'] : '';
$title = tse_sp_event_export_get_event_title_value( $home_id, $away_id, $selected_ids, $title_format );
$events[] = array(
'event_id' => $event_id,
'label' => '',
'date' => get_post_time( 'm/d/Y', false, $event_id, true ),
'time' => strtoupper( (string) ( function_exists( 'sp_get_time' ) ? sp_get_time( $event_id ) : get_post_time( get_option( 'time_format' ), false, $event_id, true ) ) ),
'team_name' => is_string( $team_name ) ? $team_name : '',
'title' => $title,
'team_name' => $context_team_id > 0 ? get_the_title( $context_team_id ) : '',
'separator' => $separator,
'opponent_name' => $opponent_id > 0 ? get_the_title( $opponent_id ) : '',
'location_flag' => $location_flag,
'home_team' => $home_id > 0 ? get_the_title( $home_id ) : '',
@@ -352,6 +374,100 @@ function tse_sp_event_export_get_events( $filters ) {
return $events;
}
/**
* Resolve a display title for a schedule event.
*
* @param int $home_id Home team ID.
* @param int $away_id Away team ID.
* @param int[] $selected_ids Selected team IDs.
* @param string $title_format Title format.
* @return string
*/
function tse_sp_event_export_get_event_title_value( $home_id, $away_id, $selected_ids, $title_format ) {
$home_id = absint( $home_id );
$away_id = absint( $away_id );
$selected_ids = array_values( array_filter( array_map( 'absint', (array) $selected_ids ) ) );
$title_format = tse_sp_event_export_sanitize_title_format( $title_format );
if ( 'matchup' === $title_format ) {
return tse_sp_event_export_join_title_parts( $away_id, 'at', $home_id );
}
$context = tse_sp_event_export_get_team_context( $home_id, $away_id, $selected_ids );
if ( empty( $context['team_id'] ) ) {
return '';
}
if ( 1 === count( $selected_ids ) ) {
$opponent_id = isset( $context['opponent_id'] ) ? (int) $context['opponent_id'] : 0;
return $opponent_id > 0 ? get_the_title( $opponent_id ) : '';
}
return tse_sp_event_export_join_title_parts(
isset( $context['team_id'] ) ? (int) $context['team_id'] : 0,
isset( $context['separator'] ) ? (string) $context['separator'] : '',
isset( $context['opponent_id'] ) ? (int) $context['opponent_id'] : 0
);
}
/**
* Join two team names with a title separator.
*
* @param int $first_id First team ID.
* @param string $separator Separator.
* @param int $second_id Second team ID.
* @return string
*/
function tse_sp_event_export_join_title_parts( $first_id, $separator, $second_id ) {
$first_name = $first_id > 0 ? get_the_title( $first_id ) : '';
$second_name = $second_id > 0 ? get_the_title( $second_id ) : '';
$parts = array_values( array_filter( array( $first_name, trim( $separator ), $second_name ), 'strlen' ) );
return implode( ' ', $parts );
}
/**
* Resolve the team-layout perspective for an event.
*
* The selected team is always first. If both selected teams are in the game,
* use the away team first and the home team second.
*
* @param int $home_id Home team ID.
* @param int $away_id Away team ID.
* @param int[] $selected_ids Selected team IDs.
* @return array
*/
function tse_sp_event_export_get_team_context( $home_id, $away_id, $selected_ids ) {
$home_id = absint( $home_id );
$away_id = absint( $away_id );
$selected_ids = array_values( array_filter( array_map( 'absint', (array) $selected_ids ) ) );
if ( $away_id > 0 && in_array( $away_id, $selected_ids, true ) ) {
return array(
'team_id' => $away_id,
'opponent_id' => $home_id,
'separator' => 'at',
'location_flag' => 'Away',
);
}
if ( $home_id > 0 && in_array( $home_id, $selected_ids, true ) ) {
return array(
'team_id' => $home_id,
'opponent_id' => $away_id,
'separator' => 'vs',
'location_flag' => 'Home',
);
}
return array(
'team_id' => 0,
'opponent_id' => 0,
'separator' => '',
'location_flag' => '',
);
}
/**
* Get event term names as a semicolon-delimited string.
*
@@ -476,25 +592,22 @@ function tse_sp_event_export_fold_ical_line( $line ) {
* @return string
*/
function tse_sp_event_export_get_ical_summary( $event, $filters ) {
$format = tse_sp_event_export_sanitize_format( isset( $filters['format'] ) ? $filters['format'] : 'matchup' );
$team_ids = isset( $filters['team_ids'] ) && is_array( $filters['team_ids'] ) ? array_values( array_filter( array_map( 'absint', $filters['team_ids'] ) ) ) : array();
$team_id = isset( $team_ids[0] ) ? $team_ids[0] : 0;
$format = tse_sp_event_export_sanitize_format( isset( $filters['format'] ) ? $filters['format'] : 'matchup' );
$team_ids = isset( $filters['team_ids'] ) && is_array( $filters['team_ids'] ) ? array_values( array_filter( array_map( 'absint', $filters['team_ids'] ) ) ) : array();
$title_format = tse_sp_event_export_sanitize_title_format( isset( $filters['title_format'] ) ? $filters['title_format'] : 'selected_first' );
$team_id = isset( $team_ids[0] ) ? $team_ids[0] : 0;
if ( 'team' === $format && $team_id > 0 ) {
$teams = array_values( array_unique( array_map( 'intval', get_post_meta( $event->ID, 'sp_team', false ) ) ) );
$home_id = isset( $teams[0] ) ? (int) $teams[0] : 0;
$away_id = isset( $teams[1] ) ? (int) $teams[1] : 0;
$context = tse_sp_event_export_get_team_context( $home_id, $away_id, $team_ids );
if ( in_array( $team_id, $teams, true ) ) {
$is_home = $home_id === $team_id;
$opponent_id = $is_home ? $away_id : $home_id;
$opponent = $opponent_id > 0 ? get_the_title( $opponent_id ) : __( 'TBD', 'tonys-sportspress-enhancements' );
$summary = sprintf(
/* translators: 1: preposition, 2: opponent name. */
__( '%1$s %2$s', 'tonys-sportspress-enhancements' ),
$is_home ? 'vs' : 'at',
$opponent
);
if ( ! empty( $context['team_id'] ) ) {
$summary = tse_sp_event_export_get_event_title_value( $home_id, $away_id, $team_ids, $title_format );
if ( '' === $summary ) {
$summary = $event->post_title;
}
return apply_filters( 'sportspress_ical_feed_summary', $summary, $event );
}
@@ -915,13 +1028,14 @@ function tse_sp_event_export_get_feed_url( $args = array(), $feed_type = 'csv' )
$filters = tse_sp_event_export_normalize_request_args( $args );
$feed = 'ics' === sanitize_key( $feed_type ) ? 'sp-ics' : 'sp-csv';
$query = array(
'feed' => $feed,
'format' => $filters['format'],
'team_id' => ! empty( $filters['team_ids'] ) ? implode( ',', $filters['team_ids'] ) : '',
'season_id' => ! empty( $filters['season_ids'] ) ? implode( ',', $filters['season_ids'] ) : '',
'league_id' => ! empty( $filters['league_ids'] ) ? implode( ',', $filters['league_ids'] ) : '',
'field_id' => ! empty( $filters['field_ids'] ) ? implode( ',', $filters['field_ids'] ) : '',
'columns' => implode( ',', $filters['columns'] ),
'feed' => $feed,
'format' => $filters['format'],
'team_id' => ! empty( $filters['team_ids'] ) ? implode( ',', $filters['team_ids'] ) : '',
'season_id' => ! empty( $filters['season_ids'] ) ? implode( ',', $filters['season_ids'] ) : '',
'league_id' => ! empty( $filters['league_ids'] ) ? implode( ',', $filters['league_ids'] ) : '',
'field_id' => ! empty( $filters['field_ids'] ) ? implode( ',', $filters['field_ids'] ) : '',
'title_format' => isset( $filters['title_format'] ) ? tse_sp_event_export_sanitize_title_format( $filters['title_format'] ) : 'selected_first',
'columns' => implode( ',', $filters['columns'] ),
);
return add_query_arg( array_filter( $query, 'strlen' ), home_url( '/' ) );

View File

@@ -83,11 +83,7 @@ if ( ! class_exists( 'Tony_Sportspress_GitHub_Updater' ) ) {
$remote_version = $this->normalize_version( $release['version'] );
$current_version = $this->normalize_version( TONY_SPORTSPRESS_ENHANCEMENTS_VERSION );
if ( version_compare( $remote_version, $current_version, '<=' ) ) {
return $transient;
}
$transient->response[ $this->plugin_basename ] = (object) array(
$plugin_data = (object) array(
'id' => $release['url'],
'slug' => $this->plugin_slug,
'plugin' => $this->plugin_basename,
@@ -102,6 +98,16 @@ if ( ! class_exists( 'Tony_Sportspress_GitHub_Updater' ) ) {
'translations' => array(),
);
if ( version_compare( $remote_version, $current_version, '<=' ) ) {
unset( $transient->response[ $this->plugin_basename ] );
$transient->no_update[ $this->plugin_basename ] = $plugin_data;
return $transient;
}
unset( $transient->no_update[ $this->plugin_basename ] );
$transient->response[ $this->plugin_basename ] = $plugin_data;
return $transient;
}

File diff suppressed because it is too large Load Diff

View File

@@ -86,11 +86,13 @@ function tse_sp_schedule_exporter_render_shortcode() {
$seasons = tse_sp_schedule_exporter_get_seasons();
$season_id = tse_sp_schedule_exporter_resolve_season_id( $seasons );
$teams = tse_sp_schedule_exporter_get_teams( $league_id, $season_id );
$team_id = tse_sp_schedule_exporter_resolve_team_id( $teams );
$fields = tse_sp_schedule_exporter_get_fields();
$field_id = tse_sp_schedule_exporter_resolve_field_id( $fields );
$export_type = tse_sp_schedule_exporter_resolve_export_type();
$subformat = tse_sp_schedule_exporter_resolve_subformat();
$team_ids = tse_sp_schedule_exporter_resolve_team_ids( $teams );
$fields = tse_sp_schedule_exporter_get_fields();
$field_id = tse_sp_schedule_exporter_resolve_field_id( $fields );
$export_type = tse_sp_schedule_exporter_resolve_export_type();
$subformat = tse_sp_schedule_exporter_resolve_subformat();
$title_format = tse_sp_schedule_exporter_resolve_title_format();
$black_white = tse_sp_schedule_exporter_resolve_black_white();
if ( empty( $teams ) ) {
return '<p>' . esc_html__( 'No SportsPress teams match the selected league and season.', 'tonys-sportspress-enhancements' ) . '</p>';
@@ -115,6 +117,15 @@ function tse_sp_schedule_exporter_render_shortcode() {
<p class="description"><?php esc_html_e( 'CSV builds a feed URL, iCal Link builds a subscription URL, and Printable opens the printable page.', 'tonys-sportspress-enhancements' ); ?></p>
</div>
<div data-title-format-wrap="1" style="margin-bottom:16px;">
<label for="tse-public-title-format"><strong><?php esc_html_e( 'Title Format', 'tonys-sportspress-enhancements' ); ?></strong></label><br />
<select id="tse-public-title-format" name="title_format">
<option value="selected_first" <?php selected( $title_format, 'selected_first' ); ?>><?php esc_html_e( 'Selected Teams First', 'tonys-sportspress-enhancements' ); ?></option>
<option value="matchup" <?php selected( $title_format, 'matchup' ); ?>><?php esc_html_e( 'Matchup', 'tonys-sportspress-enhancements' ); ?></option>
</select>
<p class="description"><?php esc_html_e( 'Selected Teams First shows only the opponent when one team is selected; Matchup always shows away at home.', 'tonys-sportspress-enhancements' ); ?></p>
</div>
<div data-subformat-wrap="1" style="margin-bottom:16px;">
<label for="tse-public-subformat"><strong><?php esc_html_e( 'CSV Layout', 'tonys-sportspress-enhancements' ); ?></strong></label><br />
<select id="tse-public-subformat" name="subformat">
@@ -124,7 +135,7 @@ function tse_sp_schedule_exporter_render_shortcode() {
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'Matchup is away vs home. Team is opponent-based and requires one specific team.', 'tonys-sportspress-enhancements' ); ?></p>
<p class="description"><?php esc_html_e( 'Matchup is away vs home. Team puts a selected team first with vs or at as the separator.', 'tonys-sportspress-enhancements' ); ?></p>
</div>
<div style="margin-bottom:16px;">
@@ -152,14 +163,14 @@ function tse_sp_schedule_exporter_render_shortcode() {
<div style="margin-bottom:16px;">
<label for="tse-public-team"><strong><?php esc_html_e( 'Team', 'tonys-sportspress-enhancements' ); ?></strong></label><br />
<select id="tse-public-team" name="team_id">
<option value="0"><?php esc_html_e( 'All teams', 'tonys-sportspress-enhancements' ); ?></option>
<select id="tse-public-team" name="team_id[]" multiple="multiple" size="<?php echo esc_attr( (string) min( 8, max( 3, count( $teams ) ) ) ); ?>">
<?php foreach ( $teams as $team ) : ?>
<option value="<?php echo esc_attr( (string) $team->ID ); ?>" <?php selected( $team_id, (int) $team->ID ); ?>>
<option value="<?php echo esc_attr( (string) $team->ID ); ?>" <?php selected( in_array( (int) $team->ID, $team_ids, true ) ); ?>>
<?php echo esc_html( $team->post_title ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'Select one or more teams. Team layout keeps one selected team first for each game.', 'tonys-sportspress-enhancements' ); ?></p>
</div>
<div style="margin-bottom:16px;">
@@ -174,15 +185,36 @@ function tse_sp_schedule_exporter_render_shortcode() {
</select>
</div>
<div style="margin-bottom:16px;">
<label for="tse-public-month-pages" style="display:inline-flex;align-items:center;gap:6px;">
<input id="tse-public-month-pages" type="checkbox" name="month_pages" value="1" <?php checked( isset( $_GET['month_pages'] ) && '1' === (string) wp_unslash( $_GET['month_pages'] ) ); ?> />
<strong><?php esc_html_e( 'Print each month on its own page', 'tonys-sportspress-enhancements' ); ?></strong>
</label>
</div>
<div style="margin-bottom:16px;">
<label for="tse-public-black-white" style="display:inline-flex;align-items:center;gap:6px;">
<input id="tse-public-black-white" type="checkbox" name="black_white" value="1" <?php checked( $black_white ); ?> />
<strong><?php esc_html_e( 'High-legibility greyscale printable calendar', 'tonys-sportspress-enhancements' ); ?></strong>
</label>
</div>
</form>
<?php tse_sp_schedule_exporter_render_column_picker( 'matchup', 'public', $subformat ); ?>
<?php tse_sp_schedule_exporter_render_column_picker( 'team', 'public', $subformat ); ?>
<?php
$csv_url = tse_sp_event_export_get_feed_url( array( 'team_id' => $team_id, 'season_id' => $season_id, 'league_id' => $league_id, 'field_id' => $field_id, 'format' => $subformat ), 'csv' );
$ics_url = tse_sp_event_export_get_feed_url( array( 'team_id' => $team_id, 'season_id' => $season_id, 'league_id' => $league_id, 'field_id' => $field_id ), 'ics' );
$print_url = tse_sp_schedule_exporter_get_printable_url( $team_id, $season_id, 'letter', $league_id );
$feed_args = array(
'team_id' => $team_ids,
'season_id' => $season_id,
'league_id' => $league_id,
'field_id' => $field_id,
'title_format' => $title_format,
);
$csv_url = tse_sp_event_export_get_feed_url( array_merge( $feed_args, array( 'format' => $subformat ) ), 'csv' );
$ics_url = tse_sp_event_export_get_feed_url( $feed_args, 'ics' );
$print_url = tse_sp_schedule_exporter_get_printable_url( $team_ids, $season_id, 'letter', $league_id, false, $field_id, false, 'name', 'name', $title_format, $black_white );
$current_url = tse_sp_schedule_exporter_get_output_url( $export_type, $csv_url, $ics_url, $print_url );
?>
<div style="display:flex;align-items:center;gap:8px;max-width:100%;margin-top:16px;">
@@ -331,26 +363,34 @@ function tse_sp_schedule_exporter_get_teams( $league_id = 0, $season_id = 0 ) {
}
/**
* Resolve selected team ID.
* Resolve selected team IDs.
*
* @param WP_Post[] $teams Team posts.
* @return int
* @return int[]
*/
function tse_sp_schedule_exporter_resolve_team_id( $teams ) {
$requested = isset( $_GET['team_id'] ) ? absint( wp_unslash( $_GET['team_id'] ) ) : 0;
if ( $requested > 0 && 'sp_team' === get_post_type( $requested ) ) {
foreach ( $teams as $team ) {
if ( $team instanceof WP_Post && (int) $team->ID === $requested ) {
return $requested;
}
function tse_sp_schedule_exporter_resolve_team_ids( $teams ) {
$available = array();
foreach ( $teams as $team ) {
if ( $team instanceof WP_Post ) {
$available[] = (int) $team->ID;
}
}
if ( isset( $teams[0] ) && $teams[0] instanceof WP_Post ) {
return (int) $teams[0]->ID;
$requested = array();
if ( isset( $_GET['team_id'] ) ) {
$requested = tse_sp_event_export_parse_id_list( wp_unslash( $_GET['team_id'] ) );
}
return 0;
$selected = array_values( array_intersect( $requested, $available ) );
if ( ! empty( $selected ) ) {
return $selected;
}
if ( isset( $available[0] ) ) {
return array( (int) $available[0] );
}
return array();
}
/**
@@ -543,6 +583,38 @@ function tse_sp_schedule_exporter_resolve_subformat() {
return tse_sp_event_export_sanitize_format( $requested );
}
/**
* Resolve selected title format.
*
* @return string
*/
function tse_sp_schedule_exporter_resolve_title_format() {
$requested = isset( $_GET['title_format'] ) ? sanitize_key( wp_unslash( $_GET['title_format'] ) ) : 'selected_first';
return tse_sp_schedule_exporter_resolve_title_format_value( $requested );
}
/**
* Normalize a title format value.
*
* @param string $value Raw value.
* @return string
*/
function tse_sp_schedule_exporter_resolve_title_format_value( $value ) {
$value = sanitize_key( (string) $value );
return in_array( $value, tse_sp_event_export_get_title_formats(), true ) ? $value : 'selected_first';
}
/**
* Resolve whether printable URLs should use black-and-white styling.
*
* @return bool
*/
function tse_sp_schedule_exporter_resolve_black_white() {
return isset( $_GET['black_white'] ) && '1' === (string) wp_unslash( $_GET['black_white'] );
}
/**
* Get current output URL for the selected export type.
*
@@ -700,17 +772,29 @@ function tse_sp_schedule_exporter_get_primary_venue( $event_id ) {
* @param int $season_id Season ID.
* @param string $paper Paper size.
* @param int $league_id League ID.
* @param string $team_label_mode Team label mode.
* @param string $field_label_mode Field label mode.
* @param string $title_format Title format.
* @param bool $black_white Whether to use black-and-white printable styling.
* @return string
*/
function tse_sp_schedule_exporter_get_printable_url( $team_id, $season_id, $paper, $league_id = 0, $autoprint = false ) {
function tse_sp_schedule_exporter_get_printable_url( $team_id, $season_id, $paper, $league_id = 0, $autoprint = false, $field_id = 0, $month_pages = false, $team_label_mode = 'name', $field_label_mode = 'name', $title_format = 'selected_first', $black_white = false ) {
$team_ids = is_array( $team_id ) ? array_values( array_filter( array_map( 'absint', $team_id ) ) ) : array( absint( $team_id ) );
return add_query_arg(
array(
Tony_Sportspress_Printable_Calendars::QUERY_FLAG => '1',
'sp_team' => (string) absint( $team_id ),
'sp_team' => implode( ',', $team_ids ),
'sp_season' => $season_id > 0 ? (string) absint( $season_id ) : '',
'sp_league' => $league_id > 0 ? (string) absint( $league_id ) : '',
'sp_field' => $field_id > 0 ? (string) absint( $field_id ) : '',
'team_label' => sanitize_key( (string) $team_label_mode ),
'field_label' => sanitize_key( (string) $field_label_mode ),
'title_format' => tse_sp_schedule_exporter_resolve_title_format_value( $title_format ),
'paper' => $paper,
'autoprint' => $autoprint ? '1' : '',
'month_pages' => $month_pages ? '1' : '',
'black_white' => $black_white ? '1' : '',
),
home_url( '/' )
);
@@ -758,17 +842,28 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) {
var league = form.querySelector('[name="league_id"]');
var season = form.querySelector('[name="season_id"]');
var team = form.querySelector('[name="team_id"]');
var team = form.querySelector('[name="team_id[]"], [name="team_id"]');
var exportType = form.querySelector('[name="export_type"]');
var subformat = form.querySelector('[name="subformat"]');
var titleFormat = form.querySelector('[name="title_format"]');
var field = form.querySelector('[name="field_id"]');
var monthPages = form.querySelector('[name="month_pages"]');
var blackWhite = form.querySelector('[name="black_white"]');
var outputUrl = scope.querySelector('.tse-output-url');
var openButton = scope.querySelector('.tse-open-link');
var iosButton = scope.querySelector('.tse-ics-ios-link');
var androidButton = scope.querySelector('.tse-ics-android-link');
var outputNote = scope.querySelector('.tse-output-note');
var copyButton = scope.querySelector('.tse-copy-link');
var teamValue = team ? (team.value || '0') : '0';
var teamValues = team ? Array.prototype.slice.call(team.selectedOptions || []).map(function(option){
return option.value;
}).filter(function(value){
return value && value !== '0';
}) : [];
if (!teamValues.length && team && team.value && team.value !== '0') {
teamValues = [team.value];
}
var teamValue = teamValues.length ? teamValues.join(',') : '0';
var activeSubformat = subformat ? (subformat.value || 'matchup') : 'matchup';
var selectedExportType = exportType ? (exportType.value || 'csv') : 'csv';
@@ -792,6 +887,7 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) {
if (season) csvUrl.searchParams.set('season_id', season.value || '0');
if (team) csvUrl.searchParams.set('team_id', teamValue);
if (field) csvUrl.searchParams.set('field_id', field.value || '0');
if (titleFormat) csvUrl.searchParams.set('title_format', titleFormat.value || 'selected_first');
csvUrl.searchParams.set('format', activeSubformat);
var columns = Array.prototype.slice.call(scope.querySelectorAll('[data-columns-format="' + activeSubformat + '"]:checked')).map(function(input){
return input.value;
@@ -808,6 +904,7 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) {
if (season) icsUrl.searchParams.set('season_id', season.value || '0');
if (team) icsUrl.searchParams.set('team_id', teamValue);
if (field) icsUrl.searchParams.set('field_id', field.value || '0');
if (titleFormat) icsUrl.searchParams.set('title_format', titleFormat.value || 'selected_first');
icsUrl.searchParams.delete('format');
icsUrl.searchParams.delete('columns');
}
@@ -816,6 +913,18 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) {
if (league) printUrl.searchParams.set('sp_league', league.value || '0');
if (season) printUrl.searchParams.set('sp_season', season.value || '0');
if (team) printUrl.searchParams.set('sp_team', teamValue);
if (field) printUrl.searchParams.set('sp_field', field.value || '0');
if (titleFormat) printUrl.searchParams.set('title_format', titleFormat.value || 'selected_first');
if (monthPages && monthPages.checked) {
printUrl.searchParams.set('month_pages', '1');
} else {
printUrl.searchParams.delete('month_pages');
}
if (blackWhite && blackWhite.checked) {
printUrl.searchParams.set('black_white', '1');
} else {
printUrl.searchParams.delete('black_white');
}
printUrl.searchParams.set('paper', 'letter');
}
@@ -833,11 +942,11 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) {
resolvedUrl = printUrl.toString();
if (teamValue === '0') {
disabled = true;
note = 'Printable requires a specific team. All teams is not supported.';
note = 'Printable requires at least one selected team.';
}
} else if (selectedExportType === 'csv' && activeSubformat === 'team' && teamValue === '0') {
disabled = true;
note = 'CSV team layout requires a specific team. All teams is not supported.';
note = 'CSV team layout requires at least one selected team.';
}
if (outputUrl) {
@@ -885,10 +994,10 @@ function tse_sp_schedule_exporter_render_link_sync_script( $echo = false ) {
syncLinks(scope);
scope.querySelectorAll('.tse-schedule-exporter-form select').forEach(function(select){
select.addEventListener('change', function(){
if (select.dataset.autoSubmit === '1') {
select.form.submit();
scope.querySelectorAll('.tse-schedule-exporter-form select, .tse-schedule-exporter-form input[type="checkbox"]').forEach(function(input){
input.addEventListener('change', function(){
if (input.dataset.autoSubmit === '1') {
input.form.submit();
return;
}

View File

@@ -2,7 +2,7 @@
/**
* Venue term metadata support.
*
* Adds short name and abbreviation fields to SportsPress venues.
* Adds short name, abbreviation, and ground rules fields to SportsPress venues.
*/
if ( ! defined( 'ABSPATH' ) ) {
@@ -40,9 +40,319 @@ function tony_sportspress_register_venue_term_meta() {
},
)
);
register_term_meta(
'sp_venue',
'tse_ground_rules',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => 'wp_kses_post',
'show_in_rest' => true,
'auth_callback' => static function() {
return current_user_can( 'manage_categories' );
},
)
);
}
add_action( 'init', 'tony_sportspress_register_venue_term_meta' );
/**
* Determine whether the field page should show its event list.
*
* The setting is stored in the core SportsPress Events settings page under
* the venue/field section.
*
* @return bool
*/
function tony_sportspress_field_event_list_enabled() {
$enabled = get_option( 'sportspress_event_show_venue_list', 'yes' ) === 'yes';
return (bool) apply_filters( 'tony_sportspress_field_event_list_enabled', $enabled );
}
/**
* Get the venue map caption text.
*
* @return string
*/
function tony_sportspress_get_venue_map_caption() {
return (string) apply_filters( 'tony_sportspress_venue_map_caption', __( 'Field Map', 'tonys-sportspress-enhancements' ) );
}
/**
* Add the field page event list setting to SportsPress > Settings > Games > Fields.
*
* @param array $options Existing venue settings.
* @return array
*/
function tony_sportspress_add_venue_settings( $options ) {
$options[] = array(
'title' => esc_attr__( 'Event List', 'tonys-sportspress-enhancements' ),
'desc' => esc_attr__( 'Display event list on field pages', 'tonys-sportspress-enhancements' ),
'id' => 'sportspress_event_show_venue_list',
'default' => 'yes',
'type' => 'checkbox',
);
return $options;
}
add_filter( 'sportspress_venue_options', 'tony_sportspress_add_venue_settings' );
/**
* Enqueue the visual editor on the venue taxonomy screen.
*
* This turns the built-in description textarea into the standard WordPress
* TinyMCE editor so venue descriptions can use markup and links.
*
* @param string $hook_suffix Current admin page hook.
*/
function tony_sportspress_enqueue_venue_description_editor( $hook_suffix ) {
if ( ! in_array( $hook_suffix, array( 'edit-tags.php', 'term.php' ), true ) ) {
return;
}
$taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : '';
if ( 'sp_venue' !== $taxonomy ) {
return;
}
if ( ! function_exists( 'wp_enqueue_editor' ) ) {
return;
}
wp_enqueue_editor();
$script = <<<'JS'
(function() {
function initEditor(id) {
var textarea = document.getElementById(id);
if (!textarea || textarea.dataset.tseEditorInitialized) {
return;
}
textarea.dataset.tseEditorInitialized = '1';
window.wp.editor.initialize(id, {
tinymce: {
wpautop: true,
menubar: false,
statusbar: true,
toolbar1: 'formatselect,bold,italic,bullist,numlist,blockquote,alignleft,aligncenter,alignright,link,unlink,wp_more,spellchecker,fullscreen',
toolbar2: 'strikethrough,hr,forecolor,pastetext,removeformat,charmap,outdent,indent,undo,redo,wp_help',
block_formats: 'Paragraph=p; Heading 2=h2; Heading 3=h3; Heading 4=h4; Preformatted=pre',
},
quicktags: true,
mediaButtons: true
});
}
document.addEventListener('DOMContentLoaded', function() {
if (!window.wp || !wp.editor) {
return;
}
// Different taxonomy screens use different IDs for the same description field.
initEditor('description');
initEditor('tag-description');
});
})();
JS;
wp_add_inline_script( 'editor', $script );
}
add_action( 'admin_enqueue_scripts', 'tony_sportspress_enqueue_venue_description_editor' );
/**
* Hide the built-in venue archive description output.
*
* Venue content is rendered as SportsPress-style sections instead of inside
* the archive header.
*
* @param string $description Archive description HTML.
* @return string
*/
function tony_sportspress_hide_venue_archive_description( $description ) {
if ( is_tax( 'sp_venue' ) ) {
return '';
}
return $description;
}
add_filter( 'get_the_archive_description', 'tony_sportspress_hide_venue_archive_description', 99 );
/**
* Determine whether the current venue has map coordinates.
*
* @return bool
*/
function tony_sportspress_current_venue_has_map() {
$term = get_queried_object();
if ( ! $term instanceof WP_Term ) {
return false;
}
$latitude = trim( (string) get_term_meta( $term->term_id, 'sp_latitude', true ) );
$longitude = trim( (string) get_term_meta( $term->term_id, 'sp_longitude', true ) );
return '' !== $latitude && '' !== $longitude;
}
/**
* Render the venue ground rules section.
*
* @return void
*/
function tony_sportspress_render_venue_ground_rules_section() {
if ( ! is_tax( 'sp_venue' ) ) {
return;
}
$term = get_queried_object();
if ( ! $term instanceof WP_Term ) {
return;
}
$ground_rules = get_term_meta( $term->term_id, 'tse_ground_rules', true );
if ( ! is_string( $ground_rules ) || '' === trim( $ground_rules ) ) {
return;
}
$ground_rules = apply_filters( 'the_content', $ground_rules );
echo '<div class="sp-section-content sp-section-content-details"><div class="sp-template sp-template-venue-details tse-ground-rules"><h4 class="sp-table-caption">' . esc_html__( 'Ground Rules', 'tonys-sportspress-enhancements' ) . '</h4><div class="sp-table-wrapper tse-ground-rules-content">' . $ground_rules . '</div></div></div>';
}
add_action( 'sportspress_before_venue_map', 'tony_sportspress_render_venue_ground_rules_section', 5 );
/**
* Open the venue map section wrapper.
*
* @return void
*/
function tony_sportspress_open_venue_map_section() {
if ( ! is_tax( 'sp_venue' ) || ! tony_sportspress_current_venue_has_map() ) {
return;
}
$GLOBALS['tse_venue_map_section_open'] = true;
echo '<div class="sp-section-content sp-section-content-venue"><div class="sp-template sp-template-venue-map"><h4 class="sp-table-caption tse-venue-map-caption">' . esc_html( tony_sportspress_get_venue_map_caption() ) . '</h4><div class="sp-table-wrapper tse-venue-map-content">';
}
add_action( 'sportspress_before_venue_map', 'tony_sportspress_open_venue_map_section', 15 );
/**
* Close the venue map section wrapper.
*
* @return void
*/
function tony_sportspress_close_venue_map_section() {
if ( empty( $GLOBALS['tse_venue_map_section_open'] ) ) {
return;
}
$GLOBALS['tse_venue_map_section_open'] = false;
echo '</div></div></div>';
}
add_action( 'sportspress_after_venue_map', 'tony_sportspress_close_venue_map_section', 5 );
/**
* Enqueue venue section styles.
*
* The venue map and ground rules are rendered as SportsPress-style sections
* rather than archive header content, so they need section-scoped typography
* and list styling.
*
* @return void
*/
function tony_sportspress_enqueue_venue_section_styles() {
if ( ! is_tax( 'sp_venue' ) ) {
return;
}
wp_register_style( 'tony-sportspress-venue-sections', false, array(), TONY_SPORTSPRESS_ENHANCEMENTS_VERSION );
wp_enqueue_style( 'tony-sportspress-venue-sections' );
$event_list_display = tony_sportspress_field_event_list_enabled() ? 'block' : 'none';
$css = <<<CSS
body.tax-sp_venue .sp-template-venue-map .sp-table-wrapper,
body.tax-sp_venue .sp-template-venue-details .sp-table-wrapper {
background: #fff;
border: 1px solid #e0e0e0;
border-top: 0;
}
body.tax-sp_venue .site-main article.sp_event,
body.tax-sp_venue .sp-template-event-list,
body.tax-sp_venue .sp-template-event-blocks,
body.tax-sp_venue .sp-template-event-logos,
body.tax-sp_venue .sp-template-event-results {
display: {$event_list_display};
}
body.tax-sp_venue .sp-template-venue-map .sp-google-map {
display: block;
margin: 0;
}
body.tax-sp_venue .tse-ground-rules-content {
line-height: 1.7;
padding: 1em 15px 0;
}
body.tax-sp_venue .tse-ground-rules-content h2,
body.tax-sp_venue .tse-ground-rules-content h3,
body.tax-sp_venue .tse-ground-rules-content h4,
body.tax-sp_venue .tse-ground-rules-content h5,
body.tax-sp_venue .tse-ground-rules-content h6 {
margin: 1.25em 0 0.55em;
font-weight: 700;
line-height: 1.25;
text-transform: none;
}
body.tax-sp_venue .tse-ground-rules-content h2 {
font-size: 2rem;
}
body.tax-sp_venue .tse-ground-rules-content h3 {
font-size: 1.75rem;
}
body.tax-sp_venue .tse-ground-rules-content h4 {
font-size: 1.5rem;
}
body.tax-sp_venue .tse-ground-rules-content p,
body.tax-sp_venue .tse-ground-rules-content ul,
body.tax-sp_venue .tse-ground-rules-content ol,
body.tax-sp_venue .tse-ground-rules-content blockquote,
body.tax-sp_venue .tse-ground-rules-content pre {
margin: 0 0 1em;
}
body.tax-sp_venue .tse-ground-rules-content ul,
body.tax-sp_venue .tse-ground-rules-content ol {
margin-left: 1.5em;
padding-left: 1.5em;
list-style-position: outside;
}
body.tax-sp_venue .tse-ground-rules-content ul {
list-style: disc;
}
body.tax-sp_venue .tse-ground-rules-content ol {
list-style: decimal;
}
body.tax-sp_venue .tse-ground-rules-content li {
margin: 0.35em 0;
}
CSS;
wp_add_inline_style( 'tony-sportspress-venue-sections', $css );
}
add_action( 'wp_enqueue_scripts', 'tony_sportspress_enqueue_venue_section_styles' );
/**
* Render add-form fields for venue metadata.
*/
@@ -58,6 +368,24 @@ function tony_sportspress_add_venue_meta_fields() {
<input name="tse_abbreviation" id="tse_abbreviation" type="text" value="" maxlength="20" />
<p><?php esc_html_e( 'Optional abbreviation such as CC East or Field 1.', 'tonys-sportspress-enhancements' ); ?></p>
</div>
<div class="form-field term-ground-rules-wrap">
<label for="tse_ground_rules"><?php esc_html_e( 'Ground Rules', 'tonys-sportspress-enhancements' ); ?></label>
<?php
wp_editor(
'',
'tse_ground_rules',
array(
'textarea_name' => 'tse_ground_rules',
'textarea_rows' => 10,
'media_buttons' => true,
'teeny' => false,
'quicktags' => true,
'drag_drop_upload' => true,
)
);
?>
<p><?php esc_html_e( 'Supports headings, lists, links, and images.', 'tonys-sportspress-enhancements' ); ?></p>
</div>
<?php
}
add_action( 'sp_venue_add_form_fields', 'tony_sportspress_add_venue_meta_fields' );
@@ -70,6 +398,7 @@ add_action( 'sp_venue_add_form_fields', 'tony_sportspress_add_venue_meta_fields'
function tony_sportspress_edit_venue_meta_fields( $term ) {
$short_name = get_term_meta( $term->term_id, 'tse_short_name', true );
$abbreviation = get_term_meta( $term->term_id, 'tse_abbreviation', true );
$ground_rules = get_term_meta( $term->term_id, 'tse_ground_rules', true );
?>
<tr class="form-field term-short-name-wrap">
<th scope="row">
@@ -89,6 +418,28 @@ function tony_sportspress_edit_venue_meta_fields( $term ) {
<p class="description"><?php esc_html_e( 'Optional abbreviation such as CC East or Field 1.', 'tonys-sportspress-enhancements' ); ?></p>
</td>
</tr>
<tr class="form-field term-ground-rules-wrap">
<th scope="row">
<label for="tse_ground_rules"><?php esc_html_e( 'Ground Rules', 'tonys-sportspress-enhancements' ); ?></label>
</th>
<td>
<?php
wp_editor(
(string) $ground_rules,
'tse_ground_rules',
array(
'textarea_name' => 'tse_ground_rules',
'textarea_rows' => 10,
'media_buttons' => true,
'teeny' => false,
'quicktags' => true,
'drag_drop_upload' => true,
)
);
?>
<p class="description"><?php esc_html_e( 'Supports headings, lists, links, and images.', 'tonys-sportspress-enhancements' ); ?></p>
</td>
</tr>
<?php
}
add_action( 'sp_venue_edit_form_fields', 'tony_sportspress_edit_venue_meta_fields' );
@@ -109,8 +460,12 @@ function tony_sportspress_save_venue_meta_fields( $term_id ) {
$abbreviation = isset( $_POST['tse_abbreviation'] ) ? sanitize_text_field( wp_unslash( $_POST['tse_abbreviation'] ) ) : '';
$abbreviation = is_string( $abbreviation ) ? trim( $abbreviation ) : '';
$ground_rules = isset( $_POST['tse_ground_rules'] ) ? wp_kses_post( wp_unslash( $_POST['tse_ground_rules'] ) ) : '';
$ground_rules = is_string( $ground_rules ) ? trim( $ground_rules ) : '';
update_term_meta( $term_id, 'tse_short_name', $short_name );
update_term_meta( $term_id, 'tse_abbreviation', $abbreviation );
update_term_meta( $term_id, 'tse_ground_rules', $ground_rules );
}
add_action( 'created_sp_venue', 'tony_sportspress_save_venue_meta_fields' );
add_action( 'edited_sp_venue', 'tony_sportspress_save_venue_meta_fields' );

2679
includes/sp-webhooks.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "tonys-sportspress-enhancements",
"version": "0.1.5",
"version": "0.1.15",
"main": "Gruntfile.js",
"author": "YOUR NAME HERE",
"scripts" : {

View File

@@ -0,0 +1,222 @@
<?php
/**
* Tests for the SportsPress event image generator.
*
* @package Tonys_Sportspress_Enhancements
*/
/**
* Featured image generator tests.
*/
class Test_Featured_Image_Generator extends WP_UnitTestCase {
/**
* Temp files created during a test.
*
* @var string[]
*/
private $temp_files = array();
/**
* Clean up temp files.
*/
public function tear_down(): void {
foreach ( $this->temp_files as $file ) {
if ( file_exists( $file ) ) {
unlink( $file );
}
}
$this->temp_files = array();
parent::tear_down();
}
/**
* Create a post.
*
* @param string $type Post type.
* @param string $title Title.
* @return int
*/
private function create_post_of_type( $type, $title ) {
return self::factory()->post->create(
array(
'post_type' => $type,
'post_title' => $title,
'post_status' => 'publish',
)
);
}
/**
* Create a small raster fixture.
*
* @param string $extension File extension.
* @return string
*/
private function create_raster_fixture( $extension ) {
$image = imagecreatetruecolor( 24, 24 );
$red = imagecolorallocate( $image, 200, 0, 0 );
imagefilledrectangle( $image, 0, 0, 23, 23, $red );
$file = tempnam( sys_get_temp_dir(), 'sp-img-' );
$path = $file . '.' . $extension;
rename( $file, $path );
switch ( $extension ) {
case 'jpg':
imagejpeg( $image, $path );
break;
case 'gif':
imagegif( $image, $path );
break;
case 'webp':
imagewebp( $image, $path );
break;
case 'png':
default:
imagepng( $image, $path );
break;
}
asc_sp_event_image_destroy( $image );
$this->temp_files[] = $path;
return $path;
}
/**
* Invalid IDs and non-event posts produce request errors.
*/
public function test_invalid_and_non_event_requests_prepare_404_errors() {
$this->assertWPError( asc_sp_event_prepare_image_request( 999999 ) );
$post_id = $this->create_post_of_type( 'post', 'Regular Post' );
$error = asc_sp_event_prepare_image_request( $post_id );
$this->assertWPError( $error );
$this->assertSame( 'invalid_event', $error->get_error_code() );
}
/**
* Missing team logo paths fall back to generated text and valid dimensions.
*/
public function test_missing_logo_path_generates_png_with_expected_dimensions() {
$image_data = generate_bisected_image( '#123456', '#abcdef', '/missing-left.png', '/missing-right.png', 'Hawks', 'Electrons' );
$image = imagecreatefromstring( $image_data );
$this->assertNotFalse( $image );
$this->assertSame( 1200, imagesx( $image ) );
$this->assertSame( 628, imagesy( $image ) );
asc_sp_event_image_destroy( $image );
}
/**
* Square image variant generates square PNG dimensions.
*/
public function test_square_variant_generates_expected_dimensions() {
$dimensions = asc_sp_event_image_variant_dimensions( 'square' );
$image_data = generate_bisected_image( '#123456', '#abcdef', '/missing-left.png', '/missing-right.png', 'Hawks', 'Electrons', $dimensions['width'], $dimensions['height'] );
$image = imagecreatefromstring( $image_data );
$this->assertNotFalse( $image );
$this->assertSame( 1200, imagesx( $image ) );
$this->assertSame( 1200, imagesy( $image ) );
asc_sp_event_image_destroy( $image );
}
/**
* Raster loader supports common GD-backed formats.
*/
public function test_raster_loader_supports_common_formats_when_available() {
$formats = array(
'png' => 'imagecreatefrompng',
'jpg' => 'imagecreatefromjpeg',
'gif' => 'imagecreatefromgif',
);
if ( function_exists( 'imagewebp' ) && function_exists( 'imagecreatefromwebp' ) ) {
$formats['webp'] = 'imagecreatefromwebp';
}
foreach ( $formats as $extension => $function ) {
if ( ! function_exists( $function ) ) {
continue;
}
$path = $this->create_raster_fixture( $extension );
$image = asc_sp_event_image_create_from_file( $path );
$this->assertNotFalse( $image, "Failed loading {$extension}" );
$this->assertSame( 24, imagesx( $image ) );
$this->assertSame( 24, imagesy( $image ) );
asc_sp_event_image_destroy( $image );
}
}
/**
* Bundled sporty font is available for fallback text.
*/
public function test_bundled_bebas_neue_font_is_available() {
$this->assertFileExists( asc_sp_event_image_font_path() );
$this->assertIsReadable( asc_sp_event_image_font_path() );
}
/**
* Prepared event request includes fallback text for missing logos.
*/
public function test_prepare_image_request_uses_team_short_name_fallbacks() {
$team1 = $this->create_post_of_type( 'sp_team', 'Hawks' );
$team2 = $this->create_post_of_type( 'sp_team', 'Electrons' );
$event = $this->create_post_of_type( 'sp_event', 'Hawks vs Electrons' );
add_post_meta( $event, 'sp_team', $team1 );
add_post_meta( $event, 'sp_team', $team2 );
$request = asc_sp_event_prepare_image_request( $event );
$this->assertIsArray( $request );
$this->assertSame( 'Hawks', $request['team1_fallback'] );
$this->assertSame( 'Electrons', $request['team2_fallback'] );
$this->assertSame( '', $request['team1_logo'] );
$this->assertSame( '', $request['team2_logo'] );
}
/**
* Invalid colors are safely normalized.
*/
public function test_invalid_colors_fall_back_to_configured_defaults() {
$this->assertSame( '#4B5563', asc_sp_event_image_color( 'not-a-color' ) );
$this->assertSame( '#6B7280', asc_sp_event_image_color( 'not-a-color', '#6B7280' ) );
$this->assertSame( '#112233', asc_sp_event_image_color( '#112233' ) );
}
/**
* Image cache keys include the generator version and style hash.
*/
public function test_prepare_image_request_uses_versioned_style_cache_key() {
$team1 = $this->create_post_of_type( 'sp_team', 'Hawks' );
$team2 = $this->create_post_of_type( 'sp_team', 'Electrons' );
$event = $this->create_post_of_type( 'sp_event', 'Hawks vs Electrons' );
add_post_meta( $event, 'sp_team', $team1 );
add_post_meta( $event, 'sp_team', $team2 );
$request = asc_sp_event_prepare_image_request( $event );
$this->assertStringStartsWith( 'team_image_v' . ASC_SP_EVENT_IMAGE_CACHE_VERSION . '_' . asc_sp_event_image_cache_style_hash(), $request['cache_key'] );
$this->assertSame( 'wide', $request['variant'] );
$this->assertSame( 1200, $request['width'] );
$this->assertSame( 628, $request['height'] );
$square_request = asc_sp_event_prepare_image_request( $event, 'square' );
$this->assertStringContainsString( '_square_', $square_request['cache_key'] );
$this->assertSame( 'square', $square_request['variant'] );
$this->assertSame( 1200, $square_request['width'] );
$this->assertSame( 1200, $square_request['height'] );
}
}

View File

@@ -0,0 +1,267 @@
<?php
/**
* Tests for SportsPress event Open Graph output.
*
* @package Tonys_Sportspress_Enhancements
*/
if ( ! class_exists( 'SP_Event' ) ) {
/**
* Minimal SportsPress event test double.
*/
class SP_Event {
/**
* Event post ID.
*
* @var int
*/
private $id;
/**
* Status values by event ID.
*
* @var array<int,string>
*/
public static $statuses = array();
/**
* Result values by event ID.
*
* @var array<int,array>
*/
public static $results = array();
/**
* Constructor.
*
* @param int $id Event post ID.
*/
public function __construct( $id ) {
$this->id = absint( $id );
}
/**
* Get event status.
*
* @return string
*/
public function status() {
return self::$statuses[ $this->id ] ?? '';
}
/**
* Get event results.
*
* @return array
*/
public function results() {
return self::$results[ $this->id ] ?? array();
}
}
}
/**
* Open Graph tests.
*/
class Test_Open_Graph_Tags extends WP_UnitTestCase {
/**
* Reset mock SportsPress state.
*/
public function set_up(): void {
parent::set_up();
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses = array();
SP_Event::$results = array();
}
update_option( 'sportspress_event_reverse_teams', 'no' );
update_option( 'sportspress_event_teams_delimiter', 'vs' );
}
/**
* Create a team.
*
* @param string $name Team name.
* @return int
*/
private function create_team( $name ) {
return self::factory()->post->create(
array(
'post_type' => 'sp_team',
'post_title' => $name,
)
);
}
/**
* Create an event.
*
* @param array $args Post args.
* @return int
*/
private function create_event( array $args = array() ) {
return self::factory()->post->create(
wp_parse_args(
$args,
array(
'post_type' => 'sp_event',
'post_title' => 'Test Event',
'post_status' => 'future',
'post_date' => '2026-05-02 13:00:00',
'post_content' => 'First pitch at one.',
)
)
);
}
/**
* Future event emits complete Open Graph data.
*/
public function test_future_event_emits_core_open_graph_values() {
$home = $this->create_team( 'Hawks' );
$away = $this->create_team( 'Electrons' );
$event = $this->create_event();
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses[ $event ] = 'future';
}
$meta = asc_sp_event_open_graph_data( $event );
$this->assertSame( 'article', $meta['type'] );
$this->assertStringContainsString( 'Hawks vs Electrons', $meta['title'] );
$this->assertStringContainsString( 'First pitch at one.', $meta['description'] );
$this->assertCount( 1, $meta['images'] );
$this->assertSame( '1200', $meta['images'][0]['width'] );
$this->assertSame( '628', $meta['images'][0]['height'] );
$this->assertSame( '1200', $meta['image_width'] );
$this->assertSame( '628', $meta['image_height'] );
$this->assertStringContainsString( '/head-to-head?post=' . $event, $meta['image'] );
$this->assertNotEmpty( $meta['url'] );
}
/**
* Postponed, cancelled, and TBD labels appear in title and description.
*
* @dataProvider status_provider
*
* @param string $status Status slug.
*/
public function test_schedule_status_appears_in_title_and_description( $status ) {
$home = $this->create_team( 'Hawks' );
$away = $this->create_team( 'Electrons' );
$event = $this->create_event();
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
update_post_meta( $event, 'sp_status', $status );
$meta = asc_sp_event_open_graph_data( $event );
$label = strtoupper( $status );
$this->assertStringStartsWith( $label, $meta['title'] );
$this->assertStringStartsWith( $label, $meta['description'] );
}
/**
* Status provider.
*
* @return array
*/
public function status_provider() {
return array(
array( 'postponed' ),
array( 'cancelled' ),
array( 'tbd' ),
);
}
/**
* Result events with scores emit score titles.
*/
public function test_result_event_with_scores_emits_score_title() {
$home = $this->create_team( 'Hawks' );
$away = $this->create_team( 'Electrons' );
$event = $this->create_event( array( 'post_status' => 'publish' ) );
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses[ $event ] = 'results';
SP_Event::$results[ $event ] = array(
0 => array( 'r' => 'R' ),
$home => array( 'r' => '7' ),
$away => array( 'r' => '4' ),
);
}
$meta = asc_sp_event_open_graph_data( $event );
$this->assertStringContainsString( 'Hawks 7-4 Electrons', $meta['title'] );
}
/**
* Missing teams/results/outcomes still produce valid data.
*/
public function test_missing_sportspress_data_does_not_break_meta_generation() {
$event = $this->create_event(
array(
'post_title' => 'Sparse Event',
'post_content' => '',
)
);
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses[ $event ] = 'results';
SP_Event::$results[ $event ] = array();
}
$meta = asc_sp_event_open_graph_data( $event );
$this->assertSame( 'Sparse Event', $meta['title'] );
$this->assertNotEmpty( $meta['description'] );
$this->assertSame( '1200', $meta['image_width'] );
}
/**
* HTML-heavy post content is stripped and escaped in rendered tags.
*/
public function test_description_strips_html_and_rendered_tags_are_escaped() {
$home = $this->create_team( 'Hawks "A"' );
$away = $this->create_team( 'Electrons <B>' );
$event = $this->create_event(
array(
'post_content' => '<script>alert("x")</script><p>Bring <strong>bats</strong> & gloves.</p>',
)
);
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
$meta = asc_sp_event_open_graph_data( $event );
$this->assertStringNotContainsString( '<script', $meta['description'] );
$this->assertStringContainsString( 'Bring bats & gloves.', $meta['description'] );
$GLOBALS['post'] = get_post( $event );
$GLOBALS['wp_query']->is_single = true;
ob_start();
custom_open_graph_tags_with_sportspress_integration();
$output = ob_get_clean();
$this->assertStringContainsString( 'og:image:width', $output );
$this->assertSame( 1, substr_count( $output, 'property="og:image" content=' ) );
$this->assertStringContainsString( 'content="628"', $output );
$this->assertStringContainsString( 'content="1200"', $output );
$this->assertStringContainsString( 'Hawks &quot;A&quot;', $output );
$this->assertStringNotContainsString( '<B>', $output );
$this->assertStringNotContainsString( '<script', $output );
}
}

View File

@@ -0,0 +1,341 @@
<?php
/**
* Tests for schedule exporter and printable schedule helpers.
*
* @package Tonys_Sportspress_Enhancements
*/
/**
* Schedule exporter tests.
*/
class Test_SP_Schedule_Exporter extends WP_UnitTestCase {
/**
* Original request globals.
*
* @var array
*/
private $original_get = array();
/**
* Set up shared test fixtures.
*/
public function set_up() {
parent::set_up();
$this->original_get = $_GET;
foreach ( array( 'sp_venue', 'sp_league', 'sp_season' ) as $taxonomy ) {
if ( ! taxonomy_exists( $taxonomy ) ) {
register_taxonomy( $taxonomy, 'sp_event' );
}
}
}
/**
* Restore request globals.
*/
public function tear_down() {
$_GET = $this->original_get;
parent::tear_down();
}
/**
* Create a SportsPress team post.
*
* @param string $name Team name.
* @return int
*/
private function create_team( $name ) {
return self::factory()->post->create(
array(
'post_type' => 'sp_team',
'post_status' => 'publish',
'post_title' => $name,
)
);
}
/**
* Create a SportsPress event with ordered teams.
*
* @param int $home_id Home team ID.
* @param int $away_id Away team ID.
* @param int[] $venue_ids Venue IDs.
* @return int
*/
private function create_event( $home_id, $away_id, $venue_ids = array() ) {
$event_id = self::factory()->post->create(
array(
'post_type' => 'sp_event',
'post_status' => 'publish',
'post_title' => 'Game',
'post_date' => '2026-05-20 18:00:00',
'post_date_gmt' => '2026-05-20 23:00:00',
)
);
add_post_meta( $event_id, 'sp_team', (string) $home_id );
add_post_meta( $event_id, 'sp_team', (string) $away_id );
if ( ! empty( $venue_ids ) ) {
wp_set_object_terms( $event_id, $venue_ids, 'sp_venue' );
}
return $event_id;
}
/**
* Multiple selected team IDs should be retained in request order.
*/
public function test_resolve_team_ids_accepts_multiple_request_values() {
$team_one = $this->create_team( 'Blue' );
$team_two = $this->create_team( 'Red' );
$teams = array( get_post( $team_one ), get_post( $team_two ) );
$_GET['team_id'] = array( (string) $team_one, (string) $team_two );
$this->assertSame( array( $team_one, $team_two ), tse_sp_schedule_exporter_resolve_team_ids( $teams ) );
}
/**
* Printable URLs should carry multiple team IDs and the selected field.
*/
public function test_printable_url_accepts_multiple_teams_and_field() {
$url = tse_sp_schedule_exporter_get_printable_url( array( 12, 34 ), 56, 'letter', 78, false, 90, true );
$query = array();
wp_parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query );
$this->assertSame( '12,34', $query['sp_team'] );
$this->assertSame( '90', $query['sp_field'] );
$this->assertSame( 'name', $query['team_label'] );
$this->assertSame( 'name', $query['field_label'] );
$this->assertSame( 'selected_first', $query['title_format'] );
$this->assertSame( '1', $query['month_pages'] );
}
/**
* Printable URLs should carry selected team and field label modes.
*/
public function test_printable_url_accepts_label_modes() {
$url = tse_sp_schedule_exporter_get_printable_url( 12, 56, 'letter', 78, false, 90, false, 'shortname', 'abbreviation' );
$query = array();
wp_parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query );
$this->assertSame( 'shortname', $query['team_label'] );
$this->assertSame( 'abbreviation', $query['field_label'] );
}
/**
* Printable URLs should carry black-and-white mode when requested.
*/
public function test_printable_url_accepts_black_white_mode() {
$url = tse_sp_schedule_exporter_get_printable_url( 12, 56, 'letter', 78, false, 90, false, 'name', 'name', 'selected_first', true );
$query = array();
wp_parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query );
$this->assertSame( '1', $query['black_white'] );
}
/**
* Single-team printable entries should keep the existing opponent perspective.
*/
public function test_printable_single_team_entries_keep_opponent_perspective() {
$home_id = $this->create_team( 'Home Team' );
$away_id = $this->create_team( 'Away Team' );
$venue_id = self::factory()->term->create( array( 'taxonomy' => 'sp_venue', 'name' => 'North Field' ) );
$this->create_event( $home_id, $away_id, array( $venue_id ) );
$printable = Tony_Sportspress_Printable_Calendars::instance();
$method = new ReflectionMethod( $printable, 'get_schedule_entries' );
$method->setAccessible( true );
$entries = $method->invoke( $printable, $home_id, 0, 0, array() );
$this->assertCount( 1, $entries );
$this->assertFalse( $entries[0]['is_matchup'] );
$this->assertTrue( $entries[0]['is_home'] );
$this->assertSame( 'Away Team', $entries[0]['opponent_name'] );
}
/**
* Printable entries should honor requested team and field labels.
*/
public function test_printable_entries_honor_label_modes() {
$home_id = $this->create_team( 'Home Team' );
$away_id = $this->create_team( 'Away Team' );
$venue_id = self::factory()->term->create( array( 'taxonomy' => 'sp_venue', 'name' => 'North Field' ) );
update_post_meta( $away_id, 'sp_abbreviation', 'AWY' );
update_term_meta( $venue_id, 'tse_abbreviation', 'NF' );
$this->create_event( $home_id, $away_id, array( $venue_id ) );
$printable = Tony_Sportspress_Printable_Calendars::instance();
$method = new ReflectionMethod( $printable, 'get_schedule_entries' );
$method->setAccessible( true );
$entries = $method->invoke( $printable, $home_id, 0, 0, array(), 'abbreviation', 'abbreviation' );
$this->assertCount( 1, $entries );
$this->assertSame( 'AWY', $entries[0]['opponent_name'] );
$this->assertSame( 'NF', $entries[0]['venue_label'] );
}
/**
* Team CSV layout should support multiple selected teams with away-first context.
*/
public function test_team_export_multiple_selected_teams_uses_away_team_first() {
$home_id = $this->create_team( 'Home Team' );
$away_id = $this->create_team( 'Away Team' );
$this->create_event( $home_id, $away_id );
$events = tse_sp_event_export_get_events(
array(
'team_ids' => array( $home_id, $away_id ),
)
);
$this->assertCount( 1, $events );
$this->assertSame( 'Away Team', $events[0]['team_name'] );
$this->assertSame( 'Away Team at Home Team', $events[0]['title'] );
$this->assertSame( 'at', $events[0]['separator'] );
$this->assertSame( 'Home Team', $events[0]['opponent_name'] );
$this->assertSame( 'Away', $events[0]['location_flag'] );
}
/**
* Team CSV layout should use vs when the selected team is home.
*/
public function test_team_export_home_selected_team_uses_vs_separator() {
$home_id = $this->create_team( 'Home Team' );
$away_id = $this->create_team( 'Away Team' );
$this->create_event( $home_id, $away_id );
$events = tse_sp_event_export_get_events(
array(
'team_ids' => array( $home_id ),
)
);
$this->assertCount( 1, $events );
$this->assertSame( 'Home Team', $events[0]['team_name'] );
$this->assertSame( 'Away Team', $events[0]['title'] );
$this->assertSame( 'vs', $events[0]['separator'] );
$this->assertSame( 'Away Team', $events[0]['opponent_name'] );
$this->assertSame( 'Home', $events[0]['location_flag'] );
}
/**
* Team CSV layout should use at when the selected team is away.
*/
public function test_team_export_away_selected_team_uses_at_separator() {
$home_id = $this->create_team( 'Home Team' );
$away_id = $this->create_team( 'Away Team' );
$this->create_event( $home_id, $away_id );
$events = tse_sp_event_export_get_events(
array(
'team_ids' => array( $away_id ),
)
);
$this->assertCount( 1, $events );
$this->assertSame( 'Away Team', $events[0]['team_name'] );
$this->assertSame( 'Home Team', $events[0]['title'] );
$this->assertSame( 'at', $events[0]['separator'] );
$this->assertSame( 'Home Team', $events[0]['opponent_name'] );
$this->assertSame( 'Away', $events[0]['location_flag'] );
}
/**
* Matchup title format should ignore selected-team perspective.
*/
public function test_team_export_matchup_title_format_uses_away_at_home() {
$home_id = $this->create_team( 'Home Team' );
$away_id = $this->create_team( 'Away Team' );
$this->create_event( $home_id, $away_id );
$events = tse_sp_event_export_get_events(
array(
'team_ids' => array( $home_id ),
'title_format' => 'matchup',
)
);
$this->assertCount( 1, $events );
$this->assertSame( 'Away Team at Home Team', $events[0]['title'] );
}
/**
* Multi-team printable entries should use selected-team-first titles by default.
*/
public function test_printable_multi_team_entries_use_selected_first_titles() {
$home_id = $this->create_team( 'Home Team' );
$away_id = $this->create_team( 'Away Team' );
$venue_id = self::factory()->term->create( array( 'taxonomy' => 'sp_venue', 'name' => 'North Field' ) );
$this->create_event( $home_id, $away_id, array( $venue_id ) );
$printable = Tony_Sportspress_Printable_Calendars::instance();
$method = new ReflectionMethod( $printable, 'get_schedule_entries' );
$method->setAccessible( true );
$entries = $method->invoke( $printable, array( $home_id, $away_id ), 0, 0, array() );
$this->assertCount( 1, $entries );
$this->assertFalse( $entries[0]['is_matchup'] );
$this->assertSame( 'Away Team', $entries[0]['title_team_name'] );
$this->assertSame( 'at', $entries[0]['title_separator'] );
$this->assertSame( 'Home Team', $entries[0]['title_opponent_name'] );
}
/**
* Exactly one selected field should suppress per-event venue labels.
*/
public function test_render_month_grid_can_suppress_event_venue_label() {
$printable = Tony_Sportspress_Printable_Calendars::instance();
$method = new ReflectionMethod( $printable, 'render_month_grid' );
$method->setAccessible( true );
$entries = array(
'2026-05-20' => array(
array(
'day_key' => '2026-05-20',
'month_key' => '2026-05',
'timestamp' => strtotime( '2026-05-20 18:00:00' ),
'is_matchup' => true,
'away_team_name' => 'Away',
'home_team_name' => 'Home',
'event_time' => '6:00 PM',
'venue_label' => 'North',
'venue_name' => 'North Field',
'venue_key' => 'v:1',
),
),
);
ob_start();
$method->invoke(
$printable,
'2026-05',
$entries,
array( 'v:1' => array( 'name' => 'North Field', 'color' => '#1D4ED8' ) ),
array(
'primary' => '#1D4ED8',
'secondary' => '#DC2626',
'accent' => '#1D4ED8',
'ink' => '#111827',
'muted_ink' => '#334155',
),
true,
true
);
$output = ob_get_clean();
$this->assertStringContainsString( 'Away', $output );
$this->assertStringContainsString( 'Home', $output );
$this->assertStringNotContainsString( 'class="event-venue"', $output );
}
}

408
tests/test-sp-webhooks.php Normal file
View File

@@ -0,0 +1,408 @@
<?php
/**
* Tests for configurable SportsPress webhooks.
*
* @package Tonys_Sportspress_Enhancements
*/
/**
* Webhook feature tests.
*/
class Test_SP_Webhooks extends WP_UnitTestCase {
/**
* Template placeholders should resolve nested values and JSON serialization.
*/
public function test_render_template_supports_dot_paths_and_tojson() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = 'Trigger={{ trigger.key }} Team={{ event.teams.0.name }} Image={{ event.image }} Payload={{ event|tojson }}';
$context = array(
'trigger' => array(
'key' => 'event_results_updated',
),
'event' => array(
'id' => 55,
'image' => 'https://example.com/head-to-head?post=55',
'teams' => array(
array(
'name' => 'Blue Team',
),
),
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertStringContainsString( 'Trigger=event_results_updated', $rendered );
$this->assertStringContainsString( 'Team=Blue Team', $rendered );
$this->assertStringContainsString( 'Image=https://example.com/head-to-head?post=55', $rendered );
$this->assertStringContainsString( '"id":55', $rendered );
}
/**
* Venue aliases and split schedule fields should render from the context.
*/
public function test_render_template_supports_field_alias_and_schedule_parts() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = 'Field={{ event.field.short_name }} Venue={{ event.venue.abbreviation }} Time={{ event.scheduled.time }} {{ event.scheduled.timezone }}';
$context = array(
'event' => array(
'field' => array(
'short_name' => 'North',
),
'venue' => array(
'abbreviation' => 'NF',
),
'scheduled' => array(
'time' => '7:30 PM',
'timezone' => 'CDT',
),
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( 'Field=North Venue=NF Time=7:30 PM CDT', $rendered );
}
/**
* Event context should expose home and away team aliases.
*/
public function test_render_template_supports_event_team_aliases() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = 'Home={{ event.home_team.name }} Away={{ event.away_team.name }}';
$context = array(
'event' => array(
'home_team' => array(
'name' => 'Home Team',
),
'away_team' => array(
'name' => 'Away Team',
),
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( 'Home=Home Team Away=Away Team', $rendered );
}
/**
* Event context should expose the current SportsPress schedule status.
*/
public function test_render_template_supports_event_status_alias() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = 'Status={{ event.status }}';
$context = array(
'event' => array(
'status' => 'Postponed',
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( 'Status=Postponed', $rendered );
}
/**
* Date filter should accept PHP date format strings for schedule values.
*/
public function test_render_template_supports_date_filter() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = 'Time={{ event.scheduled.timestamp|date("g:i A") }} ISO={{ event.scheduled.local_iso|date("m/d g:i A") }}';
$context = array(
'event' => array(
'scheduled' => array(
'timestamp' => 1714005000,
'local_iso' => '2024-04-24T19:30:00-05:00',
),
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( 'Time=7:30 PM ISO=04/24 7:30 PM', $rendered );
}
/**
* Change notifications should expose before and after venue/time values.
*/
public function test_render_template_supports_before_after_venue_and_time() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = 'Venue {{ changes.previous.venue.name }} -> {{ changes.current.venue.name }} Time {{ changes.previous.time }} -> {{ changes.current.time }}';
$context = array(
'changes' => array(
'previous' => array(
'time' => '6:00 PM',
'venue' => array(
'name' => 'North Field',
),
),
'current' => array(
'time' => '7:30 PM',
'venue' => array(
'name' => 'South Field',
),
),
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( 'Venue North Field -> South Field Time 6:00 PM -> 7:30 PM', $rendered );
}
/**
* Change notifications should expose before and after home/away team values.
*/
public function test_render_template_supports_before_after_teams() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = 'Home {{ changes.previous.home_team.name }} -> {{ changes.current.home_team.name }} Away {{ changes.previous.away_team.name }} -> {{ changes.current.away_team.name }}';
$context = array(
'changes' => array(
'previous' => array(
'home_team' => array(
'name' => 'Old Home',
),
'away_team' => array(
'name' => 'Old Away',
),
),
'current' => array(
'home_team' => array(
'name' => 'New Home',
),
'away_team' => array(
'name' => 'New Away',
),
),
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( 'Home Old Home -> New Home Away Old Away -> New Away', $rendered );
}
/**
* Change notifications should expose before and after status values.
*/
public function test_render_template_supports_before_after_status() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = 'Status {{ changes.previous.status }} -> {{ changes.current.status }}';
$context = array(
'changes' => array(
'previous' => array(
'status' => 'On time',
),
'current' => array(
'status' => 'Postponed',
),
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( 'Status On time -> Postponed', $rendered );
}
/**
* Schedule snapshots should treat status changes as meaningful changes.
*/
public function test_schedule_snapshot_signature_changes_when_status_changes() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$method = new ReflectionMethod( $webhooks, 'schedule_snapshots_match' );
$method->setAccessible( true );
$left = array(
'local_iso' => '2026-05-02T10:30:00-05:00',
'gmt_iso' => '2026-05-02T15:30:00+00:00',
'status' => 'On time',
'venue' => array(
'name' => 'Winnemac Park',
),
'teams' => array(
array( 'name' => 'Hawks' ),
array( 'name' => 'Electrons' ),
),
);
$right = $left;
$right['status'] = 'Canceled';
$this->assertFalse( $method->invoke( $webhooks, $left, $right ) );
}
/**
* Status snapshots should expose a display label and keep the raw SportsPress key.
*/
public function test_build_change_snapshot_normalizes_status_label_and_slug() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$method = new ReflectionMethod( $webhooks, 'build_change_snapshot' );
$method->setAccessible( true );
$snapshot = $method->invoke( $webhooks, array(), array(), array(), 'cancelled' );
$this->assertSame( 'Canceled', $snapshot['status'] );
$this->assertSame( 'cancelled', $snapshot['sp_status'] );
}
/**
* Conditionals should support simple comparisons and else branches.
*/
public function test_render_template_supports_conditionals() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = "{% if changes.previous.time != changes.current.time %}Time changed: {{ changes.previous.time }} -> {{ changes.current.time }}\n{% endif %}{% if changes.previous.field.name == changes.current.field.name %}Field unchanged{% else %}Field changed: {{ changes.previous.field.name }} -> {{ changes.current.field.name }}{% endif %}";
$context = array(
'changes' => array(
'previous' => array(
'time' => '6:00 PM',
'field' => array(
'name' => 'North Field',
),
),
'current' => array(
'time' => '7:30 PM',
'field' => array(
'name' => 'South Field',
),
),
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( "Time changed: 6:00 PM -> 7:30 PM\nField changed: North Field -> South Field", $rendered );
}
/**
* Truthy conditionals should render when the referenced value exists.
*/
public function test_render_template_supports_truthy_conditionals() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = '{% if changes.current.home_team.name %}Home: {{ changes.current.home_team.name }}{% endif %}';
$context = array(
'changes' => array(
'current' => array(
'home_team' => array(
'name' => 'Home Team',
),
),
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( 'Home: Home Team', $rendered );
}
/**
* Team change conditionals should stay false when only schedule fields changed.
*/
public function test_team_conditionals_do_not_fire_for_schedule_only_changes() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = '{% if changes.previous.away_team.name != changes.current.away_team.name %}Away changed{% else %}Away unchanged{% endif %}';
$context = array(
'changes' => array(
'previous' => array(
'time' => '6:00 PM',
'away_team' => array(
'name' => 'Away Team',
),
),
'current' => array(
'time' => '7:30 PM',
'away_team' => array(
'name' => 'Away Team',
),
),
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( 'Away unchanged', $rendered );
}
/**
* Conditionals should support simple or expressions with else branches.
*/
public function test_render_template_supports_or_conditionals() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$template = '{% if event.status == "Postponed" or event.status == "Canceled" %}Delayed{% else %}Normal{% endif %}';
$context = array(
'event' => array(
'status' => 'Canceled',
),
);
$rendered = $webhooks->render_template( $template, $context );
$this->assertSame( 'Delayed', $rendered );
}
/**
* Test webhook AJAX should honor the submitted row index.
*/
public function test_get_submitted_test_webhook_row_uses_matching_index() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$method = new ReflectionMethod( $webhooks, 'get_submitted_test_webhook_row' );
$method->setAccessible( true );
$result = $method->invoke(
$webhooks,
array(
'2' => array(
'name' => 'Second Row',
),
),
array(
'2' => '123',
)
);
$this->assertSame( 'Second Row', $result['row']['name'] );
$this->assertSame( 123, $result['event_id'] );
}
/**
* Sanitization should keep only complete provider-specific webhook rows.
*/
public function test_sanitize_settings_keeps_only_valid_webhooks() {
$webhooks = Tony_Sportspress_Webhooks::instance();
$sanitized = $webhooks->sanitize_settings(
array(
'webhooks' => array(
array(
'name' => 'Results',
'enabled' => '1',
'provider' => 'google_chat',
'url' => 'https://chat.googleapis.com/v1/spaces/AAA/messages?key=test&token=test',
'triggers' => array( 'event_results_updated' ),
'template' => '{"summary":"{{ results.summary }}"}',
),
array(
'name' => 'Invalid',
'enabled' => '1',
'provider' => 'groupme_bot',
'url' => 'invalid bot id',
'triggers' => array( 'event_datetime_changed' ),
'template' => 'ignored',
),
array(
'name' => 'Missing trigger',
'enabled' => '1',
'provider' => 'generic_json',
'url' => 'https://example.com/missing-trigger',
'template' => 'ignored',
),
),
)
);
$this->assertCount( 1, $sanitized['webhooks'] );
$this->assertSame( 'Results', $sanitized['webhooks'][0]['name'] );
$this->assertSame( 'google_chat', $sanitized['webhooks'][0]['provider'] );
$this->assertSame( 'https://chat.googleapis.com/v1/spaces/AAA/messages?key=test&token=test', $sanitized['webhooks'][0]['url'] );
$this->assertSame( array( 'event_results_updated' ), $sanitized['webhooks'][0]['triggers'] );
}
}

View File

@@ -8,13 +8,13 @@
* Text Domain: tonys-sportspress-enhancements
* Domain Path: /languages
* Update URI: https://github.com/anthonyscorrea/tonys-sportspress-enhancements
* Version: 0.1.9
* Version: 0.1.15
*
* @package Tonys_Sportspress_Enhancements
*/
if ( ! defined( 'TONY_SPORTSPRESS_ENHANCEMENTS_VERSION' ) ) {
define( 'TONY_SPORTSPRESS_ENHANCEMENTS_VERSION', '0.1.9' );
define( 'TONY_SPORTSPRESS_ENHANCEMENTS_VERSION', '0.1.15' );
}
if ( ! defined( 'TONY_SPORTSPRESS_ENHANCEMENTS_FILE' ) ) {
@@ -46,6 +46,7 @@ require_once plugin_dir_path(__FILE__) . 'includes/sp-event-quick-edit-officials
require_once plugin_dir_path(__FILE__) . 'includes/sp-event-team-ordering.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-printable-calendars.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-url-builder.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-webhooks.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-schedule-exporter.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-venue-meta.php';