├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── binder └── requirements.txt ├── chapters ├── chapter1.md └── chapter2.md ├── docker-compose.yml ├── dockerfile ├── docs ├── _config.yml ├── img │ ├── chapter_layout.png │ ├── chapters_img.png │ ├── julia.png │ ├── multi-q-exercise.png │ └── naome.png └── index.md ├── exercises ├── bookquotes.json ├── exc_01_03.py ├── solution_01_03.py └── test_01_03.py ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── meta.json ├── package-lock.json ├── package.json ├── slides └── chapter1_01_introduction.md ├── src ├── components │ ├── button.js │ ├── choice.js │ ├── code.js │ ├── exercise.js │ ├── hint.js │ ├── juniper.js │ ├── layout.js │ ├── link.js │ ├── seo.js │ ├── slides.js │ └── typography.js ├── context.js ├── markdown.js ├── pages │ └── index.js ├── styles │ ├── button.module.sass │ ├── chapter.module.sass │ ├── choice.module.sass │ ├── code.module.sass │ ├── exercise.module.sass │ ├── hint.module.sass │ ├── index.module.sass │ ├── index.sass │ ├── layout.module.sass │ ├── link.module.sass │ ├── reveal.css │ ├── slides.module.sass │ └── typography.module.sass └── templates │ └── chapter.js ├── static ├── icon.png ├── icon_check.svg ├── icon_slides.svg ├── logo.svg ├── profile.jpg └── social.jpg └── theme.sass /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # dotenv environment variables file 57 | .env 58 | 59 | # gatsby files 60 | .cache/ 61 | public 62 | 63 | # Mac files 64 | .DS_Store 65 | 66 | # Yarn 67 | yarn-error.log 68 | .pnp/ 69 | .pnp.js 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 4, 6 | "printWidth": 100, 7 | "overrides": [ 8 | { 9 | "files": "*.sass", 10 | "options": { 11 | "printWidth": 999 12 | } 13 | }, 14 | { 15 | "files": "*.md", 16 | "options": { 17 | "tabWidth": 2, 18 | "printWidth": 80, 19 | "proseWrap": "always", 20 | "htmlWhitespaceSensitivity": "strict" 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2019 Ines Montani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Online course starter: Python 2 | 3 | This is a starter repo based on the 4 | [course framework](https://github.com/ines/spacy-course) I developed for my 5 | [spaCy course](https://course.spacy.io). The front-end is powered by 6 | [Gatsby](http://gatsbyjs.org/) and [Reveal.js](https://revealjs.com) and the 7 | back-end code execution uses [Binder](https://mybinder.org) 💖 8 | 9 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/ines/courser-starter-python) 10 | 11 | [![](https://user-images.githubusercontent.com/13643239/56341448-68fe9380-61b5-11e9-816f-5c71ae71b94f.png)](https://course-starter-python.netlify.com) 12 | 13 | ## 📖 Documentation 14 | 15 | 16 | 17 | Thanks to [@hfboyce](https://github.com/hfboyce) for contributing a super detailed guide on getting started with this course framework, adding exercises, slides and other content, and customizing the design. It also comes with a Dockerfile that takes care of the dependencies for you. 18 | 19 | [➡️ **Read the documentation here**](https://ines.github.io/course-starter-python/) 20 | 21 | 22 | ## ✅ Quickstart 23 | 24 | 1. [Import](https://github.com/new/import) this repo, install it and make sure 25 | the app is running locally. 26 | 2. Customize the [`meta.json`](meta.json) and 27 | [`binder/requirements.txt`](binder/requirements.txt). 28 | 3. Build a [Binder](https://mybinder.org) from the `binder` branch of this repo. 29 | 4. Add content (chapters, exercises and slides) and optionally add separate 30 | content license. 31 | 5. Customize the UI theme in [`theme.sass`](theme.sass) and update images in 32 | [`static`](static) as needed. 33 | 6. Deploy the app, e.g. to [Netlify](https://netlify.com). 34 | 35 | ### Running the app 36 | 37 | To start the local development server, install [Gatsby](https://gatsbyjs.org) 38 | and then all other dependencies. This should serve up the app on 39 | `localhost:8000`. 40 | 41 | ```bash 42 | npm install -g gatsby-cli # Install Gatsby globally 43 | npm install # Install dependencies 44 | npm run dev # Run the development server 45 | ``` 46 | 47 | ## 🎨 Customization 48 | 49 | The app separates its source and content – so you usually shouldn't have to dig 50 | into the JavaScript source to change things. The following points of 51 | customization are available: 52 | 53 | | Location | Description | 54 | | ------------------------- | ------------------------------------------------------ | 55 | | `meta.json` | General config settings, title, description etc. | 56 | | `theme.sass` | Color theme. | 57 | | `binder/requirements.txt` | Python requirements to install. | 58 | | `chapters` | The chapters, one Markdown file per chapter. | 59 | | `slides` | The slides, one Markdown file per slide deck. | 60 | | `static` | Static assets like images, will be copied to the root. | 61 | 62 | ### `meta.json` 63 | 64 | The following meta settings are available. **Note that you have to re-start 65 | Gatsby to see the changes if you're editing it while the server is running.** 66 | 67 | | Setting | Description | 68 | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | 69 | | `courseId` | Unique ID of the course. Will be used when saving completed exercises to the browser's local storage. | 70 | | `title` | The title of the course. | 71 | | `slogan` | Course slogan, displayed in the page title on the front page. | 72 | | `description` | Course description. Used for site meta and in footer. | 73 | | `bio` | Author bio. Used in the footer. | 74 | | `siteUrl` | URL of the deployed site (without trailing slash). | 75 | | `twitter` | Author twitter handle, used in Twitter cards meta. | 76 | | `fonts` | [Google Fonts](https://fonts.google.com) to load. Should be the font part of the URL in the embed string, e.g. `Lato:400,400i,700,700i`. | 77 | | `testTemplate` | Template used to validate the answers. `${solution}` will be replaced with the user code and `${test}` with the contents of the test file. | 78 | | `juniper.repo` | Repo to build on Binder in `user/repo` format. Usually the same as this repo. | 79 | | `juniper.branch` | Branch to build. Ideally not `master`, so the image is not rebuilt every time you push. | 80 | | `juniper.lang` | Code language for syntax highlighting. | 81 | | `juniper.kernelType` | The name of the kernel to use. | 82 | | `juniper.debug` | Logs additional debugging info to the console. | 83 | | `showProfileImage` | Whether to show the profile image in the footer. If `true`, a file `static/profile.jpg` needs to be available. | 84 | | `footerLinks` | List of objects with `"text"` and `"url"` to display as links in the footer. | 85 | | `theme` | Currently only used for the progressive web app, e.g. as the theme color on mobile. For the UI theme, edit `theme.sass`. | 86 | 87 | ### Static assets 88 | 89 | All files added to `/static` will become available at the root of the deployed 90 | site. So `/static/image.jpg` can be referenced in your course as `/image.jpg`. 91 | The following assets need to be available and can be customized: 92 | 93 | | File | Description | 94 | | ----------------- | -------------------------------------------------------- | 95 | | `icon.png` | Custom [favicon](https://en.wikipedia.org/wiki/Favicon). | 96 | | `logo.svg` | The course logo. | 97 | | `profile.jpg` | Photo or profile image. | 98 | | `social.jpg` | Social image, displayed in Twitter and Facebook cards. | 99 | | `icon_check.svg` | "Check" icon displayed on "Mark as completed" button. | 100 | | `icon_slides.svg` | Icon displayed in the corner of a slides exercise. | 101 | 102 | ## ✏️ Content 103 | 104 | ### File formats 105 | 106 | #### Chapters 107 | 108 | Chapters are placed in [`/chapters`](/chapters) and are Markdown files 109 | consisting of `` components. They'll be turned into pages, e.g. 110 | `/chapter1`. In their frontmatter block at the top of the file, they need to 111 | specify `type: chapter`, as well as the following meta: 112 | 113 | ```yaml 114 | --- 115 | title: The chapter title 116 | description: The chapter description 117 | prev: /chapter1 # exact path to previous chapter or null to not show a link 118 | next: /chapter3 # exact path to next chapter or null to not show a link 119 | id: 2 # unique identifier for chapter 120 | type: chapter # important: this creates a standalone page from the chapter 121 | --- 122 | 123 | ``` 124 | 125 | #### Slides 126 | 127 | Slides are placed in [`/slides`](/slides) and are markdown files consisting of 128 | slide content, separated by `---`. They need to specify the following 129 | frontmatter block at the top of the file: 130 | 131 | ```yaml 132 | --- 133 | type: slides 134 | --- 135 | 136 | ``` 137 | 138 | The **first and last slide** use a special layout and will display the headline 139 | in the center of the slide. **Speaker notes** (in this case, the script) can be 140 | added at the end of a slide, prefixed by `Notes:`. They'll then be shown on the 141 | right next to the slides. Here's an example slides file: 142 | 143 | ```markdown 144 | --- 145 | type: slides 146 | --- 147 | 148 | # Processing pipelines 149 | 150 | Notes: This is a slide deck about processing pipelines. 151 | 152 | --- 153 | 154 | # Next slide 155 | 156 | - Some bullet points here 157 | - And another bullet point 158 | 159 | An image located in /static 160 | ``` 161 | 162 | ### Custom Elements 163 | 164 | When using custom elements, make sure to place a newline between the 165 | opening/closing tags and the children. Otherwise, Markdown content may not 166 | render correctly. 167 | 168 | #### `` 169 | 170 | Container of a single exercise. 171 | 172 | | Argument | Type | Description | 173 | | ------------ | --------------- | -------------------------------------------------------------- | 174 | | `id` | number / string | Unique exercise ID within chapter. | 175 | | `title` | string | Exercise title. | 176 | | `type` | string | Optional type. `"slides"` makes container wider and adds icon. | 177 | | **children** | - | The contents of the exercise. | 178 | 179 | ```markdown 180 | 181 | 182 | Content goes here... 183 | 184 | 185 | ``` 186 | 187 | #### `` 188 | 189 | | Argument | Type | Description | 190 | | ------------ | --------------- | -------------------------------------------------------------------------------------------- | 191 | | `id` | number / string | Unique identifier of the code exercise. | 192 | | `source` | string | Name of the source file (without file extension). Defaults to `exc_${id}` if not set. | 193 | | `solution` | string | Name of the solution file (without file extension). Defaults to `solution_${id}` if not set. | 194 | | `test` | string | Name of the test file (without file extension). Defaults to `test_${id}` if not set. | 195 | | **children** | string | Optional hints displayed when the user clicks "Show hints". | 196 | 197 | ```markdown 198 | 199 | 200 | This is a hint! 201 | 202 | 203 | ``` 204 | 205 | #### `` 206 | 207 | Container to display slides interactively using Reveal.js and a Markdown file. 208 | 209 | | Argument | Type | Description | 210 | | -------- | ------ | --------------------------------------------- | 211 | | `source` | string | Name of slides file (without file extension). | 212 | 213 | ```markdown 214 | 215 | 216 | ``` 217 | 218 | #### `` 219 | 220 | Container for multiple-choice question. 221 | 222 | | Argument | Type | Description | 223 | | ------------ | --------------- | -------------------------------------------------------------------------------------------- | 224 | | `id` | string / number | Optional unique ID. Can be used if more than one choice question is present in one exercise. | 225 | | **children** | nodes | Only `` components for the options. | 226 | 227 | ```markdown 228 | 229 | 230 | You have selected option one! This is not good. 231 | Yay! 232 | 233 | 234 | ``` 235 | 236 | #### `` 237 | 238 | A multiple-choice option. 239 | 240 | | Argument | Type | Description | 241 | | ------------ | ------ | ---------------------------------------------------------------------------------------------- | 242 | | `text` | string | The option text to be displayed. Supports inline HTML. | 243 | | `correct` | string | `"true"` if the option is the correct answer. | 244 | | **children** | string | The text to be displayed if the option is selected (explaining why it's correct or incorrect). | 245 | 246 | ### Setting up Binder 247 | 248 | The [`requirements.txt`](binder/requirements.txt) in the repository defines the 249 | packages that are installed when building it with Binder. You can specify the 250 | binder settings like repo, branch and kernel type in the `"juniper"` section of 251 | the `meta.json`. I'd recommend running the very first build via the interface on 252 | the [Binder website](https://mybinder.org), as this gives you a detailed build 253 | log and feedback on whether everything worked as expected. Enter your repository 254 | URL, click "launch" and wait for it to install the dependencies and build the 255 | image. 256 | 257 | ![Binder](https://user-images.githubusercontent.com/13643239/39412757-a518d416-4c21-11e8-9dad-8b4cc14737bc.png) 258 | 259 | ### Adding tests 260 | 261 | To validate the code when the user hits "Submit", we're currently using a 262 | slightly hacky trick. Since the Python code is sent back to the kernel as a 263 | string, we can manipulate it and add tests – for example, exercise 264 | `exc_01_02_01.py` will be validated using `test_01_02_01.py` (if available). The 265 | user code and test are combined using a string template. At the moment, the 266 | `testTemplate` in the `meta.json` looks like this: 267 | 268 | ``` 269 | from wasabi import Printer 270 | __msg__ = Printer() 271 | __solution__ = """${solution}""" 272 | ${solution} 273 | 274 | ${test} 275 | try: 276 | test() 277 | except AssertionError as e: 278 | __msg__.fail(e) 279 | ``` 280 | 281 | If present, `${solution}` will be replaced with the string value of the 282 | submitted user code. In this case, we're inserting it twice: once as a string so 283 | we can check whether the submission includes something, and once as the code, so 284 | we can actually run it and check the objects it creates. `${test}` is replaced 285 | by the contents of the test file. It's also making 286 | [`wasabi`](https://github.com/ines/wasabi)'s printer available as `__msg__`, so 287 | we can easily print pretty messages in the tests. Finally, the `try`/`accept` 288 | block checks if the test function raises an `AssertionError` and if so, displays 289 | the error message. This also hides the full error traceback (which can easily 290 | leak the correct answers). 291 | 292 | A test file could then look like this: 293 | 294 | ```python 295 | def test(): 296 | assert "spacy.load" in __solution__, "Are you calling spacy.load?" 297 | assert nlp.meta["lang"] == "en", "Are you loading the correct model?" 298 | assert nlp.meta["name"] == "core_web_sm", "Are you loading the correct model?" 299 | assert "nlp(text)" in __solution__, "Are you processing the text correctly?" 300 | assert "print(doc.text)" in __solution__, "Are you printing the Doc's text?" 301 | 302 | __msg__.good( 303 | "Well done! Now that you've practiced loading models, let's look at " 304 | "some of their predictions." 305 | ) 306 | ``` 307 | 308 | The string answer is available as `__solution__`, and the test also has access 309 | to the solution code. 310 | 311 | --- 312 | 313 | For more details on how it all works behind the scenes, see 314 | [the original course repo](https://github.com/ines/spacy-course). 315 | -------------------------------------------------------------------------------- /binder/requirements.txt: -------------------------------------------------------------------------------- 1 | wasabi>=0.2.1,<1.1.0 2 | -------------------------------------------------------------------------------- /chapters/chapter1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Chapter 1: Getting started' 3 | description: 4 | 'This chapter will teach you about many cool things and introduce you to the 5 | most important concepts of the course.' 6 | prev: null 7 | next: /chapter2 8 | type: chapter 9 | id: 1 10 | --- 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Let's ask some questions about the slides. Whats the correct answer? 22 | 23 | 24 | 25 | 26 | This is not the correct answer. 27 | 28 | 29 | 30 | 31 | 32 | Good job! 33 | 34 | 35 | 36 | 37 | 38 | This is not correct either. 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | This is a code exercise. The content can be formatted in simple Markdown – so 48 | you can have **bold text**, `code` or [links](https://spacy.io) or lists, like 49 | the one for the instructions below. 50 | 51 | - These are instructions and they can have bullet points. 52 | - The code block below will look for the files `exc_01_03`, `solution_01_03` and 53 | `test_01_03` in `/exercises`. 54 | 55 | 56 | 57 | This is a hint. 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /chapters/chapter2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Chapter 2: More stuff' 3 | description: 4 | 'This chapter will teach you even more stuff and help you learn some new 5 | concepts.' 6 | prev: /chapter1 7 | next: null 8 | type: chapter 9 | id: 2 10 | --- 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | gatsby: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | working_dir: /app 8 | command: gatsby develop -H 0.0.0.0 9 | ports: 10 | - "8000:8000" 11 | volumes: 12 | - .:/app 13 | - /app/node_modules/ 14 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | # Docker file for running Gatsby without installing node version 10 or Gatsby. 2 | # Attribution: https://stackoverflow.com/questions/57405792/gatsby-not-rebuilding-whenever-mounted-code-changes 3 | # Hayley Boyce (kinda not really), February 6th, 2020 4 | 5 | FROM node:10 6 | 7 | # Add the package.json file and build the node_modules folder 8 | WORKDIR /app 9 | COPY ./package*.json ./ 10 | RUN mkdir node_modules 11 | RUN npm install --g gatsby-cli 12 | RUN npm install -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /docs/img/chapter_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/218183a0845ec89cd4d27e8f350ccdc595855d87/docs/img/chapter_layout.png -------------------------------------------------------------------------------- /docs/img/chapters_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/218183a0845ec89cd4d27e8f350ccdc595855d87/docs/img/chapters_img.png -------------------------------------------------------------------------------- /docs/img/julia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/218183a0845ec89cd4d27e8f350ccdc595855d87/docs/img/julia.png -------------------------------------------------------------------------------- /docs/img/multi-q-exercise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/218183a0845ec89cd4d27e8f350ccdc595855d87/docs/img/multi-q-exercise.png -------------------------------------------------------------------------------- /docs/img/naome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/218183a0845ec89cd4d27e8f350ccdc595855d87/docs/img/naome.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # From Zero to ICSP (Ines Course Starter - Python) 2 | 3 | Table of Contents 4 | ================= 5 | 6 | * [What to Expect](#what-to-expect) 7 | * [Creating Your Website Without Installing Dependencies (Using Docker Compose)](#creating-your-website-without-installing-dependencies-using-docker-compose) 8 | * [Creating your website by Installing Dependencies](#creating-your-website-by-installing-dependencies) 9 | * [Install Node](#install-node) 10 | * [Install Gatsby](#install-gatsby) 11 | * [Clone or Install the Repository](#clone-or-install-the-repository) 12 | * [Running on local Server](#running-on-local-server) 13 | * [Repository Structure](#repository-structure) 14 | * [Customization](#customization) 15 | * [Course Homepage Information](#course-homepage-information) 16 | * [theme.sass](#themesass) 17 | * [Introduction on Homepage](#introduction-on-homepage) 18 | * [Contents](#contents) 19 | * [Chapters](#chapters) 20 | * [Slides](#slides) 21 | * [Embedding Video and Audio](#embedding-video-and-audio) 22 | * [Multiple choice questions:](#multiple-choice-questions) 23 | * [🔆You can have several questions in one exercise container](#you-can-have-several-questions-in-one-exercise-container) 24 | * [Codeblock Exercises:](#codeblock-exercises) 25 | * [Let's talk about importing functions!](#lets-talk-about-importing-functions) 26 | * [binder/requirements.txt](#binderrequirementstxt) 27 | * [static folder](#static-folder) 28 | 29 | 30 | Course Starter python is a starter repo based on the course framework [Ines Montani](https://ines.io/) developed for her [online open-source spaCy course](https://course.spacy.io/). Since creating this framework in April 2019, it has since become a useful tool and platform for data scientists and developers alike to implement their courses in a manner similar to other popular online Data science educational platforms. 31 | 32 | This course gives the developer the versatility of a lecture slide-type informational piece followed by multiple-choice questions and coding exercises equipped with verification of the students' submitted answers. 33 | 34 | Ines Montani has created this framework using Gatsby and Reveal.js in the front-end and Binder and Docker in the back-end. This framework was made possible thanks to the useful JavaScript library [`Juniper`](https://github.com/ines/juniper) Ines created which can add interactive, editable and runnable code snippets to websites. 35 | 36 | ***Please Note:*** 37 | This project is under active development and there are possibilities of changes. If you would like to contribute or point out corrections, please create a new issue addressing your concern, suggestions or contribution. 38 | [![](https://user-images.githubusercontent.com/13643239/56341448-68fe9380-61b5-11e9-816f-5c71ae71b94f.png)](https://course-starter-python.netlify.com) 39 | 40 | 🔆 **Hot Tips:** This symbol will be used in this guide for helpful tips/recommendations and suggestions when building your course. 41 | 42 | ⚠️ **Warning:** This symbol will give you preventative tips to avoid debugging or issues that I ran into. 43 | 44 | 45 | ## What to Expect 46 | 47 | I hope that this thorough documentation will help you deploy, customize and troubleshoot your starter course. Ines provides some wonderful instructions in her `README.md` file, but I noticed there were a few notes I wanted to add to it for my colleagues and others attempting to make their course so that they can save time on troubleshooting. 48 | 49 | You will be working with different file types including `.md` (and potentially `.rmd`), `.json`, `.py` and `.txt`. 50 | You may need to know _some_ Html for additional customization, however by no means in-depth. 51 | 52 | This tutorial will describe the steps to create a complete initial "Starter Course" with zero customization. From here we will then change, edit and add files to complete your desired unique course. 53 | 54 | You can choose either to create your website by [installing the dependencies (Node and Gatsby)](#creating-your-website-by-installing-dependencies) or we have conveniently made a docker compose file available to avoid that. If you don't want to install the dependencies follow the steps [here](#creating-your-website-without-installing-dependencies-using-docker-compose) 55 | 56 | ## Creating Your Website Without Installing Dependencies (Using Docker Compose) 57 | 58 | 1. Clone this repo [starter course repo](https://github.com/UBC-MDS/course-starter-python) 59 | and locate yourself to the root of the repo where the `Dockerfile` is located. 60 | 61 | 2. Run the following command. It should take about 1 or 2 to run. 62 | ``` 63 | docker-compose up 64 | ``` 65 | You will know when it is done when you see: 66 | ``` 67 | You can now view course-starter-python in the browser. 68 | ``` 69 | 70 | 3. Go to your favourite web browser and type this in the search bar: 71 | [http://0.0.0.0:8000/](http://0.0.0.0:8000/) 72 | 73 | 74 | This should be the beginning of a functioning starter-course! 75 | 76 | 77 | Now that you have a website that is deploying on your local server, we can now begin the steps to customize it to your own taste. 78 | 79 | 80 | ## Creating your website by Installing Dependencies 81 | 82 | ### Install Node 83 | 84 | **Mac instructions** 85 | 86 | _Make sure that you have homebrew installed in order to download `Node`_ 87 | 88 | The most important part of this installation is making sure that you are running some version of 10. 89 | 90 | 91 | Check if you have `Node` installed already by using the command : 92 | ``` 93 | node --version 94 | ``` 95 | 96 | If that produces an error then you can simply download version 10 with the following command: 97 | 98 | ``` 99 | brew install node@10 100 | ``` 101 | 102 | If it's a version other than 10, you will **need** to downgrade/upgrade to version 10 - or Gatsby will not be able to start a development server or build a page. 103 | 104 | To change to version 10, follow the following commands: 105 | 106 | ``` 107 | brew search node 108 | ``` 109 | This will give you an output similar to this: 110 | 111 | ``` 112 | ==> Formulae 113 | libbitcoin-node node-build node@12 nodeenv 114 | llnode node-sass node_exporter nodenv 115 | node ✓ node@10 nodebrew 116 | ``` 117 | 118 | Next, you will want to install version 10 with the command: 119 | 120 | ``` 121 | brew install node@10 122 | ``` 123 | 124 | if the checkmark is currently on `node` we then unlink node from its current version first using: 125 | 126 | ``` 127 | brew unlink node 128 | ``` 129 | 130 | Everyone will need to link version 10 that was just installed: 131 | 132 | ``` 133 | brew link node@10 134 | ``` 135 | 136 | This will likely need to be forced and thus will require: 137 | 138 | ``` 139 | brew link --force --overwrite node@10 140 | ``` 141 | 142 | You may also be prompted to specify that you need to have node@10 first in your PATH so you should run the command below before attempting force linking node@10 (the command above) again: 143 | 144 | ``` 145 | echo 'export PATH="/usr/local/opt/node@10/bin:$PATH"' >> ~/.bash_profile 146 | ``` 147 | 148 | Next check again what version you are running to confirm that it is a version of 10. 149 | there is a possibility that an error will be produced so you can either permanently set your 150 | ``` 151 | node --version 152 | ``` 153 | this should output the following: 154 | 155 | ``` 156 | v10.13.0 157 | ``` 158 | 159 | Now that we have this done, Gatsby's installation and building process should be much easier. 160 | 161 | ### Install Gatsby 162 | 163 | This should a single command to complete this and will install Gatsby globally on your computer. 164 | 165 | ``` 166 | npm install -g gatsby-cli 167 | ``` 168 | ***⚠️ Warning: Do not update your dependencies here.*** 169 | 170 | ### Clone or Install the Repository 171 | 172 | There are 2 methods in which this step can be done. 173 | 174 | a) Simply clone the [starter course repo](https://github.com/ines/course-starter-python) and initialize it as a [GitHub repository](https://help.github.com/en/github/importing-your-projects-to-github/adding-an-existing-project-to-github-using-the-command-line) 175 | b) [Import](https://github.com/new/import) and install this repo 176 | 177 | Make sure that you ***merge all the changes on the other branches to the master one** if you do not create a pull request for `electron` and `feature/deep-links` branches your course will not successfully deploy.* 178 | 179 | Once you have done this you will need to locate yourself to the root of the repo. 180 | 181 | ### Running on local Server 182 | 183 | Next, we must install all relevant dependencies by running the following: 184 | ``` 185 | npm install 186 | ``` 187 | 188 | ***⚠️ Warning: You will be prompted to `run "npm audit fix" to fix them`. 189 | I do not recommend doing this as it will burn your site down.*** 190 | 191 | The output below will still build your course: 192 | ``` 193 | found 572 vulnerabilities (4 low, 4 moderate, 564 high) 194 | ``` 195 | 196 | and finally, to build the site on your local: 197 | 198 | ``` 199 | npm run dev 200 | ``` 201 | 202 | Delivering this as an output (copy and paste this address into any browser) : 203 | ``` 204 | You can now view course-starter-python in the browser. 205 | 206 | http://localhost:8000/ 207 | ``` 208 | **Quick link:** [http://localhost:8000/](http://localhost:8000/) 209 | 210 | This should be the beginning of a functioning starter-course! 211 | 212 | 213 | Now that you have a website that is deploying on your local server, we can now begin the steps to customize it to your own taste. 214 | 215 | 216 | 217 | ## Repository Structure 218 | 219 | See the architecture below. Make sure to add this to your path when calling them in your `md` file. Some of these files will be explained in further detail depending on if customization or additions is required. 220 | 221 | ``` 222 | course-starter-python 223 | ├── .gitignore # Files you change on your local that you do not want to track changes for or commit to the repo. 224 | ├── .prettierrc # Adds consistency to coding style. 225 | ├── LICENSE # Terms able to use this platform 226 | ├── README.md # Documentation and Description 227 | ├── docker-compose.yml # This and the dockerfile and needed to create a container used to install Gatsby and node10 228 | ├── dockerfile # See above 229 | ├── gatsby-browser.js 230 | ├── gatsby-config.js 231 | ├── gatsby-node.js 232 | ├── main.js 233 | ├── meta.json # Add necessary customization such as descriptions bio and branch needed to make binder from 234 | ├── package-lock.json 235 | ├── package.json 236 | ├── theme.sass # Can be customizable to change fonts style and size and website colours and font 237 | │ │ 238 | ├── binder 239 | | └── requirements.txt # A file containing all the packages needed for the coding exercises 240 | │ │ 241 | ├── chapter # n = the number of modules/chapters you want. 242 | | ├── module0.md 243 | | ├── module1.md 244 | | ├── ... 245 | | └── moduleN.md 246 | │ │ 247 | ├── data # Store exercise datafiles here 248 | └── exercise-data.csv 249 | │ │ 250 | ├── exercises # This file will contain all the coding exercise scripts. 251 | | ├── exercise_01.py 252 | | ├── solution_01.py 253 | | ├── test_01.py 254 | | ├── function.py 255 | | └── price_linearanalysis3.png 256 | │ │ 257 | ├── slides # This is where the slide decks live 258 | | ├── module0_00.md 259 | | ├── ... 260 | | └── moduleN_nn.md 261 | │ │ 262 | ├── src # Don't want to go too much into this 263 | | ├── markdown.js 264 | | ├── context.js 265 | | ├── components 266 | | | ├── button.js 267 | | | ├── choice.js 268 | | | ├── code.js 269 | | | ├── exercise.js 270 | | | ├── hint.js 271 | | | ├── juniper.js 272 | | | ├── layout.js 273 | | | ├── link.js 274 | | | ├── seo.js 275 | | | ├── slides.js 276 | | | └── typography.js 277 | | | | 278 | | ├── pages 279 | | | └── index.js 280 | | | | 281 | | ├── styles 282 | | | ├── button.module.sass 283 | | | ├── choice.module.sass 284 | | | ├── code.module.sass 285 | | | ├── exercise.module.sass 286 | | | ├── hint.module.sass 287 | | | ├── index.module.sass 288 | | | ├── index.sass 289 | | | ├── layout.module.sass 290 | | | ├── link.module.sass 291 | | | ├── reveal.css 292 | | | ├── slides.module.sass 293 | | | └── typography.module.sass 294 | | | | 295 | └── templates 296 | └── chapter.js 297 | | | | 298 | └── static # This is where most of your media will live, be it for slides, or anything else. 299 | ├── icon.png 300 | ├── icon_check.svg 301 | ├── icon_slides.svg 302 | ├── logo.svg 303 | ├── profile.jpg 304 | └── social.jpg 305 | 306 | 307 | ``` 308 | 309 | ## Customization 310 | 311 | There is a lot of different areas to make your site unique but below we are going to edit the files systematically. 312 | 313 | 314 | ### Course Homepage Information 315 | 316 | Here is where we will be changing all the homepage information including Course Name, "About This Course", "About Me", Website and Source. All of these factors are edited in the `meta.json` file located at the root of the repo. Ines has provided [a detailed discription](https://github.com/ines/course-starter-python#metajson) of what each component is responsible for. I am simply going to add some points that could be considered helpful when navigating in these documents 317 | 318 | | Setting | Additional Notes: | 319 | | -------------------- | ----------------- | 320 | | `courseId` | Ines does not have this parameter in her spacy course, however, deleting this will not let the course function properly so not having this setting is not an option unless you want to explore what makes her spacy course repo different than her course-starter repo.| 321 | | `slogan` | This will show up once you deploy your site and it will be shown in the image of the link that you send. | 322 | |`juniper.repo` | Make sure you insert your GitHub repository path ex: GitHub-login/repository-name | 323 | | `juniper.branch` | We will address this further when building a binder but note that the branch here specified is called binder. That means that we will need to edit the `requirements.txt` file and push it to the binder branch| 324 | 325 | For guidance on the other settings refer to [Ines Montani's Documentation](https://github.com/ines/course-starter-python#metajson). 326 | 327 | ### `theme.sass` 328 | 329 | This is where you can change certain design elements of the course including font size, style and colour, overall theme colour, button colour. 330 | 331 | 332 | ### Introduction on Homepage 333 | 334 | _It's important to attribute Noam Ross and Julia Silge's courses for this section as they are responsible for the code pasted below._ 335 | 336 | Unlike Ines's [Spacy Course](https://course.spacy.io/), you may want an introduction similar to what [Julia Silge](https://supervised-ml-course.netlify.com/) and [Noam Ross](https://noamross.github.io/gams-in-r-course/) did for their courses. 337 | 338 | They introduced their courses with a summary and course description. 339 | 340 | | ![alt-text-1](img/julia.png) | ![alt-text-2](img/naome.png) | 341 | |:---:|:---:| 342 | | Julia Silge's course front page | Noam Ross's course front page| 343 | 344 | This can be done by doing the following: 345 | 346 | - Navigate into the `src/pages/` and open `index.js` 347 | 348 | You will be adding a new `
` (Html code) under `` and between the following two lines shown below : 349 | ``` 350 | 351 | # HERE 352 | {chapters.map(({ slug, title, description }) => ( 353 | ``` 354 | Here is an example of the code you can add. 355 | 356 | ``` 357 |
358 |

