├── requirements.txt ├── .gitignore ├── tools ├── update ├── preview ├── users ├── clean ├── test_check.py └── check.py ├── _includes ├── github-ribbon.html ├── banner.html ├── javascript.html ├── footer.html └── header.html ├── lessons └── data │ ├── examp_data.txt │ ├── examp_data_species_mass.txt │ ├── examp_output.txt │ └── ratings.txt ├── css ├── bootstrap │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── bootstrap-theme.css ├── book.css ├── swc-print.css ├── slideshow.css └── swc.css ├── _misc └── etherpad.txt ├── _layouts ├── page.html └── workshop.html ├── CONTRIBUTING.md ├── CONDUCT.md ├── _config.yml ├── setup ├── swc-installation-test-1.py ├── index.md └── swc-installation-test-2.py ├── LICENSE.md ├── DESIGN.md ├── CUSTOMIZATION.md ├── FAQ.md ├── README.md └── index.html /requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | PyYAML 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | .DS_Store 4 | _site 5 | -------------------------------------------------------------------------------- /tools/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Update shared files. 4 | 5 | # FIXME 6 | -------------------------------------------------------------------------------- /_includes/github-ribbon.html: -------------------------------------------------------------------------------- 1 | Find us on GitHub 2 | -------------------------------------------------------------------------------- /tools/preview: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build the workshop website locally for checking. 3 | jekyll build -t -d _site 4 | -------------------------------------------------------------------------------- /lessons/data/examp_data.txt: -------------------------------------------------------------------------------- 1 | a, b, c, d 2 | 1.0, 2.0, 4.0 3 | 5.0, 6.0, 7.0 4 | 1e2, 2e3, 4e5 5 | 1e-2, 2e-3, 4.345e-5 6 | -------------------------------------------------------------------------------- /lessons/data/examp_data_species_mass.txt: -------------------------------------------------------------------------------- 1 | site,species,mass 2 | 1,DS,125 3 | 1,DM,70 4 | 2,DM,55 5 | 1,CB,40 6 | 2,DS,110 7 | 1,CB,45 8 | -------------------------------------------------------------------------------- /css/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacetelescope/2015-09-24-stsci/gh-pages/css/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /css/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacetelescope/2015-09-24-stsci/gh-pages/css/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /css/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacetelescope/2015-09-24-stsci/gh-pages/css/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /css/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacetelescope/2015-09-24-stsci/gh-pages/css/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /tools/users: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | '''Manage user permissions for GitHub repository.''' 4 | 5 | # FIXME: make 'users add name' and 'users remove name' work with the GitHub API. 6 | -------------------------------------------------------------------------------- /tools/clean: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Delete junk files. 3 | find . -name '*~' -exec rm -rf {} \; 4 | find . -name '*.pyc' -exec rm -rf {} \; 5 | find . -name '.DS_Store' -exec rm -rf {} \; 6 | rm -rf _site 7 | -------------------------------------------------------------------------------- /_includes/banner.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /_includes/javascript.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /css/book.css: -------------------------------------------------------------------------------- 1 | span.subtitle { 2 | color: #030303; 3 | display: block; 4 | font-family: inherit; 5 | font-size: 31.5px; 6 | font-weight: bold; 7 | line-height: 40px; 8 | margin: 40px 0px 10px 0px; 9 | text-rendering: optimizelegibility; 10 | } 11 | -------------------------------------------------------------------------------- /lessons/data/examp_output.txt: -------------------------------------------------------------------------------- 1 | 1.000000000000000000e+00, 2.000000000000000000e+00, 4.000000000000000000e+00 2 | 5.000000000000000000e+00, 6.000000000000000000e+00, 7.000000000000000000e+00 3 | 1.000000000000000000e+02, 2.000000000000000000e+03, 4.000000000000000000e+05 4 | 1.000000000000000021e-02, 2.000000000000000042e-03, 4.344999999999999904e-05 5 | -------------------------------------------------------------------------------- /_includes/footer.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /_misc/etherpad.txt: -------------------------------------------------------------------------------- 1 | Welcome to Software Carpentry 2 | 3 | We will use this Etherpad to share links and snippets of code, take notes, ask and answer questions, and whatever else comes to mind. 4 | The page displays a screen with three major parts: 5 | 6 | * The left side holds today's notes: please edit these as we go along. 7 | * The top right side shows the names of users who are logged in: please add your name and pick the color that best reflects your mood and personality. 8 | * The bottom right is a real time chat window for asking questions of the instructor and your fellow learners. 9 | 10 | To start, please add yourself to the attendee list below: 11 | 12 | * Instructor (discipline, institution) 13 | -------------------------------------------------------------------------------- /_includes/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 10 | 11 | 12 | Software Carpentry: {{ page.venue }} 13 | {% include header.html %} 14 | 15 | 16 | {% include github-ribbon.html %} 17 |
18 | {% include banner.html %} 19 |
20 |
21 | {{content}} 22 |
23 | {% include footer.html %} 24 |
25 |
26 | {% include javascript.html %} 27 | 28 | 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | Software Carpentry is an open source project, 4 | and we welcome contributions of all kinds: 5 | new lessons, 6 | fixes to existing material, 7 | bug reports, 8 | and reviews of proposed changes are all equally welcome. 9 | 10 | By contributing, 11 | you are agreeing that Software Carpentry may redistribute your work under 12 | [these licenses][license]. 13 | You also agree to abide by our 14 | [contributor code of conduct][conduct]. 15 | 16 | ## Getting Started 17 | 18 | 1. We use GitHub flow to manage changes, 19 | which is explained in the chapter [Contributing to a Project][pro-git-chapter] 20 | in Scott Chacon's book *Pro Git*. 21 | 22 | 2. You should branch from and submit pull requests against the `gh-pages` branch of this repository. 23 | 24 | ## Other Resources 25 | 26 | This lesson is based on the template found at 27 | [https://github.com/swcarpentry/workshop-template][workshop-template]. 28 | That repository has instructions on formatting and previewing workshop websites. 29 | 30 | [conduct]: CONDUCT.md 31 | [license]: LICENSE.md 32 | [pro-git-chapter]: http://git-scm.com/book/en/v2/GitHub-Contributing-to-a-Project 33 | [workshop-template]: https://github.com/swcarpentry/workshop-template 34 | -------------------------------------------------------------------------------- /css/swc-print.css: -------------------------------------------------------------------------------- 1 | /* Printing */ 2 | @media print { 3 | h1 { 4 | font-size: 16pt; 5 | line-height: 18pt; 6 | } 7 | 8 | h2,h3,h4,h5,h6 { 9 | font-size: 12pt; 10 | line-height: 13pt; 11 | } 12 | 13 | /* Objectives, Callout Box and Challenges */ 14 | .objectives, .keypoints { 15 | background-color: unset; 16 | border: 5px solid; 17 | } 18 | 19 | .callout { 20 | background-color: unset; 21 | border: 5px solid; 22 | } 23 | 24 | .challenge { 25 | background-color: unset; 26 | border: 5px solid; 27 | } 28 | 29 | p,ul,ol,li,pre,code { 30 | font-size: 8pt; 31 | line-height: 9pt; 32 | } 33 | 34 | code { 35 | padding: 0px; 36 | border: 0px; 37 | background: unset; 38 | } 39 | 40 | pre.sourceCode::before, 41 | pre.input::before. { 42 | content: "Input:"; 43 | } 44 | 45 | pre.output::before { 46 | content: "Output:"; 47 | } 48 | 49 | pre.error::before { 50 | content: "Error:"; 51 | } 52 | 53 | pre.sourceCode code, 54 | pre.input code, 55 | pre.output code, 56 | pre.error code { 57 | display: block; 58 | margin-top: 1em; 59 | margin-left: 2em; 60 | } 61 | 62 | #github-ribbon { 63 | display: none; 64 | } 65 | 66 | .banner { 67 | display: none; 68 | } 69 | 70 | .footer { 71 | display: none; 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /lessons/data/ratings.txt: -------------------------------------------------------------------------------- 1 | Name, Paper, Rating 2 | Bhargan Basepair, Chen 2002, 3.5 3 | Bhargan Basepair, Chen 2008, 2.5 4 | Bhargan Basepair, El Awy 2005, 3.5 5 | Bhargan Basepair, Falkirk et al 2006, 3.0 6 | Bhargan Basepair, Jackson 1999, 2.5 7 | Bhargan Basepair, Rollins and Khersau 2002, 3.0 8 | Fan Fullerene, Chen 2002, 3.5 9 | Fan Fullerene, Chen 2008, 3.5 10 | Fan Fullerene, El Awy 2005, 5.0 11 | Fan Fullerene, Falkirk et al 2006, 3.0 12 | Fan Fullerene, Jackson 1999, 3.0 13 | Fan Fullerene, Rollins and Khersau 2002, 1.5 14 | Gail Graphics, Chen 2002, 4.0 15 | Gail Graphics, Chen 2008, 3.5 16 | Gail Graphics, El Awy 2005, 5.0 17 | Gail Graphics, Falkirk et al 2006, 3.0 18 | Gail Graphics, Jackson 1999, 3.0 19 | Helen Helmet, Chen 2002, 3.0 20 | Helen Helmet, El Awy 2005, 3.5 21 | Helen Helmet, Falkirk et al 2006, 4.0 22 | Helen Helmet, Jackson 1999, 2.5 23 | Mehrdad Mapping, Chen 2002, 3.5 24 | Mehrdad Mapping, Chen 2008, 2.5 25 | Mehrdad Mapping, El Awy 2005, 4.0 26 | Mehrdad Mapping, Falkirk et al 2006, 4.5 27 | Mehrdad Mapping, Rollins and Khersau 2002, 3.0 28 | Miguel Monopole, Chen 2002, 4.0 29 | Miguel Monopole, Chen 2008, 2.0 30 | Miguel Monopole, El Awy 2005, 3.0 31 | Miguel Monopole, Falkirk et al 2006, 3.0 32 | Miguel Monopole, Jackson 1999, 3.0 33 | Miguel Monopole, Rollins and Khersau 2002, 2.0 34 | Stephen Scanner, Chen 2002, 4.5 35 | Stephen Scanner, Chen 2008, 1.0 36 | Stephen Scanner, El Awy 2005, 4.0 37 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/). 14 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | #-------------------------------------------------------------------------------- 2 | # Values for this workshop - EDIT THIS SECTION. 3 | #-------------------------------------------------------------------------------- 4 | workshop_repo : "https://github.com/spacetelescope/2015-09-24-stsci" 5 | workshop_site : "http://spacetelescope.github.io/2015-09-24-stsci" 6 | #-------------------------------------------------------------------------------- 7 | # Standard Software Carpentry settings - should not need to be modified. 8 | #-------------------------------------------------------------------------------- 9 | swc_blog : "http://software-carpentry.org/feed.xml" 10 | swc_contact : "admin@software-carpentry.org" 11 | swc_files : "http://files.software-carpentry.org" 12 | swc_github : "http://github.com/swcarpentry" 13 | swc_githubio : "http://swcarpentry.github.io/" 14 | swc_installer : "http://files.software-carpentry.org/SWCarpentryInstaller.exe" 15 | swc_site : "http://software-carpentry.org" 16 | swc_twitter : "http://twitter.com/swcarpentry" 17 | swc_vm : "https://docs.google.com/uc?id=0B4Kr6DYkzkQtSTZLQzF4aVB4NDQ&export=download" 18 | #-------------------------------------------------------------------------------- 19 | # Settings for Jekyll - should not need to be modified. 20 | #-------------------------------------------------------------------------------- 21 | markdown : kramdown 22 | kramdown : 23 | entity_output : as_input # — is output as — 24 | smart_quotes : [39, 39, 34, 34] # decimal for left/right single/double quotes 25 | exclude : [tools] 26 | -------------------------------------------------------------------------------- /setup/swc-installation-test-1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test script to check required Python version. 4 | 5 | Execute this code at the command line by typing: 6 | 7 | python swc-installation-test-1.py 8 | 9 | How to get a command line: 10 | 11 | - On OSX run this with the Terminal application. 12 | 13 | - On Windows, go to the Start menu, select 'Run' and type 'cmd' 14 | (without the quotes) to run the 'cmd.exe' Windows Command Prompt. 15 | 16 | - On Linux, either use your login shell directly, or run one of a 17 | number of graphical terminals (e.g. 'xterm', 'gnome-terminal', ...). 18 | 19 | For some screen shots, see: 20 | 21 | http://software-carpentry.org/setup/terminal.html 22 | 23 | Run the script and follow the instructions it prints at the end. If 24 | you see an error saying that the 'python' command was not found, than 25 | you may not have any version of Python installed. See: 26 | 27 | http://www.python.org/download/releases/2.7.3/#download 28 | 29 | for installation instructions. 30 | 31 | This test is separate to avoid Python syntax errors parsing the more 32 | elaborate `swc-installation-test-2.py`. 33 | """ 34 | 35 | import sys as _sys 36 | 37 | 38 | __version__ = '0.2' 39 | 40 | 41 | def check(): 42 | if _sys.version_info < (3, 3): 43 | print('check for Python version (python):') 44 | print('outdated version of Python: ' + _sys.version) 45 | return False 46 | return True 47 | 48 | 49 | if __name__ == '__main__': 50 | if check(): 51 | print('Passed') 52 | else: 53 | print('Failed') 54 | print('Install a current version of Python 3!') 55 | print('http://continuum.io/downloads') 56 | _sys.exit(1) 57 | -------------------------------------------------------------------------------- /setup/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Testing Setup 4 | root: .. 5 | --- 6 | 7 | This directory contains scripts for testing your machine to make sure 8 | you have the software you'll need for your workshop installed. To use 9 | these scripts: 10 | 11 | 1. Download [swc-installation-test-1.py](swc-installation-test-1.py). 12 | 13 | 2. Run it from the shell: 14 | 15 | ~~~ 16 | $ python swc-installation-test-1.py 17 | Passed 18 | ~~~ 19 | 20 | 3. Download [swc-installation-test-2.py](swc-installation-test-2.py). 21 | 22 | 4. Run it from the shell: 23 | 24 | ~~~ 25 | $ python swc-installation-test-2.py 26 | check virtual-shell... pass 27 | ... 28 | Successes: 29 | 30 | virtual-shell Bourne Again Shell (bash) 4.2.37 31 | ... 32 | ~~~ 33 | 34 | If you see something like: 35 | 36 | ~~~ 37 | $ python swc-installation-test-2.py 38 | check virtual-shell... fail 39 | ... 40 | check for command line shell (virtual-shell) failed: 41 | command line shell (virtual-shell) requires at least one of the following dependencies 42 | For instructions on installing an up-to-date version, see 43 | http://software-carpentry.org/setup/ 44 | causes: 45 | check for Bourne Again Shell (bash) failed: 46 | could not find 'bash' executable for Bourne Again Shell (bash) 47 | For instructions on installing an up-to-date version, see 48 | http://software-carpentry.org/setup/ 49 | ... 50 | ~~~ 51 | 52 | follow the suggestions to try and install any missing software. For 53 | additional troubleshooting information, you can use the `--verbose` 54 | option: 55 | 56 | ~~~ 57 | $ python swc-installation-test-2.py --verbose 58 | check virtual-shell... fail 59 | ... 60 | ================== 61 | System information 62 | ================== 63 | os.name : posix 64 | ... 65 | ~~~ 66 | -------------------------------------------------------------------------------- /_layouts/workshop.html: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 8 | 9 | 10 | Software Carpentry: {{ page.venue }} 11 | {% include header.html %} 12 | 13 | 14 | 15 | 16 | 17 | {% if page.redirect %} 18 | 19 | {% endif %} 20 | 21 | 22 | 23 | {% include github-ribbon.html %} 24 | 25 |
26 | {% include banner.html %} 27 |
28 |
29 |
30 |

{{page.venue}}

31 |
32 |
33 |

34 | Day: 35 | {{page.humandate}} 36 |

37 |

38 | Time: 39 | {% if page.humantime %}{{page.humantime}}{% endif %} 40 |

41 |

42 | Room: 43 | {{page.room}} 44 |

45 |
46 |
47 |

48 | Instructors: 49 | {% if page.instructor %} 50 | {{page.instructor | join: ', ' %}} 51 | {% else %} 52 | to be announced. 53 | {% endif %} 54 |

55 | {% if page.helper %} 56 |

57 | Helpers: 58 | {{page.helper | join: ', ' %}} 59 |

