About:Pharmacopedia.ext
More actions
Pharmacopedia extension — specification
Version: 0.7.10 · Requires: MediaWiki ≥ 1.45.0
Author: MDElliottMD · License: GPL-2.0-or-later
Source: /var/www/mediawiki/extensions/Pharmacopedia/
The Pharmacopedia extension turns a MediaWiki install into a structured, community-edited med reference. It adds parser tags, special pages, API modules, and a database schema that together support:
- Structured med pages via the
template{{MedTemplate}} - Per-user rating, voting, and reporting on effects, indications, titration strategies, anecdotes, and interactions
- Two-perspective data capture (personal vs. provider) wherever clinically meaningful
- Curated med-class categories used as interaction endpoints
- A verified-provider role with document-based verification
High-level architecture
- Backend (PHP):
includes/— one class per parser tag, store, special page, or API module. Auto-loaded underMediaWiki\Extension\Pharmacopedia\. - Frontend (JS):
resources/ext.pharmacopedia.js— single IIFE binding click handlers, modals, and inline AJAX submits. - Styles (CSS):
resources/ext.pharmacopedia.css— shared row layout, per-tag chrome, dark-theme-friendly colors. - Schema:
sql/— ten core tables plus four migration patches. Picked up via theLoadExtensionSchemaUpdateshook.
Parser tags
All eight tags are registered via Hooks::onParserFirstCallInit:
| Tag | Purpose | Class |
|---|---|---|
<vote> |
Generic up/down binary vote on an arbitrary slug | VoteTag
|
<effect> |
Therapeutic or adverse effect, dual patient/provider perspectives | EffectTag
|
<discuss> |
Threaded comment widget | CommentTag
|
<effectsummary> |
Roll-up aggregate header | EffectSummaryTag
|
<titration> |
Titration strategy card with up/down vote | TitrationTag
|
<anecdote> |
Personal or provider story with up/down vote | AnecdoteTag
|
<indication> |
Condition the med is used for, 0–5 likert rating | IndicationTag
|
<pharmaInteractions/> |
Self-closing; renders the Interactions section for the current page | InteractionTag
|
All tags except <pharmaInteractions/> take a slug argument and (where relevant) a title, label, author, ref, or perspective.
Tag wikitext examples
<indication slug="ssri-depression" title="Major depressive disorder" author="MDElliottMD">Use cautiously in adolescents.</indication> <effect slug="nausea" label="Nausea"/> <effect ref="hyperkalemia"/> <!-- ref to global effect library --> <titration slug="slow-start-elderly" title="Slow start (elderly)" author="MDElliottMD">Begin at 10 mg q AM; titrate by 10 mg every 14 days.</titration> <anecdote slug="qi8sg2" perspective="provider" author="MDElliottMD">One patient developed serotonin syndrome at week 3...</anecdote> <pharmaInteractions/>
The unified compact row layout
Indication, Effect, Interaction, Titration, and Anecdote all render through a shared row pattern:
<div class="pcp-row pcp-row-{type} pcp-{type}" ...data-*>
<div class="pcp-row-head">
<span class="pcp-row-title">...</span>
<span class="pcp-row-aggs">...</span>
<span class="pcp-row-actions">
<button class="pcp-row-action pcp-row-action-toggle" data-target="rate">Rate</button>
[<button data-target="notes">Notes (N)</button>] # only for Interaction
[× delete button] # only for sysop/admin
</span>
</div>
[<div class="pcp-row-panel pcp-row-rate-panel" hidden>...</div>]
[<div class="pcp-row-panel pcp-row-notes-panel" hidden>...</div>]
[<div class="pcp-row-body">...wikitext body, always visible if present...</div>]
</div>
- Rate / Notes panels are hidden by default; the shared
.pcp-row-action-toggleJS handler reveals them inline on click. - Bodies (page-specific descriptions for Indication / Effect / Titration / Anecdote) are always visible.
- The
×admin delete button uses the established.pcp-del-btnpattern (or.pcp-ix-del-rowfor interactions). - Each row sits in its own block-formatting context (
display: flow-root) so its border-box respects floated infoboxes.
Voting / rating semantics
| Element | Scale | Perspectives | Storage |
|---|---|---|---|
| Vote tag | +1 / −1 binary | single | pcp_votes
|
| Titration | +1 / −1 binary | single | pcp_votes
|
| Anecdote | +1 / −1 binary | single (perspective is a metadata label, not a separate aggregate) | pcp_votes
|
| Indication | 0–5 likert + "don't know" | single | pcp_likert_reports
|
| Effect (patient) | experienced ∈ {yes, no, unsure} + valence −3..+3 | patient | pcp_effect_reports (perspective=1)
|
| Effect (provider) | frequency ∈ {0, 5, 20, 33, 50, 66, 80, 95, −1 don't know} + valence −3..+3 | provider | pcp_effect_reports (perspective=2)
|
| Interaction | experience 1–5 + outcome −3..+3 + optional free-text note | user + provider, separate aggregates | pcp_interaction_reports
|
Server-side aggregates: n, mean of the rating field, and (where applicable) severe = (vmean ≤ −2.5).
Aggregates are recomputed and returned by every report-submit API call so the row re-renders in place without a page reload.
Effect bucketing
When a wiki <ul> contains only <effect> cards, JavaScript groups them into buckets by the provider frequency mean (data-fmean):
| Bucket | fmean band | Default state |
|---|---|---|
| Common | > 20 | expanded, always visible |
| Uncommon | > 5 and ≤ 20 | collapsed |
| Rare | ≤ 5, provider vmean > −2.5 | collapsed |
| Rare but Severe | ≤ 5 and vmean ≤ −2.5 | expanded by default, red highlight |
| Not yet rated | no provider data (n=0) | collapsed, only renders if non-empty |
The vmean ≤ −2.5 threshold is also the trip-wire for the "severe" red treatment on interaction rows.
Interactions feature
The Interactions section is rendered by placing <pharmaInteractions/> anywhere in the wikitext of a med article (NS_MAIN) or a Category page (NS_CATEGORY).
Entity model
An interaction is an undirected edge between two endpoints. Each endpoint has a type (med or category) and a slug (DB-key form of the page title).
Pairs are stored in canonical order: smaller (type, slug) tuple on the left. This collapses A↔B and B↔A into a single row.
Tables
pcp_interactions: one row per interaction edge.
pi_id auto-increment pi_element_id FK -> pcp_votable_elements (reuse votes/comments infra) pi_left_type 'med' | 'category' pi_left_slug VARBINARY(255) pi_right_type 'med' | 'category' pi_right_slug VARBINARY(255) pi_created_user_id INT pi_created BINARY(14) UNIQUE (pi_left_type, pi_left_slug, pi_right_type, pi_right_slug)
pcp_interaction_reports: one row per (interaction, user, perspective).
pir_element_id FK -> pcp_votable_elements pir_user_id INT pir_perspective 1 = user, 2 = provider pir_experience TINYINT (1..5, nullable) pir_valence TINYINT (-3..+3, nullable) pir_note MEDIUMBLOB (nullable) pir_created BINARY(14) pir_updated BINARY(14) UNIQUE (pir_element_id, pir_user_id, pir_perspective)
Rendering rules
- On a med page M, list:
- Direct edges: rows where M is one side.
- Transitive edges: rows where one side is a category C that M is itself a member of (via MW's
categorylinks).
- Direct wins: if the same counterparty is reachable both directly and transitively, drop the transitive duplicate.
- On a Category page, list direct edges only (no transitive walk).
- Sort: pooled
valence_meanascending (most negative on top). Nulls sink. Tiebreakers:ndesc, then alphabetic. - Severe (any of pooled / user / provider vmean ≤ −2.5): red 4 px left border + red-tinted background + "severe" pill + counterparty title in red.
Add-interaction modal
Triggered by the + Add interaction button at the bottom of the section. Two-stage UX:
- Search input → results split into Meds (teal chip) and Categories (amber chip).
- Click
Use→ confirm viaAdd interactionbutton → POST topharmacopediainteractionadd.
Categories appear in the modal only if tagged with the marker category (default Category:MedCategory, configurable via $wgPharmacopediaInteractionCategoryMarker). The transitive walk during rendering, however, uses all of a page's categories regardless of marker — historic data isn't hidden by changing the marker policy.
Delete
Two a