INSERT CATCHY TAG LINE HERE

359 |
360 |

361 | FILLER WORDS HERE. WHAT IS YOUR COURSE ABOUT? DINOSAURS? NEURAL NETS? HOW TO SURVIVE EVENTS WITH THE INLAWS? WRITE IT HERE! 362 |

363 |
364 |
365 | ``` 366 | 367 | Since we are adding new class names will are going to need to edit the document that formats the class name. This can be found in `src/styles/index.module.sass`. 368 | 369 | You will need to paste the new classes as follows below into the document. 370 | 371 | ``` 372 | .subtitle 373 | font-family: var(--font-display) 374 | width: 600px 375 | height: auto 376 | max-width: 100% 377 | margin: 0 auto 1rem 378 | display: block 379 | text-align: center 380 | 381 | .introduction 382 | width: var(--width-container) 383 | max-width: 100% 384 | padding: 1rem 385 | margin: 0 auto 386 | display: block 387 | text-align: left 388 | ``` 389 | 390 | If you want to play with the measurements this is a welcomed opportunity to customize your course further. 391 | 392 | This can be done by doing the following: 393 | 394 | - Navigate into the `src/pages/index.js` 395 | 396 | You will be adding a new `
` (Html code) under `` and between the following two lines shown below: 397 | ``` 398 | 399 | # HERE 400 | {chapters.map(({ slug, title, description }) => ( 401 | ``` 402 | Here is an example of the code you can add. 403 | 404 | ``` 405 |
406 |