60 | {% endif %} 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | {{content}} 69 |
70 |
71 | {% include footer.html %} 72 |
73 | {% include javascript.html %} 74 | 75 | 76 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | root: . 4 | title: Licenses 5 | --- 6 | ## Instructional Material 7 | 8 | All Software Carpentry instructional material is made available under 9 | the [Creative Commons Attribution license][cc-by-human]. The following 10 | is a human-readable summary of (and not a substitute for) the [full 11 | legal text of the CC BY 4.0 license][cc-by-legal]. 12 | 13 | You are free: 14 | 15 | * to **Share**---copy and redistribute the material in any medium or format 16 | * to **Adapt**---remix, transform, and build upon the material 17 | 18 | for any purpose, even commercially. 19 | 20 | The licensor cannot revoke these freedoms as long as you follow the 21 | license terms. 22 | 23 | Under the following terms: 24 | 25 | * **Attribution**---You must give appropriate credit (mentioning that 26 | your work is derived from work that is Copyright © Software 27 | Carpentry and, where practical, linking to 28 | http://software-carpentry.org/), provide a [link to the 29 | license][cc-by-human], and indicate if changes were made. You may do 30 | so in any reasonable manner, but not in any way that suggests the 31 | licensor endorses you or your use. 32 | 33 | **No additional restrictions**---You may not apply legal terms or 34 | technological measures that legally restrict others from doing 35 | anything the license permits. With the understanding that: 36 | 37 | Notices: 38 | 39 | * You do not have to comply with the license for elements of the 40 | material in the public domain or where your use is permitted by an 41 | applicable exception or limitation. 42 | * No warranties are given. The license may not give you all of the 43 | permissions necessary for your intended use. For example, other 44 | rights such as publicity, privacy, or moral rights may limit how you 45 | use the material. 46 | 47 | ## Software 48 | 49 | Except where otherwise noted, the example programs and other software 50 | provided by Software Carpentry are made available under the 51 | [OSI][osi]-approved 52 | [MIT license][mit-license]. 53 | 54 | Permission is hereby granted, free of charge, to any person obtaining 55 | a copy of this software and associated documentation files (the 56 | "Software"), to deal in the Software without restriction, including 57 | without limitation the rights to use, copy, modify, merge, publish, 58 | distribute, sublicense, and/or sell copies of the Software, and to 59 | permit persons to whom the Software is furnished to do so, subject to 60 | the following conditions: 61 | 62 | The above copyright notice and this permission notice shall be 63 | included in all copies or substantial portions of the Software. 64 | 65 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 66 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 67 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 68 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 69 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 70 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 71 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 72 | 73 | ## Trademark 74 | 75 | "Software Carpentry" and the Software Carpentry logo are registered 76 | trademarks of [NumFOCUS][numfocus]. 77 | 78 | [cc-by-human]: https://creativecommons.org/licenses/by/4.0/ 79 | [cc-by-legal]: https://creativecommons.org/licenses/by/4.0/legalcode 80 | [mit-license]: http://opensource.org/licenses/mit-license.html 81 | [numfocus]: http://numfocus.org/ 82 | [osi]: http://opensource.org 83 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # Background and Design 2 | 3 | There are a few things you need to know in order to understand why we 4 | do things the way we do. Some of them are specific to GitHub, rather 5 | than Git itself. 6 | 7 | 1. Git uses the term "clone" to mean "a copy of a repository". 8 | GitHub uses the term "fork" to mean, "a copy of a GitHub-hosted 9 | repo that is also hosted on GitHub", and the term "clone" to mean 10 | "a copy of a GitHub-hosted repo that's located on someone else's 11 | machine". In both cases, the duplicate has a remote called 12 | `origin` that points to the original repo; other remotes can be 13 | added manually. 14 | 15 | 2. A user on GitHub can only have one fork of a particular repo. 16 | This is a problem for us because an instructor may be involved in 17 | several workshops, each of which has its own website repo. Those 18 | website repositories ought to be forks of this one, but since 19 | GitHub doesn't allow that, we have to use `import.github.com` 20 | as described in [the main page](README.md#creating-a-repository). 21 | 22 | 3. If a repository has a file called `README.md` in its root 23 | directory, GitHub displays the contents of that file on the 24 | repository's home page. 25 | 26 | 4. If a repository has a branch called `gh-pages` (which stands for 27 | "GitHub pages"), then GitHub uses the HTML and Markdown files in 28 | that branch to create a website for the repository. If the 29 | repository's URL is `http://github.com/darwin/finches`, the URL 30 | for the website is `http://darwin.github.io/finches`. 31 | 32 | 5. If an HTML or Markdown file has a header consisting of three 33 | dashes, some data about the page, and three more dashes: 34 | 35 | ~~~ 36 | --- 37 | key: value 38 | other_key: other_value 39 | --- 40 | stuff in the page 41 | ~~~ 42 | 43 | then GitHub doesn't just copy the file over verbatim. Instead, it 44 | runs the file through a translator called 45 | [Jekyll](https://en.wikipedia.org/wiki/Jekyll_%28software%29) that 46 | looks for specially-formatted commands embedded in the file and uses 47 | them to fill in the page. 48 | 49 | 6. Commands can be embedded in the body of a page. One is 50 | `{% raw %}{% include something.html %}{% endraw %}`, which tells 51 | Jekyll to copy the contents of `something.html` into the file 52 | being translated; this is used to create standard headers and 53 | footers for pages. Another is `{{variable}}`: when Jekyll sees 54 | this, it replaces it with the value of `variable`. This is used 55 | to insert things like a contact email address and the URL for our 56 | Twitter account. 57 | 58 | 7. Jekyll gets variables from two places: a file called `_config.yml` 59 | located in the repo's root directory, and the header of each 60 | individual page. Variables from `_config.yml` are put in an 61 | object called `site`, and referred to as `site.variable`, so that 62 | (for example) `{{site.swc_site}}` in a page is replaced by the URL 63 | of the main Software Carpentry web site. Variables from the 64 | page's header are put in an object called `page`, and referred to 65 | as `page.variable`, so if a page's header defines a variable 66 | called `venue`, `{{page.venue}}` is replaced by "Euphoric State 67 | University" (or whatever value the variable has). 68 | 69 | 8. If a page uses `{% raw %}{% include something.html %}{% endraw %}` 70 | to include a snippet of HTML, Jekyll looks in a directory called 71 | `_includes` to find `something.html`. It always looks there, and 72 | nowhere else, so anything we want people to be able to include in 73 | their pages has to be stored in `_includes`. 74 | 75 | 9. A repository can have another special directory called `_layouts`. 76 | If a page like `index.html` has a variable called `layout`, and 77 | that variable's value is `standard.html`, Jekyll loads the file 78 | `_layouts/standard.html` and copies the content of `index.html` 79 | into it, then expands the result. This is used to give the pages 80 | in a site a uniform appearance. 81 | We have created two layouts for workshop pages: 82 | 83 | * `workshop.html` is used for workshops' home pages, and is the 84 | layout for the `index.html` page in your repo's root directory. 85 | That `index.html` page's header must define several variables 86 | as specified in [CUSTOMIZATION.md](CUSTOMIZATION.md) 87 | in order for your workshop to be included in our main website. 88 | 89 | * `page.html` is used for any other pages you want to create. 90 | -------------------------------------------------------------------------------- /CUSTOMIZATION.md: -------------------------------------------------------------------------------- 1 | # Customizing Your Workshop's Website 2 | 3 | ## Configuration File 4 | 5 | You must edit the `_config.yml` configuration file in the root directory of your workshop 6 | and change the URLS called `workshop_repo` and `workshop_site` 7 | to point to the repository for the lesson and its GitHub Pages site respectively. 8 | If the URL for the repository is `https://github.com/gvwilson/2015-07-01-mistaktonic`, 9 | the URL for the website will be `http://gvwilson.github.io/2015-07-01-miskatonic`. 10 | 11 | You should not need to modify any of the other values in `_config.yml`. 12 | 13 | ## Home Page: Data 14 | 15 | Your workshop's home page lives in `index.html`, 16 | which must define the following values in its header: 17 | 18 | * `layout` must be `workshop`. 19 | 20 | * `root` must be the path to the repository's root directory. This is 21 | '.' if the page is in the root directory (which `index.html` is). 22 | In other pages, `root` is '..' if the page is one directory down, 23 | '../..' if it is two levels down, and so on. 24 | 25 | * `venue` is the short name of the institution or group hosting the 26 | workshop, like "Euphoric State University". It should *not* 27 | include the address or other details, since this value is 28 | displayed in a table on the main 29 | [Software Carpentry](http://software-carpentry.org) website. 30 | 31 | * `address` is the workshop's address (including details like the 32 | room number). The address should be all on one line. 33 | 34 | * `country` must be a two-letter ISO-3166 code for the country in 35 | which the workshop is going to take place, such as "fr" (for 36 | France) or "nz" (for New Zealand) - see [Wikipedia](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements) 37 | for a complete list. 38 | 39 | * `language` is the language that will be used in the workshop. 40 | It must be an [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). 41 | Note that two-letter codes mean different things for countries 42 | and languages: "ar" is Arabic when used for a language, but 43 | Argentina when used for a country. 44 | 45 | * `latlng` is the latitude and longitude of the workshop site (so we 46 | can put a pin on our map). You can use 47 | [this site](http://itouchmap.com/latlong.html) to find these 48 | values. You can *not* put spaces around the comma separating the 49 | latitude from the longitude. 50 | 51 | * `humandate` is the human-friendly dates for the workshop. Please 52 | use three-letter month names and abbreviations (e.g., `Jul` 53 | instead of `July`), since these values are displayed in a table on 54 | the [Software Carpentry](http://software-carpentry.org) website. 55 | 56 | * `startdate` is the workshop's starting date in YYYY-MM-DD format, 57 | such as `2015-07-01`. You must use four digits for the year and 58 | two each for the month and day. 59 | 60 | * `enddate` is the workshop's ending date in the same format. If your 61 | workshop is only one day long, the `enddate` field should be deleted. 62 | If your workshop has a more complicated schedule (e.g., a half day a 63 | week for four weeks), please delete the `enddate` field and only tell 64 | us its start date. 65 | 66 | * `instructor` is a comma-separated list of instructor names. The 67 | list must be enclosed in square brackets, and each name must be in 68 | double quotes, as in `["Alan Turing","Grace Hopper"]`. Do not 69 | include other information (such as the word "instructor") in these 70 | values. 71 | 72 | * `helper` is a comma-separated list of helper names formatted in the 73 | same way as the instructor names. If there are no helpers, use an 74 | empty list `[]`. 75 | 76 | * `contact` is the contact email address to use for your workshop. 77 | If you do not provide a contact email address, your website will 78 | display the address for the workshop coordinators (who probably 79 | won't be able to answer questions about the specific details of 80 | your workshop). 81 | 82 | The header may optionally define the following: 83 | 84 | * `etherpad` is the URL for the Etherpad for your workshop. If you are 85 | not using an Etherpad, you can delete this line. 86 | 87 | * `eventbrite` is the multi-digit Eventbrite registration key. If you 88 | are using Eventbrite, the Software Carpentry administraotrs will 89 | give this to you. If you are using something else, you may delete 90 | this line. Note: this value must be given as a string in double 91 | quotes, rather than as a number. 92 | 93 | ## Home Page: Schedule and Syllabus 94 | 95 | You should edit the sections titled `Schedule` and `Syllabus` 96 | so that they show what you're actually planning to teach and when. 97 | 98 | ## Home Page: Setup 99 | 100 | You should delete the pieces of the `Setup` section 101 | related to software you will not be using in your workshop, 102 | so that learners don't spend time installing software they don't need. 103 | After you edit the `Setup` section, you should edit the installation test script. 104 | 105 | `swc-installation-test-1.py` is pretty simple, and just checks that 106 | the students have a recent enough version of Python installed that 107 | they can run `swc-installation-test-2.py`. 108 | 109 | `swc-installation-test-2.py` 110 | checks for a list of dependencies and prints error messages if a 111 | package is not installed, or if the installed version is not current 112 | enough. By default, the script checks for pretty much anything that 113 | has ever been used at a Software Carpentry workshop, which is probably 114 | not what you want for your particular workshop. 115 | 116 | Go through `swc-installation-test-2.py` and 117 | comment any dependencies you don't need out of the `CHECKS` list. You 118 | might also want to skim through the minimum version numbers listed 119 | where particular dependencies are defined (e.g. `('git', 'Git', (1, 7, 120 | 0), None)`). For the most part, fairly conservative values have been 121 | selected, so students with modern machines should be fine. If your 122 | workshop has stricter version requirements, feel free to bump them 123 | accordingly. 124 | 125 | Similarly, the virtual dependencies can be satisfied by any of several 126 | packages. If you don't want to support a particular package (e.g. if 127 | you have no Emacs experience and don't want to be responsible for 128 | students who show up with Emacs as their only editor), you can comment 129 | out that particular `or_dependency`. 130 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | * *Where can I get help?* 4 | 5 | Mail us at [admin@software-carpentry.org](mailto:admin@software-carpentry.org), 6 | or join our [discussion list](http://lists.software-carpentry.org/mailman/listinfo/discuss_lists.software-carpentry.org) 7 | and ask for help there. 8 | 9 | * *Where can I report problems or suggest improvements?* 10 | 11 | Please file an issue against [https://github.com/swcarpentry/workshop-template](this repository) 12 | or [mail us](mailto:admin@software-carpentry.org). 13 | 14 | * *Why does the workshop repository have to be created using `import.github.com`? Why not fork `workshop-template` on GitHub?* 15 | 16 | Because any particular user can only have one fork of a repository, 17 | but instructors frequently need to work on several workshops at once. 18 | 19 | * *Why use the `gh-pages` branch instead of `master`? 20 | 21 | Because [GitHub automatically publishes `gh-pages`](https://help.github.com/articles/creating-project-pages-manually/) 22 | as a website. 23 | 24 | * *Why use Jekyll? Why not some other markup language and some other converter?* 25 | 26 | Because it's the default on GitHub. 27 | If we're going to teach people to use that site, 28 | we should teach them to use it as it is, 29 | not as we wish it was. 30 | 31 | * *Where should pages go if multiple workshops are running at a site simultaneously?* 32 | 33 | Use subdirectories like `2015-07-01-esu/beginners`, 34 | so that main directory names always follow our four-part convention. 35 | 36 | * *What if I want to add more values to `index.html`, like `address1` and `address2` for different rooms on different days?* 37 | 38 | Go ahead, 39 | but you *must* have the variables described in [CUSTOMIZATION.md](CUSTOMIZATION.md). 40 | Please make sure to run `tools/check.py` after adding or changing variables 41 | to make sure that our main website will understand your changes. 42 | 43 | * *What is the "Windows installer"?* 44 | 45 | We have built a small installation helper for Windows 46 | that installs nano and SQLite, adds R to the path, and so on. 47 | It is maintained in 48 | [https://github.com/swcarpentry/windows-installer](https://github.com/swcarpentry/windows-installer), 49 | which also has an up-to-date description of what it actually does. 50 | The latest version is always available at 51 | [http://files.software-carpentry.org/SWCarpentryInstaller.exe](http://files.software-carpentry.org/SWCarpentryInstaller.exe); 52 | contributions are always welcome. 53 | 54 | * *What do the [labels](https://github.com/swcarpentry/lesson-template/issues?q=is%3Aopen+is%3Aissue) mean?* 55 | 56 | * `bug`: something is wrong in our tools or documentation 57 | * `discussion`: marks issues used for conversations about specific problems and questions 58 | * `duplicate`: marks an issue that was closed as redundant (include the number of the original issue in the closing comment) 59 | * `enhancement`: asks for, or adds, a new feature or new information 60 | * `filed-by-newcomer`: issue or pull request was filed by someone who is relatively new to GitHub and/or this project, 61 | and would appreciate guidance as well as feedback 62 | * `help-wanted`: a question or request for assistance 63 | * `leave-as-is`: marks an issue closed because the item in question will be left as is 64 | * `suitable-for-newcomer`: issue or pull request is a good starting point for someone who is relatively new to GitHub and/or this project 65 | * `work-in-progress`: a pull request that is not yet ready for review 66 | 67 | ## Debugging 68 | 69 | * *Eventbrite registration isn't showing up on the workshop's home page.* 70 | 71 | First check that you have something like 72 | 73 | ~~~ 74 | eventbrite: 1234567890AB 75 | ~~~ 76 | 77 | at the YAML header of `index.html`. 78 | If the YAML header is set properly you probably are accessing 79 | `file:///home/to/workshop/directory/_site/index.html` directly. 80 | Instead, 81 | please run 82 | 83 | ~~~ 84 | $ jekyll server -d _site 85 | ~~~ 86 | 87 | and look at `http://localhost:4000` in your browser 88 | (or push your changes to GitHub and view your page there). 89 | 90 | * *What do I do if I see a `invalid byte sequence in ...` error when I run `tools/check`?* 91 | 92 | Your computer is telling you that it doesn't understand some of the characters you're using. 93 | Declare your locale to be `en_US.UTF-8` in your shell: 94 | 95 | ~~~ 96 | $ export LC_ALL=en_US.UTF-8 97 | $ export LANG=en_US.UTF-8 98 | ~~~ 99 | 100 | * *What do I do if I see a `Conversion error` when I run `tools/check`?* 101 | 102 | The error message may look something like this: 103 | 104 | ~~~ 105 | Configuration file: d:/OpenCourses/swc/2013-10-17-round6.4/_config.yml 106 | Source: d:/OpenCourses/swc/2013-10-17-round6.4 107 | Destination: _site 108 | Generating... c:/Ruby193/lib/ruby/gems/1.9.1/gems/posix-spawn-0.3.6/lib/posix/spawn.rb:162: wa 109 | rning: cannot close fd before spawn 110 | Conversion error: There was an error converting 'lessons/misc-biopython/fastq.md'. 111 | done. 112 | ~~~ 113 | 114 | This is a [problem in Pygments.rb](http://stackoverflow.com/questions/17364028/jekyll-on-windows-pygments-not-working), 115 | which Jekyll uses for syntax highlighting. 116 | To fix the problem 117 | uninstall pygments.rb 0.5.1 or 0.5.2 and install version 0.5.0. 118 | For example, here's how you would uninstall pygments 0.5.2 and restore version 0.5.0: 119 | 120 | ~~~ 121 | $ gem uninstall pygments.rb --version "=0.5.2" 122 | $ gem install pygments.rb --version "=0.5.0" 123 | ~~~ 124 | 125 | * *What do I do if I get a "can't convert nil into String" error?* 126 | 127 | On some Linux distributions (e.g, Ubuntu 14.04), you may get this error: 128 | 129 | ~~~ 130 | $ ./tools/preview 131 | /usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require': iconv will be deprecated in the future, use String#encode instead. 132 | /usr/lib/ruby/1.9.1/time.rb:265:in `_parse': can't convert nil into String (TypeError) 133 | from /usr/lib/ruby/1.9.1/time.rb:265:in `parse' 134 | from /usr/bin/jekyll:95:in `block (2 levels) in
' 135 | from /usr/lib/ruby/1.9.1/optparse.rb:1391:in `call' 136 | from /usr/lib/ruby/1.9.1/optparse.rb:1391:in `block in parse_in_order' 137 | from /usr/lib/ruby/1.9.1/optparse.rb:1347:in `catch' 138 | from /usr/lib/ruby/1.9.1/optparse.rb:1347:in `parse_in_order' 139 | from /usr/lib/ruby/1.9.1/optparse.rb:1341:in `order!' 140 | from /usr/lib/ruby/1.9.1/optparse.rb:1432:in `permute!' 141 | from /usr/lib/ruby/1.9.1/optparse.rb:1453:in `parse!' 142 | from /usr/bin/jekyll:137:in `
' 143 | ~~~ 144 | 145 | This occurs because you are using an old version of Jekyll located in `/usr/bin`. 146 | Make sure that you have installed Jekyll using: 147 | 148 | ~~~ 149 | $ gem install jekyll 150 | ~~~ 151 | 152 | This installs Jekyll in `/usr/local/bin`, 153 | so make sure this directory comes before `/usr/bin` in your `PATH` environment variable. 154 | When your path is set correctly, 155 | you should see: 156 | 157 | ~~~ 158 | $ which jekyll 159 | /usr/local/bin/jekyll 160 | ~~~ 161 | 162 | You may also have to install the `nodejs` package to disable references to JavaScript, 163 | which you can do using: 164 | 165 | ~~~ 166 | $ sudo apt-get install nodejs 167 | ~~~ 168 | 169 | For more information, see 170 | [http://michaelchelen.net/81fa/install-jekyll-2-ubuntu-14-04/](this article). 171 | 172 | * *Help, my website isn't rendering correctly once its pushed to GitHub!* 173 | 174 | This can occur if you're trying to view the website over a HTTPS connection 175 | because the content from the Software Carpentry website is not currently served over HTTPS. 176 | Your browser sees this as unsecure content, so won't load it. 177 | 178 | To solve this, 179 | use `http` in the URL instead of `https` 180 | and make sure the link you distribute does so as well. 181 | If you're using a browser plugin like [HTTPS Everywhere](https://www.eff.org/https-everywhere) 182 | you will need to disable it for your workshop's site. 183 | We are presently (January 2015) working to get HTTPS working properly on our website. 184 | -------------------------------------------------------------------------------- /tools/test_check.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test suite for index page checking script.""" 4 | 5 | from io import StringIO 6 | from datetime import date 7 | import check 8 | 9 | def test_check_layout(): 10 | assert check.check_layout("workshop") 11 | 12 | def test_check_layout_fail(): 13 | assert not check.check_layout("lesson") 14 | 15 | def test_check_root(): 16 | assert check.check_root(".") 17 | 18 | def test_check_root_fail(): 19 | assert not check.check_root("setup") 20 | 21 | def test_check_country_none(): 22 | assert not check.check_country(None) 23 | 24 | def test_check_country_two_words(): 25 | assert not check.check_country("Some Country") 26 | 27 | def test_check_country_abbrev(): 28 | assert not check.check_country("USA") 29 | 30 | def test_check_country_correct_unhyphenated(): 31 | assert check.check_country("Canada") 32 | 33 | def test_check_country_correct_hyphenated(): 34 | assert check.check_country("United-Kingdom") 35 | 36 | def test_check_language_none(): 37 | assert not check.check_layout(None) 38 | 39 | def test_check_language_name(): 40 | assert not check.check_layout('english') 41 | 42 | def test_check_language_upper_name(): 43 | assert not check.check_layout('English') 44 | 45 | def test_check_language_correct(): 46 | assert check.check_language('en') 47 | 48 | def test_check_non_language_correct(): 49 | assert not check.check_language('xx') 50 | 51 | def test_check_humandate(): 52 | assert check.check_humandate("Feb 18-20, 2525") 53 | 54 | def test_check_humandate_fail(): 55 | assert not check.check_humandate("February 18-20, 2525") 56 | 57 | def test_check_humandate_chars(): 58 | assert not check.check_humandate("XXX SomeDay, Year") 59 | 60 | def test_check_humantime(): 61 | assert not check.check_humantime("09:00am") 62 | 63 | def test_check_euro_humantime(): 64 | assert check.check_humantime("09:00-17:00") 65 | 66 | def test_check_humantime_fail(): 67 | assert not check.check_humantime("09:00") 68 | 69 | def test_check_humantime_only_am(): 70 | assert not check.check_humantime("am") 71 | 72 | def test_check_humantime_without_spaces(): 73 | assert check.check_humantime("9:00am-5:00pm") 74 | 75 | def test_check_humantime_with_spaces(): 76 | assert check.check_humantime("9:00am - 5:00pm") 77 | 78 | def test_check_humantime_with_extra_spaces(): 79 | assert check.check_humantime("9:00 am - 5:00 pm") 80 | 81 | def test_check_humantime_with_to(): 82 | assert check.check_humantime("9:00am to 5:00pm") 83 | 84 | def test_check_humantime_with_to_and_spaces(): 85 | assert check.check_humantime("9:00 am to 5:00 pm") 86 | 87 | def test_check_humantime_without_am_pm(): 88 | assert check.check_humantime("9:00-17:00") 89 | 90 | def test_check_humantime_without_am_pm_with_to(): 91 | assert check.check_humantime("9:00 to 17:00") 92 | 93 | def test_check_date(): 94 | assert check.check_date(date(2525, 2, 20)) 95 | 96 | def test_check_date_fail(): 97 | assert not check.check_date("Feb 18-20, 2525") 98 | 99 | def test_check_latitude_longitude(): 100 | assert check.check_latitude_longitude("0.0,0.0") 101 | 102 | def test_check_latitude_longitude_chars(): 103 | assert not check.check_latitude_longitude("foo,bar") 104 | 105 | def test_check_instructors(): 106 | assert check.check_instructors(["John Doe", "Jane Doe"]) 107 | 108 | def test_check_instructor_only_one(): 109 | assert check.check_instructors(["John Doe"]) 110 | 111 | def test_check_instructor_empty(): 112 | assert not check.check_instructors([]) 113 | 114 | def test_check_instructor_string(): 115 | assert not check.check_instructors("John Doe") 116 | 117 | def test_check_helpers(): 118 | assert check.check_helpers(["John Doe", "Jane Doe"]) 119 | 120 | def test_check_helpers_only_one(): 121 | assert check.check_helpers(["John Doe"]) 122 | 123 | def test_check_helpers_empty(): 124 | assert check.check_helpers([]) 125 | 126 | def test_check_helper_string(): 127 | assert not check.check_helpers("John Doe") 128 | 129 | def test_check_email(): 130 | assert check.check_email("user@box.com") 131 | 132 | def test_check_email_obfuscate(): 133 | assert not check.check_email("user AT box DOT com") 134 | 135 | def test_check_email_not_default(): 136 | assert not check.check_email('admin@software-carpentry.org') 137 | 138 | def test_check_eventbrite_9_digits(): 139 | assert check.check_eventbrite('1' * 9) 140 | 141 | def test_check_eventbrite_10_digits(): 142 | assert check.check_eventbrite('1' * 10) 143 | 144 | def test_check_not_eventbrite_8_digits(): 145 | assert not check.check_eventbrite('1' * 8) 146 | 147 | def test_check_not_eventbrite_empty(): 148 | assert not check.check_eventbrite('') 149 | 150 | def test_check_not_eventbrite_non_digits(): 151 | assert not check.check_eventbrite('1' * 8 + 'a') 152 | 153 | def test_check_with_enddate(): 154 | header = """--- 155 | layout: workshop 156 | root: . 157 | venue: Euphoric State University 158 | address: 123 College Street, Euphoria 159 | country: United-States 160 | humandate: Feb 17-18, 2020 161 | humantime: 9:00 am - 4:30 pm 162 | startdate: 2020-06-17 163 | enddate: 2020-06-18 164 | latlng: 41.7901128,-87.6007318 165 | instructor: ["Grace Hopper", "Alan Turing"] 166 | helper: [ ] 167 | contact: alan@turing.com 168 | ---""" 169 | 170 | assert check.check_file('test.html', header) == [] 171 | 172 | def test_check_without_enddate(): 173 | header = """--- 174 | layout: workshop 175 | root: . 176 | venue: Euphoric State University 177 | address: 123 College Street, Euphoria 178 | country: United-States 179 | humandate: Feb 17-18, 2020 180 | humantime: 9:00 am - 4:30 pm 181 | startdate: 2020-06-17 182 | latlng: 41.7901128,-87.6007318 183 | instructor: ["Grace Hopper", "Alan Turing"] 184 | contact: alan@turing.com 185 | helper: [ "John von Neumann" ] 186 | ---""" 187 | 188 | assert check.check_file('test.html', header) == [] 189 | 190 | def test_check_with_blank_lines(): 191 | header = """--- 192 | layout: workshop 193 | 194 | root: . 195 | 196 | venue: Euphoric State University 197 | address: 123 College Street, Euphoria 198 | country: United-States 199 | humandate: Feb 17-18, 2020 200 | humantime: 9:00 am - 4:30 pm 201 | startdate: 2020-06-17 202 | enddate: 2020-06-18 203 | latlng: 41.7901128,-87.6007318 204 | instructor: ["Grace Hopper", "Alan Turing"] 205 | helper: [ ] 206 | contact: alan@turing.com 207 | ---""" 208 | 209 | assert check.check_file('test.html', header) != [] 210 | 211 | def test_check_with_commented_lines(): 212 | header = """--- 213 | layout: workshop 214 | root: . 215 | venue: Euphoric State University 216 | address: 123 College Street, Euphoria 217 | country: United-States 218 | humandate: Feb 17-18, 2020 219 | humantime: 9:00 am - 4:30 pm 220 | startdate: 2020-06-17 221 | enddate: 2020-06-18 222 | latlng: 41.7901128,-87.6007318 223 | instructor: ["Grace Hopper", "Alan Turing"] 224 | helper: [ ] 225 | contact: alan@turing.com 226 | # eventbrite: 227 | ---""" 228 | 229 | assert check.check_file('test.html', header) == [] 230 | 231 | def test_check_with_commented_values(): 232 | header = """--- 233 | layout: workshop 234 | root: . 235 | venue: Euphoric State University 236 | address: 123 College Street, Euphoria 237 | country: United-States 238 | humandate: Feb 17-18, 2020 239 | humantime: 9:00 am - 4:30 pm 240 | startdate: 2020-06-17 241 | enddate: 2020-06-18 242 | latlng: 41.7901128,-87.6007318 243 | instructor: ["Grace Hopper", "Alan Turing"] 244 | helper: [ ] 245 | contact: alan@turing.com 246 | eventbrite: # FIXME 247 | ---""" 248 | 249 | assert check.check_file('test.html', header) == [] 250 | 251 | def test_check_with_leading_blank_lines(): 252 | header = """ 253 | --- 254 | layout: workshop 255 | root: . 256 | venue: Euphoric State University 257 | address: 123 College Street, Euphoria 258 | country: United-States 259 | humandate: Feb 17-18, 2020 260 | humantime: 9:00 am - 4:30 pm 261 | startdate: 2020-06-17 262 | enddate: 2020-06-18 263 | latlng: 41.7901128,-87.6007318 264 | instructor: ["Grace Hopper", "Alan Turing"] 265 | helper: [ ] 266 | contact: alan@turing.com 267 | eventbrite: # FIXME 268 | ---""" 269 | 270 | assert check.check_file('test.html', header) != [] 271 | 272 | def test_check_with_missing_yaml_terminator(): 273 | header = """ 274 | --- 275 | layout: workshop 276 | root: . 277 | 282 | """ 283 | 284 | assert check.check_file('test.html', header) != [] 285 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # workshop-template 2 | 3 | This repository is [Software Carpentry](http://software-carpentry.org)'s 4 | template for creating websites for workshops. 5 | 6 | 1. Do *not* fork this repository directly on GitHub. 7 | Instead, please use GitHub's importer following the instructions [below](#creating-a-repository) 8 | to create a website repository for your workshop. 9 | 10 | 2. Please *do your work in your repository's `gh-pages` branch*, 11 | since that is what is [automatically published as a website by GitHub](https://help.github.com/articles/creating-project-pages-manually/). 12 | 13 | 3. Once you are done, 14 | please **send your repository's URL to the [Software Carpentry administrator](mailto:admin@software-carpentry.org)**. 15 | We build the [list of workshops on the main website](http://software-carpentry.org/workshops/index.html) 16 | from the data included in your `index.html` page. 17 | We can only do that if you [customize](CUSTOMIZATION.md) that page correctly 18 | *and* send us a link to your workshop website. 19 | 20 | 4. Please also read 21 | [the notes on customizing your website](CUSTOMIZATION.md) and the [FAQ](FAQ.md). 22 | If you're interested in knowing more about why we do things the way we do, 23 | please check out the [design notes](DESIGN.md). 24 | 25 | 5. If you are teaching Git, 26 | please [create a separate repository](#setting-up-a-separate-repository-for-learners) 27 | for your learners to practice in. 28 | 29 | 6. If you run into problems, 30 | or have ideas about how to make this process simpler, 31 | please [get in touch](#getting-and-giving-help). 32 | 33 | ## Creating a Repository 34 | 35 | 1. Go to [GitHub's importer][import]. 36 | 37 | 2. Click on "Check the URL". (GitHub won't import until you've done this.) 38 | 39 | 3. Select the owner for your new repository. 40 | (This will probably be you, but may instead be an organization you belong to.) 41 | 42 | 4. Choose a name for your workshop website repository. 43 | This name should have the form `YYYY-MM-DD-site`, 44 | e.g., `2015-07-01-miskatonic`. 45 | 46 | 5. Make sure the repository is public. 47 | 48 | 6. At this point, you should have a page like this: 49 | 50 | ![](http://software-carpentry.org/img/workshop-template/using-github-import.png) 51 | 52 | You can now click "Begin Import". 53 | When the process is done, 54 | you can click "Continue to repository" to visit your newly-created repository. 55 | 56 | **Note:** 57 | some people have had intermittent errors during the import process, 58 | possibly because of the network timing out. 59 | If you experience a problem, please re-try; 60 | if the problem persists, 61 | please [get in touch](#getting-and-giving-help). 62 | 63 | To clone your new repository, use: 64 | 65 | ~~~ 66 | git clone -b gh-pages https://github.com/your_username/YYYY-MM-DD-site 67 | ~~~ 68 | 69 | This is needed because the imported repository doesn't have a `master` branch. 70 | 71 | **Note:** please do all of your work in your repository's `gh-pages` branch, 72 | since [GitHub automatically publishes that as a website](https://help.github.com/articles/creating-project-pages-manually/). 73 | 74 | ## Customizing Your Website 75 | 76 | 1. Go into your newly-created repository, 77 | which will be at `https://github.com/your_username/YYYY-MM-DD-site`. 78 | For example, 79 | if `your_username` is `gvwilson`, 80 | the repository's URL will be `https://github.com/gvwilson/2015-07-01-mistaktonic`. 81 | 82 | 2. Edit `index.html` to customize the list of instructors, 83 | workshop venue, 84 | etc. 85 | You can do this in the browser by clicking on it in the file view 86 | and then selecting the pencil icon in the menu bar: 87 | 88 | ![](http://software-carpentry.org/img/workshop-template/edit-index-file-menu-bar.png) 89 | 90 | or you can clone the repository to your desktop, 91 | edit `index.html` there, 92 | and push your changes back to the repository. 93 | Editing hints are embedded in `index.html`, 94 | and full instructions are in [CUSTOMIZATION.md](CUSTOMIZATION.md). 95 | 96 | 3. Edit `_config.yml` in the same way 97 | so that `workshop_repo` and `workshop_site` 98 | are the URLs of your repository and your GitHub Pages website respectively. 99 | 100 | Note: the URL for your website is determined automatically 101 | based on the URL for your repository. 102 | If your repository is at `https://github.com/gvwilson/2015-07-01-mistaktonic`, 103 | its GitHub Pages website is at `http://gvwilson.github.io/2015-07-01-miskatonic`. 104 | 105 | 4. When you are done editing, 106 | you can preview your website. 107 | Again, 108 | if your repository is `https://github.com/your_username/YYYY-MM-DD-site`, 109 | its website will be `http://your_username.github.io/YYYY-MM-DD-site`. 110 | 111 | Full instructions are available in [CUSTOMIZATION.md](CUSTOMIZATION.md). 112 | This [FAQ](FAQ.md) includes a few extra tips 113 | (additions are always welcome) 114 | and these notes on [the background and design](DESIGN.md) of this template may help as well. 115 | 116 | That's it. 117 | The following steps are only necessary if you want to run the website locally on your computer. 118 | 119 | ## Checking Your Changes 120 | 121 | **Note:** to check your changes you need some softwares 122 | that are describe at [Installing Software session](#installing-software). 123 | 124 | No matter how you edit `index.html`, you should: 125 | 126 | 1. Check your changes by running `tools/check.py` at the command line 127 | from the root directory of your repository. 128 | 129 | 2. Preview your changes by running `tools/preview` and looking at `_site/index.html`. 130 | 131 | For some links to work properly, 132 | particularly the link to your workshop's Eventbrite registration page, 133 | you must view `_site/index.html` using an HTTP server. 134 | If you have Jekyll installed, 135 | you can do this by running: 136 | 137 | ~~~ 138 | $ jekyll server -d _site 139 | ~~~ 140 | 141 | and going to http://localhost:4000. 142 | 143 | ## Installing Software 144 | 145 | In order to preview the workshop website locally on your computer, 146 | you must install the software described below. 147 | 148 | > If you aren't able to install this software (or you just can't be 149 | > bothered), you can still create a website for your workshop. Every 150 | > time you push a change to your website respository the live website 151 | > will update automatically, so you can check your changes on the live 152 | > site instead of locally. 153 | 154 | 1. Ruby 2.0 or greater 155 | 156 | On Debian/Ubuntu based machines you can install it using 157 | 158 | ~~~ 159 | $ sudo apt-get install ruby2.0 ruby2.0-dev 160 | ~~~ 161 | 162 | 2. NodeJS 163 | 164 | On Debian/Ubuntu based machines you can install it using 165 | 166 | ~~~ 167 | $ sudo apt-get install nodejs 168 | ~~~ 169 | 170 | 3. Jekyll 171 | 172 | Install `github-pages`: 173 | 174 | ~~~ 175 | $ gem install github-pages 176 | ~~~ 177 | 178 | or if that doesn't work: 179 | 180 | ~~~ 181 | $ gem install jekyll 182 | $ gem install kramdown 183 | ~~~ 184 | 185 | We use Kramdown to translate Markdown into HTML, instead of 186 | the default Redcarpet, because Kramdown handles Markdown 187 | inside HTML blocks. 188 | 189 | 2. The Python YAML module 190 | 191 | If you are using the Anaconda Python distribution, you probably 192 | already have it; if you don't, you can install it with: 193 | 194 | ~~~ 195 | $ conda install pyyaml 196 | ~~~ 197 | 198 | If you are using some other distribution, you can install the 199 | Python YAML module using Pip: 200 | 201 | ~~~ 202 | $ pip install pyyaml 203 | ~~~ 204 | 205 | and if you are on Debian Linux, you can use: 206 | 207 | ~~~ 208 | $ apt-get install python-yaml 209 | ~~~ 210 | 211 | ## Setting Up a Separate Repository for Learners 212 | 213 | If you are teaching Git, 214 | you should create a separate repository for learners to use in that lesson. 215 | You should not have them use the workshop website repository because: 216 | 217 | * your workshop website repository contains many files 218 | that most learners don't need to see during the lesson, 219 | and 220 | 221 | * you probably don't want to accidentally merge 222 | a damaging pull request from a novice Git user 223 | into your workshop's website while you are using it to teach. 224 | 225 | You can call this repository whatever you like, 226 | and add whatever content you need to it. 227 | 228 | ## Getting and Giving Help 229 | 230 | We are committed to offering a pleasant setup experience for our learners and organizers. 231 | If you find bugs in our instructions, 232 | or would like to suggest improvements, 233 | please [file an issue](https://github.com/swcarpentry/workshop-template/issues) 234 | or [mail us](mailto:admin@software-carpentry.org). 235 | 236 | [import]: http://import.github.com/new?import_url=https://github.com/swcarpentry/workshop-template 237 | -------------------------------------------------------------------------------- /css/slideshow.css: -------------------------------------------------------------------------------- 1 | /* This theme is generated by deck.js-theme-builder. */ 2 | /* https://github.com/twitwi/deck.js-theme-builder */ 3 | /* Resets and base styles from HTML5 Boilerplate */ 4 | div, span, object, iframe, 5 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 6 | abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, 7 | small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, figcaption, figure, 11 | footer, header, hgroup, menu, nav, section, summary, 12 | time, mark, audio, video { 13 | margin: 0; 14 | padding: 0; 15 | border: 0; 16 | font-size: 100%; 17 | font: inherit; 18 | vertical-align: baseline; 19 | } 20 | 21 | article, aside, details, figcaption, figure, 22 | footer, header, hgroup, menu, nav, section { 23 | display: block; 24 | } 25 | 26 | blockquote, q { 27 | quotes: none; 28 | } 29 | blockquote:before, blockquote:after, q:before, q:after { 30 | content: ""; 31 | content: none; 32 | } 33 | 34 | ins { 35 | background-color: #ff9; 36 | color: #000; 37 | text-decoration: none; 38 | } 39 | 40 | mark { 41 | background-color: #ff9; 42 | color: #000; 43 | font-style: italic; 44 | font-weight: bold; 45 | } 46 | 47 | del { 48 | text-decoration: line-through; 49 | } 50 | 51 | abbr[title], dfn[title] { 52 | border-bottom: 1px dotted; 53 | cursor: help; 54 | } 55 | 56 | table { 57 | border-collapse: collapse; 58 | border-spacing: 0; 59 | } 60 | 61 | hr { 62 | display: block; 63 | height: 1px; 64 | border: 0; 65 | border-top: 1px solid #ccc; 66 | margin: 1em 0; 67 | padding: 0; 68 | } 69 | 70 | input, select { 71 | vertical-align: middle; 72 | } 73 | 74 | select, input, textarea, button { 75 | font: 99% sans-serif; 76 | } 77 | 78 | pre, code, kbd, samp { 79 | font-family: monospace, sans-serif; 80 | } 81 | 82 | a { 83 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 84 | } 85 | a:hover, a:active { 86 | outline: none; 87 | } 88 | 89 | ul, ol { 90 | margin-left: 2em; 91 | vertical-align: top; 92 | } 93 | 94 | ol { 95 | list-style-type: decimal; 96 | } 97 | 98 | nav ul, nav li { 99 | margin: 0; 100 | list-style: none; 101 | list-style-image: none; 102 | } 103 | 104 | small { 105 | font-size: 85%; 106 | } 107 | 108 | strong, th { 109 | font-weight: bold; 110 | } 111 | 112 | td { 113 | vertical-align: top; 114 | } 115 | 116 | sub, sup { 117 | font-size: 75%; 118 | line-height: 0; 119 | position: relative; 120 | } 121 | 122 | sup { 123 | top: -0.5em; 124 | } 125 | 126 | sub { 127 | bottom: -0.25em; 128 | } 129 | 130 | textarea { 131 | overflow: auto; 132 | } 133 | 134 | input[type="radio"] { 135 | vertical-align: text-bottom; 136 | } 137 | 138 | input[type="checkbox"] { 139 | vertical-align: bottom; 140 | } 141 | 142 | label, 143 | input[type="button"], 144 | input[type="submit"], 145 | input[type="image"], 146 | button { 147 | cursor: pointer; 148 | } 149 | 150 | button, input, select, textarea { 151 | margin: 0; 152 | } 153 | 154 | input:invalid, textarea:invalid { 155 | border-radius: 1px; 156 | -moz-box-shadow: 0px 0px 5px red; 157 | -webkit-box-shadow: 0px 0px 5px red; 158 | box-shadow: 0px 0px 5px red; 159 | } 160 | input:invalid .no-boxshadow, textarea:invalid .no-boxshadow { 161 | background-color: #f0dddd; 162 | } 163 | 164 | button { 165 | width: auto; 166 | overflow: visible; 167 | } 168 | 169 | select, input, textarea { 170 | color: #444444; 171 | } 172 | 173 | a { 174 | color: #607890; 175 | } 176 | a:hover, a:focus { 177 | color: #036; 178 | } 179 | a:link { 180 | -webkit-tap-highlight-color: #fff; 181 | } 182 | 183 | /* End HTML5 Boilerplate adaptations */ 184 | h1 { 185 | font-size: 4.5em; 186 | } 187 | 188 | h1, .vcenter { 189 | font-weight: bold; 190 | text-align: center; 191 | padding-top: 1em; 192 | max-height: 100%; 193 | } 194 | .csstransforms h1, .csstransforms .vcenter { 195 | padding: 0 48px; 196 | position: absolute; 197 | left: 0; 198 | right: 0; 199 | top: 50%; 200 | -webkit-transform: translate(0, -50%); 201 | -moz-transform: translate(0, -50%); 202 | -ms-transform: translate(0, -50%); 203 | -o-transform: translate(0, -50%); 204 | transform: translate(0, -50%); 205 | } 206 | 207 | .vcenter h1 { 208 | position: relative; 209 | top: auto; 210 | padding: 0; 211 | -webkit-transform: none; 212 | -moz-transform: none; 213 | -ms-transform: none; 214 | -o-transform: none; 215 | transform: none; 216 | } 217 | 218 | h2 { 219 | font-size: 2.25em; 220 | font-weight: bold; 221 | padding-top: .5em; 222 | margin: 0 0 .66666em 0; 223 | border-bottom: 3px solid #888; 224 | } 225 | 226 | h3 { 227 | font-size: 1.4375em; 228 | font-weight: bold; 229 | margin-bottom: .30435em; 230 | } 231 | 232 | h4 { 233 | font-size: 1.25em; 234 | font-weight: bold; 235 | margin-bottom: .25em; 236 | } 237 | 238 | h5 { 239 | font-size: 1.125em; 240 | font-weight: bold; 241 | margin-bottom: .2222em; 242 | } 243 | 244 | h6 { 245 | font-size: 1em; 246 | font-weight: bold; 247 | } 248 | 249 | img, iframe, video { 250 | display: block; 251 | max-width: 100%; 252 | } 253 | 254 | video, iframe, img { 255 | display: block; 256 | margin: 0 auto; 257 | } 258 | 259 | p, blockquote, iframe, img, ul, ol, pre, video { 260 | margin-bottom: 1em; 261 | } 262 | 263 | pre { 264 | white-space: pre; 265 | white-space: pre-wrap; 266 | word-wrap: break-word; 267 | padding: 1em; 268 | border: 1px solid #888; 269 | } 270 | 271 | em { 272 | font-style: italic; 273 | } 274 | 275 | li { 276 | padding: .25em 0; 277 | vertical-align: middle; 278 | } 279 | li > ol, li > ul { 280 | margin-bottom: inherit; 281 | } 282 | 283 | .deck-container { 284 | font-size: 16px; 285 | line-height: 1.25; 286 | color: #444; 287 | } 288 | 289 | .slide { 290 | -moz-box-sizing: border-box; 291 | box-sizing: border-box; 292 | width: 100%; 293 | } 294 | 295 | /* colors */ 296 | /* extras, all together for now */ 297 | @font-face { 298 | font-family: 'Montserrat'; 299 | font-style: normal; 300 | font-weight: 400; 301 | src: local("Montserrat-Regular"), url(local-fonts/zhcz-_WihjSQC0oHJ9TCYBsxEYwM7FgeyaSgU71cLG0.woff) format("woff"), url(http://fonts.gstatic.com/s/montserrat/v6/zhcz-_WihjSQC0oHJ9TCYBsxEYwM7FgeyaSgU71cLG0.woff) format("woff"); 302 | } 303 | 304 | .timekeeper { 305 | font-family: monospace; 306 | } 307 | 308 | .deck-container { 309 | font-family: 'Montserrat', 'Arial', 'Sans'; 310 | } 311 | 312 | /* sizes */ 313 | .deck-container { 314 | font-size: 30px; 315 | } 316 | 317 | h1 { 318 | font-size: 1.75em; 319 | } 320 | 321 | h2 { 322 | margin: 0; 323 | padding: 0 10px; 324 | font-size: 1.5em; 325 | } 326 | 327 | .deck-container > .slide { 328 | padding-top: 3em; 329 | } 330 | 331 | h2 { 332 | position: absolute; 333 | top: 0; 334 | left: 0; 335 | width: 100%; 336 | } 337 | 338 | .deck-container > .slide.noh2, .deck-container > .slide.noH2 { 339 | padding-top: 0; 340 | } 341 | 342 | h2 { 343 | text-align: center; 344 | } 345 | 346 | .deck-container > .slide > ul > li ul, .deck-container > .slide > ul > li ol, .deck-container > .slide > ol > li ul, .deck-container > .slide > ol > li ol { 347 | line-height: 0.85em; 348 | margin-bottom: 0px; 349 | } 350 | .deck-container > .slide > ul > li ul > li ul, .deck-container > .slide > ul > li ul > li ol, .deck-container > .slide > ul > li ol > li ul, .deck-container > .slide > ul > li ol > li ol, .deck-container > .slide > ol > li ul > li ul, .deck-container > .slide > ol > li ul > li ol, .deck-container > .slide > ol > li ol > li ul, .deck-container > .slide > ol > li ol > li ol { 351 | margin-bottom: 0px; 352 | } 353 | .deck-container > .slide > ul > li ul > li ul > li, .deck-container > .slide > ul > li ul > li ol > li, .deck-container > .slide > ul > li ol > li ul > li, .deck-container > .slide > ul > li ol > li ol > li, .deck-container > .slide > ol > li ul > li ul > li, .deck-container > .slide > ol > li ul > li ol > li, .deck-container > .slide > ol > li ol > li ul > li, .deck-container > .slide > ol > li ol > li ol > li { 354 | margin: 0.2em; 355 | font-size: 0.8em; 356 | margin-bottom: 0px; 357 | } 358 | .deck-container > .slide > ul > li ul > li, .deck-container > .slide > ul > li ol > li, .deck-container > .slide > ol > li ul > li, .deck-container > .slide > ol > li ol > li { 359 | margin: 0.2em; 360 | font-size: 0.85em; 361 | margin-bottom: 0px; 362 | } 363 | .deck-container > .slide > ul li, .deck-container > .slide > ol li { 364 | margin: 0; 365 | padding: 0; 366 | margin-top: 0.5em; 367 | padding-left: 0em; 368 | } 369 | .deck-container > .slide > ul { 370 | list-style: disc outside none; 371 | } 372 | .deck-container > .slide > ul > li ul { 373 | list-style: square outside none; 374 | } 375 | 376 | blockquote { 377 | font-size: 2em; 378 | font-style: italic; 379 | padding: 1em 2em; 380 | color: #000; 381 | border-left: 5px solid #ccc; 382 | } 383 | blockquote p { 384 | margin: 0; 385 | } 386 | blockquote cite { 387 | font-size: .5em; 388 | font-style: normal; 389 | font-weight: bold; 390 | color: #888; 391 | } 392 | 393 | a, a:hover, a:focus, a:active, a:visited { 394 | text-decoration: none; 395 | } 396 | a:hover, a:focus { 397 | text-decoration: underline; 398 | } 399 | 400 | .deck-prev-link, .deck-next-link { 401 | background: #ccc; 402 | font-family: serif; 403 | } 404 | .deck-prev-link, .deck-prev-link:hover, .deck-prev-link:focus, .deck-prev-link:active, .deck-prev-link:visited, .deck-next-link, .deck-next-link:hover, .deck-next-link:focus, .deck-next-link:active, .deck-next-link:visited { 405 | color: #fff; 406 | } 407 | .deck-prev-link:hover, .deck-prev-link:focus, .deck-next-link:hover, .deck-next-link:focus { 408 | background: #c00; 409 | text-decoration: none; 410 | } 411 | 412 | .deck-menu > .slide { 413 | /* matching toplevel slides... ideally, we would expect them to have an additional class */ 414 | background: #eee; 415 | } 416 | .deck-menu > .deck-current, .deck-menu > .deck-child-current { 417 | outline-offset: 20px; 418 | outline: 10px solid #f88; 419 | } 420 | .no-touch .deck-menu .slide:hover { 421 | outline-offset: 20px; 422 | outline: 10px solid red; 423 | background: #ddf; 424 | } 425 | 426 | .deck-container { 427 | background: white; 428 | color: #1f2969; 429 | font-weight: normal; 430 | } 431 | .deck-container:not(.no-status) > .slide { 432 | background: url(../img/software-carpentry-banner.png) bottom 10px left 10px no-repeat; 433 | background-size: 150px; 434 | } 435 | .deck-container .slide :not(h1):not(h2) { 436 | /* TODO: the original style mispelled the font name, so it was using Helvetica for everything but the titles, this line does the same. */ 437 | font-family: 'Helvetica', 'Arial', 'sans'; 438 | } 439 | 440 | .deck-container .slide { 441 | margin: 0; 442 | padding: 160px 50px; 443 | /* text-align: justify;*/ 444 | } 445 | 446 | .slide.no-bullets ol, .slide.no-bullets ul { 447 | list-style: none; 448 | } 449 | 450 | .slide .center { 451 | text-align: center; 452 | } 453 | 454 | .slide .left { 455 | float: left; 456 | width: 50%; 457 | } 458 | 459 | .slide .right { 460 | float: left; 461 | width: 50%; 462 | } 463 | 464 | .slide .c1 { 465 | width: 8.33333% !important; 466 | } 467 | 468 | .slide .c2 { 469 | width: 16.66667% !important; 470 | } 471 | 472 | .slide .c3 { 473 | width: 25% !important; 474 | } 475 | 476 | .slide .c4 { 477 | width: 33.33333% !important; 478 | } 479 | 480 | .slide .c5 { 481 | width: 41.66667% !important; 482 | } 483 | 484 | .slide .c6 { 485 | width: 50% !important; 486 | } 487 | 488 | .slide .c7 { 489 | width: 58.33333% !important; 490 | } 491 | 492 | .slide .c8 { 493 | width: 66.66667% !important; 494 | } 495 | 496 | .slide .c9 { 497 | width: 75% !important; 498 | } 499 | 500 | .slide .c10 { 501 | width: 83.33333% !important; 502 | } 503 | 504 | .slide .c11 { 505 | width: 91.66667% !important; 506 | } 507 | 508 | .slide .c12 { 509 | width: 100% !important; 510 | } 511 | 512 | .slide .C1 { 513 | width: 4.16667% !important; 514 | } 515 | 516 | .slide .C2 { 517 | width: 8.33333% !important; 518 | } 519 | 520 | .slide .C3 { 521 | width: 12.5% !important; 522 | } 523 | 524 | .slide .C4 { 525 | width: 16.66667% !important; 526 | } 527 | 528 | .slide .C5 { 529 | width: 20.83333% !important; 530 | } 531 | 532 | .slide .C6 { 533 | width: 25% !important; 534 | } 535 | 536 | .slide .C7 { 537 | width: 29.16667% !important; 538 | } 539 | 540 | .slide .C8 { 541 | width: 33.33333% !important; 542 | } 543 | 544 | .slide .C9 { 545 | width: 37.5% !important; 546 | } 547 | 548 | .slide .C10 { 549 | width: 41.66667% !important; 550 | } 551 | 552 | .slide .C11 { 553 | width: 45.83333% !important; 554 | } 555 | 556 | .slide .C12 { 557 | width: 50% !important; 558 | } 559 | 560 | .slide .C13 { 561 | width: 54.16667% !important; 562 | } 563 | 564 | .slide .C14 { 565 | width: 58.33333% !important; 566 | } 567 | 568 | .slide .C15 { 569 | width: 62.5% !important; 570 | } 571 | 572 | .slide .C16 { 573 | width: 66.66667% !important; 574 | } 575 | 576 | .slide .C17 { 577 | width: 70.83333% !important; 578 | } 579 | 580 | .slide .C18 { 581 | width: 75% !important; 582 | } 583 | 584 | .slide .C19 { 585 | width: 79.16667% !important; 586 | } 587 | 588 | .slide .C20 { 589 | width: 83.33333% !important; 590 | } 591 | 592 | .slide .C21 { 593 | width: 87.5% !important; 594 | } 595 | 596 | .slide .C22 { 597 | width: 91.66667% !important; 598 | } 599 | 600 | .slide .C23 { 601 | width: 95.83333% !important; 602 | } 603 | 604 | .slide .C24 { 605 | width: 100% !important; 606 | } 607 | 608 | .slide .clearboth { 609 | clear: both; 610 | } 611 | 612 | .slide .right { 613 | float: right; 614 | width: 50%; 615 | } 616 | 617 | div.figure p.caption { 618 | color: #141a41; 619 | text-align: center; 620 | font-size: 80%; 621 | } 622 | 623 | .slide .darker { 624 | color: #141a41; 625 | } 626 | 627 | .slide.media-left > *:not(h2):not(img):not(blockquote):not(pre):not(div), .slide.media-left > div:not(.figure) { 628 | margin-left: 400px; 629 | } 630 | .slide.media-left > div.figure, .slide.media-left > img, .slide.media-left > blockquote, .slide.media-left > pre { 631 | float: left; 632 | width: 50%; 633 | box-sizing: border-box; 634 | } 635 | 636 | .slide.media-right > *:not(h2):not(img):not(blockquote):not(pre):not(div), .slide.media-right > div:not(.figure) { 637 | margin-right: 400px; 638 | } 639 | .slide.media-right > div.figure, .slide.media-right > img, .slide.media-right > blockquote, .slide.media-right > pre { 640 | float: right; 641 | width: 50%; 642 | box-sizing: border-box; 643 | } 644 | 645 | .slide.image-stripes > div { 646 | overflow: hidden; 647 | align-content: center; 648 | } 649 | .slide.image-stripes img { 650 | height: 400px; 651 | max-width: none; 652 | position: relative; 653 | left: 50%; 654 | transform: translate(-50%, 0); 655 | } 656 | 657 | h1, h2 { 658 | color: #2b3990; 659 | font-size: 70px; 660 | line-height: 1em; 661 | font-weight: normal; 662 | } 663 | 664 | h2 { 665 | border-bottom: none; 666 | font-size: 60px; 667 | position: absolute; 668 | left: 0; 669 | right: 0; 670 | bottom: 480px; 671 | top: auto; 672 | } 673 | 674 | h3 { 675 | color: #888; 676 | } 677 | 678 | pre { 679 | border-color: #ccc; 680 | } 681 | 682 | code { 683 | color: #555; 684 | } 685 | 686 | blockquote { 687 | font-size: 20px; 688 | } 689 | 690 | a { 691 | transition: color 200ms; 692 | } 693 | a, a:focus, a:active, a:visited { 694 | color: #1f2969; 695 | } 696 | a:hover { 697 | color: #6877d2; 698 | text-decoration: none; 699 | } 700 | 701 | .slide.title-slide { 702 | text-align: center; 703 | padding-top: 325px; 704 | } 705 | .slide.title-slide h1 { 706 | padding-bottom: 50px; 707 | bottom: 50%; 708 | top: auto; 709 | height: auto; 710 | transform: none; 711 | } 712 | .slide.title-slide img.logo { 713 | position: absolute; 714 | padding-top: 100px; 715 | left: 0; 716 | right: 0; 717 | top: 50%; 718 | align: center; 719 | } 720 | 721 | .progress-bar { 722 | background: #2b3990; 723 | opacity: 0.5; 724 | transition: width 500ms; 725 | } 726 | 727 | /* #20267b? */ 728 | -------------------------------------------------------------------------------- /tools/check.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | '''Check that index.html is valid and print out warnings and errors 4 | when the header is malformed. See the docstrings on the checking 5 | functions for a summary of the checks. 6 | ''' 7 | 8 | from __future__ import print_function 9 | import sys 10 | import os 11 | import re 12 | import logging 13 | import yaml 14 | from collections import Counter 15 | 16 | try: # Hack to make codebase compatible with python 2 and 3 17 | our_basestring = basestring 18 | except NameError: 19 | our_basestring = str 20 | 21 | __version__ = '0.6' 22 | 23 | 24 | # basic logging configuration 25 | logger = logging.getLogger(__name__) 26 | verbosity = logging.INFO # severity of at least INFO will emerge 27 | logger.setLevel(verbosity) 28 | 29 | # create console handler and set level to debug 30 | console_handler = logging.StreamHandler() 31 | console_handler.setLevel(verbosity) 32 | 33 | formatter = logging.Formatter('%(levelname)s: %(message)s') 34 | console_handler.setFormatter(formatter) 35 | logger.addHandler(console_handler) 36 | 37 | 38 | # TODO: these regexp patterns need comments inside 39 | EMAIL_PATTERN = r'[^@]+@[^@]+\.[^@]+' 40 | HUMANTIME_PATTERN = r'((0?[1-9]|1[0-2]):[0-5]\d(am|pm)(-|to)(0?[1-9]|1[0-2]):[0-5]\d(am|pm))|((0?\d|1\d|2[0-3]):[0-5]\d(-|to)(0?\d|1\d|2[0-3]):[0-5]\d)' 41 | EVENTBRITE_PATTERN = r'\d{9,10}' 42 | URL_PATTERN = r'https?://.+' 43 | 44 | DEFAULT_CONTACT_EMAIL = 'admin@software-carpentry.org' 45 | 46 | USAGE = 'Usage: "python check.py" or "python check.py path/to/index.html"\n' 47 | 48 | # Country and language codes. Note that codes mean different things: 'ar' 49 | # is 'Arabic' as a language but 'Argentina' as a country. 50 | 51 | ISO_COUNTRY = [ 52 | 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar', 'as', 53 | 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 54 | 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 55 | 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 56 | 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 57 | 'ec', 'ee', 'eg', 'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 58 | 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 59 | 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 60 | 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 61 | 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 62 | 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 63 | 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mk', 'ml', 'mm', 64 | 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 65 | 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 66 | 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 67 | 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 68 | 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 69 | 'sr', 'st', 'sv', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 'tk', 70 | 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'um', 71 | 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 72 | 'ye', 'yt', 'za', 'zm', 'zw' 73 | ] 74 | 75 | ISO_LANGUAGE = [ 76 | 'aa', 'ab', 'ae', 'af', 'ak', 'am', 'an', 'ar', 'as', 'av', 'ay', 'az', 77 | 'ba', 'be', 'bg', 'bh', 'bi', 'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'ce', 78 | 'ch', 'co', 'cr', 'cs', 'cu', 'cv', 'cy', 'da', 'de', 'dv', 'dz', 'ee', 79 | 'el', 'en', 'eo', 'es', 'et', 'eu', 'fa', 'ff', 'fi', 'fj', 'fo', 'fr', 80 | 'fy', 'ga', 'gd', 'gl', 'gn', 'gu', 'gv', 'ha', 'he', 'hi', 'ho', 'hr', 81 | 'ht', 'hu', 'hy', 'hz', 'ia', 'id', 'ie', 'ig', 'ii', 'ik', 'io', 'is', 82 | 'it', 'iu', 'ja', 'jv', 'ka', 'kg', 'ki', 'kj', 'kk', 'kl', 'km', 'kn', 83 | 'ko', 'kr', 'ks', 'ku', 'kv', 'kw', 'ky', 'la', 'lb', 'lg', 'li', 'ln', 84 | 'lo', 'lt', 'lu', 'lv', 'mg', 'mh', 'mi', 'mk', 'ml', 'mn', 'mr', 'ms', 85 | 'mt', 'my', 'na', 'nb', 'nd', 'ne', 'ng', 'nl', 'nn', 'no', 'nr', 'nv', 86 | 'ny', 'oc', 'oj', 'om', 'or', 'os', 'pa', 'pi', 'pl', 'ps', 'pt', 'qu', 87 | 'rm', 'rn', 'ro', 'ru', 'rw', 'sa', 'sc', 'sd', 'se', 'sg', 'si', 'sk', 88 | 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'ss', 'st', 'su', 'sv', 'sw', 'ta', 89 | 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tn', 'to', 'tr', 'ts', 'tt', 'tw', 90 | 'ty', 'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'wa', 'wo', 'xh', 'yi', 91 | 'yo', 'za', 'zh', 'zu' 92 | ] 93 | 94 | 95 | def add_error(msg, errors): 96 | """Add error to the list of errors.""" 97 | errors.append(msg) 98 | 99 | 100 | def add_suberror(msg, errors): 101 | """Add sub error, ie. error indented by 1 level ("\t"), to the list of errors.""" 102 | errors.append("\t{0}".format(msg)) 103 | 104 | 105 | def look_for_fixme(func): 106 | '''Decorator to fail test if text argument starts with "FIXME".''' 107 | def inner(arg): 108 | if (arg is not None) and \ 109 | isinstance(arg, our_basestring) and \ 110 | arg.lstrip().startswith('FIXME'): 111 | return False 112 | return func(arg) 113 | return inner 114 | 115 | 116 | @look_for_fixme 117 | def check_layout(layout): 118 | '''"layout" in YAML header must be "workshop".''' 119 | 120 | return layout == 'workshop' 121 | 122 | 123 | @look_for_fixme 124 | def check_root(root): 125 | '''"root" (the path from this page to the root directory) must be "."''' 126 | 127 | return root == '.' 128 | 129 | 130 | @look_for_fixme 131 | def check_country(country): 132 | '''"country" must be an ISO-3166 two-letter code.''' 133 | 134 | return country in ISO_COUNTRY 135 | 136 | 137 | @look_for_fixme 138 | def check_language(language): 139 | '''"language" must be an ISO-639 two-letter code.''' 140 | 141 | return language in ISO_LANGUAGE 142 | 143 | 144 | @look_for_fixme 145 | def check_humandate(date): 146 | '''"humandate" must be a human-readable date with a 3-letter month and 147 | 4-digit year. Examples include "Feb 18-20, 2025" and "Feb 18 and 148 | 20, 2025". It may be in languages other than English, but the 149 | month name should be kept short to aid formatting of the main 150 | Software Carpentry web site.''' 151 | 152 | if "," not in date: 153 | return False 154 | 155 | month_dates, year = date.split(",") 156 | 157 | # The first three characters of month_dates are not empty 158 | month = month_dates[:3] 159 | if any(char == " " for char in month): 160 | return False 161 | 162 | # But the fourth character is empty ("February" is illegal) 163 | if month_dates[3] != " ": 164 | return False 165 | 166 | # year contains *only* numbers 167 | try: 168 | int(year) 169 | except: 170 | return False 171 | 172 | return True 173 | 174 | 175 | @look_for_fixme 176 | def check_humantime(time): 177 | '''"humantime" is a human-readable start and end time for the workshop, 178 | such as "09:00 - 16:00".''' 179 | 180 | return bool(re.match(HUMANTIME_PATTERN, time.replace(" ", ""))) 181 | 182 | 183 | def check_date(this_date): 184 | '''"startdate" and "enddate" are machine-readable start and end dates for 185 | the workshop, and must be in YYYY-MM-DD format, e.g., "2015-07-01".''' 186 | 187 | from datetime import date 188 | # yaml automatically loads valid dates as datetime.date 189 | return isinstance(this_date, date) 190 | 191 | 192 | @look_for_fixme 193 | def check_latitude_longitude(latlng): 194 | '''"latlng" must be a valid latitude and longitude represented as two 195 | floating-point numbers separated by a comma.''' 196 | 197 | try: 198 | lat, lng = latlng.split(',') 199 | lat = float(lat) 200 | long = float(lng) 201 | except ValueError: 202 | return False 203 | return (-90.0 <= lat <= 90.0) and (-180.0 <= long <= 180.0) 204 | 205 | 206 | def check_instructors(instructors): 207 | '''"instructor" must be a non-empty comma-separated list of quoted names, 208 | e.g. ['First name', 'Second name', ...']. Do not use "TBD" or other 209 | placeholders.''' 210 | 211 | # yaml automatically loads list-like strings as lists 212 | return isinstance(instructors, list) and len(instructors) > 0 213 | 214 | 215 | def check_helpers(helpers): 216 | '''"helper" must be a comma-separated list of quoted names, 217 | e.g. ['First name', 'Second name', ...']. The list may be empty. Do 218 | not use "TBD" or other placeholders.''' 219 | 220 | # yaml automatically loads list-like strings as lists 221 | return isinstance(helpers, list) and len(helpers) >= 0 222 | 223 | 224 | @look_for_fixme 225 | def check_email(email): 226 | '''"contact" must be a valid email address consisting of characters, a 227 | @, and more characters. It should not be the default contact 228 | email address "admin@software-carpentry.org".''' 229 | 230 | return bool(re.match(EMAIL_PATTERN, email)) and \ 231 | (email != DEFAULT_CONTACT_EMAIL) 232 | 233 | 234 | def check_eventbrite(eventbrite): 235 | '''"eventbrite" (the Eventbrite registration key) must be 9 or more digits.''' 236 | 237 | if isinstance(eventbrite, int): 238 | return True 239 | else: 240 | return bool(re.match(EVENTBRITE_PATTERN, eventbrite)) 241 | 242 | 243 | @look_for_fixme 244 | def check_etherpad(etherpad): 245 | '''"etherpad" must be a valid URL.''' 246 | 247 | return bool(re.match(URL_PATTERN, etherpad)) 248 | 249 | 250 | @look_for_fixme 251 | def check_pass(value): 252 | '''This test always passes (it is used for "checking" things like 253 | addresses, for which no sensible validation is feasible).''' 254 | 255 | return True 256 | 257 | 258 | HANDLERS = { 259 | 'layout': (True, check_layout, 'layout isn\'t "workshop"'), 260 | 'root': (True, check_root, 'root can only be "."'), 261 | 262 | 'country': (True, check_country, 263 | 'country invalid: must use two-letter ISO code from ' + 264 | ', '.join(ISO_COUNTRY)), 265 | 'language' : (False, check_language, 266 | 'language invalid: must use two-letter ISO code from ' + 267 | ', '.join(ISO_LANGUAGE)), 268 | 269 | 'humandate': (True, check_humandate, 270 | 'humandate invalid. Please use three-letter months like ' + 271 | '"Jan" and four-letter years like "2025".'), 272 | 'humantime': (True, check_humantime, 273 | 'humantime doesn\'t include numbers'), 274 | 'startdate': (True, check_date, 275 | 'startdate invalid. Must be of format year-month-day, ' + 276 | 'i.e., 2014-01-31.'), 277 | 'enddate': (False, check_date, 278 | 'enddate invalid. Must be of format year-month-day, i.e.,' + 279 | ' 2014-01-31.'), 280 | 281 | 'latlng': (True, check_latitude_longitude, 282 | 'latlng invalid. Check that it is two floating point ' + 283 | 'numbers, separated by a comma.'), 284 | 285 | 'instructor': (True, check_instructors, 286 | 'instructor list isn\'t a valid list of format ' + 287 | '["First instructor", "Second instructor",..].'), 288 | 'helper': (True, check_helpers, 289 | 'helper list isn\'t a valid list of format ' + 290 | '["First helper", "Second helper",..].'), 291 | 292 | 'contact': (True, check_email, 293 | 'contact email invalid or still set to ' + 294 | '"{0}".'.format(DEFAULT_CONTACT_EMAIL)), 295 | 296 | 'eventbrite': (False, check_eventbrite, 'Eventbrite key appears invalid.'), 297 | 'etherpad': (False, check_etherpad, 'Etherpad URL appears invalid.'), 298 | 299 | 'venue': (False, check_pass, 'venue name not specified'), 300 | 'address': (False, check_pass, 'address not specified') 301 | } 302 | 303 | # REQUIRED is all required categories. 304 | REQUIRED = set([k for k in HANDLERS if HANDLERS[k][0]]) 305 | 306 | # OPTIONAL is all optional categories. 307 | OPTIONAL = set([k for k in HANDLERS if not HANDLERS[k][0]]) 308 | 309 | 310 | def check_validity(data, function, errors, error_msg): 311 | '''Wrapper-function around the various check-functions.''' 312 | valid = function(data) 313 | if not valid: 314 | add_error(error_msg, errors) 315 | add_suberror('Offending entry is: "{0}"'.format(data), errors) 316 | return valid 317 | 318 | 319 | def check_blank_lines(raw_data, errors, error_msg): 320 | '''Blank lines are not allowed in category headers.''' 321 | lines = [x.strip() for x in raw_data.split('\n')] 322 | if '' in lines: 323 | add_error(error_msg, errors) 324 | add_suberror('{0} blank lines found in header'.format(lines.count('')), errors) 325 | return False 326 | return True 327 | 328 | 329 | def check_categories(left, right, errors, error_msg): 330 | '''Report set difference of categories.''' 331 | result = left - right 332 | if result: 333 | add_error(error_msg, errors) 334 | add_suberror('Offending entries: {0}'.format(result), errors) 335 | return False 336 | return True 337 | 338 | 339 | def get_header(text): 340 | '''Extract YAML header from raw data, returning (None, None) if no 341 | valid header found and (raw, parsed) if header found.''' 342 | 343 | # YAML header must be right at the start of the file. 344 | if not text.startswith('---'): 345 | return None, None 346 | 347 | # YAML header must start and end with '---' 348 | pieces = text.split('---') 349 | if len(pieces) < 3: 350 | return None, None 351 | 352 | # Return raw text and YAML-ized form. 353 | raw = pieces[1].strip() 354 | return raw, yaml.load(raw) 355 | 356 | 357 | def check_file(filename, data): 358 | '''Get header from index.html, call all other functions and check file 359 | for validity. Return list of errors (empty when no errors).''' 360 | 361 | errors = [] 362 | raw, header = get_header(data) 363 | if header is None: 364 | msg = ('Cannot find YAML header in given file "{0}".'.format(filename)) 365 | add_error(msg, errors) 366 | return errors 367 | 368 | # Do we have any blank lines in the header? 369 | is_valid = check_blank_lines(raw, errors, 370 | 'There are blank lines in the header') 371 | 372 | # Look through all header entries. If the category is in the input 373 | # file and is either required or we have actual data (as opposed to 374 | # a commented-out entry), we check it. If it *isn't* in the header 375 | # but is required, report an error. 376 | for category in HANDLERS: 377 | required, handler_function, error_message = HANDLERS[category] 378 | if category in header: 379 | if required or header[category]: 380 | is_valid &= check_validity(header[category], 381 | handler_function, errors, 382 | error_message) 383 | elif required: 384 | msg = 'index file is missing mandatory key "{0}"'.format(category) 385 | add_error(msg, errors) 386 | is_valid = False 387 | 388 | # Check whether we have missing or too many categories 389 | seen_categories = set(header.keys()) 390 | 391 | is_valid &= check_categories(REQUIRED, seen_categories, errors, 392 | 'There are missing categories') 393 | 394 | is_valid &= check_categories(seen_categories, REQUIRED.union(OPTIONAL), 395 | errors, 'There are superfluous categories') 396 | 397 | return errors 398 | 399 | 400 | def main(): 401 | '''Run as the main program.''' 402 | filename = None 403 | if len(sys.argv) == 1: 404 | if os.path.exists('./index.html'): 405 | filename = './index.html' 406 | elif os.path.exists('../index.html'): 407 | filename = '../index.html' 408 | elif len(sys.argv) == 2: 409 | filename = sys.argv[1] 410 | 411 | if filename is None: 412 | print(USAGE, file=sys.stderr) 413 | sys.exit(1) 414 | 415 | logger.info('Testing "{0}"'.format(filename)) 416 | 417 | with open(filename) as reader: 418 | data = reader.read() 419 | errors = check_file(filename, data) 420 | 421 | if errors: 422 | for m in errors: 423 | logger.error(m) 424 | sys.exit(1) 425 | else: 426 | logger.info('Everything seems to be in order') 427 | sys.exit(0) 428 | 429 | 430 | if __name__ == '__main__': 431 | main() 432 | -------------------------------------------------------------------------------- /css/swc.css: -------------------------------------------------------------------------------- 1 | /* Card is used only at lessons */ 2 | div.container.card { 3 | background-color: white; 4 | } 5 | 6 | /* Headings */ 7 | h1, h2, h3, h4, h5, h6 { 8 | font-family: "Open Sans", "Helvetica", "Arial", sans-serif; 9 | font-weight: bold; 10 | } 11 | 12 | h1, h2 { 13 | margin-top: 40px; 14 | margin-bottom: 10px; 15 | } 16 | 17 | h1.title { 18 | margin: 40px 0px; 19 | } 20 | 21 | h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { 22 | color: inherit; 23 | } 24 | 25 | h2 code, h3 code, h4 code, h5 code, h6 code { 26 | font-size: inherit; 27 | } 28 | 29 | /* Objectives, Callout Box, Challenges and prerequisites are now rendered as 30 | Bootstrap panels. There's a pandoc error with rendering headers nested under 31 | blockquote: these headers don't preserve attributes. It means we have to 32 | manually set styles for these headers to look like Bootstrap's "panel-title" 33 | class. */ 34 | .panel-heading h1, 35 | .panel-heading h2, 36 | .panel-heading h3, 37 | .panel-heading h4, 38 | .panel-heading h5, 39 | .panel-heading h6 { 40 | margin-top: 0px; 41 | margin-bottom: 0px; 42 | color: inherit; 43 | font-size: 16px; 44 | font-family: inherit; 45 | font-weight: 500; 46 | line-height: 1.1; 47 | } 48 | 49 | /* Make space between the glyphicon and the rest of the header */ 50 | .panel-heading h1 span, 51 | .panel-heading h2 span, 52 | .panel-heading h3 span, 53 | .panel-heading h4 span, 54 | .panel-heading h5 span, 55 | .panel-heading h6 span { 56 | padding-right: 10px; 57 | } 58 | 59 | /* Make the CSS compatible with Pandoc <= 1.13.2 and Pandoc > 1.14. */ 60 | h1.panel, 61 | h2.panel, 62 | h3.panel, 63 | h4.panel, 64 | h5.panel, 65 | h6.panel { 66 | margin-top: 0px; 67 | margin-bottom: 0px; 68 | border: 0px; 69 | color: inherit; 70 | background-color: inherit; 71 | background-image: inherit; 72 | box-shadow: none; 73 | } 74 | 75 | /* Comments in code. */ 76 | .comment { 77 | color: purple; 78 | } 79 | 80 | /* Error messages. */ 81 | .err { 82 | color: darkred; 83 | font-style: italic; 84 | } 85 | 86 | /* Things to fix. */ 87 | .fixme { 88 | text-decoration: underline; 89 | color: darkred; 90 | background-color: lightgray; 91 | } 92 | 93 | /* Highlighted changes in code. */ 94 | .highlight { 95 | background-color: mistyrose; 96 | } 97 | 98 | /* Manual input. */ 99 | .in { 100 | color: darkgreen; 101 | } 102 | 103 | /* Program output. */ 104 | .out { 105 | color: darkblue; 106 | font-style: italic; 107 | } 108 | 109 | /* Putting shadows around things. */ 110 | .shadow { 111 | -moz-box-shadow: 0 0 30px 5px #999; 112 | -webkit-box-shadow: 0 0 30px 5px #999; 113 | box-shadow: 0 0 30px 5px #999; 114 | } 115 | 116 | /* Things to understand (lead-in to sections in book). */ 117 | .understand { 118 | background-color: lightyellow; 119 | } 120 | 121 | /* Block quotations. */ 122 | blockquote { 123 | margin: 1em; 124 | padding: 1em 1em .5em 1em; 125 | width: 90%; 126 | font-size: inherit; 127 | } 128 | 129 | /* Citation for testimonial quote. */ 130 | blockquote.testimonial cite { 131 | font-style: italic; 132 | font-size: inherit; 133 | } 134 | 135 | /* Images 136 | * 137 | * Rules from http://getbootstrap.com/css/#images-responsive. 138 | * 139 | * This is compatible with Pandoc behavior for HTML and HTML5. */ 140 | article img { 141 | max-width: 100%; 142 | height: auto; 143 | display: block; 144 | margin-left: auto; 145 | margin-right: auto; 146 | } 147 | 148 | article div.figure, 149 | article figure { 150 | text-align: center; 151 | } 152 | 153 | article p.caption::before, 154 | article figcaption::before { 155 | content: "Figure: "; 156 | } 157 | 158 | /* Main body of pages. */ 159 | body { 160 | font-family: "Open Sans", "Helvetica", "Arial", sans-serif; 161 | color: rgb(03, 03, 03); 162 | } 163 | 164 | body.workshop, body.lesson { 165 | background-color: #BEC3C6; 166 | margin: 20px 0; 167 | } 168 | 169 | /* Styling for editorial stylesheet */ 170 | body.stylesheet { 171 | background: #ffffff; 172 | width: 60em; 173 | margin: 20px auto; 174 | } 175 | 176 | /* Code sample */ 177 | pre.sourceCode, 178 | pre.input { 179 | color: ForestGreen; 180 | } 181 | pre.output { 182 | color: MediumBlue; 183 | } 184 | pre.error { 185 | color: Red; 186 | } 187 | 188 | @media (max-width: 700px) { 189 | div.banner a img { 190 | padding: 20px 0px; 191 | } 192 | } 193 | 194 | /*----------------------------------------*/ 195 | /* Override Bootstrap CSS */ 196 | /*----------------------------------------*/ 197 | 198 | /* navbar */ 199 | .navbar { 200 | min-height: 85px; 201 | margin-bottom: 0; 202 | } 203 | 204 | .navbar-nav { 205 | margin: 15px 10px 0px 0px; 206 | } 207 | 208 | #swc-navbar { 209 | border-top: 5px solid #2b3990; 210 | width: 100%; 211 | background: #d6d6d6; 212 | border-bottom: 1px solid #CCC; 213 | } 214 | 215 | .navbar-brand { 216 | position: absolute; 217 | padding-top: 7px; 218 | } 219 | 220 | .navbar-brand img { 221 | width: 250px; 222 | height: 50px; 223 | } 224 | 225 | #swc-navbar-search { 226 | background-color: #ffffff; 227 | color: #666666; 228 | border-color:#2b3990; 229 | width: 150px; 230 | } 231 | 232 | .navbar-default .navbar-nav > li > a { 233 | color: #20267D; 234 | } 235 | 236 | .navbar-nav li { 237 | margin-right: -7px; 238 | margin-left: -7px; 239 | } 240 | 241 | .navbar-nav .navbar { 242 | diplay: inline-block; 243 | float: none; 244 | } 245 | 246 | .navbar .navbar-collapse { 247 | text-align: center; 248 | } 249 | 250 | .navbar-nav .nav-collapse .nav > li > a, 251 | .navbar-nav .nav-collapse .dropdown-menu a { 252 | color: #2b3990; 253 | text-align: center; 254 | } 255 | 256 | .navbar-nav .nav .active > a, 257 | .navbar-nav .nav .active > a:hover, .navbar-nav .nav .active > a:focus { 258 | color: #ffffff; 259 | background-color: #20267D; 260 | } 261 | 262 | .navbar-nav .nav li.dropdown.open > .dropdown-toggle, 263 | .navbar-nav .nav li.dropdown.active > .dropdown-toggle, 264 | .navbar-nav .nav li.dropdown.open.active > .dropdown-toggle { 265 | color: #ffffff; 266 | background-color: #20267D; 267 | } 268 | .navbar-nav .nav li.dropdown.active > .dropdown-toggle .caret { 269 | border-top-color: #999999; 270 | border-bottom-color: #999999; 271 | } 272 | .navbar-nav .nav li.dropdown.open > .dropdown-toggle .caret { 273 | border-top-color: #ffffff; 274 | border-bottom-color: #ffffff; 275 | } 276 | 277 | blockquote p { 278 | font-size: inherit; 279 | font-weight: inherit; 280 | line-height: inherit; 281 | } 282 | 283 | blockquote h2{ 284 | margin-top: 0px; 285 | } 286 | 287 | /* readability: darken the alert colour for contrast with background */ 288 | 289 | .alert { 290 | color: rgb(0, 0, 0); 291 | } 292 | 293 | code { 294 | color: #333333; 295 | } 296 | 297 | /* Top banner of every page. */ 298 | div.banner { 299 | background-color: #FFFFFF; 300 | width: 100%; 301 | height: 90px; 302 | margin: 0px; 303 | padding: 0; 304 | border-bottom: 1px solid #A6A6A6; 305 | } 306 | 307 | /* Padding around image in top banner. */ 308 | div.banner a img { 309 | padding: 20px 25px; 310 | } 311 | 312 | /* Explanatory call-out boxes. */ 313 | div.box { 314 | width: 54em; 315 | background-color: mistyrose; 316 | display: block; 317 | margin-left: auto; 318 | margin-right: auto; 319 | padding-top: 1px; 320 | padding-bottom: 1px; 321 | padding-left: 10px; 322 | padding-right: 10px; 323 | outline-color: gray; 324 | outline-width: 1px; 325 | outline-style: solid; 326 | } 327 | 328 | /* Level 2 headings inside pages. */ 329 | div.content div h3 { 330 | border-bottom: 1px solid #CCCCCC; 331 | display: block; 332 | font-family: Verdana,"BitStream vera Sans"; 333 | margin-top: 10px; 334 | padding: 0 5px 3px; 335 | } 336 | 337 | /* PDF and slide files referenced from lectures. */ 338 | div.files { 339 | padding: 10px; 340 | } 341 | 342 | .swc-blue-bg { 343 | /*background-color: #20267D;*/ 344 | /* svg colour is slightly different? */ 345 | background: #2b3990; 346 | } 347 | 348 | /* Main menu at the top of every page. */ 349 | div.mainmenu { 350 | clear: both; 351 | background-color: #F4F4F4; 352 | margin: 0px; 353 | padding: 3px 0px 3px 25px; 354 | border-bottom: 1px solid #A6A6A6; 355 | height: 30px 356 | } 357 | 358 | /* Narration for audio-only lectures. */ 359 | div.narration { 360 | text-align: center; 361 | font-size: 2em; 362 | } 363 | 364 | /* Table of contents. */ 365 | div.toc { 366 | /* No special styling yet. */ 367 | } 368 | 369 | .transcript { 370 | display: table; 371 | } 372 | 373 | .transcript .media img { 374 | border: 1px solid grey; 375 | } 376 | 377 | /* YouTube video embed. */ 378 | div.youtube { 379 | text-align: center; 380 | padding: 10px; 381 | } 382 | 383 | /* Glossary description lists. */ 384 | dl.gloss { 385 | /* Empty for now. */ 386 | } 387 | 388 | /* Displaying YouTube videos. */ 389 | iframe.youtube_player { 390 | border: 0; 391 | text-align: center; 392 | width: 640px; 393 | height: 500px; 394 | } 395 | 396 | /* Sections in book chapters. */ 397 | section { 398 | clear: both; 399 | } 400 | 401 | /* Person's name in team.html. */ 402 | .person { 403 | font-weight: bold; 404 | font-style: italic; 405 | } 406 | 407 | /* Short review of book in bibliography. */ 408 | span.review { 409 | font-style: italic; 410 | } 411 | 412 | /* Bibliography book covers. */ 413 | img.book-cover { 414 | width: 80px; 415 | } 416 | 417 | /* Blog calendar table in blog/index.html. */ 418 | table.blogcalendar th { 419 | text-align : right; 420 | font-weight : bold; 421 | } 422 | 423 | /* See above. */ 424 | table.blogcalendar th.left { 425 | text-align : left; 426 | } 427 | 428 | /* See above. */ 429 | table.blogcalendar tr td { 430 | text-align : right; 431 | } 432 | 433 | /* Blog index tables in blog/index.html. */ 434 | table.blogindex td.date { 435 | text-align: left ; 436 | width:10em; 437 | } 438 | 439 | /* Tables used for displaying choices in challenges. */ 440 | table.choices tr td { 441 | vertical-align : top; 442 | } 443 | 444 | /* Database tables do _not_ have double borders */ 445 | table.outlined { 446 | border-collapse: collapse; 447 | } 448 | 449 | /* Link items (to workshop pages) in the workshops tables */ 450 | table.workshops td.link { 451 | width: 50%; 452 | text-align: left; 453 | } 454 | 455 | /* Spacer items (i.e. ellipsis) on the workshops tables */ 456 | table.workshops td.spacer { 457 | max-width: 100%; 458 | text-align: center; 459 | } 460 | 461 | /* Date columns on the workshops tables */ 462 | table.workshops td.date { 463 | width: 50%; 464 | text-align: right; 465 | } 466 | 467 | /* Badge modal dialog */ 468 | #assertion-modal { 469 | width:700px; 470 | margin-left:-350px; 471 | } 472 | #assertion-modal iframe { 473 | background-color: transparent; 474 | border: 0px none transparent; 475 | padding: 0px; 476 | width: 100%; 477 | height: 20em; 478 | } 479 | 480 | #assertion-model img.badge { 481 | position: absolute; 482 | right: 15px; 483 | bottom: 35px; 484 | opacity: 0.5; 485 | } 486 | 487 | #modal-badge-img { 488 | position:absolute; 489 | right: 25px; 490 | bottom: 25px; 491 | opacity: 0.5; 492 | } 493 | #assertion-modal.in { 494 | color: black; 495 | } 496 | 497 | /* list with checkbox as bullet */ 498 | ul.checklist { 499 | list-style-image: url('../img/checkbox.png'); 500 | } 501 | 502 | /* FAQ */ 503 | dl.faq dt { 504 | font-style: italic; 505 | font-weight: bold; 506 | } 507 | 508 | section.content { 509 | width:100%; 510 | background: white; 511 | } 512 | 513 | dd { 514 | margin-left: 10px; 515 | } 516 | 517 | .header.home { 518 | background: url(../img/header.png) no-repeat center center; 519 | background-attachment: fixed; 520 | -webkit-background-size: cover; 521 | -moz-background-size: cover; 522 | -o-background-size: cover; 523 | background-size: cover; 524 | } 525 | 526 | .header { 527 | background:#2b3990; 528 | } 529 | 530 | .header h1 { 531 | line-height: 1.1; 532 | margin: 60px 0px 80px; 533 | font-size: 40pt; 534 | } 535 | 536 | table { 537 | margin-bottom: 15px; 538 | } 539 | 540 | table th, table td { 541 | padding: 5px 10px; 542 | } 543 | 544 | table > thead > .header { 545 | background: transparent; 546 | } 547 | 548 | table > thead > tr > td, table > thead > tr > th, 549 | table > tbody > tr > td, table > tbody > tr > th, 550 | table > tfoot > tr > td, table > tfoot > tr > th { 551 | border: 1px solid #DDD; 552 | } 553 | 554 | table > thead > tr > th, 555 | table > thead > tr > td { 556 | border-bottom-width: 2px; 557 | } 558 | 559 | table tbody > tr:nth-of-type(2n+1) { 560 | background-color: #F9F9F9; 561 | } 562 | 563 | #header-text { 564 | font-size:20pt; 565 | margin:0; 566 | } 567 | 568 | #home-options { 569 | background:#F6F6F6; 570 | border-top:1px solid #DDDDDD; 571 | border-bottom:1px solid #DDDDDD; 572 | padding:20px 0; 573 | margin-bottom:20px; 574 | } 575 | 576 | #title { 577 | background:#F6F6F6; 578 | border-top:1px solid #DDDDDD; 579 | border-bottom:1px solid #DDDDDD; 580 | padding:0 0 20px; 581 | margin-bottom:20px; 582 | } 583 | 584 | h5 a:link, h5 a:visited, 585 | h4 a:link, h4 a:visited, 586 | h3 a:link, h3 a:visited { 587 | color:#2b3990; 588 | } 589 | 590 | h5 a:hover, 591 | h4 a:hover, 592 | h3 a:hover { 593 | color:#C26D17; 594 | text-decoration: none; 595 | } 596 | 597 | a { 598 | color:#3E51CF; 599 | -webkit-transition: all 0.2s ease; 600 | -moz-transition: all 0.2s ease; 601 | -o-transition: all 0.2s ease; 602 | transition: all 0.2s ease; 603 | } 604 | 605 | a:hover { 606 | color:#965412; 607 | } 608 | 609 | footer { 610 | background: #2b3990; 611 | padding: 20px 0; 612 | font-size: 10pt; 613 | margin-top: 10px; 614 | } 615 | 616 | footer a, footer a:hover{ 617 | color:#FFF; 618 | padding-left: 10px; 619 | } 620 | 621 | footer .container .links { 622 | background:url('../img/software-carpentry-banner-white.png') no-repeat; 623 | background-size: 200px; 624 | background-position: 0; 625 | min-height: 40px; 626 | padding: 20px 0px 0px 200px; 627 | text-align: right; 628 | } 629 | 630 | 631 | /* Footer of every page. */ 632 | /* TODO -- might clash with site footer */ 633 | div.footer { 634 | clear: both; 635 | background: url("/img/main_shadow.png") repeat-x scroll center top #FFFFFF; 636 | padding: 4px 10px 7px 10px; 637 | border-top: 1px solid #A6A6A6; 638 | text-align: right; 639 | } 640 | 641 | 642 | /* doesn't seem to be used on site, workshop or lesson */ 643 | /* Chapter titles. */ 644 | div.chapter h2 { 645 | text-align: center; 646 | font-style: italic; 647 | } 648 | 649 | 650 | /* For the Request a Workshop form */ 651 | #ss-form .ss-q-title { 652 | display: block; 653 | font-weight: bold; 654 | padding-bottom: 0.5em; 655 | } 656 | #ss-form .ss-required-asterisk { 657 | color: #c43b1d; 658 | } 659 | #ss-form label { 660 | display:inline; 661 | cursor: default; 662 | } 663 | #ss-form .ss-secondary-text { 664 | color: #666; 665 | } 666 | #ss-form .ss-form-entry { 667 | margin-bottom: 1.5em; 668 | } 669 | #ss-form ul { 670 | margin:0; 671 | padding:0; 672 | list-style: none; 673 | } 674 | #ss-form ol { 675 | margin:0; 676 | } 677 | #ss-form .ss-choice-item { 678 | line-height: 1.3em; 679 | padding-bottom: .5em; 680 | } 681 | #ss-form .ss-q-long { 682 | resize: vertical; 683 | width: 70%; 684 | } 685 | #ss-form .error-message, .required-message { 686 | display: none; 687 | } 688 | #ss-form .ss-form-entry input { 689 | vertical-align: middle; 690 | margin: 0; 691 | padding:0 4px; 692 | } 693 | #ss-form .ss-choice-item-control { 694 | padding-right: 4px; 695 | } 696 | 697 | 698 | /* GitHub Ribbon */ 699 | #github-ribbon a { 700 | background: #000; 701 | color: #fff; 702 | text-decoration: none; 703 | font-family: arial, sans-serif; 704 | text-align: center; 705 | font-weight: bold; 706 | padding: 5px 40%; 707 | font-size: 1.2rem; 708 | line-height: 2rem; 709 | position: relative; 710 | transition: 0.5s; 711 | width: 100%; 712 | margin: 0 auto; 713 | white-space: nowrap; 714 | z-index: 99999; 715 | } 716 | #github-ribbon a:hover { 717 | background: #600; 718 | color: #fff; 719 | } 720 | #github-ribbon a::before, #github-ribbon a::after { 721 | content: ""; 722 | width: 100%; 723 | display: block; 724 | position: absolute; 725 | top: 1px; 726 | left: 0; 727 | height: 1px; 728 | background: #fff; 729 | } 730 | #github-ribbon a::after { 731 | bottom: 1px; 732 | top: auto; 733 | } 734 | 735 | /* Collapse navbar */ 736 | @media (max-width: 993px) { 737 | .navbar-header { 738 | float: none; 739 | min-height: 80px; 740 | } 741 | .navbar-left,.navbar-right { 742 | float: none !important; 743 | } 744 | .navbar-toggle { 745 | display: block; 746 | background-color: #2b3990; 747 | margin-top: 22px; 748 | margin-right: 100px; 749 | } 750 | .navbar-collapse { 751 | border-top: 1px solid transparent; 752 | box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); 753 | } 754 | .navbar-fixed-top { 755 | top: 0; 756 | border-width: 0 0 1px; 757 | } 758 | .navbar-collapse.collapse { 759 | display: none!important; 760 | } 761 | .navbar-nav { 762 | float: none!important; 763 | } 764 | .navbar-nav>li { 765 | float: none; 766 | } 767 | .navbar-nav>li>a { 768 | padding-top: 10px; 769 | padding-bottom: 10px; 770 | } 771 | .collapse.in{ 772 | display:block !important; 773 | } 774 | } 775 | 776 | @media (max-width: 600px) { 777 | .navbar-toggle { 778 | margin-right: 20px; 779 | } 780 | .navbar-brand img { 781 | width: 180px; 782 | height: 36px; 783 | margin-top: 10px 784 | } 785 | footer .container .links { 786 | background:url('../img/software-carpentry-banner-white.png') no-repeat; 787 | background-size: 180px; 788 | background-position: 0; 789 | } 790 | } 791 | 792 | /* GitHub ribbon breaking point */ 793 | @media screen and (min-width: 600px) { 794 | #github-ribbon { 795 | position: absolute; 796 | display: block; 797 | top: 0; 798 | right: 0; 799 | width: 150px; 800 | overflow: hidden; 801 | height: 150px; 802 | } 803 | #github-ribbon a { 804 | width: 200px; 805 | position: absolute; 806 | padding: 5px 40px; 807 | top: 40px; 808 | right: -40px; 809 | transform: rotate(45deg); 810 | -webkit-transform: rotate(45deg); 811 | box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.8); 812 | } 813 | } 814 | @media (max-width: 599px) { 815 | .header h1 { 816 | font-size: 20pt; 817 | } 818 | #header-text { 819 | font-size: 16pt; 820 | } 821 | #github-ribbon { 822 | width: 100%; 823 | } 824 | #github-ribbon a { 825 | display: block; 826 | padding: 0px 0px; 827 | margin: 0px 0px; 828 | } 829 | } 830 | -------------------------------------------------------------------------------- /css/bootstrap/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | .btn-default, 8 | .btn-primary, 9 | .btn-success, 10 | .btn-info, 11 | .btn-warning, 12 | .btn-danger { 13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 16 | } 17 | .btn-default:active, 18 | .btn-primary:active, 19 | .btn-success:active, 20 | .btn-info:active, 21 | .btn-warning:active, 22 | .btn-danger:active, 23 | .btn-default.active, 24 | .btn-primary.active, 25 | .btn-success.active, 26 | .btn-info.active, 27 | .btn-warning.active, 28 | .btn-danger.active { 29 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 31 | } 32 | .btn-default .badge, 33 | .btn-primary .badge, 34 | .btn-success .badge, 35 | .btn-info .badge, 36 | .btn-warning .badge, 37 | .btn-danger .badge { 38 | text-shadow: none; 39 | } 40 | .btn:active, 41 | .btn.active { 42 | background-image: none; 43 | } 44 | .btn-default { 45 | text-shadow: 0 1px 0 #fff; 46 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 47 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 48 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 49 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 50 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 51 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 52 | background-repeat: repeat-x; 53 | border-color: #dbdbdb; 54 | border-color: #ccc; 55 | } 56 | .btn-default:hover, 57 | .btn-default:focus { 58 | background-color: #e0e0e0; 59 | background-position: 0 -15px; 60 | } 61 | .btn-default:active, 62 | .btn-default.active { 63 | background-color: #e0e0e0; 64 | border-color: #dbdbdb; 65 | } 66 | .btn-default.disabled, 67 | .btn-default:disabled, 68 | .btn-default[disabled] { 69 | background-color: #e0e0e0; 70 | background-image: none; 71 | } 72 | .btn-primary { 73 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 74 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 75 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 76 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 77 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 78 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 79 | background-repeat: repeat-x; 80 | border-color: #245580; 81 | } 82 | .btn-primary:hover, 83 | .btn-primary:focus { 84 | background-color: #265a88; 85 | background-position: 0 -15px; 86 | } 87 | .btn-primary:active, 88 | .btn-primary.active { 89 | background-color: #265a88; 90 | border-color: #245580; 91 | } 92 | .btn-primary.disabled, 93 | .btn-primary:disabled, 94 | .btn-primary[disabled] { 95 | background-color: #265a88; 96 | background-image: none; 97 | } 98 | .btn-success { 99 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 100 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 101 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 102 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 103 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 104 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 105 | background-repeat: repeat-x; 106 | border-color: #3e8f3e; 107 | } 108 | .btn-success:hover, 109 | .btn-success:focus { 110 | background-color: #419641; 111 | background-position: 0 -15px; 112 | } 113 | .btn-success:active, 114 | .btn-success.active { 115 | background-color: #419641; 116 | border-color: #3e8f3e; 117 | } 118 | .btn-success.disabled, 119 | .btn-success:disabled, 120 | .btn-success[disabled] { 121 | background-color: #419641; 122 | background-image: none; 123 | } 124 | .btn-info { 125 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 126 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 127 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 128 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 129 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 130 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 131 | background-repeat: repeat-x; 132 | border-color: #28a4c9; 133 | } 134 | .btn-info:hover, 135 | .btn-info:focus { 136 | background-color: #2aabd2; 137 | background-position: 0 -15px; 138 | } 139 | .btn-info:active, 140 | .btn-info.active { 141 | background-color: #2aabd2; 142 | border-color: #28a4c9; 143 | } 144 | .btn-info.disabled, 145 | .btn-info:disabled, 146 | .btn-info[disabled] { 147 | background-color: #2aabd2; 148 | background-image: none; 149 | } 150 | .btn-warning { 151 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 152 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 153 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 154 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 155 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 156 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 157 | background-repeat: repeat-x; 158 | border-color: #e38d13; 159 | } 160 | .btn-warning:hover, 161 | .btn-warning:focus { 162 | background-color: #eb9316; 163 | background-position: 0 -15px; 164 | } 165 | .btn-warning:active, 166 | .btn-warning.active { 167 | background-color: #eb9316; 168 | border-color: #e38d13; 169 | } 170 | .btn-warning.disabled, 171 | .btn-warning:disabled, 172 | .btn-warning[disabled] { 173 | background-color: #eb9316; 174 | background-image: none; 175 | } 176 | .btn-danger { 177 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 178 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 179 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 180 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 181 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 182 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 183 | background-repeat: repeat-x; 184 | border-color: #b92c28; 185 | } 186 | .btn-danger:hover, 187 | .btn-danger:focus { 188 | background-color: #c12e2a; 189 | background-position: 0 -15px; 190 | } 191 | .btn-danger:active, 192 | .btn-danger.active { 193 | background-color: #c12e2a; 194 | border-color: #b92c28; 195 | } 196 | .btn-danger.disabled, 197 | .btn-danger:disabled, 198 | .btn-danger[disabled] { 199 | background-color: #c12e2a; 200 | background-image: none; 201 | } 202 | .thumbnail, 203 | .img-thumbnail { 204 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 205 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 206 | } 207 | .dropdown-menu > li > a:hover, 208 | .dropdown-menu > li > a:focus { 209 | background-color: #e8e8e8; 210 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 211 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 212 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 213 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 214 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 215 | background-repeat: repeat-x; 216 | } 217 | .dropdown-menu > .active > a, 218 | .dropdown-menu > .active > a:hover, 219 | .dropdown-menu > .active > a:focus { 220 | background-color: #2e6da4; 221 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 222 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 223 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 224 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 225 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 226 | background-repeat: repeat-x; 227 | } 228 | .navbar-default { 229 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 230 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 231 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 232 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 233 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 234 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 235 | background-repeat: repeat-x; 236 | border-radius: 4px; 237 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 238 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 239 | } 240 | .navbar-default .navbar-nav > .open > a, 241 | .navbar-default .navbar-nav > .active > a { 242 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 243 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 244 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 245 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 246 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 247 | background-repeat: repeat-x; 248 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 249 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 250 | } 251 | .navbar-brand, 252 | .navbar-nav > li > a { 253 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 254 | } 255 | .navbar-inverse { 256 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 257 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 258 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 259 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 260 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 261 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 262 | background-repeat: repeat-x; 263 | } 264 | .navbar-inverse .navbar-nav > .open > a, 265 | .navbar-inverse .navbar-nav > .active > a { 266 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 267 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 268 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 269 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 270 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 271 | background-repeat: repeat-x; 272 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 273 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 274 | } 275 | .navbar-inverse .navbar-brand, 276 | .navbar-inverse .navbar-nav > li > a { 277 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 278 | } 279 | .navbar-static-top, 280 | .navbar-fixed-top, 281 | .navbar-fixed-bottom { 282 | border-radius: 0; 283 | } 284 | @media (max-width: 767px) { 285 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 286 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 287 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 288 | color: #fff; 289 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 290 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 291 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 292 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 293 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 294 | background-repeat: repeat-x; 295 | } 296 | } 297 | .alert { 298 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 299 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 300 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 301 | } 302 | .alert-success { 303 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 304 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 305 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 306 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 307 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 308 | background-repeat: repeat-x; 309 | border-color: #b2dba1; 310 | } 311 | .alert-info { 312 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 313 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 314 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 315 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 316 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 317 | background-repeat: repeat-x; 318 | border-color: #9acfea; 319 | } 320 | .alert-warning { 321 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 322 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 323 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 324 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 325 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 326 | background-repeat: repeat-x; 327 | border-color: #f5e79e; 328 | } 329 | .alert-danger { 330 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 331 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 332 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 333 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 334 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 335 | background-repeat: repeat-x; 336 | border-color: #dca7a7; 337 | } 338 | .progress { 339 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 340 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 342 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 344 | background-repeat: repeat-x; 345 | } 346 | .progress-bar { 347 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 348 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 349 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 350 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 351 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 352 | background-repeat: repeat-x; 353 | } 354 | .progress-bar-success { 355 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 356 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 357 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 358 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 359 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 360 | background-repeat: repeat-x; 361 | } 362 | .progress-bar-info { 363 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 364 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 365 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 366 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 367 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 368 | background-repeat: repeat-x; 369 | } 370 | .progress-bar-warning { 371 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 372 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 373 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 374 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 375 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 376 | background-repeat: repeat-x; 377 | } 378 | .progress-bar-danger { 379 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 380 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 381 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 382 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 383 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 384 | background-repeat: repeat-x; 385 | } 386 | .progress-bar-striped { 387 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 388 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 389 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 390 | } 391 | .list-group { 392 | border-radius: 4px; 393 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 394 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 395 | } 396 | .list-group-item.active, 397 | .list-group-item.active:hover, 398 | .list-group-item.active:focus { 399 | text-shadow: 0 -1px 0 #286090; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 405 | background-repeat: repeat-x; 406 | border-color: #2b669a; 407 | } 408 | .list-group-item.active .badge, 409 | .list-group-item.active:hover .badge, 410 | .list-group-item.active:focus .badge { 411 | text-shadow: none; 412 | } 413 | .panel { 414 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 415 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 416 | } 417 | .panel-default > .panel-heading { 418 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 419 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 420 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 421 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 422 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 423 | background-repeat: repeat-x; 424 | } 425 | .panel-primary > .panel-heading { 426 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 427 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 428 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 429 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 430 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 431 | background-repeat: repeat-x; 432 | } 433 | .panel-success > .panel-heading { 434 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 435 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 436 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 437 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 438 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 439 | background-repeat: repeat-x; 440 | } 441 | .panel-info > .panel-heading { 442 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 443 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 444 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 445 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 446 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 447 | background-repeat: repeat-x; 448 | } 449 | .panel-warning > .panel-heading { 450 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 451 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 453 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .panel-danger > .panel-heading { 458 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 459 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 461 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .well { 466 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 467 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 469 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 471 | background-repeat: repeat-x; 472 | border-color: #dcdcdc; 473 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 474 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 475 | } 476 | /*# sourceMappingURL=bootstrap-theme.css.map */ 477 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: workshop 3 | root: . 4 | venue: Space Telescope Science Institute 5 | address: 3700 San Martin Drive, Baltimore, Maryland 21218 6 | country: us 7 | language: en 8 | room: Boardroom 9 | latlng: 39.332728, -76.623277 10 | humandate: Sep 24-25, 2015 11 | humantime: 9:00 am - 5:00 pm 12 | startdate: 2015-09-24 13 | enddate: 2015-09-25 14 | instructor: [Erik Bray, Justin Ely] 15 | helper: [Joseph Long] 16 | contact: embray@stsci.edu 17 | etherpad: https://swcarpentry.etherpad.mozilla.org/2015-09-24-stsci 18 | eventbrite: # optional (insert the alphanumeric key for Eventbrite registration, e.g., "1234567890AB") 19 | --- 20 | 28 | 29 | 37 | {% if page.eventbrite %} 38 | 45 | {% endif %} 46 | 47 |

