' . 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; } }