42 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
360b971880 Update schedule exporter and bump version to 0.1.9
Some checks failed
Release Plugin Zip / release (push) Has been cancelled
2026-04-04 09:17:24 -04:00
2dbdc5fae9 Bump version to 0.1.8
Some checks failed
Release Plugin Zip / release (push) Has been cancelled
2026-04-02 14:45:48 -05:00
4c07787a44 Add GitHub releases updater 2026-04-02 14:44:10 -05:00
25014f6368 Add quick week filter options 2026-04-02 13:11:28 -05:00
37d1037238 Align admin filter labels with venue wording 2026-04-02 13:07:32 -05:00
65df3525a4 Disable event title links for officials managers 2026-04-02 12:29:37 -05:00
bc0e913dfb Refine admin week filter behavior 2026-04-02 12:23:29 -05:00
2baebf9c30 Add officials manager role and event officials column 2026-04-02 11:28:08 -05:00
b2d4c240ad Split ICS subscribe actions by platform
Some checks failed
Release Plugin Zip / release (push) Has been cancelled
2026-04-01 18:25:47 -05:00
6837907aa9 Bump plugin version to 0.1.7 2026-04-01 18:22:46 -05:00
bfc74fcab6 Refactor schedule exports into feed builders 2026-04-01 18:20:56 -05:00
894109ae6c bump version
Some checks failed
Release Plugin Zip / release (push) Has been cancelled
2026-03-31 21:53:37 -05:00
569d11ac1a Add SportsPress schedule exporter 2026-03-31 21:51:20 -05:00
e019bf2061 Add configurable printable venue labels 2026-03-29 09:32:41 -05:00
d2ff863ca5 Add printable calendars feature 2026-03-27 16:22:44 -05:00
785f617134 Fix Games admin filter links 2026-03-27 16:02:56 -05:00
910cc8905e bump version 2026-03-26 21:33:12 -05:00
723b90be52 Improve SportsPress event admin filter controls and team/result ordering
Some checks failed
Release Plugin Zip / release (push) Has been cancelled
2026-03-26 20:11:45 -05:00
e3d9c677c1 Consolidate team ordering and add explicit away-first settings 2026-03-26 20:11:45 -05:00
344a00cf82 bump version
Some checks failed
Release Plugin Zip / release (push) Has been cancelled
2026-03-24 19:27:23 -05:00
8f741f8f00 Add CSV feed 2026-03-24 19:26:25 -05:00
e4a596b479 ignore data folder 2026-03-24 19:19:59 -05:00
5f0622c052 version v0.1.3
All checks were successful
Release Plugin Zip / release (push) Successful in 4s
2026-02-17 15:33:24 -06:00
e1b3abbfb0 add officials to quick edit menu 2026-02-17 15:32:39 -06:00
30 changed files with 15454 additions and 414 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ node_modules/
*.sql
*.tar.gz
*.zip
data

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.

834
assets/print-calendar.css Normal file
View File