General Information

48 | 49 | 55 |

56 | Software Carpentry's mission 57 | is to help scientists and engineers get more research done in less 58 | time and with less pain by teaching them basic lab skills for 59 | scientific computing. This hands-on workshop will cover basic 60 | concepts and tools, including program design, version control, data 61 | management, and task automation. Participants will be encouraged to 62 | help one another and to apply what they have learned to their own 63 | research problems. 64 |

65 |

66 | 67 | For more information on what we teach and why, 68 | please see our paper 69 | "Best Practices for Scientific Computing". 70 | 71 |

72 | 73 | 79 |

80 | Who: 81 | Faculty, staff, and post-docs in the INS division. 82 | You don't need to have any previous knowledge of the tools that will 83 | be presented at the workshop, though you will be expected to have some experience working with the command line shell. 84 |

85 | 86 | 94 | {% if page.latlng %} 95 |

96 | Where: 97 | {{page.address}}. 98 | Get directions with 99 | OpenStreetMap 100 | or 101 | Google Maps. 102 |

103 | {% endif %} 104 | 105 | 110 |

111 | Requirements: Participants must bring a laptop with 112 | a few specific software packages installed (listed 113 | below). They are also required to abide by 114 | Software Carpentry's 115 | Code of Conduct. 116 |

117 | 118 | 125 |

