feat: initial peptide-repo-core plugin (v0.1.0) — canonical schema sprint
Implements ADR-0002: canonical peptide schema as shared data layer for the peptiderepo.com ecosystem. All Tier 1 tools (Reconstitution Calculator, Dosage Reference, Legal Status Tracker, PSA enhancements) build on this. Includes: - pr_peptide CPT with REST, taxonomies, 11 meta fields - 3 custom tables: pr_dosing_rows, pr_legal_cells, pr_ai_candidate_queue - Sequential migration runner with schema versioning - 4 typed DTOs and 4 repository classes (full CRUD) - REST API (pr-core/v1): 6 endpoints (read public, write auth-gated) - Admin UI: 3 meta boxes, custom columns, AI candidate queue page - Shared disclaimer component (shortcode + static API) - JSON-LD Drug schema emission on single peptide pages - Seed data fixture (3 peptides, 10 dosing rows, 5 legal cells) - Full teardown in uninstall.php - CI: PHP lint 8.0-8.2, PHPCS WordPress, 300-line check All 22 PHP files under 300 lines. Highest: peptide-cpt at 265. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
63
includes/admin/class-pr-core-admin-columns.php
Normal file
63
includes/admin/class-pr-core-admin-columns.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Custom admin list-table columns for the pr_peptide CPT.
|
||||
*
|
||||
* What: Adds evidence strength, editorial status, and dosing count columns.
|
||||
* Who calls it: PR_Core_Admin via manage_posts_columns and custom_column hooks.
|
||||
* Dependencies: PR_Core_Dosing_Repository for row counts.
|
||||
*
|
||||
* @see admin/class-pr-core-admin.php — Registers column hooks.
|
||||
*/
|
||||
class PR_Core_Admin_Columns {
|
||||
|
||||
/**
|
||||
* Add custom columns to the peptide list table.
|
||||
*
|
||||
* @param array<string, string> $columns Existing columns.
|
||||
* @return array<string, string> Modified columns.
|
||||
*/
|
||||
public function add_columns( array $columns ): array {
|
||||
$new = [];
|
||||
foreach ( $columns as $key => $label ) {
|
||||
$new[ $key ] = $label;
|
||||
|
||||
// Insert custom columns after the title column.
|
||||
if ( 'title' === $key ) {
|
||||
$new['pr_evidence'] = __( 'Evidence', 'peptide-repo-core' );
|
||||
$new['pr_editorial'] = __( 'Editorial', 'peptide-repo-core' );
|
||||
$new['pr_dosing'] = __( 'Dosing Rows', 'peptide-repo-core' );
|
||||
}
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom column content for a peptide row.
|
||||
*
|
||||
* @param string $column Column key.
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function render_column( string $column, int $post_id ): void {
|
||||
switch ( $column ) {
|
||||
case 'pr_evidence':
|
||||
$strength = get_post_meta( $post_id, 'evidence_strength', true ) ?: 'preclinical';
|
||||
$label = apply_filters( 'pr_core_evidence_strength_label', $strength, $strength );
|
||||
echo esc_html( $label );
|
||||
break;
|
||||
|
||||
case 'pr_editorial':
|
||||
$status = get_post_meta( $post_id, 'editorial_review_status', true ) ?: 'draft';
|
||||
echo esc_html( ucfirst( str_replace( '-', ' ', $status ) ) );
|
||||
break;
|
||||
|
||||
case 'pr_dosing':
|
||||
$count = ( new PR_Core_Dosing_Repository() )->count_by_peptide( $post_id );
|
||||
echo esc_html( (string) $count );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
includes/admin/class-pr-core-admin.php
Normal file
77
includes/admin/class-pr-core-admin.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Admin initialization and hook registration.
|
||||
*
|
||||
* What: Registers admin menu pages, meta boxes, and custom columns for peptides.
|
||||
* Who calls it: PR_Core::init() when is_admin().
|
||||
* Dependencies: PR_Core_Peptide_Metaboxes, PR_Core_Candidate_Queue_Page, PR_Core_Admin_Columns.
|
||||
*
|
||||
* @see admin/class-pr-core-peptide-metaboxes.php — Dosing + legal meta boxes.
|
||||
* @see admin/class-pr-core-candidate-queue-page.php — Candidate review screen.
|
||||
* @see admin/class-pr-core-admin-columns.php — Custom list-table columns.
|
||||
*/
|
||||
class PR_Core_Admin {
|
||||
|
||||
/**
|
||||
* Register all admin hooks.
|
||||
*
|
||||
* Side effects: registers add_meta_boxes, save_post, admin_menu hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks(): void {
|
||||
$metaboxes = new PR_Core_Peptide_Metaboxes();
|
||||
add_action( 'add_meta_boxes', [ $metaboxes, 'add_meta_boxes' ] );
|
||||
add_action( 'save_post_' . PR_Core_Peptide_CPT::POST_TYPE, [ $metaboxes, 'save_meta' ], 10, 2 );
|
||||
|
||||
$columns = new PR_Core_Admin_Columns();
|
||||
add_filter( 'manage_' . PR_Core_Peptide_CPT::POST_TYPE . '_posts_columns', [ $columns, 'add_columns' ] );
|
||||
add_action( 'manage_' . PR_Core_Peptide_CPT::POST_TYPE . '_posts_custom_column', [ $columns, 'render_column' ], 10, 2 );
|
||||
|
||||
add_action( 'admin_menu', [ $this, 'add_admin_pages' ] );
|
||||
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_styles' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the Candidate Queue submenu page under the Peptides menu.
|
||||
*
|
||||
* Side effects: registers admin menu page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_admin_pages(): void {
|
||||
$pending_count = ( new PR_Core_Candidate_Queue_Repository() )->count_by_status( 'pending' );
|
||||
$badge = $pending_count > 0 ? sprintf( ' <span class="awaiting-mod">%d</span>', $pending_count ) : '';
|
||||
|
||||
add_submenu_page(
|
||||
'edit.php?post_type=' . PR_Core_Peptide_CPT::POST_TYPE,
|
||||
__( 'AI Candidate Queue', 'peptide-repo-core' ),
|
||||
__( 'Candidate Queue', 'peptide-repo-core' ) . $badge,
|
||||
PR_Core_Peptide_CPT::CAPABILITY,
|
||||
'pr-core-candidates',
|
||||
[ new PR_Core_Candidate_Queue_Page(), 'render' ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue minimal admin CSS for meta boxes.
|
||||
*
|
||||
* @param string $hook_suffix Current admin page hook.
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_admin_styles( string $hook_suffix ): void {
|
||||
$screen = get_current_screen();
|
||||
if ( ! $screen || PR_Core_Peptide_CPT::POST_TYPE !== $screen->post_type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'pr-core-admin',
|
||||
PR_CORE_PLUGIN_URL . 'assets/css/admin.css',
|
||||
[],
|
||||
PR_CORE_VERSION
|
||||
);
|
||||
}
|
||||
}
|
||||
142
includes/admin/class-pr-core-candidate-queue-page.php
Normal file
142
includes/admin/class-pr-core-candidate-queue-page.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Admin page for the AI Candidate Queue.
|
||||
*
|
||||
* What: Renders a review screen for AI-extracted dosing candidates.
|
||||
* Who calls it: PR_Core_Admin::add_admin_pages() registers this as a submenu page.
|
||||
* Dependencies: PR_Core_Candidate_Queue_Repository, PR_Core_Peptide_Repository.
|
||||
*
|
||||
* Researchers see pending candidates sorted by confidence, can approve/reject with notes.
|
||||
*
|
||||
* @see admin/class-pr-core-admin.php — Menu registration.
|
||||
* @see repositories/class-pr-core-candidate-queue-repository.php — Data layer.
|
||||
*/
|
||||
class PR_Core_Candidate_Queue_Page {
|
||||
|
||||
/**
|
||||
* Render the candidate queue admin page.
|
||||
*
|
||||
* Side effects: processes approve/reject actions, outputs HTML.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render(): void {
|
||||
if ( ! current_user_can( PR_Core_Peptide_CPT::CAPABILITY ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to access this page.', 'peptide-repo-core' ) );
|
||||
}
|
||||
|
||||
$this->handle_actions();
|
||||
|
||||
$repo = new PR_Core_Candidate_Queue_Repository();
|
||||
$candidates = $repo->find_by_status( 'pending' );
|
||||
$pep_repo = new PR_Core_Peptide_Repository();
|
||||
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html__( 'AI Candidate Queue', 'peptide-repo-core' ) . '</h1>';
|
||||
echo '<p>' . esc_html__( 'Review AI-extracted dosing data candidates. Approved rows are copied to the dosing table.', 'peptide-repo-core' ) . '</p>';
|
||||
|
||||
if ( empty( $candidates ) ) {
|
||||
echo '<p><strong>' . esc_html__( 'No pending candidates.', 'peptide-repo-core' ) . '</strong></p>';
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<table class="widefat striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th>' . esc_html__( 'Peptide', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Route', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Dose Range', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Population', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Study', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Confidence', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Actions', 'peptide-repo-core' ) . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
foreach ( $candidates as $c ) {
|
||||
$peptide = $pep_repo->find_by_id( $c->peptide_id );
|
||||
$name = $peptide ? $peptide->title : '#' . $c->peptide_id;
|
||||
|
||||
$dose = $c->dose_min !== null ? number_format( $c->dose_min, 2 ) : '?';
|
||||
if ( $c->dose_max !== null && $c->dose_max !== $c->dose_min ) {
|
||||
$dose .= ' – ' . number_format( $c->dose_max, 2 );
|
||||
}
|
||||
$dose .= ' ' . esc_html( $c->dose_unit );
|
||||
|
||||
$study = $c->study_title ? esc_html( mb_substr( $c->study_title, 0, 60 ) ) : '—';
|
||||
if ( $c->citation_pubmed_id ) {
|
||||
$study .= sprintf( ' <a href="https://pubmed.ncbi.nlm.nih.gov/%s/" target="_blank">PubMed</a>', esc_attr( $c->citation_pubmed_id ) );
|
||||
}
|
||||
|
||||
$confidence_pct = number_format( $c->extraction_confidence * 100, 0 ) . '%';
|
||||
$approve_url = wp_nonce_url(
|
||||
add_query_arg( [ 'action' => 'pr_core_approve', 'candidate_id' => $c->id ] ),
|
||||
'pr_core_queue_action'
|
||||
);
|
||||
$reject_url = wp_nonce_url(
|
||||
add_query_arg( [ 'action' => 'pr_core_reject', 'candidate_id' => $c->id ] ),
|
||||
'pr_core_queue_action'
|
||||
);
|
||||
|
||||
echo '<tr>';
|
||||
printf( '<td>%s</td>', esc_html( $name ) );
|
||||
printf( '<td>%s</td>', esc_html( $c->route ) );
|
||||
printf( '<td>%s</td>', esc_html( $dose ) );
|
||||
printf( '<td>%s</td>', esc_html( $c->population ) );
|
||||
printf( '<td>%s</td>', $study ); // Contains escaped HTML + link.
|
||||
printf( '<td>%s</td>', esc_html( $confidence_pct ) );
|
||||
printf(
|
||||
'<td><a href="%s" class="button button-primary button-small">%s</a> <a href="%s" class="button button-small">%s</a></td>',
|
||||
esc_url( $approve_url ),
|
||||
esc_html__( 'Approve', 'peptide-repo-core' ),
|
||||
esc_url( $reject_url ),
|
||||
esc_html__( 'Reject', 'peptide-repo-core' )
|
||||
);
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
echo '</tbody></table></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Process approve/reject actions from query parameters.
|
||||
*
|
||||
* Side effects: database writes, admin notices.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_actions(): void {
|
||||
$action = sanitize_text_field( $_GET['action'] ?? '' );
|
||||
|
||||
if ( ! in_array( $action, [ 'pr_core_approve', 'pr_core_reject' ], true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! check_admin_referer( 'pr_core_queue_action' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$candidate_id = absint( $_GET['candidate_id'] ?? 0 );
|
||||
if ( 0 === $candidate_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$repo = new PR_Core_Candidate_Queue_Repository();
|
||||
$reviewer_id = get_current_user_id();
|
||||
|
||||
if ( 'pr_core_approve' === $action ) {
|
||||
$dosing_id = $repo->approve( $candidate_id, $reviewer_id );
|
||||
if ( $dosing_id > 0 ) {
|
||||
add_action( 'admin_notices', static function () {
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Candidate approved and added to dosing data.', 'peptide-repo-core' ) . '</p></div>';
|
||||
} );
|
||||
}
|
||||
} else {
|
||||
$repo->reject( $candidate_id, $reviewer_id );
|
||||
add_action( 'admin_notices', static function () {
|
||||
echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html__( 'Candidate rejected.', 'peptide-repo-core' ) . '</p></div>';
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
260
includes/admin/class-pr-core-peptide-metaboxes.php
Normal file
260
includes/admin/class-pr-core-peptide-metaboxes.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Meta boxes for the peptide edit screen: scientific identifiers, dosing, and legal.
|
||||
*
|
||||
* What: Renders and saves three meta boxes on the pr_peptide edit screen.
|
||||
* Who calls it: PR_Core_Admin via add_meta_boxes and save_post hooks.
|
||||
* Dependencies: PR_Core_Dosing_Repository, PR_Core_Legal_Repository.
|
||||
*
|
||||
* @see admin/class-pr-core-admin.php — Registers these hooks.
|
||||
* @see cpt/class-pr-core-peptide-cpt.php — Meta field definitions.
|
||||
*/
|
||||
class PR_Core_Peptide_Metaboxes {
|
||||
|
||||
/**
|
||||
* Register meta boxes on the peptide edit screen.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'pr-core-identifiers',
|
||||
__( 'Scientific Identifiers', 'peptide-repo-core' ),
|
||||
[ $this, 'render_identifiers_box' ],
|
||||
PR_Core_Peptide_CPT::POST_TYPE,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'pr-core-dosing',
|
||||
__( 'Dosing Data', 'peptide-repo-core' ),
|
||||
[ $this, 'render_dosing_box' ],
|
||||
PR_Core_Peptide_CPT::POST_TYPE,
|
||||
'normal',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'pr-core-legal',
|
||||
__( 'Legal Status by Country', 'peptide-repo-core' ),
|
||||
[ $this, 'render_legal_box' ],
|
||||
PR_Core_Peptide_CPT::POST_TYPE,
|
||||
'normal',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Scientific Identifiers meta box.
|
||||
*
|
||||
* @param \WP_Post $post Current post.
|
||||
* @return void
|
||||
*/
|
||||
public function render_identifiers_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'pr_core_save_meta', 'pr_core_meta_nonce' );
|
||||
|
||||
$fields = [
|
||||
'display_name' => __( 'Display Name', 'peptide-repo-core' ),
|
||||
'aliases' => __( 'Aliases (JSON array)', 'peptide-repo-core' ),
|
||||
'molecular_formula' => __( 'Molecular Formula', 'peptide-repo-core' ),
|
||||
'molecular_weight' => __( 'Molecular Weight (Da)', 'peptide-repo-core' ),
|
||||
'cas_number' => __( 'CAS Number', 'peptide-repo-core' ),
|
||||
'drugbank_id' => __( 'DrugBank ID', 'peptide-repo-core' ),
|
||||
'chembl_id' => __( 'ChEMBL ID', 'peptide-repo-core' ),
|
||||
];
|
||||
|
||||
echo '<table class="form-table pr-core-meta-table">';
|
||||
foreach ( $fields as $key => $label ) {
|
||||
$value = esc_attr( (string) get_post_meta( $post->ID, $key, true ) );
|
||||
printf(
|
||||
'<tr><th><label for="pr_core_%1$s">%2$s</label></th><td><input type="text" id="pr_core_%1$s" name="pr_core_%1$s" value="%3$s" class="regular-text" /></td></tr>',
|
||||
esc_attr( $key ),
|
||||
esc_html( $label ),
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
// Evidence strength dropdown.
|
||||
$current_strength = get_post_meta( $post->ID, 'evidence_strength', true ) ?: 'preclinical';
|
||||
echo '<tr><th><label for="pr_core_evidence_strength">' . esc_html__( 'Evidence Strength', 'peptide-repo-core' ) . '</label></th><td><select id="pr_core_evidence_strength" name="pr_core_evidence_strength">';
|
||||
foreach ( PR_Core_Peptide_CPT::EVIDENCE_STRENGTHS as $strength ) {
|
||||
$label = apply_filters( 'pr_core_evidence_strength_label', $strength, $strength );
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr( $strength ),
|
||||
selected( $current_strength, $strength, false ),
|
||||
esc_html( $label )
|
||||
);
|
||||
}
|
||||
echo '</select></td></tr>';
|
||||
|
||||
// Editorial review status.
|
||||
$current_review = get_post_meta( $post->ID, 'editorial_review_status', true ) ?: 'draft';
|
||||
echo '<tr><th><label for="pr_core_editorial_review_status">' . esc_html__( 'Editorial Status', 'peptide-repo-core' ) . '</label></th><td><select id="pr_core_editorial_review_status" name="pr_core_editorial_review_status">';
|
||||
foreach ( PR_Core_Peptide_CPT::REVIEW_STATUSES as $status ) {
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr( $status ),
|
||||
selected( $current_review, $status, false ),
|
||||
esc_html( ucfirst( str_replace( '-', ' ', $status ) ) )
|
||||
);
|
||||
}
|
||||
echo '</select></td></tr>';
|
||||
|
||||
echo '</table>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Dosing Data meta box (read-only summary with link to manage).
|
||||
*
|
||||
* @param \WP_Post $post Current post.
|
||||
* @return void
|
||||
*/
|
||||
public function render_dosing_box( \WP_Post $post ): void {
|
||||
if ( 'auto-draft' === $post->post_status ) {
|
||||
echo '<p>' . esc_html__( 'Save the peptide first, then add dosing data.', 'peptide-repo-core' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$repo = new PR_Core_Dosing_Repository();
|
||||
$rows = $repo->find_by_peptide( $post->ID );
|
||||
|
||||
if ( empty( $rows ) ) {
|
||||
echo '<p>' . esc_html__( 'No dosing rows yet.', 'peptide-repo-core' ) . '</p>';
|
||||
} else {
|
||||
echo '<table class="widefat striped"><thead><tr>';
|
||||
echo '<th>' . esc_html__( 'Route', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Dose Range', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Population', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Evidence', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Source', 'peptide-repo-core' ) . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
$dose = $row->dose_min !== null ? number_format( $row->dose_min, 2 ) : '?';
|
||||
if ( $row->dose_max !== null && $row->dose_max !== $row->dose_min ) {
|
||||
$dose .= ' – ' . number_format( $row->dose_max, 2 );
|
||||
}
|
||||
$dose .= ' ' . esc_html( $row->dose_unit );
|
||||
|
||||
$evidence_label = apply_filters( 'pr_core_evidence_strength_label', $row->evidence_strength, $row->evidence_strength );
|
||||
|
||||
printf(
|
||||
'<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>',
|
||||
esc_html( $row->route ),
|
||||
esc_html( $dose ),
|
||||
esc_html( $row->population ),
|
||||
esc_html( $evidence_label ),
|
||||
esc_html( $row->source )
|
||||
);
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
printf(
|
||||
'<p class="description">%s</p>',
|
||||
esc_html__( 'Dosing rows are managed via the REST API or AI Candidate Queue. Direct admin editing coming in v0.2.', 'peptide-repo-core' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Legal Status meta box (read-only summary).
|
||||
*
|
||||
* @param \WP_Post $post Current post.
|
||||
* @return void
|
||||
*/
|
||||
public function render_legal_box( \WP_Post $post ): void {
|
||||
if ( 'auto-draft' === $post->post_status ) {
|
||||
echo '<p>' . esc_html__( 'Save the peptide first, then add legal data.', 'peptide-repo-core' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$repo = new PR_Core_Legal_Repository();
|
||||
$cells = $repo->find_by_peptide( $post->ID );
|
||||
|
||||
if ( empty( $cells ) ) {
|
||||
echo '<p>' . esc_html__( 'No legal status data yet.', 'peptide-repo-core' ) . '</p>';
|
||||
} else {
|
||||
echo '<table class="widefat striped"><thead><tr>';
|
||||
echo '<th>' . esc_html__( 'Country', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Status', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Framework', 'peptide-repo-core' ) . '</th>';
|
||||
echo '<th>' . esc_html__( 'Verified', 'peptide-repo-core' ) . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
foreach ( $cells as $cell ) {
|
||||
printf(
|
||||
'<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>',
|
||||
esc_html( $cell->country_code ),
|
||||
esc_html( ucfirst( $cell->status ) ),
|
||||
esc_html( $cell->regulatory_framework ?? '—' ),
|
||||
esc_html( $cell->last_verified_at )
|
||||
);
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
printf(
|
||||
'<p class="description">%s</p>',
|
||||
esc_html__( 'Legal cells are managed via the REST API. Direct admin editing coming in v0.2.', 'peptide-repo-core' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save peptide meta fields on post save.
|
||||
*
|
||||
* Side effects: updates post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! isset( $_POST['pr_core_meta_nonce'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pr_core_meta_nonce'] ) ), 'pr_core_save_meta' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( PR_Core_Peptide_CPT::CAPABILITY ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$meta_fields = PR_Core_Peptide_CPT::get_meta_fields();
|
||||
|
||||
foreach ( $meta_fields as $key => $config ) {
|
||||
$input_key = 'pr_core_' . $key;
|
||||
if ( ! isset( $_POST[ $input_key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$raw = wp_unslash( $_POST[ $input_key ] );
|
||||
$clean = is_callable( $config['sanitize'] )
|
||||
? call_user_func( $config['sanitize'], $raw )
|
||||
: sanitize_text_field( (string) $raw );
|
||||
|
||||
update_post_meta( $post_id, $key, $clean );
|
||||
}
|
||||
|
||||
// Set last_editorial_review_at when status changes to published.
|
||||
$new_review_status = sanitize_text_field( $_POST['pr_core_editorial_review_status'] ?? '' );
|
||||
if ( 'published' === $new_review_status ) {
|
||||
$old_status = get_post_meta( $post_id, 'editorial_review_status', true );
|
||||
if ( $old_status !== 'published' ) {
|
||||
update_post_meta( $post_id, 'last_editorial_review_at', current_time( 'mysql' ) );
|
||||
update_post_meta( $post_id, 'medical_editor_id', get_current_user_id() );
|
||||
}
|
||||
}
|
||||
|
||||
do_action( 'pr_core_after_peptide_publish', $post_id, $post );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user