INSERT CATCHY TAG LINE HERE

407 |
408 |

409 | FILLER WORDS HERE. WHAT IS YOUR COURSE ABOUT? DINOSAURS? NEURAL NETS? HOW TO SURVIVE EVENTS WITH THE INLAWS? WRITE IT HERE! 410 |

411 |
412 |
413 | ``` 414 | 415 | Since we are adding new class names will are going to need to edit the document that formats the class name. This can be found in `src/styles/index.module.sass`. 416 | 417 | You will need to paste the new classes as follows below into the document. 418 | 419 | ``` 420 | .subtitle 421 | font-family: var(--font-display) 422 | width: 600px 423 | height: auto 424 | max-width: 100% 425 | margin: 0 auto 1rem 426 | display: block 427 | text-align: center 428 | 429 | .introduction 430 | width: var(--width-container) 431 | max-width: 100% 432 | padding: 1rem 433 | margin: 0 auto 434 | display: block 435 | text-align: left 436 | ``` 437 | 438 | If you want to play with the measurements this is a welcomed opportunity to customize your course further. 439 | 440 | 441 | ## Contents 442 | 443 | This is where the majority of your course lies. 444 | 445 | Ines Montani [discusses in detail each section](https://github.com/UBC-MDS/course-starter-python#%EF%B8%8F-content), however, there are a few little details I want to emphasize that could help as you create this site. 446 | 447 | ### Chapters 448 | 449 | These are the files that make up the topics of your course and will be displayed on your course site as below: 450 | 451 | ![](img/chapters_img.png) 452 | Source: Ines Montani from https://course.spacy.io 453 | 454 |
455 | 456 | Each `chapter.md` file will need this YAML specification that Ines explains: 457 | 458 | ``` 459 | --- 460 | title: The chapter title 461 | description: The chapter description 462 | prev: /chapter1 # exact path to previous chapter or null to not show a link 463 | next: /chapter3 # exact path to next chapter or null to not show a link 464 | id: 2 # unique identifier for chapter 465 | type: chapter # important: this creates a standalone page from the chapter 466 | --- 467 | ``` 468 | Here are some additional comments: 469 | 470 | 1. Make sure each `id` is unique or you may have some issues with some modules not showing up. 471 | 2. Take care specifying the correct `prev` and `next` otherwise it could damage the flow of your material. 472 | 3. You don't need to have the website extension of your course labeled as "chapters" if you wish to have your link extensions named something other than "chapter" in the URL, you can change the file names to `module`, `topic`, `lecture` or anything else followed by the number. Do not change the folder name and do not change the `type` in the YAML. 473 | 474 | Each `chapter.md` file will contain the code of what that chapter will look like. Specifically: 475 | 476 | ![](img/chapter_layout.png) 477 | Source: Ines Montani from https://course.spacy.io/chapter1 478 | 479 |
480 | 481 | Now that we have a chapter.md file with a completed YAML, let's add the course content. 482 | 483 | Each numbered container displayed in the image above corresponds to an `exercise`. Each exercise needs a unique id and specified with a title. 484 | 485 | ``` 486 | 487 | 488 | something here 489 | 490 | 491 | ``` 492 | 493 | These exercises can in the form of different activities as well: 494 | 1. Slides: Lecture material and content 495 | 2. Multiple choice questions: An opportunity for students to test themselves on the material they just learned. 496 | 3. Code block exercises: An opportunity for students to test their coding skills 497 | 498 | 499 | ### Slides 500 | 501 | To makes your exercise a slide deck exercises you will need to do the write the following in your `chapter.md` file: 502 | 503 | ``` 504 | 505 | 506 | 507 | 508 | 509 | 510 | ``` 511 | Notice that we specify slides using `type="slides"` argument in the exercise container. 512 | 513 | You'll also notice we are calling a source file to display our slides. These slides are stored in the `slides` folder. 514 | 515 | [Ines explains](https://github.com/ines/course-starter-python#slides) how your slide markdown document should be structured. 516 | 517 | ***⚠️ Warning: Be wary of trailing spaces 😵😱!! 518 | Although ```---``` may appear to be the same as ```--- ``` they are not and any information placed after the latter will break your slides.*** 519 | 520 | #### Embedding Video and Audio 521 | 522 | If you are hoping to make your course particularly engaging, you may want to add videos or audio files to your slides (or questions even). 523 | This can be achieved with the following code: 524 | 525 | **Video:** 526 | ``` 527 | 528 | 532 | ``` 533 | _The video size should now respond to the browser size adjustment. `video-file-name.mp4` should be living in the `static` folder._ 534 | 535 | 536 | **Audio:** 537 | ``` 538 | 539 | 542 | ``` 543 | _`audio-file-name.mp3` should be living in the `static` folder._ 544 | 545 | ### Multiple choice questions: 546 | 547 | In a similar style to slides you will have code for questions that looks like the following: 548 | 549 | ``` 550 | 551 | 552 | Insert questions here. 553 | 554 | 555 | 556 | 557 | Great job! 558 | 559 | 560 | 561 | 562 | 563 | Try again! This is incorrect. 564 | 565 | 566 | 567 | 568 | ``` 569 | - Your question text will live in the ``. 570 | - You can then specify the solution options that correspond to this question using ``. 571 | - Each option will live in ``. If you want to specify a solution option as correct you can give it an argument `correct="true"` otherwise if it's wrong, no argument is needed. ***🔆You can have multiple right answers in a question.*** 572 | 573 | 574 | ***⚠️ Warning: You must have padding (empty lines) above and below your question text as well as your answer feedback text as shown above.*** 575 | 576 | #### 🔆You can have several questions in one exercise container 577 | 578 | ![](img/multi-q-exercise.png) 579 | Source: Hayley Boyce @ UBC MDS from https://mcl-dsci-571-intro-machine-learning.netlify.com/module1 580 | 581 |
582 | 583 | The key to having several questions in one exercise container is to give `` an id. 584 | ``` 585 | exercise id="18" title= "Multiple questions in one exercise"> 586 | 587 | Question 1 here. 588 | 589 | 590 | 591 | 592 | Incorrect. 593 | 594 | 595 | 596 | 597 | 598 | Great! 599 | 600 | 601 | 602 | 603 | 604 | Question 2 here 605 | 606 | 607 | 608 | 609 | Incorrect. 610 | 611 | 612 | 613 | 614 | 615 | Great! 616 | 617 | 618 | 619 | 620 | 621 |
622 | ``` 623 | 624 | ### Codeblock Exercises: 625 | 626 | In a fashion similar to slides we are going to reference a source file that corresponds to the coding activity. 627 | your code in `chapter.md` will look like this: 628 | 629 | ``` 630 | 631 | 632 | 633 | You can type your question instructions here 634 | 635 | 636 | 637 | 638 | In this space is where you will write any coding or question hints 639 | 640 | 641 | 642 | 643 | ``` 644 | 645 | This is where things can get a bit tricky. Each coding exercise will require 3 files: 646 | 647 | - `exc_coding-question.py` -> The code displayed to the student that they will have to fill in 648 | - `solution_coding-question.py` -> the expected coding solution 649 | - `test_coding-question.py` -> Tests to see if the input done by the student were correct (output when the student clicks "submit") 650 | 651 | These are all saved in the `exercises` folder. 652 | 653 | When we want to specify this exercise in the `chapter.md` file in the exercise container, we must only write `id = coding-question`. 654 | 655 | Ines has explained more of this [here](https://github.com/ines/course-starter-python#codeblock). 656 | 657 | ***🔆You can also have multiple code blocks in a single exercise container.*** 658 | 659 | Python code is written in regular scripts while importing packages per usual. 660 | 661 | ***🔆Remember that you are located at the root of the repo and you will have to reflect that in the path to the file. E.g. data from the data folder.*** 662 | 663 | #### Let's talk about importing functions! 664 | 665 | If you want to import a function you made, the script can be stored in the `exercises` folder. However, since you are running everything from the root of the directory you must locate yourself into the `exercises` folder. You can do this with the following code. 666 | ``` 667 | import sys 668 | sys.path.insert(0, 'exercises/') 669 | ``` 670 | you can then import as normal. 671 | 672 | #### `binder/requirements.txt` 673 | 674 | Remember back in `meta.json` we discussed the argument `juniper.branch` briefly? Code block exercises are where this comes to play. 675 | 676 | Any imported packages need to be added to `binder/requirements.txt` and since we specified in juniper `"branch": "binder"` this is the branch where we will need to push changes to this document. 677 | - Add your packages and if need be the versions to the `requirements.txt`. 678 | - Build your [binder](https://mybinder.org/) and refer to [Ines's explanation](https://github.com/ines/spacy-course#setting-up-binder) for more. 679 | - ⚠️ When you build your binder make sure you specify `Git branch, tag, or commit` as `binder` since that is the branch where you will be adjusting the `requirements.txt`. 680 | 681 | 682 | ### `static` folder 683 | 684 | The `static` file is where any additional images, videos and audio files need for the questions or slides part of your course are stored. 685 | 686 | I find it particularly useful to create additional files in here to address the different chapters you will be making for added clarity and organization. aka I add a folder for each chapter/module and save the media files in its corresponding folder. 687 | ex: 688 | ``` 689 | ... 690 | └── static # This is where most of your media will live, be it for slides, or anything else. 691 | ├── icon.png 692 | ├── icon_check.svg 693 | ├── icon_slides.svg 694 | ├── logo.svg 695 | ├── profile.jpg 696 | ├── social.jpg 697 | ├── module1 698 | ├── audio.mp3 699 | ├── img.png 700 | └── video.mp4 701 | ├── .... 702 | └── moduleN 703 | ├── audio_n.mp3 704 | ├── img_n.jpg 705 | └── video_n.mp4 706 | ``` 707 | The required images are all specified in Ines' documentation in her [`README.md` here](https://github.com/UBC-MDS/course-starter-python#static-assets). 708 | 709 | -------------------------------------------------------------------------------- /exercises/bookquotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin.", 4 | { "author": "Franz Kafka", "book": "Metamorphosis" } 5 | ], 6 | [ 7 | "I know not all that may be coming, but be it what it will, I'll go to it laughing.", 8 | { "author": "Herman Melville", "book": "Moby-Dick or, The Whale" } 9 | ], 10 | [ 11 | "It was the best of times, it was the worst of times.", 12 | { "author": "Charles Dickens", "book": "A Tale of Two Cities" } 13 | ], 14 | [ 15 | "The only people for me are the mad ones, the ones who are mad to live, mad to talk, mad to be saved, desirous of everything at the same time, the ones who never yawn or say a commonplace thing, but burn, burn, burn like fabulous yellow roman candles exploding like spiders across the stars.", 16 | { "author": "Jack Kerouac", "book": "On the Road" } 17 | ], 18 | [ 19 | "It was a bright cold day in April, and the clocks were striking thirteen.", 20 | { "author": "George Orwell", "book": "1984" } 21 | ], 22 | [ 23 | "Nowadays people know the price of everything and the value of nothing.", 24 | { "author": "Oscar Wilde", "book": "The Picture Of Dorian Gray" } 25 | ] 26 | ] 27 | -------------------------------------------------------------------------------- /exercises/exc_01_03.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | # This code will run relative to the root of the repo, so we can load files 4 | with open("exercises/bookquotes.json") as f: 5 | DATA = json.loads(f.read()) 6 | 7 | # Print the first record in the DATA 8 | print(___[____]) 9 | 10 | # Assign the length of DATA to some_var 11 | some_var = ___ 12 | -------------------------------------------------------------------------------- /exercises/solution_01_03.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | # This code will run relative to the root of the repo, so we can load files 4 | with open("exercises/bookquotes.json") as f: 5 | DATA = json.loads(f.read()) 6 | 7 | # Print the first record in the DATA 8 | print(DATA[0]) 9 | 10 | # Assign the length of DATA to some_var 11 | some_var = len(DATA) 12 | -------------------------------------------------------------------------------- /exercises/test_01_03.py: -------------------------------------------------------------------------------- 1 | def test(): 2 | # Here we can either check objects created in the solution code, or the 3 | # string value of the solution, available as __solution__. A helper for 4 | # printing formatted messages is available as __msg__. See the testTemplate 5 | # in the meta.json for details. 6 | 7 | # If an assertion fails, the message will be displayed 8 | assert "print(DATA[0])" in __solution__, "Are you printing the first record?" 9 | assert some_var == len(DATA), "Are you getting the correct length?" 10 | 11 | __msg__.good("Well done!") 12 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import python from 'codemirror/mode/python/python' // eslint-disable-line no-unused-vars 2 | 3 | // This doesn't have to be here – but if we do import Juniper here, it's already 4 | // preloaded and cached when we dynamically import it in code.js. 5 | import Juniper from './src/components/juniper' // eslint-disable-line no-unused-vars 6 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const meta = require('./meta.json') 2 | const autoprefixer = require('autoprefixer') 3 | 4 | module.exports = { 5 | siteMetadata: meta, 6 | plugins: [ 7 | { 8 | resolve: `gatsby-plugin-sass`, 9 | options: { 10 | indentedSyntax: true, 11 | postCssPlugins: [autoprefixer()], 12 | cssLoaderOptions: { 13 | localIdentName: 14 | process.env.NODE_ENV == 'development' 15 | ? '[name]-[local]-[hash:8]' 16 | : '[hash:8]', 17 | }, 18 | }, 19 | }, 20 | `gatsby-plugin-react-helmet`, 21 | { 22 | resolve: `gatsby-source-filesystem`, 23 | options: { 24 | name: `chapters`, 25 | path: `${__dirname}/chapters`, 26 | }, 27 | }, 28 | { 29 | resolve: `gatsby-source-filesystem`, 30 | options: { 31 | name: `slides`, 32 | path: `${__dirname}/slides`, 33 | }, 34 | }, 35 | { 36 | resolve: `gatsby-source-filesystem`, 37 | options: { 38 | name: `exercises`, 39 | path: `${__dirname}/exercises`, 40 | }, 41 | }, 42 | { 43 | resolve: 'gatsby-plugin-react-svg', 44 | options: { 45 | rule: { 46 | include: /static/, 47 | }, 48 | }, 49 | }, 50 | { 51 | resolve: `gatsby-transformer-remark`, 52 | options: { 53 | plugins: [ 54 | `gatsby-remark-copy-linked-files`, 55 | { 56 | resolve: `gatsby-remark-prismjs`, 57 | options: { 58 | noInlineHighlight: true, 59 | }, 60 | }, 61 | { 62 | resolve: `gatsby-remark-smartypants`, 63 | options: { 64 | dashes: 'oldschool', 65 | }, 66 | }, 67 | { 68 | resolve: `gatsby-remark-images`, 69 | options: { 70 | maxWidth: 790, 71 | linkImagesToOriginal: true, 72 | sizeByPixelDensity: false, 73 | showCaptions: true, 74 | quality: 80, 75 | withWebp: { quality: 80 }, 76 | }, 77 | }, 78 | `gatsby-remark-unwrap-images`, 79 | ], 80 | }, 81 | }, 82 | `gatsby-transformer-sharp`, 83 | `gatsby-plugin-sharp`, 84 | `gatsby-plugin-sitemap`, 85 | { 86 | resolve: `gatsby-plugin-manifest`, 87 | options: { 88 | name: meta.title, 89 | short_name: meta.title, 90 | start_url: `/`, 91 | background_color: meta.theme, 92 | theme_color: meta.theme, 93 | display: `minimal-ui`, 94 | icon: `static/icon.png`, 95 | }, 96 | }, 97 | `gatsby-plugin-offline`, 98 | ], 99 | } 100 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { createFilePath } = require('gatsby-source-filesystem') 3 | 4 | const chapterTemplate = path.resolve('src/templates/chapter.js') 5 | 6 | function replacePath(pagePath) { 7 | return pagePath === `/` ? pagePath : pagePath.replace(/\/$/, ``) 8 | } 9 | 10 | async function onCreateNode({ 11 | node, 12 | actions, 13 | getNode, 14 | loadNodeContent, 15 | createNodeId, 16 | createContentDigest, 17 | }) { 18 | const { createNodeField, createNode, createParentChildLink } = actions 19 | if (node.internal.type === 'MarkdownRemark') { 20 | const slug = createFilePath({ node, getNode, basePath: 'chapters', trailingSlash: false }) 21 | createNodeField({ name: 'slug', node, value: slug }) 22 | } else if (node.extension === 'py') { 23 | // Load the contents of the Python file and make it available via GraphQL 24 | // https://www.gatsbyjs.org/docs/creating-a-transformer-plugin/ 25 | const content = await loadNodeContent(node) 26 | const contentDigest = createContentDigest(content) 27 | const id = createNodeId(`${node.id}-code`) 28 | const internal = { type: 'Code', contentDigest } 29 | const codeNode = { 30 | id, 31 | parent: node.id, 32 | children: [], 33 | code: content, 34 | name: node.name, 35 | internal, 36 | } 37 | createNode(codeNode) 38 | createParentChildLink({ parent: node, child: codeNode }) 39 | } 40 | } 41 | 42 | exports.onCreateNode = onCreateNode 43 | 44 | exports.createPages = ({ actions, graphql }) => { 45 | const { createPage } = actions 46 | return graphql(` 47 | { 48 | allMarkdownRemark { 49 | edges { 50 | node { 51 | frontmatter { 52 | title 53 | type 54 | } 55 | fields { 56 | slug 57 | } 58 | } 59 | } 60 | } 61 | } 62 | `).then(result => { 63 | if (result.errors) { 64 | return Promise.reject(result.errors) 65 | } 66 | const posts = result.data.allMarkdownRemark.edges.filter( 67 | ({ node }) => node.frontmatter.type == 'chapter' 68 | ) 69 | posts.forEach(({ node }) => { 70 | createPage({ 71 | path: replacePath(node.fields.slug), 72 | component: chapterTemplate, 73 | context: { slug: node.fields.slug }, 74 | }) 75 | }) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "courseId": "course-starter-python", 3 | "title": "My cool online course", 4 | "slogan": "A free online course", 5 | "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique libero at est congue, sed vestibulum tortor laoreet. Aenean egestas massa non commodo consequat. Curabitur faucibus, sapien vitae euismod imperdiet, arcu erat semper urna, in accumsan sapien dui ac mi. Pellentesque felis lorem, semper nec velit nec, consectetur placerat enim.", 6 | "bio": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique libero at est congue, sed vestibulum tortor laoreet. Aenean egestas massa non commodo consequat. Curabitur faucibus, sapien vitae euismod imperdiet, arcu erat semper urna.", 7 | "siteUrl": "https://course-starter-python.netlify.com", 8 | "twitter": "spacy_io", 9 | "fonts": "IBM+Plex+Mono:500|IBM+Plex+Sans:700|Lato:400,400i,700,700i", 10 | "testTemplate": "from wasabi import Printer\n__msg__ = Printer()\n__solution__ = \"\"\"${solution}\"\"\"\n\n${solution}\n\n${test}\n\ntry:\n test()\nexcept AssertionError as e:\n __msg__.fail(e)", 11 | "juniper": { 12 | "repo": "ines/course-starter-python", 13 | "branch": "binder", 14 | "lang": "python", 15 | "kernelType": "python3", 16 | "debug": false 17 | }, 18 | "showProfileImage": true, 19 | "footerLinks": [ 20 | { "text": "Website", "url": "https://spacy.io" }, 21 | { "text": "Source", "url": "https://github.com/ines/course-starter-python" }, 22 | { "text": "Built with ♥", "url": "https://github.com/ines/course-starter-python" } 23 | ], 24 | "theme": "#de7878" 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "course-starter-python", 3 | "private": true, 4 | "description": "Starter package to build interactive Python courses", 5 | "version": "0.0.1", 6 | "author": "Ines Montani ", 7 | "dependencies": { 8 | "@illinois/react-use-local-storage": "^1.1.0", 9 | "@jupyterlab/outputarea": "^0.19.1", 10 | "@jupyterlab/rendermime": "^0.19.1", 11 | "@phosphor/widgets": "^1.6.0", 12 | "autoprefixer": "^9.4.7", 13 | "classnames": "^2.2.6", 14 | "codemirror": "^5.43.0", 15 | "gatsby": "^2.1.4", 16 | "gatsby-image": "^2.0.29", 17 | "gatsby-plugin-manifest": "^2.0.17", 18 | "gatsby-plugin-offline": "^2.0.23", 19 | "gatsby-plugin-react-helmet": "^3.0.6", 20 | "gatsby-plugin-react-svg": "^2.1.1", 21 | "gatsby-plugin-sass": "^2.0.10", 22 | "gatsby-plugin-sharp": "^2.0.29", 23 | "gatsby-plugin-sitemap": "^2.0.5", 24 | "gatsby-remark-copy-linked-files": "^2.0.9", 25 | "gatsby-remark-images": "^3.0.4", 26 | "gatsby-remark-prismjs": "^3.2.4", 27 | "gatsby-remark-smartypants": "^2.0.8", 28 | "gatsby-remark-unwrap-images": "^1.0.1", 29 | "gatsby-source-filesystem": "^2.0.20", 30 | "gatsby-transformer-remark": "^2.2.5", 31 | "gatsby-transformer-sharp": "^2.1.17", 32 | "juniper-js": "^0.1.0", 33 | "node-sass": "^4.11.0", 34 | "prismjs": "^1.15.0", 35 | "react": "^16.8.2", 36 | "react-dom": "^16.8.2", 37 | "react-helmet": "^5.2.0", 38 | "rehype-react": "^3.1.0", 39 | "remark-react": "^5.0.1", 40 | "reveal.js": "^3.8.0" 41 | }, 42 | "scripts": { 43 | "build": "gatsby build", 44 | "dev": "gatsby develop", 45 | "lint": "eslint **", 46 | "clear": "rm -rf .cache", 47 | "test": "echo \"Write tests! -> https://gatsby.app/unit-testing\"" 48 | }, 49 | "devDependencies": { 50 | "browser-monads": "^1.0.0", 51 | "prettier": "^1.16.4" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/ines/course-starter-python" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /slides/chapter1_01_introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: slides 3 | --- 4 | 5 | # Introduction 6 | 7 | Notes: Text at the end of a slide prefixed like this will be displayed as 8 | speaker notes on the side. Slides can be separated with a divider: ---. 9 | 10 | --- 11 | 12 | # This is a slide 13 | 14 | ```python 15 | # Print something 16 | print("Hello world") 17 | ``` 18 | 19 | ```out 20 | Hello world 21 | ``` 22 | 23 | - Slides can have code, bullet points, tables and pretty much all other Markdown 24 | elements. 25 | - This is another bullet point. 26 | 27 | This image is in /static 28 | 29 | Notes: Some more notes go here 30 | 31 | --- 32 | 33 | # Let's practice! 34 | 35 | Notes: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique 36 | libero at est congue, sed vestibulum tortor laoreet. Aenean egestas massa non 37 | commodo consequat. Curabitur faucibus, sapien vitae euismod imperdiet, arcu erat 38 | semper urna, in accumsan sapien dui ac mi. Pellentesque felis lorem, semper nec 39 | velit nec, consectetur placerat enim. 40 | -------------------------------------------------------------------------------- /src/components/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | 4 | import IconCheck from '../../static/icon_check.svg' 5 | import classes from '../styles/button.module.sass' 6 | 7 | export const Button = ({ Component = 'button', children, onClick, variant, small, className }) => { 8 | const buttonClassNames = classNames(classes.root, className, { 9 | [classes.primary]: variant === 'primary', 10 | [classes.secondary]: variant === 'secondary', 11 | [classes.small]: !!small, 12 | }) 13 | return ( 14 | 15 | {children} 16 | 17 | ) 18 | } 19 | 20 | export const CompleteButton = ({ completed, toggleComplete, small = true }) => { 21 | const buttonClassNames = classNames({ 22 | [classes.completeInactive]: !completed, 23 | [classes.completeActive]: completed, 24 | }) 25 | return ( 26 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/choice.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react' 2 | import classNames from 'classnames' 3 | 4 | import { Button } from './button' 5 | import classes from '../styles/choice.module.sass' 6 | 7 | const Choice = ({ id = '0', children = [] }) => { 8 | const [selected, setSelected] = useState(null) 9 | const [answer, setAnswer] = useState(null) 10 | const handleAnswer = useCallback(() => setAnswer(selected), [selected]) 11 | const options = children.filter(child => child !== '\n') 12 | return ( 13 | <> 14 | {options.map(({ key, props }, i) => ( 15 |

16 | setSelected(i)} 24 | /> 25 |