126 | Contact: 127 | Please mail 128 | {% if page.contact %} 129 | {{page.contact}} 130 | {% else %} 131 | {{site.contact}} 132 | {% endif %} 133 | for more information. 134 |

135 | 136 |
137 | 138 | 145 |

Schedule

146 |

A loose schedule is outlined below, but we're really here to teach the areas 147 | +of most interest to the students. As such, the topics for each section may 148 | +change based on student feedback. Coffee breaks will be interpersed throughout. Lunch will be at noon.

149 |
150 |
151 |

Day 1

152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
Setup and introductions
Discussion of Unix shell with intermediate tips
Introduction to scientific programming and data analysis with Python
Split: Python for beginners
Split: Building programs with Python
Testing with Python
160 |
161 |
162 |

Day 2

163 | 164 | 165 | 166 | 167 | 168 |
Version control with Git
Managing data with SQL
Programming for databases in Python
Wrap-up
169 |
170 |
171 | 172 | 186 | {% if page.etherpad %} 187 |

188 | Etherpad: {{page.etherpad}}. 189 |
190 | We will use this Etherpad for chatting, taking notes, and sharing URLs and bits of code. 191 |

192 | {% endif %} 193 | 194 |
195 | 196 | 213 |

Syllabus

214 | 215 |
216 | 217 |
218 |

