Skip to content

EP Courses

EP Courses is course and lesson management for PageMotor. Courses contain lessons, students enrol, their progress is tracked as they work through the material, and content can be translated into any of 24 languages including Welsh, Irish, and several South African languages.

Published by ElmsPark Studio.

Work in progress (version 0.5.14). The core structure is stable but some features are still being built. This guide describes what is currently shipping. Expect changes in subsequent versions.

  • Course catalogue with title, slug, description, learning outcomes, access, price, status. Access is the free / premium switch: the edit form offers Free and Premium. A Free course is joined with one click; a Premium course with a price is bought through Stripe (see Selling courses). Before 0.4.13 this field was labelled Level, with Premium labelled Intermediate. Same switch, clearer name.
  • Lessons attached to courses, with ordered sequence and translatable content.
  • Selling courses from 0.5.0. Set Access to Premium with a price and the catalogue shows a Buy button; the student pays on a Stripe-hosted page and comes back enrolled. Needs EP Ecommerce Stripe.
  • Enrolment tracking — which student is in which course.
  • Progress tracking — which lessons has the student completed.
  • Multilingual content — store title and body as JSON translations per course and per lesson.

This plugin deliberately has a narrow scope. It does not:

  • Handle frontend student login — that’s EP Membership’s job.
  • Host videos. Embed from YouTube, Vimeo, or an MP4 file you host yourself. If that file lives in your own S3-compatible bucket, EP Media Storage will serve it behind a link that expires.
  • Issue certificates — not currently built in.
  • Run quizzes inside lessons — not currently built in.
  • Sell recurring access. A course sale is a one-off purchase. For subscription billing, pair with EP Ecommerce Subscriptions.
  • Refund or revoke a purchase from the admin. Refund in Stripe, then remove the enrolment by hand.
  • Sell to a signed-out visitor. An enrolment belongs to a user account, so the buyer needs to be registered and signed in before they can pay.

For a full learning management system, combine EP Courses with EP Membership (for login and access control) and EP Ecommerce Stripe (to take the payment).

  • PageMotor 0.8.2b or later
  • EP Suite base class

Optional but commonly paired:

  • EP Membership for student accounts and authenticated course access.
  • EP Ecommerce and EP Ecommerce Stripe 0.1.21 or later to sell courses. Both are required to charge for one; EP Ecommerce Stripe builds on EP Ecommerce and does nothing without it.
  • EP Media Storage to serve lesson videos from your own bucket behind expiring links.
  1. ep-courses.zip comes with an EP Suite licence — ElmsPark supplies it directly (see EP Suite plugins); after install it updates through your site’s Updates screen.
  2. Upload via Plugins → Manage Plugins. Activate.
  3. Open the settings page and set the viewer slug (URL slug of the page that will host the lesson viewer).
  4. Create a PageMotor page with that slug and add the [ep-course-viewer] shortcode to it.
ShortcodePurpose
[ep-courses]Course catalogue grid. Shows every published course with lesson count, duration, and whether it’s free or paid. Links each course to its first lesson.
[ep-course-viewer]Lesson viewer. Renders the current lesson with navigation to previous and next. Place on a single page — the viewer URL is shared across all lessons.
  • {prefix}ep_courses — course catalogue rows.
  • {prefix}ep_lessons — lesson content with translations JSON.
  • Enrolment and progress tables keyed to course ID and student ID.

Each course and lesson stores its translations as a JSON map keyed by language name, exactly as the language appears in settings — not by language code:

{
"Welsh": { "title": "Cyflwyniad", "content": "...", "video_url": "..." },
"Irish": { "title": "Réamhrá", "content": "...", "video_url": "..." }
}

Lessons translate title, content, and video URL (so each language can carry its own cut of the video). Courses translate title, description, and outcome. English isn’t in the map: the English text lives on the course or lesson itself, and it is also the fallback wherever a translation is missing.

The language a student reads is stored on their enrolment, chosen at enrol time — the language of the catalogue they enrolled from — and the viewer renders every lesson in that language from then on. The catalogue’s own language comes from the shortcode, [ep-courses language="Welsh"], defaulting to the default language in settings. The site language plays no part.

The 24 supported languages are ticked on and off in settings. Welsh, Irish, Scots Gaelic, and a full set of South African languages (Afrikaans, isiZulu, isiXhosa, Sepedi, Setswana, Sesotho, Xitsonga, siSwati, Tshivenda, isiNdebele) are available alongside the usual European set.

  1. Create a course with title, description, outcome, access, and price.
  2. Add lessons to the course. Each lesson has a title, body, and sort order.
  3. (Optional) Tick the languages you need in settings, then fill in each language’s fields in the Translations accordion on the course and lesson edit forms.
  4. Create a page with the viewer slug, add [ep-course-viewer].
  5. Create a catalogue page, add [ep-courses].
  6. If the course is paid, set Access to Premium with a price, and make sure both EP Ecommerce and EP Ecommerce Stripe are active, with your Stripe keys saved in EP Ecommerce Stripe.
  7. If gated, configure EP Membership to require login for the viewer page.