31 | ))} 32 | 35 | {options.map(({ key, props }, i) => { 36 | const isCorrect = !!props.correct 37 | return answer === i ? ( 38 |
42 | 47 | {isCorrect ? "That's correct! " : 'Incorrect. '} 48 | 49 | {props.children} 50 |
51 | ) : null 52 | })} 53 | 54 | ) 55 | } 56 | 57 | export const Option = ({ children }) => { 58 | return children 59 | } 60 | 61 | export default Choice 62 | -------------------------------------------------------------------------------- /src/components/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StaticQuery, graphql } from 'gatsby' 3 | 4 | import { Hint } from './hint' 5 | import { Button } from './button' 6 | 7 | import classes from '../styles/code.module.sass' 8 | 9 | function getFiles({ allCode }) { 10 | return Object.assign( 11 | {}, 12 | ...allCode.edges.map(({ node }) => ({ 13 | [node.name]: node.code, 14 | })) 15 | ) 16 | } 17 | 18 | function makeTest(template, testFile, solution) { 19 | // Escape quotation marks in the solution code, for cases where we 20 | // can only place the solution in regular quotes. 21 | const solutionEscaped = solution.replace(/"/g, '\\"') 22 | return template 23 | .replace(/\${solutionEscaped}/g, solutionEscaped) 24 | .replace(/\${solution}/g, solution) 25 | .replace(/\${test}/g, testFile) 26 | } 27 | 28 | class CodeBlock extends React.Component { 29 | state = { Juniper: null, showSolution: false, key: 0 } 30 | 31 | handleShowSolution() { 32 | this.setState({ showSolution: true }) 33 | } 34 | 35 | handleReset() { 36 | // Using the key as a hack to force component to rerender 37 | this.setState({ showSolution: false, key: this.state.key + 1 }) 38 | } 39 | 40 | updateJuniper() { 41 | // This type of stuff only really works in class components. I'm not 42 | // sure why, but I've tried with function components and hooks lots of 43 | // times and couldn't get it to work. So class component it is. 44 | if (!this.state.Juniper) { 45 | // We need a dynamic import here for SSR. Juniper's dependencies 46 | // include references to the global window object and I haven't 47 | // managed to fix this using webpack yet. If we imported Juniper 48 | // at the top level, Gatsby won't build. 49 | import('./juniper').then(Juniper => { 50 | this.setState({ Juniper: Juniper.default }) 51 | }) 52 | } 53 | } 54 | 55 | componentDidMount() { 56 | this.updateJuniper() 57 | } 58 | 59 | componentDidUpdate() { 60 | this.updateJuniper() 61 | } 62 | 63 | render() { 64 | const { Juniper, showSolution } = this.state 65 | const { id, source, solution, test, children } = this.props 66 | const sourceId = source || `exc_${id}` 67 | const solutionId = solution || `solution_${id}` 68 | const testId = test || `test_${id}` 69 | const juniperClassNames = { 70 | cell: classes.cell, 71 | input: classes.input, 72 | button: classes.button, 73 | output: classes.output, 74 | } 75 | const hintActions = [ 76 | { text: 'Show solution', onClick: () => this.handleShowSolution() }, 77 | { text: 'Reset', onClick: () => this.handleReset() }, 78 | ] 79 | 80 | return ( 81 | { 107 | const { testTemplate } = data.site.siteMetadata 108 | const { repo, branch, kernelType, debug, lang } = data.site.siteMetadata.juniper 109 | const files = getFiles(data) 110 | const sourceFile = files[sourceId] 111 | const solutionFile = files[solutionId] 112 | const testFile = files[testId] 113 | return ( 114 |
115 | {Juniper && ( 116 | ( 125 | <> 126 | 127 | {testFile && ( 128 | 138 | )} 139 | 140 | )} 141 | > 142 | {showSolution ? solutionFile : sourceFile} 143 | 144 | )} 145 | {children} 146 |
147 | ) 148 | }} 149 | /> 150 | ) 151 | } 152 | } 153 | 154 | export default CodeBlock 155 | -------------------------------------------------------------------------------- /src/components/exercise.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useCallback, useContext, useEffect } from 'react' 2 | import classNames from 'classnames' 3 | 4 | import { Button, CompleteButton } from './button' 5 | import { ChapterContext } from '../context' 6 | import IconSlides from '../../static/icon_slides.svg' 7 | import classes from '../styles/exercise.module.sass' 8 | 9 | const Exercise = ({ id, title, type, children }) => { 10 | const excRef = useRef() 11 | const excId = parseInt(id) 12 | const { activeExc, setActiveExc, completed, setCompleted } = useContext(ChapterContext) 13 | const isExpanded = activeExc === excId 14 | const isCompleted = completed.includes(excId) 15 | useEffect(() => { 16 | if (isExpanded && excRef.current) { 17 | excRef.current.scrollIntoView() 18 | } 19 | }, [isExpanded]) 20 | const handleExpand = useCallback(() => setActiveExc(isExpanded ? null : excId), [ 21 | isExpanded, 22 | excId, 23 | ]) 24 | const handleNext = useCallback(() => setActiveExc(excId + 1)) 25 | const handleSetCompleted = useCallback(() => { 26 | const newCompleted = isCompleted 27 | ? completed.filter(v => v !== excId) 28 | : [...completed, excId] 29 | setCompleted(newCompleted) 30 | }, [isCompleted, completed, excId]) 31 | const rootClassNames = classNames(classes.root, { 32 | [classes.expanded]: isExpanded, 33 | [classes.wide]: isExpanded && type === 'slides', 34 | [classes.completed]: !isExpanded && isCompleted, 35 | }) 36 | const titleClassNames = classNames(classes.title, { 37 | [classes.titleExpanded]: isExpanded, 38 | }) 39 | return ( 40 |
41 |

42 | 43 | 46 | {excId} 47 | 48 | {title} 49 | 50 | {type === 'slides' && } 51 |

52 | {isExpanded && ( 53 |
54 | {children} 55 |
56 | 60 | 63 |
64 |
65 | )} 66 |
67 | ) 68 | } 69 | 70 | export default Exercise 71 | -------------------------------------------------------------------------------- /src/components/hint.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react' 2 | 3 | import classes from '../styles/hint.module.sass' 4 | 5 | export const Hint = ({ expanded = false, actions = [], children }) => { 6 | const [isExpanded, setIsExpanded] = useState(expanded) 7 | const handleExpand = useCallback(() => setIsExpanded(!isExpanded), [isExpanded]) 8 | return ( 9 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/juniper.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import CodeMirror from 'codemirror' 4 | import { Widget } from '@phosphor/widgets' 5 | import { Kernel, ServerConnection } from '@jupyterlab/services' 6 | import { OutputArea, OutputAreaModel } from '@jupyterlab/outputarea' 7 | import { RenderMimeRegistry, standardRendererFactories } from '@jupyterlab/rendermime' 8 | import { window } from 'browser-monads' 9 | 10 | class Juniper extends React.Component { 11 | outputRef = null 12 | inputRef = null 13 | state = { content: null, cm: null, kernel: null, renderers: null, fromStorage: null } 14 | 15 | static defaultProps = { 16 | children: '', 17 | branch: 'master', 18 | url: 'https://mybinder.org', 19 | serverSettings: {}, 20 | kernelType: 'python3', 21 | lang: 'python', 22 | theme: 'default', 23 | isolateCells: true, 24 | useBinder: true, 25 | storageKey: 'juniper', 26 | useStorage: true, 27 | storageExpire: 60, 28 | debug: true, 29 | msgButton: 'run', 30 | msgLoading: 'Loading...', 31 | msgError: 'Connecting failed. Please reload and try again.', 32 | classNames: { 33 | cell: 'juniper-cell', 34 | input: 'juniper-input', 35 | button: 'juniper-button', 36 | output: 'juniper-output', 37 | }, 38 | } 39 | 40 | static propTypes = { 41 | children: PropTypes.string, 42 | repo: PropTypes.string.isRequired, 43 | branch: PropTypes.string, 44 | url: PropTypes.string, 45 | serverSettings: PropTypes.object, 46 | kernelType: PropTypes.string, 47 | lang: PropTypes.string, 48 | theme: PropTypes.string, 49 | isolateCells: PropTypes.bool, 50 | useBinder: PropTypes.bool, 51 | useStorage: PropTypes.bool, 52 | storageExpire: PropTypes.number, 53 | msgButton: PropTypes.string, 54 | msgLoading: PropTypes.string, 55 | msgError: PropTypes.string, 56 | classNames: PropTypes.shape({ 57 | cell: PropTypes.string, 58 | input: PropTypes.string, 59 | button: PropTypes.string, 60 | output: PropTypes.string, 61 | }), 62 | actions: PropTypes.func, 63 | } 64 | 65 | componentDidMount() { 66 | this.setState({ content: this.props.children }) 67 | const renderers = standardRendererFactories.filter(factory => 68 | factory.mimeTypes.includes('text/latex') ? window.MathJax : true 69 | ) 70 | 71 | const outputArea = new OutputArea({ 72 | model: new OutputAreaModel({ trusted: true }), 73 | rendermime: new RenderMimeRegistry({ initialFactories: renderers }), 74 | }) 75 | 76 | const cm = new CodeMirror(this.inputRef, { 77 | value: this.props.children.trim(), 78 | mode: this.props.lang, 79 | theme: this.props.theme, 80 | }) 81 | this.setState({ cm }) 82 | 83 | const runCode = wrapper => { 84 | const value = cm.getValue() 85 | this.execute(outputArea, wrapper ? wrapper(value) : value) 86 | } 87 | const setValue = value => cm.setValue(value) 88 | cm.setOption('extraKeys', { 'Shift-Enter': runCode }) 89 | Widget.attach(outputArea, this.outputRef) 90 | this.setState({ runCode, setValue }) 91 | } 92 | 93 | log(logFunction) { 94 | if (this.props.debug) { 95 | logFunction() 96 | } 97 | } 98 | 99 | componentWillReceiveProps({ children }) { 100 | if (children !== this.state.content && this.state.cm) { 101 | this.state.cm.setValue(children.trim()) 102 | } 103 | } 104 | 105 | /** 106 | * Request a binder, e.g. from mybinder.org 107 | * @param {string} repo - Repository name in the format 'user/repo'. 108 | * @param {string} branch - The repository branch, e.g. 'master'. 109 | * @param {string} url - The binder reployment URL, including 'http(s)'. 110 | * @returns {Promise} - Resolved with Binder settings, rejected with Error. 111 | */ 112 | requestBinder(repo, branch, url) { 113 | const binderUrl = `${url}/build/gh/${repo}/${branch}` 114 | this.log(() => console.info('building', { binderUrl })) 115 | return new Promise((resolve, reject) => { 116 | const es = new EventSource(binderUrl) 117 | es.onerror = err => { 118 | es.close() 119 | this.log(() => console.error('failed')) 120 | reject(new Error(err)) 121 | } 122 | let phase = null 123 | es.onmessage = ({ data }) => { 124 | const msg = JSON.parse(data) 125 | if (msg.phase && msg.phase !== phase) { 126 | phase = msg.phase.toLowerCase() 127 | this.log(() => console.info(phase === 'ready' ? 'server-ready' : phase)) 128 | } 129 | if (msg.phase === 'failed') { 130 | es.close() 131 | reject(new Error(msg)) 132 | } else if (msg.phase === 'ready') { 133 | es.close() 134 | const settings = { 135 | baseUrl: msg.url, 136 | wsUrl: `ws${msg.url.slice(4)}`, 137 | token: msg.token, 138 | } 139 | resolve(settings) 140 | } 141 | } 142 | }) 143 | } 144 | 145 | /** 146 | * Request kernel and estabish a server connection via the JupyerLab service 147 | * @param {object} settings - The server settings. 148 | * @returns {Promise} - A promise that's resolved with the kernel. 149 | */ 150 | requestKernel(settings) { 151 | if (this.props.useStorage) { 152 | const timestamp = new Date().getTime() + this.props.storageExpire * 60 * 1000 153 | const json = JSON.stringify({ settings, timestamp }) 154 | window.localStorage.setItem(this.props.storageKey, json) 155 | } 156 | const serverSettings = ServerConnection.makeSettings(settings) 157 | return Kernel.startNew({ 158 | type: this.props.kernelType, 159 | name: this.props.kernelType, 160 | serverSettings, 161 | }).then(kernel => { 162 | this.log(() => console.info('ready')) 163 | return kernel 164 | }) 165 | } 166 | 167 | /** 168 | * Get a kernel by requesting a binder or from localStorage / user settings 169 | * @returns {Promise} 170 | */ 171 | getKernel() { 172 | if (this.props.useStorage) { 173 | const stored = window.localStorage.getItem(this.props.storageKey) 174 | if (stored) { 175 | this.setState({ fromStorage: true }) 176 | const { settings, timestamp } = JSON.parse(stored) 177 | if (timestamp && new Date().getTime() < timestamp) { 178 | return this.requestKernel(settings) 179 | } 180 | window.localStorage.removeItem(this.props.storageKey) 181 | } 182 | } 183 | if (this.props.useBinder) { 184 | return this.requestBinder(this.props.repo, this.props.branch, this.props.url).then( 185 | settings => this.requestKernel(settings) 186 | ) 187 | } 188 | return this.requestKernel(this.props.serverSettings) 189 | } 190 | 191 | /** 192 | * Render the kernel response in a JupyterLab output area 193 | * @param {OutputArea} outputArea - The cell's output area. 194 | * @param {string} code - The code to execute. 195 | */ 196 | renderResponse(outputArea, code) { 197 | outputArea.future = this.state.kernel.requestExecute({ code }) 198 | outputArea.model.add({ 199 | output_type: 'stream', 200 | name: 'loading', 201 | text: this.props.msgLoading, 202 | }) 203 | outputArea.model.clear(true) 204 | } 205 | 206 | /** 207 | * Process request to execute the code 208 | * @param {OutputArea} - outputArea - The cell's output area. 209 | * @param {string} code - The code to execute. 210 | */ 211 | execute(outputArea, code) { 212 | this.log(() => console.info('executing')) 213 | if (this.state.kernel) { 214 | if (this.props.isolateCells) { 215 | this.state.kernel 216 | .restart() 217 | .then(() => this.renderResponse(outputArea, code)) 218 | .catch(() => { 219 | this.log(() => console.error('failed')) 220 | this.setState({ kernel: null }) 221 | outputArea.model.clear() 222 | outputArea.model.add({ 223 | output_type: 'stream', 224 | name: 'failure', 225 | text: this.props.msgError, 226 | }) 227 | }) 228 | return 229 | } 230 | this.renderResponse(outputArea, code) 231 | return 232 | } 233 | this.log(() => console.info('requesting kernel')) 234 | const url = this.props.url.split('//')[1] 235 | const action = !this.state.fromStorage ? 'Launching' : 'Reconnecting to' 236 | outputArea.model.clear() 237 | outputArea.model.add({ 238 | output_type: 'stream', 239 | name: 'stdout', 240 | text: `${action} Docker container on ${url}...`, 241 | }) 242 | new Promise((resolve, reject) => 243 | this.getKernel() 244 | .then(resolve) 245 | .catch(reject) 246 | ) 247 | .then(kernel => { 248 | this.setState({ kernel }) 249 | this.renderResponse(outputArea, code) 250 | }) 251 | .catch(() => { 252 | this.log(() => console.error('failed')) 253 | this.setState({ kernel: null }) 254 | if (this.props.useStorage) { 255 | this.setState({ fromStorage: false }) 256 | window.localStorage.removeItem(this.props.storageKey) 257 | } 258 | outputArea.model.clear() 259 | outputArea.model.add({ 260 | output_type: 'stream', 261 | name: 'failure', 262 | text: this.props.msgError, 263 | }) 264 | }) 265 | } 266 | 267 | render() { 268 | return ( 269 |
270 |
{ 273 | this.inputRef = x 274 | }} 275 | /> 276 | {this.props.msgButton && ( 277 | 280 | )} 281 | {this.props.actions && this.props.actions(this.state)} 282 |
{ 284 | this.outputRef = x 285 | }} 286 | className={this.props.classNames.output} 287 | /> 288 |
289 | ) 290 | } 291 | } 292 | 293 | export default Juniper 294 | -------------------------------------------------------------------------------- /src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StaticQuery, graphql } from 'gatsby' 3 | 4 | import SEO from './seo' 5 | import { Link } from './link' 6 | import { H3 } from './typography' 7 | import Logo from '../../static/logo.svg' 8 | 9 | import '../styles/index.sass' 10 | import classes from '../styles/layout.module.sass' 11 | 12 | const Layout = ({ isHome, title, description, children }) => { 13 | return ( 14 | { 32 | const meta = data.site.siteMetadata 33 | return ( 34 | <> 35 | 36 |
37 | {!isHome && ( 38 |

39 | 40 | 41 | 42 |

43 | )} 44 |
45 | {(title || description) && ( 46 |
47 | {title &&

{title}

} 48 | {description && ( 49 |

{description}

50 | )} 51 |
52 | )} 53 | {children} 54 |
55 | 56 |
57 |
58 |
59 |

About this course

60 |

{meta.description}

61 |
62 | 63 |
64 |

About me

65 | {meta.showProfileImage && ( 66 | 71 | )} 72 |

{meta.bio}

73 |
74 | 75 | {meta.footerLinks && ( 76 |
    77 | {meta.footerLinks.map(({ text, url }, i) => ( 78 |
  • 79 | 80 | {text} 81 | 82 |
  • 83 | ))} 84 |
85 | )} 86 |
87 |
88 |
89 | 90 | ) 91 | }} 92 | /> 93 | ) 94 | } 95 | 96 | export default Layout 97 | -------------------------------------------------------------------------------- /src/components/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link as GatsbyLink } from 'gatsby' 4 | import classNames from 'classnames' 5 | 6 | import classes from '../styles/link.module.sass' 7 | 8 | export const Link = ({ children, to, href, onClick, variant, hidden, className, ...other }) => { 9 | const dest = to || href 10 | const external = /(http(s?)):\/\//gi.test(dest) 11 | const linkClassNames = classNames(classes.root, className, { 12 | [classes.hidden]: hidden, 13 | [classes.secondary]: variant === 'secondary', 14 | }) 15 | 16 | if (!external) { 17 | if ((dest && /^#/.test(dest)) || onClick) { 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | } 24 | return ( 25 | 26 | {children} 27 | 28 | ) 29 | } 30 | return ( 31 | 38 | {children} 39 | 40 | ) 41 | } 42 | 43 | Link.propTypes = { 44 | children: PropTypes.node.isRequired, 45 | to: PropTypes.string, 46 | href: PropTypes.string, 47 | onClick: PropTypes.func, 48 | variant: PropTypes.oneOf(['secondary', null]), 49 | hidden: PropTypes.bool, 50 | className: PropTypes.string, 51 | } 52 | -------------------------------------------------------------------------------- /src/components/seo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Helmet from 'react-helmet' 3 | import { StaticQuery, graphql } from 'gatsby' 4 | 5 | const SEO = ({ title, description }) => ( 6 | { 9 | const lang = 'en' 10 | const siteMetadata = data.site.siteMetadata 11 | const pageTitle = title 12 | ? `${title} · ${siteMetadata.title}` 13 | : `${siteMetadata.title} · ${siteMetadata.slogan}` 14 | const pageDesc = description || siteMetadata.description 15 | const image = `${siteMetadata.siteUrl}/social.jpg` 16 | const meta = [ 17 | { 18 | name: 'description', 19 | content: pageDesc, 20 | }, 21 | { 22 | property: 'og:title', 23 | content: pageTitle, 24 | }, 25 | { 26 | property: 'og:description', 27 | content: pageDesc, 28 | }, 29 | { 30 | property: 'og:type', 31 | content: `website`, 32 | }, 33 | { 34 | property: 'og:site_name', 35 | content: siteMetadata.title, 36 | }, 37 | { 38 | property: 'og:image', 39 | content: image, 40 | }, 41 | { 42 | name: 'twitter:card', 43 | content: 'summary_large_image', 44 | }, 45 | { 46 | name: 'twitter:image', 47 | content: image, 48 | }, 49 | { 50 | name: 'twitter:creator', 51 | content: `@${siteMetadata.twitter}`, 52 | }, 53 | { 54 | name: 'twitter:site', 55 | content: `@${siteMetadata.twitter}`, 56 | }, 57 | { 58 | name: 'twitter:title', 59 | content: pageTitle, 60 | }, 61 | { 62 | name: 'twitter:description', 63 | content: pageDesc, 64 | }, 65 | ] 66 | 67 | return ( 68 | 69 | {siteMetadata.fonts && ( 70 | 74 | )} 75 | 76 | ) 77 | }} 78 | /> 79 | ) 80 | 81 | export default SEO 82 | 83 | const query = graphql` 84 | query DefaultSEOQuery { 85 | site { 86 | siteMetadata { 87 | title 88 | description 89 | slogan 90 | siteUrl 91 | twitter 92 | fonts 93 | } 94 | } 95 | } 96 | ` 97 | -------------------------------------------------------------------------------- /src/components/slides.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StaticQuery, graphql } from 'gatsby' 3 | import Marked from 'reveal.js/plugin/markdown/marked.js' 4 | import classNames from 'classnames' 5 | 6 | import '../styles/reveal.css' 7 | import classes from '../styles/slides.module.sass' 8 | 9 | function getFiles({ allMarkdownRemark }) { 10 | return Object.assign( 11 | {}, 12 | ...allMarkdownRemark.edges.map(({ node }) => ({ 13 | [node.fields.slug.replace('/', '')]: node.rawMarkdownBody, 14 | })) 15 | ) 16 | } 17 | 18 | function getSlideContent(data, source) { 19 | const files = getFiles(data) 20 | const file = files[source] || '' 21 | return file.split('\n---\n').map(f => f.trim()) 22 | } 23 | 24 | class Slides extends React.Component { 25 | componentDidMount() { 26 | import('reveal.js').then(({ default: Reveal }) => { 27 | window.Reveal = Reveal 28 | window.marked = Marked 29 | import('reveal.js/plugin/markdown/markdown.js').then(({ RevealMarkdown }) => { 30 | RevealMarkdown.init() 31 | Reveal.initialize({ 32 | center: false, 33 | progress: false, 34 | showNotes: true, 35 | controls: true, 36 | width: '100%', 37 | height: 600, 38 | minScale: 0.75, 39 | maxScale: 1, 40 | }) 41 | }) 42 | }) 43 | } 44 | 45 | componentWillUnmount() { 46 | // Work around default reveal.js behaviour that doesn't allow 47 | // re-initialization and clashes with React 48 | delete window.Reveal 49 | delete window.marked 50 | delete require.cache[require.resolve('reveal.js')] 51 | delete require.cache[require.resolve('reveal.js/plugin/markdown/markdown.js')] 52 | } 53 | 54 | render() { 55 | const { source } = this.props 56 | const revealClassNames = classNames('reveal', 'show-notes', classes.reveal) 57 | const slideClassNames = classNames('slides', classes.slides) 58 | 59 | return ( 60 |
61 |
62 | { 80 | const content = getSlideContent(data, source) 81 | return ( 82 |
83 | {content.map((markdown, i) => ( 84 |
89 |