The Unix Shell

219 |
    220 |
  • Files and directories
  • 221 |
  • History and tab completion
  • 222 |
  • Pipes and redirection
  • 223 |
  • Looping over files
  • 224 |
  • Creating and running shell scripts
  • 225 |
  • Finding things
  • 226 |
  • Reference...
  • 227 |
228 |
229 | 230 |
231 |

Programming in Python

232 |
    233 |
  • Using libraries
  • 234 |
  • Working with arrays
  • 235 |
  • Reading and plotting data
  • 236 |
  • Creating and using functions
  • 237 |
  • Loops and conditionals
  • 238 |
  • Defensive programming
  • 239 |
  • Using Python from the command line
  • 240 |
  • Reference...
  • 241 |
242 |
243 | 256 | 269 |
270 | 271 |
272 |
273 |

Version Control with Git

274 |
    275 |
  • Creating a repository
  • 276 |
  • Recording changes to files: add, commit, ...
  • 277 |
  • Viewing changes: status, diff, ...
  • 278 |
  • Ignoring files
  • 279 |
  • Working on the web: clone, pull, push, ...
  • 280 |
  • Resolving conflicts
  • 281 |
  • Open licenses
  • 282 |
  • Where to host work, and why
  • 283 |
  • Reference...
  • 284 |