From 0.5.0 a course can be sold. Set Access to Premium, give it a price and currency, and the catalogue shows a Buy button in place of the old disabled “Coming Soon”.

You need both EP Ecommerce and EP Ecommerce Stripe 0.1.21 or later active, with your Stripe keys saved in the latter. EP Ecommerce Stripe does nothing on its own: it builds on EP Ecommerce, and without it the payment confirmation never runs.

If any part of that chain is missing, the catalogue keeps showing the old disabled “Coming Soon” button rather than a Buy button. That is deliberate. A Buy button is only offered where a payment could actually be completed, so nobody can pay into a site that cannot finish the enrolment.

The buyer must be signed in first. An enrolment belongs to a user account, so a signed-out visitor sees a link to register instead.

This is the part worth understanding, because it is deliberately strict.

Pressing Buy records the enrolment as pending and sends the student to a Stripe-hosted payment page. A pending enrolment grants nothing: the course stays locked, exactly as if they had never pressed the button. Someone who reaches the payment page and abandons it is in no different a position from someone who never started.

The course unlocks in one place only, when Stripe confirms the payment. Returning to the success page does not by itself grant anything, because anyone can visit a URL. If the confirmation has not arrived yet, that page says the payment went through and the course is being set up.

The price is read from the course record at the moment the payment session is created. The browser only ever says which course, never what it costs.

Confirmation is not always instant. Stripe notifies your site, your site records that notification, and the enrolment is completed on the next page view. In practice that is usually seconds. It is normal for a student to land back on the course a moment before it opens, which is what the “setting up your course” message covers.

Refunds are not wired up. Refund in Stripe as usual, then remove the enrolment by hand. A refund does not revoke access on its own.

Enrolment gates the lesson page. Until version 0.4.12 it did not gate the video file: the page carried the plain storage URL, so a student could copy that URL and pass it to anyone, and it kept working.

From 0.4.12, if EP Media Storage is installed and pointed at your bucket, an MP4 lesson is served through a link that expires instead, two hours by default. Nothing changes in EP Courses itself: set Video Type to MP4 and paste the bucket URL as before. Enrolment gating is unchanged, seeking still works, and a video already playing is not interrupted when its link expires.

Without EP Media Storage, or for a file hosted outside your bucket, the URL is emitted exactly as it always was. YouTube and Vimeo lessons are unaffected either way.

We recommend Cloudflare R2. Every time a student plays a lesson, the video is downloaded from your storage, and most providers charge for that, so a popular course costs more the more it is watched. R2 does not charge for downloads at all, includes 10 GB of storage free each month, and can keep your files within the EU. A 20 GB course library costs about 15 cents a month however many students watch it.

Setup, pricing details and the read-only key are covered in EP Media Storage: Choosing a storage provider. If your videos already live in Backblaze B2 or Amazon S3, they work as they are.

Worth being honest with your own students about what this is: expiring links stop a shared link from working later, which is how course content usually escapes. They do not stop somebody who is entitled to watch from saving the file while they watch it. No link scheme does.

Only published courses appear. Check your courses have status = Published, not Draft.

The viewer page must exist at the slug configured in Settings → Viewer slug, and that page must contain [ep-course-viewer]. Verify both.

The viewer renders the language stored on the student’s enrolment, set when they enrolled — not the site language. Check the student enrolled from a catalogue in the right language, and that the translation keys are full language names exactly as they appear in settings (Welsh, not cy). Anything missing falls back to the untranslated English content.

“I want to restrict a course to paid subscribers only”

Section titled ““I want to restrict a course to paid subscribers only””

Two different things, so pick the one you mean. To sell a course outright, set Access to Premium with a price and let EP Ecommerce Stripe take the payment (see Selling courses). To gate a course behind an existing membership rather than a single sale, that is EP Membership’s access rules on the viewer page, optionally with EP Ecommerce Subscriptions handling the recurring billing.

“A Premium course shows Coming Soon instead of a Buy button”

Section titled ““A Premium course shows Coming Soon instead of a Buy button””

The Buy button only appears where a payment could actually be completed, which is deliberate. Check all four: EP Ecommerce is active, EP Ecommerce Stripe is active with your keys saved, the course has a price above zero, and you are signed in rather than viewing signed out. Missing EP Ecommerce is the easy one to overlook, because EP Ecommerce Stripe looks installed and configured while the half that completes the sale is absent.

“A student paid but the course has not opened”

Section titled ““A student paid but the course has not opened””

Give it a page view. Confirmation is recorded when Stripe notifies your site and applied on the next request, so the enrolment completes moments later rather than instantly.