@@ -0,0 +1,834 @@
@import url("https://fonts.googleapis.com/css2?family=Nunito+Sans:opsz,wdth,wght@6..12,75..125,600..900&family=Open+Sans:wght@400..800&display=swap");
:root {
--pc-font-body: "Open Sans", Arial, sans-serif;
--pc-font-display: "Nunito Sans", "Open Sans", Arial, sans-serif;
--pc-page-padding: 0.45in;
--pc-gap: 8px;
--pc-border: #d5dde6;
--pc-preview-bg: #d9dce3;
--pc-team-logo-size: clamp(40px, 5vw, 56px);
--pc-brand-logo-height: clamp(40px, 5vw, 56px);
--pc-brand-logo-max-width: 34%;
--pc-muted-day-bg: rgba(243, 245, 248, 0.72);
--pc-empty-day-bg: rgba(233, 237, 242, 0.72);
--pc-event-logo-height: clamp(28px, 100%, 74px);
--pc-qr-size: 50px;
--pc-qr-offset: calc(var(--pc-qr-size) + var(--pc-gap));
}
body {
margin: 0;
padding: 20px;
font-family: var(--pc-font-body);
color: #111;
background: #fff;
}
.print-shell {
margin: 0;
background: #fff;
}
.print-page {
padding: var(--pc-page-padding);
}
@media screen {
body.print-preview {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 24px;
background: var(--pc-preview-bg);
}
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 {
width: 8.5in;
min-height: 11in;
}
body.print-preview .print-shell.ledger {
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 {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin: 0 0 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--team-accent, #b61f0f);
}
.header-brand {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.team-logo,
.league-logo {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
}
.team-logo {
width: var(--pc-team-logo-size);
height: var(--pc-team-logo-size);
}
.team-logo img,
.team-logo-img {
width: 100%;
height: 100%;
display: block;
object-fit: contain;
}
.league-logo {
height: var(--pc-brand-logo-height);
max-width: var(--pc-brand-logo-max-width);
}
.league-logo a,
.league-logo .custom-logo-link,
.league-logo .wp-block-site-logo__link {
height: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
}
.league-logo img,
.league-logo-img,
.league-logo .custom-logo,
.league-logo .wp-block-site-logo__image {
width: auto;
height: 100%;
max-width: 100%;
display: block;
object-fit: contain;
}
.header-copy {
min-width: 0;
}
.title {
margin: 0;
font-family: var(--pc-font-display);
font-size: 28px;
font-weight: 800;
font-variation-settings: "wdth" 92, "wght" 800;
line-height: 1.1;
color: var(--team-ink, #111);
}
.meta {
margin: 0;
font-size: 13px;
color: var(--team-muted-ink, #333);
}
.legend {
display: flex;
flex-wrap: wrap;
gap: var(--pc-gap);
margin: 0;
}
.legend-item {
display: inline-flex;
align-items: center;
font-family: var(--pc-font-display);
font-variation-settings: "wdth" 70, "wght" 700;
background: #fff;
}
.sheet-grid {
display: grid;
grid-template-columns: repeat(var(--month-columns), minmax(0, 1fr));
gap: var(--pc-gap);
align-items: start;
}
.sheet-grid > .month:nth-child(3):last-child {
grid-column: 1 / -1;
width: calc((100% - var(--pc-gap)) / 2);
max-width: calc((100% - var(--pc-gap)) / 2);
justify-self: center;
}
.month {
margin: 0;
break-inside: avoid;
page-break-inside: avoid;
}
.month-title {
margin: 0;
padding: 2px;
font-family: var(--pc-font-display);
font-size: calc(20px * var(--month-font-scale));
font-weight: 800;
font-variation-settings: "wdth" 80, "wght" 800;
line-height: 1.1;
letter-spacing: 0.01em;
text-align: center;
text-transform: uppercase;
background: var(--team-primary);
color: var(--team-on-primary);
}
.dow,
.grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.dow span {
display: block;
padding: 2px 1px;
font-size: calc(10px * var(--month-font-scale));
font-weight: 700;
text-align: center;
text-transform: uppercase;
border-bottom: 1px solid var(--pc-border);
background: var(--team-link-color);
color: var(--team-on-link-color);
}
.day {
--corner-badge-size: calc(15px * var(--month-font-scale));
--corner-badge-offset: 0px;
position: relative;
overflow: hidden;
aspect-ratio: var(--day-aspect);
background: #fff;
}
.day.muted {
background: var(--pc-muted-day-bg);
color: #9aa5b1;
}
.day.no-events {
background: var(--pc-empty-day-bg);
}
.day-num {
position: absolute;
top: var(--corner-badge-offset);
left: var(--corner-badge-offset);
z-index: 20;
width: var(--corner-badge-size);
height: var(--corner-badge-size);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--pc-font-display);
font-size: calc(12px * var(--month-font-scale));
font-weight: 800;
font-variation-settings: "wdth" 86, "wght" 800;
line-height: 1;
}
.day.has-events .day-num {
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;
--event-logo-height: var(--pc-event-logo-height);
position: relative;
background: var(--event-bg, var(--team-primary, #1b76d1));
color: var(--event-fg, #fff);
}
.event.h {
--event-bg: var(--team-primary, #1b76d1);
}
.event.a {
--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);
left: 0;
right: 0;
bottom: var(--event-bottom-band);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
width: 100%;
font-size: calc(10px * var(--month-font-scale));
font-weight: 700;
line-height: 1;
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);
max-width: 100%;
max-height: 100%;
display: block;
object-fit: contain;
object-position: center;
}
.event-name {
width: 100%;
max-width: 100%;
max-height: 100%;
overflow: hidden;
text-overflow: clip;
white-space: normal;
word-break: normal;
overflow-wrap: normal;
hyphens: none;
line-height: 1.05;
font-weight: 700;
font-stretch: condensed;
font-variation-settings: "wdth" 42, "wght" 700;
opacity: 0.85;
}
.event.no-logo .event-name {
font-family: var(--pc-font-display);
font-weight: 700;
font-variation-settings: "wdth" 30, "wght" 700;
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);
right: var(--corner-badge-offset, 2px);
z-index: 3;
width: var(--corner-badge-size, 11px);
height: var(--corner-badge-size, 11px);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--pc-font-display);
font-size: calc(10px * var(--month-font-scale));
font-weight: 900;
font-variation-settings: "wdth" 84, "wght" 900;
line-height: 1;
letter-spacing: 0;
background: #111;
color: #fff;
}
.event.a .ha-flag {
background: #fff;
color: #111;
}
.event-meta {
position: absolute;
left: 0;
right: 0;
bottom: 2px;
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: 1px;
padding: 0 2px;
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;
color: currentColor;
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
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;
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
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;
background: #f8fafc;
}
.footer-meta {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--pc-gap);
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--pc-border);
}
.legend-bottom {
--month-width: calc((100% - ((var(--month-columns) - 1) * var(--pc-gap))) / var(--month-columns));
flex: 0 0 var(--month-width);
width: var(--month-width);
max-width: var(--month-width);
align-content: flex-start;
justify-content: center;
gap: 6px;
}
.legend-bottom .legend-item {
padding: 3px 6px;
font-size: 10px;
text-align: center;
}
.footer-qr {
position: relative;
flex: 0 0 auto;
display: flex;
align-items: flex-end;
justify-content: flex-end;
gap: var(--pc-gap);
min-height: var(--pc-qr-size);
padding-right: var(--pc-qr-offset);
}
.footer-qr-copy {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
text-align: right;
}
.footer-qr-label {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--team-muted-ink, #333);
}
.footer-qr-link {
font-size: 10px;
font-weight: 700;
text-decoration: none;
color: var(--team-ink, #111);
}
.footer-qr-image {
position: absolute;
right: 0;
bottom: 0;
width: var(--pc-qr-size);
height: var(--pc-qr-size);
display: block;
border: 1px solid var(--pc-border);
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 {
padding: 0;
display: block;
background: #fff;
}
.print-shell,
body.print-preview .print-shell,
body.print-preview .print-shell.letter,
body.print-preview .print-shell.ledger {
width: auto;
min-height: auto;
box-shadow: none;
transform: none;
}
.print-page {
padding: 0;
}
.header {
margin-bottom: 8px;
}
.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;
}
}

View File

@@ -0,0 +1,27 @@
( function ( blocks, blockEditor, element, i18n ) {
var el = element.createElement;
var useBlockProps = blockEditor.useBlockProps;
var __ = i18n.__;
blocks.registerBlockType( 'tse/schedule-exporter', {
edit: function () {
var blockProps = useBlockProps( {
className: 'tse-schedule-exporter-block-placeholder',
} );
return el(
'div',
blockProps,
el( 'strong', null, __( 'Schedule Exporter', 'tonys-sportspress-enhancements' ) ),
el(
'p',
null,
__( 'This block renders the public schedule exporter on the frontend.', 'tonys-sportspress-enhancements' )
)
);
},
save: function () {
return null;
},
} );
} )( window.wp.blocks, window.wp.blockEditor, window.wp.element, window.wp.i18n );

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}.";
$post = asc_sp_event_get_post();
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;
}
if ( ! $post ) {
return;
}
if ( 'future' == $status ) {
$description = $description;
$title = $title . "" . asc_generate_short_date($post) . "" . $venue_name;
}
$meta = asc_sp_event_open_graph_data( $post );
if ( 'results' == $status ) { // checks if there is a final score
// Get event result data
$data = $event->results();
if ( empty( $meta ) ) {
return;
}
// The first row should be column labels
$labels = $data[0];
// 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

@@ -10,6 +10,183 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Screen-option defaults for event list filters.
*
* @return array<string, bool>
*/
function tony_sportspress_event_filter_defaults() {
return array(
'month' => true,
'week' => false,
'team' => true,
'venue' => true,
'league' => true,
'season' => true,
'match_day' => true,
);
}
/**
* Build user meta key for an event filter screen option.
*
* @param string $key Filter key.
* @return string
*/
function tony_sportspress_event_filter_meta_key( $key ) {
return 'tony_sp_event_filter_' . $key;
}
/**
* Get the current singular label for event venues.
*
* @return string
*/
function tony_sportspress_get_event_venue_label() {
$taxonomy = get_taxonomy( 'sp_venue' );
if ( $taxonomy && ! empty( $taxonomy->labels->singular_name ) ) {
return (string) $taxonomy->labels->singular_name;
}
return __( 'Venue', 'sportspress' );
}
/**
* Get the current plural label for event venues.
*
* @return string
*/
function tony_sportspress_get_event_venue_label_plural() {
$taxonomy = get_taxonomy( 'sp_venue' );
if ( $taxonomy && ! empty( $taxonomy->labels->name ) ) {
return (string) $taxonomy->labels->name;
}
return __( 'Venues', 'sportspress' );
}
/**
* Normalize event filter visibility rules.
*
* Month/Year and Week are mutually exclusive.
*
* @param array<string, bool> $filters Filter states keyed by filter name.
* @return array<string, bool>
*/
function tony_sportspress_normalize_event_filter_states( $filters ) {
if ( ! empty( $filters['month'] ) && ! empty( $filters['week'] ) ) {
$filters['week'] = false;
}
return $filters;
}
/**
* Check whether a filter is enabled for the current user.
*
* @param string $key Filter key.
* @return bool
*/
function tony_sportspress_event_filter_enabled( $key ) {
$defaults = tony_sportspress_normalize_event_filter_states( tony_sportspress_event_filter_defaults() );
if ( ! array_key_exists( $key, $defaults ) ) {
return true;
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
return (bool) $defaults[ $key ];
}
$stored = get_user_meta( $user_id, tony_sportspress_event_filter_meta_key( $key ), true );
if ( '' === $stored ) {
return (bool) $defaults[ $key ];
}
$states = array();
foreach ( $defaults as $filter_key => $enabled ) {
$current = get_user_meta( $user_id, tony_sportspress_event_filter_meta_key( $filter_key ), true );
$states[ $filter_key ] = '' === $current ? (bool) $enabled : '1' === (string) $current;
}
$states = tony_sportspress_normalize_event_filter_states( $states );
return ! empty( $states[ $key ] );
}
/**
* Persist event filter Screen Options via AJAX.
*/
function tony_sportspress_save_event_filter_screen_options_ajax() {
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error();
}
check_ajax_referer( 'tony_sp_event_filters', 'nonce' );
$defaults = tony_sportspress_normalize_event_filter_states( tony_sportspress_event_filter_defaults() );
$filters = isset( $_POST['filters'] ) && is_array( $_POST['filters'] ) ? $_POST['filters'] : array();
$user_id = get_current_user_id();
$states = array();
foreach ( $defaults as $key => $_enabled ) {
$value = isset( $filters[ $key ] ) ? sanitize_text_field( wp_unslash( $filters[ $key ] ) ) : '0';
$states[ $key ] = '1' === $value;
}
$states = tony_sportspress_normalize_event_filter_states( $states );
foreach ( $states as $key => $enabled ) {
update_user_meta( $user_id, tony_sportspress_event_filter_meta_key( $key ), $enabled ? '1' : '0' );
}
wp_send_json_success();
}
add_action( 'wp_ajax_tony_sp_event_filter_prefs_save', 'tony_sportspress_save_event_filter_screen_options_ajax' );
/**
* Add filter visibility toggles to Screen Options on event list admin page.
*
* @param string $settings Existing settings HTML.
* @param WP_Screen $screen Current screen.
* @return string
*/
function tony_sportspress_event_filter_screen_options_markup( $settings, $screen ) {
if ( ! $screen || 'edit-sp_event' !== $screen->id ) {
return $settings;
}
$labels = array(
'month' => __( 'Month/Year', 'tonys-sportspress-enhancements' ),
'week' => __( 'Year/Week', 'tonys-sportspress-enhancements' ),
'team' => __( 'Team', 'tonys-sportspress-enhancements' ),
'venue' => tony_sportspress_get_event_venue_label(),
'league' => __( 'League', 'tonys-sportspress-enhancements' ),
'season' => __( 'Season', 'tonys-sportspress-enhancements' ),
'match_day' => __( 'Match Day', 'tonys-sportspress-enhancements' ),
);
$settings .= '<fieldset class="metabox-prefs">';
$settings .= '<legend>' . esc_html__( 'Event Filters', 'tonys-sportspress-enhancements' ) . '</legend>';
foreach ( $labels as $key => $label ) {
$meta_key = tony_sportspress_event_filter_meta_key( $key );
$checked = tony_sportspress_event_filter_enabled( $key ) ? ' checked="checked"' : '';
$settings .= '<label for="' . esc_attr( $meta_key ) . '">';
$settings .= '<input type="checkbox" id="' . esc_attr( $meta_key ) . '" name="' . esc_attr( $meta_key ) . '" value="1"' . $checked . ' />';
$settings .= esc_html( $label );
$settings .= '</label>';
}
$settings .= '</fieldset>';
return $settings;
}
add_filter( 'screen_settings', 'tony_sportspress_event_filter_screen_options_markup', 10, 2 );
/**
* Parse an ISO week input (e.g. 2026-W07) from the request.
*
@@ -20,7 +197,21 @@ function tony_sportspress_parse_admin_week_filter() {
return null;
}
$raw = sanitize_text_field( wp_unslash( $_GET['sp_week_filter'] ) );
$raw = sanitize_text_field( wp_unslash( $_GET['sp_week_filter'] ) );
$timezone = wp_timezone();
if ( 'this-week' === $raw || 'next-week' === $raw ) {
$base = new DateTimeImmutable( 'now', $timezone );
if ( 'next-week' === $raw ) {
$base = $base->modify( '+1 week' );
}
return array(
'year' => (int) $base->format( 'o' ),
'week' => (int) $base->format( 'W' ),
);
}
if ( ! preg_match( '/^(\d{4})-W(0[1-9]|[1-4][0-9]|5[0-3])$/', $raw, $matches ) ) {
return null;
}
@@ -34,6 +225,66 @@ function tony_sportspress_parse_admin_week_filter() {
);
}
/**
* Get available ISO week options from event post dates.
*
* @return array<int, array{value:string,label:string}>
*/
function tony_sportspress_get_admin_week_filter_options() {
global $wpdb;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT DISTINCT DATE_FORMAT(post_date, '%%x-W%%v') AS iso_week
FROM {$wpdb->posts}
WHERE post_type = %s
AND post_status NOT IN ('auto-draft', 'trash')
AND post_date IS NOT NULL
AND post_date <> '0000-00-00 00:00:00'
ORDER BY iso_week DESC",
'sp_event'
),
ARRAY_A
);
if ( ! is_array( $results ) || empty( $results ) ) {
return array();
}
$options = array();
$timezone = wp_timezone();
foreach ( $results as $result ) {
if ( empty( $result['iso_week'] ) || ! preg_match( '/^(\d{4})-W(0[1-9]|[1-4][0-9]|5[0-3])$/', $result['iso_week'], $matches ) ) {
continue;
}
$year = (int) $matches[1];
$week = (int) $matches[2];
$monday = ( new DateTimeImmutable( 'now', $timezone ) )->setISODate( $year, $week, 1 )->setTime( 0, 0, 0 );
$sunday = $monday->modify( '+6 days' );
$start_label = wp_date( 'M j', $monday->getTimestamp(), $timezone );
$end_label = wp_date(
$monday->format( 'n' ) === $sunday->format( 'n' ) ? 'j' : 'M j',
$sunday->getTimestamp(),
$timezone
);
$options[] = array(
'value' => $result['iso_week'],
/* translators: 1: ISO week code, 2: Monday date, 3: Sunday date. */
'label' => sprintf(
__( '%1$s (%2$s to %3$s)', 'tonys-sportspress-enhancements' ),
$result['iso_week'],
$start_label,
$end_label
),
);
}
return $options;
}
/**
* Render week filter control in event admin list.
*
@@ -43,36 +294,32 @@ function tony_sportspress_render_admin_week_filter( $post_type ) {
if ( 'sp_event' !== $post_type ) {
return;
}
if ( ! tony_sportspress_event_filter_enabled( 'week' ) ) {
return;
}
$value = '';
if ( ! empty( $_GET['sp_week_filter'] ) ) {
$value = sanitize_text_field( wp_unslash( $_GET['sp_week_filter'] ) );
}
$summary_text = __( 'Select a week', 'tonys-sportspress-enhancements' );
$parsed = tony_sportspress_parse_admin_week_filter();
if ( is_array( $parsed ) ) {
$timezone = wp_timezone();
$monday = ( new DateTimeImmutable( 'now', $timezone ) )->setISODate( $parsed['year'], $parsed['week'], 1 )->setTime( 0, 0, 0 );
$sunday = $monday->modify( '+6 days' )->setTime( 23, 59, 59 );
/* translators: 1: Monday label/date, 2: Sunday label/date. */
$summary_text = sprintf(
__( '%1$s to %2$s', 'tonys-sportspress-enhancements' ),
wp_date( 'D M j, Y', $monday->getTimestamp(), $timezone ),
wp_date( 'D M j, Y', $sunday->getTimestamp(), $timezone )
);
}
$options = tony_sportspress_get_admin_week_filter_options();
?>
<label for="sp_week_filter" class="screen-reader-text"><?php esc_html_e( 'Filter by week', 'tonys-sportspress-enhancements' ); ?></label>
<input
type="week"
<select
id="sp_week_filter"
name="sp_week_filter"
class="sp-week-filter-field"
value="<?php echo esc_attr( $value ); ?>"
title="<?php esc_attr_e( 'Week (Monday start)', 'tonys-sportspress-enhancements' ); ?>"
/>
<span id="sp-week-filter-summary" class="sp-week-filter-summary"><?php echo esc_html( $summary_text ); ?></span>
>
<option value=""><?php esc_html_e( 'Year/Week', 'tonys-sportspress-enhancements' ); ?></option>
<option value="this-week" <?php selected( $value, 'this-week' ); ?>><?php esc_html_e( 'This week', 'tonys-sportspress-enhancements' ); ?></option>
<option value="next-week" <?php selected( $value, 'next-week' ); ?>><?php esc_html_e( 'Next week', 'tonys-sportspress-enhancements' ); ?></option>
<?php foreach ( $options as $option ) : ?>
<option value="<?php echo esc_attr( $option['value'] ); ?>" <?php selected( $value, $option['value'] ); ?>>
<?php echo esc_html( $option['label'] ); ?>
</option>
<?php endforeach; ?>
</select>
<?php
}
add_action( 'restrict_manage_posts', 'tony_sportspress_render_admin_week_filter' );
@@ -85,8 +332,46 @@ function tony_sportspress_admin_week_filter_styles() {
if ( ! $screen || 'edit-sp_event' !== $screen->id ) {
return;
}
$hide = array(
'month' => ! tony_sportspress_event_filter_enabled( 'month' ),
'team' => ! tony_sportspress_event_filter_enabled( 'team' ),
'venue' => ! tony_sportspress_event_filter_enabled( 'venue' ),
'league' => ! tony_sportspress_event_filter_enabled( 'league' ),
'season' => ! tony_sportspress_event_filter_enabled( 'season' ),
'match_day' => ! tony_sportspress_event_filter_enabled( 'match_day' ),
'week' => ! tony_sportspress_event_filter_enabled( 'week' ),
);
?>
<style>
<?php if ( $hide['month'] ) : ?>
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="m"] { display: none !important; }
<?php endif; ?>
<?php if ( $hide['team'] ) : ?>
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="team"],
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="team"] + .chosen-container,
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="team"] + .select2 { display: none !important; }
<?php endif; ?>
<?php if ( $hide['venue'] ) : ?>
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="sp_venue"],
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="sp_venue"] + .chosen-container,
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="sp_venue"] + .select2 { display: none !important; }
<?php endif; ?>
<?php if ( $hide['league'] ) : ?>
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="sp_league"],
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="sp_league"] + .chosen-container,
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="sp_league"] + .select2 { display: none !important; }
<?php endif; ?>
<?php if ( $hide['season'] ) : ?>
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="sp_season"],
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="sp_season"] + .chosen-container,
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) select[name="sp_season"] + .select2 { display: none !important; }
<?php endif; ?>
<?php if ( $hide['match_day'] ) : ?>
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) input[name="match_day"] { display: none !important; }
<?php endif; ?>
<?php if ( $hide['week'] ) : ?>
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) #sp_week_filter { display: none !important; }
<?php endif; ?>
@media (max-width: 1200px) {
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) {
display: flex;
@@ -101,14 +386,6 @@ function tony_sportspress_admin_week_filter_styles() {
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) .sp-week-filter-field {
min-width: 145px;
}
.post-type-sp_event .tablenav.top .alignleft.actions:not(.bulkactions) .sp-week-filter-summary {
display: block;
width: 100%;
margin-top: 2px;
color: #50575e;
font-size: 12px;
line-height: 1.4;
}
}
</style>
<?php
@@ -116,56 +393,89 @@ function tony_sportspress_admin_week_filter_styles() {
add_action( 'admin_head-edit.php', 'tony_sportspress_admin_week_filter_styles' );
/**
* Update week summary text when week input changes.
* Update admin filter labels and persist screen options.
*/
function tony_sportspress_admin_week_filter_script() {
$screen = get_current_screen();
if ( ! $screen || 'edit-sp_event' !== $screen->id ) {
return;
}
$venue_filter_text = sprintf(
/* translators: %s: plural venue label. */
__( 'Show all %s', 'sportspress' ),
strtolower( tony_sportspress_get_event_venue_label_plural() )
);
?>
<script>
(function() {
const input = document.getElementById('sp_week_filter');
const summary = document.getElementById('sp-week-filter-summary');
if (!input || !summary) {
return;
}
const filterCheckboxes = Array.from(
document.querySelectorAll('#screen-options-wrap input[type="checkbox"][id^="tony_sp_event_filter_"]')
);
const monthCheckbox = document.getElementById('tony_sp_event_filter_month');
const weekCheckbox = document.getElementById('tony_sp_event_filter_week');
function updateSummary() {
const raw = (input.value || '').trim();
const match = raw.match(/^(\d{4})-W(\d{2})$/);
if (!match) {
summary.textContent = 'Select a week';
function syncExclusiveFilters(changedCheckbox) {
if (!changedCheckbox || !changedCheckbox.checked) {
return;
}
const year = parseInt(match[1], 10);
const week = parseInt(match[2], 10);
if (changedCheckbox === monthCheckbox && weekCheckbox) {
weekCheckbox.checked = false;
}
const jan4 = new Date(Date.UTC(year, 0, 4));
const jan4Day = jan4.getUTCDay() || 7;
const mondayWeek1 = new Date(jan4);
mondayWeek1.setUTCDate(jan4.getUTCDate() - jan4Day + 1);
const monday = new Date(mondayWeek1);
monday.setUTCDate(mondayWeek1.getUTCDate() + (week - 1) * 7);
const sunday = new Date(monday);
sunday.setUTCDate(monday.getUTCDate() + 6);
const fmt = new Intl.DateTimeFormat(undefined, {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC'
});
summary.textContent = fmt.format(monday) + ' to ' + fmt.format(sunday);
if (changedCheckbox === weekCheckbox && monthCheckbox) {
monthCheckbox.checked = false;
}
}
input.addEventListener('change', updateSummary);
updateSummary();
function saveFilterPrefs() {
if (!filterCheckboxes.length || typeof ajaxurl === 'undefined') {
return;
}
const body = new URLSearchParams();
body.append('action', 'tony_sp_event_filter_prefs_save');
body.append('nonce', <?php echo wp_json_encode( wp_create_nonce( 'tony_sp_event_filters' ) ); ?>);
filterCheckboxes.forEach(function(checkbox) {
const key = checkbox.id.replace('tony_sp_event_filter_', '');
body.append('filters[' + key + ']', checkbox.checked ? '1' : '0');
});
fetch(ajaxurl, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: body.toString(),
keepalive: true
}).catch(function() {});
}
filterCheckboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
syncExclusiveFilters(checkbox);
saveFilterPrefs();
});
});
const monthSelect = document.querySelector('select[name="m"]');
if (monthSelect) {
const allDates = monthSelect.querySelector('option[value="0"]');
if (allDates) {
allDates.textContent = <?php echo wp_json_encode( __( 'Month/Year', 'tonys-sportspress-enhancements' ) ); ?>;
}
}
const venueSelect = document.querySelector('select[name="sp_venue"]');
if (venueSelect) {
const allVenues = venueSelect.querySelector('option[value="0"]');
if (allVenues) {
allVenues.textContent = <?php echo wp_json_encode( $venue_filter_text ); ?>;
}
}
})();
</script>
<?php