285 |
286 |
287 |

Managing Data with SQL

288 |
    289 |
  • Reading and sorting data
  • 290 |
  • Filtering with where
  • 291 |
  • Calculating new values on the fly
  • 292 |
  • Handling missing values
  • 293 |
  • Combining values using aggregation
  • 294 |
  • Combining information from multiple tables using join
  • 295 |
  • Creating, modifying, and deleting data
  • 296 |
  • Programming with databases
  • 297 |
  • Reference...
  • 298 |
299 |
300 |
301 | 302 |
303 | 304 | 315 | 316 |

Setup

317 | 318 |

319 | To participate in this Software Carpentry workshop, you will need 320 | access to the software described below. 321 |

322 |

323 | Most of the software outlined below should be pre-installed on your system (particularly if 324 | you're using an STScI-provided laptop). However, you should go over each section and veryify that 325 | you do indeed have it installed. 326 |

327 | 328 |
329 |

The Bash Shell

330 | 331 |

332 | Bash is a commonly-used shell that gives you the power to do simple 333 | tasks more quickly. 334 |

335 | 336 |
337 |
338 |

Windows

339 |

340 | Download the Git for Windows 341 | installer. Run the 342 | installer. Important: on the 6th page of the installation wizard (the 343 | page titled `Configuring the terminal emulator...`) select `Use Windows' 344 | default console window`. If you forgot to do this programs that you need 345 | for the workshop will not work properly. If this happens rerun the 346 | installer and select the appropriate option. This will provide you 347 | with both Git and Bash in the Git Bash program. 348 |

349 |
350 |
351 |

Mac OS X

352 |

353 | The default shell in all versions of Mac OS X is bash, so no 354 | need to install anything. You access bash from the Terminal 355 | (found in 356 | /Applications/Utilities). You may want to keep 357 | Terminal in your dock for this workshop. 358 |

359 |
360 |
361 |

Linux

362 |

363 | The default shell is usually Bash, but if your 364 | machine is set up differently you can run it by opening a 365 | terminal and typing bash. There is no need to 366 | install anything. 367 |

368 |
369 |
370 |
371 | 372 |
374 |

Git

375 | 376 |

377 | Git is a version control system that lets you track who made changes 378 | to what when and has options for easily updating a shared or public 379 | version of your code 380 | on github.com. You will need a 381 | supported 382 | web browser (current versions of Chrome, Firefox or Safari, 383 | or Internet Explorer version 9 or above). 384 |

385 | 386 |
387 |
388 |

Windows

389 |

390 | Git should be installed on your computer as part of your Bash 391 | install (described above). 392 |

393 |
394 |
395 |

Mac OS X

396 |

397 | For OS X 10.9 and higher, install Git for Mac 398 | by downloading and running the most recent "mavericks" installer from 399 | this list. 400 | After installing Git, there will not be anything in your /Applications folder, 401 | as Git is a command line program. 402 | For older versions of OS X (10.5-10.8) use the 403 | most recent available installer labelled "snow-leopard" 404 | available here. 405 |

406 |
407 |
408 |

Linux

409 |

410 | If Git is not already available on your machine you can try to 411 | install it via your distro's package manager. For Debian/Ubuntu run 412 | sudo apt-get install git and for Fedora run 413 | sudo yum install git. 414 |

415 |
416 |
417 |
418 | 479 | 480 | 481 |
485 |

Python

486 | 487 |

488 | Python is a popular language for 489 | scientific computing, and great for general-purpose programming as 490 | well. For this course we'll be using the standard STScI science package 491 | Ureka. If you're using an 492 | STScI laptop, this should already be installed. 493 |

494 | 495 |

496 | We will teach Python using the IPython notebook, a programming environment 497 | that runs in a web browser. For this to work you will need a reasonably 498 | up-to-date browser. The current versions of the Chrome, Safari and 499 | Firefox browsers are all supported 501 | (some older browsers, including Internet Explorer version 9 502 | and below, are not). 503 |

504 | 561 |
562 | 563 | 608 | 609 |
610 |

SQLite

611 | 612 |

613 | SQL is a specialized programming language used with databases. We 614 | use a simple database manager called 615 | SQLite in our lessons. 616 |

617 | 618 |
619 |
620 |

Windows

621 |

622 | The Software Carpentry Windows Installer 623 | installs SQLite for Windows. 624 | If you used the installer to configure nano, you don't need to run it again. 625 |

626 |
627 |
628 |

Mac OS X

629 |

630 | SQLite comes pre-installed on Mac OS X. 631 |

632 |
633 |
634 |

Linux

635 |

636 | SQLite comes pre-installed on Linux. 637 |

638 |
639 |
640 | 641 |
642 | 643 | 670 | -------------------------------------------------------------------------------- /setup/swc-installation-test-2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test script to check for required functionality. 4 | 5 | Execute this code at the command line by typing: 6 | 7 | python swc-installation-test-2.py 8 | 9 | Run the script and follow the instructions it prints at the end. 10 | 11 | This script requires at least Python 2.6. You can check the version 12 | of Python that you have installed with 'swc-installation-test-1.py'. 13 | 14 | By default, this script will test for all the dependencies your 15 | instructor thinks you need. If you want to test for a different set 16 | of packages, you can list them on the command line. For example: 17 | 18 | python swc-installation-test-2.py git virtual-editor 19 | 20 | This is useful if the original test told you to install a more recent 21 | version of a particular dependency, and you just want to re-test that 22 | dependency. 23 | """ 24 | 25 | from __future__ import print_function # for Python 2.6 compatibility 26 | 27 | import distutils.ccompiler as _distutils_ccompiler 28 | import fnmatch as _fnmatch 29 | try: # Python 2.7 and 3.x 30 | import importlib as _importlib 31 | except ImportError: # Python 2.6 and earlier 32 | class _Importlib (object): 33 | """Minimal workarounds for functions we need 34 | """ 35 | @staticmethod 36 | def import_module(name): 37 | module = __import__(name) 38 | for n in name.split('.')[1:]: 39 | module = getattr(module, n) 40 | return module 41 | _importlib = _Importlib() 42 | import logging as _logging 43 | import os as _os 44 | import platform as _platform 45 | import re as _re 46 | import shlex as _shlex 47 | import subprocess as _subprocess 48 | import sys as _sys 49 | try: # Python 3.x 50 | import urllib.parse as _urllib_parse 51 | except ImportError: # Python 2.x 52 | import urllib as _urllib_parse # for quote() 53 | 54 | 55 | if not hasattr(_shlex, 'quote'): # Python versions older than 3.3 56 | # Use the undocumented pipes.quote() 57 | import pipes as _pipes 58 | _shlex.quote = _pipes.quote 59 | 60 | 61 | __version__ = '0.1' 62 | 63 | # Comment out any entries you don't need 64 | CHECKS = [ 65 | # Shell 66 | 'virtual-shell', 67 | # Editors 68 | 'virtual-editor', 69 | # Browsers 70 | 'virtual-browser', 71 | # Version control 72 | 'git', 73 | 'hg', # Command line tool 74 | #'mercurial', # Python package 75 | 'EasyMercurial', 76 | # Build tools and packaging 77 | 'make', 78 | 'virtual-pypi-installer', 79 | 'setuptools', 80 | #'xcode', 81 | # Testing 82 | 'nosetests', # Command line tool 83 | 'nose', # Python package 84 | 'py.test', # Command line tool 85 | 'pytest', # Python package 86 | # SQL 87 | 'sqlite3', # Command line tool 88 | 'sqlite3-python', # Python package 89 | # Python 90 | 'python', 91 | 'ipython', # Command line tool 92 | 'IPython', # Python package 93 | 'argparse', # Useful for utility scripts 94 | 'numpy', 95 | 'scipy', 96 | 'matplotlib', 97 | 'pandas', 98 | 'sympy', 99 | 'Cython', 100 | 'networkx', 101 | 'mayavi.mlab', 102 | ] 103 | 104 | CHECKER = {} 105 | 106 | _ROOT_PATH = _os.sep 107 | if _platform.system() == 'win32': 108 | _ROOT_PATH = 'c:\\' 109 | 110 | 111 | class InvalidCheck (KeyError): 112 | def __init__(self, check): 113 | super(InvalidCheck, self).__init__(check) 114 | self.check = check 115 | 116 | def __str__(self): 117 | return self.check 118 | 119 | 120 | class DependencyError (Exception): 121 | _default_url = 'http://software-carpentry.org/setup/' 122 | _setup_urls = { # (system, version, package) glob pairs 123 | ('*', '*', 'Cython'): 'http://docs.cython.org/src/quickstart/install.html', 124 | ('Linux', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-linux', 125 | ('Darwin', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-mac', 126 | ('Windows', '*', 'EasyMercurial'): 'http://easyhg.org/download.html#download-windows', 127 | ('*', '*', 'EasyMercurial'): 'http://easyhg.org/download.html', 128 | ('*', '*', 'argparse'): 'https://pypi.python.org/pypi/argparse#installation', 129 | ('*', '*', 'ash'): 'http://www.in-ulm.de/~mascheck/various/ash/', 130 | ('*', '*', 'bash'): 'http://www.gnu.org/software/bash/manual/html_node/Basic-Installation.html#Basic-Installation', 131 | ('Linux', '*', 'chromium'): 'http://code.google.com/p/chromium/wiki/LinuxBuildInstructions', 132 | ('Darwin', '*', 'chromium'): 'http://code.google.com/p/chromium/wiki/MacBuildInstructions', 133 | ('Windows', '*', 'chromium'): 'http://www.chromium.org/developers/how-tos/build-instructions-windows', 134 | ('*', '*', 'chromium'): 'http://www.chromium.org/developers/how-tos', 135 | ('Windows', '*', 'emacs'): 'http://www.gnu.org/software/emacs/windows/Installing-Emacs.html', 136 | ('*', '*', 'emacs'): 'http://www.gnu.org/software/emacs/#Obtaining', 137 | ('*', '*', 'firefox'): 'http://www.mozilla.org/en-US/firefox/new/', 138 | ('Linux', '*', 'gedit'): 'http://www.linuxfromscratch.org/blfs/view/svn/gnome/gedit.html', 139 | ('*', '*', 'git'): 'http://git-scm.com/downloads', 140 | ('*', '*', 'google-chrome'): 'https://www.google.com/intl/en/chrome/browser/', 141 | ('*', '*', 'hg'): 'http://mercurial.selenic.com/', 142 | ('*', '*', 'mercurial'): 'http://mercurial.selenic.com/', 143 | ('*', '*', 'IPython'): 'http://ipython.org/install.html', 144 | ('*', '*', 'ipython'): 'http://ipython.org/install.html', 145 | ('*', '*', 'jinja'): 'http://jinja.pocoo.org/docs/intro/#installation', 146 | ('*', '*', 'kate'): 'http://kate-editor.org/get-it/', 147 | ('*', '*', 'make'): 'http://www.gnu.org/software/make/', 148 | ('Darwin', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#building-on-osx', 149 | ('Windows', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#installing-on-windows', 150 | ('*', '*', 'matplotlib'): 'http://matplotlib.org/users/installing.html#installing', 151 | ('*', '*', 'mayavi.mlab'): 'http://docs.enthought.com/mayavi/mayavi/installation.html', 152 | ('*', '*', 'nano'): 'http://www.nano-editor.org/dist/latest/faq.html#3', 153 | ('*', '*', 'networkx'): 'http://networkx.github.com/documentation/latest/install.html#installing', 154 | ('*', '*', 'nose'): 'https://nose.readthedocs.org/en/latest/#installation-and-quick-start', 155 | ('*', '*', 'nosetests'): 'https://nose.readthedocs.org/en/latest/#installation-and-quick-start', 156 | ('*', '*', 'notepad++'): 'http://notepad-plus-plus.org/download/v6.3.html', 157 | ('*', '*', 'numpy'): 'http://docs.scipy.org/doc/numpy/user/install.html', 158 | ('*', '*', 'pandas'): 'http://pandas.pydata.org/pandas-docs/stable/install.html', 159 | ('*', '*', 'pip'): 'http://www.pip-installer.org/en/latest/installing.html', 160 | ('*', '*', 'pytest'): 'http://pytest.org/latest/getting-started.html', 161 | ('*', '*', 'python'): 'http://www.python.org/download/releases/2.7.3/#download', 162 | ('*', '*', 'pyzmq'): 'https://github.com/zeromq/pyzmq/wiki/Building-and-Installing-PyZMQ', 163 | ('*', '*', 'py.test'): 'http://pytest.org/latest/getting-started.html', 164 | ('Linux', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Linux', 165 | ('Darwin', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Mac_OS_X', 166 | ('Windows', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy/Windows', 167 | ('*', '*', 'scipy'): 'http://www.scipy.org/Installing_SciPy', 168 | ('*', '*', 'setuptools'): 'https://pypi.python.org/pypi/setuptools#installation-instructions', 169 | ('*', '*', 'sqlite3'): 'http://www.sqlite.org/download.html', 170 | ('*', '*', 'sublime-text'): 'http://www.sublimetext.com/2', 171 | ('*', '*', 'sympy'): 'http://docs.sympy.org/dev/install.html', 172 | ('Darwin', '*', 'textmate'): 'http://macromates.com/', 173 | ('Darwin', '*', 'textwrangler'): 'http://www.barebones.com/products/textwrangler/download.html', 174 | ('*', '*', 'tornado'): 'http://www.tornadoweb.org/', 175 | ('*', '*', 'vim'): 'http://www.vim.org/download.php', 176 | ('Darwin', '*', 'xcode'): 'https://developer.apple.com/xcode/', 177 | ('*', '*', 'xemacs'): 'http://www.us.xemacs.org/Install/', 178 | ('*', '*', 'zsh'): 'http://www.zsh.org/', 179 | } 180 | 181 | def _get_message(self): 182 | return self._message 183 | def _set_message(self, message): 184 | self._message = message 185 | message = property(_get_message, _set_message) 186 | 187 | def __init__(self, checker, message, causes=None): 188 | super(DependencyError, self).__init__(message) 189 | self.checker = checker 190 | self.message = message 191 | if causes is None: 192 | causes = [] 193 | self.causes = causes 194 | 195 | def get_url(self): 196 | system = _platform.system() 197 | version = None 198 | for pversion in ( 199 | 'linux_distribution', 200 | 'mac_ver', 201 | 'win32_ver', 202 | ): 203 | value = getattr(_platform, pversion)() 204 | if value[0]: 205 | version = value[0] 206 | break 207 | package = self.checker.name 208 | for (s,v,p),url in self._setup_urls.items(): 209 | if (_fnmatch.fnmatch(system, s) and 210 | _fnmatch.fnmatch(version, v) and 211 | _fnmatch.fnmatch(package, p)): 212 | return url 213 | return self._default_url 214 | 215 | def __str__(self): 216 | url = self.get_url() 217 | lines = [ 218 | 'check for {0} failed:'.format(self.checker.full_name()), 219 | ' ' + self.message, 220 | ' For instructions on installing an up-to-date version, see', 221 | ' ' + url, 222 | ] 223 | if self.causes: 224 | lines.append(' causes:') 225 | for cause in self.causes: 226 | lines.extend(' ' + line for line in str(cause).splitlines()) 227 | return '\n'.join(lines) 228 | 229 | 230 | def check(checks=None): 231 | successes = [] 232 | failures = [] 233 | if not checks: 234 | checks = CHECKS 235 | for check in checks: 236 | try: 237 | checker = CHECKER[check] 238 | except KeyError as e: 239 | raise InvalidCheck(check)# from e 240 | _sys.stdout.write('check {0}...\t'.format(checker.full_name())) 241 | try: 242 | version = checker.check() 243 | except DependencyError as e: 244 | failures.append(e) 245 | _sys.stdout.write('fail\n') 246 | else: 247 | _sys.stdout.write('pass\n') 248 | successes.append((checker, version)) 249 | if successes: 250 | print('\nSuccesses:\n') 251 | for checker,version in successes: 252 | print('{0} {1}'.format( 253 | checker.full_name(), 254 | version or 'unknown')) 255 | if failures: 256 | print('\nFailures:') 257 | printed = [] 258 | for failure in failures: 259 | if failure not in printed: 260 | print() 261 | print(failure) 262 | printed.append(failure) 263 | return False 264 | return True 265 | 266 | 267 | class Dependency (object): 268 | def __init__(self, name, long_name=None, minimum_version=None, 269 | version_delimiter='.', and_dependencies=None, 270 | or_dependencies=None): 271 | self.name = name 272 | self.long_name = long_name or name 273 | self.minimum_version = minimum_version 274 | self.version_delimiter = version_delimiter 275 | if not and_dependencies: 276 | and_dependencies = [] 277 | self.and_dependencies = and_dependencies 278 | if not or_dependencies: 279 | or_dependencies = [] 280 | self.or_dependencies = or_dependencies 281 | self._check_error = None 282 | 283 | def __str__(self): 284 | return '<{0} {1}>'.format(type(self).__name__, self.name) 285 | 286 | def full_name(self): 287 | if self.name == self.long_name: 288 | return self.name 289 | else: 290 | return '{0} ({1})'.format(self.long_name, self.name) 291 | 292 | def check(self): 293 | if self._check_error: 294 | raise self._check_error 295 | try: 296 | self._check_dependencies() 297 | return self._check() 298 | except DependencyError as e: 299 | self._check_error = e # cache for future calls 300 | raise 301 | 302 | def _check_dependencies(self): 303 | for dependency in self.and_dependencies: 304 | if not hasattr(dependency, 'check'): 305 | dependency = CHECKER[dependency] 306 | try: 307 | dependency.check() 308 | except DependencyError as e: 309 | raise DependencyError( 310 | checker=self, 311 | message=( 312 | 'some dependencies for {0} were not satisfied' 313 | ).format(self.full_name()), 314 | causes=[e]) 315 | self.or_pass = None 316 | or_errors = [] 317 | for dependency in self.or_dependencies: 318 | if not hasattr(dependency, 'check'): 319 | dependency = CHECKER[dependency] 320 | try: 321 | version = dependency.check() 322 | except DependencyError as e: 323 | or_errors.append(e) 324 | else: 325 | self.or_pass = { 326 | 'dependency': dependency, 327 | 'version': version, 328 | } 329 | break # no need to test other dependencies 330 | if self.or_dependencies and not self.or_pass: 331 | raise DependencyError( 332 | checker=self, 333 | message=( 334 | '{0} requires at least one of the following dependencies' 335 | ).format(self.full_name()), 336 | causes=or_errors) 337 | 338 | def _check(self): 339 | version = self._get_version() 340 | parsed_version = None 341 | if hasattr(self, '_get_parsed_version'): 342 | parsed_version = self._get_parsed_version() 343 | if self.minimum_version: 344 | self._check_version(version=version, parsed_version=parsed_version) 345 | return version 346 | 347 | def _get_version(self): 348 | raise NotImplementedError(self) 349 | 350 | def _minimum_version_string(self): 351 | return self.version_delimiter.join( 352 | str(part) for part in self.minimum_version) 353 | 354 | def _check_version(self, version, parsed_version=None): 355 | if not parsed_version: 356 | parsed_version = self._parse_version(version=version) 357 | if not parsed_version or parsed_version < self.minimum_version: 358 | raise DependencyError( 359 | checker=self, 360 | message='outdated version of {0}: {1} (need >= {2})'.format( 361 | self.full_name(), version, self._minimum_version_string())) 362 | 363 | def _parse_version(self, version): 364 | if not version: 365 | return None 366 | parsed_version = [] 367 | for part in version.split(self.version_delimiter): 368 | try: 369 | parsed_version.append(int(part)) 370 | except ValueError as e: 371 | raise DependencyError( 372 | checker=self, 373 | message=( 374 | 'unparsable {0!r} in version {1} of {2}, (need >= {3})' 375 | ).format( 376 | part, version, self.full_name(), 377 | self._minimum_version_string()))# from e 378 | return tuple(parsed_version) 379 | 380 | 381 | class PythonDependency (Dependency): 382 | def __init__(self, name='python', long_name='Python version', 383 | minimum_version=(2, 6), **kwargs): 384 | super(PythonDependency, self).__init__( 385 | name=name, long_name=long_name, minimum_version=minimum_version, 386 | **kwargs) 387 | 388 | def _get_version(self): 389 | return _sys.version 390 | 391 | def _get_parsed_version(self): 392 | return _sys.version_info 393 | 394 | 395 | CHECKER['python'] = PythonDependency() 396 | 397 | 398 | class CommandDependency (Dependency): 399 | exe_extension = _distutils_ccompiler.new_compiler().exe_extension 400 | 401 | def __init__(self, command, paths=None, version_options=('--version',), 402 | stdin=None, version_regexp=None, version_stream='stdout', 403 | **kwargs): 404 | if 'name' not in kwargs: 405 | kwargs['name'] = command 406 | super(CommandDependency, self).__init__(**kwargs) 407 | self.command = command 408 | self.paths = paths 409 | self.version_options = version_options 410 | self.stdin = None 411 | if not version_regexp: 412 | regexp = r'([\d][\d{0}]*[\d])'.format(self.version_delimiter) 413 | version_regexp = _re.compile(regexp) 414 | self.version_regexp = version_regexp 415 | self.version_stream = version_stream 416 | 417 | def _get_command_version_stream(self, command=None, stdin=None, 418 | expect=(0,)): 419 | if command is None: 420 | command = self.command + (self.exe_extension or '') 421 | if not stdin: 422 | stdin = self.stdin 423 | if stdin: 424 | popen_stdin = _subprocess.PIPE 425 | else: 426 | popen_stdin = None 427 | try: 428 | p = _subprocess.Popen( 429 | [command] + list(self.version_options), stdin=popen_stdin, 430 | stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, 431 | universal_newlines=True) 432 | except OSError as e: 433 | raise DependencyError( 434 | checker=self, 435 | message="could not find '{0}' executable".format(command), 436 | )# from e 437 | stdout,stderr = p.communicate(stdin) 438 | status = p.wait() 439 | if status not in expect: 440 | lines = [ 441 | "failed to execute: {0} {1}".format( 442 | command, 443 | ' '.join(_shlex.quote(arg) 444 | for arg in self.version_options)), 445 | 'status: {0}'.format(status), 446 | ] 447 | for name,string in [('stdout', stdout), ('stderr', stderr)]: 448 | if string: 449 | lines.extend([name + ':', string]) 450 | raise DependencyError(checker=self, message='\n'.join(lines)) 451 | for name,string in [('stdout', stdout), ('stderr', stderr)]: 452 | if name == self.version_stream: 453 | if not string: 454 | raise DependencyError( 455 | checker=self, 456 | message='empty version stream on {0} for {1}'.format( 457 | self.version_stream, command)) 458 | return string 459 | raise NotImplementedError(self.version_stream) 460 | 461 | def _get_version_stream(self, **kwargs): 462 | paths = [self.command + (self.exe_extension or '')] 463 | if self.exe_extension: 464 | paths.append(self.command) # also look at the extension-less path 465 | if self.paths: 466 | paths.extend(self.paths) 467 | or_errors = [] 468 | for path in paths: 469 | try: 470 | return self._get_command_version_stream(command=path, **kwargs) 471 | except DependencyError as e: 472 | or_errors.append(e) 473 | raise DependencyError( 474 | checker=self, 475 | message='errors finding {0} version'.format( 476 | self.full_name()), 477 | causes=or_errors) 478 | 479 | def _get_version(self): 480 | version_stream = self._get_version_stream() 481 | match = self.version_regexp.search(version_stream) 482 | if not match: 483 | raise DependencyError( 484 | checker=self, 485 | message='no version string in output:\n{0}'.format( 486 | version_stream)) 487 | return match.group(1) 488 | 489 | 490 | def _program_files_paths(*args): 491 | "Utility for generating MS Windows search paths" 492 | pf = _os.environ.get('ProgramFiles', '/usr/bin') 493 | pfx86 = _os.environ.get('ProgramFiles(x86)', pf) 494 | paths = [_os.path.join(pf, *args)] 495 | if pfx86 != pf: 496 | paths.append(_os.path.join(pfx86, *args)) 497 | return paths 498 | 499 | 500 | for command,long_name,minimum_version,paths in [ 501 | ('sh', 'Bourne Shell', None, None), 502 | ('ash', 'Almquist Shell', None, None), 503 | ('bash', 'Bourne Again Shell', None, None), 504 | ('csh', 'C Shell', None, None), 505 | ('ksh', 'KornShell', None, None), 506 | ('dash', 'Debian Almquist Shell', None, None), 507 | ('tcsh', 'TENEX C Shell', None, None), 508 | ('zsh', 'Z Shell', None, None), 509 | ('git', 'Git', (1, 7, 0), None), 510 | ('hg', 'Mercurial', (2, 0, 0), None), 511 | ('EasyMercurial', None, (1, 3), None), 512 | ('pip', None, None, None), 513 | ('sqlite3', 'SQLite 3', None, None), 514 | ('nosetests', 'Nose', (1, 0, 0), None), 515 | ('ipython', 'IPython script', (1, 0), None), 516 | ('emacs', 'Emacs', None, None), 517 | ('xemacs', 'XEmacs', None, None), 518 | ('vim', 'Vim', None, None), 519 | ('vi', None, None, None), 520 | ('nano', 'Nano', None, None), 521 | ('gedit', None, None, None), 522 | ('kate', 'Kate', None, None), 523 | ('notepad++', 'Notepad++', None, 524 | _program_files_paths('Notepad++', 'notepad++.exe')), 525 | ('firefox', 'Firefox', None, 526 | _program_files_paths('Mozilla Firefox', 'firefox.exe')), 527 | ('google-chrome', 'Google Chrome', None, 528 | _program_files_paths('Google', 'Chrome', 'Application', 'chrome.exe') 529 | ), 530 | ('chromium', 'Chromium', None, None), 531 | ]: 532 | if not long_name: 533 | long_name = command 534 | CHECKER[command] = CommandDependency( 535 | command=command, paths=paths, long_name=long_name, 536 | minimum_version=minimum_version) 537 | del command, long_name, minimum_version, paths # cleanup namespace 538 | 539 | 540 | class MakeDependency (CommandDependency): 541 | makefile = '\n'.join([ 542 | 'all:', 543 | '\t@echo "MAKE_VERSION=$(MAKE_VERSION)"', 544 | '\t@echo "MAKE=$(MAKE)"', 545 | '', 546 | ]) 547 | 548 | def _get_version(self): 549 | try: 550 | return super(MakeDependency, self)._get_version() 551 | except DependencyError as e: 552 | version_options = self.version_options 553 | self.version_options = ['-f', '-'] 554 | try: 555 | stream = self._get_version_stream(stdin=self.makefile) 556 | info = {} 557 | for line in stream.splitlines(): 558 | try: 559 | key,value = line.split('=', 1) 560 | except ValueError as ve: 561 | raise e# from NotImplementedError(stream) 562 | info[key] = value 563 | if info.get('MAKE_VERSION', None): 564 | return info['MAKE_VERSION'] 565 | elif info.get('MAKE', None): 566 | return None 567 | raise e 568 | finally: 569 | self.version_options = version_options 570 | 571 | 572 | CHECKER['make'] = MakeDependency(command='make', minimum_version=None) 573 | 574 | 575 | class EasyInstallDependency (CommandDependency): 576 | def _get_version(self): 577 | try: 578 | return super(EasyInstallDependency, self)._get_version() 579 | except DependencyError as e: 580 | version_stream = self.version_stream 581 | try: 582 | self.version_stream = 'stderr' 583 | stream = self._get_version_stream(expect=(1,)) 584 | if 'option --version not recognized' in stream: 585 | return 'unknown (possibly Setuptools?)' 586 | finally: 587 | self.version_stream = version_stream 588 | 589 | 590 | CHECKER['easy_install'] = EasyInstallDependency( 591 | command='easy_install', long_name='Setuptools easy_install', 592 | minimum_version=None) 593 | 594 | 595 | CHECKER['py.test'] = CommandDependency( 596 | command='py.test', version_stream='stderr', 597 | minimum_version=None) 598 | 599 | 600 | class PathCommandDependency (CommandDependency): 601 | """A command that doesn't support --version or equivalent options 602 | 603 | On some operating systems (e.g. OS X), a command's executable may 604 | be hard to find, or not exist in the PATH. Work around that by 605 | just checking for the existence of a characteristic file or 606 | directory. Since the characteristic path may depend on OS, 607 | installed version, etc., take a list of paths, and succeed if any 608 | of them exists. 609 | """ 610 | def _get_command_version_stream(self, *args, **kwargs): 611 | raise NotImplementedError() 612 | 613 | def _get_version_stream(self, *args, **kwargs): 614 | raise NotImplementedError() 615 | 616 | def _get_version(self): 617 | for path in self.paths: 618 | if _os.path.exists(path): 619 | return None 620 | raise DependencyError( 621 | checker=self, 622 | message=( 623 | 'nothing exists at any of the expected paths for {0}:\n {1}' 624 | ).format( 625 | self.full_name(), 626 | '\n '.join(p for p in self.paths))) 627 | 628 | 629 | for paths,name,long_name in [ 630 | ([_os.path.join(_ROOT_PATH, 'Applications', 'Sublime Text 2.app')], 631 | 'sublime-text', 'Sublime Text'), 632 | ([_os.path.join(_ROOT_PATH, 'Applications', 'TextMate.app')], 633 | 'textmate', 'TextMate'), 634 | ([_os.path.join(_ROOT_PATH, 'Applications', 'TextWrangler.app')], 635 | 'textwrangler', 'TextWrangler'), 636 | ([_os.path.join(_ROOT_PATH, 'Applications', 'Safari.app')], 637 | 'safari', 'Safari'), 638 | ([_os.path.join(_ROOT_PATH, 'Applications', 'Xcode.app'), # OS X >=1.7 639 | _os.path.join(_ROOT_PATH, 'Developer', 'Applications', 'Xcode.app' 640 | ) # OS X 1.6, 641 | ], 642 | 'xcode', 'Xcode'), 643 | ]: 644 | if not long_name: 645 | long_name = name 646 | CHECKER[name] = PathCommandDependency( 647 | command=None, paths=paths, name=name, long_name=long_name) 648 | del paths, name, long_name # cleanup namespace 649 | 650 | 651 | class PythonPackageDependency (Dependency): 652 | def __init__(self, package, **kwargs): 653 | if 'name' not in kwargs: 654 | kwargs['name'] = package 655 | if 'and_dependencies' not in kwargs: 656 | kwargs['and_dependencies'] = [] 657 | if 'python' not in kwargs['and_dependencies']: 658 | kwargs['and_dependencies'].append('python') 659 | super(PythonPackageDependency, self).__init__(**kwargs) 660 | self.package = package 661 | 662 | def _get_version(self): 663 | package = self._get_package(self.package) 664 | return self._get_version_from_package(package) 665 | 666 | def _get_package(self, package): 667 | try: 668 | return _importlib.import_module(package) 669 | except ImportError as e: 670 | raise DependencyError( 671 | checker=self, 672 | message="could not import the '{0}' package for {1}".format( 673 | package, self.full_name()), 674 | )# from e 675 | 676 | def _get_version_from_package(self, package): 677 | try: 678 | version = package.__version__ 679 | except AttributeError: 680 | version = None 681 | return version 682 | 683 | 684 | for package,name,long_name,minimum_version,and_dependencies in [ 685 | ('nose', None, 'Nose Python package', 686 | CHECKER['nosetests'].minimum_version, None), 687 | ('pytest', None, 'pytest Python package', 688 | CHECKER['py.test'].minimum_version, None), 689 | ('jinja2', 'jinja', 'Jinja', (2, 6), None), 690 | ('zmq', 'pyzmq', 'PyZMQ', (2, 1, 4), None), 691 | ('IPython', None, 'IPython Python package', 692 | CHECKER['ipython'].minimum_version, ['jinja', 'tornado', 'pyzmq']), 693 | ('argparse', None, 'Argparse', None, None), 694 | ('numpy', None, 'NumPy', None, None), 695 | ('scipy', None, 'SciPy', None, None), 696 | ('matplotlib', None, 'Matplotlib', None, None), 697 | ('pandas', None, 'Pandas', (0, 8), None), 698 | ('sympy', None, 'SymPy', None, None), 699 | ('Cython', None, None, None, None), 700 | ('networkx', None, 'NetworkX', None, None), 701 | ('mayavi.mlab', None, 'MayaVi', None, None), 702 | ('setuptools', None, 'Setuptools', None, None), 703 | ]: 704 | if not name: 705 | name = package 706 | if not long_name: 707 | long_name = name 708 | kwargs = {} 709 | if and_dependencies: 710 | kwargs['and_dependencies'] = and_dependencies 711 | CHECKER[name] = PythonPackageDependency( 712 | package=package, name=name, long_name=long_name, 713 | minimum_version=minimum_version, **kwargs) 714 | # cleanup namespace 715 | del package, name, long_name, minimum_version, and_dependencies, kwargs 716 | 717 | 718 | class MercurialPythonPackage (PythonPackageDependency): 719 | def _get_version(self): 720 | try: # mercurial >= 1.2 721 | package = _importlib.import_module('mercurial.util') 722 | except ImportError as e: # mercurial <= 1.1.2 723 | package = self._get_package('mercurial.version') 724 | return package.get_version() 725 | else: 726 | return package.version() 727 | 728 | 729 | CHECKER['mercurial'] = MercurialPythonPackage( 730 | package='mercurial.util', name='mercurial', 731 | long_name='Mercurial Python package', 732 | minimum_version=CHECKER['hg'].minimum_version) 733 | 734 | 735 | class TornadoPythonPackage (PythonPackageDependency): 736 | def _get_version_from_package(self, package): 737 | return package.version 738 | 739 | def _get_parsed_version(self): 740 | package = self._get_package(self.package) 741 | return package.version_info 742 | 743 | 744 | CHECKER['tornado'] = TornadoPythonPackage( 745 | package='tornado', name='tornado', long_name='Tornado', minimum_version=(2, 0)) 746 | 747 | 748 | class SQLitePythonPackage (PythonPackageDependency): 749 | def _get_version_from_package(self, package): 750 | return _sys.version 751 | 752 | def _get_parsed_version(self): 753 | return _sys.version_info 754 | 755 | 756 | CHECKER['sqlite3-python'] = SQLitePythonPackage( 757 | package='sqlite3', name='sqlite3-python', 758 | long_name='SQLite Python package', 759 | minimum_version=CHECKER['sqlite3'].minimum_version) 760 | 761 | 762 | class UserTaskDependency (Dependency): 763 | "Prompt the user to complete a task and check for success" 764 | def __init__(self, prompt, **kwargs): 765 | super(UserTaskDependency, self).__init__(**kwargs) 766 | self.prompt = prompt 767 | 768 | def _check(self): 769 | if _sys.version_info >= (3, ): 770 | result = input(self.prompt) 771 | else: # Python 2.x 772 | result = raw_input(self.prompt) 773 | return self._check_result(result) 774 | 775 | def _check_result(self, result): 776 | raise NotImplementedError() 777 | 778 | 779 | class EditorTaskDependency (UserTaskDependency): 780 | def __init__(self, **kwargs): 781 | self.path = _os.path.expanduser(_os.path.join( 782 | '~', 'swc-installation-test.txt')) 783 | self.contents = 'Hello, world!' 784 | super(EditorTaskDependency, self).__init__( 785 | prompt=( 786 | 'Open your favorite text editor and create the file\n' 787 | ' {0}\n' 788 | 'containing the line:\n' 789 | ' {1}\n' 790 | 'Press enter here after you have done this.\n' 791 | 'You may remove the file after you have finished testing.' 792 | ).format(self.path, self.contents), 793 | **kwargs) 794 | 795 | def _check_result(self, result): 796 | message = None 797 | try: 798 | with open(self.path, 'r') as f: 799 | contents = f.read() 800 | except IOError as e: 801 | raise DependencyError( 802 | checker=self, 803 | message='could not open {0!r}: {1}'.format(self.path, e) 804 | )# from e 805 | if contents.strip() != self.contents: 806 | raise DependencyError( 807 | checker=self, 808 | message=( 809 | 'file contents ({0!r}) did not match the expected {1!r}' 810 | ).format(contents, self.contents)) 811 | 812 | 813 | CHECKER['other-editor'] = EditorTaskDependency( 814 | name='other-editor', long_name='') 815 | 816 | 817 | class VirtualDependency (Dependency): 818 | def _check(self): 819 | return '{0} {1}'.format( 820 | self.or_pass['dependency'].full_name(), 821 | self.or_pass['version']) 822 | 823 | 824 | for name,long_name,dependencies in [ 825 | ('virtual-shell', 'command line shell', ( 826 | 'bash', 827 | 'dash', 828 | 'ash', 829 | 'zsh', 830 | 'ksh', 831 | 'csh', 832 | 'tcsh', 833 | 'sh', 834 | )), 835 | ('virtual-editor', 'text/code editor', ( 836 | 'emacs', 837 | 'xemacs', 838 | 'vim', 839 | 'vi', 840 | 'nano', 841 | 'gedit', 842 | 'kate', 843 | 'notepad++', 844 | 'sublime-text', 845 | 'textmate', 846 | 'textwrangler', 847 | 'other-editor', # last because it requires user interaction 848 | )), 849 | ('virtual-browser', 'web browser', ( 850 | 'firefox', 851 | 'google-chrome', 852 | 'chromium', 853 | 'safari', 854 | )), 855 | ('virtual-pypi-installer', 'PyPI installer', ( 856 | 'pip', 857 | 'easy_install', 858 | )), 859 | ]: 860 | CHECKER[name] = VirtualDependency( 861 | name=name, long_name=long_name, or_dependencies=dependencies) 862 | del name, long_name, dependencies # cleanup namespace 863 | 864 | 865 | def _print_info(key, value, indent=19): 866 | print('{0}{1}: {2}'.format(key, ' '*(indent-len(key)), value)) 867 | 868 | def print_system_info(): 869 | print("If you do not understand why the above failures occurred,") 870 | print("copy and send the *entire* output (all info above and summary") 871 | print("below) to the instructor for help.") 872 | print() 873 | print('==================') 874 | print('System information') 875 | print('==================') 876 | _print_info('os.name', _os.name) 877 | _print_info('os.uname', _platform.uname()) 878 | _print_info('platform', _sys.platform) 879 | _print_info('platform+', _platform.platform()) 880 | for pversion in ( 881 | 'linux_distribution', 882 | 'mac_ver', 883 | 'win32_ver', 884 | ): 885 | value = getattr(_platform, pversion)() 886 | if value[0]: 887 | _print_info(pversion, value) 888 | _print_info('prefix', _sys.prefix) 889 | _print_info('exec_prefix', _sys.exec_prefix) 890 | _print_info('executable', _sys.executable) 891 | _print_info('version_info', _sys.version_info) 892 | _print_info('version', _sys.version) 893 | _print_info('environment', '') 894 | for key,value in sorted(_os.environ.items()): 895 | print(' {0}={1}'.format(key, value)) 896 | print('==================') 897 | 898 | def print_suggestions(instructor_fallback=True): 899 | print() 900 | print('For suggestions on installing missing packages, see') 901 | print('http://software-carpentry.org/setup/') 902 | print('') 903 | print('For instructings on installing a particular package,') 904 | print('see the failure message for that package printed above.') 905 | if instructor_fallback: 906 | print('') 907 | print('For help, email the *entire* output of this script to') 908 | print('your instructor.') 909 | 910 | 911 | if __name__ == '__main__': 912 | import optparse as _optparse 913 | 914 | parser = _optparse.OptionParser(usage='%prog [options] [check...]') 915 | epilog = __doc__ 916 | parser.format_epilog = lambda formatter: '\n' + epilog 917 | parser.add_option( 918 | '-v', '--verbose', action='store_true', 919 | help=('print additional information to help troubleshoot ' 920 | 'installation issues')) 921 | options,args = parser.parse_args() 922 | try: 923 | passed = check(args) 924 | except InvalidCheck as e: 925 | print("I don't know how to check for {0!r}".format(e.check)) 926 | print('I do know how to check for:') 927 | for key,checker in sorted(CHECKER.items()): 928 | if checker.long_name != checker.name: 929 | print(' {0} {1}({2})'.format( 930 | key, ' '*(20-len(key)), checker.long_name)) 931 | else: 932 | print(' {0}'.format(key)) 933 | _sys.exit(1) 934 | if not passed: 935 | if options.verbose: 936 | print() 937 | print_system_info() 938 | print_suggestions(instructor_fallback=True) 939 | _sys.exit(1) 940 | --------------------------------------------------------------------------------