If it is still closed after that, check the payment actually succeeded in your Stripe dashboard, and that the webhook endpoint in Stripe points at your site with the signing secret saved in EP Ecommerce Stripe. A payment that never produced a webhook cannot complete an enrolment.

“Previous is missing, and Next goes back to lesson one”

Section titled ““Previous is missing, and Next goes back to lesson one””

Fixed in 0.4.15. The Sort Order box on each lesson used to arrive pre-filled with 0, and leaving it alone left every lesson at position 0, so the viewer could not tell them apart. Previous never rendered, Next returned to the first lesson, and every sidebar link went to the same place.

Updating repairs existing courses automatically on the next admin page load. Lessons you had already numbered keep their order and stay in front; only the zeros are renumbered, in the order they were created. A course already numbered properly is left untouched.

New lessons now leave Sort Order blank, meaning “add at the end”.

“Students can see lessons they haven’t completed prerequisites for”

Section titled ““Students can see lessons they haven’t completed prerequisites for””

Prerequisite-based progression isn’t built in yet. On the roadmap but not shipped. See the review queue.

For a quick question about this plugin, EP Support inside your admin is the fastest option. The chat widget sits on every EP plugin settings page and knows which one you’re on, with starter questions and links preloaded for that exact screen.

For anything bigger — a bug report, a feature request, or a “how do I…” that needs a real reply — open a ticket at help.elmspark.com. A real person, helped by AI, writes the reply. Usually within a few hours. Tickets don’t disappear into the void.

  • Settings language menu. The language menu in this plugin’s settings now lists only the languages it is actually translated into, plus English, so you can no longer pick a language that changes nothing.
  • Danish. Adds a Danish translation.
  • Nothing changes that you can see. The course tables now use the same text settings (collation) as the rest of the EP suite, so reports and plugins that combine course data with other suite data can no longer fail with a “mix of collations” error. Existing tables are converted once, automatically.
  • A course refunded before its payment arrived is shown as refunded straight away. With EP Ecommerce Stripe 0.1.31, EP Courses is told when a course payment was refunded in full before it was handed over. The enrolment waiting for it is recorded as refunded, with no course, instead of staying pending.
  • If that refund later fails or you cancel it, the learner gets the course. Before, a refund that arrived first and then failed left the learner paid but without the course. Their earlier progress is carried over, as for any purchase. If they had already bought the course again, the extra payment is listed as paid twice, as before.
  • A refund that Stripe reports before the payment itself no longer gives the learner the course. Stripe does not always deliver its messages in order. If you refunded a purchase in full very quickly, the refund could arrive first, be set aside because the sale was not yet recorded, and the payment arriving afterwards then opened the course: the learner had both the course and their money back. EP Courses now checks with Stripe before opening a course, and a payment that has already been refunded in full is recorded as refunded and opens nothing.
  • A partial refund still leaves the course open, as before. If Stripe cannot be reached at that moment, the course is opened as before.
  • Each course purchase now records the price the learner actually paid. If you changed a course’s price while a learner had a payment page open, and they paid on one page or on both, the enrolment could show the price from their first click instead of what that payment charged. Each payment now records its own amount and currency, taken from Stripe, including any promotion code discount.
  • Payment pages opened before this update are recorded as before; the amount is corrected only for pages opened from now on.
  • A learner who buys a course again after a refund gets their progress back. Until now the new purchase started from lesson one. Lessons they had finished are now marked as done again the moment the new payment comes through. Only their own earlier purchase of that course counts, only lessons that are still published are brought back, and nothing they have done is ever removed.
  • If they had finished every lesson before the refund, the course shows as completed straight away, dated when they finished the last lesson rather than the day they paid again. Completing a course sends nothing and triggers nothing else, so no email or message goes out a second time.
  • The course page for a refunded learner now adds that the lessons they finished will be marked as done if they buy the course again.
  • When a double payment is settled (from 0.5.8 and 0.5.9), a course that ends up finished is also dated when the last lesson was finished, and lessons you have since unpublished or deleted are no longer copied across.
  • A learner who pays is now always given the course. If a learner opened the payment page twice (for example once before and once after you changed the price) and paid on the older page, Stripe took the money but the course stayed locked until someone stepped in. Any payment page EP Courses opened for that learner now opens the course. A payment page EP Courses did not open for them still opens nothing.
  • A second payment for a course the learner already has no longer gives them the course twice. If they pay on both pages, or pay a new purchase after a refund you made has failed and given them the course back, the learner keeps one copy of the course, and the notice at the top of the EP Courses settings screen says the customer has paid twice and names the payment to refund in Stripe. The notice clears once that refund comes through.
  • If you refund the other payment of the two instead, the learner does not lose the course: the payment you kept now opens it, with their progress, and the notice clears.
  • The course page for a refunded learner no longer says their progress has been kept. Buying the course again starts a fresh enrolment, so it now simply says they are welcome to buy it again.
  • A customer who paid twice for a course is now pointed out to you, instead of quietly getting the course twice. This can happen when you refund a course, the learner buys it again, and then Stripe fails or cancels the first refund: you now hold two payments for one course. Until now EP Courses gave the first purchase back as well, so the learner had two copies of the course. Now the learner keeps the newer purchase, the older one stays refunded, and a notice at the top of the EP Courses settings screen says the customer has paid twice and names the payment to refund again in Stripe. The notice clears once that refund comes through.
  • The learner loses nothing: lessons they finished under the earlier purchase are carried over to the newer one, and if that finishes every lesson the course shows as completed.
  • When there is no second purchase, a failed or cancelled refund gives the course back exactly as in 0.5.7.
  • A course taken away by a Stripe refund now comes back if that refund fails or is cancelled. When you refund a course purchase in full, the learner loses access straight away. Stripe can later fail that refund (for example, the card was closed) or you can cancel it while it is still pending; the money then stays with you, but until now the learner stayed locked out and access had to be given back by hand. It is now given back automatically, with all their progress, and a learner who had finished the course shows as finished again. No email is sent.
  • Only a course that the Stripe refund itself took away is given back. An enrolment you marked as refunded some other way, or one refunded before this update, is left exactly as it is.
  • Needs EP Ecommerce Stripe 0.1.28 or later, which tells EP Courses when a refund fails or is cancelled. EP Courses 0.5.6 (the lesson-completion fix) comes first.
  • Security: a paid course could be opened without paying. A signed-in learner who started a purchase and then left the payment page, or whose purchase was later refunded, could mark lessons as complete and so unlock the whole course. Marking a lesson complete now works only on an enrolment that is active or completed, and only for a published lesson of that same course.
  • Course completion now counts only the course’s own published lessons.
  • Enrol and Buy buttons now go to your own sign-up page. On the course catalogue, a visitor who is not signed in and clicks Enrol Free (or Buy) was always sent to /register/. If EP Membership’s registration page on your site has a different address, such as /join/, that visitor landed on Page not found. The buttons now use the registration page set in EP Membership, and still use /register/ when EP Membership is not installed or the setting is blank.
  • The Log in link shown to a signed-out visitor in the lesson viewer follows EP Membership’s login page in the same way, with /login/ as before when nothing is set.
  • Signed-in visitors see no change.
  • The check that decides who adds the cache-busting tag to the course stylesheet now reads beta and release-candidate PageMotor cores (such as 0.11b or 0.11rc1) correctly. It previously mistook them for versions older than 0.11 and added a second tag on top of the core’s.
  • No change on regular PageMotor releases, and no change to what the stylesheet does.
  • Fixes a doubled cache-busting tag on this plugin’s front-end stylesheet. On PageMotor 0.11 and later the page asked for the file as ep-courses-frontend.css?v=…?v=…, because the core had started adding its own version tag and the plugin was still adding one too. The file loaded correctly, so nothing was visibly broken; the address was just malformed and every cache saw a longer key than it needed. The plugin now leaves the tag to the core on 0.11 and later, and still adds its own on older cores, where nothing else would.
  • No change to what the front-end stylesheet does.
  • A lesson can now contain an exercise, a form, or anything else a plugin provides. Until this release the lesson body was printed exactly as written, so a shortcode dropped into a lesson appeared on the page as literal text rather than doing anything. There was no error and nothing in any log to explain it: the lesson simply showed the shortcode back to the learner.
  • Lessons that contain no shortcode are completely unaffected. If your course is plain text, images and video, this release changes nothing you can see.
  • If you have a lesson with a shortcode in it that has never worked, it will start working the moment you update. Worth a quick look at your lessons afterwards.
  • Refunds now take the course back. Refund a course purchase in Stripe, from the dashboard or anywhere else, and the student’s access is withdrawn automatically. Previously the money went back but the course stayed open until someone removed the enrolment by hand. Needs EP Ecommerce Stripe 0.1.22 or later.
  • Only a full refund revokes access. A partial refund is usually a goodwill gesture or a price adjustment, and locking someone out of something they paid most of would be worse than doing nothing, so partial refunds are recorded and left alone. Chargebacks are deliberately left alone too: a dispute can be won, and acting on the bank’s first message would punish a customer whose case later goes their way.
  • The enrolment and the student’s progress are kept, not deleted. Access stops immediately, but if they buy the course again later they pick up where they left off.
  • A refunded student who opens the course is told so plainly, instead of seeing the generic “you need to enrol first”, which reads as though the site has forgotten them.
  • Fixed before release: on a site already running 0.5.0, the database change this feature needs was never applied, so the refund failed, retried ten times and the student kept the course. Sites on 0.5.0 pick it up automatically on this version.