1344
includes/sp-event-csv.php Normal file

File diff suppressed because it is too large Load Diff

1042
includes/sp-event-export.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -75,15 +75,27 @@ function custom_event_rewrite_flush() {
register_activation_hook(__FILE__, 'custom_event_rewrite_flush');
register_deactivation_hook(__FILE__, 'flush_rewrite_rules');
// Modify the query to handle custom permalinks and include future posts
function custom_event_parse_request($query) {
$post_type = sp_array_value( $query->query, 'post_type', null );
if (isset($query->query_vars['post_type']) && $query->query_vars['post_type'] === 'sp_event') {
if (isset($query->query_vars['p'])) {
$query->set('post_type', 'sp_event');
$query->set('p', $query->query_vars['p']);
$query->set('post_status', array('publish', 'future'));
}
}
// Modify the front-end single event query to allow scheduled events to resolve.
function custom_event_parse_request( $query ) {
if ( ! $query instanceof WP_Query ) {
return;
}
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( 'sp_event' !== $query->get( 'post_type' ) ) {
return;
}
$post_id = absint( $query->get( 'p' ) );
if ( $post_id <= 0 ) {
return;
}
$query->set( 'post_type', 'sp_event' );
$query->set( 'p', $post_id );
$query->set( 'post_status', array( 'publish', 'future' ) );
}
add_action('pre_get_posts', 'custom_event_parse_request');

View File

@@ -0,0 +1,329 @@
<?php
/**
* Quick Edit officials support for SportsPress events.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Add an Officials column to the event admin list.
*
* @param array $columns Existing columns.
* @return array
*/
function tony_sportspress_event_add_officials_column( $columns ) {
$updated = array();
foreach ( $columns as $key => $label ) {
$updated[ $key ] = $label;
if ( 'sp_team' === $key ) {
$updated['tony_sp_officials'] = esc_html__( 'Officials', 'tonys-sportspress-enhancements' );
}
}
if ( ! isset( $updated['tony_sp_officials'] ) ) {
$updated['tony_sp_officials'] = esc_html__( 'Officials', 'tonys-sportspress-enhancements' );
}
return $updated;
}
add_filter( 'manage_edit-sp_event_columns', 'tony_sportspress_event_add_officials_column', 20 );
/**
* Build a display-ready officials map for an event.
*
* @param int $post_id Post ID.
* @return array<int, array{name: string, officials: string[]}>
*/
function tony_sportspress_event_get_officials_display( $post_id ) {
$officials_by_duty = get_post_meta( $post_id, 'sp_officials', true );
if ( ! is_array( $officials_by_duty ) || empty( $officials_by_duty ) ) {
return array();
}
$duties = get_terms(
array(
'taxonomy' => 'sp_duty',
'hide_empty' => false,
)
);
$duty_names = array();
if ( is_array( $duties ) ) {
foreach ( $duties as $duty ) {
if ( isset( $duty->term_id, $duty->name ) ) {
$duty_names[ (int) $duty->term_id ] = $duty->name;
}
}
}
$rows = array();
foreach ( $officials_by_duty as $duty_id => $official_ids ) {
$duty_id = absint( $duty_id );
$official_ids = array_filter( array_map( 'absint', (array) $official_ids ) );
if ( $duty_id <= 0 || empty( $official_ids ) ) {
continue;
}
$names = array();
foreach ( $official_ids as $official_id ) {
$title = get_the_title( $official_id );
if ( is_string( $title ) && '' !== $title ) {
$names[] = $title;
}
}
if ( empty( $names ) ) {
continue;
}
$rows[] = array(
'name' => isset( $duty_names[ $duty_id ] ) ? $duty_names[ $duty_id ] : (string) $duty_id,
'officials' => $names,
);
}
return $rows;
}
/**
* Print hidden officials data on each event row for quick edit prefill.
*
* @param string $column Column key.
* @param int $post_id Post ID.
*/
function tony_sportspress_event_quick_edit_officials_row_data( $column, $post_id ) {
if ( 'tony_sp_officials' !== $column ) {
return;
}
$officials = get_post_meta( $post_id, 'sp_officials', true );
if ( ! is_array( $officials ) ) {
$officials = array();
}
$serialized = wp_json_encode( $officials );
if ( ! is_string( $serialized ) ) {
$serialized = '{}';
}
$rows = tony_sportspress_event_get_officials_display( $post_id );
if ( empty( $rows ) ) {
echo '&mdash;';
} else {
foreach ( $rows as $row ) {
echo '<div class="tony-sp-event-official-row">';
echo '<strong>' . esc_html( $row['name'] ) . ':</strong> ';
echo esc_html( implode( ', ', $row['officials'] ) );
echo '</div>';
}
}
echo '<span class="hidden tony-event-officials-data" data-officials="' . esc_attr( $serialized ) . '"></span>';
}
add_action( 'manage_sp_event_posts_custom_column', 'tony_sportspress_event_quick_edit_officials_row_data', 20, 2 );
/**
* Render quick edit UI for officials.
*
* @param string $column_name Column key.
* @param string $post_type Post type key.
*/
function tony_sportspress_event_quick_edit_officials_field( $column_name, $post_type ) {
if ( 'sp_event' !== $post_type || 'tony_sp_officials' !== $column_name ) {
return;
}
static $printed = false;
if ( $printed ) {
return;
}
$printed = true;
wp_nonce_field( 'tony_sp_event_officials_quick_edit', 'tony_sp_event_officials_quick_edit_nonce' );
$duties = get_terms(
array(
'taxonomy' => 'sp_duty',
'hide_empty' => false,
'orderby' => 'meta_value_num',
'meta_query' => array(
'relation' => 'OR',
array(
'key' => 'sp_order',
'compare' => 'NOT EXISTS',
),
array(
'key' => 'sp_order',
'compare' => 'EXISTS',
),
),
)
);
if ( ! is_array( $duties ) || empty( $duties ) ) {
return;
}
$officials = get_posts(
array(
'post_type' => 'sp_official',
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
)
);
if ( ! is_array( $officials ) || empty( $officials ) ) {
return;
}
?>
<fieldset class="inline-edit-col-right tony-sp-event-officials-wrap">
<div class="inline-edit-col">
<span class="title inline-edit-categories-label"><?php esc_html_e( 'Officials', 'tonys-sportspress-enhancements' ); ?></span>
<?php foreach ( $duties as $duty ) : ?>
<div class="tony-sp-duty-group">
<span class="title inline-edit-categories-label"><?php echo esc_html( $duty->name ); ?></span>
<input type="hidden" name="tony_sp_officials[<?php echo esc_attr( $duty->term_id ); ?>][]" value="0">
<ul class="cat-checklist">
<?php foreach ( $officials as $official ) : ?>
<li>
<label class="selectit">
<input
value="<?php echo esc_attr( $official->ID ); ?>"
type="checkbox"
name="tony_sp_officials[<?php echo esc_attr( $duty->term_id ); ?>][]"
class="tony-sp-official-checkbox"
data-duty-id="<?php echo esc_attr( $duty->term_id ); ?>"
>
<?php echo esc_html( $official->post_title ); ?>
</label>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endforeach; ?>
</div>
</fieldset>
<?php
}
add_action( 'quick_edit_custom_box', 'tony_sportspress_event_quick_edit_officials_field', 10, 2 );
/**
* Save quick edit officials data.
*
* @param int $post_id Post ID.
*/
function tony_sportspress_event_quick_edit_officials_save( $post_id ) {
if ( empty( $_POST ) ) {
return;
}
if ( wp_is_post_revision( $post_id ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) {
return;
}
if ( 'sp_event' !== get_post_type( $post_id ) ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$nonce = isset( $_POST['tony_sp_event_officials_quick_edit_nonce'] )
? sanitize_text_field( wp_unslash( $_POST['tony_sp_event_officials_quick_edit_nonce'] ) )
: '';
if ( '' === $nonce || ! wp_verify_nonce( $nonce, 'tony_sp_event_officials_quick_edit' ) ) {
return;
}
$raw_officials = isset( $_POST['tony_sp_officials'] ) ? wp_unslash( $_POST['tony_sp_officials'] ) : array();
if ( ! is_array( $raw_officials ) ) {
$raw_officials = array();
}
$clean_officials = array();
foreach ( $raw_officials as $duty_id => $official_ids ) {
$duty_id = absint( $duty_id );
if ( $duty_id <= 0 || ! is_array( $official_ids ) ) {
continue;
}
$official_ids = array_map( 'absint', $official_ids );
$official_ids = array_filter( $official_ids );
$official_ids = array_values( array_unique( $official_ids ) );
if ( ! empty( $official_ids ) ) {
$clean_officials[ $duty_id ] = $official_ids;
}
}
update_post_meta( $post_id, 'sp_officials', $clean_officials );
}
add_action( 'save_post', 'tony_sportspress_event_quick_edit_officials_save' );
/**
* Prefill quick edit checkboxes with existing officials.
*/
function tony_sportspress_event_quick_edit_officials_script() {
$screen = get_current_screen();
if ( ! $screen || 'edit-sp_event' !== $screen->id ) {
return;
}
?>
<script>
(function($) {
if (typeof inlineEditPost === 'undefined' || !inlineEditPost.edit) {
return;
}
var wpInlineEdit = inlineEditPost.edit;
inlineEditPost.edit = function(id) {
wpInlineEdit.apply(this, arguments);
var postId = 0;
if (typeof id === 'object') {
postId = parseInt(this.getId(id), 10);
}
if (!postId) {
return;
}
var editRow = $('#edit-' + postId);
var postRow = $('#post-' + postId);
var payload = postRow.find('.tony-event-officials-data').attr('data-officials');
var selected = {};
try {
selected = payload ? JSON.parse(payload) : {};
} catch (e) {
selected = {};
}
editRow.find('.tony-sp-official-checkbox').prop('checked', false);
Object.keys(selected).forEach(function(dutyId) {
var ids = selected[dutyId];
if (!Array.isArray(ids)) {
return;
}
ids.forEach(function(officialId) {
editRow
.find('.tony-sp-official-checkbox[data-duty-id="' + dutyId + '"][value="' + String(officialId) + '"]')
.prop('checked', true);
});
});
};
})(jQuery);
</script>
<?php
}
add_action( 'admin_footer-edit.php', 'tony_sportspress_event_quick_edit_officials_script' );

View File

@@ -0,0 +1,275 @@
<?php
/**
* Unified team-ordering behavior for SportsPress event admin and frontend lists.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Check whether SportsPress reverse teams option is enabled.
*
* @return bool
*/
function tony_sportspress_reverse_teams_enabled() {
return get_option( 'sportspress_event_reverse_teams', 'no' ) === 'yes';
}
/**
* Add Away | Home option to Event List title format setting.
*
* @param array $options Event list settings options.
* @return array
*/
function tony_sportspress_event_list_add_away_home_option( $options ) {
if ( ! is_array( $options ) ) {
return $options;
}
foreach ( $options as &$option ) {
if ( ! is_array( $option ) || ! isset( $option['id'] ) || 'sportspress_event_list_title_format' !== $option['id'] ) {
continue;
}
if ( ! isset( $option['options'] ) || ! is_array( $option['options'] ) ) {
continue;
}
if ( ! array_key_exists( 'awayhome', $option['options'] ) ) {
$option['options']['awayhome'] = sprintf( '%s | %s', esc_attr__( 'Away', 'sportspress' ), esc_attr__( 'Home', 'sportspress' ) );
}
}
unset( $option );
return $options;
}
add_filter( 'sportspress_event_list_options', 'tony_sportspress_event_list_add_away_home_option' );
/**
* Clarify wording of SportsPress Teams reverse-order option.
*
* @param array $options Team options.
* @return array
*/
function tony_sportspress_relabel_reverse_teams_option( $options ) {
if ( ! is_array( $options ) ) {
return $options;
}
foreach ( $options as &$option ) {
if ( ! is_array( $option ) || ! isset( $option['id'] ) || 'sportspress_event_reverse_teams' !== $option['id'] ) {
continue;
}
$option['desc'] = esc_attr__( 'Show away team first', 'tonys-sportspress-enhancements' );
}
unset( $option );
return $options;
}
add_filter( 'sportspress_event_logo_options', 'tony_sportspress_relabel_reverse_teams_option' );
/**
* Add Event Results team order option.
*
* @param array $options Event Results options.
* @return array
*/
function tony_sportspress_add_event_results_order_option( $options ) {
if ( ! is_array( $options ) ) {
return $options;
}
$options[] = array(
'title' => esc_attr__( 'Row order', 'tonys-sportspress-enhancements' ),
'id' => 'tony_sportspress_event_results_row_order',
'default' => 'home_away',
'type' => 'radio',
'options' => array(
'home_away' => sprintf( '%s | %s', esc_attr__( 'Home', 'sportspress' ), esc_attr__( 'Away', 'sportspress' ) ),
'away_home' => sprintf( '%s | %s', esc_attr__( 'Away', 'sportspress' ), esc_attr__( 'Home', 'sportspress' ) ),
),
);
return $options;
}
add_filter( 'sportspress_result_options', 'tony_sportspress_add_event_results_order_option' );
/**
* Override SportsPress event templates with plugin versions.
*
* @param string $template Located template path.
* @param string $template_name Template filename.
* @param string $template_path Template base path.
* @return string
*/
function tony_sportspress_locate_event_list_template( $template, $template_name, $template_path ) {
$supported = array(
'event-list.php',
'event-results.php',
);
if ( ! in_array( $template_name, $supported, true ) ) {
return $template;
}
$override = dirname( __DIR__ ) . '/templates/' . $template_name;
if ( file_exists( $override ) ) {
return $override;
}
return $template;
}
add_filter( 'sportspress_locate_template', 'tony_sportspress_locate_event_list_template', 10, 3 );
/**
* Add admin styles for explicit Home/Away labels on event edit screens.
*/
function tony_sportspress_event_team_order_admin_styles() {
$screen = get_current_screen();
if ( ! $screen || 'sp_event' !== $screen->post_type ) {
return;
}
?>
<style>
#sp_teamdiv .sp-instance {
padding-top: 8px;
border-top: 1px solid #dcdcde;
}
#sp_teamdiv .sp-instance:first-child {
padding-top: 0;
border-top: 0;
}
#sp_teamdiv .tony-sp-home-away-label {
margin: 0 0 8px;
font-size: 12px;
letter-spacing: 0.02em;
text-transform: uppercase;
color: #50575e;
}
</style>
<?php
}
add_action( 'admin_head-post.php', 'tony_sportspress_event_team_order_admin_styles' );
add_action( 'admin_head-post-new.php', 'tony_sportspress_event_team_order_admin_styles' );
/**
* Add explicit Home/Away labels and reverse visual order in event edit teams metabox.
*/
function tony_sportspress_event_team_order_admin_script() {
$screen = get_current_screen();
if ( ! $screen || 'sp_event' !== $screen->post_type ) {
return;
}
$slot_labels = array(
__( 'Home Team', 'tonys-sportspress-enhancements' ),
__( 'Away Team', 'tonys-sportspress-enhancements' ),
);
$show_away_first = tony_sportspress_reverse_teams_enabled();
?>
<script>
(function($) {
function applyHomeAwayLabels() {
var labels = <?php echo wp_json_encode( $slot_labels ); ?>;
var showAwayFirst = <?php echo $show_away_first ? 'true' : 'false'; ?>;
var $instances = $('#sp_teamdiv .sp-instance');
if (!$instances.length) {
return;
}
var $container = $instances.first().parent();
$instances.css('order', '');
if (showAwayFirst && $instances.length > 1) {
$container.css({
display: 'flex',
flexDirection: 'column'
});
$instances.each(function(index) {
$(this).css('order', index + 1);
});
$instances.eq(0).css('order', 2);
$instances.eq(1).css('order', 1);
}
$instances.each(function(index) {
var label = labels[index] || ('Team ' + (index + 1));
var $instance = $(this);
if (!$instance.children('.tony-sp-home-away-label').length) {
$instance.prepend('<p class="tony-sp-home-away-label"><strong>' + label + '</strong></p>');
} else {
$instance.children('.tony-sp-home-away-label').find('strong').text(label);
}
$instance.find('select[name="sp_team[]"]').first().attr('aria-label', label);
});
}
$(applyHomeAwayLabels);
$(document).on('sp-init-chosen sp-init', applyHomeAwayLabels);
})(jQuery);
</script>
<?php
}
add_action( 'admin_footer-post.php', 'tony_sportspress_event_team_order_admin_script' );
add_action( 'admin_footer-post-new.php', 'tony_sportspress_event_team_order_admin_script' );
/**
* Normalize event-list results array to match event team order.
*
* @param array $main_results Team results array.
* @param int $event_id Event ID.
* @return array
*/
function tony_sportspress_event_list_score_order( $main_results, $event_id ) {
if ( ! is_array( $main_results ) || empty( $main_results ) ) {
return $main_results;
}
$teams = (array) get_post_meta( $event_id, 'sp_team', false );
$teams = array_values( array_filter( array_map( 'absint', $teams ) ) );
if ( empty( $teams ) ) {
return $main_results;
}
$ordered = array();
foreach ( $teams as $index => $team_id ) {
if ( array_key_exists( $team_id, $main_results ) ) {
$ordered[ $team_id ] = $main_results[ $team_id ];
continue;
}
// SportsPress main_results() can be positional (0,1,...) in team order.
if ( array_key_exists( $index, $main_results ) ) {
$ordered[ $team_id ] = $main_results[ $index ];
}
}
if ( empty( $ordered ) ) {
return $main_results;
}
foreach ( $main_results as $team_id => $result ) {
if ( array_key_exists( $team_id, $ordered ) ) {
continue;
}
// Skip positional keys that have already been remapped to team IDs.
if ( is_int( $team_id ) || ctype_digit( (string) $team_id ) ) {
$position = (int) $team_id;
if ( array_key_exists( $position, $teams ) ) {
continue;
}
}
$ordered[ $team_id ] = $result;
}
return $ordered;
}
add_filter( 'sportspress_event_list_main_results', 'tony_sportspress_event_list_score_order', 999, 2 );

View File

@@ -0,0 +1,312 @@
<?php
/**
* GitHub release updater for the plugin.
*
* @package Tonys_Sportspress_Enhancements
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! defined( 'TONY_SPORTSPRESS_ENHANCEMENTS_GITHUB_REPO' ) ) {
define( 'TONY_SPORTSPRESS_ENHANCEMENTS_GITHUB_REPO', 'anthonyscorrea/tonys-sportspress-enhancements' );
}
if ( ! class_exists( 'Tony_Sportspress_GitHub_Updater' ) ) {
/**
* Integrates WordPress plugin updates with GitHub Releases.
*/
class Tony_Sportspress_GitHub_Updater {
/**
* GitHub API URL for the latest release.
*
* @var string
*/
private $release_api_url;
/**
* Plugin basename.
*
* @var string
*/
private $plugin_basename;
/**
* Plugin slug.
*
* @var string
*/
private $plugin_slug;
/**
* Cache key for release metadata.
*
* @var string
*/
private $cache_key = 'tony_sportspress_github_release';
/**
* Constructor.
*/
public function __construct() {
$this->release_api_url = sprintf(
'https://api.github.com/repos/%s/releases/latest',
TONY_SPORTSPRESS_ENHANCEMENTS_GITHUB_REPO
);
$this->plugin_basename = TONY_SPORTSPRESS_ENHANCEMENTS_PLUGIN_BASENAME;
$this->plugin_slug = dirname( $this->plugin_basename );
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'inject_update' ) );
add_filter( 'plugins_api', array( $this, 'plugin_information' ), 20, 3 );
add_filter( 'upgrader_source_selection', array( $this, 'normalize_source_directory' ), 10, 4 );
add_action( 'upgrader_process_complete', array( $this, 'purge_release_cache' ), 10, 2 );
}
/**
* Adds plugin update data to WordPress' update transient.
*
* @param stdClass $transient Existing update transient.
* @return stdClass
*/
public function inject_update( $transient ) {
if ( ! is_object( $transient ) || empty( $transient->checked ) ) {
return $transient;
}
$release = $this->get_latest_release();
if ( ! $release ) {
return $transient;
}
$remote_version = $this->normalize_version( $release['version'] );
$current_version = $this->normalize_version( TONY_SPORTSPRESS_ENHANCEMENTS_VERSION );
$plugin_data = (object) array(
'id' => $release['url'],
'slug' => $this->plugin_slug,
'plugin' => $this->plugin_basename,
'new_version' => $remote_version,
'url' => $release['url'],
'package' => $release['package'],
'tested' => '',
'requires_php' => '',
'icons' => array(),
'banners' => array(),
'banners_rtl' => array(),
'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;
}
/**
* Provides plugin information for the update details modal.
*
* @param false|object|array $result Existing result.
* @param string $action API action.
* @param object $args API args.
* @return false|object|array
*/
public function plugin_information( $result, $action, $args ) {
if ( 'plugin_information' !== $action || empty( $args->slug ) || $this->plugin_slug !== $args->slug ) {
return $result;
}
$release = $this->get_latest_release();
if ( ! $release ) {
return $result;
}
return (object) array(
'name' => 'Tonys SportsPress Enhancements',
'slug' => $this->plugin_slug,
'version' => $this->normalize_version( $release['version'] ),
'author' => '<a href="https://github.com/anthonyscorrea/">Tony Correa</a>',
'author_profile'=> 'https://github.com/anthonyscorrea/',
'homepage' => $release['url'],
'download_link' => $release['package'],
'sections' => array(
'description' => wp_kses_post( wpautop( 'Suite of SportsPress Enhancements.' ) ),
'changelog' => wp_kses_post( wpautop( $release['body'] ) ),
),
);
}
/**
* Ensures GitHub's extracted directory name matches the installed plugin slug.
*
* @param string $source Source file location.
* @param string $remote_source Remote file source location.
* @param WP_Upgrader $upgrader Upgrader instance.
* @param array $hook_extra Extra hook arguments.
* @return string|WP_Error
*/
public function normalize_source_directory( $source, $remote_source, $upgrader, $hook_extra ) {
global $wp_filesystem;
if ( empty( $hook_extra['plugin'] ) || $this->plugin_basename !== $hook_extra['plugin'] ) {
return $source;
}
$expected_dir = trailingslashit( $remote_source ) . $this->plugin_slug;
if ( untrailingslashit( $source ) === untrailingslashit( $expected_dir ) ) {
return $source;
}
if ( ! $wp_filesystem ) {
return $source;
}
if ( $wp_filesystem->exists( $expected_dir ) ) {
$wp_filesystem->delete( $expected_dir, true );
}
if ( ! $wp_filesystem->move( $source, $expected_dir ) ) {
return new WP_Error(
'tony_sportspress_updater_rename_failed',
__( 'The plugin update package could not be prepared for installation.', 'tonys-sportspress-enhancements' )
);
}
return $expected_dir;
}
/**
* Clears cached release metadata after plugin updates complete.
*
* @param WP_Upgrader $upgrader Upgrader instance.
* @param array $hook_extra Extra hook arguments.
* @return void
*/
public function purge_release_cache( $upgrader, $hook_extra ) {
if ( empty( $hook_extra['type'] ) || 'plugin' !== $hook_extra['type'] ) {
return;
}
if ( empty( $hook_extra['plugins'] ) || ! in_array( $this->plugin_basename, (array) $hook_extra['plugins'], true ) ) {
return;
}
delete_site_transient( $this->cache_key );
}
/**
* Reads and caches the latest GitHub release metadata.
*
* @return array|null
*/
private function get_latest_release() {
$cached = get_site_transient( $this->cache_key );
if ( is_array( $cached ) ) {
return $cached;
}
$response = wp_remote_get(
$this->release_api_url,
array(
'timeout' => 15,
'headers' => array(
'Accept' => 'application/vnd.github+json',
'User-Agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . home_url( '/' ),
),
)
);
if ( is_wp_error( $response ) ) {
return null;
}
if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
return null;
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $data ) || empty( $data['tag_name'] ) || empty( $data['html_url'] ) ) {
return null;
}
$release = array(
'version' => $data['tag_name'],
'url' => $data['html_url'],
'body' => isset( $data['body'] ) ? (string) $data['body'] : '',
'package' => $this->determine_package_url( $data ),
);
if ( empty( $release['package'] ) ) {
return null;
}
set_site_transient( $this->cache_key, $release, 6 * HOUR_IN_SECONDS );
return $release;
}
/**
* Selects the best package URL from a release payload.
*
* @param array $release GitHub release payload.
* @return string
*/
private function determine_package_url( $release ) {
if ( ! empty( $release['assets'] ) && is_array( $release['assets'] ) ) {
$fallback_asset = '';
foreach ( $release['assets'] as $asset ) {
if ( empty( $asset['browser_download_url'] ) || empty( $asset['name'] ) ) {
continue;
}
if ( '.zip' !== strtolower( substr( $asset['name'], -4 ) ) ) {
continue;
}
if ( false !== strpos( $asset['name'], $this->plugin_slug ) ) {
return $asset['browser_download_url'];
}
if ( empty( $fallback_asset ) ) {
$fallback_asset = $asset['browser_download_url'];
}
}
if ( ! empty( $fallback_asset ) ) {
return $fallback_asset;
}
}
if ( ! empty( $release['zipball_url'] ) ) {
return $release['zipball_url'];
}
return '';
}
/**
* Normalizes release versions so Git tags like v1.2.3 compare correctly.
*
* @param string $version Version string.
* @return string
*/
private function normalize_version( $version ) {
return ltrim( (string) $version, "vV \t\n\r\0\x0B" );
}
}
new Tony_Sportspress_GitHub_Updater();
}

