' . esc_html__( 'This is a sample course generated by the Academy Dummy Data seeder. It demonstrates every lesson type, a quiz covering every question type, and the full curriculum structure — ideal for testing themes, layouts and integrations. Delete it anytime from the Dummy Data tab.', 'academy' ) . '
';
}
/**
* @return string[]
*/
public static function categories() {
return [
__( 'Development', 'academy' ),
__( 'Design', 'academy' ),
];
}
/**
* @return string[]
*/
public static function tags() {
return [
__( 'sample', 'academy' ),
__( 'demo', 'academy' ),
__( 'beginner', 'academy' ),
];
}
/**
* A short filler paragraph for lesson/description bodies.
*
* @return string
*/
public static function paragraph() {
return '
' . esc_html__( 'Sample lesson content generated for testing. Replace this with your own material. It covers the key ideas of the topic with a couple of short, illustrative examples so the layout has something realistic to render.', 'academy' ) . '
';
}
/**
* The curriculum blueprint: modules, each with lessons (one per video source
* type + a text lesson). The quiz + assignment are attached by the provider.
*
* Each lesson: [ title, source (video_source type or 'text'), body ].
*
* @return array
*/
public static function modules() {
return [
[
'title' => __( 'Getting Started', 'academy' ),
'description' => __( 'Orientation and the different ways a lesson can present its content.', 'academy' ),
'lessons' => [
[
'title' => __( 'Welcome & Course Overview (Text)', 'academy' ),
'source' => 'text',
],
[
'title' => __( 'Intro Video (YouTube)', 'academy' ),
'source' => 'youtube',
],
[
'title' => __( 'Deep Dive (Vimeo)', 'academy' ),
'source' => 'vimeo',
],
],
],
[
'title' => __( 'Core Concepts', 'academy' ),
'description' => __( 'Hands-on lessons delivered through embedded media.', 'academy' ),
'lessons' => [
[
'title' => __( 'Embedded Player', 'academy' ),
'source' => 'embedded',
],
[
'title' => __( 'Shortcode Media', 'academy' ),
'source' => 'short_code',
],
],
],
];
}
/**
* Build the `video_source` meta value for a given lesson source type.
*
* @param string $source One of text|youtube|vimeo|embedded|short_code.
*
* @return array{type:string,url?:string}|array
*/
public static function video_source_for( $source ) {
switch ( $source ) {
case 'youtube':
return [
'type' => 'youtube',
'url' => self::SAMPLE_YOUTUBE_URL,
];
case 'vimeo':
return [
'type' => 'vimeo',
'url' => self::SAMPLE_VIMEO_URL,
];
case 'embedded':
return [
'type' => 'embedded',
'url' => '',
];
case 'short_code':
return [
'type' => 'short_code',
'url' => '[embed]' . self::SAMPLE_YOUTUBE_URL . '[/embed]',
];
case 'text':
default:
return [];
}//end switch
}
/**
* Question blueprints covering every supported question type.
*
* Shape mirrors the built-in course importer:
* [ 'question', 'slug', 'options' => [ ['text','slug','image'?] ], 'correctAnswer' ]
* `correctAnswer` is a slug or array of slugs; for fillInTheBlanks it is the
* blank value(s). `image` on an option marks it for placeholder-image use
* (imageAnswer type).
*
* @return array
*/
public static function question_definitions() {
return [
[
'question' => __( 'Academy LMS is a WordPress plugin.', 'academy' ),
'slug' => 'trueFalse',
'options' => [
[
'text' => __( 'True', 'academy' ),
'slug' => 'true'
],
[
'text' => __( 'False', 'academy' ),
'slug' => 'false'
],
],
'correctAnswer' => 'true',
],
[
'question' => __( 'Which language runs in the browser?', 'academy' ),
'slug' => 'singleChoice',
'options' => [
[
'text' => __( 'PHP', 'academy' ),
'slug' => 'opt_php'
],
[
'text' => __( 'JavaScript', 'academy' ),
'slug' => 'opt_js'
],
[
'text' => __( 'MySQL', 'academy' ),
'slug' => 'opt_sql'
],
[
'text' => __( 'Bash', 'academy' ),
'slug' => 'opt_bash'
],
],
'correctAnswer' => 'opt_js',
],
[
'question' => __( 'Select all front-end technologies.', 'academy' ),
'slug' => 'multipleChoice',
'options' => [
[
'text' => __( 'HTML', 'academy' ),
'slug' => 'opt_html'
],
[
'text' => __( 'CSS', 'academy' ),
'slug' => 'opt_css'
],
[
'text' => __( 'Apache', 'academy' ),
'slug' => 'opt_apache'
],
[
'text' => __( 'React', 'academy' ),
'slug' => 'opt_react'
],
],
'correctAnswer' => [ 'opt_html', 'opt_css', 'opt_react' ],
],
[
'question' => __( 'The CSS property for text colour is {dash}.', 'academy' ),
'slug' => 'fillInTheBlanks',
'options' => [],
'correctAnswer' => [ 'color' ],
],
[
'question' => __( 'In your own words, explain what an API is.', 'academy' ),
'slug' => 'shortAnswer',
'options' => [],
'correctAnswer' => '',
],
[
'question' => __( 'Pick the image that represents the front-end layer.', 'academy' ),
'slug' => 'imageAnswer',
'options' => [
[
'text' => __( 'Browser UI', 'academy' ),
'slug' => 'img_ui',
'image' => true
],
[
'text' => __( 'Database', 'academy' ),
'slug' => 'img_db',
'image' => true
],
],
'correctAnswer' => 'img_ui',
],
[
'question' => __( 'Which language usually runs on the server?', 'academy' ),
'slug' => 'dropDown',
'options' => [
[
'text' => __( 'PHP', 'academy' ),
'slug' => 'dd_php'
],
[
'text' => __( 'CSS', 'academy' ),
'slug' => 'dd_css'
],
[
'text' => __( 'HTML', 'academy' ),
'slug' => 'dd_html'
],
],
'correctAnswer' => 'dd_php',
],
];
}
/**
* Create a placeholder image attachment (no network) — used as both the
* seeded course's featured image and every one of its lessons'
* featured/attachment image. Always prefers sideloading the plugin's own
* bundled add-ons banner artwork (a real, designed image) over a GD-drawn
* coloured tile with a label, which only falls back to a blank 1×1 pixel
* once GD is unavailable — that blank tile still attaches/sets as the
* featured image "successfully" but shows nothing wherever it's used.
*
* @param string $label Text drawn on the tile (GD fallback only).
* @param int $variant Colour variant selector (GD fallback only).
*
* @return int Attachment id, or 0 on failure.
*/
public static function create_placeholder_image( $label = 'Academy', $variant = 0 ) {
$attachment_id = self::bundled_banner_image();
if ( $attachment_id ) {
return $attachment_id;
}
// Fallback, only if the bundled artwork couldn't be sideloaded
// (missing file, disk write failure, etc.) and GD happens to be
// available.
return self::gd_placeholder_image( $label, $variant );
}
/**
* GD-drawn coloured tile with a label — the fallback when the bundled
* banner artwork (see bundled_banner_image()) can't be used.
*
* @param string $label Text drawn on the tile.
* @param int $variant Colour variant selector.
*
* @return int Attachment id, or 0 on failure.
*/
private static function gd_placeholder_image( $label, $variant ) {
if ( ! function_exists( 'imagecreatetruecolor' ) ) {
return 0;
}
$bytes = self::placeholder_png_bytes( $label, $variant );
if ( '' === $bytes ) {
return 0;
}
$filename = 'academy-seeder-' . sanitize_title( $label . '-' . $variant ) . '.png';
$upload = wp_upload_bits( $filename, null, $bytes );
if ( ! empty( $upload['error'] ) || empty( $upload['file'] ) ) {
return 0;
}
$attachment_id = wp_insert_attachment( [
'post_mime_type' => 'image/png',
'post_title' => sanitize_file_name( $label ),
'post_status' => 'inherit',
], $upload['file'] );
if ( is_wp_error( $attachment_id ) || ! $attachment_id ) {
return 0;
}
if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
require_once ABSPATH . 'wp-admin/includes/image.php';
}
$meta = wp_generate_attachment_metadata( $attachment_id, $upload['file'] );
wp_update_attachment_metadata( $attachment_id, $meta );
return (int) $attachment_id;
}
/**
* GD-less fallback: sideloads assets/images/addons-pro-banner.svg (the
* "Unlock Every Add-On with One Price" background on the Add-ons page —
* a purely abstract gradient graphic, no promotional text baked into the
* file itself) as the seeded course's featured image.
*
* @return int Attachment id, or 0 on failure.
*/
private static function bundled_banner_image() {
$source = ACADEMY_ASSETS_DIR_PATH . 'images/addons-pro-banner.svg';
if ( ! file_exists( $source ) ) {
return 0;
}
// phpcs:disable WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown -- local file, not a remote URL
$bytes = file_get_contents( $source ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
// phpcs:enable WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown
if ( false === $bytes || '' === $bytes ) {
return 0;
}
// wp_upload_bits() checks the file extension against the site's
// allowed mime types, and SVG isn't in WordPress's default list —
// allow it only for this one sideload of a trusted, bundled plugin
// asset (never for user-supplied uploads).
$allow_svg = function ( $mimes ) {
$mimes['svg'] = 'image/svg+xml';
return $mimes;
};
add_filter( 'upload_mimes', $allow_svg ); // phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.upload_mimes -- dev-only seeder, filter removed right after
$upload = wp_upload_bits( 'academy-seeder-course-banner.svg', null, $bytes );
remove_filter( 'upload_mimes', $allow_svg );
if ( ! empty( $upload['error'] ) || empty( $upload['file'] ) ) {
return 0;
}
$attachment_id = wp_insert_attachment( [
'post_mime_type' => 'image/svg+xml',
'post_title' => __( 'Sample Course Banner', 'academy' ),
'post_status' => 'inherit',
], $upload['file'] );
if ( is_wp_error( $attachment_id ) || ! $attachment_id ) {
return 0;
}
return (int) $attachment_id;
}
/**
* Raw PNG bytes for a placeholder tile. Only called once the caller has
* already confirmed GD (imagecreatetruecolor) is available.
*
* @param string $label Text drawn on the tile.
* @param int $variant Colour variant.
*
* @return string Binary PNG data, or '' on failure.
*/
private static function placeholder_png_bytes( $label, $variant ) {
$palette = [
[ 79, 70, 229 ],
[ 16, 185, 129 ],
[ 249, 115, 22 ],
[ 236, 72, 153 ],
];
$rgb = $palette[ $variant % count( $palette ) ];
$width = 1200;
$height = 675;
$img = imagecreatetruecolor( $width, $height );
$bg = imagecolorallocate( $img, $rgb[0], $rgb[1], $rgb[2] );
$fg = imagecolorallocate( $img, 255, 255, 255 );
imagefill( $img, 0, 0, $bg );
$text = substr( (string) $label, 0, 40 );
imagestring( $img, 5, 40, (int) ( $height / 2 ) - 10, $text, $fg );
ob_start();
imagepng( $img );
$bytes = ob_get_clean();
imagedestroy( $img );
return (string) $bytes;
}
}