View File

@@ -0,0 +1,420 @@
<?php
/**
* Officials Manager role and capability restrictions.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! defined( 'TONY_SPORTSPRESS_OFFICIALS_MANAGER_ROLE' ) ) {
define( 'TONY_SPORTSPRESS_OFFICIALS_MANAGER_ROLE', 'sp_officials_manager' );
}
/**
* Build the primitive capabilities for a custom post type.
*
* @param string $singular Singular capability base.
* @param string $plural Plural capability base.
* @return string[]
*/
function tony_sportspress_build_post_type_caps( $singular, $plural ) {
return array(
"edit_{$singular}",
"read_{$singular}",
"delete_{$singular}",
"edit_{$plural}",
"edit_others_{$plural}",
"publish_{$plural}",
"read_private_{$plural}",
"delete_{$plural}",
"delete_private_{$plural}",
"delete_published_{$plural}",
"delete_others_{$plural}",
"edit_private_{$plural}",
"edit_published_{$plural}",
);
}
/**
* Get officials manager role capabilities.
*
* @return array<string, bool>
*/
function tony_sportspress_get_officials_manager_caps() {
$caps = array(
'read' => true,
'edit_posts' => true,
'delete_posts' => true,
'edit_published_posts' => true,
);
foreach ( tony_sportspress_build_post_type_caps( 'sp_official', 'sp_officials' ) as $cap ) {
$caps[ $cap ] = true;
}
// Allow access to the event list and quick edit for assignments, without full event management.
$caps['read_sp_event'] = true;
$caps['edit_sp_event'] = true;
$caps['edit_sp_events'] = true;
$caps['edit_others_sp_events'] = true;
$caps['edit_published_sp_events'] = true;
$caps['read_private_sp_events'] = true;
return $caps;
}
/**
* Get the capabilities managed for the officials manager role.
*
* @return string[]
*/
function tony_sportspress_get_officials_manager_managed_caps() {
return array_keys( tony_sportspress_get_officials_manager_caps() );
}
/**
* Grant custom official caps to existing roles that already have matching event caps.
*
* This preserves existing access after moving officials off the `sp_event` capability type.
*
* @return void
*/
function tony_sportspress_sync_official_caps_to_existing_roles() {
global $wp_roles;
if ( ! class_exists( 'WP_Roles' ) ) {
return;
}
if ( ! isset( $wp_roles ) ) {
$wp_roles = wp_roles();
}
if ( ! $wp_roles instanceof WP_Roles ) {
return;
}
$cap_map = array(
'edit_sp_event' => 'edit_sp_official',
'read_sp_event' => 'read_sp_official',
'delete_sp_event' => 'delete_sp_official',
'edit_sp_events' => 'edit_sp_officials',
'edit_others_sp_events' => 'edit_others_sp_officials',
'publish_sp_events' => 'publish_sp_officials',
'read_private_sp_events' => 'read_private_sp_officials',
'delete_sp_events' => 'delete_sp_officials',
'delete_private_sp_events' => 'delete_private_sp_officials',
'delete_published_sp_events' => 'delete_published_sp_officials',
'delete_others_sp_events' => 'delete_others_sp_officials',
'edit_private_sp_events' => 'edit_private_sp_officials',
'edit_published_sp_events' => 'edit_published_sp_officials',
);
foreach ( $wp_roles->role_objects as $role ) {
if ( ! $role instanceof WP_Role ) {
continue;
}
foreach ( $cap_map as $event_cap => $official_cap ) {
if ( $role->has_cap( $event_cap ) ) {
$role->add_cap( $official_cap );
}
}
}
}
/**
* Create or update the officials manager role.
*
* @return void
*/
function tony_sportspress_sync_officials_manager_roles() {
$role = get_role( TONY_SPORTSPRESS_OFFICIALS_MANAGER_ROLE );
if ( ! $role ) {
$role = add_role(
TONY_SPORTSPRESS_OFFICIALS_MANAGER_ROLE,
__( 'Officials Manager', 'tonys-sportspress-enhancements' ),
array()
);
}
if ( ! $role instanceof WP_Role ) {
return;
}
$desired_caps = tony_sportspress_get_officials_manager_caps();
foreach ( tony_sportspress_get_officials_manager_managed_caps() as $cap ) {
if ( ! empty( $desired_caps[ $cap ] ) ) {
$role->add_cap( $cap );
} else {
$role->remove_cap( $cap );
}
}
tony_sportspress_sync_official_caps_to_existing_roles();
}
add_action( 'init', 'tony_sportspress_sync_officials_manager_roles' );
/**
* Assign custom capabilities to the officials post type.
*
* @param array $args Post type registration args.
* @return array
*/
function tony_sportspress_officials_post_type_caps( $args ) {
$args['capability_type'] = array( 'sp_official', 'sp_officials' );
$args['map_meta_cap'] = true;
$args['capabilities'] = array(
'create_posts' => 'edit_sp_officials',
);
return $args;
}
add_filter( 'sportspress_register_post_type_official', 'tony_sportspress_officials_post_type_caps' );
/**
* Determine whether the current user should be restricted to assignment-only event access.
*
* @return bool
*/
function tony_sportspress_is_officials_manager_user() {
$user = wp_get_current_user();
if ( ! $user instanceof WP_User || empty( $user->roles ) ) {
return false;
}
if ( ! in_array( TONY_SPORTSPRESS_OFFICIALS_MANAGER_ROLE, $user->roles, true ) ) {
return false;
}
if ( current_user_can( 'manage_options' ) || current_user_can( 'manage_sportspress' ) ) {
return false;
}
return true;
}
/**
* Prevent assignment-only users from opening full event edit screens.
*
* @return void
*/
function tony_sportspress_lock_event_editor_for_officials_manager() {
global $pagenow;
if ( ! is_admin() || ! tony_sportspress_is_officials_manager_user() ) {
return;
}
if ( 'post-new.php' === $pagenow ) {
$post_type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : 'post';
if ( 'sp_event' === $post_type ) {
wp_safe_redirect( admin_url( 'edit.php?post_type=sp_event&tse_event_editor_locked=1' ) );
exit;
}
}
if ( 'post.php' !== $pagenow ) {
return;
}
$post_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0;
if ( $post_id > 0 && 'sp_event' === get_post_type( $post_id ) ) {
wp_safe_redirect( admin_url( 'edit.php?post_type=sp_event&tse_event_editor_locked=1' ) );
exit;
}
}
add_action( 'admin_init', 'tony_sportspress_lock_event_editor_for_officials_manager' );
/**
* Show an admin notice when event editor access is blocked.
*
* @return void
*/
function tony_sportspress_officials_manager_admin_notice() {
if ( ! tony_sportspress_is_officials_manager_user() ) {
return;
}
if ( empty( $_GET['tse_event_editor_locked'] ) ) {
return;
}
echo '<div class="notice notice-info is-dismissible"><p>' . esc_html__( 'Officials Managers can assign officials from the events list via Quick Edit, but cannot open the full event editor.', 'tonys-sportspress-enhancements' ) . '</p></div>';
}
add_action( 'admin_notices', 'tony_sportspress_officials_manager_admin_notice' );
/**
* Remove event row actions that would expose broader editing.
*
* @param array $actions Row actions.
* @param WP_Post $post Post object.
* @return array
*/
function tony_sportspress_limit_event_row_actions_for_officials_manager( $actions, $post ) {
if ( ! tony_sportspress_is_officials_manager_user() || ! $post instanceof WP_Post || 'sp_event' !== $post->post_type ) {
return $actions;
}
$allowed = array();
if ( isset( $actions['inline hide-if-no-js'] ) ) {
$allowed['inline hide-if-no-js'] = $actions['inline hide-if-no-js'];
}
if ( isset( $actions['view'] ) ) {
$allowed['view'] = $actions['view'];
}
return $allowed;
}
add_filter( 'post_row_actions', 'tony_sportspress_limit_event_row_actions_for_officials_manager', 10, 2 );
/**
* Remove bulk actions from the events list for assignment-only users.
*
* @param array $actions Bulk actions.
* @return array
*/
function tony_sportspress_limit_event_bulk_actions_for_officials_manager( $actions ) {
if ( ! tony_sportspress_is_officials_manager_user() ) {
return $actions;
}
return array();
}
add_filter( 'bulk_actions-edit-sp_event', 'tony_sportspress_limit_event_bulk_actions_for_officials_manager' );
/**
* Remove the Add New events submenu for assignment-only users.
*
* @return void
*/
function tony_sportspress_limit_event_admin_menu_for_officials_manager() {
if ( ! tony_sportspress_is_officials_manager_user() ) {
return;
}
remove_submenu_page( 'edit.php?post_type=sp_event', 'post-new.php?post_type=sp_event' );
}
add_action( 'admin_menu', 'tony_sportspress_limit_event_admin_menu_for_officials_manager', 99 );
/**
* Hide event Add New buttons on the list screen for assignment-only users.
*
* @return void
*/
function tony_sportspress_limit_event_admin_ui_for_officials_manager() {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || 'edit-sp_event' !== $screen->id || ! tony_sportspress_is_officials_manager_user() ) {
return;
}
?>
<style>
.post-type-sp_event .page-title-action,
.post-type-sp_event .wrap .bulkactions {
display: none;
}
.post-type-sp_event .wp-list-table .column-title .row-title {
color: inherit;
cursor: default;
text-decoration: none;
}
</style>
<script>
(function() {
const replaceTitleLinks = function() {
document.querySelectorAll('.post-type-sp_event .wp-list-table .column-title a.row-title').forEach(function(link) {
const text = document.createTextNode(link.textContent || '');
const span = document.createElement('span');
span.className = link.className;
span.appendChild(text);
link.replaceWith(span);
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', replaceTitleLinks);
} else {
replaceTitleLinks();
}
})();
</script>
<?php
}
add_action( 'admin_head', 'tony_sportspress_limit_event_admin_ui_for_officials_manager' );
/**
* Prevent clickable edit links to events for assignment-only users.
*
* This keeps the list-table title as plain text while preserving Quick Edit.
*
* @param string|false $link Edit link.
* @param int $post_id Post ID.
* @param string $context Link context.
* @return string|false
*/
function tony_sportspress_disable_event_edit_links_for_officials_manager( $link, $post_id, $context ) {
if ( ! is_admin() || ! tony_sportspress_is_officials_manager_user() ) {
return $link;
}
if ( 'sp_event' !== get_post_type( $post_id ) ) {
return $link;
}
return false;
}
add_filter( 'get_edit_post_link', 'tony_sportspress_disable_event_edit_links_for_officials_manager', 10, 3 );
/**
* Preserve core event fields so assignment-only users cannot alter them via Quick Edit.
*
* @param array $data Sanitized post data.
* @param array $postarr Raw post array.
* @return array
*/
function tony_sportspress_protect_event_fields_for_officials_manager( $data, $postarr ) {
if ( ! is_admin() || ! tony_sportspress_is_officials_manager_user() ) {
return $data;
}
if ( empty( $postarr['ID'] ) || 'sp_event' !== $data['post_type'] ) {
return $data;
}
$existing_post = get_post( (int) $postarr['ID'], ARRAY_A );
if ( ! is_array( $existing_post ) ) {
return $data;
}
$protected_fields = array(
'post_author',
'post_content',
'post_content_filtered',
'post_date',
'post_date_gmt',
'post_excerpt',
'post_name',
'post_parent',
'post_password',
'post_status',
'post_title',
);
foreach ( $protected_fields as $field ) {
if ( isset( $existing_post[ $field ] ) ) {
$data[ $field ] = $existing_post[ $field ];
}
}
return $data;
}
add_filter( 'wp_insert_post_data', 'tony_sportspress_protect_event_fields_for_officials_manager', 20, 2 );

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

285
includes/sp-url-builder.php Normal file
View File

@@ -0,0 +1,285 @@
<?php
/**
* Tony's Settings CSV URL builder tab.
*
* @package Tonys_Sportspress_Enhancements
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register the URL builder tab label.
*
* @param array $tabs Existing tab map.
* @return array
*/
function tse_sp_url_builder_register_tab( $tabs ) {
$tabs['url-builder'] = __( 'Feed Builder', 'tonys-sportspress-enhancements' );
return $tabs;
}
add_filter( 'tse_tonys_settings_tabs', 'tse_sp_url_builder_register_tab' );
/**
* Render the URL builder tab.
*
* @return void
*/
function tse_sp_url_builder_render_tab() {
$leagues = function_exists( 'tse_sp_schedule_exporter_get_leagues' ) ? tse_sp_schedule_exporter_get_leagues() : array();
$seasons = function_exists( 'tse_sp_schedule_exporter_get_seasons' ) ? tse_sp_schedule_exporter_get_seasons() : array();
$teams = function_exists( 'tse_sp_schedule_exporter_get_teams' ) ? tse_sp_schedule_exporter_get_teams() : array();
$fields = function_exists( 'tse_sp_schedule_exporter_get_fields' ) ? tse_sp_schedule_exporter_get_fields() : array();
$formats = function_exists( 'tse_sp_event_export_get_formats' ) ? tse_sp_event_export_get_formats() : array();
$columns = function_exists( 'tse_sp_event_export_get_column_definitions' ) ? tse_sp_event_export_get_column_definitions() : array();
echo '<h2>' . esc_html__( 'Feed Builder', 'tonys-sportspress-enhancements' ) . '</h2>';
echo '<p>' . esc_html__( 'Build a shareable CSV feed URL with format, filters, and custom columns. This does not save settings.', 'tonys-sportspress-enhancements' ) . '</p>';
echo '<div class="tse-url-builder" style="max-width:1100px;padding:20px 24px;border:1px solid #dcdcde;background:#fff;">';
echo '<table class="form-table" role="presentation"><tbody>';
echo '<tr><th scope="row"><label for="tse-url-builder-feed-type">' . esc_html__( 'Feed Type', 'tonys-sportspress-enhancements' ) . '</label></th><td>';
echo '<select id="tse-url-builder-feed-type">';
echo '<option value="csv">' . esc_html__( 'CSV', 'tonys-sportspress-enhancements' ) . '</option>';
echo '<option value="ics">' . esc_html__( 'iCal / ICS', 'tonys-sportspress-enhancements' ) . '</option>';
echo '</select>';
echo '</td></tr>';
echo '<tr><th scope="row"><label for="tse-url-builder-format">' . esc_html__( 'Format', 'tonys-sportspress-enhancements' ) . '</label></th><td>';
echo '<select id="tse-url-builder-format">';
foreach ( $formats as $format_key => $format ) {
echo '<option value="' . esc_attr( $format_key ) . '">' . esc_html( $format['label'] ) . '</option>';
}
echo '</select>';
echo '</td></tr>';
echo '<tr><th scope="row">' . esc_html__( 'League', 'tonys-sportspress-enhancements' ) . '</th><td>';
echo '<div id="tse-url-builder-league" style="display:flex;flex-wrap:wrap;gap:10px 14px;max-width:720px;">';
foreach ( $leagues as $league ) {
$input_id = 'tse-url-builder-league-' . absint( $league->term_id );
echo '<label for="' . esc_attr( $input_id ) . '" style="display:inline-flex;align-items:center;gap:6px;">';
echo '<input id="' . esc_attr( $input_id ) . '" type="checkbox" data-tse-builder-filter="league_id" value="' . esc_attr( (string) $league->term_id ) . '" />';
echo esc_html( $league->name );
echo '</label>';
}
echo '</div>';
echo '<p class="description">' . esc_html__( 'Select one or more leagues. Leave empty to include all leagues.', 'tonys-sportspress-enhancements' ) . '</p>';
echo '</td></tr>';
echo '<tr><th scope="row">' . esc_html__( 'Season', 'tonys-sportspress-enhancements' ) . '</th><td>';
echo '<div id="tse-url-builder-season" style="display:flex;flex-wrap:wrap;gap:10px 14px;max-width:720px;">';
foreach ( $seasons as $season ) {
$input_id = 'tse-url-builder-season-' . absint( $season->term_id );
echo '<label for="' . esc_attr( $input_id ) . '" style="display:inline-flex;align-items:center;gap:6px;">';
echo '<input id="' . esc_attr( $input_id ) . '" type="checkbox" data-tse-builder-filter="season_id" value="' . esc_attr( (string) $season->term_id ) . '" />';
echo esc_html( $season->name );
echo '</label>';
}
echo '</div>';
echo '<p class="description">' . esc_html__( 'Select one or more seasons. Leave empty to include all seasons.', 'tonys-sportspress-enhancements' ) . '</p>';
echo '</td></tr>';
echo '<tr><th scope="row">' . esc_html__( 'Team', 'tonys-sportspress-enhancements' ) . '</th><td>';
echo '<div id="tse-url-builder-team" style="display:flex;flex-wrap:wrap;gap:10px 14px;max-width:720px;">';
foreach ( $teams as $team ) {
$input_id = 'tse-url-builder-team-' . absint( $team->ID );
echo '<label for="' . esc_attr( $input_id ) . '" style="display:inline-flex;align-items:center;gap:6px;">';
echo '<input id="' . esc_attr( $input_id ) . '" type="checkbox" data-tse-builder-filter="team_id" value="' . esc_attr( (string) $team->ID ) . '" />';
echo esc_html( $team->post_title );
echo '</label>';
}
echo '</div>';
echo '<p class="description">' . esc_html__( 'Select one or more teams. Team format requires exactly one team.', 'tonys-sportspress-enhancements' ) . '</p>';
echo '</td></tr>';
echo '<tr><th scope="row">' . esc_html__( 'Field', 'tonys-sportspress-enhancements' ) . '</th><td>';
echo '<div id="tse-url-builder-field" style="display:flex;flex-wrap:wrap;gap:10px 14px;max-width:720px;">';
foreach ( $fields as $field ) {
$input_id = 'tse-url-builder-field-' . absint( $field->term_id );
echo '<label for="' . esc_attr( $input_id ) . '" style="display:inline-flex;align-items:center;gap:6px;">';
echo '<input id="' . esc_attr( $input_id ) . '" type="checkbox" data-tse-builder-filter="field_id" value="' . esc_attr( (string) $field->term_id ) . '" />';
echo esc_html( $field->name );
echo '</label>';
}
echo '</div>';
echo '<p class="description">' . esc_html__( 'Select one or more fields. Leave empty to include all fields.', 'tonys-sportspress-enhancements' ) . '</p>';
echo '</td></tr>';
echo '</tbody></table>';
echo '<div style="display:grid;gap:16px;margin-top:20px;">';
foreach ( $columns as $format_key => $format_columns ) {
$default_columns = function_exists( 'tse_sp_event_export_get_default_columns' ) ? tse_sp_event_export_get_default_columns( $format_key ) : array();
$label = isset( $formats[ $format_key ]['label'] ) ? $formats[ $format_key ]['label'] : ucfirst( $format_key );
echo '<fieldset data-tse-builder-columns="' . esc_attr( $format_key ) . '" style="margin:0;padding:16px;border:1px solid #d7d7db;">';
echo '<legend><strong>' . esc_html( sprintf( __( '%s Columns', 'tonys-sportspress-enhancements' ), $label ) ) . '</strong></legend>';
echo '<div style="display:flex;flex-wrap:wrap;gap:12px 18px;">';
foreach ( $format_columns as $column_key => $column_label ) {
$input_id = 'tse-url-builder-' . sanitize_html_class( $format_key . '-' . $column_key );
echo '<label for="' . esc_attr( $input_id ) . '" style="display:inline-flex;align-items:center;gap:6px;">';
echo '<input id="' . esc_attr( $input_id ) . '" type="checkbox" data-tse-builder-column="' . esc_attr( $format_key ) . '" value="' . esc_attr( $column_key ) . '" ' . checked( in_array( $column_key, $default_columns, true ), true, false ) . ' />';
echo esc_html( $column_label );
echo '</label>';
}
echo '</div>';
echo '</fieldset>';
}
echo '</div>';
echo '<h3 style="margin-top:24px;">' . esc_html__( 'Generated URL', 'tonys-sportspress-enhancements' ) . '</h3>';
echo '<div style="display:flex;align-items:center;gap:8px;max-width:100%;">';
echo '<input type="text" id="tse-url-builder-output" class="large-text code" readonly="readonly" />';
echo '<button type="button" id="tse-url-builder-copy" class="button" aria-label="' . esc_attr__( 'Copy URL', 'tonys-sportspress-enhancements' ) . '" title="' . esc_attr__( 'Copy URL', 'tonys-sportspress-enhancements' ) . '" style="display:inline-flex;align-items:center;justify-content:center;min-width:40px;padding:0 10px;">';
echo '<span aria-hidden="true" style="font-size:16px;line-height:1;">⧉</span>';
echo '</button>';
echo '</div>';
echo '<p><a id="tse-url-builder-open" class="button button-primary" href="' . esc_url( home_url( '/' ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open Feed URL', 'tonys-sportspress-enhancements' ) . '</a></p>';
echo '<p class="description">' . esc_html__( 'The builder always generates the standalone CSV feed endpoint using the selected filters and columns.', 'tonys-sportspress-enhancements' ) . '</p>';
echo '</div>';
tse_sp_url_builder_render_script();
}
add_action( 'tse_tonys_settings_render_tab_url-builder', 'tse_sp_url_builder_render_tab' );
/**
* Render builder script.
*
* @return void
*/
function tse_sp_url_builder_render_script() {
$base_url = home_url( '/' );
?>
<script>
(function(){
var root = document.querySelector('.tse-url-builder');
if (!root) {
return;
}
var baseUrl = <?php echo wp_json_encode( $base_url ); ?>;
var feedType = root.querySelector('#tse-url-builder-feed-type');
var format = root.querySelector('#tse-url-builder-format');
var output = root.querySelector('#tse-url-builder-output');
var copyButton = root.querySelector('#tse-url-builder-copy');
var openLink = root.querySelector('#tse-url-builder-open');
function getSelectedValues(filterName) {
return Array.prototype.slice.call(root.querySelectorAll('[data-tse-builder-filter="' + filterName + '"]:checked')).map(function(input){
return input.value;
});
}
function syncColumnGroups() {
var selectedFormat = format.value || 'matchup';
var isCsv = (feedType.value || 'csv') === 'csv';
root.querySelectorAll('[data-tse-builder-columns]').forEach(function(group){
group.style.display = (isCsv && group.getAttribute('data-tse-builder-columns') === selectedFormat) ? 'block' : 'none';
});
}
function buildUrl() {
var selectedFeedType = feedType.value || 'csv';
var selectedFormat = format.value || 'matchup';
var url = new URL(baseUrl, window.location.origin);
var leagues = getSelectedValues('league_id');
var seasons = getSelectedValues('season_id');
var teams = getSelectedValues('team_id');
var fields = getSelectedValues('field_id');
var selectedColumns = Array.prototype.slice.call(root.querySelectorAll('[data-tse-builder-column="' + selectedFormat + '"]:checked')).map(function(input){
return input.value;
}).filter(Boolean);
url.searchParams.set('feed', selectedFeedType === 'ics' ? 'sp-ics' : 'sp-csv');
url.searchParams.set('format', selectedFormat);
if (leagues.length) {
url.searchParams.set('league_id', leagues.join(','));
} else {
url.searchParams.delete('league_id');
}
if (seasons.length) {
url.searchParams.set('season_id', seasons.join(','));
} else {
url.searchParams.delete('season_id');
}
if (teams.length) {
url.searchParams.set('team_id', teams.join(','));
} else {
url.searchParams.delete('team_id');
}
if (fields.length) {
url.searchParams.set('field_id', fields.join(','));
} else {
url.searchParams.delete('field_id');
}
if (selectedFeedType === 'csv' && selectedColumns.length) {
url.searchParams.set('columns', selectedColumns.join(','));
} else {
url.searchParams.delete('columns');
}
output.value = url.toString();
openLink.href = url.toString();
openLink.textContent = selectedFeedType === 'ics' ? 'Open ICS Feed' : 'Open Feed URL';
}
syncColumnGroups();
buildUrl();
[feedType, format].forEach(function(input){
input.addEventListener('change', function(){
syncColumnGroups();
buildUrl();
});
});
root.querySelectorAll('[data-tse-builder-filter]').forEach(function(input){
input.addEventListener('change', buildUrl);
});
root.querySelectorAll('[data-tse-builder-column]').forEach(function(input){
input.addEventListener('change', buildUrl);
});
if (copyButton) {
copyButton.addEventListener('click', function(){
var value = output.value || '';
if (!value) {
return;
}
var defaultTitle = copyButton.getAttribute('data-default-title') || copyButton.title || 'Copy URL';
copyButton.setAttribute('data-default-title', defaultTitle);
function markCopied() {
copyButton.title = 'Copied';
window.setTimeout(function(){
copyButton.title = defaultTitle;
}, 1200);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(value).then(function(){
markCopied();
}).catch(function(){
output.focus();
output.select();
document.execCommand('copy');
markCopied();
});
return;
}
output.focus();
output.select();
document.execCommand('copy');
markCopied();
});
}
})();
</script>
<?php
}

471
includes/sp-venue-meta.php Normal file
View File

@@ -0,0 +1,471 @@
<?php
/**
* Venue term metadata support.
*
* Adds short name, abbreviation, and ground rules fields to SportsPress venues.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register venue term meta.
*/
function tony_sportspress_register_venue_term_meta() {
register_term_meta(
'sp_venue',
'tse_short_name',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => 'sanitize_text_field',
'show_in_rest' => true,
'auth_callback' => static function() {
return current_user_can( 'manage_categories' );
},
)
);
register_term_meta(
'sp_venue',
'tse_abbreviation',
array(
'type' => 'string',
'single' => true,
'sanitize_callback' => 'sanitize_text_field',
'show_in_rest' => true,
'auth_callback' => static function() {
return current_user_can( 'manage_categories' );
},
)
);
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.
*/
function tony_sportspress_add_venue_meta_fields() {
?>
<div class="form-field term-short-name-wrap">
<label for="tse_short_name"><?php esc_html_e( 'Short Name', 'tonys-sportspress-enhancements' ); ?></label>
<input name="tse_short_name" id="tse_short_name" type="text" value="" maxlength="100" />
<p><?php esc_html_e( 'Optional shorter label for this field or venue.', 'tonys-sportspress-enhancements' ); ?></p>
</div>
<div class="form-field term-abbreviation-wrap">
<label for="tse_abbreviation"><?php esc_html_e( 'Abbreviation', 'tonys-sportspress-enhancements' ); ?></label>
<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' );
/**
* Render edit-form fields for venue metadata.
*
* @param WP_Term $term Venue term.
*/
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">
<label for="tse_short_name"><?php esc_html_e( 'Short Name', 'tonys-sportspress-enhancements' ); ?></label>
</th>
<td>
<input name="tse_short_name" id="tse_short_name" type="text" value="<?php echo esc_attr( $short_name ); ?>" maxlength="100" />
<p class="description"><?php esc_html_e( 'Optional shorter label for this field or venue.', 'tonys-sportspress-enhancements' ); ?></p>
</td>
</tr>
<tr class="form-field term-abbreviation-wrap">
<th scope="row">
<label for="tse_abbreviation"><?php esc_html_e( 'Abbreviation', 'tonys-sportspress-enhancements' ); ?></label>
</th>
<td>
<input name="tse_abbreviation" id="tse_abbreviation" type="text" value="<?php echo esc_attr( $abbreviation ); ?>" maxlength="20" />
<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' );
/**
* Save venue metadata fields.
*
* @param int $term_id Venue term ID.
*/
function tony_sportspress_save_venue_meta_fields( $term_id ) {
if ( ! current_user_can( 'manage_categories' ) ) {
return;
}
$short_name = isset( $_POST['tse_short_name'] ) ? sanitize_text_field( wp_unslash( $_POST['tse_short_name'] ) ) : '';
$short_name = is_string( $short_name ) ? trim( $short_name ) : '';
$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.0",
"version": "0.1.15",
"main": "Gruntfile.js",
"author": "YOUR NAME HERE",
"scripts" : {

View File

@@ -9,12 +9,14 @@ Tony's SportsPress Enhancements is a collection of add-ons for the [SportsPress]
- **Custom event permalinks** for `sp_event` post types, including season and team slugs.
- **Open Graph meta tags** for events, with dynamic titles, descriptions, and images.
- **Automatic featured image generation** for events, combining team colors and logos into a shareable image.
- **Printable team schedules** with season-aware venue colors and a print-friendly calendar layout.
## Features
- Custom rewrite rules and permalinks for SportsPress events.
- Open Graph integration for better social sharing.
- Dynamic, cached event images based on team data.
- Printable schedule pages linked from team profiles.
- Compatible with WordPress 4.5+ and PHP 5.6+.
## Installation

576
templates/event-list.php Normal file
View File

@@ -0,0 +1,576 @@
<?php
/**
* Event List
*
* @author ThemeBoy
* @package SportsPress/Templates
* @version 2.7.23
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
$defaults = array(
'id' => null,
'title' => false,
'status' => 'default',
'format' => 'default',
'date' => 'default',
'date_from' => 'default',
'date_to' => 'default',
'date_past' => 'default',
'date_future' => 'default',
'date_relative' => 'default',
'day' => 'default',
'league' => null,
'season' => null,
'venue' => null,
'team' => null,
'teams_past' => null,
'date_before' => null,
'player' => null,
'number' => -1,
'show_team_logo' => get_option( 'sportspress_event_list_show_logos', 'no' ) == 'yes' ? true : false,
'link_events' => get_option( 'sportspress_link_events', 'yes' ) == 'yes' ? true : false,
'link_teams' => get_option( 'sportspress_link_teams', 'no' ) == 'yes' ? true : false,
'link_venues' => get_option( 'sportspress_link_venues', 'yes' ) == 'yes' ? true : false,
'responsive' => get_option( 'sportspress_enable_responsive_tables', 'no' ) == 'yes' ? true : false,
'sortable' => get_option( 'sportspress_enable_sortable_tables', 'yes' ) == 'yes' ? true : false,
'scrollable' => get_option( 'sportspress_enable_scrollable_tables', 'yes' ) == 'yes' ? true : false,
'paginated' => get_option( 'sportspress_event_list_paginated', 'yes' ) == 'yes' ? true : false,
'rows' => get_option( 'sportspress_event_list_rows', 10 ),
'order' => 'default',
'columns' => null,
'show_all_events_link' => false,
'show_title' => get_option( 'sportspress_event_list_show_title', 'yes' ) == 'yes' ? true : false,
'title_format' => get_option( 'sportspress_event_list_title_format', 'title' ),
'time_format' => get_option( 'sportspress_event_list_time_format', 'combined' ),
);
extract( $defaults, EXTR_SKIP );
$calendar = new SP_Calendar( $id );
if ( $status != 'default' ) {
$calendar->status = $status;
}
if ( $format != 'default' ) {
$calendar->event_format = $format;
}
if ( $date != 'default' ) {
$calendar->date = $date;
}
if ( $date_from != 'default' ) {
$calendar->from = $date_from;
}
if ( $date_to != 'default' ) {
$calendar->to = $date_to;
}
if ( $date_past != 'default' ) {
$calendar->past = $date_past;
}
if ( $date_future != 'default' ) {
$calendar->future = $date_future;
}
if ( $date_relative != 'default' ) {
$calendar->relative = $date_relative;
}
if ( $league ) {
$calendar->league = $league;
}
if ( $season ) {
$calendar->season = $season;
}
if ( $venue ) {
$calendar->venue = $venue;
}
if ( $team ) {
$calendar->team = $team;
}
if ( $teams_past ) {
$calendar->teams_past = $teams_past;
}
if ( $date_before ) {
$calendar->date_before = $date_before;
}
if ( $player ) {
$calendar->player = $player;
}
if ( $order != 'default' ) {
$calendar->order = $order;
}
if ( $day != 'default' ) {
$calendar->day = $day;
}
$data = $calendar->data();
$usecolumns = $calendar->columns;
if ( isset( $columns ) ) :
if ( is_array( $columns ) ) {
$usecolumns = $columns;
} else {
$usecolumns = explode( ',', $columns );
}
endif;
$title_format_raw = $title_format;
$is_awayhome_format = 'awayhome' === $title_format_raw;
if ( $is_awayhome_format ) {
$title_format = 'homeaway';
}
$home_label = $is_awayhome_format ? esc_attr__( 'Away', 'sportspress' ) : esc_attr__( 'Home', 'sportspress' );
$away_label = $is_awayhome_format ? esc_attr__( 'Home', 'sportspress' ) : esc_attr__( 'Away', 'sportspress' );
if ( $show_title && false === $title && $id ) :
$caption = $calendar->caption;
if ( $caption ) {
$title = $caption;
} else {
$title = get_the_title( $id );
}
endif;
$labels = array();
// Create a unique identifier based on the current time in microseconds
$identifier = uniqid( 'eventlist_' );
?>
<div class="sp-template sp-template-event-list">
<?php if ( $title ) { ?>
<h4 class="sp-table-caption"><?php echo wp_kses_post( $title ); ?></h4>
<?php } ?>
<div class="sp-table-wrapper">
<table class="sp-event-list sp-event-list-format-<?php echo esc_attr( $title_format ); ?> sp-data-table
<?php
if ( $paginated ) {
?>
sp-paginated-table
<?php
} if ( $sortable ) {
?>
sp-sortable-table
<?php
} if ( $responsive ) {
echo ' sp-responsive-table ' . esc_attr( $identifier ); } if ( $scrollable ) {
?>
sp-scrollable-table <?php } ?>" data-sp-rows="<?php echo esc_attr( $rows ); ?>">
<thead>
<tr>
<?php
echo '<th class="data-date">' . esc_attr__( 'Date', 'sportspress' ) . '</th>';
switch ( $title_format ) {
case 'homeaway':
if ( sp_column_active( $usecolumns, 'event' ) ) {
echo '<th class="data-home">' . esc_html( $home_label ) . '</th>';
}
if ( 'combined' == $time_format && sp_column_active( $usecolumns, 'time' ) ) {
echo '<th class="data-time">' . esc_attr__( 'Time/Results', 'sportspress' ) . '</th>';
$labels[] = esc_attr__( 'Time/Results', 'sportspress' );
} elseif ( in_array( $time_format, array( 'separate', 'results' ) ) && sp_column_active( $usecolumns, 'results' ) ) {
echo '<th class="data-results">' . esc_attr__( 'Results', 'sportspress' ) . '</th>';
}
if ( sp_column_active( $usecolumns, 'event' ) ) {
echo '<th class="data-away">' . esc_html( $away_label ) . '</th>';
}
if ( in_array( $time_format, array( 'separate', 'time' ) ) && sp_column_active( $usecolumns, 'time' ) ) {
echo '<th class="data-time">' . esc_attr__( 'Time', 'sportspress' ) . '</th>';
}
break;
default:
if ( sp_column_active( $usecolumns, 'event' ) ) {
if ( $title_format == 'teams' ) {
echo '<th class="data-teams">' . esc_attr__( 'Teams', 'sportspress' ) . '</th>';
} else {
echo '<th class="data-event">' . esc_attr__( 'Event', 'sportspress' ) . '</th>';
}
}
switch ( $time_format ) {
case 'separate':
if ( sp_column_active( $usecolumns, 'time' ) ) {
echo '<th class="data-time">' . esc_attr__( 'Time', 'sportspress' ) . '</th>';
}
if ( sp_column_active( $usecolumns, 'results' ) ) {
echo '<th class="data-results">' . esc_attr__( 'Results', 'sportspress' ) . '</th>';
}
break;
case 'time':
if ( sp_column_active( $usecolumns, 'time' ) ) {
echo '<th class="data-time">' . esc_attr__( 'Time', 'sportspress' ) . '</th>';
}
break;
case 'results':
if ( sp_column_active( $usecolumns, 'results' ) ) {
echo '<th class="data-results">' . esc_attr__( 'Results', 'sportspress' ) . '</th>';
}
break;
default:
if ( sp_column_active( $usecolumns, 'time' ) ) {
echo '<th class="data-time">' . esc_attr__( 'Time/Results', 'sportspress' ) . '</th>';
}
}
}
if ( sp_column_active( $usecolumns, 'league' ) ) {
echo '<th class="data-league">' . esc_attr__( 'League', 'sportspress' ) . '</th>';
}
if ( sp_column_active( $usecolumns, 'season' ) ) {
echo '<th class="data-season">' . esc_attr__( 'Season', 'sportspress' ) . '</th>';
}
if ( sp_column_active( $usecolumns, 'venue' ) ) {
echo '<th class="data-venue">' . esc_attr__( 'Venue', 'sportspress' ) . '</th>';
} else {
echo '<th style="display:none;" class="data-venue">' . esc_attr__( 'Venue', 'sportspress' ) . '</th>';
}
if ( sp_column_active( $usecolumns, 'article' ) ) {
echo '<th class="data-article">' . esc_attr__( 'Article', 'sportspress' ) . '</th>';
}
if ( sp_column_active( $usecolumns, 'day' ) ) {
echo '<th class="data-day">' . esc_attr__( 'Match Day', 'sportspress' ) . '</th>';
}
do_action( 'sportspress_event_list_head_row', $usecolumns );
?>
</tr>
</thead>
<tbody>
<?php
$i = 0;
if ( is_numeric( $number ) && $number > 0 ) {
$limit = $number;
}
foreach ( $data as $event ) :
if ( isset( $limit ) && $i >= $limit ) {
continue;
}
$teams = get_post_meta( $event->ID, 'sp_team' );
$video = get_post_meta( $event->ID, 'sp_video', true );
$status = get_post_meta( $event->ID, 'sp_status', true );
$main_results = apply_filters( 'sportspress_event_list_main_results', sp_get_main_results( $event ), $event->ID );
$reverse_results = $is_awayhome_format;
$reverse_teams = false;
if ( 'homeaway' === $title_format ) {
$reverse_teams = $is_awayhome_format;
} elseif ( 'teams' === $title_format ) {
$reverse_teams = get_option( 'sportspress_event_reverse_teams', 'no' ) === 'yes' ? true : false;
}
if ( $reverse_results ) {
$main_results = array_reverse( $main_results, true );
}
$teams_output = '';
$team_class = '';
$teams_array = array();
$team_logos = array();
if ( $teams ) :
foreach ( $teams as $t => $team ) :
$name = sp_team_short_name( $team );
if ( $name ) :
$name = '<meta itemprop="name" content="' . $name . '">' . $name;
if ( $show_team_logo ) :
if ( has_post_thumbnail( $team ) ) :
$logo = '<span class="team-logo">' . sp_get_logo( $team, 'mini', array( 'itemprop' => 'url' ) ) . '</span>';
$team_logos[] = $logo;
$team_class .= ' has-logo';
if ( $t ) :
$name = $logo . ' ' . $name;
else :
$name .= ' ' . $logo;
endif;
endif;
endif;
if ( $link_teams ) :
$team_output = '<a href="' . get_post_permalink( $team ) . '" itemprop="url">' . $name . '</a>';
else :
$team_output = $name;
endif;
$team_result = sp_array_value( $main_results, $team, null );
if ( $team_result != null ) :
if ( $usecolumns != null && ! in_array( 'time', $usecolumns ) ) :
$team_output .= ' (' . $team_result . ')';
endif;
endif;
$teams_array[] = $team_output;
$teams_output .= $team_output . '<br>';
endif;
endforeach;
else :
$teams_output .= '&mdash;';
endif;
echo '<tr class="sp-row sp-post' . ( $i % 2 == 0 ? ' alternate' : '' ) . ' sp-row-no-' . esc_attr( $i ) . '" itemscope itemtype="http://schema.org/SportsEvent">';
$date_html = '<date>' . get_post_time( 'Y-m-d H:i:s', false, $event ) . '</date>' . apply_filters( 'sportspress_event_date', get_post_time( get_option( 'date_format' ), false, $event, true ), $event->ID );
if ( $link_events ) {
$date_html = '<a href="' . get_post_permalink( $event->ID, false, true ) . '" itemprop="url">' . $date_html . '</a>';
}
echo '<td class="data-date" itemprop="startDate" content="' . esc_attr( mysql2date( 'Y-m-d\TH:i:sP', $event->post_date ) ) . '" data-label="' . esc_attr__( 'Date', 'sportspress' ) . '">' . wp_kses( $date_html, array( 'a' => array( 'href' => array(), 'itemprop' => array() ), 'date' => array() ) ) . '</td>';
// Check if the reverse_teams option is selected and alter the teams order
if ( $reverse_teams ) {
$teams_array = array_reverse( $teams_array, true );
}
switch ( $title_format ) {
case 'homeaway':
if ( sp_column_active( $usecolumns, 'event' ) ) {
$team = array_shift( $teams_array );
echo '<td class="data-home' . esc_attr( $team_class ) . '" itemprop="competitor" itemscope itemtype="http://schema.org/SportsTeam" data-label="' . esc_attr( $home_label ) . '">' . wp_kses_post( $team ) . '</td>';
}
if ( 'combined' == $time_format && sp_column_active( $usecolumns, 'time' ) ) {
echo '<td class="data-time ' . esc_attr( $status ) . '" data-label="' . esc_attr__( 'Time/Results', 'sportspress' ) . '">';
if ( $link_events ) {
echo '<a href="' . esc_url( get_post_permalink( $event->ID, false, true ) ) . '" itemprop="url">';
}
if ( ! empty( $main_results ) ) :
echo wp_kses_post( implode( ' - ', $main_results ) );
else :
echo '<date>&nbsp;' . wp_kses_post( get_post_time( 'H:i:s', false, $event ) ) . '</date>' . wp_kses_post( apply_filters( 'sportspress_event_time', sp_get_time( $event ), $event->ID ) );
endif;
if ( $link_events ) {
echo '</a>';
}
echo '</td>';
} elseif ( in_array( $time_format, array( 'separate', 'results' ) ) && sp_column_active( $usecolumns, 'results' ) ) {
echo '<td class="data-results" data-label="' . esc_attr__( 'Results', 'sportspress' ) . '">';
if ( $link_events ) {
echo '<a href="' . esc_url( get_post_permalink( $event->ID, false, true ) ) . '" itemprop="url">';
}
if ( ! empty( $main_results ) ) :
echo wp_kses_post( implode( ' - ', $main_results ) );
else :
echo '-';
endif;
if ( $link_events ) {
echo '</a>';
}
echo '</td>';
}
if ( sp_column_active( $usecolumns, 'event' ) ) {
$team = array_shift( $teams_array );
echo '<td class="data-away' . esc_attr( $team_class ) . '" itemprop="competitor" itemscope itemtype="http://schema.org/SportsTeam" data-label="' . esc_attr( $away_label ) . '">' . wp_kses_post( $team ) . '</td>';
}
if ( in_array( $time_format, array( 'separate', 'time' ) ) && sp_column_active( $usecolumns, 'time' ) ) {
echo '<td class="data-time ' . esc_attr( $status ) . '" data-label="' . esc_attr__( 'Time', 'sportspress' ) . '">';
if ( $link_events ) {
echo '<a href="' . esc_url( get_post_permalink( $event->ID, false, true ) ) . '" itemprop="url">';
}
echo '<date>&nbsp;' . wp_kses_post( get_post_time( 'H:i:s', false, $event ) ) . '</date>' . wp_kses_post( apply_filters( 'sportspress_event_time', sp_get_time( $event ), $event->ID ) );
if ( $link_events ) {
echo '</a>';
}
echo '</td>';
}
break;
default:
if ( sp_column_active( $usecolumns, 'event' ) ) {
if ( $title_format == 'teams' ) {
echo '<td class="data-event data-teams" data-label="' . esc_attr__( 'Teams', 'sportspress' ) . '">' . wp_kses_post( $teams_output ) . '</td>';
} else {
$title_html = implode( ' ', $team_logos ) . ' ' . $event->post_title;
if ( $link_events ) {
$title_html = '<a href="' . get_post_permalink( $event->ID, false, true ) . '" itemprop="url name">' . $title_html . '</a>';
}
echo '<td class="data-event" data-label="' . esc_attr__( 'Event', 'sportspress' ) . '">' . wp_kses_post( $title_html ) . '</td>';
}
}
switch ( $time_format ) {
case 'separate':
if ( sp_column_active( $usecolumns, 'time' ) ) {
echo '<td class="data-time ' . esc_attr( $status ) . '" data-label="' . esc_attr__( 'Time', 'sportspress' ) . '">';
if ( $link_events ) {
echo '<a href="' . esc_url( get_post_permalink( $event->ID, false, true ) ) . '" itemprop="url">';
}
echo '<date>&nbsp;' . wp_kses_post( get_post_time( 'H:i:s', false, $event ) ) . '</date>' . wp_kses_post( apply_filters( 'sportspress_event_time', sp_get_time( $event ), $event->ID ) );
if ( $link_events ) {
echo '</a>';
}
echo '</td>';
}
if ( sp_column_active( $usecolumns, 'results' ) ) {
echo '<td class="data-results" data-label="' . esc_attr__( 'Results', 'sportspress' ) . '">';
if ( $link_events ) {
echo '<a href="' . esc_url( get_post_permalink( $event->ID, false, true ) ) . '" itemprop="url">';
}
if ( ! empty( $main_results ) ) :
echo wp_kses_post( implode( ' - ', $main_results ) );
else :
echo '-';
endif;
if ( $link_events ) {
echo '</a>';
}
echo '</td>';
}
break;
case 'time':
if ( sp_column_active( $usecolumns, 'time' ) ) {
echo '<td class="data-time ' . esc_attr( $status ) . '" data-label="' . esc_attr__( 'Time', 'sportspress' ) . '">';
if ( $link_events ) {
echo '<a href="' . esc_url( get_post_permalink( $event->ID, false, true ) ) . '" itemprop="url">';
}
echo '<date>&nbsp;' . wp_kses_post( get_post_time( 'H:i:s', false, $event ) ) . '</date>' . wp_kses_post( apply_filters( 'sportspress_event_time', sp_get_time( $event ), $event->ID ) );
if ( $link_events ) {
echo '</a>';
}
echo '</td>';
}
break;
case 'results':
if ( sp_column_active( $usecolumns, 'results' ) ) {
echo '<td class="data-results" data-label="' . esc_attr__( 'Results', 'sportspress' ) . '">';
if ( $link_events ) {
echo '<a href="' . esc_url( get_post_permalink( $event->ID, false, true ) ) . '" itemprop="url">';
}
if ( ! empty( $main_results ) ) :
echo wp_kses_post( implode( ' - ', $main_results ) );
else :
echo '-';
endif;
if ( $link_events ) {
echo '</a>';
}
echo '</td>';
}
break;
default:
if ( sp_column_active( $usecolumns, 'time' ) ) {
echo '<td class="data-time ' . esc_attr( $status ) . '" data-label="' . esc_attr__( 'Time/Results', 'sportspress' ) . '">';
if ( $link_events ) {
echo '<a href="' . esc_url( get_post_permalink( $event->ID, false, true ) ) . '" itemprop="url">';
}
if ( ! empty( $main_results ) ) :
echo wp_kses_post( implode( ' - ', $main_results ) );
else :
echo '<date>&nbsp;' . wp_kses_post( get_post_time( 'H:i:s', false, $event ) ) . '</date>' . wp_kses_post( apply_filters( 'sportspress_event_time', sp_get_time( $event ), $event->ID ) );
endif;
if ( $link_events ) {
echo '</a>';
}
echo '</td>';
}
}
}
if ( sp_column_active( $usecolumns, 'league' ) ) :
echo '<td class="data-league" data-label="' . esc_attr__( 'League', 'sportspress' ) . '">';
$leagues = get_the_terms( $event->ID, 'sp_league' );
if ( $leagues ) :
echo wp_kses_post( implode( ', ', wp_list_pluck( $leagues, 'name' ) ) );
endif;
echo '</td>';
endif;
if ( sp_column_active( $usecolumns, 'season' ) ) :
echo '<td class="data-season" data-label="' . esc_attr__( 'Season', 'sportspress' ) . '">';
$seasons = get_the_terms( $event->ID, 'sp_season' );
if ( $seasons ) :
echo wp_kses_post( implode( ', ', wp_list_pluck( $seasons, 'name' ) ) );
endif;
echo '</td>';
endif;
if ( sp_column_active( $usecolumns, 'venue' ) ) :
echo '<td class="data-venue" data-label="' . esc_attr__( 'Venue', 'sportspress' ) . '" itemprop="location" itemscope itemtype="http://schema.org/Place">';
echo '<div itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">';
if ( $link_venues ) :
the_terms( $event->ID, 'sp_venue' );
else :
$venues = get_the_terms( $event->ID, 'sp_venue' );
if ( $venues ) :
echo wp_kses_post( implode( ', ', wp_list_pluck( $venues, 'name' ) ) );
endif;
endif;
echo '</div>';
echo '</td>';
else :
echo '<td style="display:none;" class="data-venue" data-label="' . esc_attr__( 'Venue', 'sportspress' ) . '" itemprop="location" itemscope itemtype="http://schema.org/Place">';
echo '<div itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">';
esc_attr_e( 'N/A', 'sportspress' );
echo '</div>';
echo '</td>';
endif;
if ( sp_column_active( $usecolumns, 'article' ) ) :
echo '<td class="data-article" data-label="' . esc_attr__( 'Article', 'sportspress' ) . '">';
if ( $link_events ) {
echo '<a href="' . esc_url( get_post_permalink( $event->ID, false, true ) ) . '" itemprop="url">';
}
if ( $video ) :
echo '<div class="dashicons dashicons-video-alt"></div>';
elseif ( has_post_thumbnail( $event->ID ) ) :
echo '<div class="dashicons dashicons-camera"></div>';
endif;
if ( $event->post_content !== null ) :
if ( $event->post_status == 'publish' ) :
esc_attr_e( 'Recap', 'sportspress' );
else :
esc_attr_e( 'Preview', 'sportspress' );
endif;
endif;
if ( $link_events ) {
echo '</a>';
}
echo '</td>';
endif;
if ( sp_column_active( $usecolumns, 'day' ) ) :
echo '<td class="data-day" data-label="' . esc_attr__( 'Match Day', 'sportspress' ) . '">';
$day = get_post_meta( $event->ID, 'sp_day', true );
if ( '' == $day ) {
echo '-';
} else {
echo wp_kses_post( $day );
}
echo '</td>';
endif;
do_action( 'sportspress_event_list_row', $event, $usecolumns );
echo '</tr>';
$i++;
endforeach;
?>
</tbody>
</table>
</div>
<?php
// If responsive tables are enabled then load the inline css code
if ( $responsive ) {
// sportspress_responsive_tables_css( $identifier );
}
if ( $id && $show_all_events_link ) {
echo '<div class="sp-calendar-link sp-view-all-link"><a href="' . esc_url( get_permalink( $id ) ) . '">' . esc_attr__( 'View all events', 'sportspress' ) . '</a></div>';
}
?>
</div>

166
templates/event-results.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
/**
* Event Results
*
* @author ThemeBoy
* @package SportsPress/Templates
* @version 2.7.1
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
if ( get_option( 'sportspress_event_show_results', 'yes' ) === 'no' ) {
return;
}
if ( ! isset( $id ) ) {
$id = get_the_ID();
}
$event = new SP_Event( $id );
$status = $event->status();
if ( 'results' != $status ) {
return;
}
if ( ! isset( $caption ) ) {
$caption = esc_attr__( 'Results', 'sportspress' );
}
// Get event result data
$data = $event->results();
// The first row should be column labels
$labels = $data[0];
// Remove the first row to leave us with the actual data
unset( $data[0] );
$data = array_filter( $data );
if ( empty( $data ) ) {
return false;
}
$scrollable = get_option( 'sportspress_enable_scrollable_tables', 'yes' ) == 'yes' ? true : false;
$link_teams = get_option( 'sportspress_link_teams', 'no' ) == 'yes' ? true : false;
$show_outcomes = array_key_exists( 'outcome', $labels );
// Initialize
$output = '';
$table_rows = '';
$i = 0;
// Event Results row order is controlled in SportsPress > Settings > Events > Event Results.
$row_order = get_option( 'tony_sportspress_event_results_row_order', '' );
if ( '' === $row_order ) {
// Backward compatibility with the legacy checkbox option.
$legacy_away_first = get_option( 'tony_sportspress_event_results_away_first', 'no' ) === 'yes';
$row_order = $legacy_away_first ? 'away_home' : 'home_away';
}
$teams = array_values( array_filter( array_map( 'absint', (array) get_post_meta( $id, 'sp_team', false ) ) ) );
if ( 'away_home' === $row_order ) {
$teams = array_reverse( $teams );
}
if ( ! empty( $teams ) ) {
$ordered = array();
foreach ( $teams as $index => $team_id ) {
if ( array_key_exists( $team_id, $data ) ) {
$ordered[ $team_id ] = $data[ $team_id ];
continue;
}
// SportsPress can store positional rows in team order (0,1,...) depending on context.
if ( array_key_exists( $index, $data ) ) {
$ordered[ $team_id ] = $data[ $index ];
}
}
foreach ( $data as $team_id => $result ) {
if ( ! array_key_exists( $team_id, $ordered ) ) {
$ordered[ $team_id ] = $result;
}
}
if ( ! empty( $ordered ) ) {
$data = $ordered;
}
}
foreach ( $data as $team_id => $result ) :
if ( $show_outcomes ) :
$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;
endif;
unset( $result['outcome'] );
$table_rows .= '<tr class="' . ( $i % 2 == 0 ? 'odd' : 'even' ) . '">';
$team_name = sp_team_short_name( $team_id );
if ( $link_teams && sp_post_exists( $team_id ) ) :
$team_name = '<a href="' . get_post_permalink( $team_id ) . '">' . $team_name . '</a>';
endif;
$table_rows .= '<td class="data-name">' . $team_name . '</td>';
foreach ( $labels as $key => $label ) :
if ( in_array( $key, array( 'name', 'outcome' ) ) ) {
continue;
}
if ( array_key_exists( $key, $result ) && $result[ $key ] != '' ) :
$value = $result[ $key ];
else :
$value = apply_filters( 'sportspress_event_empty_result_string', '&mdash;' );
endif;
$table_rows .= '<td class="data-' . $key . '">' . $value . '</td>';
endforeach;
if ( $show_outcomes ) :
$table_rows .= '<td class="data-outcome">' . implode( ', ', $outcomes ) . '</td>';
endif;
$table_rows .= '</tr>';
$i++;
endforeach;
if ( empty( $table_rows ) ) :
return false;
else :
$output .= '<h4 class="sp-table-caption">' . $caption . '</h4>';
$output .= '<div class="sp-table-wrapper">' .
'<table class="sp-event-results sp-data-table' . ( $scrollable ? ' sp-scrollable-table' : '' ) . '"><thead>' .
'<th class="data-name">' . esc_attr__( 'Team', 'sportspress' ) . '</th>';
foreach ( $labels as $key => $label ) :
$output .= '<th class="data-' . $key . '">' . $label . '</th>';
endforeach;
$output .= '</tr>' . '</thead>' . '<tbody>';
$output .= $table_rows;
$output .= '</tbody>' . '</table>' . '</div>';
endif;
?>
<div class="sp-template sp-template-event-results">
<?php echo wp_kses_post( $output ); ?>
</div>

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,81 @@
<?php
/**
* Tests for custom event permalink query behavior.
*
* @package Tonys_Sportspress_Enhancements
*/
/**
* Event permalink query tests.
*/
class Test_SP_Event_Permalink extends WP_UnitTestCase {
/**
* Preserve global main query references for each test.
*
* @var WP_Query
*/
private $original_wp_query;
/**
* Preserve global main query references for each test.
*
* @var WP_Query
*/
private $original_wp_the_query;
/**
* Set up test case state.
*/
public function set_up(): void {
parent::set_up();
$this->original_wp_query = $GLOBALS['wp_query'];
$this->original_wp_the_query = $GLOBALS['wp_the_query'];
set_current_screen( 'front' );
}
/**
* Restore global query references.
*/
public function tear_down(): void {
$GLOBALS['wp_query'] = $this->original_wp_query;
$GLOBALS['wp_the_query'] = $this->original_wp_the_query;
set_current_screen( 'front' );
parent::tear_down();
}
/**
* The admin event list query should not be altered by permalink handling.
*/
public function test_admin_event_queries_are_not_modified() {
set_current_screen( 'edit-sp_event' );
$query = new WP_Query();
$query->set( 'post_type', 'sp_event' );
$query->set( 'p', 123 );
$query->set( 'post_status', 'future' );
custom_event_parse_request( $query );
$this->assertSame( 'future', $query->get( 'post_status' ) );
$this->assertSame( 123, $query->get( 'p' ) );
}
/**
* Front-end single event requests should include future posts.
*/
public function test_frontend_single_event_queries_include_future_posts() {
$query = new WP_Query();
$query->set( 'post_type', 'sp_event' );
$query->set( 'p', 456 );
$GLOBALS['wp_query'] = $query;
$GLOBALS['wp_the_query'] = $query;
custom_event_parse_request( $query );
$this->assertSame( 'sp_event', $query->get( 'post_type' ) );
$this->assertSame( 456, $query->get( 'p' ) );
$this->assertSame( array( 'publish', 'future' ), $query->get( 'post_status' ) );
}
}

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

@@ -7,13 +7,47 @@
* Author URI: https://github.com/anthonyscorrea/
* Text Domain: tonys-sportspress-enhancements
* Domain Path: /languages
* Version: 0.1.2
* Update URI: https://github.com/anthonyscorrea/tonys-sportspress-enhancements
* Version: 0.1.15
*
* @package Tonys_Sportspress_Enhancements
*/
if ( ! defined( 'TONY_SPORTSPRESS_ENHANCEMENTS_VERSION' ) ) {
define( 'TONY_SPORTSPRESS_ENHANCEMENTS_VERSION', '0.1.15' );
}
if ( ! defined( 'TONY_SPORTSPRESS_ENHANCEMENTS_FILE' ) ) {
define( 'TONY_SPORTSPRESS_ENHANCEMENTS_FILE', __FILE__ );
}
if ( ! defined( 'TONY_SPORTSPRESS_ENHANCEMENTS_DIR' ) ) {
define( 'TONY_SPORTSPRESS_ENHANCEMENTS_DIR', plugin_dir_path( __FILE__ ) );
}
if ( ! defined( 'TONY_SPORTSPRESS_ENHANCEMENTS_URL' ) ) {
define( 'TONY_SPORTSPRESS_ENHANCEMENTS_URL', plugin_dir_url( __FILE__ ) );
}
if ( ! defined( 'TONY_SPORTSPRESS_ENHANCEMENTS_PLUGIN_BASENAME' ) ) {
define( 'TONY_SPORTSPRESS_ENHANCEMENTS_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
}
// Include other files here
require_once plugin_dir_path(__FILE__) . 'includes/sp-github-updater.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-officials-manager-role.php';
require_once plugin_dir_path(__FILE__) . 'includes/open-graph-tags.php';
require_once plugin_dir_path(__FILE__) . 'includes/featured-image-generator.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-event-permalink.php';require_once plugin_dir_path(__FILE__) . 'includes/sp-event-admin-week-filter.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-event-permalink.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-event-export.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-event-csv.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-event-admin-week-filter.php';
require_once plugin_dir_path(__FILE__) . 'includes/sp-event-quick-edit-officials.php';
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';
register_activation_hook( __FILE__, 'tony_sportspress_sync_officials_manager_roles' );